diff --git a/.circleci/config.yml b/.circleci/config.yml index e62a582ab70fe..9012cf635442d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,9 @@ parameters: ai_contracts_test_dispatch: type: boolean default: false + kona_dispatch: + type: boolean + default: false github-event-type: type: string default: "__not_set__" @@ -372,7 +375,411 @@ commands: command: bash scripts/ops/use-latest-fallback.sh working_directory: packages/contracts-bedrock + # --- Rust environment setup commands --- + rust-install-toolchain: + description: "Install Rust toolchain via rustup" + parameters: + channel: + description: The Rust channel release to use (stable, nightly, etc.) + type: string + default: "stable" + components: + description: Components to install (space or comma separated) + type: string + default: "" + toolchain_version: + description: The specific toolchain version for stable channel + type: string + default: "1.92.0" + target: + description: A custom target architecture to add to the toolchain + type: string + default: "" + steps: + - run: + name: Install Rust toolchain (<< parameters.channel >>) + command: | + rustup default << parameters.toolchain_version >> + + if [ -n "<< parameters.components >>" ]; then + rustup component add << parameters.components >> + fi + if [ -n "<< parameters.target >>" ]; then + rustup target add << parameters.target >> + fi + + rust-setup-env: + description: "Fix rust mise environment variables" + steps: + - run: + name: Fix mise environment variables + command: | + echo "export CARGO_HOME=${MISE_CARGO_HOME}" >> "$BASH_ENV" + echo "export RUSTUP_HOME=${MISE_RUSTUP_HOME}" >> "$BASH_ENV" + echo "source ${MISE_CARGO_HOME}/env" >> "$BASH_ENV" + - run: + name: Configure Rust binary paths (sysgo) + command: | + ROOT_DIR="$(pwd)" + BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + echo "export RUST_BINARY_PATH_KONA_NODE=$ROOT_DIR/kona/target/release/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$ROOT_DIR/kona/target/release/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" + + rust-prepare: + parameters: + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)." + type: boolean + default: true + description: "Prepare the Rust environment for the build. Does not copy binaries to/from the cache." + steps: + - rust-setup-env + - when: + condition: << parameters.needs_clang >> + steps: + - run: + name: Install clang + command: | + export NEEDRESTART_MODE=a + sudo -E apt-get update --yes + sudo -E apt-get install -y --no-install-recommends clang llvm-dev libclang-dev + + rust-restore-build-cache: + description: "Restore the target directory cache for a Rust workspace" + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + version: + description: "Version of the cache" + type: string + default: "15" + profile: + description: "Profile to restore the cache for" + type: string + default: "debug" + features: + description: "Comma-separated list of features to restore the cache for" + type: string + default: "" + prefix: + description: "Prefix to add to the cache key" + type: string + default: "rust-build" + steps: + - restore_cache: + name: Restore << parameters.directory >> target cache + keys: + - << parameters.prefix >>-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }}-<< parameters.profile >>-<< parameters.features >> + - << parameters.prefix >>-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }}-<< parameters.profile >> + - << parameters.prefix >>-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }} + - restore_cache: + name: Restore << parameters.directory >> registry cache (mise) + keys: + - << parameters.prefix >>-registry-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }} + + rust-save-build-cache: + description: "Save the target directory cache for a Rust workspace" + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + version: + description: "Version of the cache" + type: string + default: "15" + profile: + description: "Profile to save the cache for" + type: string + default: "debug" + features: + description: "Comma-separated list of features to save the cache for" + type: string + default: "" + prefix: + description: "Prefix to add to the cache key" + type: string + default: "rust-build" + steps: + - save_cache: + key: << parameters.prefix >>-registry-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }} + name: Save << parameters.directory >> registry cache (mise) + paths: + - /data/mise-data/.cargo/registry + - /data/mise-data/.cargo/git + - /data/mise-data/.cargo/bin + - /data/mise-data/.rustup + - save_cache: + key: << parameters.prefix >>-v<< parameters.version >>-{{ checksum "<< parameters.directory >>/Cargo.lock" }}-<< parameters.profile >>-<< parameters.features >> + name: Save << parameters.directory >> target cache + paths: + - << parameters.directory >>/target/<< parameters.profile >> + + rust-prepare-and-restore-cache: + description: "Prepare the Rust environment and restore the cache" + parameters: + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: true + directory: + description: "Directory containing the Cargo workspace" + type: string + version: + description: "Version of the cache" + type: string + default: "15" + profile: + description: "Profile to restore the cache for" + type: string + default: "debug" + features: + description: "Comma-separated list of features to restore the cache for" + type: string + default: "" + prefix: + description: "Prefix to add to the cache key" + type: string + default: "rust-build" + steps: + - rust-prepare: + needs_clang: << parameters.needs_clang >> + - rust-restore-build-cache: + version: << parameters.version >> + directory: << parameters.directory >> + profile: << parameters.profile >> + features: << parameters.features >> + prefix: << parameters.prefix >> + + rust-build: + description: "Build a Rust workspace with target directory caching" + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: true + version: + description: "Version of the cache" + type: string + default: "15" + profile: + description: "Profile to build the binary with" + type: string + default: "debug" + features: + description: "Comma-separated list of features to build the binary with" + type: string + default: "default" + package: + description: "Package name to build. If not specified, the workspace will be built." + type: string + default: "" + binary: + description: "Binary name to build. If not specified, all targets will be built." + type: string + default: "" + save_cache: + description: "Whether to save the cache at the end of the build" + type: boolean + default: false + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + needs_clang: << parameters.needs_clang >> + version: << parameters.version >> + directory: << parameters.directory >> + profile: << parameters.profile >> + features: << parameters.features >> + - run: + name: Build << parameters.directory >> + command: | + PWD=$(pwd) + export CARGO_TARGET_DIR="$PWD/<< parameters.directory >>/target" + echo "CARGO_TARGET_DIR: $CARGO_TARGET_DIR" + export PROFILE="--profile << parameters.profile >>" + + # Debug profile is specified as "debug" in the config/target, but cargo build expects "dev" + if [ "<< parameters.profile >>" = "debug" ]; then + export PROFILE="--profile dev" + fi + + export PACKAGE="--workspace" + if [ -n "<< parameters.package >>" ]; then + export PACKAGE="--package << parameters.package >>" + fi + + export BINARY="--all-targets" + if [ -n "<< parameters.binary >>" ]; then + export BINARY="--bin << parameters.binary >>" + fi + + export FEATURES="--features << parameters.features >>" + if [ "<< parameters.features >>" = "all" ]; then + export FEATURES="--all-features" + fi + + cd << parameters.directory >> && cargo build $PROFILE $TARGET $PACKAGE $FEATURES + no_output_timeout: 30m + - when: + condition: << parameters.save_cache >> + steps: + - rust-save-build-cache: + directory: << parameters.directory >> + version: << parameters.version >> + profile: << parameters.profile >> + features: << parameters.features >> + jobs: + # Build a single Rust binary from a workspace. Will cache the binary by default. + rust-build-binary: + description: "Build a Rust workspace with target directory caching" + docker: + - image: <> + resource_class: xlarge + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: true + version: + description: "Version of the cache" + type: string + default: "15" + profile: + description: "Profile to build the binary with" + type: string + default: "debug" + features: + description: "Comma-separated list of features to build the binary with" + type: string + default: "default" + package: + description: "Package name to build. If not specified, the workspace will be built." + type: string + default: "" + binary: + description: "Binary name to build. If not specified, all targets will be built." + type: string + default: "" + save_cache: + description: "Whether to save the cache at the end of the build" + type: boolean + default: true + steps: + - rust-build: + directory: << parameters.directory >> + needs_clang: << parameters.needs_clang >> + version: << parameters.version >> + profile: << parameters.profile >> + features: << parameters.features >> + package: << parameters.package >> + binary: << parameters.binary >> + save_cache: << parameters.save_cache >> + + # Build a single Rust binary from a submodule. + rust-build-submodule: + docker: + - image: <> + resource_class: xlarge + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + binaries: + description: "Space-separated list of binary names to copy from target/release into the cache" + type: string + build_command: + description: "Cargo build command to run" + type: string + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: false + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - run: + name: Get << parameters.directory >> submodule HASH + command: | + mkdir -p .circleci-cache + # Verify the path is a git submodule (gitlink). For submodules, ls-tree prints: + # 160000 commit + MODE_AND_TYPE="$(git ls-tree HEAD << parameters.directory >> | awk '{print $1 " " $2}')" + if [ "$MODE_AND_TYPE" != "160000 commit" ]; then + echo "ERROR: << parameters.directory >> is not a submodule path (expected '160000 commit', got '$MODE_AND_TYPE')" >&2 + git ls-tree HEAD << parameters.directory >> >&2 || true + exit 1 + fi + + git ls-tree HEAD << parameters.directory >> | awk '{print $3}' > .circleci-cache/expected-submod-sha.txt + echo "Expected submodule HASH: $(cat .circleci-cache/expected-submod-sha.txt)" + echo "<< parameters.binaries >>" | tr ' ' '\n' | awk 'NF' | sort -u > .circleci-cache/expected-binaries.txt + echo "Expected binaries: $(tr '\n' ' ' < .circleci-cache/expected-binaries.txt)" + - restore_cache: + name: Restore << parameters.directory >> cache + keys: + - rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + - run: + name: Build << parameters.directory >> submodule (if needed) + command: | + ROOT_DIR="$(pwd)" + BIN_CACHE_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + + HIT=true + for bin in << parameters.binaries >>; do + if [ ! -f "$BIN_CACHE_DIR/$bin" ]; then + HIT=false + break + fi + done + if [ "$HIT" = "true" ]; then + echo "Cache hit - binaries exist" + exit 0 + fi + + echo "Cache miss - will build" + # Only run on cache miss. We intentionally do not cache into the submodule directory so that cache + # restore cannot make the submodule path non-empty (which breaks `git submodule update --init`). + git submodule update --init --recursive --depth 1 -j 8 --single-branch << parameters.directory >> + + if [ "<< parameters.needs_clang >>" = "true" ]; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang + fi + + cd << parameters.directory >> && << parameters.build_command >> + + mkdir -p "$BIN_CACHE_DIR" + for bin in << parameters.binaries >>; do + SRC="$ROOT_DIR/<< parameters.directory >>/target/release/$bin" + DST="$BIN_CACHE_DIR/$bin" + if [ ! -f "$SRC" ]; then + echo "ERROR: expected built binary not found at $SRC" >&2 + exit 1 + fi + cp "$SRC" "$DST" + chmod +x "$DST" || true + done + no_output_timeout: 30m + - save_cache: + key: rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + name: Save << parameters.directory >> cache + paths: + - ".circleci-cache/rust-binaries" + - persist_to_workspace: + root: . + paths: + - ".circleci-cache/rust-binaries" + # Kurtosis-based acceptance tests op-acceptance-tests-kurtosis: parameters: @@ -767,7 +1174,7 @@ jobs: fi # Let them cook! - KONA_VERSION=$(jq -r .version kona-proofs/version.json) \ + KONA_VERSION=$(jq -r .version kona/version.json) \ docker buildx bake \ --progress plain \ --builder=buildx-build \ @@ -1699,13 +2106,17 @@ jobs: enable-mise-cache: true - attach_workspace: at: . + # Build kona-node for the acceptance tests. This automatically gets kona from the cache. + - rust-build: + directory: kona + profile: "release" - run: name: Configure Rust binary paths (sysgo) command: | ROOT_DIR="$(pwd)" BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" - echo "export RUST_BINARY_PATH_KONA_NODE=$BIN_DIR/kona-node" >> "$BASH_ENV" - echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$BIN_DIR/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_NODE=$ROOT_DIR/kona/target/release/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$ROOT_DIR/kona/target/release/kona-supervisor" >> "$BASH_ENV" echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" # Restore cached Go modules @@ -2066,150 +2477,27 @@ jobs: enable-mise-cache: true - restore_cache: name: Restore kona cache - key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} + key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} - run: name: Build kona prestates command: just build-prestates - working_directory: kona-proofs + working_directory: kona - save_cache: - key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} + key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} name: Save Kona to cache paths: - - "kona-proofs/prestates/" + - "kona/prestates/" - persist_to_workspace: root: . paths: - - "kona-proofs/prestates/*" + - "kona/prestates/*" - cannon-kona-host: + + # Aggregator job - allows downstream jobs to depend on a single job instead of listing all build jobs. + rust-binaries-for-sysgo: docker: - image: <> - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true - - restore_cache: - name: Restore kona host cache - key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - - run: # The mise plugin for clang is broken so install via apt-get - name: Install clang - command: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends clang - - run: - name: Build kona host - command: just build-kona-host - working_directory: kona-proofs - - save_cache: - key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - name: Save Kona host to cache - paths: - - "kona-proofs/bin/kona-host" - - persist_to_workspace: - root: . - paths: - - "kona-proofs/bin/kona-host" - - # Build a single Rust binary for sysgo tests (parameterized). - rust-binary-build: - docker: - - image: <> - resource_class: xlarge - parameters: - directory: - description: "Directory containing the Cargo workspace" - type: string - binaries: - description: "Space-separated list of binary names to copy from target/release into the cache" - type: string - build_command: - description: "Cargo build command to run" - type: string - needs_clang: - description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" - type: boolean - default: false - steps: - - utils/checkout-with-mise: - checkout-method: blobless - - run: - name: Get << parameters.directory >> submodule HASH - command: | - mkdir -p .circleci-cache - # Verify the path is a git submodule (gitlink). For submodules, ls-tree prints: - # 160000 commit - MODE_AND_TYPE="$(git ls-tree HEAD << parameters.directory >> | awk '{print $1 " " $2}')" - if [ "$MODE_AND_TYPE" != "160000 commit" ]; then - echo "ERROR: << parameters.directory >> is not a submodule path (expected '160000 commit', got '$MODE_AND_TYPE')" >&2 - git ls-tree HEAD << parameters.directory >> >&2 || true - exit 1 - fi - - git ls-tree HEAD << parameters.directory >> | awk '{print $3}' > .circleci-cache/expected-submod-sha.txt - echo "Expected submodule HASH: $(cat .circleci-cache/expected-submod-sha.txt)" - echo "<< parameters.binaries >>" | tr ' ' '\n' | awk 'NF' | sort -u > .circleci-cache/expected-binaries.txt - echo "Expected binaries: $(tr '\n' ' ' < .circleci-cache/expected-binaries.txt)" - - restore_cache: - name: Restore << parameters.directory >> cache - keys: - - rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} - - run: - name: Build << parameters.directory >> submodule (if needed) - command: | - ROOT_DIR="$(pwd)" - BIN_CACHE_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" - - HIT=true - for bin in << parameters.binaries >>; do - if [ ! -f "$BIN_CACHE_DIR/$bin" ]; then - HIT=false - break - fi - done - if [ "$HIT" = "true" ]; then - echo "Cache hit - binaries exist" - exit 0 - fi - - echo "Cache miss - will build" - # Only run on cache miss. We intentionally do not cache into the submodule directory so that cache - # restore cannot make the submodule path non-empty (which breaks `git submodule update --init`). - git submodule update --init --recursive --depth 1 -j 8 --single-branch << parameters.directory >> - - if [ "<< parameters.needs_clang >>" = "true" ]; then - sudo apt-get update - sudo apt-get install -y --no-install-recommends clang - fi - - cd << parameters.directory >> && << parameters.build_command >> - - mkdir -p "$BIN_CACHE_DIR" - for bin in << parameters.binaries >>; do - SRC="$ROOT_DIR/<< parameters.directory >>/target/release/$bin" - DST="$BIN_CACHE_DIR/$bin" - if [ ! -f "$SRC" ]; then - echo "ERROR: expected built binary not found at $SRC" >&2 - exit 1 - fi - cp "$SRC" "$DST" - chmod +x "$DST" || true - done - no_output_timeout: 30m - - save_cache: - key: rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} - name: Save << parameters.directory >> cache - paths: - - ".circleci-cache/rust-binaries" - - persist_to_workspace: - root: . - paths: - - ".circleci-cache/rust-binaries" - - # Aggregator job - allows downstream jobs to depend on a single job instead of listing all build jobs. - rust-binaries-for-sysgo: - docker: - - image: <> - resource_class: small + resource_class: small steps: - run: name: All Rust binaries ready @@ -2506,146 +2794,782 @@ jobs: type: string docker: - image: <> - resource_class: large + resource_class: large + steps: + - setup_remote_docker: + docker_layer_caching: true + - gcp-cli/install + - gcp-oidc-authenticate: + gcp_cred_config_file_path: /tmp/gcp_cred_config.json + oidc_token_file_path: /tmp/oidc_token.json + - utils/checkout-with-mise: + enable-mise-cache: true + - attach_workspace: { at: "." } + - run: + name: Configure Docker + command: | + gcloud auth configure-docker us-docker.pkg.dev + - run: + name: Run goreleaser + command: | + goreleaser release --clean -f ./<>/<> + + diff-fetcher-forge-artifacts: + docker: + - image: <> + resource_class: medium + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - install-contracts-dependencies + - run: + name: Build contracts + command: | + just build-contracts + working_directory: op-fetcher + - run: + name: Compare forge artifacts + command: | + diff -qr "packages/contracts-bedrock/forge-artifacts/FetchChainInfo.s.sol" \ + "op-fetcher/pkg/fetcher/fetch/forge-artifacts/FetchChainInfo.s.sol" + + if [ $? -ne 0 ]; then + echo "ERROR: The checked-in forge artifacts for FetchChainInfo.s.sol do not match the ci build." + echo "Please run 'just build-contracts' in the op-fetcher directory and commit the changes." + exit 1 + fi + + echo "✅ Checked-in forge artifacts match the ci build" + + stale-check: + machine: + image: ubuntu-2204:2024.08.1 + steps: + - utils/github-stale: + stale-issue-message: "This issue has been automatically marked as stale and will be closed in 5 days if no updates" + stale-pr-message: "This pr has been automatically marked as stale and will be closed in 5 days if no updates" + close-issue-message: "This issue was closed as stale. Please reopen if this is a mistake" + close-pr-message: "This PR was closed as stale. Please reopen if this is a mistake" + days-before-issue-stale: 999 + days-before-pr-stale: 14 + days-before-issue-close: 5 + days-before-pr-close: 5 + + close-issue: + machine: + image: ubuntu-2204:2024.08.1 + parameters: + label_name: + type: string + message: + type: string + steps: + - github-cli/install + - utils/github-event-handler-setup: + github_event_base64: << pipeline.parameters.github-event-base64 >> + env_prefix: "github_" + - run: + name: Close issue if label is added + command: | + if [ ! -z "$github_pull_request_number" ] && [ "$github_label_name" = "$LABEL_NAME" ]; then + echo "Closing issue $github_pull_request_number as label $LABEL_NAME is added on repository ${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME} " + export GH_TOKEN=$GITHUB_TOKEN_GOVERNANCE + gh issue close --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" "$github_pull_request_number" --comment "$MESSAGE" + fi + environment: + MESSAGE: << parameters.message >> + LABEL_NAME: << parameters.label_name >> + + devnet-metrics-collect-authorship: + docker: + - image: <> + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - run: + name: Collect devnet metrics for op-acceptance-tests + command: | + ./devnet-sdk/scripts/metrics-collect-authorship.sh op-acceptance-tests/tests > .metrics--authorship--op-acceptance-tests + echo "Wrote file .metrics--authorship--op-acceptance-tests" + - gcp-cli/install + - gcp-oidc-authenticate: + gcp_cred_config_file_path: /tmp/gcp_cred_config.json + oidc_token_file_path: /tmp/oidc_token.json + - run: + name: Store artifact in Bucket + command: | + CURRENT_DATE=$(date '+%Y-%m-%d') + FOLDER_NAME="dt=$CURRENT_DATE" + + # Upload to the date-partitioned folder structure + gsutil cp .metrics--authorship--op-acceptance-tests gs://oplabs-tools-data-public-metrics/metrics-authorship/$FOLDER_NAME/metrics-$CIRCLE_SHA1.csv + + generate-flaky-report: + machine: true + resource_class: medium + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - run: + name: Generate flaky acceptance tests report + command: | + # Create reports directory + mkdir -p ./op-acceptance-tests/reports + + # Make the script executable + chmod +x ./op-acceptance-tests/scripts/generate-flaky-tests-report.sh + + # Run the script + ./op-acceptance-tests/scripts/generate-flaky-tests-report.sh \ + --branch "${CIRCLE_BRANCH:-develop}" \ + --org "${CIRCLE_PROJECT_USERNAME}" \ + --repo "${CIRCLE_PROJECT_REPONAME}" \ + --token "${CIRCLE_API_TOKEN}" \ + --output-dir "./op-acceptance-tests/reports" + + # Store the flaky test reports + - store_artifacts: + path: ./op-acceptance-tests/reports + destination: flaky-test-reports + + # ============================================================================ + # Kona Jobs (migrated from kona/.github/workflows) + # ============================================================================ + + # Kona Node E2E Sysgo Tests (from node_e2e_sysgo_tests.yaml) + kona-node-e2e-sysgo-tests: + parameters: + devnet_config: + description: The devnet configuration to test + type: string + reorg_tests: + description: Whether to run reorg tests + type: boolean + default: false + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - attach_workspace: + at: . + # Restore cached Go modules + - go-restore-cache: + namespace: kona-ci + - rust-build: + binary: "kona-node" + directory: kona + profile: release + - run: + name: Run common tests for node with sysgo orchestrator + no_output_timeout: 60m + command: | + WD=$(pwd) + echo "Running tests..." + export OP_RETH_EXEC_PATH="$WD/.circleci-cache/rust-binaries/op-reth" + export RUST_BINARY_PATH_KONA_NODE="$WD/kona/target/release/kona-node" + cd kona && just test-e2e-sysgo-run node node/common "<>" + # TODO(ethereum-optimism/optimism#18657): It seems this job breaks in monorepo's CCI. We need to investigate why and fix it. + - when: + condition: + equal: [true, <>] + steps: + - run: + name: Run reorg tests for node with sysgo orchestrator + no_output_timeout: 60m + command: | + WD=$(pwd) + echo "Running tests..." + export OP_RETH_EXEC_PATH="$WD/.circleci-cache/rust-binaries/op-reth" + export RUST_BINARY_PATH_KONA_NODE="$WD/kona/target/release/kona-node" + cd kona && just test-e2e-sysgo-run node node/reorgs "<>" + - go-save-cache: + namespace: kona-ci + # Kona Node Restart Tests (from node_e2e_sysgo_tests.yaml) + kona-node-restart-sysgo-tests: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - attach_workspace: + at: . + # Restore cached Go modules + - go-restore-cache: + namespace: kona-ci + - rust-build: + binary: "kona-node" + directory: kona + profile: release + - run: + name: Run restart tests for node with sysgo orchestrator + no_output_timeout: 60m + command: | + echo "Running tests..." + WD=$(pwd) + export RUST_BINARY_PATH_KONA_NODE="$WD/kona/target/release/kona-node" + cd kona && just test-e2e-sysgo node node/restart + - go-save-cache: + namespace: kona-ci + # Kona Proof Action Tests (from proof.yaml) + kona-proof-action-tests: + parameters: + kind: + description: The kind of action test (single or interop) + type: string + docker: + - image: <> + resource_class: xlarge + parallelism: 4 + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - attach_workspace: + at: . + # Restore cached Go modules + - go-restore-cache: + namespace: kona-ci + - rust-build: + directory: kona + profile: release + binary: "kona-host" + - run: + name: Build kona and run action tests + working_directory: kona + no_output_timeout: 90m + command: | + echo "Running action tests" + export KONA_HOST_PATH=$(pwd)/target/release/kona-host + just action-tests-single-run + - go-save-cache: + namespace: kona-ci + + # Kona Host Client Offline Runs (from proof.yaml) + kona-host-client-offline: + parameters: + target: + description: The target platform (native, asterisc, cannon) + type: string + block_number: + description: The block number to verify + type: string + default: "26215604" + machine: + image: <> + docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - install-zstd + - rust-prepare + - run: + name: Install cargo-llvm-cov + command: | + command -v cargo-llvm-cov >/dev/null || cargo install cargo-llvm-cov + - when: + condition: + equal: [asterisc, <>] + steps: + - run: + name: Clone and build asterisc + command: | + git clone https://github.com/ethereum-optimism/asterisc.git + cd asterisc && git checkout v1.3.0 && make build-rvgo + sudo mv ./rvgo/bin/asterisc /usr/local/bin/ + - when: + condition: + equal: [cannon, <>] + steps: + - run: + name: Clone and build cannon + command: | + git clone https://github.com/ethereum-optimism/optimism.git optimism-cannon + cd optimism-cannon/cannon && make + sudo mv ./bin/cannon /usr/local/bin/ + - run: + name: Set run environment + command: | + echo 'export BLOCK_NUMBER=<>' >> $BASH_ENV + echo 'export L2_CLAIM=0x7415d942f80a34f77d344e4bccb7050f14e593f5ea33669d27ea01dce273d72d' >> $BASH_ENV + echo 'export L2_OUTPUT_ROOT=0xaa34b62993bd888d7a2ad8541935374e39948576fce12aa8179a0aa5b5bc787b' >> $BASH_ENV + echo 'export L2_HEAD=0xf4adf5790bad1ffc9eee315dc163df9102473c5726a2743da27a8a10dc16b473' >> $BASH_ENV + echo 'export L1_HEAD=0x010cfdb22eaa13e8cdfbf66403f8de2a026475e96a6635d53c31f853a0e3ae25' >> $BASH_ENV + echo 'export L2_CHAIN_ID=11155420' >> $BASH_ENV + echo 'export WITNESS_TAR_NAME=holocene-op-sepolia-<>-witness.tar.zst' >> $BASH_ENV + - run: + name: Decompress witness data + working_directory: kona + command: | + tar --zstd -xvf ./bin/client/testdata/$WITNESS_TAR_NAME -C . + - run: + name: Run host + client offline + working_directory: kona/bin/client + no_output_timeout: 40m + command: | + source <(cargo llvm-cov show-env --export-prefix) + mkdir -p ../../target + just run-client-<>-offline \ + $BLOCK_NUMBER \ + $L2_CLAIM \ + $L2_OUTPUT_ROOT \ + $L2_HEAD \ + $L1_HEAD \ + $L2_CHAIN_ID + cargo llvm-cov report --lcov --output-path client_host_cov.lcov + + # Kona Rust CI - Tests (from rust_ci.yaml) + kona-cargo-tests: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + prefix: kona-tests + profile: "release" + - run: + name: Install nextest + command: | + command -v cargo-nextest >/dev/null || cargo install cargo-nextest + - run: + name: Run cargo tests + working_directory: kona + no_output_timeout: 40m + command: | + just test + - rust-save-build-cache: + directory: kona + prefix: kona-tests + profile: "release" + + # Kona Rust CI - Lint (from rust_ci.yaml) + kona-cargo-lint: + parameters: + target: + description: The lint target (native, cannon, asterisc) + type: string + machine: + image: <> + docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + features: "all" + - rust-install-toolchain: + components: rustfmt + - run: + name: Run fmt + lint for <> + working_directory: kona + no_output_timeout: 40m + command: | + just lint-<> + + kona-build-fpvm: + parameters: + target: + description: The build target (cannon-client, asterisc-client) + type: string + machine: + image: <> + docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare + - run: + name: Build <> + working_directory: kona + no_output_timeout: 40m + command: | + just build-<> + + # Kona Rust CI - Build Benchmarks (from rust_ci.yaml) + kona-cargo-build-benches: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + prefix: kona-benches + - run: + name: Build benchmarks + working_directory: kona + no_output_timeout: 40m + command: | + just benches + - rust-save-build-cache: + directory: kona + prefix: kona-benches + + # Kona Rust CI - Check unused dependencies (from rust_ci.yaml) + kona-cargo-udeps: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + prefix: kona-udeps + profile: "release" + - run: + name: Install cargo-udeps + command: | + command -v cargo-udeps >/dev/null || cargo install cargo-udeps + - run: + name: Check unused dependencies + working_directory: kona + no_output_timeout: 40m + command: | + just check-udeps + - rust-save-build-cache: + directory: kona + prefix: kona-udeps + profile: "release" + + # Kona Rust CI - Doc lint (from rust_ci.yaml) + kona-cargo-doc-lint: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + - run: + name: Lint documentation + working_directory: kona + no_output_timeout: 40m + command: | + just lint-docs + + # Kona Rust CI - Doc tests (from rust_ci.yaml) + kona-cargo-doc-test: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + prefix: kona-doc-test + - run: + name: Run doc tests + working_directory: kona + no_output_timeout: 40m + command: | + just test-docs + - rust-save-build-cache: + directory: kona + prefix: kona-doc-test + + # Kona Rust CI - Typos (from rust_ci.yaml) + kona-typos: + docker: + - image: <> + resource_class: medium + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare + - run: + name: Install typos-cli + command: | + command -v typos-cli >/dev/null || cargo install typos-cli + - run: + name: Check for typos + working_directory: kona + command: | + typos + + # Kona Rust CI - Cargo hack (from rust_ci.yaml) + kona-cargo-hack: + docker: + - image: <> + resource_class: xlarge + parallelism: 4 + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + features: "all" + - run: + name: Install cargo-hack + command: | + command -v cargo-hack >/dev/null || cargo install cargo-hack + - run: + name: Shard and run cargo hack + working_directory: kona + no_output_timeout: 40m + command: | + set -euo pipefail + + # 1) Produce the full list of concrete cargo commands (one per line) + cargo hack check \ + --feature-powerset --no-dev-deps \ + --workspace --locked \ + --print-command-list > /tmp/hack_cmds.txt + + # 2) Take only this node's slice + awk "NR % ${CIRCLE_NODE_TOTAL} == ${CIRCLE_NODE_INDEX}" /tmp/hack_cmds.txt \ + > /tmp/hack_cmds_shard.txt + + echo "Node ${CIRCLE_NODE_INDEX}/${CIRCLE_NODE_TOTAL} running $(wc -l < /tmp/hack_cmds_shard.txt) cases" + + # 3) Execute + while IFS= read -r cmd; do + echo ">>> $cmd" + bash -lc "$cmd" + done < /tmp/hack_cmds_shard.txt + + # Kona Rust CI - Check no_std (from rust_ci.yaml) + kona-check-no-std: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare + - rust-install-toolchain: + toolchain_version: 1.88.0 + target: riscv32imac-unknown-none-elf + - run: + name: Check no_std compatibility + working_directory: kona + no_output_timeout: 30m + command: | + just check-no-std + + # Kona Rust CI - Coverage (from rust_ci.yaml) + kona-coverage: + docker: + - image: <> + resource_class: xlarge + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare-and-restore-cache: + directory: kona + prefix: kona-coverage + version: "1" + - rust-install-toolchain: + components: llvm-tools-preview + - run: + name: Install cargo-llvm-cov and nextest + command: | + command -v cargo-llvm-cov >/dev/null || cargo install cargo-llvm-cov + command -v cargo-nextest >/dev/null || cargo install cargo-nextest + - run: + name: Generate lockfile if needed + working_directory: kona + command: | + [ -f Cargo.lock ] || cargo generate-lockfile + - run: + name: Run coverage + working_directory: kona + no_output_timeout: 40m + command: | + just llvm-cov-tests && mv ./target/nextest/ci/junit.xml ./junit.xml + - codecov/upload: + disable_search: true + files: kona/lcov.info + flags: unit + - store_test_results: + path: kona/junit.xml + - rust-save-build-cache: + directory: kona + prefix: kona-coverage + version: "1" + + + # Kona Rust CI - Deny (from rust_ci.yaml) + kona-cargo-deny: + docker: + - image: <> + resource_class: medium + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-restore-build-cache: + directory: kona + - run: + name: Install cargo-deny + command: | + command -v cargo-deny >/dev/null || cargo install cargo-deny + - run: + name: Run cargo deny + working_directory: kona + command: | + cargo deny check + + # Kona Rust CI - Zepter (from rust_ci.yaml) + kona-zepter: + docker: + - image: <> + resource_class: medium + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - rust-prepare + - run: + name: Install zepter + command: | + command -v zepter >/dev/null || cargo install zepter + zepter --version + - run: + name: Format and lint features + working_directory: kona + command: | + zepter format features + zepter + + # Kona Docs Build (from docs.yaml) + kona-docs-build: + docker: + - image: <> + resource_class: xlarge steps: - - setup_remote_docker: - docker_layer_caching: true - - gcp-cli/install - - gcp-oidc-authenticate: - gcp_cred_config_file_path: /tmp/gcp_cred_config.json - oidc_token_file_path: /tmp/oidc_token.json - utils/checkout-with-mise: - enable-mise-cache: true - - attach_workspace: { at: "." } + checkout-method: blobless + - rust-prepare - run: - name: Configure Docker + name: Install Node.js and Bun command: | - gcloud auth configure-docker us-docker.pkg.dev + curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - + sudo apt-get install -y nodejs + curl -fsSL https://bun.sh/install | bash + echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV + echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV + - run: + name: Install dependencies and Playwright browsers + working_directory: kona/docs + command: | + bun i + npx playwright install --with-deps chromium - run: - name: Run goreleaser + name: Build Vocs documentation + working_directory: kona/docs + no_output_timeout: 60m command: | - goreleaser release --clean -f ./<>/<> + bun run build + echo "Vocs Build Complete" + - store_artifacts: + path: kona/docs/docs/dist + destination: kona-docs - diff-fetcher-forge-artifacts: + # Kona Link Checker (from lychee.yaml) + kona-link-checker: docker: - image: <> resource_class: medium steps: - utils/checkout-with-mise: checkout-method: blobless - enable-mise-cache: true - - install-contracts-dependencies - run: - name: Build contracts + name: Install lychee command: | - just build-contracts - working_directory: op-fetcher + curl -sSfL https://github.com/lycheeverse/lychee/releases/download/v0.15.1/lychee-v0.15.1-x86_64-unknown-linux-gnu.tar.gz | tar xz + sudo mv lychee /usr/local/bin/ - run: - name: Compare forge artifacts + name: Check links + working_directory: kona command: | - diff -qr "packages/contracts-bedrock/forge-artifacts/FetchChainInfo.s.sol" \ - "op-fetcher/pkg/fetcher/fetch/forge-artifacts/FetchChainInfo.s.sol" - - if [ $? -ne 0 ]; then - echo "ERROR: The checked-in forge artifacts for FetchChainInfo.s.sol do not match the ci build." - echo "Please run 'just build-contracts' in the op-fetcher directory and commit the changes." - exit 1 - fi - - echo "✅ Checked-in forge artifacts match the ci build" - - stale-check: - machine: - image: ubuntu-2204:2024.08.1 - steps: - - utils/github-stale: - stale-issue-message: "This issue has been automatically marked as stale and will be closed in 5 days if no updates" - stale-pr-message: "This pr has been automatically marked as stale and will be closed in 5 days if no updates" - close-issue-message: "This issue was closed as stale. Please reopen if this is a mistake" - close-pr-message: "This PR was closed as stale. Please reopen if this is a mistake" - days-before-issue-stale: 999 - days-before-pr-stale: 14 - days-before-issue-close: 5 - days-before-pr-close: 5 + lychee --config ./lychee.toml --cache-exclude-status 429 '**/README.md' './docs/**/*.md' './docs/**/*.mdx' './docs/**/*.html' './docs/**/*.json' || true + # Note: Not failing the build on broken links, just reporting - close-issue: - machine: - image: ubuntu-2204:2024.08.1 + # Kona Supervisor E2E Tests (from supervisor_e2e_sysgo.yaml) + kona-supervisor-e2e-tests: parameters: - label_name: - type: string - message: + test_pkg: + description: The test package to run type: string + docker: + - image: <> + resource_class: xlarge steps: - - github-cli/install - - utils/github-event-handler-setup: - github_event_base64: << pipeline.parameters.github-event-base64 >> - env_prefix: "github_" + - utils/checkout-with-mise: + checkout-method: blobless + - rust-build: + binary: "kona-supervisor" + directory: kona + profile: release - run: - name: Close issue if label is added + name: Run supervisor tests for <> + working_directory: kona + no_output_timeout: 40m command: | - if [ ! -z "$github_pull_request_number" ] && [ "$github_label_name" = "$LABEL_NAME" ]; then - echo "Closing issue $github_pull_request_number as label $LABEL_NAME is added on repository ${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME} " - export GH_TOKEN=$GITHUB_TOKEN_GOVERNANCE - gh issue close --repo "${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" "$github_pull_request_number" --comment "$MESSAGE" - fi - environment: - MESSAGE: << parameters.message >> - LABEL_NAME: << parameters.label_name >> + just test-e2e-sysgo supervisor "/supervisor/<>" - devnet-metrics-collect-authorship: + # Kona Publish Prestate Artifacts (from publish_artifacts.yaml) + kona-publish-prestate-artifacts: + parameters: + kind: + description: The kind of prestate (cannon) + type: string + default: "cannon" + version: + description: The version to build (kona-client, kona-client-int) + type: string docker: - image: <> + resource_class: xlarge steps: - utils/checkout-with-mise: checkout-method: blobless - enable-mise-cache: true - - run: - name: Collect devnet metrics for op-acceptance-tests - command: | - ./devnet-sdk/scripts/metrics-collect-authorship.sh op-acceptance-tests/tests > .metrics--authorship--op-acceptance-tests - echo "Wrote file .metrics--authorship--op-acceptance-tests" + - rust-install-toolchain: + components: llvm-tools-preview + - rust-prepare-and-restore-cache: + directory: kona + binaries: kona-client + needs_clang: true - gcp-cli/install - gcp-oidc-authenticate: gcp_cred_config_file_path: /tmp/gcp_cred_config.json oidc_token_file_path: /tmp/oidc_token.json - run: - name: Store artifact in Bucket + name: Generate prestate artifacts + working_directory: kona + no_output_timeout: 60m command: | - CURRENT_DATE=$(date '+%Y-%m-%d') - FOLDER_NAME="dt=$CURRENT_DATE" - - # Upload to the date-partitioned folder structure - gsutil cp .metrics--authorship--op-acceptance-tests gs://oplabs-tools-data-public-metrics/metrics-authorship/$FOLDER_NAME/metrics-$CIRCLE_SHA1.csv - - generate-flaky-report: - machine: true - resource_class: medium - steps: - - utils/checkout-with-mise: - checkout-method: blobless - enable-mise-cache: true + CANNON_TAG="$(cat .config/cannon_tag)" + cd docker/fpvm-prestates + just "<>" "<>" "<< pipeline.git.branch >>" "$CANNON_TAG" - run: - name: Generate flaky acceptance tests report + name: Upload prestates to GCS command: | - # Create reports directory - mkdir -p ./op-acceptance-tests/reports - - # Make the script executable - chmod +x ./op-acceptance-tests/scripts/generate-flaky-tests-report.sh - - # Run the script - ./op-acceptance-tests/scripts/generate-flaky-tests-report.sh \ - --branch "${CIRCLE_BRANCH:-develop}" \ - --org "${CIRCLE_PROJECT_USERNAME}" \ - --repo "${CIRCLE_PROJECT_REPONAME}" \ - --token "${CIRCLE_API_TOKEN}" \ - --output-dir "./op-acceptance-tests/reports" - - # Store the flaky test reports - - store_artifacts: - path: ./op-acceptance-tests/reports - destination: flaky-test-reports + PRESTATE_HASH=$(jq -r .pre ./prestate-artifacts-<>/prestate-proof.json) + BRANCH_NAME=$(echo "<< pipeline.git.branch >>" | tr '/' '-') + echo "Publishing ${PRESTATE_HASH} as ${BRANCH_NAME}" + if [ -n "<< pipeline.git.branch >>" ]; then + echo "Publishing commit hash data" + INFO_FILE=$(mktemp) + (echo "Commit=<< pipeline.git.revision >>" && echo "Prestate: ${PRESTATE_HASH}") > "${INFO_FILE}" + gsutil cp "${INFO_FILE}" "gs://kona-proof-prestates/<>/${BRANCH_NAME}-<>-prestate.bin.gz.txt" + rm "${INFO_FILE}" + PRESTATE_HASH="${BRANCH_NAME}-<>" + fi + gsutil cp ./prestate-artifacts-<>/prestate.bin.gz "gs://kona-proof-prestates/<>/${PRESTATE_HASH}.bin.gz" + echo "Successfully published prestates artifacts to GCS" workflows: # Nightly Kurtosis acceptance tests @@ -3325,15 +4249,15 @@ workflows: - cannon-prestate-quick: context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: - name: rust-build-kona + - rust-build-binary: + name: kona-build-release directory: kona - binaries: "kona-node kona-supervisor" build_command: cargo build --release --bin kona-node --bin kona-supervisor needs_clang: true + profile: "release" context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: + - rust-build-submodule: name: rust-build-op-rbuilder directory: op-rbuilder binaries: "op-rbuilder" @@ -3341,7 +4265,7 @@ workflows: needs_clang: true context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: + - rust-build-submodule: name: rust-build-rollup-boost directory: rollup-boost binaries: "rollup-boost" @@ -3350,7 +4274,7 @@ workflows: - circleci-repo-readonly-authenticated-github-token - rust-binaries-for-sysgo: requires: - - rust-build-kona + - kona-build-release - rust-build-op-rbuilder - rust-build-rollup-boost - op-acceptance-tests-flake-shake: @@ -3462,18 +4386,23 @@ workflows: - cannon-kona-prestate: # needed for sysgo tests (if any package is in-memory) context: - circleci-repo-readonly-authenticated-github-token - - cannon-kona-host: # needed for sysgo tests (if any package is in-memory) + - rust-build-binary: + name: cannon-kona-host + directory: kona + profile: "release" + binary: "kona-host" + save_cache: true context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: - name: rust-build-kona + - rust-build-binary: + name: kona-build-release directory: kona - binaries: "kona-node kona-supervisor" - build_command: cargo build --release --bin kona-node --bin kona-supervisor - needs_clang: true + profile: "release" + features: "default" + save_cache: true context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: + - rust-build-submodule: name: rust-build-op-rbuilder directory: op-rbuilder binaries: "op-rbuilder" @@ -3481,7 +4410,7 @@ workflows: needs_clang: true context: - circleci-repo-readonly-authenticated-github-token - - rust-binary-build: + - rust-build-submodule: name: rust-build-rollup-boost directory: rollup-boost binaries: "rollup-boost" @@ -3490,7 +4419,7 @@ workflows: - circleci-repo-readonly-authenticated-github-token - rust-binaries-for-sysgo: requires: - - rust-build-kona + - kona-build-release - rust-build-op-rbuilder - rust-build-rollup-boost # IN-MEMORY (all) @@ -3556,3 +4485,233 @@ workflows: - circleci-api-token - devin-api - slack + + # ============================================================================ + # Kona Workflows (migrated from kona/.github/workflows) + # ============================================================================ + + # Kona main CI workflow (runs on push to main, merge_group, pull_request) + kona-ci: + when: + or: + - equal: ["webhook", << pipeline.trigger_source >>] + - and: + - equal: [true, <>] + - equal: ["api", << pipeline.trigger_source >>] + jobs: + - contracts-bedrock-build: # needed for sysgo tests + build_args: --skip test + context: + - circleci-repo-readonly-authenticated-github-token + - cannon-prestate-quick: # needed for sysgo tests + context: + - circleci-repo-readonly-authenticated-github-token + - cannon-kona-prestate: # needed for sysgo tests (if any package is in-memory) + context: + - circleci-repo-readonly-authenticated-github-token + - rust-build-binary: + name: cannon-kona-host + directory: kona + profile: "release" + binary: "kona-host" + save_cache: true + context: + - circleci-repo-readonly-authenticated-github-token + # Rust CI jobs (from rust_ci.yaml) + - rust-build-binary: + name: kona-build-release + directory: kona + profile: "release" + features: "default" + save_cache: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-build-binary: + name: kona-build-debug + directory: kona + profile: "debug" + features: "default" + save_cache: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-build-binary: + name: kona-build-all-features + directory: kona + profile: "debug" + features: "all" + save_cache: true + context: + - circleci-repo-readonly-authenticated-github-token + - kona-build-fpvm: + name: kona-build-fpvm-<> + matrix: + parameters: + target: ["cannon-client", "asterisc-client"] + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-tests: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-lint: + name: kona-lint-<> + matrix: + parameters: + target: ["native", "cannon", "asterisc"] + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-build-benches: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-udeps: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-doc-lint: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-doc-test: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-typos: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-cargo-deny: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-zepter: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-check-no-std: + context: + - circleci-repo-readonly-authenticated-github-token + - kona-coverage: + context: + - circleci-repo-readonly-authenticated-github-token + requires: + - kona-cargo-tests + - kona-cargo-hack: + context: + - circleci-repo-readonly-authenticated-github-token + requires: + - kona-build-release + - kona-build-debug + - kona-cargo-udeps + - kona-build-all-features + + - rust-build-submodule: + name: op-reth-build + directory: reth + binaries: op-reth + build_command: cd crates/optimism/bin && cargo build --release --bin op-reth + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + + # Node E2E tests (from node_e2e_sysgo_tests.yaml) + - kona-node-e2e-sysgo-tests: + name: kona-node-e2e-<> + matrix: + parameters: + devnet_config: ["simple-kona", "simple-kona-geth", "simple-kona-sequencer", "large-kona-sequencer"] + context: + - circleci-repo-readonly-authenticated-github-token + requires: + - contracts-bedrock-build + - cannon-prestate-quick + - cannon-kona-prestate + - cannon-kona-host + - kona-build-release + - op-reth-build + - kona-node-restart-sysgo-tests: + name: kona-node-e2e-restart + context: + - circleci-repo-readonly-authenticated-github-token + requires: + - contracts-bedrock-build + - cannon-prestate-quick + - cannon-kona-prestate + - cannon-kona-host + - kona-build-release + + # Proof tests (from proof.yaml) - single kind only, interop excluded per original config + - kona-proof-action-tests: + name: kona-proof-action-single + kind: single + requires: + - kona-build-release + - contracts-bedrock-build + context: + - circleci-repo-readonly-authenticated-github-token + - kona-host-client-offline: + name: kona-host-client-native + target: native + requires: + - kona-build-release + context: + - circleci-repo-readonly-authenticated-github-token + - kona-host-client-offline: + name: kona-host-client-asterisc + target: asterisc + requires: + - kona-build-release + context: + - circleci-repo-readonly-authenticated-github-token + - kona-host-client-offline: + name: kona-host-client-cannon + target: cannon + requires: + - kona-build-release + context: + - circleci-repo-readonly-authenticated-github-token + + # Docs build (from docs.yaml) + - kona-docs-build: + context: + - circleci-repo-readonly-authenticated-github-token + + # Kona scheduled workflows + scheduled-kona-link-checker: + when: + equal: [build_weekly, <>] + jobs: + - kona-link-checker: + context: + - circleci-repo-readonly-authenticated-github-token + + scheduled-kona-sync: + when: + equal: [build_weekly, <>] + jobs: + - kona-update-monorepo: + context: + - circleci-repo-readonly-authenticated-github-token + + # Kona supervisor E2E tests (from supervisor_e2e_sysgo.yaml) - manual dispatch only + kona-supervisor-e2e: + when: + and: + - equal: [true, <>] + - equal: ["api", << pipeline.trigger_source >>] + jobs: + - kona-supervisor-e2e-tests: + name: kona-supervisor-<> + matrix: + parameters: + test_pkg: ["pre_interop", "l1reorg/sysgo"] + context: + - circleci-repo-readonly-authenticated-github-token + + # Kona publish prestate artifacts (from publish_artifacts.yaml) - on push to main or tags + kona-publish-prestates: + when: + or: + - equal: ["develop", <>] + - equal: ["main", <>] + jobs: + - kona-publish-prestate-artifacts: + name: kona-publish-<> + matrix: + parameters: + version: ["kona-client", "kona-client-int"] + context: + - circleci-repo-readonly-authenticated-github-token + - oplabs-gcr diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c5d44c438454..dd8aef60cb03e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,6 +30,9 @@ /op-e2e/actions/proofs @ethereum-optimism/proofs @ethereum-optimism/go-reviewers /op-e2e/faultproofs @ethereum-optimism/proofs @ethereum-optimism/go-reviewers +# Kona +/kona @ethereum-optimism/kona-reviewers + # Ops /.cursor/rules/solidity-styles.mdc @ethereum-optimism/contract-reviewers diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5a416c9a7bc51..6be64a1e3dbc5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,20 @@ updates: prefix: "dependabot(gomod): " labels: - "M-dependabot" + + # Kona - Rust/Cargo dependencies + - package-ecosystem: "cargo" + directory: "/kona" + schedule: + interval: "daily" + commit-message: + prefix: "dependabot(cargo): " + labels: + - "M-dependabot" + - "F-deps" + ignore: + - dependency-name: "*" + # patch and minor updates don't matter for libraries + update-types: + - "version-update:semver-patch" + - "version-update:semver-minor" diff --git a/.gitmodules b/.gitmodules index 1ca46f672e305..2f49f00788548 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,12 +32,15 @@ [submodule "packages/contracts-bedrock/lib/superchain-registry"] path = packages/contracts-bedrock/lib/superchain-registry url = https://github.com/ethereum-optimism/superchain-registry +[submodule "kona/crates/protocol/registry/superchain-registry"] + path = kona/crates/protocol/registry/superchain-registry + url = https://github.com/ethereum-optimism/superchain-registry.git +[submodule "reth"] + path = reth + url = https://github.com/paradigmxyz/reth [submodule "op-rbuilder"] path = op-rbuilder url = https://github.com/flashbots/op-rbuilder [submodule "rollup-boost"] path = rollup-boost url = https://github.com/flashbots/rollup-boost -[submodule "kona"] - path = kona - url = https://github.com/op-rs/kona diff --git a/Makefile b/Makefile index a0ada6a77d8a9..0ce5766f9eeae 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ golang-docker: ## Builds Docker images for Go components using buildx GIT_COMMIT=$$(git rev-parse HEAD) \ GIT_DATE=$$(git show -s --format='%ct') \ IMAGE_TAGS=$$(git rev-parse HEAD),latest \ - KONA_VERSION=$$(jq -r .version kona-proofs/version.json) \ + KONA_VERSION=$$(jq -r .version kona/version.json) \ docker buildx bake \ --progress plain \ --load \ @@ -333,6 +333,10 @@ go-tests-ci: ## Runs comprehensive Go tests with gotestsum for CI (assumes deps $(MAKE) _go-tests-ci-internal GO_TEST_FLAGS="" .PHONY: go-tests-ci +go-tests-ci-kona-action: ## Runs action tests for kona with gotestsum for CI (assumes deps built by CI) + $(MAKE) _go-tests-ci-internal GO_TEST_FLAGS="-count=1 -timeout 60m -run Test_ProgramAction" +.PHONY: go-tests-ci-kona-action + go-tests-fraud-proofs-ci: ## Runs fraud proofs Go tests with gotestsum for CI (assumes deps built by CI) @echo "Setting up test directories..." mkdir -p ./tmp/test-results ./tmp/testlogs diff --git a/codecov.yml b/codecov.yml index 36d186e2c984c..3c7abb33a9d22 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,6 +7,9 @@ comment: require_changes: true flags: - contracts-bedrock-tests + - unit + - e2e + - proof ignore: - "op-e2e" @@ -15,12 +18,28 @@ ignore: - "packages/contracts-bedrock/scripts/**/*.sol" - "packages/contracts-bedrock/src/vendor/**/*.sol" - "packages/contracts-bedrock/src/interfaces/**/*.sol" + # Kona ignores + - "kona/examples/**/*" + - "kona/**/test_util*" + - "kona/**/noop.rs" coverage: + range: 80..100 + round: down + precision: 1 status: project: default: informational: true + # Kona unit tests + unit: + threshold: 1% + # Kona E2E tests + e2e: + threshold: 1% + # Kona proof/action tests + proof: + threshold: 1% patch: contracts: base: auto @@ -29,9 +48,47 @@ coverage: informational: false flags: - contracts-bedrock-tests + # Kona patch coverage + unit: + target: auto + threshold: 1% + base: auto + only_pulls: true + e2e: + target: auto + threshold: 1% + base: auto + only_pulls: true + proof: + target: auto + threshold: 1% + base: auto + only_pulls: true flag_management: individual_flags: - name: contracts-bedrock-tests paths: - packages/contracts-bedrock/src + # Kona flags + - name: unit + paths: + - kona + statuses: + - type: project + target: auto + threshold: 1% + - name: e2e + paths: + - kona + statuses: + - type: project + target: auto + threshold: 1% + - name: proof + paths: + - kona + statuses: + - type: project + target: auto + threshold: 1% diff --git a/kona b/kona deleted file mode 160000 index be9d6734effed..0000000000000 --- a/kona +++ /dev/null @@ -1 +0,0 @@ -Subproject commit be9d6734effed58a906577b5198201f8c4cd3b4f diff --git a/kona-proofs/.gitignore b/kona-proofs/.gitignore deleted file mode 100644 index 7e033e559d79b..0000000000000 --- a/kona-proofs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -prestates -bin -build diff --git a/kona-proofs/justfile b/kona-proofs/justfile deleted file mode 100644 index 02e0c79484dcd..0000000000000 --- a/kona-proofs/justfile +++ /dev/null @@ -1,88 +0,0 @@ -KONA_DIR := `realpath .` -KONA_VERSION := `jq -r .version ./version.json` -KONA_PRESTATE_HASH := `jq -r .prestateHash ./version.json` -KONA_INTEROP_PRESTATE_HASH := `jq -r .interopPrestateHash ./version.json` - -default: build-all - -build-all: build-prestates build-kona-host - -build-prestates: build-cannon-prestate build-interop-prestate - -build-cannon-prestate: - @just build-prestate kona-client {{KONA_PRESTATE_HASH}} - -build-interop-prestate: - @just build-prestate kona-client-int {{KONA_INTEROP_PRESTATE_HASH}} - -build-prestate VARIANT HASH: - #!/usr/bin/env bash - set -euo pipefail - - cd "{{KONA_DIR}}" - # Check if required prestate already exists - if [[ -f "prestates/{{HASH}}.bin.gz" ]]; then - echo "Prestate {{HASH}} for variant {{VARIANT}} already exists" - exit - fi - echo "Building prestate..." - just checkout-kona - cd "{{KONA_DIR}}/build/kona" - cd docker/fpvm-prestates - # Delete any existing artifacts (they're in .gitignore so reset --hard won't delete them) - rm -rf ../../prestate-artifacts-cannon - echo just cannon {{VARIANT}} "kona-client/v{{KONA_VERSION}}" $(cat ../../.config/cannon_tag) - just cannon {{VARIANT}} "kona-client/v{{KONA_VERSION}}" $(cat ../../.config/cannon_tag) - - # Check the prestate hash matches what we expect - ACTUAL_HASH=$(jq -r .pre ../../prestate-artifacts-cannon/prestate-proof.json) - if [[ "${ACTUAL_HASH}" != "{{HASH}}" ]]; then - echo "Incorrect prestate hash, expected {{HASH}} but was ${ACTUAL_HASH}" - exit 1 - fi - - mkdir -p "{{KONA_DIR}}/prestates" - cp ../../prestate-artifacts-cannon/prestate.bin.gz "{{KONA_DIR}}/prestates/{{HASH}}.bin.gz" - -build-kona-host: - #!/usr/bin/env bash - set -euo pipefail - - # Check if kona-host has already been built - # This is a simplistic check that relies on CircleCI's cache being keyed on this file - # which contains the kona version we're checking out. Locally you may need to run - # just clean to force a rebuild if the kona version has changed. - if [[ -f "bin/kona-host" ]]; then - echo "kona-host already built. Assuming it is built from kona-client/v{{KONA_VERSION}}" - exit - fi - - echo "Building kona-host" - just checkout-kona - cd "{{KONA_DIR}}/build/kona" - just build-native --bin kona-host - - mkdir -p "{{KONA_DIR}}/bin" - cp "{{KONA_DIR}}/build/kona/target/debug/kona-host" "{{KONA_DIR}}/bin/kona-host" - -checkout-kona: - #!/usr/bin/env bash - set -euo pipefail - - DIR="{{KONA_DIR}}/build" - mkdir -p "${DIR}" - cd "${DIR}" - if [[ -d kona ]]; then - cd kona - git fetch origin - git checkout -f "kona-client/v{{KONA_VERSION}}" - git reset --hard HEAD - else - git clone -b "kona-client/v{{KONA_VERSION}}" https://github.com/op-rs/kona kona - cd kona - fi - -clean: - #!/usr/bin/env bash - set -euo pipefail - rm -rf "{{KONA_DIR}}/build" "{{KONA_DIR}}/prestates" "{{KONA_DIR}}/bin" diff --git a/kona/.config/cannon_tag b/kona/.config/cannon_tag new file mode 100644 index 0000000000000..d59e58dba3c92 --- /dev/null +++ b/kona/.config/cannon_tag @@ -0,0 +1 @@ +b9b1429b3c342a5a8c81cf4dc1d420b767474649 \ No newline at end of file diff --git a/kona/.config/changelog.sh b/kona/.config/changelog.sh new file mode 100755 index 0000000000000..1aa5796c2c9be --- /dev/null +++ b/kona/.config/changelog.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e -o pipefail + +root=$(dirname "$(dirname "$0")") +cmd=(git cliff --workdir "$root" --output "$root/CHANGELOG.md" "$@") + +if [ "$DRY_RUN" = "true" ]; then + echo "skipping due to dry run: ${cmd[*]}" >&2 + exit 0 +else + "${cmd[@]}" +fi \ No newline at end of file diff --git a/kona/.config/nextest.toml b/kona/.config/nextest.toml new file mode 100644 index 0000000000000..76fd74b5d7fc5 --- /dev/null +++ b/kona/.config/nextest.toml @@ -0,0 +1,2 @@ +[profile.ci.junit] +path = "junit.xml" diff --git a/kona/.config/zepter.yaml b/kona/.config/zepter.yaml new file mode 100644 index 0000000000000..b90927329e360 --- /dev/null +++ b/kona/.config/zepter.yaml @@ -0,0 +1,42 @@ +version: + format: 1 + # Minimum zepter version that is expected to work. This is just for printing a nice error + # message when someone tries to use an older version. + binary: 0.13.2 + +# The examples in the following comments assume crate `A` to have a dependency on crate `B`. +workflows: + check: + - [ + "lint", + # Check that `A` activates the features of `B`. + "propagate-feature", + # These are the features to check: + "--features=arbitrary,std,serde,test-utils,metrics", + # Do not try to add a new section into `[features]` of `A` only because `B` exposes that feature. There are edge-cases where this is still needed, but we can add them manually. + "--left-side-feature-missing=ignore", + # Ignore the case that `A` is outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on. + "--left-side-outside-workspace=ignore", + # Only check normal dependencies. + # Propagating to dev-dependencies leads to compilation issues. + "--dep-kinds=normal:check,dev:ignore", + # Ignores `miniz_oxide/serde`, since its `serde` feature implies `std` + # TODO: Remove once a release has been cut with https://github.com/Frommi/miniz_oxide/pull/178 + "--ignore-missing-propagate=kona-protocol/serde:miniz_oxide/serde", + "--show-path", + "--quiet", + ] + default: + # Running `zepter` with no subcommand will check & fix. + - [$check.0, "--fix"] + +# Will be displayed when any workflow fails: +help: + text: | + Kona uses the Zepter CLI to detect abnormalities in Cargo features, e.g. missing propagation. + + It looks like one more checks failed; please check the console output. + + You can try to automatically address them by installing zepter (`cargo install zepter --locked`) and simply running `zepter` in the workspace root. + links: + - "https://github.com/ggwpez/zepter" diff --git a/kona/.dockerignore b/kona/.dockerignore new file mode 100644 index 0000000000000..98a662e11a9c2 --- /dev/null +++ b/kona/.dockerignore @@ -0,0 +1,7 @@ +target/ +book/ +assets/ +monorepo/ +.config/ +.github/ +tests/ diff --git a/kona/.gitignore b/kona/.gitignore new file mode 100644 index 0000000000000..837bd7b298342 --- /dev/null +++ b/kona/.gitignore @@ -0,0 +1,50 @@ +# MacOS dust +.DS_Store + +# Rust target +target/ + +# Ignore VSCode Configuration +.vscode/ + +# Monorepo workspace +monorepo/ + +# Release artifacts +dist/ + +# vocs node_modules +docs/node_modules + +# `optimism-package` workspace +optimism-package/ + +# Environment Variables +.env + +# kona-host data-dir +data/ + +# Ignore jwt hex tokens +jwt.hex + +# FPVM artifacts +state.bin.gz +meta.json +out.json + +# Prestate artifacts +prestate-artifacts-* + +# IDE specific +.idea + +# rustc bug report +rustc-ice-* + +# logfiles +*.log + +bin +build +prestates \ No newline at end of file diff --git a/kona/CHANGELOG.md b/kona/CHANGELOG.md new file mode 100644 index 0000000000000..75bb0daaf26b6 --- /dev/null +++ b/kona/CHANGELOG.md @@ -0,0 +1,2218 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.3] - 2025-07-31 + +### 🚀 Features + +- *(supervisor:core)* Cyclic dependency checker on block validation (#2597) + +### ⚙️ Miscellaneous Tasks + +- *(bin)* Remove unused discovery node subcommand (#2609) +- *(providers)* Metrics with Claude (#2610) +- *(grafana)* Update Dashboard (#2612) +- Release 0.3.3 + +### Release + +- *(kona-node)* 1.0.0-rc.1 +- *(kona-node)* 1.0.0-rc.1 + +## [kona-node/v0.1.2] - 2025-07-31 + +### 🚀 Features + +- *(node/service)* Expose standalone `healthz` endpoint (#2603) + +### ⚙️ Miscellaneous Tasks + +- Fix a large number of spelling issues in comments (#2592) + +### Patch + +- *(node/conductor)* Patch conductor bootstrap (#2589) + +### Release + +- *(kona-node)* 0.1.2 (#2608) + +## [kona-node/v0.1.1-beta.1] - 2025-07-30 + +### 🚀 Features + +- *(node/service)* Support sequencer recovery mode (#2460) +- *(supervisor/core)* [Safety Checker] validate interop timestamps (#2537) +- *(supervisor/core)* Integrate unsafe block reorg (#2539) +- *(node/service)* Delay sequencer's view of L1 chain (#2568) +- *(supervisor/core)* Handle invalidate blocks (#2564) +- *(supervisor/core)* [Safety Checker] executing message validation (#2570) +- *(protocol)* Add `Jovian` fork definition (#2584) +- *(node/sequencer)* Implement remote signer skeleton (#2572) +- *(node/sequencer)* Integrate remote signer in CLI (#2587) +- Introduce CLAUDE.md file (#2596) + +### 🐛 Bug Fixes + +- *(node/engine)* Audit engine for bugs. rename forkchoice task -> synchronize task. move block building logic to build task (#2524) +- *(supervisor/core)* Message checksum validation (#2586) + +### 🧪 Testing + +- *(node/engine)* Add positive engine test (#2552) + +### ⚙️ Miscellaneous Tasks + +- Fix broken url (#2557) +- *(bin/node)* Improve error pattern matching when pre-validating jwt (#2367) +- *(supervisor/storage)* Define `EntryNotFoundError` (#2542) +- *(docs)* Remove `RuntimeActor` mention (#2567) +- *(protocol)* Optional `OpAttributesWithParent::derived_from` field (#2569) +- *(docs)* Replaced the non-working link driver (#2576) +- *(supervisor)* Add supervisor path to log targets (#2565) +- *(supervisor)* Refactor interop validation (#2578) +- *(supervisor)* Refactor chain processor (#2590) +- *(workspace)* Add `theochap` as author (#2599) + +### Patch + +- *(node)* Fix macos builds (#2600) + +## [kona-node/v0.1.1] - 2025-07-24 + +### 🚀 Features + +- *(book)* Node Documentation (#2304) +- *(node/net)* Rewire unsafe block sender from engine to network (#2378) +- *(node/service)* Add L1 origin consistency check (#2373) +- *(supervisor)* Pre-interop db support (#2368) +- *(supervisor/core)* Implemented task retrier (#2386) +- *(node/engine)* Refactor engine tasks. fix sequencer startup logic (#2388) +- *(node/sequencer)* Connect unsafe head watcher from sequencer to engine (#2406) +- *(node/service)* `L1OriginSelector` advancement (#2404) +- *(supervisor/core)* Preinterop node support (#2385) +- *(node/service)* Reset engine prior to block building (#2415) +- *(node/service)* Prevent second reset on startup when sequencing (#2423) +- *(tests)* Simple `kona-node` sequencer profile (#2424) +- *(node/engine)* Fix unsafe payload signature. add large kona sequencer config (#2425) +- *(docs)* Kona Documentation with Vocs (#2398) +- *(node/sequencer)* Groundwork for op-conductor (#2405) +- *(node/service)* Wire in sequencer configuration (#2447) +- *(kona/logs)* Add logging format options (#2457) +- *(supervisor/storage)* Implemented rewinder for log storage (#2444) +- *(node/service)* Add sequencer admin RPC logs (#2472) +- *(bin/node)* L2 Chain ID Abstraction (#2394) +- *(meta/logs)* Allow logging to file, silencing stdout and more configuration (#2482) +- *(node/service)* Commit unsafe payloads to `op-conductor` (#2486) +- *(tests)* Leadership transfer test (#2493) +- *(node/rpc)* Log rpc server address (#2508) +- (supervisor/storage): rewind storage (#2484) +- *(supervisor/core)* Handle unsafe reorg (#2498) +- *(node/service)* Sequencer state metrics (#2530) +- *(docker)* Add env var to specify cluster name, docker images, grafana port (#2534) +- *(node/service)* Sequencer attributes builder duration metrics (#2531) +- *(node/service)* Sequencer block building job duration (#2532) +- *(node/service)* Conductor commitment time metric (#2533) +- *(supervisor/core)* Added invalidated block on managed node (#2541) +- *(docker)* Sequencer visualizations in `kona-node` dashboard (#2540) +- *(node/engine)* Add metrics to record task success + failure (#2527) +- *(node/engine)* Spike a dev rpc api to get inner engine state. (#2519) + +### 🐛 Bug Fixes + +- *(supervisor/core)* Unsafe block processing (#2357) +- *(supervisor/core)* Handle safe hash mismatch in reset (#2374) +- *(protocol)* Serialization compatibility for `RollupConfig` (#2416) +- *(node/service)* Block label metrics (#2417) +- *(node/engine)* Consolidate task transient safe chain updates (#2421) +- *(supervisor/rpc)* Making all head ref as optional in syncStatus method (#2427) +- *(bin/node)* Fix `SUPERVISOR_PORT` envvar typo (#2440) +- *(cli)* Correct `init_tracing_subscriber` behavior (#2452) +- *(docs)* Hide Landing Page Logo (#2458) +- *(docs)* Front Page (#2459) +- *(node/engine)* Fix consolidate + insert task metrics (#2450) +- *(supervisor)* Error consistency (#2461) +- *(docs)* FPP Dev Docs (#2470) +- *(docs)* Callouts and Doc Links (#2474) +- *(node/service)* Fix sequencer build ticker (#2473) +- *(node/service)* Add latest unsafe block hash to `admin_stopSequencer` response (#2475) +- *(engine)* Allow `ForkchoiceTask` to drive EL sync (#2514) +- *(docker)* Prometheus addr (#2515) +- *(node/engine)* Do not use FCU V1 (#2545) +- *(node/test)* Increase timeout for e2e test (#2547) + +### 📚 Documentation + +- Complete derivation documentation for kona-node (#2466) +- Complete execution engine documentation with trait abstractions and kona-node integration (#2467) +- Update shields.io badges to use crate names as labels (#2479) +- Update docker documentation to use correct kona-node targets (#2477) +- Document admin RPC methods following P2P RPC format (#2490) +- Complete rollup RPC methods documentation (#2491) +- Complete P2P RPC endpoints documentation (#2489) +- Comprehensive README for kona-node with installation and usage instructions (#2492) +- *(docker)* Add default ports to `kona-node` recipe README (#2528) + +### 🧪 Testing + +- *(supervisor/core)* Preinterop e2e test (#2420) +- *(supervisor)* Preinterop acceptance test (#2463) +- *(node/rpc)* Test rollup config rpc endpoint. fix rollup config metrics (#2509) + +### ⚙️ Miscellaneous Tasks + +- *(node/net)* Integrate the network driver inside the network actor (#2376) +- Fix 404 URL (#2393) +- *(node/engine)* Cleanup engine tasks errors. refactors the engine build task to reuse the insert task (#2399) +- Fix broken url (#2408) +- *(node/service)* Clean up engine task logs (#2422) +- *(workspace)* Remove self-referential `dev-dependencies` (#2451) +- *(docs)* Remove Mdbook (#2454) +- *(docs)* Categorize RFCs (#2455) +- *(docs)* Fix Doc Links (#2453) +- *(supervisor)* Stick to versioned op-node release (#2445) +- *(supervisor/storage)* Make `StorageError::ConflictError` type safe (#2281) +- *(docs)* Remove Supervisor Docs (#2469) +- *(node/engine)* Decouple engine and runtime actor (#2483) +- Release Some Crates (#2485) +- More Crate Releases (#2487) +- *(node/test)* Update monorepo fork (#2505) +- *(docs)* Update `kona-node` flags (#2507) +- *(docker)* Don't disable p2p scoring in `kona-node` recipe (#2510) +- Few Crate Releases (#2517) +- *(supervisor/storage)* Log improvements (#2525) +- *(supervisor)* Remove deprecated supervisor api from `kona-node` (#2024) +- *(supervisor/core)* Log improvements in managed node (#2526) + +### Release + +- Kona-node v0.1.1 (#2550) + +## [kona-node/v0.1.0-beta.5] - 2025-07-08 + +### 🚀 Features + +- *(node/test)* Monitor cpu usage inside test (#2292) +- *(node/test)* Add a way to retrieve RPC endpoints addresses from kurtosis services (#2293) +- Block processing metrics (#2296) +- *(supervisor)* Broadcast cross head update events to managed node (#2289) +- *(supervisor/syncnode)* Observe `ManagedModeApiClient` RPC calls (#2279) +- *(node/service)* L1 origin selector (#2240) +- *(node/service)* Sequencer actor (#2241) +- *(node/engine)* Build task modifications for sequencing (#2242) +- *(node/service)* Handle resets in `SequencerActor` (#2313) +- *(supervisor/rpc)* Map errors to spec errors (#2277) +- *(supervisor/e2e)* Interop test (#2335) +- *(supervisor/e2e)* Rpc e2e test (#2339) +- *(supervisor/e2e)* `checkAccessList` RPC e2e (#2352) +- *(bin/node)* Sequencer key flag (#2356) +- *(bin/node)* Change `sequencer.enabled` -> `mode` (#2342) +- *(node/sequencer)* Connect build tasks to the engine (#2359) +- *(node/service)* Propagated errors raised during NodeActor::start (#2322) +- *(node/service)* Use drop guards to ensure actors are cancelled properly (#2363) +- *(node/sequencer)* Move node mode from rollup node service trait to engine actor. (#2360) + +### 🐛 Bug Fixes + +- *(supervisor/storage)* Chaindb metric initialization (#2297) +- *(github/codeowner)* Fix code owners list (#2300) +- *(node/service)* Invert the channel arrows in the rollup node service (#2315) +- *(supervisor/core)* Added missing metrics (#2299) +- *(supervisor)* Cross head promotion stuck (#2327) +- *(supervisor/core)* Consistency check (#2330) +- *(supervisor/rpc)* Invalid param `check_accesslist` (#2334) +- *(supervisor/storage)* Handled derivation storage corner cases (#2340) +- *(supervisor/core)* Derivation reset (#2346) +- *(supervisor/storage)* Safety head ref initialization (#2369) + +### ⚙️ Miscellaneous Tasks + +- *(node)* Indexed Mode Rename (#2290) +- *(node/tests)* Deprecate devnet-sdk and reactivate RPC endpoint tests (#2294) +- *(book)* Indexed + Polling Traversal Stage Docs (#2291) +- *(supervisor/core)* Cross check l1 block with config (#2268) +- Add chainsafe as codeowner for `test`, `docker` and `Cargo` (#2301) +- *(derive)* Globalize Derivation Crate Imports (#2295) +- *(node/engine)* Refactor process method (#2302) +- *(node/service)* Move actor component building process inside the actors (#2303) +- Fix some minor issues in the comments (#2309) +- *(node/sequencer)* Refactor the sequencer module using the builder pattern (#2310) +- *(node/service)* Have `EngineActor` produce reset request channel (#2312) +- *(node/rpc)* Unify RPC actor with the other actors (#2308) +- *(protocol/docs)* Update spec for gov token address constant (#2171) +- *(supervisor)* Grafana dashboard (#2326) +- *(supervisor/storage)* Remove update_safety_head_ref (#2328) +- *(supervisor/core)* Reset handling refactor (#2332) +- *(node)* `NodeMode` helpers (#2343) +- *(workspace)* Bump dependencies (#2344) +- *(node/rpc)* Remove rpc disabled field in rpc builder (#2364) +- *(supervisor)* Op-node devnet version (#2358) +- *(supervisor)* Remove l1 cache (#2349) +- *(bean/node)* Removed `--l2-engine-kind` from kona (#2321) + +## [kona-node/v0.1.0-beta.4] - 2025-06-25 + +### 🚀 Features + +- *(derive)* New Managed Pipelines (#2287) + +### 🐛 Bug Fixes + +- *(node/p2p)* Fix immediately resolved future (#2286) +- *(node)* Interop Mode Wiring (#2288) + +### ⚙️ Miscellaneous Tasks + +- *(node/service)* Refactor node actor trait. take context out of the actor (#2271) +- *(node/service)* Refactor and simplify the `RollupNodeService` trait (#2284) + +## [kona-node/v0.1.0-beta.3] - 2025-06-25 + +### 🚀 Features + +- *(node/p2p)* Handle peer score inside `opp2p_peers` (#2118) +- *(supervisor/rpc)* Implement `supervisor_localUnsafe` (#2129) +- *(supervisor/rpc)* Implement `supervisor_crossSafe` (#2131) +- *(supervisor/rpc)* Implement `supervisor_finalized` (#2139) +- *(supervisor/core)* Finalized l1 process handling (#2147) +- *(p2p)* Post Unsafe Payload (#2064) +- *(node/test-devstack)* Update devstack configuration to large networks (#2144) +- *(interop)* Managed Event (#2160) +- *(supervisor/rpc)* Implement `supervisor_finalizedL1` (#2157) +- Add predeploy_address constants in maili-genesis now kona-genesis (#2149) +- *(supervisor/core)* Finalized l1 watcher with head ref updates (#2167) +- *(node)* Supervisor Rpc Server Flags (#2168) +- *(node)* Wire up Supervisor RPC Flags + Config (#2174) +- *(node)* Supervisor Actor Setup (#2161) +- *(supervisor/rpc)* Implement supervisor_dependencySet (#2165) +- *(supervisor/core)* Remove hardcoded finalized head from reset event (#2172) +- *(supervisor/interop)* Added a `chain_id` field to ExecutingDescriptor (#2173) +- *(rpc)* Supervisor RPC Server (#2162) +- *(bin/node)* Conductor cli flags (#2193) +- *(supervisor)* Implemented `CrossChainSafetyProvider` (#2197) +- *(supervisor/rpc)* Implement `supervisor_superRootAtTimestamp` (#2180) +- *(supervisor)* Implemented cross chain safety checker (#2200) +- *(node/rpc)* Test node rpc endpoints (#2191) +- *(protocol/derive)* Provide Block Signal (#2230) +- *(supervisor/core)* Check for inconsistency and trigger reset (#2220) +- *(supervisor/storage)* Internal database metrics (#2216) +- *(node/service)* Supervisor Engine Resets (#2235) +- *(supervisor)* Implemented safety checker job (#2212) +- Chaindb metrics (#2238) +- *(derive)* Managed Traversal Stage (#2270) +- *(ci)* Introduce `zepter` checks (#2267) +- *(supervisor)* Integrated cross safety checker job (#2264) +- *(supervisor/core)* L2 finalized head update (#2253) + +### 🐛 Bug Fixes + +- *(node/p2p)* Fix opp2p_peers rpc handler. Only return total peer score (#2121) +- *(supervisor/rpc)* Metrics added for rpc methods (#2138) +- *(supervisor)* Expose get_superhead using trait (#2150) +- *(supervisor/rpc)* LocalUnsafe always points to genesis (#2145) +- *(supervisor)* Use raw config l2 url (#2188) +- *(interop)* Remove timestamps from the depset (#2187) +- *(protocol/interop)* Made `override_message_expiry_window` as optional (#2208) +- *(node/service)* Enable TLS in alloy providers / transports (#2222) +- *(supervisor)* Supervisor kurtosis and test (#2251) +- *(supervisor/core)* Consistency check (#2250) +- *(node/docker)* Fix kona's docker deployment (#2259) +- *(docker)* Include `ca-certificates` in final executable image (#2260) +- *(hardforks)* Enable Ecotone Selector (#2263) + +### 🧪 Testing + +- *(node/e2e-devstack)* Increase node coverage with devstack (#2153) +- *(node/e2e-devstack)* Deprecate p2p tests in devnet-sdk (#2159) +- *(protocol/interop)* Add log parsing test (#2196) +- *(node/p2p)* Adding peer ban tests (#2201) + +### ⚙️ Miscellaneous Tasks + +- *(rpc)* Cleanup IpNet TODOs (#2120) +- *(supervisor/storage)* Add note about local unsafe block to `DerivationStorageReader::derived_to_source` (#2127) +- *(supervisor)* State e2e (#2132) +- *(node/cli)* Fix todos (#2142) +- *(node/p2p)* Merge peer infos and peerstore (#2143) +- *(supervisor)* Update `SupervisorError` conversion (#2134) +- *(supervisor/core)* Rm unused variant `ChainProcessorError::InvalidChainId` (#2152) +- *(supervisor/core)* Mv `SupervisorError` to own module (#2151) +- *(node/rpc)* Cleanup rpc config flags and launcher (#2163) +- *(node/net)* Simplify the network interface. (#2175) +- *(supervisor)* Passing l1_provider instead of l1_rpc_url (#2043) +- *(node/rpc)* Unify BaseFeeConfig to follow op-geth (#2198) +- *(bin/node)* Increase error verbosity on port binding failure (#2225) +- *(node/service)* Squash service traits (#2231) +- *(node/service)* Cleanup `RollupNodeService::start` (#2233) +- *(bin/node)* Move conductor flags to sequencer args (#2234) +- *(node/test)* Update dependencies (#2224) +- *(derive)* Metrics Mod Visibility (#2247) +- *(driver)* Code Doc References (#2248) +- *(derive)* Code Doc Comments (#2249) +- *(genesis)* Remove Deprecated HardforkConfig Alias (#2258) +- *(workspace)* Use `rustls` over `request`'s default TLS feature (#2261) +- *(bin/node)* Improve error verbosity for runtime loader failure (#2262) +- *(hardforks)* Code Comment References (#2265) +- *(node/service)* Remove process method from NodeActor trait (#2266) + +### Fear + +- *(supervisor/core)* Added a new method to fetch the super head in a single tx (#2140) + +## [kona-node/v0.1.0-beta.2] - 2025-06-12 + +### 🚀 Features + +- *(node/e2e-tests)* Bootstrap devstack testing suite (#2105) +- *(p2p)* Track and Return Peer Ping Latencies (#2112) +- *(node/p2p)* Impl BlockSubnet + UnblockSubnet (#2073) +- *(node/p2p)* Impl opp2p_listBlockedSubnets (#2072) +- *(node/devstack-e2e)* Convert simple tests to devstack (#2116) +- *(node/p2p)* Record peer score distribution metrics (#2117) + +### 🐛 Bug Fixes + +- *(node/p2p)* Reduce number of peers that need to be connected for large e2e tests to pass (#2099) +- *(p2p)* Properly Record Connectedness (#2103) + +### ⚙️ Miscellaneous Tasks + +- *(supervisor)* E2E workflow configuration (#2091) +- *(docker)* Update grafana dashboard (#2101) +- *(p2p)* Kona Peers (#2097) +- *(node/p2p)* Enable peer monitoring if configured (#2102) +- Update Alloy Dependencies (#2082) +- *(node)* Add L2 chain ID environment variable (#2113) +- *(supervisor/docs)* Fix docs for RPC method `supervisor_allSafeDerivedAt` (#2104) +- *(node)* Standardize envvars (#2114) +- *(node)* Add rollup config env var (#2115) + +## [kona-node/v0.1.0-beta.1] - 2025-06-11 + +### 🚀 Features + +- *(derive)* Initial Metrics (#2007) +- *(supervisor/storage)* Derivation storage initialization (#1963) +- *(supervisor/rpc)* Add RPC method `supervisor_syncStatus` (#1952) +- *(supervisor/storage)* Log storage initialization (#1979) +- *(supervisor)* Service initialisation (#1984) +- *(derive)* Frame + Channel Metrics (#2008) +- *(derive)* Max Channel Size and Timeout Metrics (#2009) +- *(derive)* Batch Metrics (#2010) +- *(derive)* Pipeline Metrics (#2021) +- *(node/p2p)* Support chain id info inside opp2p_peers (#2017) +- *(node/p2p)* Support user agent and protocol version in opp2p_peers rpc call (#2018) +- *(node/p2p)* Support gossip information inside `opp2p_peers` (#2016) +- *(derive)* System Config Update Metrics (#2029) +- *(node/p2p)* Rotate peers out of `BootStore` (#2033) +- *(derive)* Decompressed Channel Size Metric (#2031) +- *(node/service)* Introduce backpressure in DA watcher channels (#2030) +- *(node/p2p)* Add a shim handler for sync req resp protocol (#2042) +- *(rpc)* Connect Peer RPC Method (#2037) +- *(node/service)* Removed unbounded channels in kona-service actors and replaced with bounded ones (#2045) +- *(node/p2p)* Improve dial metric granularity (#2052) +- *(p2p)* Connection Gater (#2055) +- *(p2p)* Peer Blocking (#2056) +- *(supervisor/core)* Reset event handling (#2049) +- *(p2p)* Address Blocking (#2060) +- Make websocket optional (#1918) +- *(p2p)* Peer Protection (#2061) +- *(supervisor/core)* `derivationOriginUpdate` event (#2070) +- *(node/p2p)* Add a dial period to dial protections (#2075) +- *(p2p)* Peers Rpc Endpoint Progress (#2081) +- *(ci)* Improve caching of e2e jobs (#2080) +- *(supervisor/rpc)* Implement RPC method `supervisor_allSafeDerivedAt` (#2078) +- *(rpc)* Healthcheck Endpoint (#2095) + +### 🐛 Bug Fixes + +- *(node/p2p)* Fix op-p2p peers rpc call (#2014) +- Managed node initialisation (#2023) +- *(node/p2p)* Fixing gossip stability by limiting the number of dials to a given peer (#2025) +- *(supervisor/core)* Subscription handling (#2027) +- *(docker)* Correctly set RPC port in node recipe (#2035) +- *(node/p2p)* Reactivate e2e tests for gossip. (#2036) +- *(node/service)* Authenticate unsafe block signer updates (#2041) +- *(node/p2p)* Fix cli arguments values for node's p2p (#2051) +- *(node/p2p)* Fix large e2e kurtosis tests (#2059) +- *(node/p2p)* Raise default dial threshold (#2063) +- *(tests/deps)* Update monorepo fork dep (#2071) +- *(test/deps)* Fix go dependencies (#2076) +- *(node/e2e-tests)* Enable websockets for e2e tests (#2079) +- *(p2p)* Peer Blacklisting (#2085) +- *(node/net)* Added back the `pubpublish_rx `in the NetworkActor, adjusted the other sections (#1910) + +### ⚙️ Miscellaneous Tasks + +- *(docker)* Update grafana dashboard (#1991) +- Kurtosis supervisor network (#1949) +- *(derive)* Remove Prelude (#2038) +- *(docker)* Update Grafana Dashboard (#2040) +- *(derive)* Hoist Error Exports (#2039) +- *(node/service)* Display node mode in startup log (#2053) +- *(node/service)* Remove `engine_ready` flag (#2054) +- *(node/service)* Use watch channels in L1 watcher (#2057) +- Dedup GlobalArgs for log verbosity (#2066) +- *(engine)* Log finalized head updates (#2058) +- *(derive)* Flatten Exports (#2077) +- *(tests)* Add `kona-node` + `op-geth` config (#2074) +- *(supervisor)* Use devstack for e2e tests (#2062) +- *(p2p)* Banned Peer Count (#2083) +- *(p2p)* Protected Peer Stat (#2084) +- *(ci)* Don't fail fast on E2E matrix (#2098) + +## [kona-host/v1.0.2] - 2025-06-04 + +### 🚀 Features + +- *(supervisor/core)* Managed derivation data flow (#1946) +- *(node/engine)* Add `info` log for safe head promotion (#1948) +- *(node)* Override Flags (#1968) +- *(node/p2p)* Track Banned Peers (#1967) +- Unsafe block processing (#1951) +- *(supervisor/core)* Rollup config (#1953) +- *(node/p2p)* Peer score histogram (#1975) + +### 🐛 Bug Fixes + +- *(node/p2p)* Gossipsub connection metric keys (#1947) +- *(p2p)* Disable Peer Scoring (#1962) +- *(node)* CLI Metrics Init (#1969) + +### ⚡ Performance + +- Remove Duplicated L2 Block Query (#1960) + +### ⚙️ Miscellaneous Tasks + +- *(workspace)* Bump MSRV to `1.86` + `reth` deps (#1945) +- Update Dependencies (#1955) +- *(rpc)* Rollup Node RPC Endpoint Metrics (#1958) +- *(rpc)* Disconnect Peer RPC Request (#1964) +- *(node)* Additional CLI Option Stats (#1965) +- *(ci)* Update `monorepo` revision (#1966) +- *(node)* Hardfork Metrics (#1971) +- *(docker)* Update grafana dashboard (#1970) +- *(node)* Additional CLI Option Metrics (#1973) +- *(rpc)* Server Restarts (#1972) +- *(node)* Rollup Config Metrics (#1974) +- *(node)* Indexable hardfork activation time metrics (#1976) +- Fix some typos in comment (#1978) +- *(client/host)* Prepare for `v1.0.2` release (#1995) + +## [kona-node/v0.0.1-beta.6] - 2025-06-02 + +### 🚀 Features + +- *(node-service)* Derivation pipeline L1 origin metric (#1892) +- *(docker)* Update `kona-node` dashboard (#1894) +- *(supervisor/core)* Managed node event channel (#1887) +- *(justfile/docker)* Import docker/app justfile in main kona justfile (#1912) +- *(node/e2e-tests)* Add a restart recipe for hot reloads of the kurtosis network (#1914) +- *(supervisor/core)* Chain processor skeleton (#1906) +- Use alloy-op-hardfork constants (#1922) +- *(node/p2p)* Adding gossipsub event metrics (#1916) +- *(supervisor/core)* Logindexer implementation (#1898) +- *(node/service)* Add metric for critical derivation errors (#1938) + +### 🐛 Bug Fixes + +- Supervisor kurtosis devnet (#1900) +- *(node/e2e-sync)* Fix large sync test (#1899) +- *(docker)* Add fix-missing to fix docker builds (#1915) +- *(ci)* Prefix-key designation in generic setup (#1933) +- *(docker)* Use cache busting for generic app dockerfile (#1935) +- *(p2p)* Labels for Gossip Events (#1932) + +### 📚 Documentation + +- *(cargo-chef)* Add cargo chef to cache rust builds in docker (#1896) + +### ⚡ Performance + +- *(ci)* Use `mold` linker for performance (#1934) +- *(ci)* Only persist rust cache on `main` (#1936) +- *(engine)* Batch safe-head FCUs (#1937) + +### 🧪 Testing + +- *(node/e2e-sync)* Adding e2e sync tests for unsafe/finalized sync (#1861) + +### ⚙️ Miscellaneous Tasks + +- *(node-sources)* Enable `RuntimeLoader` metrics (#1893) +- *(supervisor)* Error logs consistency (#1889) +- *(supervisor)* Smol fix spacing in display string (#1903) +- *(supervisor/core)* `ManagedNode` error handling (#1904) +- *(supervisor)* Add `SubscriptionError` (#1908) +- *(supervisor)* Define `AuthenticationError` (#1911) +- *(node/supervisor)* Move MetricArgs into kona-cli (#1888) +- Update Dependencies (#1923) +- *(protocol)* Encapsulate Magic Arithmetic (#1924) +- *(docker)* Small Grafana Doc Update (#1931) +- Update Kona Node Grafana Dashboard (#1930) +- *(ci)* Remove rust toolchain installation for e2e workflows (#1939) +- *(node)* P2P CLI Metrics (#1940) + +## [kona-node/v0.0.1-beta.5] - 2025-05-28 + +### 🚀 Features + +- *(engine)* Chain label metrics (#1741) +- *(node)* Retry engine capability handshake (#1753) +- *(node-service)* Aggressively process channels (#1756) +- *(node)* Version info metrics (#1758) +- *(node)* Mark derivation as idle when it yields (#1759) +- *(supervisor/service)* L1 watcher for supervisor (#1717) +- *(engine)* Task count metrics (#1766) +- *(engine)* Engine method call duration metrics (#1767) +- Replace SafetyLevel with op_alloy type (#1782) +- *(node)* Propagate engine reset to pipeline (#1789) +- Dependency set (#1793) +- *(node)* Add kurtosis e2e testing skeleton (#1792) +- *(engine)* Simplify EL sync startup routine (#1809) +- *(node/ci)* Add e2e tests to ci (#1798) +- *(engine)* Engine reset counter metric (#1806) +- *(node/p2p)* Enable support for the peers RPC endpoint (#1811) +- *(node/p2p)* Implements the `opp2p_peerStats` endpoint (#1812) +- *(node-service)* Handle derivation reset events (#1816) +- *(node-service)* L1 reorg metrics (#1817) +- *(supervisor/storage)* Derivation schema (#1808) +- *(node/e2e-test)* Add peer count test in Kurtosis (#1823) +- *(docker)* `kona-node` recipe (#1832) +- *(protocol/interop)* Derived ref pair (#1834) +- *(node/p2p)* Streaming engine state through websockets (#1833) +- *(supervisor/storage)* Derivation provider (#1835) +- *(node)* L2 finalization routine (#1858) +- *(supervisor/storage)* Log storage (#1830) +- *(supervisor/storage)* Chaindb and chaindb factory (#1864) +- *(supervisor/db)* Safety head reference storage (#1865) +- *(node-service)* Refactor `EngineActor` (#1867) +- *(docker)* Local builds + Cleanup (#1877) +- *(supervisor/core)* Implement l2 controller (#1866) +- *(workspace)* Performance release build profile (#1882) + +### 🐛 Bug Fixes + +- *(node/engine)* Initialize Unknowns (#1705) +- *(node/rpc)* Removed the panic and added error when building RPC actor (#1709) +- *(node/engine)* Derivation Sync Start (#1708) +- *(node-p2p)* Discovery event metric labels (#1740) +- *(engine)* Reset with inconsistent chain state (#1763) +- *(node/p2p)* Bootnodes in Bootstrap + Backoff Discv5 Restart (#1755) +- *(node/p2p)* Disable Topic Scoring (#1765) +- *(p2p)* Discv5 startup panic (#1768) +- *(bin/node)* Argument Defaults (#1769) +- *(kurtosis)* Fix kurtosis configuration (#1774) +- *(node-service)* Engine sync completion condition (#1780) +- *(node/p2p)* Async Broadcast with Backoff (#1747) +- *(node/p2p)* Default Channel Size (#1783) +- *(ci)* Use stable toolchain for lint job (#1802) +- *(node)* Zero `safe` + `finalized` hashes while EL syncs (#1801) +- *(node)* Set `local-safe` and `cross-unsafe` labels (#1803) +- *(node)* Build task + consolidate condition (#1804) +- *(ci)* Free disk space on kurtosis e2e tests (#1821) +- *(sources)* Stop sync start walkback at genesis (#1818) +- *(node/kurtosis-e2e-test)* Fixes the versioning for the e2e testing package (#1814) +- *(node/p2p)* Fix node id serialization and connectedness display (#1824) +- *(p2p/rpc)* Fix formatting for the `opp2p-peers` rpc method (#1827) +- *(node/async)* Fix cpu usage from future polling (#1848) +- *(engine)* Prevent `drain` starvation (#1853) +- *(kurtosis/config)* Fix kurtosis config to use teku instead of nimbus. (#1852) +- Several typos (#1874) + +### 🧪 Testing + +- *(supervisor)* Add kurtosis network params for supervisor (#1777) +- *(node/e2e)* Extend the p2p tests (#1831) +- *(node/sync-e2e)* Adding e2e safe head sync tests (#1857) +- *(supervisor/ci)* Configure supervisor workflow (#1840) +- *(supervisor)* Comment out kona-supervisor in kurtosis network params (#1873) + +### ⚙️ Miscellaneous Tasks + +- *(meta)* Add chainsafe as code owners of `kona-supervisor` (#1706) +- *(workspace)* Update Alloy Deps (#1732) +- *(workspace)* Update REVM Deps (#1733) +- *(engine)* Remove `skip` module (#1737) +- *(node/engine)* Remove CL Sync (#1739) +- *(node/engine)* Update Docs (#1736) +- *(node/rpc)* Remove Duplicate Protocol Versioning Code (#1744) +- *(engine)* Chain label metrics cleanup (#1751) +- *(engine)* Remove pending/backup unsafe head (#1742) +- *(bin/node)* Remove Discover Subcommand (#1748) +- *(bin/node)* Subcommand Tests (#1752) +- *(node/service)* Small Cleanup (#1743) +- Update LICENSE.md (#1764) +- *(node)* Empty cargo features metric (#1770) +- *(sources)* Refactor sync start to use alloy providers (#1773) +- *(ci)* Normalize foundry version (#1787) +- Consolidate Scripts (#1788) +- *(node/p2p)* Remove Stale Docs (#1784) +- *(supervisor)* Rm deprecated `InvalidInboxEntry` error (#1778) +- *(engine)* Update block insertion log (#1791) +- *(node/p2p)* Move P2P CLI Utilities (#1785) +- *(node-service)* Lazy initialize pipeline cursor / engine state (#1790) +- *(workspace)* Manifest Hygiene (#1795) +- *(protocol/registry)* Remove Default Hasher (#1797) +- *(workspace)* Remove + Ignore Vscode Config (#1796) +- *(workspace)* Use `alloy-eips`' `EMPTY_REQUESTS_HASH` constant (#1805) +- *(engine)* Remove `SyncStatus` (#1810) +- *(engine)* Don't zero block label gauges (#1820) +- *(supervisor)* Dedup `SupervisorApi` (#1713) +- *(workspace)* Bump dependencies (#1846) +- *(docker)* Update `kona-node` dashboard (#1859) +- *(supervisor/storage)* Rename BlockRef field `time` to `timestamp` (#1870) +- *(ci)* Consolidate workflows (#1863) +- *(workspace)* Remove kurtosis recipes (#1879) +- *(kurtosis/e2e-tests)* Revamp e2e testing to natively use kurtosis and not clone any repo (#1880) +- *(workspace)* Move `kona-macros` to `utilities` (#1884) +- *(supervisor)* Rename `ManagedNodeApi` to `ManagedModeApi` to match specs (#1875) +- *(tests)* Skip `node/p2p` e2e tests (#1885) +- *(workspace)* `Justfile` -> `justfile` (#1883) + +### Patch + +- *(node/p2p)* Patch the default p2p params (#1856) + +## [kona-host/v1.0.1] - 2025-05-10 + +### 🚀 Features + +- *(docker)* Enable reproducible prestate builds for interop program (#1610) +- *(interop)* Use `FpvmOpEvmFactory` in interop proof program (#1611) +- Add code hash tests (#1601) +- *(node)* Describe and Zero Metrics (#1612) +- *(genesis)* Add `is_first_fork_block` helpers to `RollupConfig` (#1646) +- *(genesis)* Add `block_number_from_timestamp` helper (#1647) +- *(interop)* Bubble up message validity errors (#1644) +- *(interop)* Support unaligned activation time (#1645) +- *(proof)* Derivation over generic L1/L2/DA providers (#1655) +- *(host)* Trace span for `L2BlockData` re-execution (#1658) +- *(node/service)* Superchain Signaling with Runtime Loading (#1662) +- *(node/engine)* Transmit the most recent version of the `EngineState` over watch channels to the engine actor (#1673) +- *(node/service)* Flush Channel on Invalid Payloads (#1675) +- *(node/service)* Update L2 Safe Head (#1677) +- *(node/service)* Re-import Deposits Only Payload (#1676) +- *(node/engine)* Set Safe Head Consolidation (#1690) +- *(executor)* Public `compute_receipts_root` method (#1686) +- *(engine/rpc)* Skeleton implementation of the engine rpc (#1664) +- *(node/l1_watcher)* Adds query handler for the l1 watcher (#1692) +- *(node/rpc)* Implement the rollup RPC endpoints (#1697) +- *(supervisor)* Boilerplate (#1700) +- Bump `kona-client` and `kona-host` versions (#1711) + +### 🐛 Bug Fixes + +- *(node/p2p)* Use Compat Metrics Crate (#1609) +- *(node/engine)* Incorrect Engine Method Use in Insert Task (#1630) +- *(node/engine)* Don't use Genesis in EngineState (#1635) +- *(node/service)* Wait to Kickstart Derivation (#1640) +- *(node/engine)* Attributes Matching and Sync Status (#1665) +- *(bin/node)* Runtime Config Wiring (#1669) +- *(node/engine)* Attributes Tx Mismatch (#1666) +- *(node/engine)* Round Robin Task Execution (#1667) +- *(node/service)* Mark Engine Ready on Sender (#1674) +- *(node/service)* EL Sync Only (#1691) +- *(node/service)* Check L2 Safe Head BN (#1703) +- *(node/engine)* Pre-Holocene Deposits Only (#1702) + +### ⚙️ Miscellaneous Tasks + +- *(client)* Loosen type constraints on `FpvmOpEvmFactory` (#1613) +- Rm redundant bounds (#1614) +- *(genesis)* Use `abi_decode_validate` (#1632) +- *(interop)* Reduce message expiry window (#1636) +- *(workspace)* Bump op-alloy Dep (#1631) +- *(bin/node)* Disable DISCV5 logging by default (#1638) +- *(node/engine)* Decrease Temporary Log Level (#1639) +- *(bin/node)* Cleans up Logs Some More (#1641) +- *(node)* Log Cleanup (#1642) +- *(interop)* Refactor `MessageGraph` test utilities (#1643) +- *(node/rpc)* Refactor p2p rpc (#1633) +- *(bin/node)* CLI Argument Unit Tests (#1660) +- *(node/engine)* Small Engine Touchups (#1657) +- *(node/rpc)* Remove no-std requirement from kona-rpc, fix imports (#1659) +- *(docker)* Update `asterisc` tag (#1661) +- *(proof)* Drop lock before `await` (#1663) +- *(hardforks)* Correct EIP-2935 source name (#1678) +- *(meta)* Add chainsafe as supervisor code owners (#1701) +- *(kona/kurtosis)* Update the link to the optimism-package repo (#1707) + +## [kona-host/v1.0.0] - 2025-05-01 + +### 🚀 Features + +- *(workspace)* Kurtosis Justfile Targets (#1594) +- *(protocol)* Interop upgrade transactions (#1597) +- *(protocol)* Interop transition batch validity rule (#1602) + +### 🐛 Bug Fixes + +- *(node/p2p)* Enable identify protocol. Additional small fixes (#1592) +- *(workspace)* Build Kona Node (#1598) +- *(bin/node)* Enable Metrics (#1600) +- *(node/p2p)* Block Validation (#1604) + +### ⚙️ Miscellaneous Tasks + +- *(workspace)* Add FPVM artifacts to `gitignore` (#1593) +- *(interop)* Update interop fork activation check for initiating messages (#1599) +- *(docker)* Update `cannon` tag (#1606) + +### Release + +- `kona-client` + `kona-host` v1.0.0 version bump (#1605) + +## [kona-host/v0.1.0-beta.18] - 2025-04-29 + +### 🚀 Features + +- *(node)* Redial Peers (#1477) +- Add info subcommand to kona-node (#1488) +- *(node)* Discover Command (#1481) +- *(bin/node)* Add custom bootnode list CLI arg (#1496) +- *(bin/node)* Ensure genesis matches the rollup config before fetching to the known network params list. (#1498) +- *(interop)* Add message expiry check (#1506) +- *(config/kurtosis)* Add a simple kurtosis configuration file to the `.config` folder (#1512) +- *(bin/node)* Add advertise p2p flags (#1509) +- *(bin/node)* Also advertise udp port (#1516) +- *(node/p2p)* Add simple p2p rpc endpoints (#1535) +- *(proof-interop)* Block replacement transaction (#1540) +- *(protocol)* Add `OutputRoot` type (#1544) +- *(protocol)* Complete `Predeploys` definition (#1549) +- *(ci)* Interop FPP action tests (#1546) +- *(bin/node)* Extend P2P Configurability (#1553) +- *(node/p2p)* Disable dynamic ENR updates for static IPs (#1558) +- *(std-fpvm)* Instruct kernel to lazily allocate pages (#1567) + +### 🐛 Bug Fixes + +- *(bin/node)* Sequencer Args Duplicate CLI Flag (#1490) +- *(node/p2p)* Metrics Tasks (#1491) +- *(node/p2p)* Peer Score Level Off (#1493) +- *(node/p2p)* Discovery Test Fixes (#1494) +- *(utilities)* CLI Verbosity Level (#1502) +- *(proof-interop)* Operate provider off of `local-safe` heads (#1503) +- *(node)* Unsafe Block Signer (#1505) +- *(bin/node)* Magic String (#1513) +- *(bin/node)* Adds Lints (#1518) +- *(node/p2p)* Add Lints (#1520) +- Use Unspecified Ipv4Addr (#1522) +- *(bin/node)* Fix the unsafe block signer to be compatible with unknown chain ids (#1523) +- *(node/p2p)* Ensure that the discovery and the gossip keys match (#1525) +- *(bin/node)* Discovery Config Unset (#1532) +- *(host-interop)* Ignore block data hint if chain hasn't progressed (#1542) +- *(node/p2p)* Use EnrValidation (#1552) +- *(node/p2p)* Wait for Swarm Address Dialing (#1554) +- *(std-fpvm)* Large file IO (#1555) +- *(node/p2p)* Fix the address list returned by `opp2p_self` (#1559) +- *(node/p2p)* Fix multiaddress translation (#1561) + +### 📚 Documentation + +- *(providers-alloy)* Doc touchups (#1504) + +### ⚡ Performance + +- *(ci)* Cache forge artifacts in action tests (#1548) +- *(std-fpvm)* Switch to `buddy_system_allocator` (#1590) + +### ⚙️ Miscellaneous Tasks + +- Bump alloy 0.15 (#1492) +- *(node/p2p)* Broadcast Wrapper (#1489) +- *(bin/node)* Adding more flexibility to verbosity levels (#1497) +- Bump `op-alloy` to `v0.15.1` (#1515) +- *(bin/node)* Move Runtime Loading into Sources (#1517) +- *(node/p2p)* Peer Count Metric (#1527) +- *(node/p2p)* Forward Discovery Events (#1495) +- *(ci)* Remove interop proof from codecov ignore (#1547) +- *(host)* Gate experimental `debug_payloadWitness` usage (#1591) + +## [kona-node/v0.0.1-beta.1] - 2025-04-22 + +### 🐛 Bug Fixes + +- *(node)* Fix RPC client building by adding jwt auth. (#1487) + +## [kona-node/v0.0.2] - 2025-04-22 + +### 🚀 Features + +- *(node)* Bootstore Debugging Tool (#1478) +- Function to init tracing in testing (#1467) +- Add sequencer CLI params (#1485) + +### 🐛 Bug Fixes + +- *(proof)* Blob preimage keys (#1473) +- *(bin/node)* Global Argument Positioning (#1482) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Bump timeout on Rust CI jobs (#1479) +- *(workspace)* Improve proof tracing (#1476) +- *(workspace)* Convert all tracing targets to `snake_case` format (#1484) +- *(bin/node)* Subcommand Aliases (#1483) + +### Bug + +- *(bin/node)* Fix metrics address flag (#1486) + +## [kona-node/v0.0.1] - 2025-04-18 + +### 🚀 Features + +- Use `alloy-evm` for stateless block building (#1400) +- *(executor)* Add example + docs for generating new test fixtures (#1438) +- Discovery Interval Configurability (#1445) +- *(node/p2p)* Refactor block validity checks (#1451) +- *(node/p2p)* Add manual block hash checks (#1453) +- *(node/p2p)* Add version specific block checks (#1454) +- *(node/p2p)* Add remaining block checks (replays + maximum block number per height) (#1455) +- *(docker)* Use generic dockerfile for all binary apps (#1465) +- *(ci)* Generic Binary (#1470) + +### 🐛 Bug Fixes + +- *(node/p2p)* Enr Validation (#1446) +- *(bin/node)* Break after Receiving Peer Count (#1448) +- *(node)* Configurable Bootstore Path (#1449) +- *(cli)* Missing Env Filter (#1458) +- *(node)* Local Bootstore Conflicts (#1450) +- *(protocol)* Incorrect Genesis Hash Consensus Block (#1459) + +### 🧪 Testing + +- *(node/p2p)* Add extensive testing for block decoding validation (#1452) + +### ⚙️ Miscellaneous Tasks + +- Remove useless TODO (#1418) +- Use `OpPayloadAttributes::recovered_transactions` (#1434) +- *(ci)* Add action to free up disk space in action test job (#1439) +- Use encodedwith for execute (#1441) +- *(comp)* Enable `test-utils` feature with `cfg(test)` (#1442) +- *(derive)* Enable `test-utils` feature with `cfg(test)` (#1443) +- *(protocol)* Enable `test-utils` feature with `cfg(test)` (#1444) +- *(bin/node)* Discovery Config Wiring (#1456) +- *(node/service)* P2P Rpc Module Wiring (#1469) + +### Bug + +- *(node/p2p)* Fix bootstrapping for enode addresses (#1435) + +## [kona-host/v0.1.0-beta.16] - 2025-04-15 + +### 🚀 Features + +- *(docker)* Reproducible `cannon` prestate (#1389) +- *(node/service)* Wire in Engine Arguments (#1383) +- *(node/service)* Init the Engine Actor (#1387) +- *(node/service)* Wire in the Engine Actor (#1390) +- *(bin/node)* Registry Subcommand (#1379) +- *(node/service)* Engine Consolidation Task (#1391) +- *(node/service)* Insert Unsafe Payload Envelope (#1392) +- *(bin/node)* Implement peer banning (#1405) +- *(node/engine)* Add transaction checks to the consolidation step (#1412) +- *(node/engine)* Check eip1559 parameters inside consolidation (#1419) +- *(bin/node)* Extend metrics configuration options (#1422) + +### 🐛 Bug Fixes + +- *(bin/node)* Disable P2P when Specified (#1409) +- *(node/service)* Error Bubbling and Shutdown (#1410) +- *(node/engine)* Engine State Builder Missing (#1408) + +### ⚙️ Miscellaneous Tasks + +- *(node/engine)* Use thiserror instead of anyhow (#1395) +- *(docs)* Edited the license link (#1403) +- *(node/service)* Engine State Builder Error (#1406) +- *(node/service)* Touchup EngineActor (#1404) +- Bump scr + monorepo (#1420) +- *(bin/node)* In-use Port Erroring (#1411) +- *(ci)* Hide action test output (#1428) +- Add `@theochap` to `CODEOWNERS` file (#1430) +- *(bin/node)* Add jwt startup check (#1421) +- Remove redundant word in comment (#1433) + +## [kona-host/v0.1.0-beta.15] - 2025-04-08 + +### 🚀 Features + +- *(node)* RPC CLI Args (#1314) +- *(node)* RPC Config (#1315) +- *(node)* Rpc Actor (#1318) +- *(node)* Wire in the RpcConfig (#1321) +- *(protocol)* Move Compression Types (#1298) +- *(node/rpc)* RpcLauncher (#1325) +- *(node/p2p)* P2P RPC Server (#1327) +- *(node)* Network Config (#1323) +- *(bin)* Network Subcommand (#1300) +- *(node/p2p)* Redial Peers (#1367) +- *(bin/node)* Peer Scoring Setup (#1376) +- *(node/p2p)* Unsafe Payload Publishing (#1359) +- *(node/service)* Unsafe Block Signer Updates (#1386) + +### 🐛 Bug Fixes + +- *(node/service)* Unsafe Block Signer (#1322) +- *(node/p2p)* Gossip Config (#1328) +- *(node/p2p)* Forward ENRs to the Swarm (#1337) +- *(bin)* Correct Unsafe Block Signer (#1339) +- *(node/p2p)* OP Stack ENRs (#1353) +- *(node/p2p)* Unsafe Payload Sending (#1366) +- *(bin/host)* Fix typo (#1384) + +### 📚 Documentation + +- Fx incorrect link reference for MSRV section (#1302) + +### ⚙️ Miscellaneous Tasks + +- Remove Magic 0x7E Deposit Identifier Bytes (#1292) +- *(cli)* Refactors Backtrace Init (#1293) +- *(ci)* Refactor Github Action Steps (#1294) +- *(protocol)* Remove unused L1 Tx Cost Functions (#1295) +- *(protocol)* Cleanup Utilities (#1297) +- *(protocol)* Remove Unused Frame Iterator (#1296) +- Small Manifest Cleanup (#1299) +- Update Dependencies (#1304) +- Derive std Traits (#1329) +- Derive More Core Traits (#1333) +- *(node/p2p)* Network RPC Request Handling (#1330) +- *(bin)* Rename RpcArgs (#1338) +- *(node/p2p)* Cleanup Network Driver (#1349) +- *(node/p2p)* Log Cleanup (#1348) +- *(bin/node)* P2P Config Constructor (#1350) +- *(node/service)* Cleans up the Rollup Node Service (#1352) +- Bump op-alloy Patch (#1364) +- *(node/service)* Dynamic Node Mode (#1358) +- *(bin/node)* Allow subcommands to customize telemetry (#1370) +- *(node/p2p)* Small Log Cleanup (#1369) +- *(node/service)* Remove .expect (#1381) +- *(node/service)* The RPC Launcher is Used (#1382) +- *(ci)* Bump monorepo commit (#1385) + +## [kona-host/v0.1.0-beta.14] - 2025-03-24 + +### 🚀 Features + +- *(examples)* Pulls Discovery and P2P Gossip into Examples (#1250) +- *(node)* P2P Wiring (#1246) +- *(node)* P2P Overhaul (#1260) +- *(engine)* Synchronous task queue (#1256) +- *(engine)* Block building task (#1258) +- *(node)* P2P Upgrades (#1271) +- *(interop)* Add utility trait method to `InteropTxValidator` (#1291) + +### 🐛 Bug Fixes + +- *(executor)* Use correct empty `sha256` hash (#1267) +- *(proof)* EIP-2935 walkback fix (#1273) + +### ⚙️ Miscellaneous Tasks + +- *(node)* Wire in Sync Config (#1249) +- *(node)* Simplify Node CLI (#1251) +- *(node)* P2P Secret Key (#1254) +- Remove B256 Value Parser (#1255) +- *(cli)* Remove CLI Parsers (#1259) +- *(workspace)* Fix udeps check (#1263) +- *(genesis)* Localize Import for Lints (#1265) +- Fixup Benchmark CI Job (#1274) +- *(ci)* Deprecate --all Flag (#1275) +- Cleanup and Dependency Bumps (#1235) +- *(workspace)* Remove `reth` dependency (#1279) +- *(ci)* Bump Monorepo Commit for Operator Fee Tests (#1277) +- Bump Deps before Release (#1288) +- Minor Crate Releases (#1289) +- *(node)* Further P2P Fixes (#1280) +- *(interop)* Remove new L1BlockInfo variant + deposit context (#1290) + +### Refactor + +- Clap attribute macros from #[clap(...)] to #[arg(...)] and #[command(...)] in v4.x (#1285) + +## [kona-host/v0.1.0-beta.13] - 2025-03-11 + +### 🚀 Features + +- *(node)* P2P CLI Args (#1242) + +### ⚙️ Miscellaneous Tasks + +- Allow udeps in `-Zbuild-std` lints (#1245) +- *(workspace)* Use versioned `asterisc-builder` + `cannon-builder` images (#1243) + +## [kona-host/v0.1.0-beta.12] - 2025-03-11 + +### 🚀 Features + +- *(proof)* EIP-2935 lookback (#1088) +- *(bin-utils)* Add prometheus server initializer (#1100) +- *(node)* Engine Controller (#1136) +- *(node)* Initial orchestration logic (#1166) +- *(registry)* Lookup `Chain` + `RollupConfig` by identifier (#1156) +- *(engine)* Version Providers (#1168) +- *(interop)* Dedup logic for parsing `Log` to `ExecutingMessage` (#1171) +- *(node)* Hook up `RollupNodeBuilder` to CLI (#1179) +- *(book)* Umbrella Crate RFC (#1063) +- *(node)* Derivation actor (#1180) +- *(engine)* Actor + Task Model (#1177) +- *(engine)* FCU Task Updates (#1191) +- *(protocol)* Update `RollupConfig` (#1170) +- *(engine)* Engine Task Cleanup + Insert Payload Task Stub (#1193) +- *(engine)* Insert Task Updates (#1194) +- *(providers-alloy)* Refactor `AlloyChainProvider` (#1203) +- *(providers-alloy)* Refactor `AlloyL2ChainProvider` (#1204) +- *(engine)* Insert New Payload (#1197) +- *(engine)* Wire up Insert Task (#1202) +- *(node)* Add `sync_start` module (#1207) +- *(interop)* Clean up interop validator RPC component (#1172) +- *(node)* Refactor orchestration (#1231) +- *(hardforks)* Isthmus Network Upgrade Transactions (#1080) +- *(node)* P2P Wiring (#1233) + +### 🐛 Bug Fixes + +- *(genesis)* System Config Tests (#1090) +- *(derive)* Use `SystemConfig` batcher key for DAP (#1106) +- *(derive)* Hardfork Deps (#1151) +- 2021 Edition Fragment Specifier (#1155) +- *(ci)* Cargo Deny Checks (#1163) +- *(engine)* Engine Client (#1169) +- *(protocol)* Use `Prague` blob fee calculation for L1 info tx (#1192) +- *(protocol)* Add optional pectra blob fee schedule fork (#1195) +- *(executor)* Dep on kona-host (#1224) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Dependabot Label Update (#1077) +- Crate Shields (#1078) +- *(genesis)* Rename HardForkConfiguration (#1091) +- *(genesis)* Serde Test Types (#1089) +- *(genesis)* Flatten Hardforks in Rollup Config (#1092) +- *(workspace)* Bump MSRV to `1.82` (#1097) +- *(ci)* Split doc lint + doc test jobs +- *(bin)* Split up bin utilities (#1098) +- *(nexus)* Use `kona-bin-utils` (#1099) +- *(workspace)* Adjust build recipes (#1101) +- Cleanup Crate Docs (#1116) +- *(bin)* Rework Node Binary (#1120) +- *(proof-interop)* Adjust `TRANSITION_STATE_MAX_STEPS` (#1144) +- *(rpc)* Remove L2BlockRef (#1140) +- *(book)* Book Cleanup for Node Docs (#1143) +- *(book)* Maili Rename (#1145) +- *(book)* Update Protocol Crate Docs (#1146) +- *(hardforks)* Fix Alloy Reference (#1147) +- *(book)* Cleanup Protocol Docs (#1149) +- *(interop)* Replace Interop Feat Flag (#1150) +- *(workspace)* Bump `rustc` edition to 2024 (#1152) +- *(host)* Replace anyhow with thiserror (#1093) +- *(node)* Move Engine into Crate (#1164) +- *(engine)* Sync Types (#1167) +- *(engine)* Fixup EngineClient (#1173) +- *(workspace)* Updates op-alloy Dependencies (#1174) +- *(engine)* Remove pub mod Visibility Idents (#1175) +- *(workspace)* Move `external` crates to `node` (#1182) +- *(book)* Teeny Update (#1184) +- *(executor)* Fix comments in EIP-2935 syscall module (#1181) +- *(docs)* Update `README.md` (#1186) +- *(registry)* Remove Default Hasher (#1185) +- *(preimage)* Add labels to `README.md` (#1187) +- *(executor)* Add labels to `README.md` (#1188) +- *(host)* Update `README.md` (#1189) +- *(workspace)* Update `README.md` (#1190) +- *(node)* Simplify L1 watcher (#1196) +- Scr updates (#1199) +- Bump op-alloy Deps (#1205) +- Cleanup Other Deps (#1206) +- *(protocol)* RPC Block -> L2BlockInfo (#1176) +- Fix Deny Config (#1212) +- *(node-rpc)* Delete dead code (#1213) +- *(protocol)* Update Sepolia-only fork to activate on L1 blocktime (#1210) +- *(rpc)* Rename `RollupNode` -> `RollupNodeApi`, export (#1215) +- Bump alloy 0.12 (#1208) +- *(genesis)* Update `SystemConfig` ser (#1217) +- *(net)* P2P Rename (#1221) +- Update Dependencies (#1226) +- Codecov Config (#1225) +- *(book)* Small touchups (#1230) +- *(node)* Tracing Macros (#1234) + +### Release + +- *(maili)* 0.2.9 (#1087) +- Maili crates one last time (#1218) +- Kona-driver (#1229) + +## [kona-host/v0.1.0-beta.11] - 2025-02-21 + +### 🚀 Features + +- *(genesis)* Deny Unknown Fields (#1060) + +### 🐛 Bug Fixes + +- *(registry)* Use `superchain-registry` as a submodule (#1075) +- *(workspace)* Exclude Maili Shadows (#1076) + +### ⚙️ Miscellaneous Tasks + +- *(workspace)* Foundry Install Target (#1074) + +## [kona-host/v0.1.0-beta.10] - 2025-02-21 + +### 🐛 Bug Fixes + +- Maili Shadows (#1071) +- Remove Maili Shadows from Workspace (#1072) + +### 📚 Documentation + +- Release Guide (#1067) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Remove Release Plz (#1068) + +## [kona-hardforks-v0.1.0] - 2025-02-21 + +### 🚀 Features + +- *(protocol)* Introduce Hardforks Crate (#1065) + +## [kona-nexus-v0.1.0] - 2025-02-20 + +### 🚀 Features + +- *(bin)* Network Component Runner (#1058) + +## [kona-rpc-v0.1.0] - 2025-02-20 + +### 🐛 Bug Fixes + +- *(ci)* Submodule Sync Crate Path (#1061) + +### ⚙️ Miscellaneous Tasks + +- *(book)* Move the Monorepo Doc to Archives (#1062) + +### Release + +- *(kona-interop)* 0.1.2 (#1066) + +## [kona-serde-v0.1.0] - 2025-02-20 + +### 🚀 Features + +- *(client)* Support cannon mips64r1 (#1054) +- *(client)* Wire up `L2PayloadWitness` hint for single-chain proof (#1034) +- Kona Optimism Monorepo (#1055) + +### 🐛 Bug Fixes + +- Fix type annotations (#1050) +- Exclude kona-net (#1049) +- *(docker)* `mips64` target data layout (#1056) +- *(std-fpvm)* Allow non-const fn with mut ref (#1057) + +### ⚙️ Miscellaneous Tasks + +- Monorepo Proposal Doc (#1036) +- *(book)* RFC and Archives Section (#1053) + +## [kona-net-v0.1.0] - 2025-02-13 + +### ⚙️ Miscellaneous Tasks + +- Bump Dependencies (#1029) +- *(interop)* Remove horizon timestamp (#1028) +- Restructure Kona to be more Extensible (#1031) +- *(host)* Expose private SingleChainHost methods (#1030) +- *(services)* Networking Crate (#1032) + +## [kona-host/v0.1.0-beta.9] - 2025-02-11 + +### 🚀 Features + +- Derive Eq/Ord/Hash for (Archived) PreimageKey(Type) (#956) +- Allow 7702 receipts after Isthmus active (#959) +- Fill eip 7702 tx env with auth list (#958) +- *(executor)* EIP-2935 Syscall Support [ISTHMUS] (#963) +- *(executor)* EIP-7002 Syscall Support [ISTHMUS] (#965) +- *(executor)* EIP-7251 Syscall Support [ISTHMUS] (#968) +- *(executor)* Export receipts (#969) +- *(client)* EIP-2537 BLS12-381 Curve Precompile Acceleration (#960) +- *(host)* Interop optimistic block re-execution hint (#983) +- *(proof-interop)* Support multiple `RollupConfigs` in boot routine (#986) +- *(host)* Re-export default CLIs (#992) +- *(proof-sdk)* Cleanup `Hint` API (#998) +- *(proof-sdk)* Optional L2 chain ID in L2-specific hints (#999) +- *(mpt)* Copy-on-hash (#1001) +- *(host)* Reintroduce `L2BlockData` hint (#1003) +- *(client)* Superchain Consolidation (#1004) +- *(ci)* Coverage for action tests (#1005) +- *(host)* Accelerate all BLS12-381 Precompiles (#1010) +- *(executor)* Sort trie keys (#1016) +- *(host)* Proactive hints (#1017) +- *(ci)* Remove support for features after MSRV (#1018) +- *(interop)* Support full timestamp invariant (#1022) +- Isthmus upgrade txs (#1025) + +### 🐛 Bug Fixes + +- *(executor)* Don't generate a diff when running tests (#967) +- *(executor)* Withdrawals root (#974) +- *(client)* Interop transition rules (#973) +- *(executor)* Removes EIP-7002 and EIP-7251 Pre-block Calls (#990) +- *(ci)* Action tests (#997) +- *(client)* Interop bugfixes (#1006) +- *(client)* No-op sub-transitions in Superchain STF (#1011) +- *(interop)* Check timestamp invariant against executing timestamp AND horizon timestamp (#1024) + +### ⚙️ Miscellaneous Tasks + +- *(docs)* Add `kailua` to the README (#955) +- Maili 0.1.9 (#964) +- *(executor)* Update SpecId with Isthmus (#962) +- *(executor)* TxEnv Stuffing (#970) +- *(executor)* De-duplicate `TrieAccount` type (#977) +- Dep Updates (#980) +- Update Maili Deps (#978) +- Update Dependencies (#988) +- *(host)* Remove `HostOrchestrator` (#994) +- Bump op-alloy dep (#996) +- *(host)* Refactor fetchers (#995) +- Maili Dependency Update (#1007) +- *(client)* Dedup MSM Required Gas Fn (#1012) +- *(client)* Precompile Run Macro (#1014) +- *(ci)* Bump `codecov-action` to v5 (#1020) +- Use Updated Maili and op-alloy Deps (#1023) +- *(book)* Adherence to devdocs (#1026) +- *(book)* Devdocs subdirectory (#1027) + +## [kona-providers-alloy-v0.1.0] - 2025-01-26 + +### 🚀 Features + +- Use empty requests hash when isthmus enabled (#951) +- *(workspace)* Re-introduce `kona-providers-alloy` (#954) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Improve docker releases (#952) + +## [kona-client-v0.1.0-beta.8] - 2025-01-24 + +### 🚀 Features + +- *(driver)* Multi-block derivation (#888) +- *(host)* Interop proof support (part 1) (#910) +- *(client)* Interop consolidation sub-problem (#913) +- *(host)* Modular components (#915) +- *(executor)* New static test harness (#938) +- *(build)* Migrate to `mips64r2` target for `cannon` (#943) + +### 🐛 Bug Fixes + +- *(ci)* Codecov (#911) + +### ⚙️ Miscellaneous Tasks + +- *(mpt)* Remove `anyhow` dev-dependency (#919) +- *(executor)* Remove `anyhow` dev-dependency (#937) + +## [kona-proof-v0.2.3] - 2025-01-16 + +### 🚀 Features + +- *(client)* Interop binary (#903) +- *(host)* Support multiple modes (#904) + +### ⚙️ Miscellaneous Tasks + +- Fix some typos in comment (#906) +- Update Maili Deps (#908) +- Release (#900) + +## [kona-proof-interop-v0.1.0] - 2025-01-14 + +### 🚀 Features + +- *(workspace)* `kona-proof-interop` crate (#902) + +## [kona-interop-v0.1.0] - 2025-01-13 + +### 🚀 Features + +- *(workspace)* `kona-interop` crate (#899) + +## [kona-proof-v0.2.2] - 2025-01-13 + +### 🐛 Bug Fixes + +- Small Spelling Issue (#893) + +### 📚 Documentation + +- Edited the link in the documentation (#895) + +### ⚙️ Miscellaneous Tasks + +- Release v0.2.2 (#891) + +## [kona-client-v0.1.0-beta.7] - 2025-01-09 + +### ⚙️ Miscellaneous Tasks + +- Remove unused function in OnlineBlobProvider (#875) +- *(derive)* Test Ignoring EIP-7702 (#887) +- Bump Maili (#894) + +## [kona-std-fpvm-v0.1.2] - 2025-01-07 + +### 🐛 Bug Fixes + +- Op-rs rename (#883) + +### ⚙️ Miscellaneous Tasks + +- Isthmus Withdrawals Root (#881) +- Remove redundant words in comment (#882) +- Add emhane as a codeowner (#884) +- Bump Dependencies (#880) +- Release (#885) + +## [kona-client-v0.1.0-beta.6] - 2025-01-02 + +### 🚀 Features + +- *(build)* Adjust RV target - `riscv64g` -> `riscv64ima` (#868) +- *(build)* Bump `asterisc-builder` version (#879) + +### 🐛 Bug Fixes + +- *(derive)* Make tests compile (#878) +- *(derive)* `BatchStream` Past batch handling (#876) + +### ⚙️ Miscellaneous Tasks + +- Bump alloy 0.8 (#870) + +### Tooling + +- Make client justfile's commands take an optional rollup_config_path (#869) + +## [kona-client-v0.1.0-beta.5] - 2024-12-04 + +### 🚀 Features + +- *(client)* Re-accelerate precompiles (#866) + +## [kona-std-fpvm-v0.1.1] - 2024-12-04 + +### ⚙️ Miscellaneous Tasks + +- Release (#837) + +## [kona-client-v0.1.0-beta.4] - 2024-12-03 + +### 🐛 Bug Fixes + +- Bump (#855) +- Bump (#865) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Distribute `linux/arm64` `kona-fpp` image (#860) +- Bump Other Dependencies (#856) +- Update deps and clean up misc features (#864) + +## [kona-client-v0.1.0-beta.3] - 2024-12-02 + +### 🚀 Features + +- *(workspace)* Bump MSRV (#859) + +### 🐛 Bug Fixes + +- Nightly lint (#858) + +## [kona-client-v0.1.0-beta.2] - 2024-11-28 + +### 🚀 Features + +- *(host)* Delete unused blob providers (#842) +- *(driver)* Refines the executor interface for the driver (#850) +- *(client)* Invalidate impossibly old claims (#852) +- *(driver)* Wait for engine (#851) + +### 🐛 Bug Fixes + +- Use non problematic hashmap fns (#853) + +### ⚙️ Miscellaneous Tasks + +- *(derive)* Remove indexed blob hash (#847) +- *(driver)* Advance with optional target (#848) +- *(host)* Hint Parsing Cleanup (#844) + +## [kona-std-fpvm-v0.1.0] - 2024-11-26 + +### 🚀 Features + +- *(workspace)* Isolate FPVM-specific platform code (#821) + +### ⚙️ Miscellaneous Tasks + +- *(driver)* Visibility (#834) + +## [kona-proof-v0.1.0] - 2024-11-20 + +### ⚙️ Miscellaneous Tasks + +- Minor release' (#833) + +## [kona-proof-v0.0.1] - 2024-11-20 + +### 🚀 Features + +- *(driver)* Abstract, Default Pipeline (#796) +- *(driver,client)* Pipeline Cursor Refactor (#798) +- *(mpt)* Extend `TrieProvider` in `kona-executor` (#813) +- *(preimage)* Decouple from `kona-common` (#817) +- *(workspace)* `kona-proof` (#818) + +### 🐛 Bug Fixes + +- *(client)* SyncStart Refactor (#797) +- Mdbook version (#810) +- *(mpt)* Remove unused collapse (#808) +- Imports (#829) + +### 📚 Documentation + +- Update providers.md to use new next method instead of old open_data (#809) +- Fix typo in custom-backend.md (#825) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Bump monorepo commit (#805) +- Dispatch book build without cache (#807) +- *(workspace)* Migrate back to `thiserror` v2 (#811) +- *(common)* Rename IO modules (#812) +- *(workspace)* Reorganize SDK (#816) +- V0.6.6 op-alloy (#804) +- *(driver)* Use tracing macros (#822) +- *(driver)* Use tracing macros (#823) +- Op-alloy 0.6.8 (#830) +- *(derive)* Remove batch reader (#826) + +## [kona-driver-v0.0.0] - 2024-11-08 + +### 🚀 Features + +- *(driver)* Introduce driver crate (#794) + +### 🐛 Bug Fixes + +- Remove kona-derive-alloy (#789) + +### ⚙️ Miscellaneous Tasks + +- *(derive)* Re-export types (#790) + +## [kona-mpt-v0.0.6] - 2024-11-06 + +### 🚀 Features + +- *(TrieProvider)* Abstract TrieNode retrieval (#787) + +### 🐛 Bug Fixes + +- *(derive)* Hoist types out of traits (#781) +- *(derive)* Data Availability Provider Abstraction (#782) +- *(derive-alloy)* Test coverage (#785) + +### ⚙️ Miscellaneous Tasks + +- Clean codecov confiv (#783) +- *(derive)* Pipeline error test coverage (#784) +- Bump alloy deps (#788) +- Release (#753) + +## [kona-client-v0.1.0-alpha.7] - 2024-11-05 + +### 🚀 Features + +- *(derive)* Sources docs (#754) +- Flush oracle cache on reorg #724 (#756) +- *(docs)* Derivation Docs (#768) +- *(client)* Remove `anyhow` (#779) +- *(derive)* `From for PipelineErrorKind` (#780) + +### 🐛 Bug Fixes + +- *(derive-alloy)* Changelog (#752) +- Update monorepo (#761) +- *(derive)* Use signal value updated with system config. (#776) +- *(client)* Trace extension support (#778) + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Use `gotestsum` for action tests (#751) +- *(derive)* Cleanup Exports (#757) +- *(derive)* Error Exports (#758) +- *(derive)* Touchup kona-derive readme (#762) +- *(derive-alloy)* Docs (#763) +- *(executor)* Rm upstream util (#755) +- *(ci)* Use `PAT_TOKEN` for automated monorepo pin update (#773) +- *(workspace)* Bump `asterisc` version (#774) +- *(ci)* Update monorepo pin to include Holocene action tests (#775) + +## [kona-mpt-v0.0.5] - 2024-10-29 + +### 🚀 Features + +- *(derive)* Remove metrics (#743) +- Update op-alloy (#745) +- *(derive)* Use upstream op-alloy batch types (#746) + +### 🐛 Bug Fixes + +- Tracing_subscriber problem in `kona-derive` tests (#741) +- *(client)* Don't shadow `executor` in engine retry (#750) + +### ⚙️ Miscellaneous Tasks + +- *(derive)* Import hygiene (#744) +- *(ci)* Don't run `online` tests in CI (#747) +- *(derive-alloy)* Remove metrics (#748) +- Release (#749) + +## [kona-client-v0.1.0-alpha.6] - 2024-10-28 + +### 🚀 Features + +- *(ci)* Bump `go` version for action tests (#730) +- Remove thiserror (#735) +- *(derive)* Sys config accessor (#722) +- *(host)* Remove `MAX_RETRIES` (#739) +- *(host)* Ensure prefetch is falliable (#740) + +### 🐛 Bug Fixes + +- Hashmap (#732) +- *(derive)* Holocene action tests / fixes (#733) +- Add feature for `alloy-provider`, fix `test_util` (#738) + +### ⚙️ Miscellaneous Tasks + +- *(workspace)* Update `asterisc` version to `1.0.3-alpha1` (#729) +- Bump op-alloy version (#731) +- Release (#715) +- *(kona-derive-alloy)* Release v0.0.1 (#736) + +### Docs + +- Update README (#734) + +## [kona-client-v0.1.0-alpha.5] - 2024-10-22 + +### 🚀 Features + +- *(derive)* BatchQueue Update [Holocene] (#601) +- *(derive)* Add `Signal` API (#611) +- *(derive)* Holocene flush signal (#612) +- Frame queue tests (#613) +- *(client)* Pass flush signal (#615) +- *(executor)* Use EIP-1559 parameters from payload attributes (#616) +- *(trusted-sync)* Holocene flush (#617) +- *(primitives)* Blob Test Coverage (#627) +- *(executor)* Update EIP-1559 configurability (#648) +- Codecov Shield (#652) +- Codecov sources (#657) +- Use derive more display (#675) +- *(derive)* `Past` batch validity variant (#684) +- *(derive)* Stage multiplexer (#693) +- *(derive)* Signal receiver logic (#696) +- *(derive)* Add `ChannelAssembler` size limitation (#700) +- Codecov bump threshold to 90 (#674) +- *(executor)* EIP-1559 configurability spec updates (#716) +- *(derive)* `BatchValidator` stage (#703) +- *(workspace)* Distribute pipeline, not providers (#717) +- *(executor)* Clean ups (#719) +- Frame queue test asserter (#619) +- *(derive)* Hoist stage traits (#723) +- *(derive)* `BatchProvider` multiplexed stage (#726) +- *(docker)* Update asterisc reproducible build image (#728) + +### 🐛 Bug Fixes + +- *(ci)* Action tests (#608) +- *(executor)* Holocene EIP-1559 params in Header (#622) +- *(codecov)* Ignore Test Utilities (#628) +- Add codeowners (#635) +- *(providers)* Remove slot derivation (#636) +- *(derive)* Remove unused online mod (#637) +- Codecov (#656) +- *(derive)* Retain L1 blocks (#683) +- Typos (#690) +- *(derive)* Holocene `SpanBatch` prefix checks (#688) +- *(derive)* SpanBatch element limit + channel RLP size limit (#692) +- *(mpt)* Empty root node case (#705) +- *(providers-alloy)* Recycle Beacon Types (#713) + +### ⚙️ Miscellaneous Tasks + +- Doc logos (#609) +- Delete `trusted-sync` (#621) +- Refactor test providers (#623) +- Add Test Coverage (#625) +- Test coverage for common (#629) +- *(derive)* Blob Source Test Coverage (#631) +- *(providers)* Codecov Ignore Alloy-backed Providers (#633) +- *(preimage)* Test Coverage (#634) +- Update deps (#610) +- *(derive)* Single Batch Test Coverage (#643) +- *(derive)* Pipeline Core Test Coverage (#642) +- *(providers-alloy)* Blob provider fallback tests (#644) +- *(mpt)* Account conversion tests (#647) +- *(mpt)* Mpt noop trait impls (#649) +- *(providers)* Add changelog (#653) +- *(derive)* Hoist attributes queue test utils (#654) +- *(mpt)* Codecov (#655) +- *(derive)* Test channel bank reset (#658) +- *(derive)* Test channel reader resets (#660) +- *(derive)* Adds more channel bank coverage (#659) +- *(derive)* Test channel reader flushing (#661) +- *(executor)* Test Coverage over Executor Utilities (#650) +- *(derive)* Batch Timestamp Tests (#664) +- *(client)* Improve `BootInfo` field names (#665) +- *(host)* Improve CLI flag naming (#666) +- *(derive)* Test Stage Resets and Flushes (#669) +- *(derive)* Test and Clean Batch Types (#670) +- *(ci)* Reduce monorepo auto-update frequency (#671) +- *(host)* Support environment variables for `kona-host` flags (#667) +- *(executor)* Use Upstreamed op-alloy Methods (#651) +- *(derive)* Stage coverage (#673) +- *(executor)* Cover Builder (#676) +- *(executor)* Move todo to issue: (#680) +- *(derive)* Remove span batch todo comments (#682) +- *(providers-alloy)* Changelog (#685) +- Remove todos (#687) +- *(host)* Reduce disk<->mem KV proptest runs (#689) +- *(workspace)* Update dependencies + fix build (#702) +- *(derive)* Add tracing to `ChannelAssembler` (#701) +- *(workspace)* Removes Primitives (#638) +- Remove version types (#707) +- Hoist trait test utilities (#708) +- Erradicate anyhow (#712) +- Re-org imports (#711) +- Bump alloy dep minor (#718) + +## [kona-providers-v0.0.1] - 2024-10-02 + +### 🚀 Features + +- Large dependency update (#528) +- *(primitives)* Remove Attributes (#529) +- *(host)* Exit with client status in native mode (#530) +- *(workspace)* Action test runner (#531) +- *(ci)* Add action tests to CI (#533) +- Remove crates.io patch (#537) +- *(derive)* Typed error handling (#540) +- *(mpt)* Migrate to `thiserror` (#541) +- *(preimage/common)* Migrate to `thiserror` (#543) +- *(executor)* Migrate to `thiserror` (#544) +- *(book)* Custom backend, `kona-executor` extensions, and FPVM backend (#552) +- Remove L2 Execution Payload (#542) +- *(derive)* Latest BN (#521) +- *(derive)* Touchup Docs (#555) +- *(derive)* Hoist AttributesBuilder (#571) +- *(derive)* New BatchStream Stage for Holocene (#566) +- *(derive)* Wire up the batch span stage (#567) +- *(derive)* Holocene Activation (#574) +- *(derive)* Holocene Frame Queue (#579) +- *(derive)* Holocene Channel Bank Checks (#572) +- *(derive)* Holocene Buffer Flushing (#575) +- *(ci)* Split online/offline tests (#582) +- *(derive)* Interleaved channel tests (#585) +- *(derive)* Refactor out Online Providers (#569) +- *(derive)* BatchStreamProvider (#591) +- *(derive)* `BatchStream` buffering (#590) +- *(derive)* Span batch prefix checks (#592) +- Kona-providers (#596) +- Monorepo Pin Update (#604) +- *(derive)* Bump op-alloy dep (#605) + +### 🐛 Bug Fixes + +- *(derive)* Sequence window expiry (#532) +- *(preimage)* Improve error differentiation in preimage servers (#535) +- *(client)* Channel reader error handling (#539) +- *(client)* Continue derivation on execution failure (#545) +- *(derive)* Move attributes builder trait (#570) +- *(workspace)* Hoist and fix lints (#577) +- Derive pipeline params (#587) + +### ⚙️ Miscellaneous Tasks + +- *(host)* Make `l2-chain-id` optional if a rollup config was passed. (#534) +- *(host)* Clean up CLI (#538) +- *(workspace)* Bump MSRV to `1.81` (#546) +- *(ci)* Delete program diff job (#547) +- *(workspace)* Allow stdlib in `cfg(test)` (#548) +- *(workspace)* Bump dependencies (#550) +- *(readme)* Remove `kona-plasma` link (#551) +- Channel reader docs (#568) +- *(workspace)* `just lint` (#584) +- *(derive)* [Holocene] Drain previous channel in one iteration (#583) +- Use alloy primitives map (#586) +- *(ci)* Pin action tests monorepo rev (#603) + +## [kona-client-v0.1.0-alpha.3] - 2024-09-10 + +### 🚀 Features + +- *(host)* Add `TryFrom` for `MemoryKeyValueStore` (#512) +- Expose store (#513) +- *(ci)* Release prestate build image (#523) + +### ⚙️ Miscellaneous Tasks + +- *(primitives)* Rm RawTransaction (#505) +- Bumps Dependency Versions (#520) +- *(release)* Default to `amd64` platform on prestate artifacts build (#519) + +## [kona-client-v0.1.0-alpha.2] - 2024-09-06 + +### 🚀 Features + +- *(host)* Use `RocksDB` as the disk K/V store (#471) +- *(primitives)* Reuse op-alloy-protocol channel and block types (#499) + +### 🐛 Bug Fixes + +- *(primitives)* Re-use op-alloy frame type (#492) +- *(mpt)* Empty list walker (#493) +- *(ci)* Remove `PAT_TOKEN` ref (#494) +- *(primitives)* Use consensus hardforks (#497) + +### ⚙️ Miscellaneous Tasks + +- *(docker)* Update prestate builder image (#502) + +## [kona-primitives-v0.0.2] - 2024-09-04 + +### 🚀 Features + +- Increase granularity (#365) +- *(examples)* Log payload attributes on error (#371) +- *(examples)* Add metric for latest l2 reference safe head update (#375) +- *(trusted-sync)* Re-org walkback (#379) +- *(client)* Providers generic over oracles (#336) +- Add zkvm target for io (#394) +- *(derive+trusted-sync)* Online blob provider with fallback (#410) +- *(client)* Generic DerivationDriver over any BlobProvider (#412) +- *(ci)* Add scheduled FPP differential tests (#408) +- *(kdn)* Derivation Test Runner for kona-derive (#414) +- *(client+host)* Dynamic `RollupConfig` in bootloader (#439) +- *(kt)* `kdn` -> `kt`, prep for multiple test formats (#445) +- *(client)* Export `CachingOracle` (#455) +- *(primitives)* `serde` for `L1BlockInfoTx` (#460) +- Update superchain registry deps (#463) +- *(workspace)* Workspace Re-exports (#468) +- *(executor)* Expose full revm Handler (#475) +- *(client)* Granite `ecPairing` precompile limit (#479) +- Run cargo hack against workspace (#485) + +### 🐛 Bug Fixes + +- Trusted-sync metrics url (#363) +- Docker image metrics url set (#364) +- *(examples)* L2 safe head tracking (#373) +- *(examples)* Reduce Origin Advance to Warn (#372) +- *(actions)* Trusted sync docker publish (#376) +- Drift reset (#381) +- Drift Walkback (#382) +- *(derive)* Pipeline Reset (#383) +- Bubble up validation errors (#388) +- Pin two dependencies due to upstream semver issues (#391) +- Don't hold onto intermediate execution cache across block boundaries (#396) +- *(kona-derive)* Remove SignedRecoverable Shim (#400) +- *(deps)* Bump Alloy Dependencies (#409) +- Remove data iter option (#405) +- *(examples)* Backoff trusted-sync invalid payload retries (#411) +- *(trusted-sync)* Remove Panics (#413) +- *(kona-host)* Set explicit types (#421) +- *(derive)* Granite Hardfork Support (#420) +- *(host)* Backoff after `MAX_RETRIES` (#429) +- Fix superchain registry + primitives versions (#425) +- Broken link in readme (#432) +- Link to section (#419) +- *(kdn)* Update with Repository Rename (#441) +- *(kdn)* Updates `kdn` with op-test-vectors Generic Typing (#444) +- *(client)* Bootinfo serde (#448) +- *(derive)* Remove fpvm tests (#447) +- *(workspace)* Add Unused Dependency Lint (#453) +- Downgrade for release plz (#458) +- *(workspace)* Use published `revm` version (#459) +- *(client)* Walkback Channel Timeout (#456) +- *(client)* Break when the pipeline cannot advance (#478) +- Deprecate --all (#484) +- *(host)* Insert empty MPT root hash (#483) +- *(examples)* Revm Features (#482) + +### 🧪 Testing + +- *(derive)* Channel timeout (#437) + +### ⚙️ Miscellaneous Tasks + +- *(derive)* Refine channel frame count buckets (#378) +- *(common)* Remove need for cursors in `NativeIO` (#416) +- *(examples)* Add logs to trusted-sync (#415) +- *(derive)* Remove previous stage trait (#423) +- *(workspace)* Remove `minimal` and `simple-revm` examples (#430) +- *(client)* Ensure p256 precompile activation (#431) +- *(client)* Isolate FPVM-specific constructs (#435) +- *(common-proc)* Suppress doc warning (#436) +- *(host)* Remove TODOs (#438) +- Bump scr version (#440) +- *(workspace)* Remove `kona-plasma` (#443) +- Refactor types out of kona-derive (#454) +- *(bin)* Remove `kt` (#461) +- *(derive)* Remove udeps (#462) +- *(derive)* Reset docs (#464) +- *(workspace)* Reorg Workspace Manifest (#465) +- *(workspace)* Hoist Dependencies (#466) +- *(workspace)* Update for `anton-rs` org transfer (#474) +- *(workspace)* Fix `default-features` in workspace root (#472) +- *(workspace)* Alloy Version Bumps (#467) +- *(ci)* Configure codecov patch job (#477) +- Release (#476) + +## [kona-client-v0.1.0-alpha.1] - 2024-07-09 + +### 🚀 Features + +- *(examples)* Trusted Sync Metrics (#308) +- *(derive)* Stage Level Metrics (#309) +- *(build)* Dockerize trusted-sync (#299) +- *(examples)* Pipeline step metrics (#320) +- *(examples)* Send Logs to Loki (#321) +- *(derive)* Granular Provider Metrics (#325) +- *(derive)* More stage metrics (#326) +- *(derive)* Track the current channel size (#331) +- *(derive)* Histogram for number of channels for given frame counts (#337) +- *(executor)* Builder pattern for `StatelessL2BlockExecutor` (#339) +- *(executor)* Generic precompile overrides (#340) +- *(client)* `ecrecover` accelerated precompile (#342) +- *(client)* `ecpairing` accelerated precompile (#343) +- *(client)* KZG point evaluation accelerated precompile (#344) +- *(executor)* `StatelessL2BlockExecutor` benchmarks (#350) +- *(docker)* Reproducible `asterisc` prestate (#357) +- *(ci)* Run Host + Client natively in offline mode (#355) +- *(mpt)* `TrieNode` benchmarks (#351) +- *(ci)* Build benchmarks in CI (#352) + +### 🐛 Bug Fixes + +- Publish trusted-sync to GHCR (#312) +- *(ci)* Publish trusted sync docker (#314) +- *(derive)* Warnings with metrics macro (#322) +- *(examples)* Small cli fix (#323) +- *(examples)* Don't panic on validation fetch failure (#327) +- *(derive)* Prefix all metric names (#330) +- *(derive)* Bind the Pipeline trait to Iterator (#334) +- *(examples)* Reset Failed Payload Derivation Metric (#338) +- *(examples)* Justfile fixes (#341) +- *(derive)* Unused var w/o `metrics` feature (#345) +- *(examples)* Dockerfile fixes (#347) +- *(examples)* Start N Blocks Back from Tip (#349) + +### ⚙️ Miscellaneous Tasks + +- *(client)* Improve justfile (#305) +- *(derive)* Add targets to stage logs (#310) +- *(docs)* Label Cleanup (#307) +- Bump `superchain-registry` version (#306) +- *(derive)* Remove noisy batch logs (#329) +- *(preimage)* Remove dynamic dispatch (#354) +- *(host)* Make `exec` flag optional (#356) +- *(docker)* Pin `asterisc-builder` version in reproducible prestate builder (#362) + +## [kona-primitives-v0.0.1] - 2024-06-22 + +### ⚙️ Miscellaneous Tasks + +- Pin op-alloy-consensus (#304) + +## [kona-common-v0.0.2] - 2024-06-22 + +### 🚀 Features + +- *(precompile)* Add `precompile` key type (#179) +- *(preimage)* Async server components (#183) +- *(host)* Host program scaffold (#184) +- *(host)* Disk backed KV store (#185) +- *(workspace)* Add aliases in root `justfile` (#191) +- *(host)* Add local key value store (#189) +- *(preimage)* Async client handles (#200) +- *(mpt)* Trie node insertion (#195) +- *(mpt)* Trie DB commit (#196) +- *(mpt)* Simplify `TrieDB` (#198) +- *(book)* Add minimal program stage documentation (#202) +- *(mpt)* Block hash walkback (#199) +- Refactor reset provider (#207) +- Refactor the pipeline builder (#209) +- *(client)* `BootInfo` (#205) +- Minimal ResetProvider Implementation (#208) +- Pipeline Builder (#217) +- *(client)* `StatelessL2BlockExecutor` (#210) +- *(ci)* Add codecov (#233) +- *(ci)* Dependabot config (#236) +- *(kona-derive)* Updated interface (#230) +- *(client)* Add `current_output_root` to block executor (#225) +- *(client)* Account + Account storage hinting in `TrieDB` (#228) +- *(plasma)* Online Plasma Input Fetcher (#167) +- *(host)* More hint routes (#232) +- *(kona-derive)* Towards Derivation (#243) +- *(client)* Add `RollupConfig` to `BootInfo` (#251) +- *(client)* Oracle-backed derive traits (#252) +- *(client/host)* Oracle-backed Blob fetcher (#255) +- *(client)* Derivation integration (#257) +- *(preimage)* Add serde feature flag to preimage crate for keys (#271) +- *(fjord)* Fjord parameter changes (#284) + +### 🐛 Bug Fixes + +- *(ci)* Run CI on `pull_request` and `merge_group` triggers (#186) +- *(primitives)* Use decode_2718() to gracefully handle the tx type (#182) +- Strong Error Typing (#187) +- *(readme)* CI badges (#190) +- *(host)* Blocking native client program (#201) +- *(derive)* Alloy EIP4844 Blob Type (#215) +- Derivation Pipeline (#220) +- Use 2718 encoding (#231) +- *(ci)* Do not run coverage in merge queue (#239) +- *(kona-derive)* Reuse upstream reqwest provider (#229) +- Output root version to 32 bytes (#248) +- *(examples)* Clean up trusted sync logging (#263) +- Type re-exports (#280) +- *(common)* Pipe IO support (#282) +- *(examples)* Dynamic Rollup Config Loading (#293) +- Example dep feature (#297) +- *(derive)* Fjord brotli decompression (#298) +- *(mpt)* Fix extension node truncation (#300) + +### ⚙️ Miscellaneous Tasks + +- *(common)* Use `Box::leak` rather than `mem::forget` (#180) +- *(derive)* Data source unit tests (#181) +- *(ci)* Workflow trigger changes (#203) +- *(mpt)* Do not expose recursion vars (#197) +- Use alloy withdrawal type (#213) +- *(host)* Simplify host program (#206) +- Update README (#227) +- *(kona-derive)* Online Pipeline Cleanup (#241) +- *(workspace)* `kona-executor` (#259) +- *(derive)* Sources Touchups (#266) +- *(derive)* Online module touchups (#265) +- *(derive)* Cleanup pipeline tracing (#264) +- Update `README.md` (#269) +- Re-export input types (#279) +- *(client)* Add justfile for running client program (#283) +- *(ci)* Remove codecov from binaries (#285) +- Payload decoding tests (#289) +- Payload decoding tests (#287) +- *(workspace)* Reorganize binary example programs (#294) +- Version dependencies (#296) +- *(workspace)* Prep release (#301) +- Release (#302) + +## [kona-common-proc-v0.0.1] - 2024-05-23 + +### 🚀 Features + +- *(primitives)* Kona-derive type refactor (#135) +- *(derive)* Pipeline Builder (#127) +- *(plasma)* Implements Plasma Support for kona derive (#152) +- *(derive)* Online Data Source Factory Wiring (#150) +- *(derive)* Abstract Alt DA out of `kona-derive` (#156) +- *(derive)* Return the concrete online attributes queue type from the online stack constructor (#158) +- *(primitives)* Move attributes into primitives (#163) +- *(mpt)* Refactor `TrieNode` (#172) +- *(mpt)* `TrieNode` retrieval (#173) +- *(mpt)* `TrieCacheDB` scaffold (#174) +- *(workspace)* Client programs in workspace (#178) + +### 🐛 Bug Fixes + +- *(workspace)* Release plz (#138) +- *(derive)* Small Fixes and Span Batch Validation Fix (#139) +- *(derive)* Move span batch conversion to try from trait (#142) +- *(ci)* Release plz (#145) +- *(derive)* Remove unnecessary online feature decorator (#160) +- *(plasma)* Reduce plasma source generic verbosity (#165) +- *(plasma)* Plasma Data Source Cleanup (#164) +- *(derive)* Ethereum Data Source (#159) +- *(derive)* Fix span batch utils read_tx_data() (#170) +- *(derive)* Inline blob verification into the blob provider (#175) + +### ⚙️ Miscellaneous Tasks + +- *(workspace)* Exclude all crates except `kona-common` from cannon/asterisc lint job (#168) +- *(host)* Split CLI utilities out from binary (#169) + +## [kona-mpt-v0.0.1] - 2024-04-24 + +### 🚀 Features + +- L1 traversal (#39) +- Add `TxDeposit` type (#40) +- Add OP receipt fields (#41) +- System config update event parsing (#42) +- L1 retrieval (#44) +- Frame queue stage (#45) +- *(derive)* Channel bank (#46) +- Single batch type (#43) +- Data sources +- Clean up data sources to use concrete bytes type +- Async iterator and cleanup +- Fix async iterator issue +- *(derive)* Most of blob data source impl +- *(derive)* Fill blob pointers +- *(derive)* Blob decoding +- *(derive)* Test Utilities (#62) +- *(derive)* Share the rollup config across stages using an arc +- *(ci)* Add workflow to cycle issues (#73) +- *(derive)* Channel Reader Implementation (#65) +- *(types)* Span batches +- *(derive)* Raw span type refactoring +- *(derive)* Fixed bytes and encoding +- *(derive)* Refactor serialization; `SpanBatchPayload` WIP +- *(derive)* Derive raw batches, mocks +- *(derive)* `add_txs` function +- *(derive)* Reorganize modules +- *(derive)* `SpanBatch` type implementation WIP +- *(workspace)* Add `rustfmt.toml` +- *(derive)* Initial pass at telemetry +- *(derive)* Add signature protection check in `SpanBatchTransactions` +- *(derive)* Batch type for the channel reader +- *(derive)* Channel reader implementation with batch reader +- *(derive)* Batch queue +- *(derive)* Basic batch queue next batch derivation +- *(derive)* Finish up batch derivation +- *(derive)* Attributes queue stage +- *(derive)* Add next_attributes test +- *(derive)* Use upstream alloy (#89) +- *(derive)* Add `ecrecover` trait + features (#90) +- *(derive)* Batch Queue Logging (#86) +- *(common)* Move from `RegisterSize` to native ptr size type (#95) +- *(preimage)* `OracleServer` + `HintReader` (#96) +- *(derive)* Move to `tracing` for telemetry (#94) +- *(derive)* Online `ChainProvider` (#93) +- *(derive)* Payload Attribute Building (#92) +- *(derive)* Add `L1BlockInfoTx` (#100) +- *(derive)* `L2ChainProvider` w/ `op-alloy-consensus` (#98) +- *(derive)* Build `L1BlockInfoTx` in payload builder (#102) +- *(derive)* Deposit derivation testing (#115) +- *(derive)* Payload builder tests (#106) +- *(derive)* Online Blob Provider (#117) +- *(derive)* Use `L2ChainProvider` for system config fetching in attributes builder (#123) +- *(derive)* Span Batch Validation (#121) +- `kona-mpt` crate (#128) + +### 🐛 Bug Fixes + +- *(derive)* Small l1 retrieval doc comment fix (#61) +- *(derive)* Review cleanup +- *(derive)* Vec deque +- *(derive)* Remove k256 feature +- *(derive)* Async iterator type with data sources +- Result wrapping iterator item +- *(derive)* More types +- *(derive)* Span type encodings and decodings +- *(derive)* Span batch tx rlp +- *(derive)* Bitlist alignment +- *(derive)* Refactor span batch tx types +- *(derive)* Refactor tx enveloped +- *(derive)* Data sources upstream conflicts +- *(derive)* Hoist params from types +- *(derive)* Formatting +- *(derive)* Batch type lints +- *(derive)* Channel bank impl +- *(derive)* Channel reader lints +- *(derive)* Single batch validation +- *(derive)* Merge upstream changes +- *(derive)* Rebase +- *(derive)* Frame queue error bubbling and docs +- *(derive)* Clean up frame queue docs +- *(derive)* L1 retrieval docs (#80) +- *(derive)* Frame Queue Error Bubbling and Docs (#82) +- *(derive)* Fix bricked arc stage param construction (#84) +- *(derive)* Merge upstream changes +- *(derive)* Hoist params +- *(derive)* Upstream merge +- *(derive)* Clean up the channel bank and add tests +- *(derive)* Channel bank tests +- *(derive)* Channel bank testing with spinlocked primitives +- *(derive)* Rebase +- *(derive)* Omit the engine queue stage +- *(derive)* Attributes queue +- *(derive)* Rework abstractions and attributes queue testing +- *(derive)* Error equality fixes and tests +- *(derive)* Successful payload attributes building tests +- *(derive)* Extend attributes queue unit test +- *(derive)* Impl origin provider trait across stages +- *(derive)* Lints +- *(derive)* Add back removed test +- *(derive)* Stage Decoupling (#88) +- *(derive)* Derive full `SpanBatch` in channel reader (#97) +- *(derive)* Doc Touchups and Telemetry (#105) +- *(readme)* Remove blue highlights (#116) +- *(derive)* Span batch bitlist encoding (#122) +- *(derive)* Rebase span batch validation tests (#125) +- *(workspace)* Release plz (#137) + +### ⚙️ Miscellaneous Tasks + +- Scaffold (#37) +- *(derive)* Clean up RLP encoding + use `TxType` rather than ints +- *(derive)* Rebase + move `alloy` module +- *(derive)* Channel reader tests + fixes, batch type fixes +- *(derive)* L1Traversal Doc and Test Cleanup (#79) +- *(derive)* Cleanups (#91) +- *(workspace)* Cleanup justfiles (#104) +- *(ci)* Fail CI on doclint failure (#101) +- *(workspace)* Move `alloy-primitives` to workspace dependencies (#103) + +### Dependabot + +- Upgrade mio (#63) + +### Wip + +- *(derive)* `RawSpanBatch` diff decoding/encoding test + +## [kona-preimage-v0.0.1] - 2024-02-22 + +### 🐛 Bug Fixes + +- Specify common version (#32) + +## [kona-common-v0.0.1] - 2024-02-22 + +### 🚀 Features + +- `release-plz` release pipeline (#27) +- `release-plz` release pipeline (#29) + + diff --git a/kona/CLAUDE.md b/kona/CLAUDE.md new file mode 100644 index 0000000000000..5e40a1b76a605 --- /dev/null +++ b/kona/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands +- Build workspace: `just b` or `just build-native` +- Lint: `just l` or `just lint-native` +- Format: `just f` or `just fmt-native-fix` +- Run all tests: `just t` or `just tests` +- Run specific test: `cargo nextest run --package [package-name] --test [test-name]` +- Run single test: `cargo nextest run --package [package-name] --test [test-name] -- [test_function_name]` +- Documentation: `just test-docs` + +## Code Style +- MSRV: 1.88 +- Format with nightly rustfmt: `cargo +nightly fmt` +- Imports: organized by crate, reordered automatically +- Error handling: use proper error types, prefer `Result` over panics +- Naming: follow Rust conventions (snake_case for variables/functions, CamelCase for types) +- Prefer type-safe APIs and strong typing +- Documentation: rustdoc for public APIs, clear comments for complex logic +- Tests: write unit and integration tests for all functionality +- Performance: be mindful of allocations and copying, prefer references where appropriate +- No warnings policy: all clippy warnings are treated as errors (-D warnings) + +## Architecture Overview + +Kona is a monorepo for OP Stack types, components, and services built in Rust. The repository is organized into several major categories: + +### Binaries (`bin/`) +- **`client`**: The fault proof program that executes state transitions on a prover +- **`host`**: Native program serving as the Preimage Oracle server +- **`node`**: Rollup Node implementation with flexible chain ID support +- **`supervisor`**: Supervisor implementation for interop coordination + +### Protocol (`crates/protocol/`) +- **`derive`**: `no_std` compatible derivation pipeline implementation +- **`protocol`**: Core protocol types used across OP Stack rust crates +- **`genesis`**: Genesis types for OP Stack chains +- **`interop`**: Core functionality for OP Stack Interop features +- **`registry`**: Rust bindings for superchain-registry +- **`comp`**: Compression types and utilities +- **`hardforks`**: Consensus layer hardfork types and network upgrade transactions + +### Proof (`crates/proof/`) +- **`executor`**: `no_std` stateless block executor +- **`proof`**: High-level OP Stack state transition proof SDK +- **`proof-interop`**: Extension of `kona-proof` with interop support +- **`mpt`**: Merkle Patricia Trie utilities for client program +- **`preimage`**: High-level PreimageOracle ABI interfaces +- **`std-fpvm`**: Platform-specific Fault Proof VM kernel APIs +- **`driver`**: Stateful derivation pipeline driver + +### Node (`crates/node/`) +- **`service`**: OP Stack rollup node service implementation +- **`engine`**: Extensible rollup node engine client +- **`rpc`**: OP Stack RPC types and extensions +- **`p2p`**: P2P networking including Gossip and Discovery +- **`sources`**: Data source types and utilities + +### Supervisor (`crates/supervisor/`) +- **`core`**: Core supervisor functionality +- **`service`**: Supervisor service implementation +- **`rpc`**: Supervisor RPC types and client +- **`storage`**: Database storage layer +- **`types`**: Common types for supervisor components + +### Development Workflow + +1. **Testing**: The project uses `nextest` for test execution. Online tests are excluded by default and can be run separately with `just test-online` +2. **Cross-compilation**: Docker-based builds for `cannon` (MIPS) and `asterisc` (RISC-V) targets +3. **Documentation**: Both rustdoc and a separate documentation site at rollup.yoga +4. **Monorepo Integration**: Pins and integrates with the Optimism monorepo for action tests + +### Key Configuration Files +- `rust-toolchain.toml`: Pins Rust version to 1.88 +- `rustfmt.toml`: Custom formatting configuration with crate-level import grouping +- `clippy.toml`: MSRV configuration for clippy +- `deny.toml`: Dependency auditing and license compliance +- `release.toml`: Configuration for `cargo-release` tool + +### Target Architecture Support +- Native development on standard platforms +- Cross-compilation support for fault proof VMs: + - MIPS64 (cannon target) + - RISC-V (asterisc target) +- `no_std` compatibility for proof components + +### Dependencies and Features +- Heavy use of Alloy ecosystem for Ethereum types +- OP-specific extensions via op-alloy +- Modular feature flags for different compilation targets +- Workspace-level dependency management with version pinning diff --git a/kona/CONTRIBUTING.md b/kona/CONTRIBUTING.md new file mode 100644 index 0000000000000..4d8ccfa04c0fe --- /dev/null +++ b/kona/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Thank you for wanting to contribute! Before contributing to this repository, +please read through this document and discuss the change you wish to make via issue. + +## Dependencies + +Before working with this repository locally, you'll need to install the following dependencies. + +- [just][just] for our command-runner scripts. +- The [Rust toolchain][rust] + +## Pull Request Process + +1. Before anything, [create an issue][create-an-issue] to discuss the change you're + wanting to make, if it is significant or changes functionality. Feel free to skip this step for trivial changes. +1. Once your change is implemented, ensure that all checks are passing before creating a PR. The full CI pipeline can + be run locally via the `justfile`s in the repository. +1. Make sure to update any documentation that has gone stale as a result of the change, in the `README` files, the [book][book], + and in rustdoc comments. +1. Once you have sign-off from a maintainer, you may merge your pull request yourself if you have permissions to do so. + If not, the maintainer who approves your pull request will add it to the merge queue. + + + +[just]: https://github.com/casey/just +[rust]: https://rustup.rs/ + +[book]: https://rollup.yoga + +[create-an-issue]: https://github.com/op-rs/kona/issues/new diff --git a/kona/Cargo.lock b/kona/Cargo.lock new file mode 100644 index 0000000000000..160d6d3e2381a --- /dev/null +++ b/kona/Cargo.lock @@ -0,0 +1,12753 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy-chains" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8c665521d11efbb11d5e5c5d63971426bb63df00d24545baf97e7f3dc91c0c" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "num_enum", + "proptest", + "serde", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "arbitrary", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "arbitrary", + "serde", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "crc", + "rand 0.8.5", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "borsh", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "borsh", + "k256", + "rand 0.8.5", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eips" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "arbitrary", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "ethereum_ssz", + "ethereum_ssz_derive", + "serde", + "serde_with", + "sha2", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-evm" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527b47dc39850c6168002ddc1f7a2063e15d26137c1bb5330f6065a7524c1aa9" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-op-hardforks", + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-sol-types", + "auto_impl", + "derive_more", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 12.0.2", + "revm 31.0.2", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-evm" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01be36ba6f5e6e62563b369e03ca529eac46aea50677f84655084b4750816574" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-op-hardforks", + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-sol-types", + "auto_impl", + "derive_more", + "op-alloy", + "op-revm 14.1.0", + "revm 33.1.0", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-genesis" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba4b1be0988c11f0095a2380aa596e35533276b8fa6c9e06961bbfe0aebcac5" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "borsh", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9a33550fc21fd77a3f8b63e99969d17660eec8dcc50a95a80f7c9964f7680b" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", + "serde", +] + +[[package]] +name = "alloy-json-abi" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-op-evm" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eea81517a852d9e3b03979c10febe00aacc3d50fbd34c5c30281051773285f7" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-op-hardforks", + "alloy-primitives", + "auto_impl", + "op-alloy-consensus 0.22.4", + "op-revm 12.0.2", + "revm 31.0.2", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-op-evm" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231262d7e06000f3fb642d32d38ca75e09e78e04977c10be0a07a5ee2c869cfd" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.24.2", + "alloy-op-hardforks", + "alloy-primitives", + "auto_impl", + "op-alloy", + "op-revm 14.1.0", + "revm 33.1.0", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-op-hardforks" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f96fb2fce4024ada5b2c11d4076acf778a0d3e4f011c6dfd2ffce6d0fcf84ee9" +dependencies = [ + "alloy-chains", + "alloy-hardforks", + "alloy-primitives", + "auto_impl", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" +dependencies = [ + "alloy-rlp", + "arbitrary", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash 0.2.0", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "indexmap 2.12.1", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "proptest-derive 0.6.0", + "rand 0.9.2", + "ruint", + "rustc-hash 2.1.1", + "serde", + "sha3", + "tiny-keccak", +] + +[[package]] +name = "alloy-provider" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-pubsub", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-signer", + "alloy-sol-types", + "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", + "async-stream", + "async-trait", + "auto_impl", + "dashmap", + "either", + "futures", + "futures-utils-wasm", + "lru 0.13.0", + "parking_lot", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-pubsub" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd4c64eb250a18101d22ae622357c6b505e158e9165d4c7974d59082a600c5e" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "auto_impl", + "bimap", + "futures", + "parking_lot", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "wasmtimer", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "alloy-rpc-client" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-pubsub", + "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", + "futures", + "pin-project", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cf1398cb33aacb139a960fa3d8cf8b1202079f320e77e952a0b95967bf7a9f" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-debug", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-beacon" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16633087e23d8d75161c3a59aa183203637b817a5a8d2f662f612ccb6d129af0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "derive_more", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4936f579d9d10eae01772b2ab3497f9d568684f05f26f8175e12f9a1a2babc33" +dependencies = [ + "alloy-primitives", + "derive_more", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c60bdce3be295924122732b7ecd0b2495ce4790bedc5370ca7019c08ad3f26e" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "arbitrary", + "derive_more", + "ethereum_ssz", + "ethereum_ssz_derive", + "jsonwebtoken", + "rand 0.8.5", + "serde", + "strum", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "arbitrary", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-serde" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" +dependencies = [ + "alloy-primitives", + "arbitrary", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-signer-local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.12.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.111", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-transport" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" +dependencies = [ + "alloy-json-rpc", + "auto_impl", + "base64", + "derive_more", + "futures", + "futures-utils-wasm", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-transport-http" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" +dependencies = [ + "alloy-json-rpc", + "alloy-rpc-types-engine", + "alloy-transport", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "jsonwebtoken", + "reqwest", + "serde_json", + "tower 0.5.2", + "tracing", + "url", +] + +[[package]] +name = "alloy-transport-ipc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8db249779ebc20dc265920c7e706ed0d31dbde8627818d1cbde60919b875bb0" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad2344a12398d7105e3722c9b7a7044ea837128e11d453604dec6e3731a86e2" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "rustls", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + +[[package]] +name = "alloy-trie" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "arrayvec", + "derive_arbitrary", + "derive_more", + "nybbles", + "proptest", + "proptest-derive 0.5.1", + "serde", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arbtest" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3be567977128c0f71ad1462d9624ccda712193d124e944252f0c5789a06d46" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-r1cs-std", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-relations", + "ark-std 0.5.0", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff 0.5.0", + "ark-std 0.5.0", + "tracing", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "asn1_der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07a926debf178f2d355197f9caddb08e54a9329d44748034bba349c5848cb519" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version 0.4.1", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "aurora-engine-modexp" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen 0.69.5", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.16", + "instant", + "rand 0.8.5", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.111", + "which", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.111", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.111", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "buddy_system_allocator" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0108968a3a2dab95b089c0fc3f1afa7759aa5ebe6f1d86d206d6f7ba726eb" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "c-kzg" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +dependencies = [ + "arbitrary", + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.27", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a3cbbb8b6eca96f3a5c4bf6938d5b27ced3675d69f95bb51948722870bc323" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-hex" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.111", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "delay_map" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e365f083a5cb5972d50ce8b1b2c9f125dc5ec0f50c0248cfb568ae59efcf0b" +dependencies = [ + "futures", + "tokio", + "tokio-util", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.111", +] + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.111", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "discv5" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f170f4f6ed0e1df52bf43b403899f0081917ecf1500bfe312505cc3b515a8899" +dependencies = [ + "aes", + "aes-gcm", + "alloy-rlp", + "arrayvec", + "ctr", + "delay_map", + "enr", + "fnv", + "futures", + "hashlink", + "hex", + "hkdf", + "lazy_static", + "libp2p-identity", + "lru 0.12.5", + "more-asserts", + "multiaddr", + "parking_lot", + "rand 0.8.5", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", + "uint 0.10.0", + "zeroize", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enr" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" +dependencies = [ + "alloy-rlp", + "base64", + "bytes", + "ed25519-dalek", + "hex", + "k256", + "log", + "rand 0.8.5", + "serde", + "sha3", + "zeroize", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ethereum_serde_utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" +dependencies = [ + "alloy-primitives", + "hex", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "ethereum_ssz" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dcddb2554d19cde19b099fadddde576929d7a4d0c1cd3512d1fd95cf174375c" +dependencies = [ + "alloy-primitives", + "ethereum_serde_utils", + "itertools 0.13.0", + "serde", + "serde_derive", + "smallvec", + "typenum", +] + +[[package]] +name = "ethereum_ssz_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a657b6b3b7e153637dc6bdc6566ad9279d9ee11a15b12cfb24a2e04360637e9f" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "example-discovery" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "discv5", + "kona-cli", + "kona-disc", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "example-gossip" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "discv5", + "kona-cli", + "kona-disc", + "kona-node-service", + "kona-registry", + "libp2p", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "execution-fixture" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "kona-cli", + "kona-executor", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", + "url", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "git2" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +dependencies = [ + "bitflags 2.10.0", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f8970a75c006bb2f8ae79c6768a116dd215fa8346a87aed99bf9d82ca43394" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "socket2 0.5.10", + "thiserror 2.0.17", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if-addrs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "if-watch" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" +dependencies = [ + "async-io", + "core-foundation 0.9.4", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration", + "tokio", + "windows 0.53.0", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.2", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "arbitrary", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inferno" +version = "0.11.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" +dependencies = [ + "ahash", + "indexmap 2.12.1", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" +dependencies = [ + "base64", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.17", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "pin-project", + "rand 0.9.2", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower 0.5.2", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower 0.5.2", + "url", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "kona-cli" +version = "0.3.2" +dependencies = [ + "alloy-chains", + "alloy-primitives", + "clap", + "kona-genesis", + "kona-registry", + "libc", + "libp2p", + "metrics-exporter-prometheus 0.18.1", + "metrics-process", + "rstest", + "serde", + "thiserror 2.0.17", + "tracing", + "tracing-appender", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "kona-client" +version = "1.0.2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.24.2", + "alloy-op-evm 0.24.2", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "async-trait", + "cfg-if", + "kona-derive", + "kona-driver", + "kona-executor", + "kona-genesis", + "kona-interop", + "kona-mpt", + "kona-preimage", + "kona-proof", + "kona-proof-interop", + "kona-protocol", + "kona-registry", + "kona-std-fpvm", + "kona-std-fpvm-proc", + "lru 0.16.2", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 14.1.0", + "revm 33.1.0", + "serde", + "serde_json", + "sha2", + "spin 0.10.0", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-comp" +version = "0.4.5" +dependencies = [ + "alloc-no-stdlib", + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "alloy-serde", + "alloy-sol-types", + "arbitrary", + "async-trait", + "brotli", + "kona-genesis", + "kona-protocol", + "miniz_oxide", + "op-alloy-consensus 0.22.4", + "proptest", + "rand 0.9.2", + "serde", + "serde_json", + "spin 0.10.0", + "thiserror 2.0.17", + "tracing", + "tracing-subscriber 0.3.22", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "kona-derive" +version = "0.4.5" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "async-trait", + "kona-genesis", + "kona-hardforks", + "kona-macros", + "kona-protocol", + "kona-registry", + "metrics", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "proptest", + "serde", + "serde_json", + "spin 0.10.0", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "kona-disc" +version = "0.1.2" +dependencies = [ + "alloy-rlp", + "backon", + "derive_more", + "discv5", + "kona-cli", + "kona-genesis", + "kona-macros", + "kona-peers", + "libp2p", + "metrics", + "rand 0.9.2", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-driver" +version = "0.4.0" +dependencies = [ + "alloy-consensus", + "alloy-evm 0.24.2", + "alloy-primitives", + "alloy-rlp", + "async-trait", + "kona-derive", + "kona-executor", + "kona-genesis", + "kona-protocol", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "spin 0.10.0", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "kona-engine" +version = "0.1.2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-transport", + "alloy-transport-http", + "arbitrary", + "async-trait", + "derive_more", + "http", + "http-body-util", + "jsonrpsee-types", + "kona-genesis", + "kona-macros", + "kona-protocol", + "kona-registry", + "metrics", + "metrics-exporter-prometheus 0.18.1", + "op-alloy-consensus 0.22.4", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "rand 0.9.2", + "rollup-boost", + "rstest", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "tracing", + "url", +] + +[[package]] +name = "kona-executor" +version = "0.4.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.24.2", + "alloy-op-evm 0.24.2", + "alloy-op-hardforks", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-transport", + "alloy-transport-http", + "alloy-trie", + "kona-genesis", + "kona-mpt", + "kona-protocol", + "kona-registry", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 14.1.0", + "rand 0.9.2", + "revm 33.1.0", + "rocksdb", + "rstest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-genesis" +version = "0.4.5" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-hardforks", + "alloy-op-hardforks", + "alloy-primitives", + "alloy-sol-types", + "arbitrary", + "derive_more", + "op-revm 14.1.0", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tabled", + "thiserror 2.0.17", + "toml", +] + +[[package]] +name = "kona-gossip" +version = "0.1.2" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "arbitrary", + "derive_more", + "discv5", + "futures", + "ipnet", + "kona-disc", + "kona-genesis", + "kona-macros", + "kona-peers", + "lazy_static", + "libp2p", + "libp2p-identity", + "libp2p-stream", + "metrics", + "multihash", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "openssl", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "snap", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-hardforks" +version = "0.4.5" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "kona-protocol", + "op-alloy-consensus 0.22.4", + "op-revm 14.1.0", + "revm 33.1.0", +] + +[[package]] +name = "kona-host" +version = "1.0.2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-op-evm 0.24.2", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-rpc-types-beacon", + "alloy-serde", + "alloy-transport", + "alloy-transport-http", + "anyhow", + "ark-ff 0.5.0", + "async-trait", + "clap", + "kona-cli", + "kona-client", + "kona-derive", + "kona-driver", + "kona-executor", + "kona-genesis", + "kona-mpt", + "kona-preimage", + "kona-proof", + "kona-proof-interop", + "kona-protocol", + "kona-providers-alloy", + "kona-registry", + "kona-std-fpvm", + "op-alloy-network", + "op-alloy-rpc-types-engine", + "proptest", + "reqwest", + "revm 33.1.0", + "rocksdb", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "kona-interop" +version = "0.4.5" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "arbitrary", + "async-trait", + "derive_more", + "kona-genesis", + "kona-protocol", + "kona-registry", + "op-alloy-consensus 0.22.4", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-macros" +version = "0.1.2" + +[[package]] +name = "kona-mpt" +version = "0.3.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-types", + "alloy-transport-http", + "alloy-trie", + "criterion", + "op-alloy-rpc-types-engine", + "pprof", + "proptest", + "rand 0.9.2", + "reqwest", + "serde", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "kona-node" +version = "1.0.0-rc.1" +dependencies = [ + "alloy-chains", + "alloy-genesis", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-engine", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "anyhow", + "backon", + "clap", + "derive_more", + "dirs", + "discv5", + "futures", + "http", + "jsonrpsee", + "kona-cli", + "kona-derive", + "kona-disc", + "kona-engine", + "kona-genesis", + "kona-gossip", + "kona-node-service", + "kona-peers", + "kona-protocol", + "kona-providers-alloy", + "kona-registry", + "kona-rpc", + "kona-sources", + "libp2p", + "metrics", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types-engine", + "reqwest", + "rollup-boost", + "rstest", + "serde_json", + "strum", + "tabled", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber 0.3.22", + "url", + "vergen", + "vergen-git2", +] + +[[package]] +name = "kona-node-service" +version = "0.1.3" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "anyhow", + "arbitrary", + "async-stream", + "async-trait", + "backon", + "derive_more", + "discv5", + "futures", + "http", + "http-body-util", + "jsonrpsee", + "kona-derive", + "kona-disc", + "kona-engine", + "kona-genesis", + "kona-gossip", + "kona-macros", + "kona-peers", + "kona-protocol", + "kona-providers-alloy", + "kona-rpc", + "kona-sources", + "libp2p", + "libp2p-stream", + "metrics", + "mockall", + "op-alloy-consensus 0.22.4", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types-engine", + "rand 0.9.2", + "rollup-boost", + "rstest", + "strum", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tracing", + "url", +] + +[[package]] +name = "kona-peers" +version = "0.1.2" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "arbtest", + "derive_more", + "dirs", + "discv5", + "kona-genesis", + "kona-registry", + "lazy_static", + "libp2p", + "libp2p-identity", + "multihash", + "secp256k1 0.31.1", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tracing", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "kona-preimage" +version = "0.3.0" +dependencies = [ + "alloy-primitives", + "async-channel", + "async-trait", + "rkyv", + "serde", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-proof" +version = "0.3.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.24.2", + "alloy-op-evm 0.24.2", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "ark-bls12-381", + "ark-ff 0.5.0", + "async-trait", + "c-kzg", + "kona-derive", + "kona-driver", + "kona-executor", + "kona-genesis", + "kona-mpt", + "kona-preimage", + "kona-protocol", + "kona-registry", + "lazy_static", + "lru 0.16.2", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 14.1.0", + "rand 0.9.2", + "rayon", + "rstest", + "serde", + "serde_json", + "spin 0.10.0", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-proof-interop" +version = "0.2.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.24.2", + "alloy-op-evm 0.24.2", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "arbitrary", + "async-trait", + "kona-executor", + "kona-genesis", + "kona-interop", + "kona-mpt", + "kona-preimage", + "kona-proof", + "kona-protocol", + "kona-registry", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 14.1.0", + "rand 0.9.2", + "revm 33.1.0", + "serde", + "serde_json", + "spin 0.10.0", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "kona-protocol" +version = "0.4.5" +dependencies = [ + "alloc-no-stdlib", + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-sol-types", + "arbitrary", + "async-trait", + "brotli", + "derive_more", + "kona-genesis", + "kona-registry", + "miniz_oxide", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "proptest", + "rand 0.9.2", + "rstest", + "serde", + "serde_json", + "spin 0.10.0", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "kona-providers-alloy" +version = "0.3.3" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-beacon", + "alloy-rpc-types-engine", + "alloy-serde", + "alloy-transport", + "alloy-transport-http", + "async-trait", + "c-kzg", + "http-body-util", + "kona-derive", + "kona-genesis", + "kona-macros", + "kona-protocol", + "lru 0.16.2", + "metrics", + "op-alloy-consensus 0.22.4", + "op-alloy-network", + "reqwest", + "serde", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", +] + +[[package]] +name = "kona-providers-local" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "async-trait", + "kona-derive", + "kona-genesis", + "kona-macros", + "kona-protocol", + "lru 0.16.2", + "metrics", + "op-alloy-consensus 0.22.4", + "rstest", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "kona-registry" +version = "0.4.5" +dependencies = [ + "alloy-chains", + "alloy-eips", + "alloy-genesis", + "alloy-hardforks", + "alloy-op-hardforks", + "alloy-primitives", + "kona-genesis", + "lazy_static", + "serde", + "serde_json", + "tabled", + "toml", +] + +[[package]] +name = "kona-rpc" +version = "0.3.2" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "async-trait", + "backon", + "derive_more", + "getrandom 0.3.4", + "ipnet", + "jsonrpsee", + "kona-engine", + "kona-genesis", + "kona-gossip", + "kona-macros", + "kona-protocol", + "libp2p", + "metrics", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-jsonrpsee", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "rollup-boost", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-serde" +version = "0.2.2" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", + "toml", +] + +[[package]] +name = "kona-sources" +version = "0.1.2" +dependencies = [ + "alloy-primitives", + "alloy-rpc-client", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "derive_more", + "notify", + "op-alloy-rpc-types-engine", + "reqwest", + "rustls", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "kona-std-fpvm" +version = "0.2.0" +dependencies = [ + "async-trait", + "buddy_system_allocator", + "cfg-if", + "kona-preimage", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "kona-std-fpvm-proc" +version = "0.2.0" +dependencies = [ + "cfg-if", + "kona-std-fpvm", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "kona-supervisor" +version = "0.1.0" +dependencies = [ + "alloy-network", + "alloy-provider", + "alloy-rpc-types-engine", + "anyhow", + "clap", + "glob", + "kona-cli", + "kona-genesis", + "kona-interop", + "kona-protocol", + "kona-registry", + "kona-supervisor-core", + "kona-supervisor-service", + "metrics", + "reqwest", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber 0.3.22", + "vergen", + "vergen-git2", +] + +[[package]] +name = "kona-supervisor-core" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-transport", + "async-trait", + "auto_impl", + "derive_more", + "futures", + "jsonrpsee", + "kona-genesis", + "kona-interop", + "kona-protocol", + "kona-supervisor-metrics", + "kona-supervisor-rpc", + "kona-supervisor-storage", + "kona-supervisor-types", + "metrics", + "mockall", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", + "reqwest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "kona-supervisor-metrics" +version = "0.1.0" + +[[package]] +name = "kona-supervisor-rpc" +version = "0.1.1" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-serde", + "async-trait", + "derive_more", + "jsonrpsee", + "kona-interop", + "kona-protocol", + "kona-supervisor-types", + "op-alloy-consensus 0.22.4", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "kona-supervisor-service" +version = "0.1.0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "anyhow", + "async-trait", + "derive_more", + "futures", + "jsonrpsee", + "kona-genesis", + "kona-interop", + "kona-protocol", + "kona-supervisor-core", + "kona-supervisor-metrics", + "kona-supervisor-rpc", + "kona-supervisor-storage", + "kona-supervisor-types", + "mockall", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "kona-supervisor-storage" +version = "0.1.0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "bytes", + "derive_more", + "eyre", + "kona-cli", + "kona-interop", + "kona-protocol", + "kona-supervisor-metrics", + "kona-supervisor-types", + "metrics", + "modular-bitfield", + "op-alloy-consensus 0.22.4", + "reth-codecs 1.6.0", + "reth-db", + "reth-db-api", + "serde", + "tempfile", + "test-fuzz", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "kona-supervisor-types" +version = "0.1.1" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "derive_more", + "kona-interop", + "kona-protocol", + "op-alloy-consensus 0.22.4", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libgit2-sys" +version = "0.18.2+1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libp2p" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.16", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-ping", + "libp2p-quic", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "libp2p-yamux", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 2.0.17", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-core" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d28e2d2def7c344170f5c6450c0dbe3dfef655610dbfde2f6ac28a527abbe36" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.5", + "rw-stream-sink", + "thiserror 2.0.17", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver", + "libp2p-core", + "libp2p-identity", + "parking_lot", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.49.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f58e37d8d6848e5c4c9e3c35c6f61133235bff2960c9c00a663b0849301221" +dependencies = [ + "async-channel", + "asynchronous-codec", + "base64", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "getrandom 0.2.16", + "hashlink", + "hex_fmt", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "regex", + "sha2", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-identify" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "libp2p-identity" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" +dependencies = [ + "asn1_der", + "bs58", + "ed25519-dalek", + "hkdf", + "k256", + "multihash", + "quick-protobuf", + "rand 0.8.5", + "sha2", + "thiserror 2.0.17", + "tracing", + "zeroize", +] + +[[package]] +name = "libp2p-mdns" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +dependencies = [ + "futures", + "hickory-proto", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-metrics" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-ping", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-noise" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", + "quick-protobuf", + "rand 0.8.5", + "snow", + "static_assertions", + "thiserror 2.0.17", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-ping" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bb7fcdfd9fead4144a3859da0b49576f171a8c8c7c0bfc7c541921d25e60d3" +dependencies = [ + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-quic" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "quinn", + "rand 0.8.5", + "ring", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-stream" +version = "0.4.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6bd8025c80205ec2810cfb28b02f362ab48a01bee32c50ab5f12761e033464" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "tracing", +] + +[[package]] +name = "libp2p-swarm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa762e5215919a34e31c35d4b18bf2e18566ecab7f8a3d39535f4a3068f8b62" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "lru 0.12.5", + "multistream-select", + "rand 0.8.5", + "smallvec", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +dependencies = [ + "heck", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "libp2p-tcp" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b4e030c52c46c8d01559b2b8ca9b7c4185f10576016853129ca1fe5cd1a644" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring", + "rustls", + "rustls-webpki", + "thiserror 2.0.17", + "x509-parser", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-yamux" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +dependencies = [ + "either", + "futures", + "libp2p-core", + "thiserror 2.0.17", + "tracing", + "yamux 0.12.1", + "yamux 0.13.8", +] + +[[package]] +name = "libproc" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" +dependencies = [ + "bindgen 0.72.1", + "errno", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "librocksdb-sys" +version = "0.17.3+10.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" +dependencies = [ + "bindgen 0.72.1", + "bzip2-sys", + "cc", + "libc", + "libz-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3dbdd96ed57d565ec744cba02862d707acf373c5772d152abae6ec5c4e24f6c" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.111", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "indexmap 2.12.1", + "ipnet", + "metrics", + "metrics-util 0.19.1", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" +dependencies = [ + "base64", + "http-body-util", + "hyper", + "hyper-util", + "indexmap 2.12.1", + "ipnet", + "metrics", + "metrics-util 0.20.1", + "quanta", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-process" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f615e08e049bd14a44c4425415782efb9bcd479fc1e19ddeb971509074c060d0" +dependencies = [ + "libc", + "libproc", + "mach2", + "metrics", + "once_cell", + "procfs", + "rlimit", + "windows 0.62.2", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "aho-corasick", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "metrics", + "ordered-float", + "quanta", + "radix_trie", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "metrics-util" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.16.1", + "metrics", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "serde", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "async-lock", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "event-listener", + "futures-util", + "parking_lot", + "portable-atomic", + "rustc_version 0.4.1", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +dependencies = [ + "core2", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.17", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "nybbles" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +dependencies = [ + "alloy-rlp", + "arbitrary", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "op-alloy" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b13412d297c1f9341f678b763750b120a73ffe998fa54a94d6eda98449e7ca" +dependencies = [ + "op-alloy-consensus 0.22.4", + "op-alloy-network", + "op-alloy-provider", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", +] + +[[package]] +name = "op-alloy-consensus" +version = "0.18.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c88d2940558fd69f8f07b3cbd7bb3c02fc7d31159c1a7ba9deede50e7881024" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "op-alloy-consensus" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726da827358a547be9f1e37c2a756b9e3729cb0350f43408164794b370cad8ae" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-serde", + "arbitrary", + "derive_more", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "op-alloy-flz" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" + +[[package]] +name = "op-alloy-network" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63f27e65be273ec8fcb0b6af0fd850b550979465ab93423705ceb3dfddbd2ab" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-signer", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", +] + +[[package]] +name = "op-alloy-provider" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71456699aa256dc20119736422ad9a44da8b9585036117afb936778122093b9" +dependencies = [ + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-engine", + "alloy-transport", + "async-trait", + "op-alloy-rpc-types-engine", +] + +[[package]] +name = "op-alloy-rpc-jsonrpsee" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef9114426b16172254555aad34a8ea96c01895e40da92f5d12ea680a1baeaa7" +dependencies = [ + "alloy-primitives", + "jsonrpsee", +] + +[[package]] +name = "op-alloy-rpc-types" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562dd4462562c41f9fdc4d860858c40e14a25df7f983ae82047f15f08fce4d19" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "arbitrary", + "derive_more", + "jsonrpsee", + "op-alloy-consensus 0.22.4", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "op-alloy-rpc-types-engine" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f24b8cb66e4b33e6c9e508bf46b8ecafc92eadd0b93fedd306c0accb477657" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "alloy-serde", + "arbitrary", + "derive_more", + "ethereum_ssz", + "ethereum_ssz_derive", + "op-alloy-consensus 0.22.4", + "serde", + "snap", + "thiserror 2.0.17", +] + +[[package]] +name = "op-revm" +version = "12.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31622d03b29c826e48800f4c8f389c8a9c440eb796a3e35203561a288f12985" +dependencies = [ + "auto_impl", + "revm 31.0.2", + "serde", +] + +[[package]] +name = "op-revm" +version = "14.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1475a779c73999fc803778524042319691b31f3d6699d2b560c4ed8be1db802a" +dependencies = [ + "auto_impl", + "revm 33.1.0", + "serde", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", + "tracing", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" +dependencies = [ + "base64", + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "tonic", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "papergrid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "bytes", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.1", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pprof" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187" +dependencies = [ + "aligned-vec", + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "smallvec", + "spin 0.10.0", + "symbolic-demangle", + "tempfile", + "thiserror 2.0.17", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint 0.9.5", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.10.0", + "procfs-core", + "rustix 1.1.2", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.10.0", + "hex", +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.10.0", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "proptest-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "reth-basic-payload-builder" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "futures-core", + "futures-util", + "metrics", + "reth-chain-state", + "reth-metrics 1.9.3", + "reth-payload-builder", + "reth-payload-builder-primitives", + "reth-payload-primitives", + "reth-primitives-traits 1.9.3", + "reth-revm", + "reth-storage-api", + "reth-tasks", + "tokio", + "tracing", +] + +[[package]] +name = "reth-chain-state" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "derive_more", + "metrics", + "parking_lot", + "pin-project", + "reth-chainspec", + "reth-errors", + "reth-ethereum-primitives 1.9.3", + "reth-execution-types", + "reth-metrics 1.9.3", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-trie", + "revm-database 9.0.6", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "reth-chainspec" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-genesis", + "alloy-primitives", + "alloy-trie", + "auto_impl", + "derive_more", + "reth-ethereum-forks", + "reth-network-peers", + "reth-primitives-traits 1.9.3", + "serde_json", +] + +[[package]] +name = "reth-codecs" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-trie", + "bytes", + "modular-bitfield", + "op-alloy-consensus 0.18.14", + "reth-codecs-derive 1.6.0", + "reth-zstd-compressors 1.6.0", + "serde", +] + +[[package]] +name = "reth-codecs" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-trie", + "bytes", + "modular-bitfield", + "op-alloy-consensus 0.22.4", + "reth-codecs-derive 1.9.3", + "reth-zstd-compressors 1.9.3", + "serde", +] + +[[package]] +name = "reth-codecs-derive" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "convert_case 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "reth-codecs-derive" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "reth-consensus" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "auto_impl", + "reth-execution-types", + "reth-primitives-traits 1.9.3", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-consensus-common" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "reth-chainspec", + "reth-consensus", + "reth-primitives-traits 1.9.3", +] + +[[package]] +name = "reth-db" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-primitives", + "derive_more", + "eyre", + "metrics", + "page_size", + "reth-db-api", + "reth-fs-util 1.6.0", + "reth-libmdbx", + "reth-metrics 1.6.0", + "reth-nippy-jar", + "reth-static-file-types 1.6.0", + "reth-storage-errors 1.6.0", + "reth-tracing", + "rustc-hash 2.1.1", + "strum", + "sysinfo", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-db-api" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-consensus", + "alloy-genesis", + "alloy-primitives", + "bytes", + "derive_more", + "metrics", + "modular-bitfield", + "parity-scale-codec", + "reth-codecs 1.6.0", + "reth-db-models 1.6.0", + "reth-ethereum-primitives 1.6.0", + "reth-primitives-traits 1.6.0", + "reth-prune-types 1.6.0", + "reth-stages-types 1.6.0", + "reth-storage-errors 1.6.0", + "reth-trie-common 1.6.0", + "roaring", + "serde", +] + +[[package]] +name = "reth-db-models" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "bytes", + "modular-bitfield", + "reth-codecs 1.6.0", + "reth-primitives-traits 1.6.0", + "serde", +] + +[[package]] +name = "reth-db-models" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "reth-primitives-traits 1.9.3", +] + +[[package]] +name = "reth-engine-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "auto_impl", + "reth-chain-state", + "reth-errors", + "reth-ethereum-primitives 1.9.3", + "reth-evm", + "reth-execution-types", + "reth-payload-builder-primitives", + "reth-payload-primitives", + "reth-primitives-traits 1.9.3", + "reth-trie-common 1.9.3", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-errors" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "reth-consensus", + "reth-execution-errors", + "reth-storage-errors 1.9.3", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-eth-wire-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-primitives", + "alloy-rlp", + "bytes", + "derive_more", + "reth-chainspec", + "reth-codecs-derive 1.9.3", + "reth-ethereum-primitives 1.9.3", + "reth-primitives-traits 1.9.3", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-ethereum-engine-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "reth-engine-primitives", + "reth-ethereum-primitives 1.9.3", + "reth-payload-primitives", + "reth-primitives-traits 1.9.3", + "serde", + "sha2", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-ethereum-forks" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-eip2124", + "alloy-hardforks", + "alloy-primitives", + "auto_impl", + "once_cell", + "rustc-hash 2.1.1", +] + +[[package]] +name = "reth-ethereum-primitives" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "modular-bitfield", + "reth-codecs 1.6.0", + "reth-primitives-traits 1.6.0", + "reth-zstd-compressors 1.6.0", + "serde", + "serde_with", +] + +[[package]] +name = "reth-ethereum-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-serde", + "reth-primitives-traits 1.9.3", + "reth-zstd-compressors 1.9.3", + "serde", + "serde_with", +] + +[[package]] +name = "reth-evm" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-primitives", + "auto_impl", + "derive_more", + "futures-util", + "reth-execution-errors", + "reth-execution-types", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-storage-errors 1.9.3", + "reth-trie-common 1.9.3", + "revm 31.0.2", +] + +[[package]] +name = "reth-execution-errors" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-evm 0.23.3", + "alloy-primitives", + "alloy-rlp", + "nybbles", + "reth-storage-errors 1.9.3", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-execution-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-primitives", + "derive_more", + "reth-ethereum-primitives 1.9.3", + "reth-primitives-traits 1.9.3", + "reth-trie-common 1.9.3", + "revm 31.0.2", +] + +[[package]] +name = "reth-fs-util" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-fs-util" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-libmdbx" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "bitflags 2.10.0", + "byteorder", + "dashmap", + "derive_more", + "indexmap 2.12.1", + "parking_lot", + "reth-mdbx-sys", + "smallvec", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "reth-mdbx-sys" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "bindgen 0.70.1", + "cc", +] + +[[package]] +name = "reth-metrics" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "metrics", + "metrics-derive", +] + +[[package]] +name = "reth-metrics" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "metrics", + "metrics-derive", +] + +[[package]] +name = "reth-network-peers" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "secp256k1 0.30.0", + "serde_with", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "reth-nippy-jar" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "anyhow", + "bincode 1.3.3", + "derive_more", + "lz4_flex", + "memmap2", + "reth-fs-util 1.6.0", + "serde", + "thiserror 2.0.17", + "tracing", + "zstd", +] + +[[package]] +name = "reth-optimism-chainspec" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-hardforks", + "alloy-primitives", + "derive_more", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", + "reth-chainspec", + "reth-ethereum-forks", + "reth-network-peers", + "reth-optimism-forks", + "reth-optimism-primitives", + "reth-primitives-traits 1.9.3", + "serde_json", +] + +[[package]] +name = "reth-optimism-consensus" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-trie", + "reth-chainspec", + "reth-consensus", + "reth-consensus-common", + "reth-execution-types", + "reth-optimism-chainspec", + "reth-optimism-forks", + "reth-optimism-primitives", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-storage-errors 1.9.3", + "reth-trie-common 1.9.3", + "revm 31.0.2", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "reth-optimism-evm" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-op-evm 0.23.3", + "alloy-primitives", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "op-revm 12.0.2", + "reth-chainspec", + "reth-evm", + "reth-execution-errors", + "reth-execution-types", + "reth-optimism-chainspec", + "reth-optimism-consensus", + "reth-optimism-forks", + "reth-optimism-primitives", + "reth-primitives-traits 1.9.3", + "reth-storage-errors 1.9.3", + "revm 31.0.2", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-optimism-forks" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-op-hardforks", + "alloy-primitives", + "once_cell", + "reth-ethereum-forks", +] + +[[package]] +name = "reth-optimism-payload-builder" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm 0.23.3", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-debug", + "alloy-rpc-types-engine", + "derive_more", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types-engine", + "reth-basic-payload-builder", + "reth-chain-state", + "reth-chainspec", + "reth-evm", + "reth-execution-types", + "reth-optimism-evm", + "reth-optimism-forks", + "reth-optimism-primitives", + "reth-optimism-txpool", + "reth-payload-builder", + "reth-payload-builder-primitives", + "reth-payload-primitives", + "reth-payload-util", + "reth-payload-validator", + "reth-primitives-traits 1.9.3", + "reth-revm", + "reth-storage-api", + "reth-transaction-pool", + "revm 31.0.2", + "serde", + "sha2", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "reth-optimism-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "op-alloy-consensus 0.22.4", + "reth-primitives-traits 1.9.3", +] + +[[package]] +name = "reth-optimism-txpool" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "alloy-serde", + "c-kzg", + "derive_more", + "futures-util", + "metrics", + "op-alloy-consensus 0.22.4", + "op-alloy-flz", + "op-alloy-rpc-types", + "op-revm 12.0.2", + "parking_lot", + "reth-chain-state", + "reth-chainspec", + "reth-metrics 1.9.3", + "reth-optimism-evm", + "reth-optimism-forks", + "reth-optimism-primitives", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-transaction-pool", + "serde", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "reth-payload-builder" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rpc-types", + "futures-util", + "metrics", + "reth-chain-state", + "reth-ethereum-engine-primitives", + "reth-metrics 1.9.3", + "reth-payload-builder-primitives", + "reth-payload-primitives", + "reth-primitives-traits 1.9.3", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "reth-payload-builder-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "pin-project", + "reth-payload-primitives", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "reth-payload-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "auto_impl", + "either", + "op-alloy-rpc-types-engine", + "reth-chain-state", + "reth-chainspec", + "reth-errors", + "reth-primitives-traits 1.9.3", + "serde", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "reth-payload-util" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "reth-transaction-pool", +] + +[[package]] +name = "reth-payload-validator" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-rpc-types-engine", + "reth-primitives-traits 1.9.3", +] + +[[package]] +name = "reth-primitives-traits" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-trie", + "auto_impl", + "byteorder", + "bytes", + "derive_more", + "modular-bitfield", + "once_cell", + "op-alloy-consensus 0.18.14", + "reth-codecs 1.6.0", + "revm-bytecode 6.2.2", + "revm-primitives 20.2.1", + "revm-state 7.0.5", + "secp256k1 0.30.0", + "serde", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-primitives-traits" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-trie", + "auto_impl", + "bytes", + "derive_more", + "once_cell", + "op-alloy-consensus 0.22.4", + "reth-codecs 1.9.3", + "revm-bytecode 7.1.1", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "secp256k1 0.30.0", + "serde", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-prune-types" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-primitives", + "derive_more", + "modular-bitfield", + "reth-codecs 1.6.0", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-prune-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "derive_more", + "strum", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-revm" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-storage-errors 1.9.3", + "reth-trie", + "revm 31.0.2", +] + +[[package]] +name = "reth-stages-types" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-primitives", + "bytes", + "modular-bitfield", + "reth-codecs 1.6.0", + "reth-trie-common 1.6.0", + "serde", +] + +[[package]] +name = "reth-stages-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "reth-trie-common 1.9.3", +] + +[[package]] +name = "reth-static-file-types" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-primitives", + "derive_more", + "serde", + "strum", +] + +[[package]] +name = "reth-static-file-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "derive_more", + "serde", + "strum", +] + +[[package]] +name = "reth-storage-api" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "auto_impl", + "reth-chainspec", + "reth-db-models 1.9.3", + "reth-ethereum-primitives 1.9.3", + "reth-execution-types", + "reth-primitives-traits 1.9.3", + "reth-prune-types 1.9.3", + "reth-stages-types 1.9.3", + "reth-storage-errors 1.9.3", + "reth-trie-common 1.9.3", + "revm-database 9.0.6", +] + +[[package]] +name = "reth-storage-errors" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "derive_more", + "reth-primitives-traits 1.6.0", + "reth-prune-types 1.6.0", + "reth-static-file-types 1.6.0", + "revm-database-interface 7.0.5", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-storage-errors" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "derive_more", + "reth-primitives-traits 1.9.3", + "reth-prune-types 1.9.3", + "reth-static-file-types 1.9.3", + "revm-database-interface 8.0.5", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-tasks" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "auto_impl", + "dyn-clone", + "futures-util", + "metrics", + "reth-metrics 1.9.3", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-futures", +] + +[[package]] +name = "reth-tracing" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "clap", + "eyre", + "rolling-file", + "tracing", + "tracing-appender", + "tracing-journald", + "tracing-logfmt", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "reth-transaction-pool" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "aquamarine", + "auto_impl", + "bitflags 2.10.0", + "futures-util", + "metrics", + "parking_lot", + "pin-project", + "reth-chain-state", + "reth-chainspec", + "reth-eth-wire-types", + "reth-ethereum-primitives 1.9.3", + "reth-execution-types", + "reth-fs-util 1.9.3", + "reth-metrics 1.9.3", + "reth-primitives-traits 1.9.3", + "reth-storage-api", + "reth-tasks", + "revm-interpreter 29.0.1", + "revm-primitives 21.0.2", + "rustc-hash 2.1.1", + "schnellru", + "serde", + "serde_json", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "reth-trie" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "auto_impl", + "itertools 0.14.0", + "reth-execution-errors", + "reth-primitives-traits 1.9.3", + "reth-stages-types 1.9.3", + "reth-storage-errors 1.9.3", + "reth-trie-common 1.9.3", + "reth-trie-sparse", + "revm-database 9.0.6", + "tracing", +] + +[[package]] +name = "reth-trie-common" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-trie", + "bytes", + "derive_more", + "itertools 0.14.0", + "nybbles", + "reth-codecs 1.6.0", + "reth-primitives-traits 1.6.0", + "revm-database 7.0.5", + "serde", +] + +[[package]] +name = "reth-trie-common" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "derive_more", + "itertools 0.14.0", + "nybbles", + "rayon", + "reth-primitives-traits 1.9.3", + "revm-database 9.0.6", +] + +[[package]] +name = "reth-trie-sparse" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "auto_impl", + "reth-execution-errors", + "reth-primitives-traits 1.9.3", + "reth-trie-common 1.9.3", + "smallvec", + "tracing", +] + +[[package]] +name = "reth-zstd-compressors" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.6.0#d8451e54e7267f9f1634118d6d279b2216f7e2bb" +dependencies = [ + "zstd", +] + +[[package]] +name = "reth-zstd-compressors" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" +dependencies = [ + "zstd", +] + +[[package]] +name = "revm" +version = "31.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb67a5223602113cae59a305acde2d9936bc18f2478dda879a6124b267cebfb6" +dependencies = [ + "revm-bytecode 7.1.1", + "revm-context 11.0.2", + "revm-context-interface 12.0.1", + "revm-database 9.0.6", + "revm-database-interface 8.0.5", + "revm-handler 12.0.2", + "revm-inspector 12.0.2", + "revm-interpreter 29.0.1", + "revm-precompile 29.0.1", + "revm-primitives 21.0.2", + "revm-state 8.1.1", +] + +[[package]] +name = "revm" +version = "33.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c85ed0028f043f87b3c88d4a4cb6f0a76440085523b6a8afe5ff003cf418054" +dependencies = [ + "revm-bytecode 7.1.1", + "revm-context 12.1.0", + "revm-context-interface 13.1.0", + "revm-database 9.0.6", + "revm-database-interface 8.0.5", + "revm-handler 14.1.0", + "revm-inspector 14.1.0", + "revm-interpreter 31.1.0", + "revm-precompile 31.0.0", + "revm-primitives 21.0.2", + "revm-state 8.1.1", +] + +[[package]] +name = "revm-bytecode" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c52031b73cae95d84cd1b07725808b5fd1500da3e5e24574a3b2dc13d9f16d" +dependencies = [ + "bitvec", + "phf 0.11.3", + "revm-primitives 20.2.1", + "serde", +] + +[[package]] +name = "revm-bytecode" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c6b5e6e8dd1e28a4a60e5f46615d4ef0809111c9e63208e55b5c7058200fb0" +dependencies = [ + "bitvec", + "phf 0.13.1", + "revm-primitives 21.0.2", + "serde", +] + +[[package]] +name = "revm-context" +version = "11.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92850e150f4f99d46c05a20ad0cd09286a7ad4ee21866fffb87101de6e602231" +dependencies = [ + "bitvec", + "cfg-if", + "derive-where", + "revm-bytecode 7.1.1", + "revm-context-interface 12.0.1", + "revm-database-interface 8.0.5", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-context" +version = "12.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f038f0c9c723393ac897a5df9140b21cfa98f5753a2cb7d0f28fa430c4118abf" +dependencies = [ + "bitvec", + "cfg-if", + "derive-where", + "revm-bytecode 7.1.1", + "revm-context-interface 13.1.0", + "revm-database-interface 8.0.5", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-context-interface" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d701e2c2347d65216b066489ab22a0a8e1f7b2568256110d73a7d5eff3385c" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface 8.0.5", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-context-interface" +version = "13.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431c9a14e4ef1be41ae503708fd02d974f80ef1f2b6b23b5e402e8d854d1b225" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface 8.0.5", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-database" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a276ed142b4718dcf64bc9624f474373ed82ef20611025045c3fb23edbef9c" +dependencies = [ + "alloy-eips", + "revm-bytecode 6.2.2", + "revm-database-interface 7.0.5", + "revm-primitives 20.2.1", + "revm-state 7.0.5", + "serde", +] + +[[package]] +name = "revm-database" +version = "9.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980d8d6bba78c5dd35b83abbb6585b0b902eb25ea4448ed7bfba6283b0337191" +dependencies = [ + "alloy-eips", + "revm-bytecode 7.1.1", + "revm-database-interface 8.0.5", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-database-interface" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c523c77e74eeedbac5d6f7c092e3851dbe9c7fec6f418b85992bd79229db361" +dependencies = [ + "auto_impl", + "either", + "revm-primitives 20.2.1", + "revm-state 7.0.5", + "serde", +] + +[[package]] +name = "revm-database-interface" +version = "8.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cce03e3780287b07abe58faf4a7f5d8be7e81321f93ccf3343c8f7755602bae" +dependencies = [ + "auto_impl", + "either", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-handler" +version = "12.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45418ed95cfdf0cb19effdbb7633cf2144cab7fb0e6ffd6b0eb9117a50adff6" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode 7.1.1", + "revm-context 11.0.2", + "revm-context-interface 12.0.1", + "revm-database-interface 8.0.5", + "revm-interpreter 29.0.1", + "revm-precompile 29.0.1", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-handler" +version = "14.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44f8f6dbeec3fecf9fe55f78ef0a758bdd92ea46cd4f1ca6e2a946b32c367f3" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode 7.1.1", + "revm-context 12.1.0", + "revm-context-interface 13.1.0", + "revm-database-interface 8.0.5", + "revm-interpreter 31.1.0", + "revm-precompile 31.0.0", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-inspector" +version = "12.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99801eac7da06cc112df2244bd5a64024f4ef21240e923b26e73c4b4a0e5da6" +dependencies = [ + "auto_impl", + "either", + "revm-context 11.0.2", + "revm-database-interface 8.0.5", + "revm-handler 12.0.2", + "revm-interpreter 29.0.1", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", + "serde_json", +] + +[[package]] +name = "revm-inspector" +version = "14.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5617e49216ce1ca6c8826bcead0386bc84f49359ef67cde6d189961735659f93" +dependencies = [ + "auto_impl", + "either", + "revm-context 12.1.0", + "revm-database-interface 8.0.5", + "revm-handler 14.1.0", + "revm-interpreter 31.1.0", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", + "serde_json", +] + +[[package]] +name = "revm-interpreter" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22789ce92c5808c70185e3bc49732f987dc6fd907f77828c8d3470b2299c9c65" +dependencies = [ + "revm-bytecode 7.1.1", + "revm-context-interface 12.0.1", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-interpreter" +version = "31.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec36405f7477b9dccdc6caa3be19adf5662a7a0dffa6270cdb13a090c077e5" +dependencies = [ + "revm-bytecode 7.1.1", + "revm-context-interface 13.1.0", + "revm-primitives 21.0.2", + "revm-state 8.1.1", + "serde", +] + +[[package]] +name = "revm-precompile" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968b124028960201abf6d6bf8e223f15fadebb4307df6b7dc9244a0aab5d2d05" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "c-kzg", + "cfg-if", + "k256", + "p256", + "revm-primitives 21.0.2", + "ripemd", + "rug", + "secp256k1 0.31.1", + "sha2", +] + +[[package]] +name = "revm-precompile" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62958af953cc4043e93b5be9b8497df84cc3bd612b865c49a7a7dfa26a84e2" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "blst", + "c-kzg", + "cfg-if", + "k256", + "p256", + "revm-primitives 21.0.2", + "ripemd", + "rug", + "secp256k1 0.31.1", + "sha2", +] + +[[package]] +name = "revm-primitives" +version = "20.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa29d9da06fe03b249b6419b33968ecdf92ad6428e2f012dc57bcd619b5d94e" +dependencies = [ + "alloy-primitives", + "num_enum", + "once_cell", + "serde", +] + +[[package]] +name = "revm-primitives" +version = "21.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e161db429d465c09ba9cbff0df49e31049fe6b549e28eb0b7bd642fcbd4412" +dependencies = [ + "alloy-primitives", + "num_enum", + "once_cell", + "serde", +] + +[[package]] +name = "revm-state" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f64fbacb86008394aaebd3454f9643b7d5a782bd251135e17c5b33da592d84d" +dependencies = [ + "bitflags 2.10.0", + "revm-bytecode 6.2.2", + "revm-primitives 20.2.1", + "serde", +] + +[[package]] +name = "revm-state" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8be953b7e374dbdea0773cf360debed8df394ea8d82a8b240a6b5da37592fc" +dependencies = [ + "bitflags 2.10.0", + "revm-bytecode 7.1.1", + "revm-primitives 21.0.2", + "serde", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rkyv" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "roaring" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rolling-file" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8395b4f860856b740f20a296ea2cd4d823e81a2658cf05ef61be22916026a906" +dependencies = [ + "chrono", +] + +[[package]] +name = "rollup-boost" +version = "0.1.0" +source = "git+https://github.com/flashbots/rollup-boost.git?rev=7fda98f#7fda98f6a514c0d7ce9b0c44992ff679dca482ef" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "backoff", + "bytes", + "clap", + "dashmap", + "dotenvy", + "eyre", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee", + "lru 0.16.2", + "metrics", + "metrics-derive", + "metrics-exporter-prometheus 0.16.2", + "metrics-util 0.19.1", + "moka", + "op-alloy-rpc-types-engine", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "parking_lot", + "paste", + "reth-optimism-payload-builder", + "rustls", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber 0.3.22", + "url", + "uuid", + "vergen", + "vergen-git2", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.111", + "unicode-ident", +] + +[[package]] +name = "rtnetlink" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rug" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad2e973fe3c3214251a840a621812a4f40468da814b1a3d6947d433c2af11f" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + +[[package]] +name = "ruint" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +dependencies = [ + "alloy-rlp", + "arbitrary", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schnellru" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" +dependencies = [ + "ahash", + "cfg-if", + "hashbrown 0.13.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.2", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_combinators" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de5fb30ae2918667d3cee99ef4b112f1f7ca0a6c58fa349d7d9e76035c72f8b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "indexmap 2.12.1", + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +dependencies = [ + "cc", + "cfg-if", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "arbitrary", + "serde", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "ring", + "rustc_version 0.4.1", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symbolic-common" +version = "12.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d8046c5674ab857104bc4559d505f4809b8060d57806e45d49737c97afeb60" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "12.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1accb6e5c4b0f682de907623912e616b44be1c9e725775155546669dbff720ec" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tabled" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" +dependencies = [ + "papergrid", + "tabled_derive", + "testing_table", +] + +[[package]] +name = "tabled_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "test-fuzz" +version = "7.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11e5c77910b1d5b469a342be541cf44933f0ad2c4b8d5acb32ee46697fd60546" +dependencies = [ + "serde", + "serde_combinators", + "test-fuzz-internal", + "test-fuzz-macro", + "test-fuzz-runtime", +] + +[[package]] +name = "test-fuzz-internal" +version = "7.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25f2f0ee315b130411a98570dd128dfe344bfaa0a28bf33d38f4a1fe85f39b" +dependencies = [ + "bincode 2.0.1", + "cargo_metadata", + "serde", +] + +[[package]] +name = "test-fuzz-macro" +version = "7.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c03ba0a9e3e4032f94d71c85e149af147843c6f212e4ca4383542d606b04a6" +dependencies = [ + "darling 0.21.3", + "heck", + "itertools 0.14.0", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "test-fuzz-runtime" +version = "7.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a4ac481aa983d386e857a7be0006c2f0ef26e0c5326bbc7262f73c2891b91d" +dependencies = [ + "hex", + "num-traits", + "serde", + "sha1", + "test-fuzz-internal", +] + +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags 2.10.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "iri-string", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.17", + "time", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-journald" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657" +dependencies = [ + "libc", + "tracing-core", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "time", + "tracing", + "tracing-core", + "tracing-subscriber 0.3.22", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber 0.3.22", + "web-time", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "9.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" +dependencies = [ + "anyhow", + "cargo_metadata", + "derive_builder", + "regex", + "rustversion", + "time", + "vergen-lib", +] + +[[package]] +name = "vergen-git2" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6ee511ec45098eabade8a0750e76eec671e7fb2d9360c563911336bea9cac1" +dependencies = [ + "anyhow", + "derive_builder", + "git2", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.4", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +dependencies = [ + "windows-core 0.53.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version 0.4.1", + "send_wrapper 0.6.0", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yamux" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.8.5", + "static_assertions", +] + +[[package]] +name = "yamux" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.9.2", + "static_assertions", + "web-time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/kona/Cargo.toml b/kona/Cargo.toml new file mode 100644 index 0000000000000..d5832cb4a8f5f --- /dev/null +++ b/kona/Cargo.toml @@ -0,0 +1,277 @@ +[workspace.package] +edition = "2024" +license = "MIT" +rust-version = "1.88" +authors = ["clabby", "refcell", "theochap", "emhane"] +homepage = "https://github.com/op-rs/kona" +repository = "https://github.com/op-rs/kona" +keywords = ["ethereum", "optimism", "crypto"] +categories = ["cryptography", "cryptography::cryptocurrencies"] +exclude = ["**/target"] + +[workspace] +resolver = "2" +members = [ + "bin/*", + "crates/proof/*", + "crates/node/*", + "crates/supervisor/*", + "crates/protocol/*", + "crates/batcher/*", + "crates/providers/*", + "crates/utilities/*", + "examples/*", +] +default-members = [ + "bin/host", + "bin/client", + "bin/node", + "bin/supervisor", +] + +[workspace.metadata.cargo-udeps.ignore] +normal = ["rustls-platform-verifier"] + +[workspace.lints.rust] +missing-debug-implementations = "warn" +missing-docs = "warn" +unreachable-pub = "warn" +unused-must-use = "deny" +rust-2018-idioms = "deny" +unnameable-types = "warn" + +[workspace.lints.rustdoc] +all = "warn" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +missing-const-for-fn = "warn" +use-self = "warn" +option-if-let-else = "warn" +redundant-clone = "warn" + +[profile.dev] +opt-level = 1 +overflow-checks = false + +[profile.bench] +debug = true + +[profile.dev-client] +inherits = "dev" +panic = "abort" + +[profile.release-client-lto] +inherits = "release" +panic = "abort" +codegen-units = 1 +lto = "fat" + +[profile.release-perf] +inherits = "release" +lto = "fat" +codegen-units = 1 + +[workspace.dependencies] +# Binaries +kona-host = { path = "bin/host", version = "1.0.2", default-features = false } +kona-client = { path = "bin/client", version = "1.0.2", default-features = false } + +# Protocol +kona-comp = { path = "crates/batcher/comp", version = "0.4.5", default-features = false } +kona-derive = { path = "crates/protocol/derive", version = "0.4.5", default-features = false } +kona-interop = { path = "crates/protocol/interop", version = "0.4.5", default-features = false } +kona-genesis = { path = "crates/protocol/genesis", version = "0.4.5", default-features = false } +kona-protocol = { path = "crates/protocol/protocol", version = "0.4.5", default-features = false } +kona-registry = { path = "crates/protocol/registry", version = "0.4.5", default-features = false } +kona-hardforks = { path = "crates/protocol/hardforks", version = "0.4.5", default-features = false } + +# Node +kona-rpc = { path = "crates/node/rpc", version = "0.3.2", default-features = false } +kona-peers = { path = "crates/node/peers", version = "0.1.2", default-features = false } +kona-engine = { path = "crates/node/engine", version = "0.1.2", default-features = false } +kona-sources = { path = "crates/node/sources", version = "0.1.2", default-features = false } +kona-node-service = { path = "crates/node/service", version = "0.1.3", default-features = false } +kona-disc = { path = "crates/node/disc", version = "0.1.2", default-features = false } +kona-gossip = { path = "crates/node/gossip", version = "0.1.2", default-features = false } + +# Supervisor +kona-supervisor-rpc = { path = "crates/supervisor/rpc", version = "0.1.1", default-features = false } +kona-supervisor-core = { path = "crates/supervisor/core", version = "0.1.0", default-features = false } +kona-supervisor-service = { path = "crates/supervisor/service", version = "0.1.0", default-features = false } +kona-supervisor-types = { path = "crates/supervisor/types", version = "0.1.1", default-features = false } +kona-supervisor-storage = { path = "crates/supervisor/storage", version = "0.1.0", default-features = false } +kona-supervisor-metrics = { path = "crates/supervisor/metrics", version = "0.1.0", default-features = false } + +# Providers +kona-providers-alloy = { path = "crates/providers/providers-alloy", version = "0.3.3", default-features = false } +kona-providers-local = { path = "crates/providers/providers-local", version = "0.1.0", default-features = false } + +# Proof +kona-driver = { path = "crates/proof/driver", version = "0.4.0", default-features = false } +kona-mpt = { path = "crates/proof/mpt", version = "0.3.0", default-features = false } +kona-proof = { path = "crates/proof/proof", version = "0.3.0", default-features = false } +kona-executor = { path = "crates/proof/executor", version = "0.4.0", default-features = false } +kona-std-fpvm = { path = "crates/proof/std-fpvm", version = "0.2.0", default-features = false } +kona-preimage = { path = "crates/proof/preimage", version = "0.3.0", default-features = false } +kona-std-fpvm-proc = { path = "crates/proof/std-fpvm-proc", version = "0.2.0", default-features = false } +kona-proof-interop = { path = "crates/proof/proof-interop", version = "0.2.0", default-features = false } + +# Utilities +kona-cli = { path = "crates/utilities/cli", version = "0.3.2", default-features = false } +kona-serde = { path = "crates/utilities/serde", version = "0.2.2", default-features = false } +kona-macros = { path = "crates/utilities/macros", version = "0.1.2", default-features = false } + +# Alloy +alloy-rlp = { version = "0.3.12", default-features = false } +alloy-trie = { version = "0.9.1", default-features = false } +alloy-eips = { version = "1.1.3", default-features = false } +alloy-serde = { version = "1.1.3", default-features = false } +alloy-signer = { version = "1.1.3", default-features = false } +alloy-chains = { version = "0.2.22", default-features = false } +alloy-network = { version = "1.1.3", default-features = false } +alloy-genesis = { version = "1.1.3", default-features = false } +alloy-provider = { version = "1.1.3", default-features = false } +alloy-hardforks = { version = "0.4.5", default-features = false } +alloy-sol-types = { version = "1.4.1", default-features = false } +alloy-consensus = { version = "1.1.3", default-features = false } +alloy-transport = { version = "1.1.3", default-features = false } +alloy-rpc-types = { version = "1.1.3", default-features = false } +alloy-rpc-client = { version = "1.1.3", default-features = false } +alloy-primitives = { version = "1.4.1", default-features = false } +alloy-signer-local = { version = "1.1.3", default-features = false } +alloy-node-bindings = { version = "1.1.3", default-features = false } +alloy-rpc-types-eth = { version = "1.1.3", default-features = false } +alloy-transport-http = { version = "1.1.3", default-features = false } +alloy-rpc-types-engine = { version = "1.1.3", default-features = false } +alloy-rpc-types-beacon = { version = "1.1.3", default-features = false } +alloy-network-primitives = { version = "1.1.3", default-features = false } +alloy-json-rpc = { version = "1.1.3", default-features = false } + +# OP Alloy +op-alloy-network = { version = "0.22.4", default-features = false } +op-alloy-provider = { version = "0.22.4", default-features = false } +alloy-op-hardforks = { version = "0.4.5", default-features = false } +op-alloy-consensus = { version = "0.22.4", default-features = false } +op-alloy-rpc-types = { version = "0.22.4", default-features = false } +op-alloy-rpc-jsonrpsee = { version = "0.22.4", default-features = false } +op-alloy-rpc-types-engine = { version = "0.22.4", default-features = false } + +# Execution +revm = { version = "33.1.0", default-features = false } +op-revm = { version = "14.1.0", default-features = false } +alloy-evm = { version = "0.24.2", default-features = false } +alloy-op-evm = { version = "0.24.2", default-features = false } + +# Reth (pinned to v1.6.0 for kona-supervisor-storage) +reth-db-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0" } +reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0" } +reth-codecs = { git = "https://github.com/paradigmxyz/reth", tag = "v1.6.0" } + +# General +notify = "8.2" +url = "2.5.7" +http = "1.4.0" +lru = "0.16.2" +glob = "0.3.3" +dirs = "6.0.0" +eyre = "0.6.12" +spin = "0.10.0" +clap = "4.5.53" +tower = "0.5.2" +bytes = "1.11.0" +vergen = "9.0.6" +tokio = "1.48.0" +rayon = "1.11.0" +strum = "0.27" +cfg-if = "1.0.4" +rstest = "0.26.1" +ratatui = "0.29.0" +futures = "0.3.31" +futures-util = "0.3.31" +reqwest = "0.12.24" +auto_impl = "1.3.0" +tempfile = "3.23.0" +test-fuzz = "7.2.5" +arbitrary = "1.4.2" +multihash = "0.19.3" +crossterm = "0.29.0" +color-eyre = "0.6.5" +jsonrpsee = "0.26.0" +jsonrpsee-types = "0.26.0" +tokio-util = "0.7.17" +rustls = { version = "0.23", default-features = false } +rustls-pemfile = { version = "2.2", default-features = false } +vergen-git2 = "1.0.7" +async-trait = "0.1.89" +tokio-stream = "0.1.17" +async-stream = "0.3.6" +async-channel = "2.5.0" +http-body-util = "0.1.3" +unsigned-varint = "0.8.0" +modular-bitfield = "0.11.2" +buddy_system_allocator = "0.11.0" + +rand = { version = "0.9.2", default-features = false } +backon = { version = "1.6.0", default-features = false } +tabled = { version = "0.20.0", default-features = false } +anyhow = { version = "1.0.100", default-features = false } +thiserror = { version = "2.0.17", default-features = false } +derive_more = { version = "2.1.0", default-features = false } +lazy_static = { version = "1.5.0", default-features = false } + +# Compression +getrandom = "0.3.4" +miniz_oxide = "0.8.9" +alloc-no-stdlib = "2.0.4" +brotli = { version = "8.0.2", default-features = false } + +# Networking +snap = "1.1.1" +discv5 = "0.10.2" +libp2p = "0.56.0" +libp2p-stream = "0.4.0-alpha" +libp2p-identity = "0.2.12" +openssl = "0.10.75" +ipnet = "2.11.0" + +# Tracing +tracing-loki = "0.2.6" +tracing-subscriber = "0.3.22" +tracing-appender = "0.2.4" +tracing = { version = "0.1.43", default-features = false } + +# Metrics +metrics = { version = "0.24.3", default-features = false } +prometheus = { version = "0.14.0", default-features = false } +metrics-exporter-prometheus = { version = "0.18.1", default-features = false } +metrics-process = "2.4.2" + +# Testing +pprof = "0.15.0" +arbtest = "0.3.2" +proptest = "1.9.0" +criterion = "0.5.1" +mockall = "0.14.0" + +# Serialization +rkyv = "0.8.12" +serde_repr = "0.1.20" +ethereum_ssz = "0.10.0" +toml = { version = "0.9.8", default-features = false } +serde = { version = "1.0.228", default-features = false } +serde_json = { version = "1.0.145", default-features = false } + +# K/V database +rocksdb = { version = "0.24.0", default-features = false } + +# Cryptography +sha2 = { version = "0.10.9", default-features = false } +c-kzg = { version = "2.1.5", default-features = false } +ark-ff = { version = "0.5.0", default-features = false } +secp256k1 = { version = "0.31.1", default-features = false } +ark-bls12-381 = { version = "0.5.0", default-features = false } + +# Rollup Boost (required for rollup-boost integration) +rollup-boost = { git = "https://github.com/flashbots/rollup-boost.git", rev = "7fda98f" } +parking_lot = "0.12.5" diff --git a/kona/LICENSE.md b/kona/LICENSE.md new file mode 100644 index 0000000000000..54878fbf60b9c --- /dev/null +++ b/kona/LICENSE.md @@ -0,0 +1,26 @@ +# The MIT License (MIT) + +Copyright © `2023` `kona contributors` +Copyright © `2024` `Optimism` +Copyright © `2025` `kona contributors` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/kona/README.md b/kona/README.md new file mode 100644 index 0000000000000..ae3a57f7d8b9c --- /dev/null +++ b/kona/README.md @@ -0,0 +1,173 @@ +

+Kona +

+ +

+ The Monorepo for OP Stack Types, Components, and Services built in Rust. +

+ +

+ + + CI + Codecov + License + Docs +

+ +

+ What's Kona? • + Overview • + MSRV • + Contributing • + Credits • + License +

+ + +## What's Kona? + +Originally a suite of portable implementations of the OP Stack rollup state transition, +Kona has been extended to be _the monorepo_ for OP Stack +types, components, and services built in Rust. Kona provides an ecosystem of extensible, low-level +crates that compose into components and services required for the OP Stack. + +The [docs][site] contains a more in-depth overview of the project, contributor guidelines, tutorials for +getting started with building your own programs, and a reference for the libraries and tools provided by Kona. + +## Overview + +> [!NOTE] +> +> Ethereum (Alloy) types modified for the OP Stack live in [op-alloy](https://github.com/alloy-rs/op-alloy). + +**Binaries** + +- [`client`](./bin/client): The bare-metal program that executes the state transition, to be run on a prover. +- [`host`](./bin/host): The host program that runs natively alongside the prover, serving as the [Preimage Oracle][g-preimage-oracle] server. +- [`node`](./bin/node): [WIP] A [Rollup Node][rollup-node-spec] implementation, backed by [`kona-derive`](./crates/protocol/derive). Supports flexible chain ID specification via `--l2-chain-id` using either numeric IDs (`10`) or chain names (`optimism`). +- [`supervisor`](./bin/supervisor): [WIP] A [Supervisor][supervisor-spec] implementation. + +**Protocol** + +- [`genesis`](./crates/protocol/genesis): Genesis types for OP Stack chains. +- [`protocol`](./crates/protocol/protocol): Core protocol types used across OP Stack rust crates. +- [`derive`](./crates/protocol/derive): `no_std` compatible implementation of the [derivation pipeline][g-derivation-pipeline]. +- [`driver`](./crates/proof/driver): Stateful derivation pipeline driver. +- [`interop`](./crates/protocol/interop): Core functionality and primitives for the [Interop feature](https://specs.optimism.io/interop/overview.html) of the OP Stack. +- [`registry`](./crates/protocol/registry): Rust bindings for the [superchain-registry][superchain-registry]. +- [`comp`](./crates/batcher/comp): Compression types for the OP Stack. +- [`hardforks`](./crates/protocol/hardforks): Consensus layer hardfork types for the OP Stack including network upgrade transactions. + +**Proof** + +- [`mpt`](./crates/proof/mpt): Utilities for interacting with the Merkle Patricia Trie in the client program. +- [`executor`](./crates/proof/executor): `no_std` stateless block executor for the [OP Stack][op-stack]. +- [`proof`](./crates/proof/proof): High level OP Stack state transition proof SDK. +- [`proof-interop`](./crates/proof/proof-interop): Extension of `kona-proof` with interop support. +- [`preimage`](./crates/proof/preimage): High level interfaces to the [`PreimageOracle`][fpp-specs] ABI. +- [`std-fpvm`](./crates/proof/std-fpvm): Platform specific [Fault Proof VM][g-fault-proof-vm] kernel APIs. +- [`std-fpvm-proc`](./crates/proof/std-fpvm-proc): Proc macro for [Fault Proof Program][fpp-specs] entrypoints. + +**Node** + +- [`service`](./crates/node/service): The OP Stack rollup node service. +- [`engine`](./crates/node/engine): An extensible implementation of the [OP Stack][op-stack] rollup node engine client +- [`rpc`](./crates/node/rpc): OP Stack RPC types and extensions. +- [`gossip`](./crates/node/gossip): OP Stack P2P Networking - Gossip. +- [`disc`](./crates/node/disc): OP Stack P2P Networking - Discovery. +- [`peers`](./crates/node/peers): Networking Utilities ported from reth. +- [`sources`](./crates/node/sources): Data source types and utilities for the kona-node. + +**Providers** + +- [`providers-alloy`](./crates/providers/providers-alloy): Provider implementations for `kona-derive` backed by [Alloy][alloy]. + +**Utilities** + +- [`serde`](./crates/utilities/serde): Serialization helpers. +- [`cli`](./crates/utilities/cli): Standard CLI utilities, used across `kona`'s binaries. +- [`macros`](./crates/utilities/macros): Utility macros. + +### Proof + +Built on top of these libraries, this repository also features a [proof program][fpp-specs] +designed to deterministically execute the rollup state transition in order to verify an +[L2 output root][g-output-root] from the L1 inputs it was [derived from][g-derivation-pipeline]. + +Kona's libraries were built with alternative backend support and extensibility in mind - the repository features +a fault proof virtual machine backend for use in the governance-approved OP Stack, but it's portable across +provers! Kona is also used by: + +- [`op-succinct`][op-succinct] +- [`kailua`][kailua] + +To build your own backend for kona, or build a new application on top of its libraries, +see the [SDK section of the docs](https://rollup.yoga/node/design/intro). + +## MSRV + +The current MSRV (minimum supported rust version) is `1.88`. + +The MSRV is not increased automatically, and will be updated +only as part of a patch (pre-1.0) or minor (post-1.0) release. + + +## Crate Releases + +`kona` releases are done using the [`cargo-release`](https://crates.io/crates/cargo-release) crate. +A detailed guide is available in [./RELEASES.md](./RELEASES.md). + + +## Contributing + +`kona` is built by open source contributors like you, thank you for improving the project! + +A [contributing guide][contributing] is available that sets guidelines for contributing. + +Pull requests will not be merged unless CI passes, so please ensure that your contribution +follows the linting rules and passes clippy. + + +## Credits + +`kona` is inspired by the work of several teams, namely [OP Labs][op-labs] and other contributors' work on the +[Optimism monorepo][op-go-monorepo] and [BadBoiLabs][bad-boi-labs]'s work on [Cannon-rs][badboi-cannon-rs]. + +`kona` is also built on rust types in [alloy][alloy], [op-alloy][op-alloy], and [maili][maili]. + +## License + +Licensed under the [MIT license.](https://github.com/op-rs/kona/blob/main/LICENSE.md) + +> [!NOTE] +> +> Contributions intentionally submitted for inclusion in these crates by you +> shall be licensed as above, without any additional terms or conditions. + + + + +[alloy]: https://github.com/alloy-rs/alloy +[maili]: https://github.com/op-rs/maili +[op-alloy]: https://github.com/alloy-rs/op-alloy +[contributing]: https://rollup.yoga/intro/contributing +[op-stack]: https://github.com/ethereum-optimism/optimism +[superchain-registry]: https://github.com/ethereum-optimism/superchain-registry +[op-go-monorepo]: https://github.com/ethereum-optimism/optimism/tree/develop +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[rollup-node-spec]: https://specs.optimism.io/protocol/rollup-node.html +[supervisor-spec]: https://specs.optimism.io/interop/supervisor.html +[badboi-cannon-rs]: https://github.com/BadBoiLabs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fpp-specs]: https://specs.optimism.io/fault-proof/index.html +[site]: https://rollup.yoga +[op-succinct]: https://github.com/succinctlabs/op-succinct +[kailua]: https://github.com/risc0/kailua +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs +[g-output-root]: https://specs.optimism.io/glossary.html#l2-output-root +[g-derivation-pipeline]: https://specs.optimism.io/protocol/derivation.html#l2-chain-derivation-pipeline +[g-fault-proof-vm]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-vm +[g-preimage-oracle]: https://specs.optimism.io/fault-proof/index.html#pre-image-oracle diff --git a/kona/RELEASES.md b/kona/RELEASES.md new file mode 100644 index 0000000000000..35d40f1088295 --- /dev/null +++ b/kona/RELEASES.md @@ -0,0 +1,82 @@ +# Releases + +This is a concise guide for cutting a release for `kona` crates. +> [!TIP] +> +> Ensure you have permission to update any affected crates before cutting a release. + + +### cargo-release + +Ensure [cargo-release][cargo-release] is installed using cargo's `install` command. + +``` +$ cargo install cargo-release +``` + +### Dry Run + +> [!TIP] +> +> Ensure that you have trunk (the `main` branch) checked out and up to date. + +Let's say we want to release the `kona-protocol` crate. +Execute the following command to perform a _dry run_ patch release. + +``` +$ cargo release patch --package kona-protocol --no-push +``` + +This will update the _patch_ version of the crate. (e.g. `0.1.0` -> `0.1.1`). + +To update minor and major versions, just specify `minor` or `major` in place of `patch`. + +If this command executes without any errors, proceed to executing the release. + +### Cutting the Release + +> [!IMPORTANT] +> +> Executing the release command may take time depending on your machine and +> how quickly it can compile the crate. Be prepared to let this run for some time. + +Append the `--execute` argument to the cargo release command to execute the dry run above. + +``` +$ cargo release patch --package kona-protocol --no-push --execute +``` + +The `kona-protocol` crate will be published. +Once this is done be sure to push the artifacts in the next step! + +### Committing Artifacts + +After the release command completes, it will automatically commit artifacts to the current +branch - `main`. + +Since we don't want to push to the `main` branch, we need to do a few things. + +Reset the git commit so changes are not committed like so. + +``` +$ git reset HEAD^ +``` + +Running `git status` should show unstaged changes to the `CHANGELOG.md` +as well as cargo manifest TOMLs. + +Now, checkout a new branch, commit, and push the artifacts to the new branch. + +``` +$ git checkout -b release/kona-protocol/0.1.1 +$ git add . +$ git commit -m "release(kona-protocol): 0.1.1" +$ git push +``` + +Open a PR and you're all set, the release is complete! + + + + +[cargo-release]: https://github.com/crate-ci/cargo-release diff --git a/kona/SECURITY.md b/kona/SECURITY.md new file mode 100644 index 0000000000000..ce24b55ba15dc --- /dev/null +++ b/kona/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Contact security at oplabs.co. diff --git a/kona/assets/banner.png b/kona/assets/banner.png new file mode 100644 index 0000000000000..af7316880ff0c Binary files /dev/null and b/kona/assets/banner.png differ diff --git a/kona/assets/favicon.ico b/kona/assets/favicon.ico new file mode 100644 index 0000000000000..6f6a6fcc06188 Binary files /dev/null and b/kona/assets/favicon.ico differ diff --git a/kona/assets/square.png b/kona/assets/square.png new file mode 100644 index 0000000000000..31d76367e4476 Binary files /dev/null and b/kona/assets/square.png differ diff --git a/kona/bin/client/Cargo.toml b/kona/bin/client/Cargo.toml new file mode 100644 index 0000000000000..c3fe27a7952c0 --- /dev/null +++ b/kona/bin/client/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "kona-client" +version = "1.0.2" +publish = false +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +# Proof +kona-mpt.workspace = true +kona-preimage.workspace = true +kona-executor.workspace = true +kona-proof.workspace = true +kona-proof-interop.workspace = true +kona-std-fpvm.workspace = true +kona-std-fpvm-proc.workspace = true + +# Protocol +kona-derive.workspace = true +kona-driver.workspace = true +kona-interop.workspace = true +kona-registry.workspace = true +kona-protocol.workspace = true +kona-genesis = { workspace = true, features = ["serde"] } + +# Alloy +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-primitives.workspace = true +alloy-rpc-types-engine.workspace = true + +# Op Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine = { workspace = true, features = ["serde"] } + +# Execution +revm.workspace = true +op-revm.workspace = true +alloy-op-evm.workspace = true +alloy-evm = { workspace = true, features = ["op"] } + +# General +lru.workspace = true +spin.workspace = true +serde.workspace = true +cfg-if.workspace = true +tracing.workspace = true +serde_json.workspace = true +async-trait.workspace = true +thiserror.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +kona-preimage = { workspace = true, features = ["std"] } +sha2.workspace = true + +[features] +default = [ "client-tracing" ] +client-tracing = [ "kona-std-fpvm/tracing" ] + +[[bin]] +name = "kona-client" +path = "src/kona.rs" + +[[bin]] +name = "kona-client-int" +path = "src/kona_interop.rs" diff --git a/kona/bin/client/README.md b/kona/bin/client/README.md new file mode 100644 index 0000000000000..96377ce9952a3 --- /dev/null +++ b/kona/bin/client/README.md @@ -0,0 +1,3 @@ +# `kona-client` + +This binary contains the client program for executing the Optimism rollup state transition. diff --git a/kona/bin/client/justfile b/kona/bin/client/justfile new file mode 100644 index 0000000000000..11fb537a136bd --- /dev/null +++ b/kona/bin/client/justfile @@ -0,0 +1,297 @@ +set fallback := true + +KONA_CLIENT_ROOT := source_directory() + +# default recipe to display help information +default: + @just --list + +# Run the client program on asterisc with the host in detached server mode. +run-client-asterisc block_number l1_rpc l1_beacon_rpc l2_rpc rollup_node_rpc verbosity='': + #!/usr/bin/env bash + + L1_NODE_ADDRESS="{{l1_rpc}}" + L1_BEACON_ADDRESS="{{l1_beacon_rpc}}" + L2_NODE_ADDRESS="{{l2_rpc}}" + OP_NODE_ADDRESS="{{rollup_node_rpc}}" + + HOST_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/release/kona-host" + CLIENT_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/riscv64imac-unknown-none-elf/release-client-lto/kona-client" + STATE_PATH="{{KONA_CLIENT_ROOT}}/../../state.bin.gz" + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + echo "Fetching configuration for block #$CLAIMED_L2_BLOCK_NUMBER..." + + # Get output root for block + CLAIMED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $CLAIMED_L2_BLOCK_NUMBER) | jq -r .outputRoot) + + # Get the info for the previous block + AGREED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .outputRoot) + AGREED_L2_HEAD_HASH=$(cast block --rpc-url $L2_NODE_ADDRESS $((CLAIMED_L2_BLOCK_NUMBER - 1)) --json | jq -r .hash) + L1_ORIGIN_NUM=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .blockRef.l1origin.number) + L1_HEAD=$(cast block --rpc-url $L1_NODE_ADDRESS $((L1_ORIGIN_NUM + 30)) --json | jq -r .hash) + L2_CHAIN_ID=$(cast chain-id --rpc-url $L2_NODE_ADDRESS) + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Building client program for RISC-V target..." + just build-asterisc-client + + echo "Loading client program into Asterisc state format..." + asterisc load-elf --path=$CLIENT_BIN_PATH + + echo "Building host program for native target..." + cargo build --bin kona-host --release + + echo "Running asterisc" + asterisc run \ + --info-at '%10000000' \ + --proof-at never \ + --input $STATE_PATH \ + -- \ + $HOST_BIN_PATH \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l2-chain-id $L2_CHAIN_ID \ + --l1-node-address $L1_NODE_ADDRESS \ + --l1-beacon-address $L1_BEACON_ADDRESS \ + --l2-node-address $L2_NODE_ADDRESS \ + --server \ + --data-dir ./data \ + {{verbosity}} + +# Run the client program natively with the host program attached. +run-client-native block_number l1_rpc l1_beacon_rpc l2_rpc rollup_node_rpc rollup_config_path='' verbosity='': + #!/usr/bin/env bash + set -o errexit -o nounset -o pipefail + + L1_NODE_ADDRESS="{{l1_rpc}}" + L1_BEACON_ADDRESS="{{l1_beacon_rpc}}" + L2_NODE_ADDRESS="{{l2_rpc}}" + OP_NODE_ADDRESS="{{rollup_node_rpc}}" + + L2_CHAIN_ID=$(cast chain-id --rpc-url $L2_NODE_ADDRESS) + if [ -z "{{rollup_config_path}}" ]; then + CHAIN_ID_OR_ROLLUP_CONFIG_ARG="--l2-chain-id $L2_CHAIN_ID" + else + CHAIN_ID_OR_ROLLUP_CONFIG_ARG="--rollup-config-path $(realpath {{rollup_config_path}})" + fi + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + echo "Fetching configuration for block #$CLAIMED_L2_BLOCK_NUMBER..." + + # Get output root for block + CLAIMED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $CLAIMED_L2_BLOCK_NUMBER) | jq -r .outputRoot) + + # Get the info for the previous block + AGREED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .outputRoot) + AGREED_L2_HEAD_HASH=$(cast block --rpc-url $L2_NODE_ADDRESS $((CLAIMED_L2_BLOCK_NUMBER - 1)) --json | jq -r .hash) + L1_ORIGIN_NUM=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .blockRef.l1origin.number) + L1_HEAD=$(cast block --rpc-url $L1_NODE_ADDRESS $((L1_ORIGIN_NUM + 30)) --json | jq -r .hash) + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Running host program with native client program..." + cargo r --bin kona-host --release -- \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l1-node-address $L1_NODE_ADDRESS \ + --l1-beacon-address $L1_BEACON_ADDRESS \ + --l2-node-address $L2_NODE_ADDRESS \ + --native \ + --data-dir ./data \ + $CHAIN_ID_OR_ROLLUP_CONFIG_ARG \ + {{verbosity}} + +# Run the client program natively with the host program attached, in offline mode. +run-client-native-offline block_number l2_claim l2_output_root l2_head l1_head l2_chain_id verbosity='': + #!/usr/bin/env bash + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + CLAIMED_L2_OUTPUT_ROOT={{l2_claim}} + AGREED_L2_OUTPUT_ROOT={{l2_output_root}} + AGREED_L2_HEAD_HASH={{l2_head}} + L1_HEAD={{l1_head}} + L2_CHAIN_ID={{l2_chain_id}} + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Running host program with native client program..." + cargo r --bin kona-host -- \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l2-chain-id $L2_CHAIN_ID \ + --native \ + --data-dir ./data \ + {{verbosity}} + +# Run the client program on asterisc with the host program detached, in offline mode. +run-client-asterisc-offline block_number l2_claim l2_output_root l2_head l1_head l2_chain_id verbosity='': + #!/usr/bin/env bash + + HOST_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/debug/kona-host" + CLIENT_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/riscv64imac-unknown-none-elf/release-client-lto/kona-client" + STATE_PATH="{{KONA_CLIENT_ROOT}}/../../state.bin.gz" + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + CLAIMED_L2_OUTPUT_ROOT={{l2_claim}} + AGREED_L2_OUTPUT_ROOT={{l2_output_root}} + AGREED_L2_HEAD_HASH={{l2_head}} + L1_HEAD={{l1_head}} + L2_CHAIN_ID={{l2_chain_id}} + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Building client program for RISC-V target..." + just build-asterisc-client + + echo "Loading client program into Asterisc state format..." + asterisc load-elf --path=$CLIENT_BIN_PATH + + echo "Building host program for native target..." + cargo build --bin kona-host + + echo "Running asterisc" + asterisc run \ + --info-at '%10000000' \ + --proof-at never \ + --input $STATE_PATH \ + -- \ + $HOST_BIN_PATH \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l2-chain-id $L2_CHAIN_ID \ + --server \ + --data-dir ./data \ + {{verbosity}} + +# Run the client program on cannon with the host in detached server mode. +run-client-cannon block_number l1_rpc l1_beacon_rpc l2_rpc rollup_node_rpc rollup_config_path='' verbosity='': + #!/usr/bin/env bash + set -o errexit -o nounset -o pipefail + + HOST_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/release/kona-host" + CLIENT_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/mips64-unknown-none/release-client-lto/kona-client" + STATE_PATH="{{KONA_CLIENT_ROOT}}/../../state.bin.gz" + + L1_NODE_ADDRESS="{{l1_rpc}}" + L1_BEACON_ADDRESS="{{l1_beacon_rpc}}" + L2_NODE_ADDRESS="{{l2_rpc}}" + OP_NODE_ADDRESS="{{rollup_node_rpc}}" + + L2_CHAIN_ID=$(cast chain-id --rpc-url $L2_NODE_ADDRESS) + if [ -z "{{rollup_config_path}}" ]; then + CHAIN_ID_OR_ROLLUP_CONFIG_ARG="--l2-chain-id $L2_CHAIN_ID" + else + CHAIN_ID_OR_ROLLUP_CONFIG_ARG="--rollup-config-path $(realpath {{rollup_config_path}})" + fi + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + echo "Fetching configuration for block #$CLAIMED_L2_BLOCK_NUMBER..." + + # Get output root for block + CLAIMED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $CLAIMED_L2_BLOCK_NUMBER) | jq -r .outputRoot) + + # Get the info for the previous block + AGREED_L2_OUTPUT_ROOT=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .outputRoot) + AGREED_L2_HEAD_HASH=$(cast block --rpc-url $L2_NODE_ADDRESS $((CLAIMED_L2_BLOCK_NUMBER - 1)) --json | jq -r .hash) + L1_ORIGIN_NUM=$(cast rpc --rpc-url $OP_NODE_ADDRESS "optimism_outputAtBlock" $(cast 2h $((CLAIMED_L2_BLOCK_NUMBER - 1))) | jq -r .blockRef.l1origin.number) + L1_HEAD=$(cast block --rpc-url $L1_NODE_ADDRESS $((L1_ORIGIN_NUM + 30)) --json | jq -r .hash) + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Building client program for MIPS64 target..." + just build-cannon-client + + echo "Loading client program into Cannon state format..." + cannon load-elf --path=$CLIENT_BIN_PATH --type multithreaded64-5 + + echo "Building host program for native target..." + cargo build --bin kona-host --release + + echo "Running cannon" + cannon run \ + --info-at '%10000000' \ + --proof-at never \ + --input $STATE_PATH \ + -- \ + $HOST_BIN_PATH \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l2-chain-id $L2_CHAIN_ID \ + --l1-node-address $L1_NODE_ADDRESS \ + --l1-beacon-address $L1_BEACON_ADDRESS \ + --l2-node-address $L2_NODE_ADDRESS \ + --server \ + --data-dir ./data \ + {{verbosity}} + +# Run the client program on cannon with the host program detached, in offline mode. +run-client-cannon-offline block_number l2_claim l2_output_root l2_head l1_head l2_chain_id verbosity='': + #!/usr/bin/env bash + + HOST_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/debug/kona-host" + CLIENT_BIN_PATH="{{KONA_CLIENT_ROOT}}/../../target/mips64-unknown-none/release-client-lto/kona-client" + STATE_PATH="{{KONA_CLIENT_ROOT}}/../../state.bin.gz" + + CLAIMED_L2_BLOCK_NUMBER={{block_number}} + CLAIMED_L2_OUTPUT_ROOT={{l2_claim}} + AGREED_L2_OUTPUT_ROOT={{l2_output_root}} + AGREED_L2_HEAD_HASH={{l2_head}} + L1_HEAD={{l1_head}} + L2_CHAIN_ID={{l2_chain_id}} + + # Move to the kona root + cd {{KONA_CLIENT_ROOT}}/../.. + + echo "Building client program for MIPS64 target..." + just build-cannon-client + + echo "Loading client program into Cannon state format..." + cannon load-elf --path=$CLIENT_BIN_PATH --type multithreaded64-5 + + echo "Building host program for native target..." + cargo build --bin kona-host + + echo "Running cannon" + cannon run \ + --info-at '%10000000' \ + --proof-at never \ + --input $STATE_PATH \ + -- \ + $HOST_BIN_PATH \ + single \ + --l1-head $L1_HEAD \ + --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ + --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ + --agreed-l2-output-root $AGREED_L2_OUTPUT_ROOT \ + --claimed-l2-block-number $CLAIMED_L2_BLOCK_NUMBER \ + --l2-chain-id $L2_CHAIN_ID \ + --server \ + --data-dir ./data \ + {{verbosity}} diff --git a/kona/bin/client/src/fpvm_evm/factory.rs b/kona/bin/client/src/fpvm_evm/factory.rs new file mode 100644 index 0000000000000..757a36c5c6fd4 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/factory.rs @@ -0,0 +1,107 @@ +//! [`EvmFactory`] implementation for the EVM in the FPVM environment. + +use super::precompiles::OpFpvmPrecompiles; +use alloy_evm::{Database, EvmEnv, EvmFactory}; +use alloy_op_evm::OpEvm; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use op_revm::{ + DefaultOp, OpContext, OpEvm as RevmOpEvm, OpHaltReason, OpSpecId, OpTransaction, + OpTransactionError, +}; +use revm::{ + Context, Inspector, + context::{BlockEnv, Evm as RevmEvm, FrameStack, TxEnv, result::EVMError}, + handler::instructions::EthInstructions, + inspector::NoOpInspector, +}; + +/// Factory producing [`OpEvm`]s with FPVM-accelerated precompile overrides enabled. +#[derive(Debug, Clone)] +pub struct FpvmOpEvmFactory { + /// The hint writer. + hint_writer: H, + /// The oracle reader. + oracle_reader: O, +} + +impl FpvmOpEvmFactory +where + H: HintWriterClient + Clone + Send + Sync, + O: PreimageOracleClient + Clone + Send + Sync, +{ + /// Creates a new [`FpvmOpEvmFactory`]. + pub fn new(hint_writer: H, oracle_reader: O) -> Self { + Self { hint_writer, oracle_reader } + } + + /// Returns a reference to the inner [`HintWriterClient`]. + pub fn hint_writer(&self) -> &H { + &self.hint_writer + } + + /// Returns a reference to the inner [`PreimageOracleClient`]. + pub fn oracle_reader(&self) -> &O { + &self.oracle_reader + } +} + +impl EvmFactory for FpvmOpEvmFactory +where + H: HintWriterClient + Clone + Send + Sync + 'static, + O: PreimageOracleClient + Clone + Send + Sync + 'static, +{ + type Evm>> = OpEvm>; + type Context = OpContext; + type Tx = OpTransaction; + type Error = + EVMError; + type HaltReason = OpHaltReason; + type Spec = OpSpecId; + type Precompiles = OpFpvmPrecompiles; + type BlockEnv = BlockEnv; + + fn create_evm( + &self, + db: DB, + input: EvmEnv, + ) -> Self::Evm { + let spec_id = *input.spec_id(); + let ctx = Context::op().with_db(db).with_block(input.block_env).with_cfg(input.cfg_env); + let revm_evm = RevmOpEvm(RevmEvm { + ctx, + inspector: NoOpInspector {}, + instruction: EthInstructions::new_mainnet(), + precompiles: OpFpvmPrecompiles::new_with_spec( + spec_id, + self.hint_writer.clone(), + self.oracle_reader.clone(), + ), + frame_stack: FrameStack::new(), + }); + + OpEvm::new(revm_evm, false) + } + + fn create_evm_with_inspector>>( + &self, + db: DB, + input: EvmEnv, + inspector: I, + ) -> Self::Evm { + let spec_id = *input.spec_id(); + let ctx = Context::op().with_db(db).with_block(input.block_env).with_cfg(input.cfg_env); + let revm_evm = RevmOpEvm(RevmEvm { + ctx, + inspector, + instruction: EthInstructions::new_mainnet(), + precompiles: OpFpvmPrecompiles::new_with_spec( + spec_id, + self.hint_writer.clone(), + self.oracle_reader.clone(), + ), + frame_stack: FrameStack::new(), + }); + + OpEvm::new(revm_evm, true) + } +} diff --git a/kona/bin/client/src/fpvm_evm/mod.rs b/kona/bin/client/src/fpvm_evm/mod.rs new file mode 100644 index 0000000000000..12e30bb8205d5 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/mod.rs @@ -0,0 +1,8 @@ +//! Custom [`EvmFactory`] for the fault proof virtual machine's EVM. +//! +//! [`EvmFactory`]: alloy_evm::EvmFactory + +mod precompiles; + +mod factory; +pub use factory::FpvmOpEvmFactory; diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_add.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_add.rs new file mode 100644 index 0000000000000..0db40ef09ae58 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_add.rs @@ -0,0 +1,103 @@ +//! Contains the accelerated precompile for the BLS12-381 curve G1 Point Addition. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/g1_add.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{G1_ADD_BASE_GAS_FEE, G1_ADD_INPUT_LENGTH}, +}; + +/// Performs an FPVM-accelerated BLS12-381 G1 addition check. +/// +/// Notice, there is no input size limit for this precompile. +/// See: +pub(crate) fn fpvm_bls12_g1_add( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if G1_ADD_BASE_GAS_FEE > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let input_len = input.len(); + if input_len != G1_ADD_INPUT_LENGTH { + return Err(PrecompileError::Other( + alloc::format!( + "G1 addition input length should be {G1_ADD_INPUT_LENGTH} bytes, was {input_len}" + ) + .into(), + )); + } + + let precompile = bls12_381::g1_add::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &G1_ADD_BASE_GAS_FEE.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(G1_ADD_BASE_GAS_FEE, result_data.into())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_add() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // G1.INF + G1.INF = G1.INF + let input = [0u8; G1_ADD_INPUT_LENGTH]; + let accelerated_result = + fpvm_bls12_g1_add(&input, u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile( + *bls12_381::g1_add::PRECOMPILE.address(), + input, + u64::MAX, + ) + .unwrap(); + + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_add_bad_input_len() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g1_add(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_add_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g1_add(&[], 0, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_msm.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_msm.rs new file mode 100644 index 0000000000000..cf118851e081c --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g1_msm.rs @@ -0,0 +1,209 @@ +//! Contains the accelerated precompile for the BLS12-381 curve G1 MSM. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/g1_msm.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{DISCOUNT_TABLE_G1_MSM, G1_MSM_BASE_GAS_FEE, G1_MSM_INPUT_LENGTH}, + bls12_381_utils::msm_required_gas, +}; + +/// The maximum input size for the BLS12-381 g1 msm operation after the Isthmus Hardfork. +/// +/// See: +const BLS12_MAX_G1_MSM_SIZE_ISTHMUS: usize = 513760; + +/// The maximum input size for the BLS12-381 g1 msm operation after the Jovian Hardfork. +const BLS12_MAX_G1_MSM_SIZE_JOVIAN: usize = 288_960; + +/// Performs an FPVM-accelerated `bls12` g1 msm check precompile call after the Isthmus Hardfork. +pub(crate) fn fpvm_bls12_g1_msm( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BLS12_MAX_G1_MSM_SIZE_ISTHMUS { + return Err(PrecompileError::Other( + alloc::format!("G1MSM input length must be at most {BLS12_MAX_G1_MSM_SIZE_ISTHMUS}") + .into(), + )); + } + + let input_len = input.len(); + if input_len == 0 || !input_len.is_multiple_of(G1_MSM_INPUT_LENGTH) { + return Err(PrecompileError::Other( + alloc::format!( + "G1MSM input length should be multiple of {G1_MSM_INPUT_LENGTH}, was {input_len}" + ) + .into(), + )); + } + + let k = input_len / G1_MSM_INPUT_LENGTH; + let required_gas = msm_required_gas(k, &DISCOUNT_TABLE_G1_MSM, G1_MSM_BASE_GAS_FEE); + if required_gas > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let precompile = bls12_381::g1_msm::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &required_gas.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(required_gas, result_data.into())) +} + +/// Performs an FPVM-accelerated `bls12` g1 msm check precompile call after the Jovian Hardfork. +pub(crate) fn fpvm_bls12_g1_msm_jovian( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BLS12_MAX_G1_MSM_SIZE_JOVIAN { + return Err(PrecompileError::Other( + alloc::format!("G1MSM input length must be at most {BLS12_MAX_G1_MSM_SIZE_JOVIAN}") + .into(), + )); + } + + fpvm_bls12_g1_msm(input, gas_limit, hint_writer, oracle_reader) +} + +#[cfg(test)] +mod test { + use alloy_primitives::hex; + + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + // https://raw.githubusercontent.com/ethereum/execution-spec-tests/a1c4eeff347a64ad6c5aedd51314d4ffc067346b/tests/prague/eip2537_bls_12_381_precompiles/vectors/msm_G1_bls.json + const TEST_INPUT: [u8; 160] = hex!( + "0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e10000000000000000000000000000000000000000000000000000000000000002" + ); + const EXPECTED_OUTPUT: [u8; 128] = hex!( + "000000000000000000000000000000000572cbea904d67468808c8eb50a9450c9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e00000000000000000000000000000000166a9d8cabc673a322fda673779d8e3822ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28" + ); + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g1_msm(TEST_INPUT.as_ref(), 12000, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile( + *bls12_381::g1_msm::PRECOMPILE.address(), + TEST_INPUT.as_ref(), + 12000, + ) + .unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_OUTPUT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_bad_input_len_isthmus() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = fpvm_bls12_g1_msm( + &[0u8; BLS12_MAX_G1_MSM_SIZE_ISTHMUS + 1], + u64::MAX, + hint_writer, + oracle_reader, + ) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_empty_input() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g1_msm(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_unaligned_input() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = fpvm_bls12_g1_msm( + &[0u8; G1_MSM_INPUT_LENGTH - 1], + u64::MAX, + hint_writer, + oracle_reader, + ) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g1_msm(&[0u8; G1_MSM_INPUT_LENGTH], 0, hint_writer, oracle_reader) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let base_result = + fpvm_bls12_g1_msm(TEST_INPUT.as_ref(), 12000, hint_writer, oracle_reader).unwrap(); + let jovian_result = + fpvm_bls12_g1_msm_jovian(TEST_INPUT.as_ref(), 12000, hint_writer, oracle_reader) + .unwrap(); + + assert_eq!(base_result.bytes, jovian_result.bytes); + assert_eq!(base_result.gas_used, jovian_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g1_msm_bad_input_len_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // Next aligned size (multiple of G1_MSM_INPUT_LENGTH) that exceeds + // BLS12_MAX_G1_MSM_SIZE_JOVIAN + const INPUT_SIZE: usize = + ((BLS12_MAX_G1_MSM_SIZE_JOVIAN / G1_MSM_INPUT_LENGTH) + 1) * G1_MSM_INPUT_LENGTH; + let input = [0u8; INPUT_SIZE]; + let accelerated_result = + fpvm_bls12_g1_msm_jovian(&input, u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_add.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_add.rs new file mode 100644 index 0000000000000..be23342861ab9 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_add.rs @@ -0,0 +1,103 @@ +//! Contains the accelerated precompile for the BLS12-381 curve G2 Point Addition. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/g2_add.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{G2_ADD_BASE_GAS_FEE, G2_ADD_INPUT_LENGTH}, +}; + +/// Performs an FPVM-accelerated BLS12-381 G2 addition check. +/// +/// Notice, there is no input size limit for this precompile. +/// See: +pub(crate) fn fpvm_bls12_g2_add( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if G2_ADD_BASE_GAS_FEE > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let input_len = input.len(); + if input_len != G2_ADD_INPUT_LENGTH { + return Err(PrecompileError::Other( + alloc::format!( + "G2 addition input length should be {G2_ADD_INPUT_LENGTH} bytes, was {input_len}" + ) + .into(), + )); + } + + let precompile = bls12_381::g2_add::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &G2_ADD_BASE_GAS_FEE.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(G2_ADD_BASE_GAS_FEE, result_data.into())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_add() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // G2.INF + G2.INF = G2.INF + let input = [0u8; G2_ADD_INPUT_LENGTH]; + let accelerated_result = + fpvm_bls12_g2_add(&input, u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile( + *bls12_381::g2_add::PRECOMPILE.address(), + input, + u64::MAX, + ) + .unwrap(); + + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_add_bad_input_len() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g2_add(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_add_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g2_add(&[], 0, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_msm.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_msm.rs new file mode 100644 index 0000000000000..f19d2c1533553 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_g2_msm.rs @@ -0,0 +1,195 @@ +//! Contains the accelerated precompile for the BLS12-381 curve G2 MSM. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/g2_msm.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{DISCOUNT_TABLE_G2_MSM, G2_MSM_BASE_GAS_FEE, G2_MSM_INPUT_LENGTH}, + bls12_381_utils::msm_required_gas, +}; + +/// The maximum input size for the BLS12-381 g2 msm operation after the Isthmus Hardfork. +/// +/// See: +const BLS12_MAX_G2_MSM_SIZE_ISTHMUS: usize = 488448; + +/// The maximum input size for the BLS12-381 g2 msm operation after the Jovian Hardfork. +const BLS12_MAX_G2_MSM_SIZE_JOVIAN: usize = 278_784; + +/// Performs an FPVM-accelerated BLS12-381 G2 msm check after the Isthmus Hardfork. +pub(crate) fn fpvm_bls12_g2_msm( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let input_len = input.len(); + + if input_len > BLS12_MAX_G2_MSM_SIZE_ISTHMUS { + return Err(PrecompileError::Other( + alloc::format!("G2MSM input length must be at most {BLS12_MAX_G2_MSM_SIZE_ISTHMUS}") + .into(), + )); + } + + if input_len == 0 || !input_len.is_multiple_of(G2_MSM_INPUT_LENGTH) { + return Err(PrecompileError::Other( + alloc::format!( + "G2MSM input length should be multiple of {G2_MSM_INPUT_LENGTH}, was {input_len}" + ) + .into(), + )); + } + + let k = input_len / G2_MSM_INPUT_LENGTH; + let required_gas = msm_required_gas(k, &DISCOUNT_TABLE_G2_MSM, G2_MSM_BASE_GAS_FEE); + if required_gas > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let precompile = bls12_381::g2_msm::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &required_gas.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(required_gas, result_data.into())) +} + +/// Performs an FPVM-accelerated BLS12-381 G2 msm check after the Jovian Hardfork. +pub(crate) fn fpvm_bls12_g2_msm_jovian( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BLS12_MAX_G2_MSM_SIZE_JOVIAN { + return Err(PrecompileError::Other( + alloc::format!("G2MSM input length must be at most {BLS12_MAX_G2_MSM_SIZE_JOVIAN}") + .into(), + )); + } + + fpvm_bls12_g2_msm(input, gas_limit, hint_writer, oracle_reader) +} + +#[cfg(test)] +mod test { + use alloy_primitives::hex; + + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + // https://raw.githubusercontent.com/ethereum/execution-spec-tests/a1c4eeff347a64ad6c5aedd51314d4ffc067346b/tests/prague/eip2537_bls_12_381_precompiles/vectors/msm_G2_bls.json + const TEST_INPUT: [u8; 288] = hex!( + "00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80000000000000000000000000000000013e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be0000000000000000000000000000000000000000000000000000000000000002" + ); + const EXPECTED_OUTPUT: [u8; 256] = hex!( + "000000000000000000000000000000001638533957d540a9d2370f17cc7ed5863bc0b995b8825e0ee1ea1e1e4d00dbae81f14b0bf3611b78c952aacab827a053000000000000000000000000000000000a4edef9c1ed7f729f520e47730a124fd70662a904ba1074728114d1031e1572c6c886f6b57ec72a6178288c47c33577000000000000000000000000000000000468fb440d82b0630aeb8dca2b5256789a66da69bf91009cbfe6bd221e47aa8ae88dece9764bf3bd999d95d71e4c9899000000000000000000000000000000000f6d4552fa65dd2638b361543f887136a43253d9c66c411697003f7a13c308f5422e1aa0a59c8967acdefd8b6e36ccf3" + ); + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g2_msm(TEST_INPUT.as_ref(), 22500, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile( + *bls12_381::g2_msm::PRECOMPILE.address(), + TEST_INPUT.as_ref(), + 22500, + ) + .unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_OUTPUT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm_bad_input_len_isthmus() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = fpvm_bls12_g2_msm( + &[0u8; BLS12_MAX_G2_MSM_SIZE_ISTHMUS + 1], + u64::MAX, + hint_writer, + oracle_reader, + ) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm_bad_input_len() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g2_msm(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_g2_msm(&[0u8; G2_MSM_INPUT_LENGTH], 0, hint_writer, oracle_reader) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let base_result = + fpvm_bls12_g2_msm(TEST_INPUT.as_ref(), 22500, hint_writer, oracle_reader).unwrap(); + let jovian_result = + fpvm_bls12_g2_msm_jovian(TEST_INPUT.as_ref(), 22500, hint_writer, oracle_reader) + .unwrap(); + + assert_eq!(base_result.bytes, jovian_result.bytes); + assert_eq!(base_result.gas_used, jovian_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_g2_msm_bad_input_len_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // Calculate the next aligned size (multiple of G2_MSM_INPUT_LENGTH) that exceeds + // BLS12_MAX_G2_MSM_SIZE_JOVIAN + const INPUT_SIZE: usize = + ((BLS12_MAX_G2_MSM_SIZE_JOVIAN / G2_MSM_INPUT_LENGTH) + 1) * G2_MSM_INPUT_LENGTH; + let input = [0u8; INPUT_SIZE]; + let accelerated_result = + fpvm_bls12_g2_msm_jovian(&input, u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp.rs new file mode 100644 index 0000000000000..935462123b833 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp.rs @@ -0,0 +1,102 @@ +//! Contains the accelerated precompile for the BLS12-381 curve FP to G1 Mapping. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/map_fp_to_g1.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{MAP_FP_TO_G1_BASE_GAS_FEE, PADDED_FP_LENGTH}, +}; + +/// Performs an FPVM-accelerated BLS12-381 map fp check. +/// +/// Notice, there is no input size limit for this precompile. +/// See: +pub(crate) fn fpvm_bls12_map_fp( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if MAP_FP_TO_G1_BASE_GAS_FEE > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + if input.len() != PADDED_FP_LENGTH { + return Err(PrecompileError::Other( + alloc::format!( + "MAP_FP_TO_G1 input should be {PADDED_FP_LENGTH} bytes, was {}", + input.len() + ) + .into(), + )); + } + + let precompile = bls12_381::map_fp_to_g1::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &MAP_FP_TO_G1_BASE_GAS_FEE.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(MAP_FP_TO_G1_BASE_GAS_FEE, result_data.into())) +} + +#[cfg(test)] +mod test { + use alloy_primitives::hex; + + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g1() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // https://github.com/ethereum/execution-spec-tests/blob/a1c4eeff347a64ad6c5aedd51314d4ffc067346b/tests/prague/eip2537_bls_12_381_precompiles/vectors/map_fp_to_G1_bls.json + let input = hex!("00000000000000000000000000000000156c8a6a2c184569d69a76be144b5cdc5141d2d2ca4fe341f011e25e3969c55ad9e9b9ce2eb833c81a908e5fa4ac5f03"); + let expected = hex!("00000000000000000000000000000000184bb665c37ff561a89ec2122dd343f20e0f4cbcaec84e3c3052ea81d1834e192c426074b02ed3dca4e7676ce4ce48ba0000000000000000000000000000000004407b8d35af4dacc809927071fc0405218f1401a6d15af775810e4e460064bcc9468beeba82fdc751be70476c888bf3"); + + let accelerated_result = fpvm_bls12_map_fp(&input, 5500, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile(*bls12_381::map_fp_to_g1::PRECOMPILE.address(), input, 5500).unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), expected.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g1_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_map_fp(&[], 0, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g1_bad_input_size() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_map_fp(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp2.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp2.rs new file mode 100644 index 0000000000000..44d111be66e2f --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_map_fp2.rs @@ -0,0 +1,102 @@ +//! Contains the accelerated precompile for the BLS12-381 curve FP2 to G2 Mapping. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/map_fp_to_g1.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{MAP_FP2_TO_G2_BASE_GAS_FEE, PADDED_FP2_LENGTH}, +}; + +/// Performs an FPVM-accelerated BLS12-381 map fp2 check. +/// +/// Notice, there is no input size limit for this precompile. +/// See: +pub(crate) fn fpvm_bls12_map_fp2( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if MAP_FP2_TO_G2_BASE_GAS_FEE > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + if input.len() != PADDED_FP2_LENGTH { + return Err(PrecompileError::Other( + alloc::format!( + "MAP_FP2_TO_G2 input should be {PADDED_FP2_LENGTH} bytes, was {}", + input.len() + ) + .into(), + )); + } + + let precompile = bls12_381::map_fp2_to_g2::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &MAP_FP2_TO_G2_BASE_GAS_FEE.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(MAP_FP2_TO_G2_BASE_GAS_FEE, result_data.into())) +} + +#[cfg(test)] +mod test { + use alloy_primitives::hex; + + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g2() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // https://github.com/ethereum/execution-spec-tests/blob/a1c4eeff347a64ad6c5aedd51314d4ffc067346b/tests/prague/eip2537_bls_12_381_precompiles/vectors/map_fp2_to_G2_bls.json + let input = hex!("0000000000000000000000000000000007355d25caf6e7f2f0cb2812ca0e513bd026ed09dda65b177500fa31714e09ea0ded3a078b526bed3307f804d4b93b040000000000000000000000000000000002829ce3c021339ccb5caf3e187f6370e1e2a311dec9b75363117063ab2015603ff52c3d3b98f19c2f65575e99e8b78c"); + let expected = hex!("0000000000000000000000000000000000e7f4568a82b4b7dc1f14c6aaa055edf51502319c723c4dc2688c7fe5944c213f510328082396515734b6612c4e7bb700000000000000000000000000000000126b855e9e69b1f691f816e48ac6977664d24d99f8724868a184186469ddfd4617367e94527d4b74fc86413483afb35b000000000000000000000000000000000caead0fd7b6176c01436833c79d305c78be307da5f6af6c133c47311def6ff1e0babf57a0fb5539fce7ee12407b0a42000000000000000000000000000000001498aadcf7ae2b345243e281ae076df6de84455d766ab6fcdaad71fab60abb2e8b980a440043cd305db09d283c895e3d"); + + let accelerated_result = fpvm_bls12_map_fp2(&input, 23800, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile(*bls12_381::map_fp2_to_g2::PRECOMPILE.address(), input, 23800).unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), expected.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g2_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_map_fp2(&[], 0, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_map_fp_g2_bad_input_size() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_map_fp2(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bls12_pair.rs b/kona/bin/client/src/fpvm_evm/precompiles/bls12_pair.rs new file mode 100644 index 0000000000000..3536ff78497ee --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bls12_pair.rs @@ -0,0 +1,192 @@ +//! Contains the accelerated precompile for the BLS12-381 curve. +//! +//! BLS12-381 is introduced in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). +//! +//! For constants and logic, see the [revm implementation]. +//! +//! [revm implementation]: https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bls12_381/pairing.rs + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, bls12_381, + bls12_381_const::{PAIRING_INPUT_LENGTH, PAIRING_MULTIPLIER_BASE, PAIRING_OFFSET_BASE}, +}; + +/// The max pairing size for BLS12-381 input given a 20M gas limit. +const BLS12_MAX_PAIRING_SIZE_ISTHMUS: usize = 235_008; + +/// The max pairing size for BLS12-381 input after the Jovian Hardfork. +const BLS12_MAX_PAIRING_SIZE_JOVIAN: usize = 156_672; + +/// Performs an FPVM-accelerated BLS12-381 pairing check after the Isthmus Hardfork. +pub(crate) fn fpvm_bls12_pairing( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let input_len = input.len(); + + if input_len > BLS12_MAX_PAIRING_SIZE_ISTHMUS { + return Err(PrecompileError::Other( + alloc::format!("Pairing input length must be at most {BLS12_MAX_PAIRING_SIZE_ISTHMUS}") + .into(), + )); + } + + if !input_len.is_multiple_of(PAIRING_INPUT_LENGTH) { + return Err(PrecompileError::Other( + alloc::format!( + "Pairing input length should be multiple of {PAIRING_INPUT_LENGTH}, was {input_len}" + ) + .into(), + )); + } + + let k = input_len / PAIRING_INPUT_LENGTH; + let required_gas: u64 = PAIRING_MULTIPLIER_BASE * k as u64 + PAIRING_OFFSET_BASE; + if required_gas > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let precompile = bls12_381::pairing::PRECOMPILE; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &required_gas.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(required_gas, result_data.into())) +} + +/// Performs an FPVM-accelerated BLS12-381 pairing check after the Jovian Hardfork. +pub(crate) fn fpvm_bls12_pairing_jovian( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BLS12_MAX_PAIRING_SIZE_JOVIAN { + return Err(PrecompileError::Other( + alloc::format!("Pairing input length must be at most {BLS12_MAX_PAIRING_SIZE_JOVIAN}") + .into(), + )); + } + + fpvm_bls12_pairing(input, gas_limit, hint_writer, oracle_reader) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + use alloy_primitives::hex; + + // https://github.com/ethereum/execution-spec-tests/blob/a1c4eeff347a64ad6c5aedd51314d4ffc067346b/tests/prague/eip2537_bls_12_381_precompiles/vectors/pairing_check_bls.json + const TEST_INPUT: [u8; 384] = hex!( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + const EXPECTED_OUTPUT: [u8; 32] = + hex!("0000000000000000000000000000000000000000000000000000000000000001"); + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_pairing(TEST_INPUT.as_ref(), 70300, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile( + *bls12_381::pairing::PRECOMPILE.address(), + TEST_INPUT.as_ref(), + 70300, + ) + .unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_OUTPUT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing_bad_input_len_isthmus() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = fpvm_bls12_pairing( + &[0u8; BLS12_MAX_PAIRING_SIZE_ISTHMUS + 1], + 0, + hint_writer, + oracle_reader, + ) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing_bad_input_len() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_pairing(&[0u8; PAIRING_INPUT_LENGTH - 1], 0, hint_writer, oracle_reader) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing_bad_gas_limit() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bls12_pairing(&[0u8; PAIRING_INPUT_LENGTH], 0, hint_writer, oracle_reader) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let base_result = + fpvm_bls12_pairing(TEST_INPUT.as_ref(), 70300, hint_writer, oracle_reader).unwrap(); + let jovian_result = + fpvm_bls12_pairing_jovian(TEST_INPUT.as_ref(), 70300, hint_writer, oracle_reader) + .unwrap(); + + assert_eq!(base_result.bytes, jovian_result.bytes); + assert_eq!(base_result.gas_used, jovian_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bls12_381_pairing_bad_input_len_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // Calculate the next aligned size (multiple of PAIRING_INPUT_LENGTH) that exceeds + // BLS12_MAX_PAIRING_SIZE_JOVIAN + const INPUT_SIZE: usize = + ((BLS12_MAX_PAIRING_SIZE_JOVIAN / PAIRING_INPUT_LENGTH) + 1) * PAIRING_INPUT_LENGTH; + let input = [0u8; INPUT_SIZE]; + let accelerated_result = + fpvm_bls12_pairing_jovian(&input, u64::MAX, hint_writer, oracle_reader) + .unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::Other(_))); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/bn128_pair.rs b/kona/bin/client/src/fpvm_evm/precompiles/bn128_pair.rs new file mode 100644 index 0000000000000..aba64db74bd8c --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/bn128_pair.rs @@ -0,0 +1,207 @@ +//! Contains the accelerated version of the `ecPairing` precompile. + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{ + PrecompileError, PrecompileOutput, PrecompileResult, + bn254::{ + PAIR_ELEMENT_LEN, + pair::{self, ISTANBUL_PAIR_BASE, ISTANBUL_PAIR_PER_POINT}, + }, +}; + +const BN256_MAX_PAIRING_SIZE_GRANITE: usize = 112_687; +const BN256_MAX_PAIRING_SIZE_JOVIAN: usize = 81_984; + +/// Runs the FPVM-accelerated `ecpairing` precompile call. +pub(crate) fn fpvm_bn128_pair( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let gas_used = + (input.len() / PAIR_ELEMENT_LEN) as u64 * ISTANBUL_PAIR_PER_POINT + ISTANBUL_PAIR_BASE; + + if gas_used > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + if !input.len().is_multiple_of(PAIR_ELEMENT_LEN) { + return Err(PrecompileError::Bn254PairLength); + } + + let precompile = pair::ISTANBUL; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[precompile.address().as_slice(), &gas_used.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(gas_used, result_data.into())) +} + +/// Runs the FPVM-accelerated `ecpairing` precompile call, with the input size limited by the +/// Granite hardfork. +pub(crate) fn fpvm_bn128_pair_granite( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BN256_MAX_PAIRING_SIZE_GRANITE { + return Err(PrecompileError::Bn254PairLength); + } + + fpvm_bn128_pair(input, gas_limit, hint_writer, oracle_reader) +} + +/// Runs the FPVM-accelerated `ecpairing` precompile call, with the input size limited by the +/// Jovian hardfork. +pub(crate) fn fpvm_bn128_pair_jovian( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + if input.len() > BN256_MAX_PAIRING_SIZE_JOVIAN { + return Err(PrecompileError::Bn254PairLength); + } + + fpvm_bn128_pair(input, gas_limit, hint_writer, oracle_reader) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + use alloy_primitives::hex; + + const TEST_INPUT: [u8; 384] = hex!( + "2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f61fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d92bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f902fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e000000000000000000000000000000000000000000000000000000000000000130644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd451971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc72a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea223a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc" + ); + const EXPECTED_OUTPUT: [u8; 32] = + hex!("0000000000000000000000000000000000000000000000000000000000000001"); + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bn128_pair(TEST_INPUT.as_ref(), u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = + execute_native_precompile(*pair::ISTANBUL.address(), TEST_INPUT.as_ref(), u64::MAX) + .unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_OUTPUT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_granite() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_bn128_pair_granite(TEST_INPUT.as_ref(), u64::MAX, hint_writer, oracle_reader) + .unwrap(); + let native_result = + execute_native_precompile(*pair::ISTANBUL.address(), TEST_INPUT.as_ref(), u64::MAX) + .unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_OUTPUT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_not_enough_gas() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let input = hex!("0badc0de"); + let accelerated_result = + fpvm_bn128_pair(&input, 0, hint_writer, oracle_reader).unwrap_err(); + + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_bad_input_len() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let input = hex!("0badc0de"); + let accelerated_result = + fpvm_bn128_pair(&input, u64::MAX, hint_writer, oracle_reader).unwrap_err(); + + assert!(matches!(accelerated_result, PrecompileError::Bn254PairLength)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_bad_input_len_granite() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // Calculate the next aligned size (multiple of PAIR_ELEMENT_LEN) that exceeds + // BN256_MAX_PAIRING_SIZE_GRANITE + const INPUT_SIZE: usize = + ((BN256_MAX_PAIRING_SIZE_GRANITE / PAIR_ELEMENT_LEN) + 1) * PAIR_ELEMENT_LEN; + let input = [0u8; INPUT_SIZE]; + let accelerated_result = + fpvm_bn128_pair_granite(&input, u64::MAX, hint_writer, oracle_reader).unwrap_err(); + + assert!(matches!(accelerated_result, PrecompileError::Bn254PairLength)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let granite_result = + fpvm_bn128_pair_granite(TEST_INPUT.as_ref(), u64::MAX, hint_writer, oracle_reader) + .unwrap(); + let jovian_result = + fpvm_bn128_pair_jovian(TEST_INPUT.as_ref(), u64::MAX, hint_writer, oracle_reader) + .unwrap(); + + assert_eq!(granite_result.bytes, jovian_result.bytes); + assert_eq!(granite_result.gas_used, jovian_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_bn128_pairing_bad_input_len_jovian() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + // Calculate the next aligned size (multiple of PAIR_ELEMENT_LEN) that exceeds + // BN256_MAX_PAIRING_SIZE_JOVIAN + const INPUT_SIZE: usize = + ((BN256_MAX_PAIRING_SIZE_JOVIAN / PAIR_ELEMENT_LEN) + 1) * PAIR_ELEMENT_LEN; + let input = [0u8; INPUT_SIZE]; + let accelerated_result = + fpvm_bn128_pair_jovian(&input, u64::MAX, hint_writer, oracle_reader).unwrap_err(); + + assert!(matches!(accelerated_result, PrecompileError::Bn254PairLength)); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/ecrecover.rs b/kona/bin/client/src/fpvm_evm/precompiles/ecrecover.rs new file mode 100644 index 0000000000000..16525744483ec --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/ecrecover.rs @@ -0,0 +1,99 @@ +//! Contains the accelerated version of the `ecrecover` precompile. + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use alloy_primitives::Address; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{PrecompileError, PrecompileOutput, PrecompileResult}; + +/// Address of the `ecrecover` precompile. +pub(crate) const ECRECOVER_ADDR: Address = revm::precompile::u64_to_address(1); + +/// Runs the FPVM-accelerated `ecrecover` precompile call. +pub(crate) fn fpvm_ec_recover( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + const ECRECOVER_BASE: u64 = 3_000; + + if ECRECOVER_BASE > gas_limit { + return Err(PrecompileError::OutOfGas); + } + + let truncated_input = &input[..input.len().min(128)]; + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[ECRECOVER_ADDR.as_slice(), &ECRECOVER_BASE.to_be_bytes(), truncated_input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into())) + .unwrap_or_default(); + + Ok(PrecompileOutput::new(ECRECOVER_BASE, result_data.into())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + use alloy_primitives::hex; + + const TEST_INPUT: [u8; 128] = hex!( + "456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3000000000000000000000000000000000000000000000000000000000000001c9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac80388256084f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada" + ); + const EXPECTED_RESULT: [u8; 32] = + hex!("0000000000000000000000007156526fbd7a3c72969b54f64e42c10fbb768c8a"); + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_ecrecover() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_ec_recover(&TEST_INPUT, u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = + execute_native_precompile(ECRECOVER_ADDR, TEST_INPUT, u64::MAX).unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_RESULT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_ecrecover_out_of_gas() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_ec_recover(&[], 0, hint_writer, oracle_reader).unwrap_err(); + + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_ecrecover_with_extra_bytes() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let mut input_with_extra = TEST_INPUT.to_vec(); + input_with_extra.extend_from_slice(&[0xFF; 100]); + + let accelerated_result = + fpvm_ec_recover(&input_with_extra, u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = + execute_native_precompile(ECRECOVER_ADDR, TEST_INPUT, u64::MAX).unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), EXPECTED_RESULT.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/kzg_point_eval.rs b/kona/bin/client/src/fpvm_evm/precompiles/kzg_point_eval.rs new file mode 100644 index 0000000000000..5b98975f13100 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/kzg_point_eval.rs @@ -0,0 +1,96 @@ +//! Contains the accelerated version of the KZG point evaluation precompile. + +use crate::fpvm_evm::precompiles::utils::precompile_run; +use alloc::string::ToString; +use alloy_primitives::Address; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use revm::precompile::{PrecompileError, PrecompileOutput, PrecompileResult}; + +/// Address of the KZG point evaluation precompile. +pub(crate) const KZG_POINT_EVAL_ADDR: Address = revm::precompile::u64_to_address(0x0A); + +/// Runs the FPVM-accelerated `kzgPointEval` precompile call. +pub(crate) fn fpvm_kzg_point_eval( + input: &[u8], + gas_limit: u64, + hint_writer: &H, + oracle_reader: &O, +) -> PrecompileResult +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + const GAS_COST: u64 = 50_000; + + if gas_limit < GAS_COST { + return Err(PrecompileError::OutOfGas); + } + + if input.len() != 192 { + return Err(PrecompileError::BlobInvalidInputLength); + } + + let result_data = kona_proof::block_on(precompile_run! { + hint_writer, + oracle_reader, + &[KZG_POINT_EVAL_ADDR.as_slice(), &GAS_COST.to_be_bytes(), input] + }) + .map_err(|e| PrecompileError::Other(e.to_string().into()))?; + + Ok(PrecompileOutput::new(GAS_COST, result_data.into())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fpvm_evm::precompiles::test_utils::{ + execute_native_precompile, test_accelerated_precompile, + }; + use alloy_eips::eip4844::VERSIONED_HASH_VERSION_KZG; + use alloy_primitives::hex; + use sha2::{Digest, Sha256}; + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_kzg_point_eval() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let commitment = hex!("8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7").to_vec(); + let mut versioned_hash = Sha256::digest(&commitment).to_vec(); + versioned_hash[0] = VERSIONED_HASH_VERSION_KZG; + let z = hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").to_vec(); + let y = hex!("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").to_vec(); + let proof = hex!("a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc2160744faf0070725e00b60ad9a026a15b1a8c").to_vec(); + + let input = [versioned_hash, z, y, commitment, proof].concat(); + + let expected_result = hex!("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"); + + let accelerated_result = fpvm_kzg_point_eval(&input, u64::MAX, hint_writer, oracle_reader).unwrap(); + let native_result = execute_native_precompile(KZG_POINT_EVAL_ADDR, input, u64::MAX).unwrap(); + + assert_eq!(accelerated_result.bytes.as_ref(), expected_result.as_ref()); + assert_eq!(accelerated_result.bytes, native_result.bytes); + assert_eq!(accelerated_result.gas_used, native_result.gas_used); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_kzg_point_eval_out_of_gas() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_kzg_point_eval(&[], 0, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::OutOfGas)); + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_accelerated_kzg_point_eval_bad_input_size() { + test_accelerated_precompile(|hint_writer, oracle_reader| { + let accelerated_result = + fpvm_kzg_point_eval(&[], u64::MAX, hint_writer, oracle_reader).unwrap_err(); + assert!(matches!(accelerated_result, PrecompileError::BlobInvalidInputLength)); + }) + .await; + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/mod.rs b/kona/bin/client/src/fpvm_evm/precompiles/mod.rs new file mode 100644 index 0000000000000..402a552aab406 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/mod.rs @@ -0,0 +1,22 @@ +//! Contains the [`PrecompileProvider`] implementation that serves FPVM-accelerated OP Stack +//! precompiles. +//! +//! [`PrecompileProvider`]: revm::handler::PrecompileProvider + +mod provider; +pub(crate) use provider::OpFpvmPrecompiles; + +mod bls12_g1_add; +mod bls12_g1_msm; +mod bls12_g2_add; +mod bls12_g2_msm; +mod bls12_map_fp; +mod bls12_map_fp2; +mod bls12_pair; +mod bn128_pair; +mod ecrecover; +mod kzg_point_eval; +mod utils; + +#[cfg(test)] +mod test_utils; diff --git a/kona/bin/client/src/fpvm_evm/precompiles/provider.rs b/kona/bin/client/src/fpvm_evm/precompiles/provider.rs new file mode 100644 index 0000000000000..8d97fd8ad59c8 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/provider.rs @@ -0,0 +1,473 @@ +//! [`PrecompileProvider`] for FPVM-accelerated OP Stack precompiles. + +use crate::fpvm_evm::precompiles::{ + ecrecover::ECRECOVER_ADDR, kzg_point_eval::KZG_POINT_EVAL_ADDR, +}; +use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use alloy_primitives::{Address, Bytes}; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use op_revm::{ + OpSpecId, + precompiles::{fjord, granite, isthmus}, +}; +use revm::{ + context::{Cfg, ContextTr}, + handler::{EthPrecompiles, PrecompileProvider}, + interpreter::{CallInputs, Gas, InstructionResult, InterpreterResult}, + precompile::{PrecompileError, PrecompileResult, Precompiles, bls12_381_const, bn254}, + primitives::{hardfork::SpecId, hash_map::HashMap}, +}; + +/// The FPVM-accelerated precompiles. +#[derive(Debug)] +pub struct OpFpvmPrecompiles { + /// The default [`EthPrecompiles`] provider. + inner: EthPrecompiles, + /// The accelerated precompiles for the current [`OpSpecId`]. + accelerated_precompiles: HashMap>, + /// The [`OpSpecId`] of the precompiles. + spec: OpSpecId, + /// The inner [`HintWriterClient`]. + hint_writer: H, + /// The inner [`PreimageOracleClient`]. + oracle_reader: O, +} + +impl OpFpvmPrecompiles +where + H: HintWriterClient + Clone + Send + Sync + 'static, + O: PreimageOracleClient + Clone + Send + Sync + 'static, +{ + /// Create a new precompile provider with the given [`OpSpecId`]. + #[inline] + pub fn new_with_spec(spec: OpSpecId, hint_writer: H, oracle_reader: O) -> Self { + let precompiles = match spec { + spec @ (OpSpecId::BEDROCK | + OpSpecId::REGOLITH | + OpSpecId::CANYON | + OpSpecId::ECOTONE) => Precompiles::new(spec.into_eth_spec().into()), + OpSpecId::FJORD => fjord(), + OpSpecId::GRANITE | OpSpecId::HOLOCENE => granite(), + OpSpecId::ISTHMUS | OpSpecId::INTEROP | OpSpecId::OSAKA | OpSpecId::JOVIAN => isthmus(), + }; + + let accelerated_precompiles = match spec { + OpSpecId::BEDROCK | OpSpecId::REGOLITH | OpSpecId::CANYON => { + accelerated_bedrock::() + } + OpSpecId::ECOTONE | OpSpecId::FJORD => accelerated_ecotone::(), + OpSpecId::GRANITE | OpSpecId::HOLOCENE => accelerated_granite::(), + OpSpecId::ISTHMUS | OpSpecId::INTEROP | OpSpecId::OSAKA => { + accelerated_isthmus::() + } + OpSpecId::JOVIAN => accelerated_jovian::(), + }; + + Self { + inner: EthPrecompiles { precompiles, spec: SpecId::default() }, + accelerated_precompiles: accelerated_precompiles + .into_iter() + .map(|p| (p.address, p.precompile)) + .collect(), + spec, + hint_writer, + oracle_reader, + } + } +} + +impl PrecompileProvider for OpFpvmPrecompiles +where + H: HintWriterClient + Clone + Send + Sync + 'static, + O: PreimageOracleClient + Clone + Send + Sync + 'static, + CTX: ContextTr>, +{ + type Output = InterpreterResult; + + #[inline] + fn set_spec(&mut self, spec: ::Spec) -> bool { + if spec == self.spec { + return false; + } + *self = Self::new_with_spec(spec, self.hint_writer.clone(), self.oracle_reader.clone()); + true + } + + #[inline] + fn run( + &mut self, + context: &mut CTX, + inputs: &CallInputs, + ) -> Result, String> { + let mut result = InterpreterResult { + result: InstructionResult::Return, + gas: Gas::new(inputs.gas_limit), + output: Bytes::new(), + }; + + use revm::context::LocalContextTr; + let input = match &inputs.input { + revm::interpreter::CallInput::Bytes(bytes) => bytes.clone(), + revm::interpreter::CallInput::SharedBuffer(range) => context + .local() + .shared_memory_buffer_slice(range.clone()) + .map(|b| Bytes::from(b.to_vec())) + .unwrap_or_default(), + }; + + // Priority: + // 1. If the precompile has an accelerated version, use that. + // 2. If the precompile is not accelerated, use the default version. + // 3. If the precompile is not found, return None. + let output = + if let Some(accelerated) = self.accelerated_precompiles.get(&inputs.bytecode_address) { + (accelerated)(&input, inputs.gas_limit, &self.hint_writer, &self.oracle_reader) + } else if let Some(precompile) = self.inner.precompiles.get(&inputs.bytecode_address) { + precompile.execute(&input, inputs.gas_limit) + } else { + return Ok(None); + }; + + match output { + Ok(output) => { + let underflow = result.gas.record_cost(output.gas_used); + assert!(underflow, "Gas underflow is not possible"); + result.result = InstructionResult::Return; + result.output = output.bytes; + } + Err(PrecompileError::Fatal(e)) => return Err(e), + Err(e) => { + result.result = if e.is_oog() { + InstructionResult::PrecompileOOG + } else { + InstructionResult::PrecompileError + }; + } + } + + Ok(Some(result)) + } + + #[inline] + fn warm_addresses(&self) -> Box> { + self.inner.warm_addresses() + } + + #[inline] + fn contains(&self, address: &Address) -> bool { + self.inner.contains(address) + } +} + +/// A precompile function that can be accelerated by the FPVM. +type AcceleratedPrecompileFn = fn(&[u8], u64, &H, &O) -> PrecompileResult; + +/// A tuple type for accelerated precompiles with an associated [`Address`]. +struct AcceleratedPrecompile { + /// The address of the precompile. + address: Address, + /// The precompile function. + precompile: AcceleratedPrecompileFn, +} + +impl AcceleratedPrecompile { + /// Create a new accelerated precompile. + fn new(address: Address, precompile: AcceleratedPrecompileFn) -> Self { + Self { address, precompile } + } +} + +/// The accelerated precompiles for the bedrock spec. +fn accelerated_bedrock() -> Vec> +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + vec![ + AcceleratedPrecompile::new(ECRECOVER_ADDR, super::ecrecover::fpvm_ec_recover::), + AcceleratedPrecompile::new( + bn254::pair::ADDRESS, + super::bn128_pair::fpvm_bn128_pair::, + ), + ] +} + +/// The accelerated precompiles for the ecotone spec. +fn accelerated_ecotone() -> Vec> +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let mut base = accelerated_bedrock::(); + base.push(AcceleratedPrecompile::new( + KZG_POINT_EVAL_ADDR, + super::kzg_point_eval::fpvm_kzg_point_eval::, + )); + base +} + +/// The accelerated precompiles for the granite spec. +fn accelerated_granite() -> Vec> +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let mut base = accelerated_ecotone::(); + base.push(AcceleratedPrecompile::new( + bn254::pair::ADDRESS, + super::bn128_pair::fpvm_bn128_pair_granite::, + )); + base +} + +/// The accelerated precompiles for the isthmus spec. +fn accelerated_isthmus() -> Vec> +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let mut base = accelerated_granite::(); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G1_ADD_ADDRESS, + super::bls12_g1_add::fpvm_bls12_g1_add::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G1_MSM_ADDRESS, + super::bls12_g1_msm::fpvm_bls12_g1_msm::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G2_ADD_ADDRESS, + super::bls12_g2_add::fpvm_bls12_g2_add::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G2_MSM_ADDRESS, + super::bls12_g2_msm::fpvm_bls12_g2_msm::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::MAP_FP_TO_G1_ADDRESS, + super::bls12_map_fp::fpvm_bls12_map_fp::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::MAP_FP2_TO_G2_ADDRESS, + super::bls12_map_fp2::fpvm_bls12_map_fp2::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::PAIRING_ADDRESS, + super::bls12_pair::fpvm_bls12_pairing::, + )); + base +} + +/// The accelerated precompiles for the jovian spec. +fn accelerated_jovian() -> Vec> +where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, +{ + let mut base = accelerated_isthmus::(); + + // Replace the 4 variable-input precompiles with Jovian versions (reduced limits) + base.retain(|p| { + p.address != bn254::pair::ADDRESS && + p.address != bls12_381_const::G1_MSM_ADDRESS && + p.address != bls12_381_const::G2_MSM_ADDRESS && + p.address != bls12_381_const::PAIRING_ADDRESS + }); + + base.push(AcceleratedPrecompile::new( + bn254::pair::ADDRESS, + super::bn128_pair::fpvm_bn128_pair_jovian::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G1_MSM_ADDRESS, + super::bls12_g1_msm::fpvm_bls12_g1_msm_jovian::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::G2_MSM_ADDRESS, + super::bls12_g2_msm::fpvm_bls12_g2_msm_jovian::, + )); + base.push(AcceleratedPrecompile::new( + bls12_381_const::PAIRING_ADDRESS, + super::bls12_pair::fpvm_bls12_pairing_jovian::, + )); + + base +} + +#[cfg(test)] +mod test { + use super::*; + use kona_preimage::{HintWriterClient, PreimageOracleClient}; + use op_revm::{DefaultOp as _, OpContext, OpSpecId}; + use revm::{Context, database::EmptyDB, handler::PrecompileProvider, interpreter::CallInput}; + + type TestContext = OpContext; + + fn create_call_inputs(address: Address, input: Bytes, gas_limit: u64) -> CallInputs { + CallInputs { + input: CallInput::Bytes(input), + gas_limit, + bytecode_address: address, + target_address: Address::ZERO, + caller: Address::ZERO, + value: revm::interpreter::CallValue::Transfer(alloy_primitives::U256::ZERO), + scheme: revm::interpreter::CallScheme::Call, + is_static: false, + return_memory_offset: 0..0, + known_bytecode: None, + } + } + + fn create_test_context() -> TestContext { + Context::op().with_db(EmptyDB::new()) + } + + /// A mock accelerated precompile function that returns a fixed output. + fn mock_accelerated_precompile( + _input: &[u8], + gas_limit: u64, + _hint_writer: &H, + _oracle_reader: &O, + ) -> PrecompileResult + where + H: HintWriterClient + Send + Sync, + O: PreimageOracleClient + Send + Sync, + { + Ok(revm::precompile::PrecompileOutput::new(gas_limit / 2, Bytes::from_static(b"mock"))) + } + + #[test] + fn test_run_accelerated_precompile() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // Override the ecrecover accelerated precompile with our mock + precompiles.accelerated_precompiles.insert(ECRECOVER_ADDR, mock_accelerated_precompile); + + let call_inputs = create_call_inputs(ECRECOVER_ADDR, Bytes::from_static(b"test"), 1000); + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap(); + assert!(result.is_some()); + + let interpreter_result = result.unwrap(); + assert_eq!(interpreter_result.result, InstructionResult::Return); + assert_eq!(interpreter_result.output.as_ref(), b"mock"); + } + + #[test] + fn test_run_default_precompile_sha256() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // SHA256 precompile address (0x02) - not accelerated, uses default + let sha256_addr = revm::precompile::u64_to_address(2); + let input = b"hello world"; + let call_inputs = create_call_inputs(sha256_addr, input.to_vec().into(), u64::MAX); + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap(); + assert!(result.is_some()); + + let interpreter_result = result.unwrap(); + assert_eq!(interpreter_result.result, InstructionResult::Return); + assert!(!interpreter_result.output.is_empty()); + } + + #[test] + fn test_run_nonexistent_precompile() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // Non-existent precompile address + let fake_addr = Address::from_slice(&[0xFFu8; 20]); + let call_inputs = create_call_inputs(fake_addr, Bytes::new(), u64::MAX); + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_run_out_of_gas() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // SHA256 with 0 gas to trigger OOG + let sha256_addr = revm::precompile::u64_to_address(2); + let input = b"hello world"; + let call_inputs = create_call_inputs(sha256_addr, input.to_vec().into(), 0); + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap(); + assert!(result.is_some()); + + let interpreter_result = result.unwrap(); + assert_eq!(interpreter_result.result, InstructionResult::PrecompileOOG); + } + + #[test] + fn test_run_with_shared_buffer_empty() { + let (hint_chan, preimage_chan) = ( + kona_preimage::BidirectionalChannel::new().unwrap(), + kona_preimage::BidirectionalChannel::new().unwrap(), + ); + let hint_writer = kona_preimage::HintWriter::new(hint_chan.client); + let oracle_reader = kona_preimage::OracleReader::new(preimage_chan.client); + + let mut ctx = create_test_context(); + + let mut precompiles = + OpFpvmPrecompiles::new_with_spec(OpSpecId::BEDROCK, hint_writer, oracle_reader); + + // Test SharedBuffer path with empty buffer + let sha256_addr = revm::precompile::u64_to_address(2); + let call_inputs = CallInputs { + input: CallInput::SharedBuffer(0..0), + gas_limit: u64::MAX, + bytecode_address: sha256_addr, + target_address: Address::ZERO, + caller: Address::ZERO, + value: revm::interpreter::CallValue::Transfer(alloy_primitives::U256::ZERO), + scheme: revm::interpreter::CallScheme::Call, + is_static: false, + return_memory_offset: 0..0, + known_bytecode: None, + }; + + let result = precompiles.run(&mut ctx, &call_inputs).unwrap(); + assert!(result.is_some()); + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/test_utils.rs b/kona/bin/client/src/fpvm_evm/precompiles/test_utils.rs new file mode 100644 index 0000000000000..bf9d781efcff1 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/test_utils.rs @@ -0,0 +1,148 @@ +//! Test utilities for accelerated precompiles. + +use alloy_primitives::{Address, Bytes, keccak256}; +use async_trait::async_trait; +use kona_preimage::{ + BidirectionalChannel, HintReader, HintReaderServer, HintRouter, HintWriter, NativeChannel, + OracleReader, OracleServer, PreimageFetcher, PreimageKey, PreimageKeyType, + PreimageOracleServer, + errors::{PreimageOracleError, PreimageOracleResult}, +}; +use kona_proof::{Hint, HintType}; +use revm::precompile::PrecompileResult; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::{Mutex, RwLock}; + +/// Runs a test with a mock host that serves [`HintType::L1Precompile`] hints and preimages. The +/// closure accepts the client's [`HintWriter`] and [`OracleReader`] as arguments. +pub(crate) async fn test_accelerated_precompile( + f: impl Fn(&HintWriter, &OracleReader) + Send + Sync + 'static, +) { + let (hint_chan, preimage_chan) = + (BidirectionalChannel::new().unwrap(), BidirectionalChannel::new().unwrap()); + + let host = tokio::task::spawn(precompile_host( + OracleServer::new(preimage_chan.host), + HintReader::new(hint_chan.host), + )); + let client = tokio::task::spawn(async move { + let oracle_reader = OracleReader::new(preimage_chan.client); + let hint_writer = HintWriter::new(hint_chan.client); + + (f)(&hint_writer, &oracle_reader) + }); + + tokio::try_join!(host, client).unwrap_or_else(|e| { + panic!("Failed to join client/host: {e:?}"); + }); +} + +/// Executes a precompile on [`revm`]. +pub(crate) fn execute_native_precompile>( + address: Address, + input: T, + gas: u64, +) -> PrecompileResult { + let precompiles = revm::handler::EthPrecompiles::default(); + let Some(precompile) = precompiles.precompiles.get(&address) else { + panic!("Precompile not found"); + }; + precompile.execute(&input.into(), gas) +} + +/// Starts a mock host thread that serves [`HintType::L1Precompile`] hints and preimages. +async fn precompile_host( + oracle_server: OracleServer, + hint_reader: HintReader, +) { + let last_hint = Arc::new(RwLock::new(None)); + let preimage_fetcher = + PrecompilePreimageFetcher { map: Default::default(), last_hint: last_hint.clone() }; + let hint_router = PrecompileHintRouter { last_hint: last_hint.clone() }; + + let server = tokio::task::spawn(async move { + loop { + match oracle_server.next_preimage_request(&preimage_fetcher).await { + Ok(_) => continue, + Err(PreimageOracleError::IOError(_)) => return, + Err(e) => { + panic!("Critical: Failed to serve preimage: {e:?}"); + } + } + } + }); + let hint_reader = tokio::task::spawn(async move { + loop { + match hint_reader.next_hint(&hint_router).await { + Ok(_) => continue, + Err(PreimageOracleError::IOError(_)) => return, + Err(e) => { + panic!("Critical: Failed to serve hint: {e:?}"); + } + } + } + }); + + tokio::try_join!(server, hint_reader).unwrap_or_else(|e| { + panic!("Failed to join server/hint reader: {e:?}"); + }); +} + +#[derive(Default, Debug, Clone)] +struct PrecompilePreimageFetcher { + /// Inner map of preimages. + map: Arc>>>, + /// The previous hint received. + last_hint: Arc>>, +} + +#[async_trait] +impl PreimageFetcher for PrecompilePreimageFetcher { + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { + let mut map_lock = self.map.lock().await; + if let Some(preimage) = map_lock.get(&key) { + return Ok(preimage.clone()); + } + + let last_hint = self.last_hint.read().await; + let Some(last_hint) = last_hint.as_ref() else { unreachable!("Hint not queued") }; + + let parsed_hint = last_hint.parse::>().unwrap(); + if matches!(parsed_hint.ty, HintType::L1Precompile) { + let address = Address::from_slice(&parsed_hint.data.as_ref()[..20]); + let gas = u64::from_be_bytes(parsed_hint.data.as_ref()[20..28].try_into().unwrap()); + let input = parsed_hint.data[28..].to_vec(); + let input_hash = keccak256(parsed_hint.data.as_ref()); + + let result = execute_native_precompile(address, input, gas).map_or_else( + |_| vec![0u8; 1], + |raw_res| { + let mut res = Vec::with_capacity(1 + raw_res.bytes.len()); + res.push(0x01); + res.extend_from_slice(&raw_res.bytes); + res + }, + ); + + map_lock + .insert(PreimageKey::new(*input_hash, PreimageKeyType::Precompile), result.clone()); + return Ok(result); + } else { + panic!("Unexpected hint type: {:?}", parsed_hint.ty); + } + } +} + +#[derive(Default, Debug, Clone)] +struct PrecompileHintRouter { + /// The latest hint received. + last_hint: Arc>>, +} + +#[async_trait] +impl HintRouter for PrecompileHintRouter { + async fn route_hint(&self, hint: String) -> PreimageOracleResult<()> { + self.last_hint.write().await.replace(hint); + Ok(()) + } +} diff --git a/kona/bin/client/src/fpvm_evm/precompiles/utils.rs b/kona/bin/client/src/fpvm_evm/precompiles/utils.rs new file mode 100644 index 0000000000000..766a0a7ab9792 --- /dev/null +++ b/kona/bin/client/src/fpvm_evm/precompiles/utils.rs @@ -0,0 +1,51 @@ +//! Utility functions for precompiles + +/// A macro that generates an async block that sends a hint to the host, constructs a key hash +/// from the hint data, fetches the result of the precompile run from the host, and returns the +/// result data. +/// +/// The macro takes the following arguments: +/// - `hint_data`: The hint data to send to the host. +#[macro_export] +macro_rules! precompile_run { + ($hint_writer:expr, $oracle_reader:expr, $hint_data:expr) => { + async move { + use alloc::{string::ToString, vec::Vec}; + use kona_preimage::{PreimageKey, PreimageKeyType, errors::PreimageOracleError}; + use kona_proof::{HintType, errors::OracleProviderError}; + + // Write the hint for the precompile run. + let hint_data = $hint_data; + HintType::L1Precompile.with_data(hint_data).send($hint_writer).await?; + + // Construct the key hash for the precompile run. + let raw_key_data = hint_data.iter().copied().flatten().copied().collect::>(); + let key_hash = alloy_primitives::keccak256(&raw_key_data); + + // Fetch the result of the precompile run from the host. + let result_data = $oracle_reader + .get(PreimageKey::new(*key_hash, PreimageKeyType::Precompile)) + .await + .map_err(OracleProviderError::Preimage)?; + + // Ensure we've received valid result data. + if result_data.is_empty() { + return Err(OracleProviderError::Preimage(PreimageOracleError::Other( + "Invalid result data".to_string(), + ))); + } + + // Ensure we've not received an error from the host. + if result_data[0] == 0 { + return Err(OracleProviderError::Preimage(PreimageOracleError::Other( + "Error executing precompile in host".to_string(), + ))); + } + + // Return the result data. + Ok(result_data[1..].to_vec()) + } + }; +} + +pub(crate) use precompile_run; diff --git a/kona/bin/client/src/interop/consolidate.rs b/kona/bin/client/src/interop/consolidate.rs new file mode 100644 index 0000000000000..efaff256eddee --- /dev/null +++ b/kona/bin/client/src/interop/consolidate.rs @@ -0,0 +1,140 @@ +//! Consolidation phase of the interop proof program. + +use super::FaultProofProgramError; +use crate::interop::util::fetch_output_block_hash; +use alloc::sync::Arc; +use alloy_evm::{EvmFactory, FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::block::OpTxEnv; +use core::fmt::Debug; +use kona_executor::TrieDBProvider; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use kona_proof::{CachingOracle, l2::OracleL2ChainProvider}; +use kona_proof_interop::{ + BootInfo, HintType, OracleInteropProvider, PreState, SuperchainConsolidator, +}; +use kona_registry::{HashMap, ROLLUP_CONFIGS}; +use op_alloy_consensus::OpTxEnvelope; +use op_revm::OpSpecId; +use revm::context::BlockEnv; +use tracing::{error, info}; + +/// Executes the consolidation phase of the interop proof with the given [PreimageOracleClient] and +/// [HintWriterClient]. +/// +/// This phase is responsible for checking the dependencies between [OptimisticBlock]s in the +/// superchain and ensuring that all dependencies are satisfied. +/// +/// [OptimisticBlock]: kona_proof_interop::OptimisticBlock +pub(crate) async fn consolidate_dependencies( + oracle: Arc>, + mut boot: BootInfo, + evm_factory: Evm, +) -> Result<(), FaultProofProgramError> +where + P: PreimageOracleClient + Send + Sync + Debug + Clone, + H: HintWriterClient + Send + Sync + Debug + Clone, + Evm: EvmFactory + Send + Sync + Debug + Clone + 'static, + ::Tx: + FromTxWithEncoded + FromRecoveredTx + OpTxEnv, +{ + info!(target: "client_interop", "Deriving local-safe headers from prestate"); + + // Ensure that the pre-state is a transition state. It is invalid to pass a non-transition state + // to this function, as it will not have the required information to derive the local-safe + // headers for the next super root. + let PreState::TransitionState(ref transition_state) = boot.agreed_pre_state else { + return Err(FaultProofProgramError::StateTransitionFailed); + }; + + // Collect the cross-safe output roots and local-safe block hashes from the transition state. + let transition_meta = transition_state + .pending_progress + .iter() + .zip(transition_state.pre_state.output_roots.iter()) + .map(|(optimistic_block, pre_state)| (pre_state, optimistic_block.block_hash)) + .collect::>(); + + let mut headers = HashMap::default(); + let mut l2_providers = HashMap::default(); + for (cross_safe_output, local_safe_block_hash) in transition_meta { + // Fetch the cross-safe head's block hash for the given L2 chain ID. + let cross_safe_head_hash = fetch_output_block_hash( + oracle.as_ref(), + cross_safe_output.output_root, + cross_safe_output.chain_id, + ) + .await?; + + // Fetch the rollup config for the given L2 chain ID. + let rollup_config = ROLLUP_CONFIGS + .get(&cross_safe_output.chain_id) + .or_else(|| boot.rollup_configs.get(&cross_safe_output.chain_id)) + .ok_or(FaultProofProgramError::MissingRollupConfig(cross_safe_output.chain_id))?; + + // Initialize the local provider for the current L2 chain. + let mut local_provider = OracleL2ChainProvider::new( + cross_safe_head_hash, + Arc::new(rollup_config.clone()), + oracle.clone(), + ); + local_provider.set_chain_id(Some(cross_safe_output.chain_id)); + + // Send hints for the L2 block data in the pending progress. This is an important step, + // because non-canonical blocks within the pending progress will not be able to be fetched + // by the host through traditional means. If the block is determined to not be canonical + // by the host, it will derive + build it and store the required preimages to complete + // deposit-only re-execution. If the block is determined to be canonical, the host will + // no-op, and preimages will be fetched through the traditional route as needed. + HintType::L2BlockData + .with_data(&[ + cross_safe_head_hash.as_slice(), + local_safe_block_hash.as_slice(), + cross_safe_output.chain_id.to_be_bytes().as_slice(), + ]) + .send(oracle.as_ref()) + .await?; + + // Fetch the header for the local-safe head of the current L2 chain. + let header = local_provider.header_by_hash(local_safe_block_hash)?; + + headers.insert(cross_safe_output.chain_id, header.seal(local_safe_block_hash)); + l2_providers.insert(cross_safe_output.chain_id, local_provider); + } + + info!( + target: "client_interop", + num_blocks = headers.len(), + "Loaded local-safe headers", + ); + + // Consolidate the superchain + let global_provider = OracleInteropProvider::new(oracle.clone(), boot.clone(), headers); + SuperchainConsolidator::new(&mut boot, global_provider, l2_providers, evm_factory) + .consolidate() + .await?; + + // Transition to the Super Root at the next timestamp. + let post = boot + .agreed_pre_state + .transition(None) + .ok_or(FaultProofProgramError::StateTransitionFailed)?; + let post_commitment = post.hash(); + + // Ensure that the post-state matches the claimed post-state. + if post_commitment != boot.claimed_post_state { + error!( + target: "client_interop", + claimed = ?boot.claimed_post_state, + actual = ?post_commitment, + "Post state validation failed", + ); + return Err(FaultProofProgramError::InvalidClaim(boot.claimed_post_state, post_commitment)); + } + + info!( + target: "client_interop", + root = ?boot.claimed_post_state, + "Super root validation succeeded" + ); + Ok(()) +} diff --git a/kona/bin/client/src/interop/mod.rs b/kona/bin/client/src/interop/mod.rs new file mode 100644 index 0000000000000..fa756b08f892d --- /dev/null +++ b/kona/bin/client/src/interop/mod.rs @@ -0,0 +1,128 @@ +//! Multi-chain, interoperable fault proof program entrypoint. + +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::B256; +use consolidate::consolidate_dependencies; +use core::{cmp::Ordering, fmt::Debug}; +use kona_derive::PipelineErrorKind; +use kona_driver::DriverError; +use kona_executor::ExecutorError; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use kona_proof::{CachingOracle, errors::OracleProviderError}; +use kona_proof_interop::{ + BootInfo, ConsolidationError, PreState, TRANSITION_STATE_MAX_STEPS, boot::BootstrapError, +}; +use thiserror::Error; +use tracing::{error, info}; +use transition::sub_transition; + +use crate::fpvm_evm::FpvmOpEvmFactory; + +pub(crate) mod consolidate; +pub(crate) mod transition; +pub(crate) mod util; + +/// An error that can occur when running the fault proof program. +#[derive(Error, Debug)] +pub enum FaultProofProgramError { + /// The claim is invalid. + #[error("Invalid claim. Expected {0}, actual {1}")] + InvalidClaim(B256, B256), + /// An error occurred in the Oracle provider. + #[error(transparent)] + OracleProvider(#[from] OracleProviderError), + /// An error occurred in the driver. + #[error(transparent)] + Driver(#[from] Box>), + /// An error occurred in the derivation pipeline. + #[error(transparent)] + PipelineError(#[from] Box), + /// Consolidation error. + #[error(transparent)] + Consolidation(#[from] ConsolidationError), + /// Bootstrap error + #[error(transparent)] + Bootstrap(#[from] BootstrapError), + /// State transition failed. + #[error("Critical state transition failure")] + StateTransitionFailed, + /// Missing a rollup configuration. + #[error("Missing rollup configuration for chain ID {0}")] + MissingRollupConfig(u64), +} + +/// Executes the interop fault proof program with the given [PreimageOracleClient] and +/// [HintWriterClient]. +#[inline] +pub async fn run(oracle_client: P, hint_client: H) -> Result<(), FaultProofProgramError> +where + P: PreimageOracleClient + Send + Sync + Debug + Clone + 'static, + H: HintWriterClient + Send + Sync + Debug + Clone + 'static, +{ + const ORACLE_LRU_SIZE: usize = 1024; + + // Instantiate the oracle and bootstrap the program from local inputs. + let oracle = + Arc::new(CachingOracle::new(ORACLE_LRU_SIZE, oracle_client.clone(), hint_client.clone())); + let boot = match BootInfo::load(oracle.as_ref()).await { + Ok(boot) => boot, + Err(BootstrapError::InvalidToInvalid) => { + info!(target: "client_interop", "No-op transition, short-circuiting."); + return Ok(()); + } + Err(e) => { + error!(target: "client_interop", "Failed to load boot info: {}", e); + return Err(e.into()); + } + }; + + let evm_factory = FpvmOpEvmFactory::new(hint_client, oracle_client); + + // Load in the agreed pre-state from the preimage oracle in order to determine the active + // sub-problem. + match boot.agreed_pre_state { + PreState::SuperRoot(ref super_root) => { + // If the claimed L2 block timestamp is less than the super root timestamp, the + // post-state must be the agreed pre-state to accommodate trace extension. + if super_root.timestamp >= boot.claimed_l2_timestamp { + if boot.agreed_pre_state_commitment == boot.claimed_post_state { + return Ok(()); + } else { + return Err(FaultProofProgramError::InvalidClaim( + boot.agreed_pre_state_commitment, + boot.claimed_post_state, + )); + } + } + + // If the pre-state is a super root, the first sub-problem is always selected. + sub_transition(oracle, boot, evm_factory).await + } + PreState::TransitionState(ref transition_state) => { + // If the claimed L2 block timestamp is less than the prestate timestamp, the + // claim must be invalid. + if transition_state.pre_state.timestamp >= boot.claimed_l2_timestamp { + return Err(FaultProofProgramError::InvalidClaim( + boot.agreed_pre_state_commitment, + boot.claimed_post_state, + )); + } + + // If the pre-state is a transition state, the sub-problem is selected based on the + // current step. + match transition_state.step.cmp(&TRANSITION_STATE_MAX_STEPS) { + Ordering::Equal => consolidate_dependencies(oracle, boot, evm_factory).await, + Ordering::Less => sub_transition(oracle, boot, evm_factory).await, + Ordering::Greater => { + error!( + target: "client_interop", + transition_state_step = transition_state.step, + transition_state_max_steps = TRANSITION_STATE_MAX_STEPS, + "Invalid transition state step; 'step' is greater than maximum value." + ); + Err(FaultProofProgramError::StateTransitionFailed) + } + } + } + } +} diff --git a/kona/bin/client/src/interop/transition.rs b/kona/bin/client/src/interop/transition.rs new file mode 100644 index 0000000000000..43103f84c4e03 --- /dev/null +++ b/kona/bin/client/src/interop/transition.rs @@ -0,0 +1,215 @@ +//! Single chain sub-transition phase of the interop proof. + +use super::FaultProofProgramError; +use crate::interop::util::fetch_l2_safe_head_hash; +use alloc::{boxed::Box, sync::Arc}; +use alloy_consensus::Sealed; +use alloy_evm::{EvmFactory, FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::block::OpTxEnv; +use alloy_primitives::B256; +use core::fmt::Debug; +use kona_derive::{EthereumDataSource, PipelineError, PipelineErrorKind}; +use kona_driver::{Driver, DriverError}; +use kona_executor::TrieDBProvider; +use kona_preimage::{HintWriterClient, PreimageOracleClient}; +use kona_proof::{ + CachingOracle, + executor::KonaExecutor, + l1::{OracleBlobProvider, OracleL1ChainProvider, OraclePipeline}, + l2::OracleL2ChainProvider, + sync::new_oracle_pipeline_cursor, +}; +use kona_proof_interop::{BootInfo, INVALID_TRANSITION_HASH, OptimisticBlock, PreState}; +use op_alloy_consensus::OpTxEnvelope; +use op_revm::OpSpecId; +use revm::context::BlockEnv; +use tracing::{error, info, warn}; + +/// Executes a sub-transition of the interop proof with the given [PreimageOracleClient] and +/// [HintWriterClient]. +pub(crate) async fn sub_transition( + oracle: Arc>, + boot: BootInfo, + evm_factory: Evm, +) -> Result<(), FaultProofProgramError> +where + P: PreimageOracleClient + Send + Sync + Debug + Clone, + H: HintWriterClient + Send + Sync + Debug + Clone, + Evm: EvmFactory + Send + Sync + Debug + Clone + 'static, + ::Tx: + FromTxWithEncoded + FromRecoveredTx + OpTxEnv, +{ + // Check if we can short-circuit the transition, if we are within padding. + if let PreState::TransitionState(ref transition_state) = boot.agreed_pre_state { + if transition_state.step >= transition_state.pre_state.output_roots.len() as u64 { + info!( + target: "interop_client", + "No derivation/execution required, transition state is already saturated." + ); + + return transition_and_check(boot.agreed_pre_state, None, boot.claimed_post_state); + } + } + + // Fetch the L2 block hash of the current safe head. + let safe_head_hash = fetch_l2_safe_head_hash(oracle.as_ref(), &boot.agreed_pre_state).await?; + + // Determine the active L2 chain ID and fetch the rollup configuration. + let rollup_config = boot + .active_rollup_config() + .map(Arc::new) + .ok_or(FaultProofProgramError::StateTransitionFailed)?; + + let l1_config = boot.active_l1_config(); + + // Instantiate the L1 EL + CL provider and the L2 EL provider. + let mut l1_provider = OracleL1ChainProvider::new(boot.l1_head, oracle.clone()); + let mut l2_provider = + OracleL2ChainProvider::new(safe_head_hash, rollup_config.clone(), oracle.clone()); + let beacon = OracleBlobProvider::new(oracle.clone()); + + // Set the active L2 chain ID for the L2 provider. + l2_provider.set_chain_id(boot.agreed_pre_state.active_l2_chain_id()); + + // Fetch the safe head's block header. + let safe_head = l2_provider + .header_by_hash(safe_head_hash) + .map(|header| Sealed::new_unchecked(header, safe_head_hash))?; + let disputed_l2_block_number = safe_head.number + 1; + + // Check if we can no-op the transition. The Superchain STF happens once every second, but + // chains have a variable block time, meaning there might be no transition to process. + if safe_head.timestamp + rollup_config.block_time > boot.agreed_pre_state.timestamp() + 1 { + info!( + target: "interop_client", + "No-op transition, short-circuiting." + ); + + let active_root = boot + .agreed_pre_state + .active_l2_output_root() + .ok_or(FaultProofProgramError::StateTransitionFailed)?; + let optimistic_block = OptimisticBlock::new(safe_head.hash(), active_root.output_root); + return transition_and_check( + boot.agreed_pre_state, + Some(optimistic_block), + boot.claimed_post_state, + ); + } + + // Create a new derivation driver with the given boot information and oracle. + let cursor = new_oracle_pipeline_cursor( + rollup_config.as_ref(), + safe_head, + &mut l1_provider, + &mut l2_provider, + ) + .await?; + l2_provider.set_cursor(cursor.clone()); + + let da_provider = + EthereumDataSource::new_from_parts(l1_provider.clone(), beacon, &rollup_config); + let pipeline = OraclePipeline::new( + rollup_config.clone(), + l1_config.into(), + cursor.clone(), + oracle.clone(), + da_provider, + l1_provider.clone(), + l2_provider.clone(), + ) + .await + .map_err(Box::new)?; + let executor = KonaExecutor::new( + rollup_config.as_ref(), + l2_provider.clone(), + l2_provider, + evm_factory, + None, + ); + let mut driver = Driver::new(cursor, executor, pipeline); + + // Run the derivation pipeline until we are able to produce the output root of the claimed + // L2 block. + match driver.advance_to_target(rollup_config.as_ref(), Some(disputed_l2_block_number)).await { + Ok((safe_head, output_root)) => { + let optimistic_block = OptimisticBlock::new(safe_head.block_info.hash, output_root); + transition_and_check( + boot.agreed_pre_state, + Some(optimistic_block), + boot.claimed_post_state, + )?; + + info!( + target: "interop_client", + "Successfully validated progressed transition state claim with commitment {post_state_commitment}", + post_state_commitment = boot.claimed_post_state + ); + + Ok(()) + } + Err(DriverError::Pipeline(PipelineErrorKind::Critical(PipelineError::EndOfSource))) => { + warn!( + target: "interop_client", + "Exhausted data source; Transitioning to invalid state." + ); + + (boot.claimed_post_state == INVALID_TRANSITION_HASH).then_some(()).ok_or( + FaultProofProgramError::InvalidClaim( + INVALID_TRANSITION_HASH, + boot.claimed_post_state, + ), + ) + } + Err(e) => { + error!( + target: "interop_client", + "Failed to advance derivation pipeline: {:?}", + e + ); + Err(Box::new(e).into()) + } + } +} + +/// Transitions the [PreState] with the given [OptimisticBlock] and checks if the resulting state +/// commitment matches the expected commitment. +fn transition_and_check( + pre_state: PreState, + optimistic_block: Option, + expected_post_state: B256, +) -> Result<(), FaultProofProgramError> { + let did_append = optimistic_block.is_some(); + let post_state = pre_state + .transition(optimistic_block) + .ok_or(FaultProofProgramError::StateTransitionFailed)?; + let post_state_commitment = post_state.hash(); + + if did_append { + info!( + target: "interop_client", + "Appended optimistic L2 block to transition state", + ); + } + + if post_state_commitment != expected_post_state { + error!( + target: "interop_client", + "Failed to validate progressed transition state. Expected post-state commitment: {expected}, actual: {actual}", + expected = expected_post_state, + actual = post_state_commitment + ); + + return Err(FaultProofProgramError::InvalidClaim( + expected_post_state, + post_state_commitment, + )); + } + + info!( + target: "interop_client", + "Successfully validated progressed transition state with commitment {post_state_commitment}", + ); + + Ok(()) +} diff --git a/kona/bin/client/src/interop/util.rs b/kona/bin/client/src/interop/util.rs new file mode 100644 index 0000000000000..e36a663098479 --- /dev/null +++ b/kona/bin/client/src/interop/util.rs @@ -0,0 +1,45 @@ +//! Utilities for the interop proof program + +use alloc::string::ToString; +use alloy_primitives::B256; +use kona_preimage::{CommsClient, PreimageKey, errors::PreimageOracleError}; +use kona_proof::errors::OracleProviderError; +use kona_proof_interop::{HintType, PreState}; + +/// Fetches the safe head hash of the L2 chain based on the agreed upon L2 output root in the +/// [PreState]. +pub(crate) async fn fetch_l2_safe_head_hash( + caching_oracle: &O, + pre: &PreState, +) -> Result +where + O: CommsClient, +{ + // Fetch the output root of the safe head block for the current L2 chain. + let rich_output = pre + .active_l2_output_root() + .ok_or(PreimageOracleError::Other("Missing active L2 output root".to_string()))?; + + fetch_output_block_hash(caching_oracle, rich_output.output_root, rich_output.chain_id).await +} + +/// Fetches the block hash that the passed output root commits to. +pub(crate) async fn fetch_output_block_hash( + caching_oracle: &O, + output_root: B256, + chain_id: u64, +) -> Result +where + O: CommsClient, +{ + HintType::L2OutputRoot + .with_data(&[output_root.as_slice(), chain_id.to_be_bytes().as_slice()]) + .send(caching_oracle) + .await?; + let output_preimage = caching_oracle + .get(PreimageKey::new_keccak256(*output_root)) + .await + .map_err(OracleProviderError::Preimage)?; + + output_preimage[96..128].try_into().map_err(OracleProviderError::SliceConversion) +} diff --git a/kona/bin/client/src/kona.rs b/kona/bin/client/src/kona.rs new file mode 100644 index 0000000000000..944cf98f1c448 --- /dev/null +++ b/kona/bin/client/src/kona.rs @@ -0,0 +1,41 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] +#![cfg_attr(any(target_arch = "mips64", target_arch = "riscv64"), no_main)] + +extern crate alloc; + +use alloc::string::String; +use kona_preimage::{HintWriter, OracleReader}; +use kona_std_fpvm::{FileChannel, FileDescriptor}; +use kona_std_fpvm_proc::client_entry; + +/// The global preimage oracle reader pipe. +static ORACLE_READER_PIPE: FileChannel = + FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite); + +/// The global hint writer pipe. +static HINT_WRITER_PIPE: FileChannel = + FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite); + +/// The global preimage oracle reader. +static ORACLE_READER: OracleReader = OracleReader::new(ORACLE_READER_PIPE); + +/// The global hint writer. +static HINT_WRITER: HintWriter = HintWriter::new(HINT_WRITER_PIPE); + +#[client_entry] +fn main() -> Result<(), String> { + #[cfg(feature = "client-tracing")] + { + use kona_std_fpvm::tracing::FpvmTracingSubscriber; + + let subscriber = FpvmTracingSubscriber::new(tracing::Level::INFO); + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set tracing subscriber"); + } + + kona_proof::block_on(kona_client::single::run(ORACLE_READER, HINT_WRITER)) +} diff --git a/kona/bin/client/src/kona_interop.rs b/kona/bin/client/src/kona_interop.rs new file mode 100644 index 0000000000000..482e8cbcfa25a --- /dev/null +++ b/kona/bin/client/src/kona_interop.rs @@ -0,0 +1,41 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] +#![cfg_attr(any(target_arch = "mips64", target_arch = "riscv64"), no_main)] + +extern crate alloc; + +use alloc::string::String; +use kona_preimage::{HintWriter, OracleReader}; +use kona_std_fpvm::{FileChannel, FileDescriptor}; +use kona_std_fpvm_proc::client_entry; + +/// The global preimage oracle reader pipe. +static ORACLE_READER_PIPE: FileChannel = + FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite); + +/// The global hint writer pipe. +static HINT_WRITER_PIPE: FileChannel = + FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite); + +/// The global preimage oracle reader. +static ORACLE_READER: OracleReader = OracleReader::new(ORACLE_READER_PIPE); + +/// The global hint writer. +static HINT_WRITER: HintWriter = HintWriter::new(HINT_WRITER_PIPE); + +#[client_entry] +fn main() -> Result<(), String> { + #[cfg(feature = "client-tracing")] + { + use kona_std_fpvm::tracing::FpvmTracingSubscriber; + + let subscriber = FpvmTracingSubscriber::new(tracing::Level::INFO); + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set tracing subscriber"); + } + + kona_proof::block_on(kona_client::interop::run(ORACLE_READER, HINT_WRITER)) +} diff --git a/kona/bin/client/src/lib.rs b/kona/bin/client/src/lib.rs new file mode 100644 index 0000000000000..58128dd352481 --- /dev/null +++ b/kona/bin/client/src/lib.rs @@ -0,0 +1,12 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![allow(clippy::type_complexity)] +#![cfg_attr(not(test), no_std)] + +extern crate alloc; + +pub mod fpvm_evm; +pub mod interop; +pub mod single; diff --git a/kona/bin/client/src/single.rs b/kona/bin/client/src/single.rs new file mode 100644 index 0000000000000..a9dbea8b44638 --- /dev/null +++ b/kona/bin/client/src/single.rs @@ -0,0 +1,186 @@ +//! Single-chain fault proof program entrypoint. + +use crate::fpvm_evm::FpvmOpEvmFactory; +use alloc::sync::Arc; +use alloy_consensus::Sealed; +use alloy_primitives::B256; +use core::fmt::Debug; +use kona_derive::{EthereumDataSource, PipelineErrorKind}; +use kona_driver::{Driver, DriverError}; +use kona_executor::{ExecutorError, TrieDBProvider}; +use kona_preimage::{CommsClient, HintWriterClient, PreimageKey, PreimageOracleClient}; +use kona_proof::{ + BootInfo, CachingOracle, HintType, + errors::OracleProviderError, + executor::KonaExecutor, + l1::{OracleBlobProvider, OracleL1ChainProvider, OraclePipeline}, + l2::OracleL2ChainProvider, + sync::new_oracle_pipeline_cursor, +}; +use thiserror::Error; +use tracing::{error, info}; + +/// An error that can occur when running the fault proof program. +#[derive(Error, Debug)] +pub enum FaultProofProgramError { + /// The claim is invalid. + #[error("Invalid claim. Expected {0}, actual {1}")] + InvalidClaim(B256, B256), + /// An error occurred in the Oracle provider. + #[error(transparent)] + OracleProviderError(#[from] OracleProviderError), + /// An error occurred in the derivation pipeline. + #[error(transparent)] + PipelineError(#[from] PipelineErrorKind), + /// An error occurred in the driver. + #[error(transparent)] + Driver(#[from] DriverError), +} + +/// Executes the fault proof program with the given [PreimageOracleClient] and [HintWriterClient]. +#[inline] +pub async fn run(oracle_client: P, hint_client: H) -> Result<(), FaultProofProgramError> +where + P: PreimageOracleClient + Send + Sync + Debug + Clone + 'static, + H: HintWriterClient + Send + Sync + Debug + Clone + 'static, +{ + const ORACLE_LRU_SIZE: usize = 1024; + + //////////////////////////////////////////////////////////////// + // PROLOGUE // + //////////////////////////////////////////////////////////////// + + let oracle = + Arc::new(CachingOracle::new(ORACLE_LRU_SIZE, oracle_client.clone(), hint_client.clone())); + let boot = BootInfo::load(oracle.as_ref()).await?; + let l1_config = boot.l1_config; + let rollup_config = Arc::new(boot.rollup_config); + let safe_head_hash = fetch_safe_head_hash(oracle.as_ref(), boot.agreed_l2_output_root).await?; + + let mut l1_provider = OracleL1ChainProvider::new(boot.l1_head, oracle.clone()); + let mut l2_provider = + OracleL2ChainProvider::new(safe_head_hash, rollup_config.clone(), oracle.clone()); + let beacon = OracleBlobProvider::new(oracle.clone()); + + // Fetch the safe head's block header. + let safe_head = l2_provider + .header_by_hash(safe_head_hash) + .map(|header| Sealed::new_unchecked(header, safe_head_hash))?; + + // If the claimed L2 block number is less than the safe head of the L2 chain, the claim is + // invalid. + if boot.claimed_l2_block_number < safe_head.number { + error!( + target: "client", + claimed = boot.claimed_l2_block_number, + safe = safe_head.number, + "Claimed L2 block number is less than the safe head", + ); + return Err(FaultProofProgramError::InvalidClaim( + boot.agreed_l2_output_root, + boot.claimed_l2_output_root, + )); + } + + // In the case where the agreed upon L2 output root is the same as the claimed L2 output root, + // trace extension is detected and we can skip the derivation and execution steps. + if boot.agreed_l2_output_root == boot.claimed_l2_output_root { + info!( + target: "client", + "Trace extension detected. State transition is already agreed upon.", + ); + return Ok(()); + } + + //////////////////////////////////////////////////////////////// + // DERIVATION & EXECUTION // + //////////////////////////////////////////////////////////////// + + // Create a new derivation driver with the given boot information and oracle. + let cursor = new_oracle_pipeline_cursor( + rollup_config.as_ref(), + safe_head, + &mut l1_provider, + &mut l2_provider, + ) + .await + .map_err(|e| { + error!(target: "client", "Failed to create pipeline cursor: {:?}", e); + e + })?; + l2_provider.set_cursor(cursor.clone()); + + let evm_factory = FpvmOpEvmFactory::new(hint_client, oracle_client); + let da_provider = + EthereumDataSource::new_from_parts(l1_provider.clone(), beacon, &rollup_config); + let pipeline = OraclePipeline::new( + rollup_config.clone(), + l1_config.into(), + cursor.clone(), + oracle.clone(), + da_provider, + l1_provider.clone(), + l2_provider.clone(), + ) + .await?; + + let executor = KonaExecutor::new( + rollup_config.as_ref(), + l2_provider.clone(), + l2_provider, + evm_factory, + None, + ); + let mut driver = Driver::new(cursor, executor, pipeline); + + // Run the derivation pipeline until we are able to produce the output root of the claimed + // L2 block. + let (safe_head, output_root) = driver + .advance_to_target(rollup_config.as_ref(), Some(boot.claimed_l2_block_number)) + .await?; + + //////////////////////////////////////////////////////////////// + // EPILOGUE // + //////////////////////////////////////////////////////////////// + + if output_root != boot.claimed_l2_output_root { + error!( + target: "client", + number = safe_head.block_info.number, + output_root = ?output_root, + claimed_output_root = ?boot.claimed_l2_output_root, + "Failed to validate L2 block", + ); + return Err(FaultProofProgramError::InvalidClaim(output_root, boot.claimed_l2_output_root)); + } + + info!( + target: "client", + number = safe_head.block_info.number, + output_root = ?output_root, + "Successfully validated L2 block", + ); + + Ok(()) +} + +/// Fetches the safe head hash of the L2 chain based on the agreed upon L2 output root in the +/// [BootInfo]. +pub async fn fetch_safe_head_hash( + caching_oracle: &O, + agreed_l2_output_root: B256, +) -> Result +where + O: CommsClient, +{ + let mut output_preimage = [0u8; 128]; + HintType::StartingL2Output + .with_data(&[agreed_l2_output_root.as_ref()]) + .send(caching_oracle) + .await?; + caching_oracle + .get_exact(PreimageKey::new_keccak256(*agreed_l2_output_root), output_preimage.as_mut()) + .await?; + + output_preimage[96..128].try_into().map_err(OracleProviderError::SliceConversion) +} diff --git a/kona/bin/client/testdata/holocene-op-sepolia-26215604-witness.tar.zst b/kona/bin/client/testdata/holocene-op-sepolia-26215604-witness.tar.zst new file mode 100644 index 0000000000000..10ef96c1c54b2 Binary files /dev/null and b/kona/bin/client/testdata/holocene-op-sepolia-26215604-witness.tar.zst differ diff --git a/kona/bin/host/Cargo.toml b/kona/bin/host/Cargo.toml new file mode 100644 index 0000000000000..edba656bb6c17 --- /dev/null +++ b/kona/bin/host/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "kona-host" +version = "1.0.2" +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +# Proof +kona-mpt.workspace = true +kona-client.workspace = true +kona-executor.workspace = true +kona-std-fpvm.workspace = true +kona-proof-interop.workspace = true +kona-proof = { workspace = true, features = ["std"] } +kona-preimage = { workspace = true, features = ["std"] } + +# Protocol +kona-driver.workspace = true +kona-derive.workspace = true +kona-registry.workspace = true +kona-protocol = { workspace = true, features = ["std", "serde"] } +kona-genesis = { workspace = true, features = ["std", "serde"] } + +# Services +kona-cli.workspace = true +kona-providers-alloy.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-transport.workspace = true +alloy-eips = { workspace = true, features = ["kzg"] } +alloy-serde.workspace = true +alloy-provider = { workspace = true, features = ["reqwest"] } +alloy-consensus = { workspace = true, features = ["std"] } +alloy-rpc-client.workspace = true +alloy-transport-http.workspace = true +alloy-rpc-types = { workspace = true, features = ["eth", "debug"] } +alloy-primitives = { workspace = true, features = ["serde"] } +alloy-rpc-types-beacon.workspace = true + +# Op Alloy +op-alloy-rpc-types-engine = { workspace = true, features = ["serde"] } +op-alloy-network.workspace = true +alloy-op-evm = { workspace = true, features = ["std"] } + +# Revm +revm = { workspace = true, features = ["std", "c-kzg", "secp256k1", "portable", "blst"] } + +# General +anyhow.workspace = true +tracing.workspace = true +reqwest.workspace = true +serde_json.workspace = true +async-trait.workspace = true +rocksdb = { workspace = true, features = ["snappy", "bindgen-runtime"] } +tokio = { workspace = true, features = ["full"] } +serde = { workspace = true, features = ["derive"] } +clap = { workspace = true, features = ["derive", "env"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } +thiserror.workspace = true + +# KZG +ark-ff.workspace = true + +[dev-dependencies] +proptest.workspace = true + +[features] +default = [ "interop", "single" ] +single = [] +interop = [ "single" ] + +[[bin]] +name = "kona-host" +path = "src/bin/host.rs" diff --git a/kona/bin/host/README.md b/kona/bin/host/README.md new file mode 100644 index 0000000000000..0df95af2b706c --- /dev/null +++ b/kona/bin/host/README.md @@ -0,0 +1,44 @@ +# `kona-host` + +kona-host is a CLI application that runs the [pre-image server][p-server] and [client program][client-program]. + +## Modes + +**Host Modes** + +| Mode | Description | +|----------|-------------------------------------------------------------------------------| +| `single` | Runs the preimage server + client program for a single-chain (pre-interop.) | +| `super` | Runs the preimage server + client program for a superchain cluster (interop.) | + +**Preimage Server Modes** + +| Mode | Description | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `server` | Starts with the preimage server only, expecting the client program to have been invoked by the host process. This mode is intended for use by the FPVM when running the client program. | +| `native` | Starts both the preimage oracle and client program in a native process. This mode is useful for witness generation as well as testing. | + +## Usage + +```txt +kona-host is a CLI application that runs the Kona pre-image server and client program. The host +can run in two modes: server mode and native mode. In server mode, the host runs the pre-image +server and waits for the client program in the parent process to request pre-images. In native +mode, the host runs the client program in a separate thread with the pre-image server in the +primary thread. + +Usage: kona-host [OPTIONS] + +Commands: + single Run the host in single-chain mode + super Run the host in super-chain (interop) mode + help Print this message or the help of the given subcommand(s) + +Options: + -v, --v... Verbosity level (0-2) + -h, --help Print help + -V, --version Print version +``` + +[p-server]: https://specs.optimism.io/fault-proof/index.html#pre-image-oracle +[client-program]: https://specs.optimism.io/fault-proof/index.html#fault-proof-program diff --git a/kona/bin/host/src/backend/mod.rs b/kona/bin/host/src/backend/mod.rs new file mode 100644 index 0000000000000..7892bd2eb2287 --- /dev/null +++ b/kona/bin/host/src/backend/mod.rs @@ -0,0 +1,9 @@ +//! Backend for the preimage server. + +mod offline; +pub use offline::OfflineHostBackend; + +mod online; +pub use online::{HintHandler, OnlineHostBackend, OnlineHostBackendCfg}; + +pub(crate) mod util; diff --git a/kona/bin/host/src/backend/offline.rs b/kona/bin/host/src/backend/offline.rs new file mode 100644 index 0000000000000..a2ca1e531fd0a --- /dev/null +++ b/kona/bin/host/src/backend/offline.rs @@ -0,0 +1,50 @@ +//! Contains the implementations of the [HintRouter] and [PreimageFetcher] traits. + +use crate::kv::KeyValueStore; +use async_trait::async_trait; +use kona_preimage::{ + HintRouter, PreimageFetcher, PreimageKey, + errors::{PreimageOracleError, PreimageOracleResult}, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// A [KeyValueStore]-backed implementation of the [PreimageFetcher] trait. +#[derive(Debug)] +pub struct OfflineHostBackend +where + KV: KeyValueStore + ?Sized, +{ + inner: Arc>, +} + +impl OfflineHostBackend +where + KV: KeyValueStore + ?Sized, +{ + /// Create a new [OfflineHostBackend] from the given [KeyValueStore]. + pub const fn new(kv_store: Arc>) -> Self { + Self { inner: kv_store } + } +} + +#[async_trait] +impl PreimageFetcher for OfflineHostBackend +where + KV: KeyValueStore + Send + Sync + ?Sized, +{ + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { + let kv_store = self.inner.read().await; + kv_store.get(key.into()).ok_or(PreimageOracleError::KeyNotFound) + } +} + +#[async_trait] +impl HintRouter for OfflineHostBackend +where + KV: KeyValueStore + Send + Sync + ?Sized, +{ + async fn route_hint(&self, _hint: String) -> PreimageOracleResult<()> { + Ok(()) + } +} diff --git a/kona/bin/host/src/backend/online.rs b/kona/bin/host/src/backend/online.rs new file mode 100644 index 0000000000000..aa4ed7e26cb3f --- /dev/null +++ b/kona/bin/host/src/backend/online.rs @@ -0,0 +1,157 @@ +//! Contains the [OnlineHostBackend] definition. + +use crate::SharedKeyValueStore; +use anyhow::Result; +use async_trait::async_trait; +use kona_preimage::{ + HintRouter, PreimageFetcher, PreimageKey, + errors::{PreimageOracleError, PreimageOracleResult}, +}; +use kona_proof::{Hint, errors::HintParsingError}; +use std::{collections::HashSet, hash::Hash, str::FromStr, sync::Arc, time::Duration}; +use tokio::{sync::RwLock, time::timeout}; +use tracing::{debug, error, trace}; + +/// The [OnlineHostBackendCfg] trait is used to define the type configuration for the +/// [OnlineHostBackend]. +pub trait OnlineHostBackendCfg { + /// The hint type describing the range of hints that can be received. + type HintType: FromStr + Hash + Eq + PartialEq + Clone + Send + Sync; + + /// The providers that are used to fetch data in response to hints. + type Providers: Send + Sync; +} + +/// A [HintHandler] is an interface for receiving hints, fetching remote data, and storing it in the +/// key-value store. +#[async_trait] +pub trait HintHandler { + /// The type configuration for the [HintHandler]. + type Cfg: OnlineHostBackendCfg; + + /// Fetches data in response to a hint. + async fn fetch_hint( + hint: Hint<::HintType>, + cfg: &Self::Cfg, + providers: &::Providers, + kv: SharedKeyValueStore, + ) -> Result<()>; +} + +/// The [OnlineHostBackend] is a [HintRouter] and [PreimageFetcher] that is used to fetch data from +/// remote sources in response to hints. +/// +/// [PreimageKey]: kona_preimage::PreimageKey +#[allow(missing_debug_implementations)] +pub struct OnlineHostBackend +where + C: OnlineHostBackendCfg, + H: HintHandler, +{ + /// The configuration that is used to route hints. + cfg: C, + /// The key-value store that is used to store preimages. + kv: SharedKeyValueStore, + /// The providers that are used to fetch data in response to hints. + providers: C::Providers, + /// Hints that should be immediately executed by the host. + proactive_hints: HashSet, + /// The last hint that was received. + last_hint: Arc>>>, + /// Phantom marker for the [HintHandler]. + _hint_handler: std::marker::PhantomData, +} + +impl OnlineHostBackend +where + C: OnlineHostBackendCfg, + H: HintHandler, +{ + /// Creates a new [HintHandler] with the given configuration, key-value store, providers, and + /// external configuration. + pub fn new(cfg: C, kv: SharedKeyValueStore, providers: C::Providers, _: H) -> Self { + Self { + cfg, + kv, + providers, + proactive_hints: HashSet::default(), + last_hint: Arc::new(RwLock::new(None)), + _hint_handler: std::marker::PhantomData, + } + } + + /// Adds a new proactive hint to the [OnlineHostBackend]. + pub fn with_proactive_hint(mut self, hint_type: C::HintType) -> Self { + self.proactive_hints.insert(hint_type); + self + } +} + +#[async_trait] +impl HintRouter for OnlineHostBackend +where + C: OnlineHostBackendCfg + Send + Sync, + H: HintHandler + Send + Sync, +{ + /// Set the last hint to be received. + async fn route_hint(&self, hint: String) -> PreimageOracleResult<()> { + trace!(target: "host_backend", "Received hint: {hint}"); + + let parsed_hint = hint + .parse::>() + .map_err(|e| PreimageOracleError::HintParseFailed(e.to_string()))?; + if self.proactive_hints.contains(&parsed_hint.ty) { + debug!(target: "host_backend", "Proactive hint received; Immediately fetching {hint}"); + H::fetch_hint(parsed_hint, &self.cfg, &self.providers, self.kv.clone()) + .await + .map_err(|e| PreimageOracleError::Other(e.to_string()))?; + } else { + let mut hint_lock = self.last_hint.write().await; + hint_lock.replace(parsed_hint); + } + + Ok(()) + } +} + +#[async_trait] +impl PreimageFetcher for OnlineHostBackend +where + C: OnlineHostBackendCfg + Send + Sync, + H: HintHandler + Send + Sync, +{ + /// Get the preimage for the given key. + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { + trace!(target: "host_backend", "Pre-image requested. Key: {key}"); + + // Acquire a read lock on the key-value store. + let kv_lock = self.kv.read().await; + let mut preimage = kv_lock.get(key.into()); + + // Drop the read lock before beginning the retry loop. + drop(kv_lock); + + // Use a loop to keep retrying the prefetch as long as the key is not found + timeout(Duration::from_secs(10), async { + while preimage.is_none() { + if let Some(hint) = self.last_hint.read().await.as_ref() { + let value = + H::fetch_hint(hint.clone(), &self.cfg, &self.providers, self.kv.clone()) + .await; + + if let Err(e) = value { + error!(target: "host_backend", "Failed to prefetch hint: {e}"); + continue; + } + + let kv_lock = self.kv.read().await; + preimage = kv_lock.get(key.into()); + } + } + }) + .await + .map_err(|_| PreimageOracleError::Timeout)?; + + preimage.ok_or(PreimageOracleError::KeyNotFound) + } +} diff --git a/kona/bin/host/src/backend/util.rs b/kona/bin/host/src/backend/util.rs new file mode 100644 index 0000000000000..79a3c07a4d03b --- /dev/null +++ b/kona/bin/host/src/backend/util.rs @@ -0,0 +1,41 @@ +//! Utilities for the preimage server backend. + +use crate::KeyValueStore; +use alloy_consensus::EMPTY_ROOT_HASH; +use alloy_primitives::keccak256; +use alloy_rlp::EMPTY_STRING_CODE; +use anyhow::Result; +use kona_preimage::{PreimageKey, PreimageKeyType}; +use tokio::sync::RwLock; + +/// Constructs a merkle patricia trie from the ordered list passed and stores all encoded +/// intermediate nodes of the trie in the [KeyValueStore]. +pub(crate) async fn store_ordered_trie>( + kv: &RwLock, + values: &[T], +) -> Result<()> { + let mut kv_write_lock = kv.write().await; + + // If the list of nodes is empty, store the empty root hash and exit early. + // The `HashBuilder` will not push the preimage of the empty root hash to the + // `ProofRetainer` in the event that there are no leaves inserted. + if values.is_empty() { + let empty_key = PreimageKey::new(*EMPTY_ROOT_HASH, PreimageKeyType::Keccak256); + return kv_write_lock.set(empty_key.into(), [EMPTY_STRING_CODE].into()); + } + + let mut hb = kona_mpt::ordered_trie_with_encoder(values, |node, buf| { + buf.put_slice(node.as_ref()); + }); + hb.root(); + let intermediates = hb.take_proof_nodes().into_inner(); + + for (_, value) in intermediates.into_iter() { + let value_hash = keccak256(value.as_ref()); + let key = PreimageKey::new(*value_hash, PreimageKeyType::Keccak256); + + kv_write_lock.set(key.into(), value.into())?; + } + + Ok(()) +} diff --git a/kona/bin/host/src/bin/host.rs b/kona/bin/host/src/bin/host.rs new file mode 100644 index 0000000000000..a7f27ca5cf659 --- /dev/null +++ b/kona/bin/host/src/bin/host.rs @@ -0,0 +1,64 @@ +//! Main entrypoint for the host binary. + +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use kona_cli::{LogArgs, LogConfig, cli_styles}; +use serde::Serialize; +use tracing::info; +use tracing_subscriber::EnvFilter; + +const ABOUT: &str = " +kona-host is a CLI application that runs the Kona pre-image server and client program. The host +can run in two modes: server mode and native mode. In server mode, the host runs the pre-image +server and waits for the client program in the parent process to request pre-images. In native +mode, the host runs the client program in a separate thread with the pre-image server in the +primary thread. +"; + +/// The host binary CLI application arguments. +#[derive(Parser, Serialize, Clone, Debug)] +#[command(about = ABOUT, version, styles = cli_styles())] +pub struct HostCli { + /// Logging arguments. + #[command(flatten)] + pub log_args: LogArgs, + /// Host mode + #[command(subcommand)] + pub mode: HostMode, +} + +/// Operation modes for the host binary. +#[derive(Subcommand, Serialize, Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum HostMode { + /// Run the host in single-chain mode. + #[cfg(feature = "single")] + Single(kona_host::single::SingleChainHost), + /// Run the host in super-chain (interop) mode. + #[cfg(feature = "interop")] + Super(kona_host::interop::InteropHost), +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<()> { + let cfg = HostCli::parse(); + LogConfig::new(cfg.log_args).init_tracing_subscriber(None::)?; + + match cfg.mode { + #[cfg(feature = "single")] + HostMode::Single(cfg) => { + cfg.start().await?; + } + #[cfg(feature = "interop")] + HostMode::Super(cfg) => { + cfg.start().await?; + } + } + + info!(target: "host", "Exiting host program."); + Ok(()) +} diff --git a/kona/bin/host/src/eth/mod.rs b/kona/bin/host/src/eth/mod.rs new file mode 100644 index 0000000000000..0ea7dd8dba532 --- /dev/null +++ b/kona/bin/host/src/eth/mod.rs @@ -0,0 +1,11 @@ +//! Ethereum utilities for the host binary. + +use alloy_provider::{Network, RootProvider}; + +mod precompiles; +pub(crate) use precompiles::execute; + +/// Returns an HTTP provider for the given URL. +pub async fn rpc_provider(url: &str) -> RootProvider { + RootProvider::connect(url).await.unwrap() +} diff --git a/kona/bin/host/src/eth/precompiles.rs b/kona/bin/host/src/eth/precompiles.rs new file mode 100644 index 0000000000000..677d9f10dccec --- /dev/null +++ b/kona/bin/host/src/eth/precompiles.rs @@ -0,0 +1,34 @@ +//! Accelerated precompile runner for the host program. + +use alloy_primitives::{Address, Bytes}; +use anyhow::{Result, anyhow}; +use revm::precompile::{self, Precompile}; + +/// List of precompiles that are accelerated by the host program. +pub(crate) const ACCELERATED_PRECOMPILES: &[Precompile] = &[ + precompile::secp256k1::ECRECOVER, // ecRecover + precompile::bn254::pair::ISTANBUL, // ecPairing + precompile::bls12_381::g1_add::PRECOMPILE, // BLS12-381 G1 Point Addition + precompile::bls12_381::g1_msm::PRECOMPILE, /* BLS12-381 G1 Point Multi-scalar + * Multiplication */ + precompile::bls12_381::g2_add::PRECOMPILE, // BLS12-381 G2 Point Addition + precompile::bls12_381::g2_msm::PRECOMPILE, // BLS12-381 G2 Point Multi-scalar Multiplication + precompile::bls12_381::map_fp2_to_g2::PRECOMPILE, // BLS12-381 FP2 to G2 Point Mapping + precompile::bls12_381::map_fp_to_g1::PRECOMPILE, // BLS12-381 FP to G1 Point Mapping + precompile::bls12_381::pairing::PRECOMPILE, // BLS12-381 pairing + precompile::kzg_point_evaluation::POINT_EVALUATION, // KZG point evaluation +]; + +/// Executes an accelerated precompile on [revm]. +pub(crate) fn execute>(address: Address, input: T, gas: u64) -> Result> { + if let Some(precompile) = + ACCELERATED_PRECOMPILES.iter().find(|precompile| *precompile.address() == address) + { + let output = precompile.precompile()(&input.into(), gas) + .map_err(|e| anyhow!("Failed precompile execution: {e}"))?; + + Ok(output.bytes.into()) + } else { + anyhow::bail!("Precompile not accelerated"); + } +} diff --git a/kona/bin/host/src/interop/cfg.rs b/kona/bin/host/src/interop/cfg.rs new file mode 100644 index 0000000000000..d75a36e5ec4ad --- /dev/null +++ b/kona/bin/host/src/interop/cfg.rs @@ -0,0 +1,385 @@ +//! This module contains all CLI-specific code for the interop entrypoint. + +use super::{InteropHintHandler, InteropLocalInputs}; +use crate::{ + DiskKeyValueStore, MemoryKeyValueStore, OfflineHostBackend, OnlineHostBackend, + OnlineHostBackendCfg, PreimageServer, SharedKeyValueStore, SplitKeyValueStore, + eth::rpc_provider, server::PreimageServerError, +}; +use alloy_primitives::{B256, Bytes}; +use alloy_provider::{Provider, RootProvider}; +use clap::Parser; +use kona_cli::cli_styles; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_preimage::{ + BidirectionalChannel, Channel, HintReader, HintWriter, OracleReader, OracleServer, +}; +use kona_proof_interop::HintType; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; +use kona_std_fpvm::{FileChannel, FileDescriptor}; +use op_alloy_network::Optimism; +use serde::Serialize; +use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc}; +use tokio::{ + sync::RwLock, + task::{self, JoinHandle}, +}; + +/// The interop host application. +#[derive(Default, Parser, Serialize, Clone, Debug)] +#[command(styles = cli_styles())] +pub struct InteropHost { + /// Hash of the L1 head block, marking a static, trusted cutoff point for reading data from the + /// L1 chain. + #[arg(long, env)] + pub l1_head: B256, + /// Agreed [PreState] to start from. + /// + /// [PreState]: kona_proof_interop::PreState + #[arg(long, visible_alias = "l2-pre-state", value_parser = Bytes::from_str, env)] + pub agreed_l2_pre_state: Bytes, + /// Claimed L2 post-state to validate. + #[arg(long, visible_alias = "l2-claim", env)] + pub claimed_l2_post_state: B256, + /// Claimed L2 timestamp, corresponding to the L2 post-state. + #[arg(long, visible_alias = "l2-timestamp", env)] + pub claimed_l2_timestamp: u64, + /// Addresses of L2 JSON-RPC endpoints to use (eth and debug namespace required). + #[arg( + long, + visible_alias = "l2s", + requires = "l1_node_address", + requires = "l1_beacon_address", + value_delimiter = ',', + env + )] + pub l2_node_addresses: Option>, + /// Address of L1 JSON-RPC endpoint to use (eth and debug namespace required) + #[arg( + long, + visible_alias = "l1", + requires = "l2_node_addresses", + requires = "l1_beacon_address", + env + )] + pub l1_node_address: Option, + /// Address of the L1 Beacon API endpoint to use. + #[arg( + long, + visible_alias = "beacon", + requires = "l1_node_address", + requires = "l2_node_addresses", + env + )] + pub l1_beacon_address: Option, + /// The Data Directory for preimage data storage. Optional if running in online mode, + /// required if running in offline mode. + #[arg( + long, + visible_alias = "db", + required_unless_present_all = ["l2_node_addresses", "l1_node_address", "l1_beacon_address"], + env + )] + pub data_dir: Option, + /// Run the client program natively. + #[arg(long, conflicts_with = "server", required_unless_present = "server")] + pub native: bool, + /// Run in pre-image server mode without executing any client program. If not provided, the + /// host will run the client program in the host process. + #[arg(long, conflicts_with = "native", required_unless_present = "native")] + pub server: bool, + /// Path to rollup configs. If provided, the host will use this config instead of attempting to + /// look up the configs in the superchain registry. + /// The rollup configs should be stored as serde-JSON serialized files. + #[arg(long, alias = "rollup-cfgs", value_delimiter = ',', env)] + pub rollup_config_paths: Option>, + /// Path to l1 configs. If provided, the host will use this config instead of attempting to + /// look up the configs in the superchain registry. + /// The l1 configs should be stored as serde-JSON serialized files. + #[arg(long, alias = "l1-cfgs", value_delimiter = ',', env)] + pub l1_config_paths: Option>, +} + +/// An error that can occur when handling interop hosts +#[derive(Debug, thiserror::Error)] +pub enum InteropHostError { + /// An error when handling preimage requests. + #[error("Error handling preimage request: {0}")] + PreimageServerError(#[from] PreimageServerError), + /// An IO error. + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), + /// A JSON parse error. + #[error("Failed deserializing RollupConfig: {0}")] + ParseError(#[from] serde_json::Error), + /// Impossible to find the L1 chain config for the given chain ID. + #[error("L1 chain config not found for chain ID: {0}")] + L1ChainConfigNotFound(u64), + /// Task failed to execute to completion. + #[error("Join error: {0}")] + ExecutionError(#[from] tokio::task::JoinError), + /// A RPC error. + #[error("Rpc Error: {0}")] + RpcError(#[from] alloy_transport::RpcError), + /// An error when no provider found for chain ID. + #[error("No provider found for chain ID: {0}")] + RootProviderError(u64), + /// Any other error. + #[error("Error: {0}")] + Other(&'static str), +} + +impl InteropHost { + /// Starts the [InteropHost] application. + pub async fn start(self) -> Result<(), InteropHostError> { + if self.server { + let hint = FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite); + let preimage = + FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite); + + self.start_server(hint, preimage).await?.await? + } else { + self.start_native().await + } + } + + /// Starts the preimage server, communicating with the client over the provided channels. + async fn start_server( + &self, + hint: C, + preimage: C, + ) -> Result>, InteropHostError> + where + C: Channel + Send + Sync + 'static, + { + let kv_store = self.create_key_value_store()?; + + let task_handle = if self.is_offline() { + task::spawn(async { + PreimageServer::new( + OracleServer::new(preimage), + HintReader::new(hint), + Arc::new(OfflineHostBackend::new(kv_store)), + ) + .start() + .await + .map_err(InteropHostError::from) + }) + } else { + let providers = self.create_providers().await?; + let backend = OnlineHostBackend::new( + self.clone(), + kv_store.clone(), + providers, + InteropHintHandler, + ) + .with_proactive_hint(HintType::L2BlockData); + + task::spawn(async { + PreimageServer::new( + OracleServer::new(preimage), + HintReader::new(hint), + Arc::new(backend), + ) + .start() + .await + .map_err(InteropHostError::from) + }) + }; + + Ok(task_handle) + } + + /// Starts the host in native mode, running both the client and preimage server in the same + /// process. + async fn start_native(&self) -> Result<(), InteropHostError> { + let hint = BidirectionalChannel::new()?; + let preimage = BidirectionalChannel::new()?; + + let server_task = self.start_server(hint.host, preimage.host).await?; + let client_task = task::spawn(kona_client::interop::run( + OracleReader::new(preimage.client), + HintWriter::new(hint.client), + )); + + let (_, client_result) = tokio::try_join!(server_task, client_task)?; + + // Bubble up the exit status of the client program if execution completes. + std::process::exit(client_result.is_err() as i32) + } + + /// Returns `true` if the host is running in offline mode. + pub const fn is_offline(&self) -> bool { + self.l1_node_address.is_none() && + self.l2_node_addresses.is_none() && + self.l1_beacon_address.is_none() && + self.data_dir.is_some() + } + + /// Reads the [RollupConfig]s from the file system and returns a map of L2 chain ID -> + /// [RollupConfig]s. + pub fn read_rollup_configs( + &self, + ) -> Option, InteropHostError>> { + let rollup_config_paths = self.rollup_config_paths.as_ref()?; + + Some(rollup_config_paths.iter().try_fold(HashMap::default(), |mut acc, path| { + // Read the serialized config from the file system. + let ser_config = std::fs::read_to_string(path)?; + + // Deserialize the config and return it. + let cfg: RollupConfig = serde_json::from_str(&ser_config)?; + + acc.insert(cfg.l2_chain_id.id(), cfg); + Ok(acc) + })) + } + + /// Reads the [`L1ChainConfig`]s from the file system and returns a map of L1 chain ID -> + /// [`L1ChainConfig`]s. + pub fn read_l1_configs(&self) -> Option, InteropHostError>> { + let l1_config_paths = self.l1_config_paths.as_ref()?; + + Some(l1_config_paths.iter().try_fold(HashMap::default(), |mut acc, path| { + // Read the serialized config from the file system. + let ser_config = fs::read_to_string(path)?; + + // Deserialize the config and return it. + let cfg: L1ChainConfig = serde_json::from_str(&ser_config)?; + + acc.insert(cfg.chain_id, cfg); + Ok(acc) + })) + } + + /// Creates the key-value store for the host backend. + fn create_key_value_store(&self) -> Result { + let local_kv_store = InteropLocalInputs::new(self.clone()); + + let kv_store: SharedKeyValueStore = if let Some(ref data_dir) = self.data_dir { + let disk_kv_store = DiskKeyValueStore::new(data_dir.clone()); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, disk_kv_store); + Arc::new(RwLock::new(split_kv_store)) + } else { + let mem_kv_store = MemoryKeyValueStore::new(); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, mem_kv_store); + Arc::new(RwLock::new(split_kv_store)) + }; + + Ok(kv_store) + } + + /// Creates the providers required for the preimage server backend. + async fn create_providers(&self) -> Result { + let l1_provider = rpc_provider( + self.l1_node_address.as_ref().ok_or(InteropHostError::Other("Provider must be set"))?, + ) + .await; + + let blob_provider = OnlineBlobProvider::init(OnlineBeaconClient::new_http( + self.l1_beacon_address + .clone() + .ok_or(InteropHostError::Other("Beacon API URL must be set"))?, + )) + .await; + + // Resolve all chain IDs to their corresponding providers. + let l2_node_addresses = self + .l2_node_addresses + .as_ref() + .ok_or(InteropHostError::Other("L2 node addresses must be set"))?; + let mut l2_providers = HashMap::default(); + for l2_node_address in l2_node_addresses { + let l2_provider = rpc_provider::(l2_node_address).await; + let chain_id = l2_provider.get_chain_id().await?; + l2_providers.insert(chain_id, l2_provider); + } + + Ok(InteropProviders { l1: l1_provider, blobs: blob_provider, l2s: l2_providers }) + } +} + +impl OnlineHostBackendCfg for InteropHost { + type HintType = HintType; + type Providers = InteropProviders; +} + +/// The providers required for the single chain host. +#[derive(Debug, Clone)] +pub struct InteropProviders { + /// The L1 EL provider. + pub l1: RootProvider, + /// The L1 beacon node provider. + pub blobs: OnlineBlobProvider, + /// The L2 EL providers, keyed by chain ID. + pub l2s: HashMap>, +} + +impl InteropProviders { + /// Returns the L2 [RootProvider] for the given chain ID. + pub fn l2(&self, chain_id: &u64) -> Result<&RootProvider, InteropHostError> { + self.l2s.get(chain_id).ok_or_else(|| InteropHostError::RootProviderError(*chain_id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::b256; + + #[test] + fn test_parse_interop_host_cli() { + let hash = b256!("ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68"); + let host = InteropHost::parse_from([ + "interop-host", + "--l1-head", + "ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68", + "--l2-pre-state", + "ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68", + "--claimed-l2-post-state", + &hash.to_string(), + "--claimed-l2-timestamp", + "0", + "--native", + "--l2-node-addresses", + "http://localhost:8545", + "--l1-node-address", + "http://localhost:8546", + "--l1-beacon-address", + "http://localhost:8547", + ]); + assert_eq!(host.l1_head, hash); + assert_eq!(host.agreed_l2_pre_state, Bytes::from(hash.0)); + assert_eq!(host.claimed_l2_post_state, hash); + assert_eq!(host.claimed_l2_timestamp, 0); + assert!(host.native); + } + + #[test] + fn test_parse_interop_hex_bytes() { + let hash = b256!("ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68"); + let host = InteropHost::parse_from([ + "interop-host", + "--l1-head", + "ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68", + "--l2-pre-state", + "ff", + "--claimed-l2-post-state", + &hash.to_string(), + "--claimed-l2-timestamp", + "0", + "--native", + "--l2-node-addresses", + "http://localhost:8545", + "--l1-node-address", + "http://localhost:8546", + "--l1-beacon-address", + "http://localhost:8547", + ]); + assert_eq!(host.l1_head, hash); + assert_eq!(host.agreed_l2_pre_state, Bytes::from([0xff])); + assert_eq!(host.claimed_l2_post_state, hash); + assert_eq!(host.claimed_l2_timestamp, 0); + assert!(host.native); + } +} diff --git a/kona/bin/host/src/interop/handler.rs b/kona/bin/host/src/interop/handler.rs new file mode 100644 index 0000000000000..82bcb4ebb4bab --- /dev/null +++ b/kona/bin/host/src/interop/handler.rs @@ -0,0 +1,618 @@ +//! [HintHandler] for the [InteropHost]. + +use super::InteropHost; +use crate::{ + HintHandler, OnlineHostBackend, OnlineHostBackendCfg, PreimageServer, SharedKeyValueStore, + backend::util::store_ordered_trie, +}; +use alloy_consensus::{Header, Sealed}; +use alloy_eips::{ + eip2718::Encodable2718, + eip4844::{BlobTransactionSidecarItem, FIELD_ELEMENTS_PER_BLOB, IndexedBlobHash}, +}; +use alloy_op_evm::OpEvmFactory; +use alloy_primitives::{Address, B256, Bytes, keccak256}; +use alloy_provider::Provider; +use alloy_rlp::{Decodable, Encodable}; +use alloy_rpc_types::Block; +use anyhow::{Result, anyhow, ensure}; +use ark_ff::{BigInteger, PrimeField}; +use async_trait::async_trait; +use kona_derive::EthereumDataSource; +use kona_driver::Driver; +use kona_executor::TrieDBProvider; +use kona_preimage::{ + BidirectionalChannel, HintReader, HintWriter, OracleReader, OracleServer, PreimageKey, + PreimageKeyType, +}; +use kona_proof::{ + CachingOracle, Hint, + executor::KonaExecutor, + l1::{OracleBlobProvider, OracleL1ChainProvider, OraclePipeline, ROOTS_OF_UNITY}, + l2::OracleL2ChainProvider, + sync::new_oracle_pipeline_cursor, +}; +use kona_proof_interop::{HintType, PreState}; +use kona_protocol::{BlockInfo, OutputRoot, Predeploys}; +use kona_registry::{L1_CONFIGS, ROLLUP_CONFIGS}; +use std::sync::Arc; +use tokio::task; +use tracing::{Instrument, debug, info, info_span, warn}; + +/// The [HintHandler] for the [InteropHost]. +#[derive(Debug, Clone, Copy)] +pub struct InteropHintHandler; + +#[async_trait] +impl HintHandler for InteropHintHandler { + type Cfg = InteropHost; + + async fn fetch_hint( + hint: Hint<::HintType>, + cfg: &Self::Cfg, + providers: &::Providers, + kv: SharedKeyValueStore, + ) -> Result<()> { + match hint.ty { + HintType::L1BlockHeader => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let raw_header: Bytes = + providers.l1.client().request("debug_getRawHeader", [hash]).await?; + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), raw_header.into())?; + } + HintType::L1Transactions => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let Block { transactions, .. } = providers + .l1 + .get_block_by_hash(hash) + .full() + .await? + .ok_or(anyhow!("Block not found"))?; + let encoded_transactions = transactions + .into_transactions() + .map(|tx| tx.inner.encoded_2718()) + .collect::>(); + + store_ordered_trie(kv.as_ref(), encoded_transactions.as_slice()).await?; + } + HintType::L1Receipts => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let raw_receipts: Vec = + providers.l1.client().request("debug_getRawReceipts", [hash]).await?; + + store_ordered_trie(kv.as_ref(), raw_receipts.as_slice()).await?; + } + HintType::L1Blob => { + ensure!(hint.data.len() == 48, "Invalid hint data length"); + + let hash_data_bytes: [u8; 32] = hint.data[0..32].try_into()?; + let index_data_bytes: [u8; 8] = hint.data[32..40].try_into()?; + let timestamp_data_bytes: [u8; 8] = hint.data[40..48].try_into()?; + + let hash: B256 = hash_data_bytes.into(); + let index = u64::from_be_bytes(index_data_bytes); + let timestamp = u64::from_be_bytes(timestamp_data_bytes); + + let partial_block_ref = BlockInfo { timestamp, ..Default::default() }; + let indexed_hash = IndexedBlobHash { index, hash }; + + // Fetch the blob sidecar from the blob provider. + let mut sidecars = providers + .blobs + .fetch_filtered_blob_sidecars(&partial_block_ref, &[indexed_hash]) + .await + .map_err(|e| anyhow!("Failed to fetch blob sidecars: {e}"))?; + + if sidecars.len() != 1 { + anyhow::bail!("Expected 1 sidecar, got {}", sidecars.len()); + } + + let BlobTransactionSidecarItem { + blob, + kzg_proof: proof, + kzg_commitment: commitment, + .. + } = sidecars.pop().expect("Expected 1 sidecar"); + + // Acquire a lock on the key-value store and set the preimages. + let mut kv_lock = kv.write().await; + + // Set the preimage for the blob commitment. + kv_lock.set( + PreimageKey::new(*hash, PreimageKeyType::Sha256).into(), + commitment.to_vec(), + )?; + + // Write all the field elements to the key-value store. There should be 4096. + // The preimage oracle key for each field element is the keccak256 hash of + // `abi.encodePacked(sidecar.KZGCommitment, bytes32(ROOTS_OF_UNITY[i]))`. + let mut blob_key = [0u8; 80]; + blob_key[..48].copy_from_slice(commitment.as_ref()); + for i in 0..FIELD_ELEMENTS_PER_BLOB { + blob_key[48..].copy_from_slice( + ROOTS_OF_UNITY[i as usize].into_bigint().to_bytes_be().as_ref(), + ); + let blob_key_hash = keccak256(blob_key.as_ref()); + + kv_lock + .set(PreimageKey::new_keccak256(*blob_key_hash).into(), blob_key.into())?; + kv_lock.set( + PreimageKey::new(*blob_key_hash, PreimageKeyType::Blob).into(), + blob[(i as usize) << 5..(i as usize + 1) << 5].to_vec(), + )?; + } + + // Write the KZG Proof as the 4096th element. + // Note: This is not associated with a root of unity, as to be backwards compatible + // with ZK users of kona that use this proof for the overall blob. + blob_key[72..].copy_from_slice((FIELD_ELEMENTS_PER_BLOB).to_be_bytes().as_ref()); + let blob_key_hash = keccak256(blob_key.as_ref()); + + kv_lock.set(PreimageKey::new_keccak256(*blob_key_hash).into(), blob_key.into())?; + kv_lock.set( + PreimageKey::new(*blob_key_hash, PreimageKeyType::Blob).into(), + proof.to_vec(), + )?; + } + HintType::L1Precompile => { + ensure!(hint.data.len() >= 28, "Invalid hint data length"); + + let address = Address::from_slice(&hint.data.as_ref()[..20]); + let gas = u64::from_be_bytes(hint.data.as_ref()[20..28].try_into()?); + let input = hint.data[28..].to_vec(); + let input_hash = keccak256(hint.data.as_ref()); + + let result = crate::eth::execute(address, input, gas).map_or_else( + |_| vec![0u8; 1], + |raw_res| { + let mut res = Vec::with_capacity(1 + raw_res.len()); + res.push(0x01); + res.extend_from_slice(&raw_res); + res + }, + ); + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*input_hash).into(), hint.data.into())?; + kv_lock.set( + PreimageKey::new(*input_hash, PreimageKeyType::Precompile).into(), + result, + )?; + } + HintType::AgreedPreState => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + + if hash != keccak256(cfg.agreed_l2_pre_state.as_ref()) { + anyhow::bail!("Agreed pre-state hash does not match."); + } + + let mut kv_write_lock = kv.write().await; + kv_write_lock.set( + PreimageKey::new_keccak256(*hash).into(), + cfg.agreed_l2_pre_state.clone().into(), + )?; + } + HintType::L2OutputRoot => { + ensure!(hint.data.len() >= 32 && hint.data.len() <= 40, "Invalid hint data length"); + + let hash = B256::from_slice(&hint.data.as_ref()[0..32]); + let chain_id = u64::from_be_bytes(hint.data.as_ref()[32..40].try_into()?); + let l2_provider = providers.l2(&chain_id)?; + + // Decode the pre-state to determine the timestamp of the block. + let pre = PreState::decode(&mut cfg.agreed_l2_pre_state.as_ref())?; + let timestamp = match pre { + PreState::SuperRoot(super_root) => super_root.timestamp, + PreState::TransitionState(transition_state) => { + transition_state.pre_state.timestamp + } + }; + + // Convert the timestamp to an L2 block number, using the rollup config for the + // chain ID embedded within the hint. + let rollup_config = cfg + .read_rollup_configs() + // If an error occurred while reading the rollup configs, return the error. + .transpose()? + // Try to find the appropriate rollup config for the chain ID. + .and_then(|configs| configs.get(&chain_id).cloned()) + // If we can't find the rollup config, try to find it in the global rollup + // configs. + .or_else(|| ROLLUP_CONFIGS.get(&chain_id).cloned()) + .map(Arc::new) + .ok_or(anyhow!("No rollup config found for chain ID: {chain_id}"))?; + let block_number = rollup_config.block_number_from_timestamp(timestamp); + + // Fetch the header for the L2 head block. + let raw_header: Bytes = l2_provider + .client() + .request("debug_getRawHeader", &[format!("0x{block_number:x}")]) + .await + .map_err(|e| anyhow!("Failed to fetch header RLP: {e}"))?; + let header = Header::decode(&mut raw_header.as_ref())?; + + // Fetch the storage root for the L2 head block. + let l2_to_l1_message_passer = l2_provider + .get_proof(Predeploys::L2_TO_L1_MESSAGE_PASSER, Default::default()) + .block_id(block_number.into()) + .await?; + + let output_root = OutputRoot::from_parts( + header.state_root, + l2_to_l1_message_passer.storage_hash, + header.hash_slow(), + ); + let output_root_hash = output_root.hash(); + + ensure!( + output_root_hash == hash, + "Output root does not match L2 head. Expected: {hash}, got: {output_root_hash}" + ); + + let mut kv_lock = kv.write().await; + kv_lock.set( + PreimageKey::new_keccak256(*output_root_hash).into(), + output_root.encode().into(), + )?; + } + HintType::L2BlockHeader => { + ensure!(hint.data.len() == 40, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref()[..32].try_into()?; + let chain_id = u64::from_be_bytes(hint.data[32..40].try_into()?); + + let raw_header: Bytes = + providers.l2(&chain_id)?.client().request("debug_getRawHeader", [hash]).await?; + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), raw_header.into())?; + } + HintType::L2Transactions => { + ensure!(hint.data.len() == 40, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref()[..32].try_into()?; + let chain_id = u64::from_be_bytes(hint.data[32..40].try_into()?); + + let Block { transactions, .. } = providers + .l2(&chain_id)? + .get_block_by_hash(hash) + .full() + .await? + .ok_or(anyhow!("Block not found"))?; + let encoded_transactions = transactions + .into_transactions() + .map(|tx| tx.inner.inner.encoded_2718()) + .collect::>(); + + store_ordered_trie(kv.as_ref(), encoded_transactions.as_slice()).await?; + } + HintType::L2Receipts => { + ensure!(hint.data.len() == 40, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref()[..32].try_into()?; + let chain_id = u64::from_be_bytes(hint.data[32..40].try_into()?); + + let raw_receipts: Vec = providers + .l2(&chain_id)? + .client() + .request("debug_getRawReceipts", [hash]) + .await?; + + store_ordered_trie(kv.as_ref(), raw_receipts.as_slice()).await?; + } + HintType::L2Code => { + // geth hashdb scheme code hash key prefix + const CODE_PREFIX: u8 = b'c'; + + ensure!(hint.data.len() == 40, "Invalid hint data length"); + + let hash: B256 = B256::from_slice(&hint.data[0..32]); + let chain_id = u64::from_be_bytes(hint.data[32..40].try_into()?); + let l2_provider = providers.l2(&chain_id)?; + + // Attempt to fetch the code from the L2 chain provider. + let code_key = [&[CODE_PREFIX], hash.as_slice()].concat(); + let code = l2_provider + .client() + .request::<&[Bytes; 1], Bytes>("debug_dbGet", &[code_key.into()]) + .await; + + // Check if the first attempt to fetch the code failed. If it did, try fetching the + // code hash preimage without the geth hashdb scheme prefix. + let code = match code { + Ok(code) => code, + Err(_) => l2_provider + .client() + .request::<&[B256; 1], Bytes>("debug_dbGet", &[hash]) + .await + .map_err(|e| anyhow!("Error fetching code hash preimage: {e}"))?, + }; + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), code.into())?; + } + HintType::L2StateNode => { + ensure!(hint.data.len() == 40, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let chain_id = u64::from_be_bytes(hint.data[32..40].try_into()?); + + // Fetch the preimage from the L2 chain provider. + let preimage: Bytes = + providers.l2(&chain_id)?.client().request("debug_dbGet", &[hash]).await?; + + let mut kv_write_lock = kv.write().await; + kv_write_lock.set(PreimageKey::new_keccak256(*hash).into(), preimage.into())?; + } + HintType::L2AccountProof => { + ensure!(hint.data.len() == 8 + 20 + 8, "Invalid hint data length"); + + let block_number = u64::from_be_bytes(hint.data.as_ref()[..8].try_into()?); + let address = Address::from_slice(&hint.data.as_ref()[8..28]); + let chain_id = u64::from_be_bytes(hint.data[28..].try_into()?); + + let proof_response = providers + .l2(&chain_id)? + .get_proof(address, Default::default()) + .block_id(block_number.into()) + .await?; + + // Write the account proof nodes to the key-value store. + let mut kv_lock = kv.write().await; + proof_response.account_proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + } + HintType::L2AccountStorageProof => { + ensure!(hint.data.len() == 8 + 20 + 32 + 8, "Invalid hint data length"); + + let block_number = u64::from_be_bytes(hint.data.as_ref()[..8].try_into()?); + let address = Address::from_slice(&hint.data.as_ref()[8..28]); + let slot = B256::from_slice(&hint.data.as_ref()[28..60]); + let chain_id = u64::from_be_bytes(hint.data[60..].try_into()?); + + let mut proof_response = providers + .l2(&chain_id)? + .get_proof(address, vec![slot]) + .block_id(block_number.into()) + .await?; + + let mut kv_lock = kv.write().await; + + // Write the account proof nodes to the key-value store. + proof_response.account_proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + + // Write the storage proof nodes to the key-value store. + let storage_proof = proof_response.storage_proof.remove(0); + storage_proof.proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + } + HintType::L2BlockData => { + ensure!(hint.data.len() == 72, "Invalid hint data length"); + + let agreed_block_hash = B256::from_slice(&hint.data.as_ref()[..32]); + let disputed_block_hash = B256::from_slice(&hint.data.as_ref()[32..64]); + let chain_id = u64::from_be_bytes(hint.data.as_ref()[64..72].try_into()?); + + // Return early if the agreed and disputed block are the same. This can occur when + // the chain has not progressed past its prestate, but the super root timestamp has + // progressed. + if agreed_block_hash == disputed_block_hash { + debug!( + target: "interop_hint_handler", + chain_id, + "Chain has not progressed. Skipping block data hint." + ); + return Ok(()); + } + + let l2_provider = providers.l2(&chain_id)?; + let rollup_config = cfg + .read_rollup_configs() + // If an error occurred while reading the rollup configs, return the error. + .transpose()? + // Try to find the appropriate rollup config for the chain ID. + .and_then(|configs| configs.get(&chain_id).cloned()) + // If we can't find the rollup config, try to find it in the global rollup + // configs. + .or_else(|| ROLLUP_CONFIGS.get(&chain_id).cloned()) + .map(Arc::new) + .ok_or(anyhow!("No rollup config found for chain ID: {chain_id}"))?; + + let l1_config = cfg + .read_l1_configs() + // If an error occurred while reading the l1 configs, return the error. + .transpose()? + // Try to find the appropriate l1 config for the chain ID. + .and_then(|configs| configs.get(&rollup_config.l1_chain_id).cloned()) + // If we can't find the l1 config, try to find it in the global l1 configs. + .or_else(|| L1_CONFIGS.get(&rollup_config.l1_chain_id).cloned()) + .map(Arc::new) + .ok_or(anyhow!( + "No l1 config found for chain ID: {}", + rollup_config.l1_chain_id + ))?; + + // Check if the block is canonical before continuing. + let parent_block = l2_provider + .get_block_by_hash(agreed_block_hash) + .await? + .ok_or(anyhow!("Block not found."))?; + let disputed_block = l2_provider + .get_block_by_number((parent_block.header.number + 1).into()) + .await? + .ok_or(anyhow!("Block not found."))?; + + // Return early if the disputed block is canonical - preimages can be fetched + // through the normal flow. + if disputed_block.header.hash == disputed_block_hash { + debug!( + target: "interop_hint_handler", + number = disputed_block.header.number, + hash = ?disputed_block.header.hash, + "Block is already canonical. Skipping re-derivation + execution." + ); + return Ok(()); + } + + info!( + target: "interop_hint_handler", + optimistic_hash = ?disputed_block_hash, + "Re-executing optimistic block for witness collection" + ); + + // Reproduce the preimages for the optimistic block's derivation + execution and + // store them in the key-value store. + let hint = BidirectionalChannel::new()?; + let preimage = BidirectionalChannel::new()?; + let backend = + OnlineHostBackend::new(cfg.clone(), kv.clone(), providers.clone(), Self); + let server_task = task::spawn( + PreimageServer::new( + OracleServer::new(preimage.host), + HintReader::new(hint.host), + Arc::new(backend), + ) + .start(), + ); + let client_task = task::spawn({ + let l1_head = cfg.l1_head; + + async move { + let oracle = Arc::new(CachingOracle::new( + 1024, + OracleReader::new(preimage.client), + HintWriter::new(hint.client), + )); + + let mut l1_provider = OracleL1ChainProvider::new(l1_head, oracle.clone()); + let mut l2_provider = OracleL2ChainProvider::new( + agreed_block_hash, + rollup_config.clone(), + oracle.clone(), + ); + let beacon = OracleBlobProvider::new(oracle.clone()); + + l2_provider.set_chain_id(Some(chain_id)); + + let safe_head = l2_provider + .header_by_hash(agreed_block_hash) + .map(|header| Sealed::new_unchecked(header, agreed_block_hash))?; + let target_block = safe_head.number + 1; + + let cursor = new_oracle_pipeline_cursor( + rollup_config.as_ref(), + safe_head, + &mut l1_provider, + &mut l2_provider, + ) + .await?; + l2_provider.set_cursor(cursor.clone()); + + let da_provider = EthereumDataSource::new_from_parts( + l1_provider.clone(), + beacon, + &rollup_config, + ); + let pipeline = OraclePipeline::new( + rollup_config.clone(), + l1_config.clone(), + cursor.clone(), + oracle, + da_provider, + l1_provider, + l2_provider.clone(), + ) + .await?; + let executor = KonaExecutor::new( + rollup_config.as_ref(), + l2_provider.clone(), + l2_provider, + OpEvmFactory::default(), + None, + ); + let mut driver = Driver::new(cursor, executor, pipeline); + + driver + .advance_to_target(rollup_config.as_ref(), Some(target_block)) + .await?; + + driver + .safe_head_artifacts + .ok_or_else(|| anyhow!("No artifacts found for the safe head")) + } + .instrument(info_span!( + "OptimisticBlockReexecution", + block_number = disputed_block.header.number + )) + }); + + // Wait on both the server and client tasks to complete. + let (_, client_result) = tokio::try_join!(server_task, client_task)?; + let (build_outcome, raw_transactions) = client_result?; + + // Store optimistic block hash preimage. + let mut kv_lock = kv.write().await; + let mut rlp_buf = Vec::with_capacity(build_outcome.header.length()); + build_outcome.header.encode(&mut rlp_buf); + kv_lock.set( + PreimageKey::new(*build_outcome.header.hash(), PreimageKeyType::Keccak256) + .into(), + rlp_buf, + )?; + + // Drop the lock on the key-value store to avoid deadlocks. + drop(kv_lock); + + // Store receipts root preimages. + let raw_receipts = build_outcome + .execution_result + .receipts + .into_iter() + .map(|receipt| Ok::<_, anyhow::Error>(receipt.encoded_2718())) + .collect::>>()?; + store_ordered_trie(kv.as_ref(), raw_receipts.as_slice()).await?; + + // Store tx root preimages. + store_ordered_trie(kv.as_ref(), raw_transactions.as_slice()).await?; + + info!( + target: "interop_hint_handler", + number = build_outcome.header.number, + hash = ?build_outcome.header.hash(), + "Re-executed optimistic block and collected witness" + ); + } + HintType::L2PayloadWitness => { + warn!( + target: "interop_hint_handler", + "L2PayloadWitness hint not implemented for interop hint handler, ignoring hint" + ); + } + } + + Ok(()) + } +} diff --git a/kona/bin/host/src/interop/local_kv.rs b/kona/bin/host/src/interop/local_kv.rs new file mode 100644 index 0000000000000..a7e548542a21c --- /dev/null +++ b/kona/bin/host/src/interop/local_kv.rs @@ -0,0 +1,52 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data on disk, +//! using the [InteropHost] config. + +use super::InteropHost; +use crate::KeyValueStore; +use alloy_primitives::{B256, keccak256}; +use anyhow::Result; +use kona_preimage::PreimageKey; +use kona_proof_interop::boot::{ + L1_CONFIG_KEY, L1_HEAD_KEY, L2_AGREED_PRE_STATE_KEY, L2_CLAIMED_POST_STATE_KEY, + L2_CLAIMED_TIMESTAMP_KEY, L2_ROLLUP_CONFIG_KEY, +}; + +/// A simple, synchronous key-value store that returns data from a [InteropHost] config. +#[derive(Debug)] +pub struct InteropLocalInputs { + cfg: InteropHost, +} + +impl InteropLocalInputs { + /// Create a new [InteropLocalInputs] with the given [InteropHost] config. + pub const fn new(cfg: InteropHost) -> Self { + Self { cfg } + } +} + +impl KeyValueStore for InteropLocalInputs { + fn get(&self, key: B256) -> Option> { + let preimage_key = PreimageKey::try_from(*key).ok()?; + match preimage_key.key_value() { + L1_HEAD_KEY => Some(self.cfg.l1_head.to_vec()), + L2_AGREED_PRE_STATE_KEY => { + Some(keccak256(self.cfg.agreed_l2_pre_state.as_ref()).to_vec()) + } + L2_CLAIMED_POST_STATE_KEY => Some(self.cfg.claimed_l2_post_state.to_vec()), + L2_CLAIMED_TIMESTAMP_KEY => Some(self.cfg.claimed_l2_timestamp.to_be_bytes().to_vec()), + L2_ROLLUP_CONFIG_KEY => { + let rollup_configs = self.cfg.read_rollup_configs()?.ok()?; + serde_json::to_vec(&rollup_configs).ok() + } + L1_CONFIG_KEY => { + let l1_configs = self.cfg.read_l1_configs()?.ok()?; + serde_json::to_vec(&l1_configs).ok() + } + _ => None, + } + } + + fn set(&mut self, _: B256, _: Vec) -> Result<()> { + unreachable!("LocalKeyValueStore is read-only") + } +} diff --git a/kona/bin/host/src/interop/mod.rs b/kona/bin/host/src/interop/mod.rs new file mode 100644 index 0000000000000..a6757835abeec --- /dev/null +++ b/kona/bin/host/src/interop/mod.rs @@ -0,0 +1,10 @@ +//! This module contains the super-chain (interop) mode for the host. + +mod cfg; +pub use cfg::{InteropHost, InteropHostError, InteropProviders}; + +mod local_kv; +pub use local_kv::InteropLocalInputs; + +mod handler; +pub use handler::InteropHintHandler; diff --git a/kona/bin/host/src/kv/disk.rs b/kona/bin/host/src/kv/disk.rs new file mode 100644 index 0000000000000..455d344defbcf --- /dev/null +++ b/kona/bin/host/src/kv/disk.rs @@ -0,0 +1,100 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data on disk +//! using [rocksdb]. + +use super::{KeyValueStore, MemoryKeyValueStore}; +use alloy_primitives::B256; +use anyhow::{Result, anyhow}; +use rocksdb::{DB, Options}; +use std::path::PathBuf; + +/// A simple, synchronous key-value store that stores data on disk. +#[derive(Debug)] +pub struct DiskKeyValueStore { + data_directory: PathBuf, + db: DB, +} + +impl DiskKeyValueStore { + /// Create a new [DiskKeyValueStore] with the given data directory. + pub fn new(data_directory: PathBuf) -> Self { + let db = DB::open(&Self::get_db_options(), data_directory.as_path()) + .unwrap_or_else(|e| panic!("Failed to open database at {data_directory:?}: {e}")); + + Self { data_directory, db } + } + + /// Gets the [Options] for the underlying RocksDB instance. + fn get_db_options() -> Options { + let mut options = Options::default(); + options.set_compression_type(rocksdb::DBCompressionType::Snappy); + options.create_if_missing(true); + options + } +} + +impl KeyValueStore for DiskKeyValueStore { + fn get(&self, key: alloy_primitives::B256) -> Option> { + self.db.get(*key).ok()? + } + + fn set(&mut self, key: alloy_primitives::B256, value: Vec) -> Result<()> { + self.db.put(*key, value).map_err(|e| anyhow!("Failed to set key-value pair: {e}")) + } +} + +impl Drop for DiskKeyValueStore { + fn drop(&mut self) { + let _ = DB::destroy(&Self::get_db_options(), self.data_directory.as_path()); + } +} + +impl TryFrom for MemoryKeyValueStore { + type Error = anyhow::Error; + + fn try_from(disk_store: DiskKeyValueStore) -> Result { + let mut memory_store = Self::new(); + let mut db_iter = disk_store.db.full_iterator(rocksdb::IteratorMode::Start); + + while let Some(Ok((key, value))) = db_iter.next() { + memory_store.set( + B256::try_from(key.as_ref()) + .map_err(|e| anyhow!("Failed to convert slice to B256: {e}"))?, + value.to_vec(), + )?; + } + + Ok(memory_store) + } +} + +#[cfg(test)] +mod test { + use super::DiskKeyValueStore; + use crate::kv::{KeyValueStore, MemoryKeyValueStore}; + use proptest::{ + arbitrary::any, + collection::{hash_map, vec}, + proptest, + test_runner::Config, + }; + use std::env::temp_dir; + + proptest! { + #![proptest_config(Config::with_cases(16))] + + /// Test that converting from a [DiskKeyValueStore] to a [MemoryKeyValueStore] is lossless. + #[test] + fn convert_disk_kv_to_mem_kv(k_v in hash_map(any::<[u8; 32]>(), vec(any::(), 0..128), 1..128)) { + let tempdir = temp_dir(); + let mut disk_kv = DiskKeyValueStore::new(tempdir); + k_v.iter().for_each(|(k, v)| { + disk_kv.set(k.into(), v.to_vec()).unwrap(); + }); + + let mem_kv = MemoryKeyValueStore::try_from(disk_kv).unwrap(); + for (k, v) in k_v { + assert_eq!(mem_kv.get(k.into()).unwrap(), v.to_vec()); + } + } + } +} diff --git a/kona/bin/host/src/kv/mem.rs b/kona/bin/host/src/kv/mem.rs new file mode 100644 index 0000000000000..7b26052e49dc5 --- /dev/null +++ b/kona/bin/host/src/kv/mem.rs @@ -0,0 +1,32 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data in memory. + +use super::KeyValueStore; +use alloy_primitives::B256; +use anyhow::Result; +use std::collections::HashMap; + +/// A simple, synchronous key-value store that stores data in memory. This is useful for testing and +/// development purposes. +#[derive(Default, Clone, Debug, Eq, PartialEq)] +pub struct MemoryKeyValueStore { + /// The underlying store. + pub store: HashMap>, +} + +impl MemoryKeyValueStore { + /// Create a new [MemoryKeyValueStore] with an empty store. + pub fn new() -> Self { + Self { store: HashMap::default() } + } +} + +impl KeyValueStore for MemoryKeyValueStore { + fn get(&self, key: B256) -> Option> { + self.store.get(&key).cloned() + } + + fn set(&mut self, key: B256, value: Vec) -> Result<()> { + self.store.insert(key, value); + Ok(()) + } +} diff --git a/kona/bin/host/src/kv/mod.rs b/kona/bin/host/src/kv/mod.rs new file mode 100644 index 0000000000000..65f41bce355d6 --- /dev/null +++ b/kona/bin/host/src/kv/mod.rs @@ -0,0 +1,27 @@ +//! This module contains the [KeyValueStore] trait and concrete implementations of it. + +use alloy_primitives::B256; +use anyhow::Result; +use std::sync::Arc; +use tokio::sync::RwLock; + +mod mem; +pub use mem::MemoryKeyValueStore; + +mod disk; +pub use disk::DiskKeyValueStore; + +mod split; +pub use split::SplitKeyValueStore; + +/// A type alias for a shared key-value store. +pub type SharedKeyValueStore = Arc>; + +/// Describes the interface of a simple, synchronous key-value store. +pub trait KeyValueStore { + /// Get the value associated with the given key. + fn get(&self, key: B256) -> Option>; + + /// Set the value associated with the given key. + fn set(&mut self, key: B256, value: Vec) -> Result<()>; +} diff --git a/kona/bin/host/src/kv/split.rs b/kona/bin/host/src/kv/split.rs new file mode 100644 index 0000000000000..325c3af213545 --- /dev/null +++ b/kona/bin/host/src/kv/split.rs @@ -0,0 +1,47 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that splits between two separate +//! [KeyValueStore]s depending on [PreimageKeyType]. + +use super::KeyValueStore; +use alloy_primitives::B256; +use anyhow::Result; +use kona_preimage::PreimageKeyType; + +/// A split implementation of the [KeyValueStore] trait that splits between two separate +/// [KeyValueStore]s. +#[derive(Clone, Debug)] +pub struct SplitKeyValueStore +where + L: KeyValueStore, + R: KeyValueStore, +{ + local_store: L, + remote_store: R, +} + +impl SplitKeyValueStore +where + L: KeyValueStore, + R: KeyValueStore, +{ + /// Create a new [SplitKeyValueStore] with the given left and right [KeyValueStore]s. + pub const fn new(local_store: L, remote_store: R) -> Self { + Self { local_store, remote_store } + } +} + +impl KeyValueStore for SplitKeyValueStore +where + L: KeyValueStore, + R: KeyValueStore, +{ + fn get(&self, key: B256) -> Option> { + match PreimageKeyType::try_from(key[0]).ok()? { + PreimageKeyType::Local => self.local_store.get(key), + _ => self.remote_store.get(key), + } + } + + fn set(&mut self, key: B256, value: Vec) -> Result<()> { + self.remote_store.set(key, value) + } +} diff --git a/kona/bin/host/src/lib.rs b/kona/bin/host/src/lib.rs new file mode 100644 index 0000000000000..a4d5f9257f773 --- /dev/null +++ b/kona/bin/host/src/lib.rs @@ -0,0 +1,21 @@ +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod server; +pub use server::{PreimageServer, PreimageServerError}; + +mod kv; +pub use kv::{ + DiskKeyValueStore, KeyValueStore, MemoryKeyValueStore, SharedKeyValueStore, SplitKeyValueStore, +}; + +mod backend; +pub use backend::{HintHandler, OfflineHostBackend, OnlineHostBackend, OnlineHostBackendCfg}; + +pub mod eth; + +#[cfg(feature = "single")] +pub mod single; + +#[cfg(feature = "interop")] +pub mod interop; diff --git a/kona/bin/host/src/server.rs b/kona/bin/host/src/server.rs new file mode 100644 index 0000000000000..6d0476b8f1dd8 --- /dev/null +++ b/kona/bin/host/src/server.rs @@ -0,0 +1,99 @@ +//! This module contains the [PreimageServer] struct and its implementation. + +use kona_preimage::{ + HintReaderServer, PreimageOracleServer, PreimageServerBackend, errors::PreimageOracleError, +}; +use std::sync::Arc; +use tokio::spawn; +use tracing::{error, info}; + +/// The [PreimageServer] is responsible for waiting for incoming preimage requests and +/// serving them to the client. +#[derive(Debug)] +pub struct PreimageServer { + /// The oracle server. + oracle_server: P, + /// The hint router. + hint_reader: H, + /// [PreimageServerBackend] that routes hints and retrieves preimages. + backend: Arc, +} + +/// An error that can occur when handling preimage requests +#[derive(Debug, thiserror::Error)] +pub enum PreimageServerError { + /// A preimage request error. + #[error("Failed to serve preimage request: {0}")] + PreimageRequestFailed(PreimageOracleError), + /// An error when failed to serve route hint. + #[error("Failed to route hint: {0}")] + RouteHintFailed(PreimageOracleError), + /// Task failed to execute to completion. + #[error("Join error: {0}")] + ExecutionError(#[from] tokio::task::JoinError), +} + +impl PreimageServer +where + P: PreimageOracleServer + Send + Sync + 'static, + H: HintReaderServer + Send + Sync + 'static, + B: PreimageServerBackend + Send + Sync + 'static, +{ + /// Create a new [PreimageServer] with the given [PreimageOracleServer], + /// [HintReaderServer], and [PreimageServerBackend]. + pub const fn new(oracle_server: P, hint_reader: H, backend: Arc) -> Self { + Self { oracle_server, hint_reader, backend } + } + + /// Starts the [PreimageServer] and waits for incoming requests. + pub async fn start(self) -> Result<(), PreimageServerError> { + // Create the futures for the oracle server and hint router. + let server = spawn(Self::start_oracle_server(self.oracle_server, self.backend.clone())); + let hint_router = spawn(Self::start_hint_router(self.hint_reader, self.backend.clone())); + + // Race the two futures to completion, returning the result of the first one to finish. + tokio::select! { + s = server => s?, + h = hint_router => h?, + } + } + + /// Starts the oracle server, which waits for incoming preimage requests and serves them to the + /// client. + async fn start_oracle_server( + oracle_server: P, + backend: Arc, + ) -> Result<(), PreimageServerError> { + info!(target: "host_server", "Starting oracle server"); + loop { + // Serve the next preimage request. This `await` will yield to the runtime + // if no progress can be made. + match oracle_server.next_preimage_request(backend.as_ref()).await { + Ok(_) => continue, + Err(PreimageOracleError::IOError(_)) => return Ok(()), + Err(e) => { + error!(target: "host_server", "Failed to serve preimage request: {e}"); + return Err(PreimageServerError::PreimageRequestFailed(e)); + } + } + } + } + + /// Starts the hint router, which waits for incoming hints and routes them to the appropriate + /// handler. + async fn start_hint_router(hint_reader: H, backend: Arc) -> Result<(), PreimageServerError> { + info!(target: "host_server", "Starting hint router"); + loop { + // Route the next hint. This `await` will yield to the runtime if no progress can be + // made. + match hint_reader.next_hint(backend.as_ref()).await { + Ok(_) => continue, + Err(PreimageOracleError::IOError(_)) => return Ok(()), + Err(e) => { + error!(target: "host_server", "Failed to serve route hint: {e}"); + return Err(PreimageServerError::RouteHintFailed(e)); + } + } + } + } +} diff --git a/kona/bin/host/src/single/cfg.rs b/kona/bin/host/src/single/cfg.rs new file mode 100644 index 0000000000000..73f4e53d4fd49 --- /dev/null +++ b/kona/bin/host/src/single/cfg.rs @@ -0,0 +1,389 @@ +//! This module contains all CLI-specific code for the single chain entrypoint. + +use super::{SingleChainHintHandler, SingleChainLocalInputs}; +use crate::{ + DiskKeyValueStore, MemoryKeyValueStore, OfflineHostBackend, OnlineHostBackend, + OnlineHostBackendCfg, PreimageServer, SharedKeyValueStore, SplitKeyValueStore, + eth::rpc_provider, server::PreimageServerError, +}; +use alloy_primitives::B256; +use alloy_provider::RootProvider; +use clap::Parser; +use kona_cli::cli_styles; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_preimage::{ + BidirectionalChannel, Channel, HintReader, HintWriter, OracleReader, OracleServer, +}; +use kona_proof::HintType; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; +use kona_std_fpvm::{FileChannel, FileDescriptor}; +use op_alloy_network::Optimism; +use serde::Serialize; +use std::{path::PathBuf, sync::Arc}; +use tokio::{ + sync::RwLock, + task::{self, JoinHandle}, +}; + +/// The host binary CLI application arguments. +#[derive(Default, Parser, Serialize, Clone, Debug)] +#[command(styles = cli_styles())] +pub struct SingleChainHost { + /// Hash of the L1 head block. Derivation stops after this block is processed. + #[arg(long, env)] + pub l1_head: B256, + /// Hash of the agreed upon safe L2 block committed to by `--agreed-l2-output-root`. + #[arg(long, visible_alias = "l2-head", env)] + pub agreed_l2_head_hash: B256, + /// Agreed safe L2 Output Root to start derivation from. + #[arg(long, visible_alias = "l2-output-root", env)] + pub agreed_l2_output_root: B256, + /// Claimed L2 output root at block # `--claimed-l2-block-number` to validate. + #[arg(long, visible_alias = "l2-claim", env)] + pub claimed_l2_output_root: B256, + /// Number of the L2 block that the claimed output root commits to. + #[arg(long, visible_alias = "l2-block-number", env)] + pub claimed_l2_block_number: u64, + /// Address of L2 JSON-RPC endpoint to use (eth and debug namespace required). + #[arg( + long, + visible_alias = "l2", + requires = "l1_node_address", + requires = "l1_beacon_address", + env + )] + pub l2_node_address: Option, + /// Address of L1 JSON-RPC endpoint to use (eth and debug namespace required) + #[arg( + long, + visible_alias = "l1", + requires = "l2_node_address", + requires = "l1_beacon_address", + env + )] + pub l1_node_address: Option, + /// Address of the L1 Beacon API endpoint to use. + #[arg( + long, + visible_alias = "beacon", + requires = "l1_node_address", + requires = "l2_node_address", + env + )] + pub l1_beacon_address: Option, + /// The Data Directory for preimage data storage. Optional if running in online mode, + /// required if running in offline mode. + #[arg( + long, + visible_alias = "db", + required_unless_present_all = ["l2_node_address", "l1_node_address", "l1_beacon_address"], + env + )] + pub data_dir: Option, + /// Run the client program natively. + #[arg(long, conflicts_with = "server", required_unless_present = "server")] + pub native: bool, + /// Run in pre-image server mode without executing any client program. If not provided, the + /// host will run the client program in the host process. + #[arg(long, conflicts_with = "native", required_unless_present = "native")] + pub server: bool, + /// The L2 chain ID of a supported chain. If provided, the host will look for the corresponding + /// rollup config in the superchain registry. + #[arg( + long, + conflicts_with = "rollup_config_path", + required_unless_present = "rollup_config_path", + env + )] + pub l2_chain_id: Option, + /// Path to rollup config. If provided, the host will use this config instead of attempting to + /// look up the config in the superchain registry. + #[arg( + long, + alias = "rollup-cfg", + conflicts_with = "l2_chain_id", + required_unless_present = "l2_chain_id", + env + )] + pub rollup_config_path: Option, + /// Path to l1 config. If provided, the host will use this config instead of attempting to + /// look up the config in the known l1 configs. + #[arg(long, alias = "l1-cfg", env)] + pub l1_config_path: Option, + /// Optionally enables the use of `debug_executePayload` to collect the execution witness from + /// the execution layer. + #[arg(long, env)] + pub enable_experimental_witness_endpoint: bool, +} + +/// An error that can occur when handling single chain hosts +#[derive(Debug, thiserror::Error)] +pub enum SingleChainHostError { + /// An error when handling preimage requests. + #[error("Error handling preimage request: {0}")] + PreimageServerError(#[from] PreimageServerError), + /// An IO error. + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), + /// A JSON parse error. + #[error("Failed deserializing RollupConfig: {0}")] + ParseError(#[from] serde_json::Error), + /// Task failed to execute to completion. + #[error("Join error: {0}")] + ExecutionError(#[from] tokio::task::JoinError), + /// No rollup config found. + #[error("No rollup config found")] + NoRollupConfig, + /// No l1 config found. + #[error("No l1 config found")] + NoL1Config, + /// Any other error. + #[error("Error: {0}")] + Other(&'static str), +} + +impl SingleChainHost { + /// Starts the [SingleChainHost] application. + pub async fn start(self) -> Result<(), SingleChainHostError> { + if self.server { + let hint = FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite); + let preimage = + FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite); + + self.start_server(hint, preimage).await?.await? + } else { + self.start_native().await + } + } + + /// Starts the preimage server, communicating with the client over the provided channels. + pub async fn start_server( + &self, + hint: C, + preimage: C, + ) -> Result>, SingleChainHostError> + where + C: Channel + Send + Sync + 'static, + { + let kv_store = self.create_key_value_store()?; + + let task_handle = if self.is_offline() { + task::spawn(async { + PreimageServer::new( + OracleServer::new(preimage), + HintReader::new(hint), + Arc::new(OfflineHostBackend::new(kv_store)), + ) + .start() + .await + .map_err(SingleChainHostError::from) + }) + } else { + let providers = self.create_providers().await?; + let backend = OnlineHostBackend::new( + self.clone(), + kv_store.clone(), + providers, + SingleChainHintHandler, + ) + .with_proactive_hint(HintType::L2PayloadWitness); + + task::spawn(async { + PreimageServer::new( + OracleServer::new(preimage), + HintReader::new(hint), + Arc::new(backend), + ) + .start() + .await + .map_err(SingleChainHostError::from) + }) + }; + + Ok(task_handle) + } + + /// Starts the host in native mode, running both the client and preimage server in the same + /// process. + async fn start_native(&self) -> Result<(), SingleChainHostError> { + let hint = BidirectionalChannel::new()?; + let preimage = BidirectionalChannel::new()?; + + let server_task = self.start_server(hint.host, preimage.host).await?; + let client_task = task::spawn(kona_client::single::run( + OracleReader::new(preimage.client), + HintWriter::new(hint.client), + )); + + let (_, client_result) = tokio::try_join!(server_task, client_task)?; + + // Bubble up the exit status of the client program if execution completes. + std::process::exit(client_result.is_err() as i32) + } + + /// Returns `true` if the host is running in offline mode. + pub const fn is_offline(&self) -> bool { + self.l1_node_address.is_none() && + self.l2_node_address.is_none() && + self.l1_beacon_address.is_none() && + self.data_dir.is_some() + } + + /// Reads the [RollupConfig] from the file system and returns the deserialized configuration. + pub fn read_rollup_config(&self) -> Result { + let path = + self.rollup_config_path.as_ref().ok_or_else(|| SingleChainHostError::NoRollupConfig)?; + + // Read the serialized config from the file system. + let ser_config = std::fs::read_to_string(path)?; + + // Deserialize the config and return it. + serde_json::from_str(&ser_config).map_err(SingleChainHostError::ParseError) + } + + /// Reads the [L1ChainConfig] from the file system and returns the deserialized configuration. + pub fn read_l1_config(&self) -> Result { + let path = self.l1_config_path.as_ref().ok_or_else(|| SingleChainHostError::NoL1Config)?; + + // Read the serialized config from the file system. + let ser_config = std::fs::read_to_string(path)?; + + // Deserialize the config and return it. + serde_json::from_str(&ser_config).map_err(SingleChainHostError::ParseError) + } + + /// Creates the key-value store for the host backend. + pub fn create_key_value_store(&self) -> Result { + let local_kv_store = SingleChainLocalInputs::new(self.clone()); + + let kv_store: SharedKeyValueStore = if let Some(ref data_dir) = self.data_dir { + let disk_kv_store = DiskKeyValueStore::new(data_dir.clone()); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, disk_kv_store); + Arc::new(RwLock::new(split_kv_store)) + } else { + let mem_kv_store = MemoryKeyValueStore::new(); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, mem_kv_store); + Arc::new(RwLock::new(split_kv_store)) + }; + + Ok(kv_store) + } + + /// Creates the providers required for the host backend. + pub async fn create_providers(&self) -> Result { + let l1_provider = rpc_provider( + self.l1_node_address + .as_ref() + .ok_or(SingleChainHostError::Other("Provider must be set"))?, + ) + .await; + let blob_provider = OnlineBlobProvider::init(OnlineBeaconClient::new_http( + self.l1_beacon_address + .clone() + .ok_or(SingleChainHostError::Other("Beacon API URL must be set"))?, + )) + .await; + let l2_provider = rpc_provider::( + self.l2_node_address + .as_ref() + .ok_or(SingleChainHostError::Other("L2 node address must be set"))?, + ) + .await; + + Ok(SingleChainProviders { l1: l1_provider, blobs: blob_provider, l2: l2_provider }) + } +} + +impl OnlineHostBackendCfg for SingleChainHost { + type HintType = HintType; + type Providers = SingleChainProviders; +} + +/// The providers required for the single chain host. +#[derive(Debug, Clone)] +pub struct SingleChainProviders { + /// The L1 EL provider. + pub l1: RootProvider, + /// The L1 beacon node provider. + pub blobs: OnlineBlobProvider, + /// The L2 EL provider. + pub l2: RootProvider, +} + +#[cfg(test)] +mod test { + use crate::single::SingleChainHost; + use alloy_primitives::B256; + use clap::Parser; + + #[test] + fn test_flags() { + let zero_hash_str = &B256::ZERO.to_string(); + let default_flags = [ + "single", + "--l1-head", + zero_hash_str, + "--l2-head", + zero_hash_str, + "--l2-output-root", + zero_hash_str, + "--l2-claim", + zero_hash_str, + "--l2-block-number", + "0", + ]; + + let cases = [ + // valid + (["--server", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), + (["--server", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), + (["--native", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), + (["--native", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), + ( + [ + "--l1-node-address", + "dummy", + "--l2-node-address", + "dummy", + "--l1-beacon-address", + "dummy", + "--server", + "--l2-chain-id", + "0", + ] + .as_slice(), + true, + ), + ( + [ + "--server", + "--l2-chain-id", + "0", + "--data-dir", + "dummy", + "--enable-experimental-witness-endpoint", + ] + .as_slice(), + true, + ), + // invalid + (["--server", "--native", "--l2-chain-id", "0"].as_slice(), false), + (["--l2-chain-id", "0", "--rollup-config-path", "dummy", "--server"].as_slice(), false), + (["--server"].as_slice(), false), + (["--native"].as_slice(), false), + (["--rollup-config-path", "dummy"].as_slice(), false), + (["--l2-chain-id", "0"].as_slice(), false), + (["--l1-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + (["--l2-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + (["--l1-beacon-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + ([].as_slice(), false), + ]; + + for (args_ext, valid) in cases.into_iter() { + let args = default_flags.iter().chain(args_ext.iter()).cloned().collect::>(); + + let parsed = SingleChainHost::try_parse_from(args); + assert_eq!(parsed.is_ok(), valid); + } + } +} diff --git a/kona/bin/host/src/single/handler.rs b/kona/bin/host/src/single/handler.rs new file mode 100644 index 0000000000000..fb2eae3fb12a6 --- /dev/null +++ b/kona/bin/host/src/single/handler.rs @@ -0,0 +1,384 @@ +//! [HintHandler] for the [SingleChainHost]. + +use crate::{ + HintHandler, OnlineHostBackendCfg, backend::util::store_ordered_trie, kv::SharedKeyValueStore, + single::cfg::SingleChainHost, +}; +use alloy_consensus::Header; +use alloy_eips::{ + eip2718::Encodable2718, + eip4844::{BlobTransactionSidecarItem, FIELD_ELEMENTS_PER_BLOB, IndexedBlobHash}, +}; +use alloy_primitives::{Address, B256, Bytes, keccak256}; +use alloy_provider::Provider; +use alloy_rlp::Decodable; +use alloy_rpc_types::{Block, debug::ExecutionWitness}; +use anyhow::{Result, anyhow, ensure}; +use ark_ff::{BigInteger, PrimeField}; +use async_trait::async_trait; +use kona_preimage::{PreimageKey, PreimageKeyType}; +use kona_proof::{Hint, HintType, l1::ROOTS_OF_UNITY}; +use kona_protocol::{BlockInfo, OutputRoot, Predeploys}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use tracing::warn; + +/// The [HintHandler] for the [SingleChainHost]. +#[derive(Debug, Clone, Copy)] +pub struct SingleChainHintHandler; + +#[async_trait] +impl HintHandler for SingleChainHintHandler { + type Cfg = SingleChainHost; + + async fn fetch_hint( + hint: Hint<::HintType>, + cfg: &Self::Cfg, + providers: &::Providers, + kv: SharedKeyValueStore, + ) -> Result<()> { + match hint.ty { + HintType::L1BlockHeader => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let raw_header: Bytes = + providers.l1.client().request("debug_getRawHeader", [hash]).await?; + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), raw_header.into())?; + } + HintType::L1Transactions => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let Block { transactions, .. } = providers + .l1 + .get_block_by_hash(hash) + .full() + .await? + .ok_or(anyhow!("Block not found"))?; + let encoded_transactions = transactions + .into_transactions() + .map(|tx| tx.inner.encoded_2718()) + .collect::>(); + + store_ordered_trie(kv.as_ref(), encoded_transactions.as_slice()).await?; + } + HintType::L1Receipts => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let raw_receipts: Vec = + providers.l1.client().request("debug_getRawReceipts", [hash]).await?; + + store_ordered_trie(kv.as_ref(), raw_receipts.as_slice()).await?; + } + HintType::L1Blob => { + ensure!(hint.data.len() == 48, "Invalid hint data length"); + + let hash_data_bytes: [u8; 32] = hint.data[0..32].try_into()?; + let index_data_bytes: [u8; 8] = hint.data[32..40].try_into()?; + let timestamp_data_bytes: [u8; 8] = hint.data[40..48].try_into()?; + + let hash: B256 = hash_data_bytes.into(); + let index = u64::from_be_bytes(index_data_bytes); + let timestamp = u64::from_be_bytes(timestamp_data_bytes); + + let partial_block_ref = BlockInfo { timestamp, ..Default::default() }; + let indexed_hash = IndexedBlobHash { index, hash }; + + // Fetch the blobs from the blob provider. + let mut blobs = providers + .blobs + .fetch_filtered_blob_sidecars(&partial_block_ref, &[indexed_hash]) + .await + .map_err(|e| anyhow!("Failed to fetch blob sidecars: {e}"))?; + if blobs.len() != 1 { + anyhow::bail!("Expected 1 blob, got {}", blobs.len()); + } + let BlobTransactionSidecarItem { + blob, + kzg_proof: proof, + kzg_commitment: commitment, + .. + } = blobs.pop().expect("Expected 1 blob"); + + // Acquire a lock on the key-value store and set the preimages. + let mut kv_lock = kv.write().await; + + // Set the preimage for the blob commitment. + kv_lock.set( + PreimageKey::new(*hash, PreimageKeyType::Sha256).into(), + commitment.to_vec(), + )?; + + // Write all the field elements to the key-value store. There should be 4096. + // The preimage oracle key for each field element is the keccak256 hash of + // `abi.encodePacked(sidecar.KZGCommitment, bytes32(ROOTS_OF_UNITY[i]))`. + let mut blob_key = [0u8; 80]; + blob_key[..48].copy_from_slice(commitment.as_ref()); + for i in 0..FIELD_ELEMENTS_PER_BLOB { + blob_key[48..].copy_from_slice( + ROOTS_OF_UNITY[i as usize].into_bigint().to_bytes_be().as_ref(), + ); + let blob_key_hash = keccak256(blob_key.as_ref()); + + kv_lock + .set(PreimageKey::new_keccak256(*blob_key_hash).into(), blob_key.into())?; + kv_lock.set( + PreimageKey::new(*blob_key_hash, PreimageKeyType::Blob).into(), + blob[(i as usize) << 5..(i as usize + 1) << 5].to_vec(), + )?; + } + + // Write the KZG Proof as the 4096th element. + // Note: This is not associated with a root of unity, as to be backwards compatible + // with ZK users of kona that use this proof for the overall blob. + blob_key[72..].copy_from_slice(FIELD_ELEMENTS_PER_BLOB.to_be_bytes().as_ref()); + let blob_key_hash = keccak256(blob_key.as_ref()); + + kv_lock.set(PreimageKey::new_keccak256(*blob_key_hash).into(), blob_key.into())?; + kv_lock.set( + PreimageKey::new(*blob_key_hash, PreimageKeyType::Blob).into(), + proof.to_vec(), + )?; + } + HintType::L1Precompile => { + ensure!(hint.data.len() >= 28, "Invalid hint data length"); + + let address = Address::from_slice(&hint.data.as_ref()[..20]); + let gas = u64::from_be_bytes(hint.data.as_ref()[20..28].try_into()?); + let input = hint.data[28..].to_vec(); + let input_hash = keccak256(hint.data.as_ref()); + + let result = crate::eth::execute(address, input, gas).map_or_else( + |_| vec![0u8; 1], + |raw_res| { + let mut res = Vec::with_capacity(1 + raw_res.len()); + res.push(0x01); + res.extend_from_slice(&raw_res); + res + }, + ); + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*input_hash).into(), hint.data.into())?; + kv_lock.set( + PreimageKey::new(*input_hash, PreimageKeyType::Precompile).into(), + result, + )?; + } + HintType::L2BlockHeader => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + // Fetch the raw header from the L2 chain provider. + let hash: B256 = hint.data.as_ref().try_into()?; + let raw_header: Bytes = + providers.l2.client().request("debug_getRawHeader", [hash]).await?; + + // Acquire a lock on the key-value store and set the preimage. + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), raw_header.into())?; + } + HintType::L2Transactions => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + let Block { transactions, .. } = providers + .l2 + .get_block_by_hash(hash) + .full() + .await? + .ok_or(anyhow!("Block not found."))?; + + let encoded_transactions = transactions + .into_transactions() + .map(|tx| tx.inner.inner.encoded_2718()) + .collect::>(); + store_ordered_trie(kv.as_ref(), encoded_transactions.as_slice()).await?; + } + HintType::StartingL2Output => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + // Fetch the header for the L2 head block. + let raw_header: Bytes = providers + .l2 + .client() + .request("debug_getRawHeader", &[cfg.agreed_l2_head_hash]) + .await?; + let header = Header::decode(&mut raw_header.as_ref())?; + + // Fetch the storage root for the L2 head block. + let l2_to_l1_message_passer = providers + .l2 + .get_proof(Predeploys::L2_TO_L1_MESSAGE_PASSER, Default::default()) + .block_id(cfg.agreed_l2_head_hash.into()) + .await?; + + let output_root = OutputRoot::from_parts( + header.state_root, + l2_to_l1_message_passer.storage_hash, + cfg.agreed_l2_head_hash, + ); + let output_root_hash = output_root.hash(); + + ensure!( + output_root_hash == cfg.agreed_l2_output_root, + "Output root does not match L2 head." + ); + + let mut kv_write_lock = kv.write().await; + kv_write_lock.set( + PreimageKey::new_keccak256(*output_root_hash).into(), + output_root.encode().into(), + )?; + } + HintType::L2Code => { + // geth hashdb scheme code hash key prefix + const CODE_PREFIX: u8 = b'c'; + + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + + // Attempt to fetch the code from the L2 chain provider. + let code_key = [&[CODE_PREFIX], hash.as_slice()].concat(); + let code = providers + .l2 + .client() + .request::<&[Bytes; 1], Bytes>("debug_dbGet", &[code_key.into()]) + .await; + + // Check if the first attempt to fetch the code failed. If it did, try fetching the + // code hash preimage without the geth hashdb scheme prefix. + let code = match code { + Ok(code) => code, + Err(_) => providers + .l2 + .client() + .request::<&[B256; 1], Bytes>("debug_dbGet", &[hash]) + .await + .map_err(|e| anyhow!("Error fetching code hash preimage: {e}"))?, + }; + + let mut kv_lock = kv.write().await; + kv_lock.set(PreimageKey::new_keccak256(*hash).into(), code.into())?; + } + HintType::L2StateNode => { + ensure!(hint.data.len() == 32, "Invalid hint data length"); + + let hash: B256 = hint.data.as_ref().try_into()?; + + warn!(target: "single_hint_handler", "L2StateNode hint was sent for node hash: {}", hash); + warn!( + target: "single_hint_handler", + "`debug_executePayload` failed to return a complete witness." + ); + + // Fetch the preimage from the L2 chain provider. + let preimage: Bytes = providers.l2.client().request("debug_dbGet", &[hash]).await?; + + let mut kv_write_lock = kv.write().await; + kv_write_lock.set(PreimageKey::new_keccak256(*hash).into(), preimage.into())?; + } + HintType::L2AccountProof => { + ensure!(hint.data.len() == 8 + 20, "Invalid hint data length"); + + let block_number = u64::from_be_bytes(hint.data.as_ref()[..8].try_into()?); + let address = Address::from_slice(&hint.data.as_ref()[8..28]); + + let proof_response = providers + .l2 + .get_proof(address, Default::default()) + .block_id(block_number.into()) + .await?; + + // Write the account proof nodes to the key-value store. + let mut kv_lock = kv.write().await; + proof_response.account_proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + } + HintType::L2AccountStorageProof => { + ensure!(hint.data.len() == 8 + 20 + 32, "Invalid hint data length"); + + let block_number = u64::from_be_bytes(hint.data.as_ref()[..8].try_into()?); + let address = Address::from_slice(&hint.data.as_ref()[8..28]); + let slot = B256::from_slice(&hint.data.as_ref()[28..]); + + let mut proof_response = providers + .l2 + .get_proof(address, vec![slot]) + .block_id(block_number.into()) + .await?; + + let mut kv_lock = kv.write().await; + + // Write the account proof nodes to the key-value store. + proof_response.account_proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + + // Write the storage proof nodes to the key-value store. + let storage_proof = proof_response.storage_proof.remove(0); + storage_proof.proof.into_iter().try_for_each(|node| { + let node_hash = keccak256(node.as_ref()); + let key = PreimageKey::new_keccak256(*node_hash); + kv_lock.set(key.into(), node.into())?; + Ok::<(), anyhow::Error>(()) + })?; + } + HintType::L2PayloadWitness => { + if !cfg.enable_experimental_witness_endpoint { + warn!( + target: "single_hint_handler", + "L2PayloadWitness hint was sent, but payload witness is disabled. Skipping hint." + ); + return Ok(()); + } + + ensure!(hint.data.len() >= 32, "Invalid hint data length"); + + let parent_block_hash = B256::from_slice(&hint.data.as_ref()[..32]); + let payload_attributes: OpPayloadAttributes = + serde_json::from_slice(&hint.data[32..])?; + + let Ok(execute_payload_response) = providers + .l2 + .client() + .request::<(B256, OpPayloadAttributes), ExecutionWitness>( + "debug_executePayload", + (parent_block_hash, payload_attributes), + ) + .await + else { + // Allow this hint to fail silently, as not all execution clients support + // the `debug_executePayload` method. + return Ok(()); + }; + + let preimages = execute_payload_response + .state + .into_iter() + .chain(execute_payload_response.codes) + .chain(execute_payload_response.keys); + + let mut kv_lock = kv.write().await; + for preimage in preimages { + let computed_hash = keccak256(preimage.as_ref()); + + let key = PreimageKey::new_keccak256(*computed_hash); + kv_lock.set(key.into(), preimage.into())?; + } + } + } + + Ok(()) + } +} diff --git a/kona/bin/host/src/single/local_kv.rs b/kona/bin/host/src/single/local_kv.rs new file mode 100644 index 0000000000000..c416f722a2963 --- /dev/null +++ b/kona/bin/host/src/single/local_kv.rs @@ -0,0 +1,57 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data on disk, +//! using the [SingleChainHost] config. + +use super::SingleChainHost; +use crate::KeyValueStore; +use alloy_primitives::B256; +use anyhow::Result; +use kona_preimage::PreimageKey; +use kona_proof::boot::{ + L1_CONFIG_KEY, L1_HEAD_KEY, L2_CHAIN_ID_KEY, L2_CLAIM_BLOCK_NUMBER_KEY, L2_CLAIM_KEY, + L2_OUTPUT_ROOT_KEY, L2_ROLLUP_CONFIG_KEY, +}; + +/// A simple, synchronous key-value store that returns data from a [SingleChainHost] config. +#[derive(Debug)] +pub struct SingleChainLocalInputs { + cfg: SingleChainHost, +} + +impl SingleChainLocalInputs { + /// Create a new [SingleChainLocalInputs] with the given [SingleChainHost] config. + pub const fn new(cfg: SingleChainHost) -> Self { + Self { cfg } + } +} + +impl KeyValueStore for SingleChainLocalInputs { + fn get(&self, key: B256) -> Option> { + let preimage_key = PreimageKey::try_from(*key).ok()?; + match preimage_key.key_value() { + L1_HEAD_KEY => Some(self.cfg.l1_head.to_vec()), + L2_OUTPUT_ROOT_KEY => Some(self.cfg.agreed_l2_output_root.to_vec()), + L2_CLAIM_KEY => Some(self.cfg.claimed_l2_output_root.to_vec()), + L2_CLAIM_BLOCK_NUMBER_KEY => { + Some(self.cfg.claimed_l2_block_number.to_be_bytes().to_vec()) + } + L2_CHAIN_ID_KEY => { + Some(self.cfg.l2_chain_id.unwrap_or_default().to_be_bytes().to_vec()) + } + L2_ROLLUP_CONFIG_KEY => { + let rollup_config = self.cfg.read_rollup_config().ok()?; + let serialized = serde_json::to_vec(&rollup_config).ok()?; + Some(serialized) + } + L1_CONFIG_KEY => { + let l1_config = self.cfg.read_l1_config().ok()?; + let serialized = serde_json::to_vec(&l1_config).ok()?; + Some(serialized) + } + _ => None, + } + } + + fn set(&mut self, _: B256, _: Vec) -> Result<()> { + unreachable!("LocalKeyValueStore is read-only") + } +} diff --git a/kona/bin/host/src/single/mod.rs b/kona/bin/host/src/single/mod.rs new file mode 100644 index 0000000000000..59048b9140311 --- /dev/null +++ b/kona/bin/host/src/single/mod.rs @@ -0,0 +1,10 @@ +//! This module contains the single-chain mode for the host. + +mod cfg; +pub use cfg::{SingleChainHost, SingleChainHostError, SingleChainProviders}; + +mod local_kv; +pub use local_kv::SingleChainLocalInputs; + +mod handler; +pub use handler::SingleChainHintHandler; diff --git a/kona/bin/node/Cargo.toml b/kona/bin/node/Cargo.toml new file mode 100644 index 0000000000000..9feedad43a820 --- /dev/null +++ b/kona/bin/node/Cargo.toml @@ -0,0 +1,87 @@ +[package] +name = "kona-node" +version = "1.0.0-rc.1" +description = "Kona Consensus Node" + +edition.workspace = true +authors.workspace = true +license.workspace = true +keywords.workspace = true +repository.workspace = true +categories.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# workspace +kona-rpc.workspace = true +kona-peers.workspace = true +kona-genesis = { workspace = true, features = ["tabled"] } +kona-protocol.workspace = true + +kona-cli = { workspace = true, features = ["secrets"] } +kona-gossip = { workspace = true, features = ["metrics"] } +kona-disc = { workspace = true, features = ["metrics"] } +kona-derive = { workspace = true, features = ["metrics"] } +kona-engine = { workspace = true, features = ["metrics"] } +kona-registry = { workspace = true, features = ["tabled"] } +kona-sources = { workspace = true } +kona-node-service = { workspace = true, features = ["metrics"] } +kona-providers-alloy = { workspace = true, features = ["metrics"] } + +# alloy +alloy-chains.workspace = true +alloy-genesis.workspace = true +alloy-signer.workspace = true +alloy-provider.workspace = true +alloy-transport.workspace = true +alloy-transport-http.workspace = true +alloy-primitives.workspace = true +alloy-signer-local.workspace = true +alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } + +# op-alloy +op-alloy-provider.workspace = true +op-alloy-network.workspace = true +op-alloy-rpc-types-engine = { workspace = true, features = ["serde"] } + +# general +url.workspace = true +http.workspace = true +dirs.workspace = true +strum.workspace = true +discv5.workspace = true +tabled.workspace = true +libp2p.workspace = true +derive_more.workspace = true +anyhow.workspace = true +futures.workspace = true +metrics.workspace = true +reqwest.workspace = true +tracing.workspace = true +thiserror.workspace = true +tokio-stream.workspace = true +tokio-util.workspace = true +serde_json = { workspace = true, features = ["std"] } +jsonrpsee = { workspace = true, features = ["server"] } +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +backon = { workspace = true, features = ["std", "tokio", "tokio-sleep"] } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } + +# rollup boost +rollup-boost.workspace = true + +[dev-dependencies] +rstest.workspace = true +tempfile.workspace = true + +[build-dependencies] +vergen = { workspace = true, features = ["build", "cargo", "emit_and_set"] } +vergen-git2.workspace = true + +[features] +default = [ "asm-keccak" ] +asm-keccak = [ "alloy-primitives/asm-keccak" ] diff --git a/kona/bin/node/README.md b/kona/bin/node/README.md new file mode 100644 index 0000000000000..e63b5360bbacf --- /dev/null +++ b/kona/bin/node/README.md @@ -0,0 +1,170 @@ +# `kona-node` + +A modular, robust rollup node implementation for the OP Stack built in rust. + +## Installation + +You can install `kona-node` directly from crates.io using Cargo: + +```bash +cargo install kona-node +``` + +## Usage + +### Basic Command Structure + +```bash +kona-node [GLOBAL_OPTIONS] [SUBCOMMAND_OPTIONS] +``` + +### Available Subcommands + +- **`node`** (alias: `n`) - Runs the consensus node for OP Stack rollup validation +- **`net`** (aliases: `p2p`, `network`) - Runs the networking stack for the node +- **`registry`** (aliases: `r`, `scr`) - Lists OP Stack chains available in the superchain-registry +- **`bootstore`** (aliases: `b`, `boot`, `store`) - Utility tool to interact with local bootstores +- **`info`** - Get information about OP Stack chains + +### Running the Consensus Node + +The primary use case is running the consensus node with the `node` subcommand: + +```bash +kona-node \ + --chain 11155420 \ + --metrics.enabled \ + --metrics.port 9002 \ + node \ + --l1 \ + --l1-beacon \ + --l2 \ + --l2-engine-jwt-secret /path/to/jwt.hex \ + --port 5060 \ + --p2p.listen.tcp 9223 \ + --p2p.listen.udp 9223 \ + --p2p.scoring off \ + --p2p.bootstore /path/to/bootstore +``` + +### Example: OP Sepolia Configuration + +Here's a complete example for running a kona-node connected to OP Sepolia: + +```bash +# Set required environment variables +export L1_PROVIDER_RPC="https://your-l1-rpc-endpoint" +export L1_BEACON_API="https://your-l1-beacon-api-endpoint" + +# Run the node +kona-node \ + --chain 11155420 \ + --metrics.enabled \ + --metrics.port 9002 \ + node \ + --l1 $L1_PROVIDER_RPC \ + --l1-beacon $L1_BEACON_API \ + --l2 http://localhost:8551 \ + --l2-engine-jwt-secret ./jwt.hex \ + --port 5060 \ + --p2p.listen.tcp 9223 \ + --p2p.listen.udp 9223 \ + --p2p.scoring off \ + --p2p.bootstore ./bootstore +``` + +### Configuration via Environment Variables + +Many configuration options can be set via environment variables: + +- `KONA_NODE_L1_ETH_RPC` - L1 execution client RPC URL +- `KONA_NODE_L1_TRUST_RPC` - Whether to trust the L1 RPC without verification (default: true) +- `KONA_NODE_L1_BEACON` - L1 beacon API URL +- `KONA_NODE_L2_ENGINE_RPC` - L2 engine API URL +- `KONA_NODE_L2_TRUST_RPC` - Whether to trust the L2 RPC without verification (default: true) +- `KONA_NODE_L2_ENGINE_AUTH` - Path to L2 engine JWT secret file +- `KONA_NODE_MODE` - Node operation mode (default: validator) +- `RUST_LOG` - Logging configuration + +Example using environment variables: + +```bash +export KONA_NODE_L1_ETH_RPC="https://your-l1-rpc" +export KONA_NODE_L1_BEACON="https://your-l1-beacon-api" +export KONA_NODE_L2_ENGINE_RPC="http://localhost:8551" +export RUST_LOG="kona_node=info,kona_derive=debug" + +kona-node node --port 5060 +``` + +### Help and Documentation + +Use the `--help` flag to see all available options and subcommands: + +```bash +# General help +kona-node --help + +# Help for specific subcommands +kona-node node --help +kona-node net --help +kona-node registry --help +``` + +### Networking and P2P + +Run just the networking stack: + +```bash +kona-node net \ + --p2p.listen.tcp 9223 \ + --p2p.listen.udp 9223 \ + --p2p.bootstore ./bootstore +``` + +### Registry Information + +List available OP Stack chains: + +```bash +kona-node registry +``` + +Get information about a specific chain: + +```bash +kona-node info --help +``` + +## Requirements + +- **L1 Execution Client**: Access to an Ethereum L1 execution client RPC endpoint +- **L1 Beacon API**: Access to an Ethereum L1 beacon chain API endpoint +- **L2 Execution Client**: Access to an OP Stack L2 execution client (e.g., op-reth) +- **JWT Secret**: A JWT secret file for authenticated communication with the L2 execution client + +## Advanced Configuration + +### RPC Trust Configuration + +By default, Kona trusts RPC providers and does not perform additional block hash verification, optimizing for performance. This can be configured using trust flags: + +```bash +# For untrusted/public RPC providers (adds verification) +kona-node node \ + --l1 https://public-rpc-endpoint.com \ + --l1-trust-rpc false \ + --l2 https://another-public-rpc.com \ + --l2-trust-rpc false \ + # ... other options +``` + +**Security Considerations:** +- Default behavior (`true`): No additional verification, assumes RPC is trustworthy +- Verification mode (`false`): All block hashes are verified against requested hashes +- Use verification (`false`) for public or third-party RPC endpoints +- Default trust (`true`) is suitable for local nodes and trusted infrastructure + +### Production Deployments + +For production deployments and advanced configurations, refer to the docker recipe in the main repository at `docker/recipes/kona-node/` which provides a complete setup example with monitoring and multiple services. diff --git a/kona/bin/node/build.rs b/kona/bin/node/build.rs new file mode 100644 index 0000000000000..c4aff57130a98 --- /dev/null +++ b/kona/bin/node/build.rs @@ -0,0 +1,92 @@ +//! Derived from [`reth-node-core`][reth-build-script] +//! +//! [reth-build-script]: https://github.com/paradigmxyz/reth/blob/805fb1012cd1601c3b4fe9e8ca2d97c96f61355b/crates/node/core/build.rs + +#![allow(missing_docs)] + +use std::{env, error::Error}; +use vergen::{BuildBuilder, CargoBuilder, Emitter}; +use vergen_git2::Git2Builder; + +fn main() -> Result<(), Box> { + let mut emitter = Emitter::default(); + + let build_builder = BuildBuilder::default().build_timestamp(true).build()?; + + // Add build timestamp information. + emitter.add_instructions(&build_builder)?; + + let cargo_builder = CargoBuilder::default().features(true).target_triple(true).build()?; + + // Add cargo features and target information. + emitter.add_instructions(&cargo_builder)?; + + let git_builder = + Git2Builder::default().describe(false, true, None).dirty(true).sha(false).build()?; + + // Add commit information. + emitter.add_instructions(&git_builder)?; + + emitter.emit_and_set()?; + let sha = env::var("VERGEN_GIT_SHA")?; + let sha_short = &sha[0..8]; + + let is_dirty = env::var("VERGEN_GIT_DIRTY")? == "true"; + // > git describe --always --tags + // if not on a tag: v0.2.0-beta.3-82-g1939939b + // if on a tag: v0.2.0-beta.3 + let not_on_tag = env::var("VERGEN_GIT_DESCRIBE")?.ends_with(&format!("-g{sha_short}")); + let version_suffix = if is_dirty || not_on_tag { "-dev" } else { "" }; + println!("cargo:rustc-env=KONA_NODE_VERSION_SUFFIX={version_suffix}"); + + // Set short SHA + println!("cargo:rustc-env=VERGEN_GIT_SHA_SHORT={sha_short}"); + + // Set the build profile + let out_dir = env::var("OUT_DIR").unwrap(); + let profile = out_dir.rsplit(std::path::MAIN_SEPARATOR).nth(3).unwrap(); + println!("cargo:rustc-env=KONA_NODE_BUILD_PROFILE={profile}"); + + // Set formatted version strings + let pkg_version = env!("CARGO_PKG_VERSION"); + + // The short version information for kona-node. + // - The latest version from Cargo.toml + // - The short SHA of the latest commit. + // Example: 0.1.0 (defa64b2) + println!("cargo:rustc-env=KONA_NODE_SHORT_VERSION={pkg_version}{version_suffix} ({sha_short})"); + + let features = env::var("VERGEN_CARGO_FEATURES")?; + + // LONG_VERSION + // The long version information for kona-node. + // + // - The latest version from Cargo.toml + version suffix (if any) + // - The full SHA of the latest commit + // - The build datetime + // - The build features + // - The build profile + // + // Example: + // + // ```text + // Version: 0.1.0 + // Commit SHA: defa64b2 + // Build Timestamp: 2023-05-19T01:47:19.815651705Z + // Build Features: jemalloc + // Build Profile: maxperf + // ``` + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_0=Version: {pkg_version}{version_suffix}"); + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_1=Commit SHA: {sha}"); + println!( + "cargo:rustc-env=KONA_NODE_LONG_VERSION_2=Build Timestamp: {}", + env::var("VERGEN_BUILD_TIMESTAMP")? + ); + println!( + "cargo:rustc-env=KONA_NODE_LONG_VERSION_3=Build Features: {}", + if features.is_empty() { "no features enabled".to_string() } else { features } + ); + println!("cargo:rustc-env=KONA_NODE_LONG_VERSION_4=Build Profile: {profile}"); + + Ok(()) +} diff --git a/kona/bin/node/src/cli.rs b/kona/bin/node/src/cli.rs new file mode 100644 index 0000000000000..4e48d73e3ade5 --- /dev/null +++ b/kona/bin/node/src/cli.rs @@ -0,0 +1,164 @@ +//! Contains the node CLI. + +use crate::{ + commands::{BootstoreCommand, InfoCommand, NetCommand, NodeCommand, RegistryCommand}, + flags::{GlobalArgs, init_unified_metrics}, + version, +}; +use anyhow::Result; +use clap::{Parser, Subcommand}; +use kona_cli::cli_styles; +use strum::Display; + +/// Subcommands for the CLI. +#[derive(Debug, Clone, Subcommand, Display)] +#[allow(clippy::large_enum_variant)] +pub enum Commands { + /// Runs the consensus node. + #[command(alias = "n")] + Node(NodeCommand), + /// Runs the networking stack for the node. + #[command(alias = "p2p", alias = "network")] + Net(NetCommand), + /// Lists the OP Stack chains available in the superchain-registry. + #[command(alias = "r", alias = "scr")] + Registry(RegistryCommand), + /// Utility tool to interact with local bootstores. + #[command(alias = "b", alias = "boot", alias = "store")] + Bootstore(BootstoreCommand), + /// Get info about op chain. + Info(InfoCommand), +} + +/// The node CLI. +#[derive(Parser, Clone, Debug)] +#[command( + author, + version = version::SHORT_VERSION, + long_version = version::LONG_VERSION, + about, + styles = cli_styles(), + long_about = None +)] +pub struct Cli { + /// The subcommand to run. + #[command(subcommand)] + pub subcommand: Commands, + /// Global arguments for the CLI. + #[command(flatten)] + pub global: GlobalArgs, +} + +impl Cli { + /// Runs the CLI. + pub fn run(self) -> Result<()> { + // Initialize telemetry - allow subcommands to customize the filter. + match self.subcommand { + Commands::Node(ref node) => node.init_logs(&self.global)?, + Commands::Net(ref net) => net.init_logs(&self.global)?, + Commands::Registry(ref registry) => registry.init_logs(&self.global)?, + Commands::Bootstore(ref bootstore) => bootstore.init_logs(&self.global)?, + Commands::Info(ref info) => info.init_logs(&self.global)?, + } + + // Initialize unified metrics + init_unified_metrics(&self.global.metrics)?; + + // Allow subcommands to initialize cli metrics. + match self.subcommand { + Commands::Node(ref node) => node.init_cli_metrics(&self.global.metrics)?, + _ => { + tracing::debug!(target: "cli", "No CLI metrics initialized for subcommand: {:?}", self.subcommand) + } + } + + // Run the subcommand. + match self.subcommand { + Commands::Node(node) => Self::run_until_ctrl_c(node.run(&self.global)), + Commands::Net(net) => Self::run_until_ctrl_c(net.run(&self.global)), + Commands::Registry(registry) => registry.run(&self.global), + Commands::Bootstore(bootstore) => bootstore.run(&self.global), + Commands::Info(info) => info.run(&self.global), + } + } + + /// Run until ctrl-c is pressed. + pub fn run_until_ctrl_c(fut: F) -> Result<()> + where + F: std::future::Future>, + { + let rt = Self::tokio_runtime().map_err(|e| anyhow::anyhow!(e))?; + rt.block_on(async move { + tokio::select! { + res = fut => res, + _ = tokio::signal::ctrl_c() => { + tracing::info!(target: "cli", "Received Ctrl-C, shutting down..."); + Ok(()) + } + } + }) + } + + /// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all + /// features enabled + pub fn tokio_runtime() -> Result { + tokio::runtime::Builder::new_multi_thread().enable_all().build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case::node_subcommand_long(Commands::Node(Default::default()), "node")] + #[case::node_subcommand_short(Commands::Node(Default::default()), "n")] + #[case::net_subcommand_extra_long(Commands::Net(Default::default()), "network")] + #[case::net_subcommand_long(Commands::Net(Default::default()), "net")] + #[case::net_subcommand_short(Commands::Net(Default::default()), "p2p")] + #[case::registry_subcommand_short(Commands::Registry(Default::default()), "r")] + #[case::registry_subcommand_long(Commands::Registry(Default::default()), "scr")] + #[case::bootstore_subcommand_short(Commands::Bootstore(Default::default()), "b")] + #[case::bootstore_subcommand_long(Commands::Bootstore(Default::default()), "boot")] + #[case::bootstore_subcommand_long2(Commands::Bootstore(Default::default()), "store")] + #[case::info_subcommand(Commands::Info(Default::default()), "info")] + fn test_parse_cli(#[case] subcommand: Commands, #[case] subcommand_alias: &str) { + let args = vec!["kona-node", subcommand_alias, "--help"]; + let cli = Cli::parse_from(args); + assert_eq!(cli.subcommand.to_string(), subcommand.to_string()); + } + + #[rstest] + #[case::numeric_optimism("--l2-chain-id", "10", 10)] + #[case::numeric_base("--l2-chain-id", "8453", 8453)] + #[case::numeric_ethereum("--l2-chain-id", "1", 1)] + #[case::string_optimism("--l2-chain-id", "optimism", 10)] + #[case::string_base("--l2-chain-id", "base", 8453)] + #[case::string_mainnet("--l2-chain-id", "mainnet", 1)] + #[case::short_flag_numeric("-c", "10", 10)] + #[case::short_flag_string("-c", "optimism", 10)] + fn test_cli_l2_chain_id_valid( + #[case] flag: &str, + #[case] value: &str, + #[case] expected_id: u64, + ) { + let cli = Cli::try_parse_from(["kona-node", flag, value, "registry"]).unwrap(); + assert_eq!(cli.global.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::invalid_string("invalid_chain")] + #[case::empty_string("")] + fn test_cli_l2_chain_id_invalid(#[case] invalid_value: &str) { + let result = Cli::try_parse_from(["kona-node", "--l2-chain-id", invalid_value, "registry"]); + assert!(result.is_err()); + } + + #[test] + fn test_cli_l2_chain_id_default() { + // Test that the default chain ID is 10 (Optimism) + let cli = Cli::try_parse_from(["kona-node", "registry"]).unwrap(); + assert_eq!(cli.global.l2_chain_id.id(), 10); + } +} diff --git a/kona/bin/node/src/commands/bootstore.rs b/kona/bin/node/src/commands/bootstore.rs new file mode 100644 index 0000000000000..44f7d1fce0067 --- /dev/null +++ b/kona/bin/node/src/commands/bootstore.rs @@ -0,0 +1,73 @@ +//! Bootstore Subcommand + +use crate::flags::GlobalArgs; +use clap::Parser; +use kona_cli::LogConfig; +use kona_peers::{BootStore, BootStoreFile}; +use std::path::PathBuf; + +/// The `bootstore` Subcommand +/// +/// The `bootstore` subcommand can be used to interact with local bootstores. +/// +/// # Usage +/// +/// ```sh +/// kona-node bootstore [FLAGS] [OPTIONS] +/// ``` +#[derive(Parser, Default, PartialEq, Debug, Clone)] +#[command(about = "Utility tool to interact with local bootstores")] +pub struct BootstoreCommand { + /// Optionally prints all bootstores. + /// This option overrides the chain ID configured with `--l2-chain-id`. + #[arg(long = "all")] + pub all: bool, + /// The directory to store the bootstore. + #[arg(long = "p2p.bootstore", env = "KONA_NODE_P2P_BOOTSTORE")] + pub bootstore: Option, +} + +impl BootstoreCommand { + /// Initializes the logging system based on global arguments. + pub fn init_logs(&self, args: &GlobalArgs) -> anyhow::Result<()> { + LogConfig::new(args.log_args.clone()).init_tracing_subscriber(None)?; + Ok(()) + } + + /// Runs the subcommand. + pub fn run(self, args: &GlobalArgs) -> anyhow::Result<()> { + println!("--------------------------"); + if self.all { + self.all()?; + } else { + self.info(args.l2_chain_id.into())?; + } + Ok(()) + } + + /// Prints all bootstores. + pub fn all(&self) -> anyhow::Result<()> { + for available in BootStore::available(self.bootstore.clone()) { + self.info(available)?; + } + Ok(()) + } + + /// Prints information for the bootstore with the given chain ID. + pub fn info(&self, chain_id: u64) -> anyhow::Result<()> { + let chain = kona_registry::OPCHAINS + .get(&chain_id) + .ok_or(anyhow::anyhow!("Chain ID {chain_id} not found in the registry"))?; + println!("{} Bootstore (Chain ID: {chain_id})", chain.name); + let bootstore: BootStoreFile = self + .bootstore + .clone() + .map_or(BootStoreFile::Default { chain_id }, BootStoreFile::Custom); + let bootstore: BootStore = bootstore.try_into()?; + println!("Path: {}", self.bootstore.clone().unwrap_or_default().display()); + println!("Peer Count: {}", bootstore.peers.len()); + println!("Valid peers: {}", bootstore.valid_peers_with_chain_id(chain_id).len()); + println!("--------------------------"); + Ok(()) + } +} diff --git a/kona/bin/node/src/commands/info.rs b/kona/bin/node/src/commands/info.rs new file mode 100644 index 0000000000000..f621e39ecf2ec --- /dev/null +++ b/kona/bin/node/src/commands/info.rs @@ -0,0 +1,49 @@ +//! Info Subcommand + +use crate::flags::GlobalArgs; +use clap::Parser; +use kona_cli::LogConfig; +use kona_registry::{OPCHAINS, ROLLUP_CONFIGS}; +use tracing::info; + +/// The `info` Subcommand +/// +/// The `info` subcommand is used to run the information stack for the `kona-node`. +/// +/// # Usage +/// +/// ```sh +/// kona-node info +/// ``` + +#[derive(Parser, Default, PartialEq, Debug, Clone)] +#[command(about = "Runs the information stack for the kona-node.")] +pub struct InfoCommand; + +impl InfoCommand { + /// Initializes the logging system based on global arguments. + pub fn init_logs(&self, args: &GlobalArgs) -> anyhow::Result<()> { + LogConfig::new(args.log_args.clone()).init_tracing_subscriber(None)?; + Ok(()) + } + + /// Runs the information stack for the kona-node. + pub fn run(&self, args: &GlobalArgs) -> anyhow::Result<()> { + info!(target: "node_info", "Running info command"); + + let op_chain_config = OPCHAINS.get(&args.l2_chain_id.id()).expect("No Chain config found"); + let op_rollup_config = + ROLLUP_CONFIGS.get(&args.l2_chain_id.id()).expect("No Rollup config found"); + + println!("Name: {}", op_chain_config.name); + println!("Block Time: {}", op_chain_config.block_time); + println!("Identifier: {}", op_chain_config.chain_id); + println!("Public RPC - {}", op_chain_config.public_rpc); + println!("Sequencer RPC - {}", op_chain_config.sequencer_rpc); + println!("Explorer - {}", op_chain_config.explorer); + println!("Hardforks: {}", op_rollup_config.hardforks); + println!("-------------"); + + Ok(()) + } +} diff --git a/kona/bin/node/src/commands/mod.rs b/kona/bin/node/src/commands/mod.rs new file mode 100644 index 0000000000000..decfd8139a705 --- /dev/null +++ b/kona/bin/node/src/commands/mod.rs @@ -0,0 +1,16 @@ +//! Contains subcommands for the kona node. + +mod info; +pub use info::InfoCommand; + +mod node; +pub use node::NodeCommand; + +mod bootstore; +pub use bootstore::BootstoreCommand; + +mod net; +pub use net::NetCommand; + +mod registry; +pub use registry::RegistryCommand; diff --git a/kona/bin/node/src/commands/net.rs b/kona/bin/node/src/commands/net.rs new file mode 100644 index 0000000000000..ce9c84e93ca52 --- /dev/null +++ b/kona/bin/node/src/commands/net.rs @@ -0,0 +1,132 @@ +//! Net Subcommand + +use crate::flags::{GlobalArgs, P2PArgs, RpcArgs}; +use clap::Parser; +use futures::future::OptionFuture; +use jsonrpsee::{RpcModule, server::Server}; +use kona_cli::LogConfig; +use kona_gossip::P2pRpcRequest; +use kona_node_service::{ + NetworkActor, NetworkBuilder, NetworkContext, NetworkInboundData, NodeActor, +}; +use kona_registry::scr_rollup_config_by_alloy_ident; +use kona_rpc::{OpP2PApiServer, P2pRpc, RpcBuilder}; +use tokio_util::sync::CancellationToken; +use tracing::{info, warn}; +use url::Url; + +/// The `net` Subcommand +/// +/// The `net` subcommand is used to run the networking stack for the `kona-node`. +/// +/// # Usage +/// +/// ```sh +/// kona-node net [FLAGS] [OPTIONS] +/// ``` +#[derive(Parser, Default, PartialEq, Debug, Clone)] +#[command(about = "Runs the networking stack for the kona-node.")] +pub struct NetCommand { + /// URL of the L1 execution client RPC API. + /// This is used to load the unsafe block signer at startup. + /// Without this, the rollup config unsafe block signer will be used which may be outdated. + #[arg(long, visible_alias = "l1", env = "L1_ETH_RPC")] + pub l1_eth_rpc: Option, + /// P2P CLI Flags + #[command(flatten)] + pub p2p: P2PArgs, + /// RPC CLI Flags + #[command(flatten)] + pub rpc: RpcArgs, +} + +impl NetCommand { + /// Initializes the logging system based on global arguments. + pub fn init_logs(&self, args: &GlobalArgs) -> anyhow::Result<()> { + // Filter out discovery warnings since they're very very noisy. + let filter = tracing_subscriber::EnvFilter::from_default_env() + .add_directive("discv5=error".parse()?) + .add_directive("bootstore=debug".parse()?); + + // Initialize the telemetry stack. + LogConfig::new(args.log_args.clone()).init_tracing_subscriber(Some(filter))?; + Ok(()) + } + + /// Run the Net subcommand. + pub async fn run(self, args: &GlobalArgs) -> anyhow::Result<()> { + let signer = args.genesis_signer()?; + info!(target: "net", "Genesis block signer: {:?}", signer); + + let rpc_config = Option::::from(self.rpc); + + // Get the rollup config from the args + let rollup_config = scr_rollup_config_by_alloy_ident(&args.l2_chain_id) + .ok_or(anyhow::anyhow!("Rollup config not found for chain id: {}", args.l2_chain_id))?; + + // Start the Network Stack + self.p2p.check_ports()?; + let p2p_config = self.p2p.config(rollup_config, args, self.l1_eth_rpc).await?; + + let (NetworkInboundData { p2p_rpc: rpc, .. }, network) = + NetworkActor::new(NetworkBuilder::from(p2p_config)); + + let (blocks, mut blocks_rx) = tokio::sync::mpsc::channel(1024); + network.start(NetworkContext { blocks, cancellation: CancellationToken::new() }).await?; + + info!(target: "net", "Network started, receiving blocks."); + + // On an interval, use the rpc tx to request stats about the p2p network. + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(2)); + + let handle = if let Some(config) = rpc_config { + info!(target: "net", socket = ?config.socket, "Starting RPC server"); + + // Setup the RPC server with the P2P RPC Module + let mut launcher = RpcModule::new(()); + launcher.merge(P2pRpc::new(rpc.clone()).into_rpc())?; + + let server = Server::builder().build(config.socket).await?; + Some(server.start(launcher)) + } else { + info!(target: "net", "RPC server disabled"); + None + }; + + loop { + tokio::select! { + Some(payload) = blocks_rx.recv() => { + info!(target: "net", "Received unsafe payload: {:?}", payload.execution_payload.block_hash()); + } + _ = interval.tick(), if !rpc.is_closed() => { + let (otx, mut orx) = tokio::sync::oneshot::channel(); + if let Err(e) = rpc.send(P2pRpcRequest::PeerCount(otx)).await { + warn!(target: "net", "Failed to send network rpc request: {:?}", e); + continue; + } + tokio::time::timeout(tokio::time::Duration::from_secs(5), async move { + loop { + match orx.try_recv() { + Ok((d, g)) => { + let d = d.unwrap_or_default(); + info!(target: "net", "Peer counts: Discovery={} | Swarm={}", d, g); + break; + } + Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + /* Keep trying to receive */ + } + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + break; + } + } + } + }).await.unwrap(); + } + _ = OptionFuture::from(handle.clone().map(|h| h.stopped())) => { + warn!(target: "net", "RPC server stopped"); + return Ok(()); + } + } + } + } +} diff --git a/kona/bin/node/src/commands/node.rs b/kona/bin/node/src/commands/node.rs new file mode 100644 index 0000000000000..bd30bcdf81cfd --- /dev/null +++ b/kona/bin/node/src/commands/node.rs @@ -0,0 +1,513 @@ +//! Node Subcommand. + +use crate::{ + flags::{ + BuilderClientArgs, GlobalArgs, L1ClientArgs, L2ClientArgs, P2PArgs, RollupBoostFlags, + RpcArgs, SequencerArgs, + }, + metrics::{CliMetrics, init_rollup_config_metrics}, +}; +use alloy_provider::RootProvider; +use alloy_rpc_types_engine::JwtSecret; +use alloy_transport_http::Http; +use anyhow::{Result, bail}; +use backon::{ExponentialBuilder, Retryable}; +use clap::Parser; +use kona_cli::{LogConfig, MetricsArgs}; +use kona_engine::{HyperAuthClient, OpEngineClient}; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_node_service::{EngineConfig, L1ConfigBuilder, NodeMode, RollupNodeBuilder}; +use kona_registry::{L1Config, scr_rollup_config_by_alloy_ident}; +use op_alloy_network::Optimism; +use op_alloy_provider::ext::engine::OpEngineApi; +use serde_json::from_reader; +use std::{fs::File, io::Write, path::PathBuf, sync::Arc, time::Duration}; +use strum::IntoEnumIterator; +use tracing::{debug, error, info}; + +/// A JWT token validation error. +#[derive(Debug, thiserror::Error)] +pub(super) enum JwtValidationError { + #[error("JWT signature is invalid")] + InvalidSignature, + #[error("Failed to exchange capabilities with engine: {0}")] + CapabilityExchange(String), +} + +/// Command-line interface for running a Kona rollup node. +/// +/// The `NodeCommand` struct defines all the configuration options needed to start and run +/// a rollup node in the Kona ecosystem. It supports multiple node modes including validator +/// and sequencer modes, and provides comprehensive networking and RPC configuration options. +/// +/// # Node Modes +/// +/// The node can operate in different modes: +/// - **Validator**: Validates L2 blocks and participates in consensus +/// - **Sequencer**: Sequences transactions and produces L2 blocks +/// +/// # Configuration Sources +/// +/// Configuration can be provided through: +/// - Command-line arguments +/// - Environment variables (prefixed with `KONA_NODE_`) +/// - Configuration files (for rollup config) +/// +/// # Examples +/// +/// ```bash +/// # Run as validator with default settings +/// kona node --l1-eth-rpc http://localhost:8545 \ +/// --l1-beacon http://localhost:5052 \ +/// --l2-engine-rpc http://localhost:8551 +/// +/// # Run as sequencer with custom JWT secret +/// kona node --mode sequencer \ +/// --l1-eth-rpc http://localhost:8545 \ +/// --l1-beacon http://localhost:5052 \ +/// --l2-engine-rpc http://localhost:8551 \ +/// --l2-engine-jwt-secret /path/to/jwt.hex +/// ``` +#[derive(Parser, Debug, Clone)] +#[command(about = "Runs the consensus node")] +pub struct NodeCommand { + /// The mode to run the node in. + #[arg( + long = "mode", + default_value_t = NodeMode::Validator, + env = "KONA_NODE_MODE", + help = format!( + "The mode to run the node in. Supported modes are: {}", + NodeMode::iter() + .map(|mode| format!("\"{}\"", mode.to_string())) + .collect::>() + .join(", ") + ) + )] + pub node_mode: NodeMode, + + /// L1 RPC CLI arguments. + #[clap(flatten)] + pub l1_rpc_args: L1ClientArgs, + + /// L2 engine CLI arguments. + #[clap(flatten)] + pub l2_client_args: L2ClientArgs, + + /// Optional block builder client. + #[clap(flatten)] + pub builder_client_args: BuilderClientArgs, + + /// Path to a custom L2 rollup configuration file + /// (overrides the default rollup configuration from the registry) + #[arg(long, visible_alias = "rollup-cfg", env = "KONA_NODE_ROLLUP_CONFIG")] + pub l2_config_file: Option, + /// Path to a custom L1 rollup configuration file + /// (overrides the default rollup configuration from the registry) + #[arg(long, visible_alias = "rollup-l1-cfg", env = "KONA_NODE_L1_CHAIN_CONFIG")] + pub l1_config_file: Option, + /// P2P CLI arguments. + #[command(flatten)] + pub p2p_flags: P2PArgs, + /// RPC CLI arguments. + #[command(flatten)] + pub rpc_flags: RpcArgs, + /// SEQUENCER CLI arguments. + #[command(flatten)] + pub sequencer_flags: SequencerArgs, + + /// Rollup boost CLI arguments - contains the builder and l2 engine arguments. + #[command(flatten)] + pub rollup_boost_flags: RollupBoostFlags, +} + +impl Default for NodeCommand { + fn default() -> Self { + Self { + l1_rpc_args: L1ClientArgs::default(), + l2_client_args: L2ClientArgs::default(), + builder_client_args: BuilderClientArgs::default(), + l2_config_file: None, + l1_config_file: None, + node_mode: NodeMode::Validator, + p2p_flags: P2PArgs::default(), + rpc_flags: RpcArgs::default(), + sequencer_flags: SequencerArgs::default(), + rollup_boost_flags: RollupBoostFlags::default(), + } + } +} + +impl NodeCommand { + /// Initializes the logging system based on global arguments. + pub fn init_logs(&self, args: &GlobalArgs) -> anyhow::Result<()> { + // Filter out discovery warnings since they're very very noisy. + let filter = tracing_subscriber::EnvFilter::from_default_env() + .add_directive("discv5=error".parse()?); + + LogConfig::new(args.log_args.clone()).init_tracing_subscriber(Some(filter))?; + Ok(()) + } + + /// Initializes CLI metrics for the Node subcommand. + pub fn init_cli_metrics(&self, args: &MetricsArgs) -> anyhow::Result<()> { + if !args.enabled { + debug!("CLI metrics are disabled"); + return Ok(()); + } + metrics::gauge!( + CliMetrics::IDENTIFIER, + &[ + (CliMetrics::P2P_PEER_SCORING_LEVEL, self.p2p_flags.scoring.to_string()), + (CliMetrics::P2P_TOPIC_SCORING_ENABLED, self.p2p_flags.topic_scoring.to_string()), + (CliMetrics::P2P_BANNING_ENABLED, self.p2p_flags.ban_enabled.to_string()), + ( + CliMetrics::P2P_PEER_REDIALING, + self.p2p_flags.peer_redial.unwrap_or(0).to_string() + ), + (CliMetrics::P2P_FLOOD_PUBLISH, self.p2p_flags.gossip_flood_publish.to_string()), + (CliMetrics::P2P_DISCOVERY_INTERVAL, self.p2p_flags.discovery_interval.to_string()), + ( + CliMetrics::P2P_ADVERTISE_IP, + self.p2p_flags + .advertise_ip + .map(|ip| ip.to_string()) + .unwrap_or(String::from("0.0.0.0")) + ), + ( + CliMetrics::P2P_ADVERTISE_TCP_PORT, + self.p2p_flags + .advertise_tcp_port + .map_or_else(|| "auto".to_string(), |p| p.to_string()) + ), + ( + CliMetrics::P2P_ADVERTISE_UDP_PORT, + self.p2p_flags + .advertise_udp_port + .map_or_else(|| "auto".to_string(), |p| p.to_string()) + ), + (CliMetrics::P2P_PEERS_LO, self.p2p_flags.peers_lo.to_string()), + (CliMetrics::P2P_PEERS_HI, self.p2p_flags.peers_hi.to_string()), + (CliMetrics::P2P_GOSSIP_MESH_D, self.p2p_flags.gossip_mesh_d.to_string()), + (CliMetrics::P2P_GOSSIP_MESH_D_LO, self.p2p_flags.gossip_mesh_dlo.to_string()), + (CliMetrics::P2P_GOSSIP_MESH_D_HI, self.p2p_flags.gossip_mesh_dhi.to_string()), + (CliMetrics::P2P_GOSSIP_MESH_D_LAZY, self.p2p_flags.gossip_mesh_dlazy.to_string()), + (CliMetrics::P2P_BAN_DURATION, self.p2p_flags.ban_duration.to_string()), + ] + ) + .set(1); + Ok(()) + } + + /// Check if the error is related to JWT signature validation + fn is_jwt_signature_error(error: &dyn std::error::Error) -> bool { + let mut source = Some(error); + while let Some(err) = source { + let err_str = err.to_string().to_lowercase(); + if err_str.contains("signature invalid") || + (err_str.contains("jwt") && err_str.contains("invalid")) || + err_str.contains("unauthorized") || + err_str.contains("authentication failed") + { + return true; + } + source = err.source(); + } + false + } + + /// Helper to check JWT signature error from anyhow::Error (for retry condition) + fn is_jwt_signature_error_from_anyhow(error: &anyhow::Error) -> bool { + Self::is_jwt_signature_error(error.as_ref() as &dyn std::error::Error) + } + + /// Validate the jwt secret if specified by exchanging capabilities with the engine. + /// Since the engine client will fail if the jwt token is invalid, this allows to ensure + /// that the jwt token passed as a cli arg is correct. + pub async fn validate_jwt(&self) -> anyhow::Result { + let jwt_secret = self.l2_jwt_secret()?; + + let engine = OpEngineClient::>::rpc_client::( + self.l2_client_args.l2_engine_rpc.clone(), + jwt_secret, + ); + + let exchange = || async { + match as OpEngineApi< + Optimism, + Http, + >>::exchange_capabilities(&engine, vec![]) + .await + { + Ok(_) => { + debug!("Successfully exchanged capabilities with engine"); + Ok(jwt_secret) + } + Err(e) => { + if Self::is_jwt_signature_error(&e) { + error!( + "Engine API JWT secret differs from the one specified by --l2.jwt-secret" + ); + error!( + "Ensure that the JWT secret file specified is correct (by default it is `jwt.hex` in the current directory)" + ); + return Err(JwtValidationError::InvalidSignature.into()) + } + Err(JwtValidationError::CapabilityExchange(e.to_string()).into()) + } + } + }; + + exchange + .retry(ExponentialBuilder::default()) + .when(|e| !Self::is_jwt_signature_error_from_anyhow(e)) + .notify(|_, duration| { + debug!("Retrying engine capability handshake after {duration:?}"); + }) + .await + } + + /// Run the Node subcommand. + pub async fn run(self, args: &GlobalArgs) -> anyhow::Result<()> { + let cfg = self.get_l2_config(args)?; + + info!( + target: "rollup_node", + chain_id = cfg.l2_chain_id.id(), + "Starting rollup node services" + ); + for hf in cfg.hardforks.to_string().lines() { + info!(target: "rollup_node", "{hf}"); + } + + let l1_config = L1ConfigBuilder { + chain_config: self.get_l1_config(cfg.l1_chain_id)?, + trust_rpc: self.l1_rpc_args.l1_trust_rpc, + beacon: self.l1_rpc_args.l1_beacon.clone(), + rpc_url: self.l1_rpc_args.l1_eth_rpc.clone(), + slot_duration_override: self.l1_rpc_args.l1_slot_duration_override, + }; + + // If metrics are enabled, initialize the global cli metrics. + args.metrics.enabled.then(|| init_rollup_config_metrics(&cfg)); + + let jwt_secret = self.validate_jwt().await?; + + self.p2p_flags.check_ports()?; + let p2p_config = self + .p2p_flags + .clone() + .config(&cfg, args, Some(self.l1_rpc_args.l1_eth_rpc.clone())) + .await?; + let rpc_config = self.rpc_flags.clone().into(); + + let engine_config = EngineConfig { + config: Arc::new(cfg.clone()), + builder_url: self.builder_client_args.l2_builder_rpc.clone(), + builder_jwt_secret: self.builder_jwt_secret()?, + builder_timeout: Duration::from_millis(self.builder_client_args.builder_timeout), + l2_url: self.l2_client_args.l2_engine_rpc.clone(), + l2_jwt_secret: jwt_secret, + l2_timeout: Duration::from_millis(self.l2_client_args.l2_engine_timeout), + l1_url: self.l1_rpc_args.l1_eth_rpc.clone(), + mode: self.node_mode, + rollup_boost: self.rollup_boost_flags.as_rollup_boost_args(), + }; + + RollupNodeBuilder::new( + cfg, + l1_config, + self.l2_client_args.l2_trust_rpc, + engine_config, + p2p_config, + rpc_config, + ) + .with_sequencer_config(self.sequencer_flags.config()) + .build() + .start() + .await + .map_err(|e| { + error!(target: "rollup_node", "Failed to start rollup node service: {e}"); + anyhow::anyhow!("{e}") + })?; + + Ok(()) + } + + /// Get the L1 config, either from a file or the known chains. + pub fn get_l1_config(&self, l1_chain_id: u64) -> Result { + match &self.l1_config_file { + Some(path) => { + debug!("Loading l1 config from file: {:?}", path); + let file = File::open(path) + .map_err(|e| anyhow::anyhow!("Failed to open l1 config file: {e}"))?; + from_reader(file).map_err(|e| anyhow::anyhow!("Failed to parse l1 config: {e}")) + } + None => { + debug!("Loading l1 config from known chains"); + let cfg = L1Config::get_l1_genesis(l1_chain_id).map_err(|e| { + anyhow::anyhow!("Failed to find l1 config for chain ID {l1_chain_id}: {e}") + })?; + Ok(cfg.into()) + } + } + } + + /// Get the L2 rollup config, either from a file or the superchain registry. + pub fn get_l2_config(&self, args: &GlobalArgs) -> Result { + match &self.l2_config_file { + Some(path) => { + debug!("Loading l2 config from file: {:?}", path); + let file = File::open(path) + .map_err(|e| anyhow::anyhow!("Failed to open l2 config file: {e}"))?; + from_reader(file).map_err(|e| anyhow::anyhow!("Failed to parse l2 config: {e}")) + } + None => { + debug!("Loading l2 config from superchain registry"); + let Some(cfg) = scr_rollup_config_by_alloy_ident(&args.l2_chain_id) else { + bail!("Failed to find l2 config for chain ID {}", args.l2_chain_id); + }; + Ok(cfg.clone()) + } + } + } + + /// Returns the L2 JWT secret for the engine API + /// using the provided [PathBuf]. If the file is not found, + /// it will return the default JWT secret. + pub fn l2_jwt_secret(&self) -> anyhow::Result { + if let Some(path) = &self.l2_client_args.l2_engine_jwt_secret && + let Ok(secret) = std::fs::read_to_string(path) + { + return JwtSecret::from_hex(secret) + .map_err(|e| anyhow::anyhow!("Failed to parse JWT secret: {e}")); + } + + if let Some(secret) = &self.l2_client_args.l2_engine_jwt_encoded { + return Ok(*secret); + } + + Self::default_jwt_secret("l2_jwt.hex") + } + + /// Returns the builder JWT secret for the engine API + /// using the provided [PathBuf]. If the file is not found, + /// it will return the default JWT secret. + pub fn builder_jwt_secret(&self) -> anyhow::Result { + if let Some(path) = &self.builder_client_args.builder_jwt_path && + let Ok(secret) = std::fs::read_to_string(path) + { + return JwtSecret::from_hex(secret) + .map_err(|e| anyhow::anyhow!("Failed to parse JWT secret: {e}")); + } + + if let Some(secret) = &self.builder_client_args.builder_jwt_secret { + return Ok(*secret); + } + + Self::default_jwt_secret("builder_jwt.hex") + } + + /// Uses the current directory to attempt to read + /// the JWT secret from a file named `file_name`. + /// If the file is not found, it will create a new random JWT secret and write it to the file. + pub fn default_jwt_secret(file_name: &str) -> anyhow::Result { + let cur_dir = std::env::current_dir() + .map_err(|e| anyhow::anyhow!("Failed to get current directory: {e}"))?; + std::fs::read_to_string(cur_dir.join(file_name)).map_or_else( + |_| { + let secret = JwtSecret::random(); + + if let Ok(mut file) = File::create(file_name) && + let Err(e) = file + .write_all(alloy_primitives::hex::encode(secret.as_bytes()).as_bytes()) + { + return Err(anyhow::anyhow!("Failed to write JWT secret to file: {e}")); + } + + Ok(secret) + }, + |content| Ok(JwtSecret::from_hex(content)?), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + + #[derive(Debug)] + struct MockError { + message: String, + } + + impl std::fmt::Display for MockError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } + } + + impl std::error::Error for MockError {} + + const fn default_flags() -> &'static [&'static str] { + &[ + "--l1-eth-rpc", + "http://localhost:8545", + "--l1-beacon", + "http://localhost:5052", + "--l2-engine-rpc", + "http://localhost:8551", + ] + } + + #[test] + fn test_node_cli_defaults() { + let args = NodeCommand::parse_from(["node"].iter().chain(default_flags().iter()).copied()); + assert_eq!(args.node_mode, NodeMode::Validator); + } + + #[test] + fn test_node_cli_missing_l1_eth_rpc() { + let err = NodeCommand::try_parse_from(["node"]).unwrap_err(); + assert!(err.to_string().contains("--l1-eth-rpc")); + } + + #[test] + fn test_node_cli_missing_l1_beacon() { + let err = NodeCommand::try_parse_from(["node", "--l1-eth-rpc", "http://localhost:8545"]) + .unwrap_err(); + assert!(err.to_string().contains("--l1-beacon")); + } + + #[test] + fn test_node_cli_missing_l2_engine_rpc() { + let err = NodeCommand::try_parse_from([ + "node", + "--l1-eth-rpc", + "http://localhost:8545", + "--l1-beacon", + "http://localhost:5052", + ]) + .unwrap_err(); + assert!(err.to_string().contains("--l2-engine-rpc")); + } + + #[test] + fn test_is_jwt_signature_error() { + let jwt_error = MockError { message: "signature invalid".to_string() }; + assert!(NodeCommand::is_jwt_signature_error(&jwt_error)); + + let other_error = MockError { message: "network timeout".to_string() }; + assert!(!NodeCommand::is_jwt_signature_error(&other_error)); + } + + #[test] + fn test_is_jwt_signature_error_from_anyhow() { + let jwt_anyhow_error = anyhow!("signature invalid"); + assert!(NodeCommand::is_jwt_signature_error_from_anyhow(&jwt_anyhow_error)); + + let other_anyhow_error = anyhow!("network timeout"); + assert!(!NodeCommand::is_jwt_signature_error_from_anyhow(&other_anyhow_error)); + } +} diff --git a/kona/bin/node/src/commands/registry.rs b/kona/bin/node/src/commands/registry.rs new file mode 100644 index 0000000000000..b203ccf445038 --- /dev/null +++ b/kona/bin/node/src/commands/registry.rs @@ -0,0 +1,39 @@ +//! Registry Subcommand + +use crate::flags::GlobalArgs; +use clap::Parser; +use kona_cli::LogConfig; + +/// The `registry` Subcommand +/// +/// The `registry` subcommand lists the OP Stack chains available in the `superchain-registry`. +/// +/// # Usage +/// +/// ```sh +/// kona-node registry [FLAGS] [OPTIONS] +/// ``` +#[derive(Parser, Default, PartialEq, Debug, Clone)] +#[command(about = "Lists the OP Stack chains available in the superchain-registry")] +pub struct RegistryCommand; + +impl RegistryCommand { + /// Initializes the logging system based on global arguments. + pub fn init_logs(&self, args: &GlobalArgs) -> anyhow::Result<()> { + LogConfig::new(args.log_args.clone()).init_tracing_subscriber(None)?; + Ok(()) + } + + /// Runs the subcommand. + pub fn run(self, _args: &GlobalArgs) -> anyhow::Result<()> { + let chains = kona_registry::CHAINS.chains.clone(); + let mut table = tabled::Table::new(chains); + table.with(tabled::settings::Style::modern()); + table.modify( + tabled::settings::object::Columns::first(), + tabled::settings::Alignment::right(), + ); + println!("{table}"); + Ok(()) + } +} diff --git a/kona/bin/node/src/flags/engine/flashblocks.rs b/kona/bin/node/src/flags/engine/flashblocks.rs new file mode 100644 index 0000000000000..a9468d3731ee6 --- /dev/null +++ b/kona/bin/node/src/flags/engine/flashblocks.rs @@ -0,0 +1,147 @@ +use clap::Parser; +use reqwest::Url; + +const DEFAULT_FLASHBLOCKS_BUILDER_URL: &str = "ws://localhost:1111"; +const DEFAULT_FLASHBLOCKS_HOST: &str = "localhost"; +const DEFAULT_FLASHBLOCKS_PORT: u16 = 1112; + +const DEFAULT_FLASHBLOCKS_BUILDER_WS_INITIAL_RECONNECT_MS: u64 = 10; +const DEFAULT_FLASHBLOCKS_BUILDER_WS_MAX_RECONNECT_MS: u64 = 5000; +const DEFAULT_FLASHBLOCKS_BUILDER_WS_PING_INTERVAL_MS: u64 = 500; +const DEFAULT_FLASHBLOCKS_BUILDER_WS_PONG_TIMEOUT_MS: u64 = 1500; + +/// Flashblocks flags. +#[derive(Clone, Debug, clap::Args)] +pub struct FlashblocksFlags { + /// Enable Flashblocks client + #[arg( + long, + visible_alias = "rollup-boost.flashblocks", + env = "KONA_NODE_FLASHBLOCKS", + default_value = "false" + )] + pub flashblocks: bool, + + /// Flashblocks Builder WebSocket URL + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-builder-url", + env = "KONA_NODE_FLASHBLOCKS_BUILDER_URL", + default_value = DEFAULT_FLASHBLOCKS_BUILDER_URL + )] + pub flashblocks_builder_url: Url, + + /// Flashblocks WebSocket host for outbound connections + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-host", + env = "KONA_NODE_FLASHBLOCKS_HOST", + default_value = DEFAULT_FLASHBLOCKS_HOST + )] + pub flashblocks_host: String, + + /// Flashblocks WebSocket port for outbound connections + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-port", + env = "KONA_NODE_FLASHBLOCKS_PORT", + default_value_t = DEFAULT_FLASHBLOCKS_PORT + )] + pub flashblocks_port: u16, + + /// Websocket connection configuration + #[command(flatten)] + pub flashblocks_ws_config: FlashblocksWebsocketFlags, +} + +impl Default for FlashblocksFlags { + fn default() -> Self { + Self { + flashblocks: false, + flashblocks_builder_url: Url::parse(DEFAULT_FLASHBLOCKS_BUILDER_URL).unwrap(), + flashblocks_host: DEFAULT_FLASHBLOCKS_HOST.to_string(), + flashblocks_port: DEFAULT_FLASHBLOCKS_PORT, + flashblocks_ws_config: FlashblocksWebsocketFlags::default(), + } + } +} + +/// Configuration for the Flashblocks WebSocket connection. +#[derive(Parser, Debug, Clone, Copy)] +pub struct FlashblocksWebsocketFlags { + /// Minimum time for exponential backoff for timeout if builder disconnected + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-initial-reconnect-ms", + env = "KONA_NODE_FLASHBLOCKS_BUILDER_WS_INITIAL_RECONNECT_MS", + default_value_t = DEFAULT_FLASHBLOCKS_BUILDER_WS_INITIAL_RECONNECT_MS + )] + pub flashblock_builder_ws_initial_reconnect_ms: u64, + + /// Maximum time for exponential backoff for timeout if builder disconnected + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-max-reconnect-ms", + env = "KONA_NODE_FLASHBLOCKS_BUILDER_WS_MAX_RECONNECT_MS", + default_value_t = DEFAULT_FLASHBLOCKS_BUILDER_WS_MAX_RECONNECT_MS + )] + pub flashblock_builder_ws_max_reconnect_ms: u64, + + /// Interval in milliseconds between ping messages sent to upstream servers to detect + /// unresponsive connections + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-ping-interval-ms", + env = "KONA_NODE_FLASHBLOCKS_BUILDER_WS_PING_INTERVAL_MS", + default_value_t = DEFAULT_FLASHBLOCKS_BUILDER_WS_PING_INTERVAL_MS + )] + pub flashblock_builder_ws_ping_interval_ms: u64, + + /// Timeout in milliseconds to wait for pong responses from upstream servers before considering + /// the connection dead + #[arg( + long, + visible_alias = "rollup-boost.flashblocks-pong-timeout-ms", + env = "KONA_NODE_FLASHBLOCKS_BUILDER_WS_PONG_TIMEOUT_MS", + default_value_t = DEFAULT_FLASHBLOCKS_BUILDER_WS_PONG_TIMEOUT_MS + )] + pub flashblock_builder_ws_pong_timeout_ms: u64, +} + +impl FlashblocksFlags { + /// Converts the flashblocks cli arguments to the flashblocks arguments used by the rollup-boost + /// server. + pub fn as_rollup_boost_args(self) -> rollup_boost::FlashblocksArgs { + rollup_boost::FlashblocksArgs { + flashblocks: self.flashblocks, + flashblocks_builder_url: self.flashblocks_builder_url, + flashblocks_host: self.flashblocks_host, + flashblocks_port: self.flashblocks_port, + flashblocks_ws_config: rollup_boost::FlashblocksWebsocketConfig { + flashblock_builder_ws_initial_reconnect_ms: self + .flashblocks_ws_config + .flashblock_builder_ws_initial_reconnect_ms, + flashblock_builder_ws_max_reconnect_ms: self + .flashblocks_ws_config + .flashblock_builder_ws_max_reconnect_ms, + flashblock_builder_ws_ping_interval_ms: self + .flashblocks_ws_config + .flashblock_builder_ws_ping_interval_ms, + flashblock_builder_ws_pong_timeout_ms: self + .flashblocks_ws_config + .flashblock_builder_ws_pong_timeout_ms, + }, + } + } +} + +impl Default for FlashblocksWebsocketFlags { + fn default() -> Self { + Self { + flashblock_builder_ws_initial_reconnect_ms: 10, + flashblock_builder_ws_max_reconnect_ms: 5000, + flashblock_builder_ws_ping_interval_ms: 500, + flashblock_builder_ws_pong_timeout_ms: 1500, + } + } +} diff --git a/kona/bin/node/src/flags/engine/mod.rs b/kona/bin/node/src/flags/engine/mod.rs new file mode 100644 index 0000000000000..65359a419c317 --- /dev/null +++ b/kona/bin/node/src/flags/engine/mod.rs @@ -0,0 +1,8 @@ +mod flashblocks; +pub use flashblocks::{FlashblocksFlags, FlashblocksWebsocketFlags}; + +mod providers; +pub use providers::{BuilderClientArgs, L1ClientArgs, L2ClientArgs}; + +mod rollup_boost; +pub use rollup_boost::RollupBoostFlags; diff --git a/kona/bin/node/src/flags/engine/providers.rs b/kona/bin/node/src/flags/engine/providers.rs new file mode 100644 index 0000000000000..fd07d1617d3d4 --- /dev/null +++ b/kona/bin/node/src/flags/engine/providers.rs @@ -0,0 +1,133 @@ +use alloy_rpc_types_engine::JwtSecret; +use std::path::PathBuf; +use url::Url; + +const DEFAULT_BUILDER_TIMEOUT: u64 = 30; +const DEFAULT_L2_ENGINE_TIMEOUT: u64 = 30_000; + +const DEFAULT_L2_TRUST_RPC: bool = true; +const DEFAULT_L1_TRUST_RPC: bool = true; + +/// Rollup-boost builder client arguments. +#[derive(Clone, Debug, clap::Args)] +pub struct BuilderClientArgs { + /// URL of the builder RPC API. + #[arg( + long, + visible_alias = "builder", + env = "KONA_NODE_BUILDER_RPC", + default_value = "http://localhost:8552" + )] + pub l2_builder_rpc: Url, + /// Hex encoded JWT secret to use for the authenticated builder RPC server. + #[arg(long, visible_alias = "builder.auth", env = "KONA_NODE_BUILDER_AUTH")] + pub builder_jwt_secret: Option, + /// Path to a JWT secret to use for the authenticated builder RPC server. + #[arg(long, visible_alias = "builder.jwt-path", env = "KONA_NODE_BUILDER_JWT_PATH")] + pub builder_jwt_path: Option, + /// Timeout for http calls in milliseconds. + #[arg( + long, + visible_alias = "builder.timeout", + env = "KONA_NODE_BUILDER_TIMEOUT", + default_value_t = DEFAULT_BUILDER_TIMEOUT + )] + pub builder_timeout: u64, +} + +impl Default for BuilderClientArgs { + fn default() -> Self { + Self { + l2_builder_rpc: Url::parse("http://localhost:8552").unwrap(), + builder_jwt_secret: None, + builder_jwt_path: None, + builder_timeout: DEFAULT_BUILDER_TIMEOUT, + } + } +} + +/// L1 client arguments. +#[derive(Clone, Debug, clap::Args)] +pub struct L1ClientArgs { + /// URL of the L1 execution client RPC API. + #[arg(long, visible_alias = "l1", env = "KONA_NODE_L1_ETH_RPC")] + pub l1_eth_rpc: Url, + /// Whether to trust the L1 RPC. + /// If false, block hash verification is performed for all retrieved blocks. + #[arg( + long, + visible_alias = "l1.trust-rpc", + env = "KONA_NODE_L1_TRUST_RPC", + default_value_t = DEFAULT_L1_TRUST_RPC + )] + pub l1_trust_rpc: bool, + /// URL of the L1 beacon API. + #[arg(long, visible_alias = "l1.beacon", env = "KONA_NODE_L1_BEACON")] + pub l1_beacon: Url, + /// Duration in seconds of an L1 slot. + /// + /// This is an optional argument that can be used to use a fixed slot duration for l1 blocks + /// and bypass the initial beacon spec fetch. This is useful for testing purposes when the + /// l1-beacon spec endpoint is not available (with anvil for example). + #[arg( + long, + visible_alias = "l1.slot-duration-override", + env = "KONA_NODE_L1_SLOT_DURATION_OVERRIDE" + )] + pub l1_slot_duration_override: Option, +} + +impl Default for L1ClientArgs { + fn default() -> Self { + Self { + l1_eth_rpc: Url::parse("http://localhost:8545").unwrap(), + l1_trust_rpc: DEFAULT_L1_TRUST_RPC, + l1_beacon: Url::parse("http://localhost:5052").unwrap(), + l1_slot_duration_override: None, + } + } +} + +/// L2 client arguments. +#[derive(Clone, Debug, clap::Args)] +pub struct L2ClientArgs { + /// URI of the engine API endpoint of an L2 execution client. + #[arg(long, visible_alias = "l2", env = "KONA_NODE_L2_ENGINE_RPC")] + pub l2_engine_rpc: Url, + /// JWT secret for the auth-rpc endpoint of the execution client. + /// This MUST be a valid path to a file containing the hex-encoded JWT secret. + #[arg(long, visible_alias = "l2.jwt-secret", env = "KONA_NODE_L2_ENGINE_AUTH")] + pub l2_engine_jwt_secret: Option, + /// Hex encoded JWT secret to use for the authenticated engine-API RPC server. + /// This MUST be a valid path to a file containing the hex-encoded JWT secret. + #[arg(long, visible_alias = "l2.jwt-path", env = "KONA_NODE_L2_ENGINE_JWT_PATH")] + pub l2_engine_jwt_encoded: Option, + /// Timeout for http calls in milliseconds. + #[arg( + long, + visible_alias = "l2.timeout", + env = "KONA_NODE_L2_ENGINE_TIMEOUT", + default_value_t = DEFAULT_L2_ENGINE_TIMEOUT + )] + pub l2_engine_timeout: u64, + /// If false, block hash verification is performed for all retrieved blocks. + #[arg( + long, + visible_alias = "l2.trust-rpc", + env = "KONA_NODE_L2_TRUST_RPC", + default_value_t = DEFAULT_L2_TRUST_RPC + )] + pub l2_trust_rpc: bool, +} + +impl Default for L2ClientArgs { + fn default() -> Self { + Self { + l2_engine_rpc: Url::parse("http://localhost:8551").unwrap(), + l2_engine_jwt_secret: None, + l2_engine_jwt_encoded: None, + l2_engine_timeout: DEFAULT_L2_ENGINE_TIMEOUT, + l2_trust_rpc: DEFAULT_L2_TRUST_RPC, + } + } +} diff --git a/kona/bin/node/src/flags/engine/rollup_boost.rs b/kona/bin/node/src/flags/engine/rollup_boost.rs new file mode 100644 index 0000000000000..8b4cfe899b9ea --- /dev/null +++ b/kona/bin/node/src/flags/engine/rollup_boost.rs @@ -0,0 +1,115 @@ +use crate::flags::engine::flashblocks::FlashblocksFlags; +use rollup_boost::{BlockSelectionPolicy, ExecutionMode}; + +/// Custom block builder flags. +#[derive(Clone, Debug, clap::Args)] +pub struct RollupBoostFlags { + /// Execution mode to start rollup boost with + #[arg( + long, + visible_alias = "rollup-boost.execution-mode", + env = "KONA_NODE_ROLLUP_BOOST_EXECUTION_MODE", + default_value = "disabled" + )] + pub execution_mode: ExecutionMode, + + /// Block selection policy to use for the rollup boost server. + #[arg( + long, + visible_alias = "rollup-boost.block-selection-policy", + env = "KONA_NODE_ROLLUP_BOOST_BLOCK_SELECTION_POLICY" + )] + pub block_selection_policy: Option, + + /// Should we use the l2 client for computing state root + #[arg( + long, + visible_alias = "rollup-boost.external-state-root", + env = "KONA_NODE_ROLLUP_BOOST_EXTERNAL_STATE_ROOT", + default_value = "false" + )] + pub external_state_root: bool, + + /// Allow all engine API calls to builder even when marked as unhealthy + /// This is default true assuming no builder CL set up + #[arg( + long, + visible_alias = "rollup-boost.ignore-unhealthy-builders", + env = "KONA_NODE_ROLLUP_BOOST_IGNORE_UNHEALTHY_BUILDERS", + default_value = "false" + )] + pub ignore_unhealthy_builders: bool, + + /// Duration in seconds between async health checks on the builder + #[arg( + long, + visible_alias = "rollup-boost.health-check-interval", + env = "KONA_NODE_ROLLUP_BOOST_HEALTH_CHECK_INTERVAL", + default_value = "60" + )] + pub health_check_interval: u64, + + /// Max duration in seconds between the unsafe head block of the builder and the current time + #[arg( + long, + visible_alias = "rollup-boost.max-unsafe-interval", + env = "KONA_NODE_ROLLUP_BOOST_MAX_UNSAFE_INTERVAL", + default_value = "10" + )] + pub max_unsafe_interval: u64, + + /// Flashblocks configuration + #[clap(flatten)] + pub flashblocks: FlashblocksFlags, +} + +impl Default for RollupBoostFlags { + fn default() -> Self { + Self { + execution_mode: ExecutionMode::Disabled, + block_selection_policy: None, + external_state_root: false, + ignore_unhealthy_builders: false, + flashblocks: FlashblocksFlags::default(), + health_check_interval: 60, + max_unsafe_interval: 10, + } + } +} + +impl RollupBoostFlags { + /// Converts the rollup boost cli arguments to the rollup boost arguments used by the engine. + pub fn as_rollup_boost_args(self) -> kona_engine::RollupBoostServerArgs { + kona_engine::RollupBoostServerArgs { + initial_execution_mode: self.execution_mode, + block_selection_policy: self.block_selection_policy, + external_state_root: self.external_state_root, + ignore_unhealthy_builders: self.ignore_unhealthy_builders, + flashblocks: self.flashblocks.flashblocks.then_some( + kona_engine::FlashblocksClientArgs { + flashblocks_builder_url: self.flashblocks.flashblocks_builder_url, + flashblocks_host: self.flashblocks.flashblocks_host, + flashblocks_port: self.flashblocks.flashblocks_port, + flashblocks_ws_config: kona_engine::FlashblocksWebsocketConfig { + flashblock_builder_ws_initial_reconnect_ms: self + .flashblocks + .flashblocks_ws_config + .flashblock_builder_ws_initial_reconnect_ms, + flashblock_builder_ws_max_reconnect_ms: self + .flashblocks + .flashblocks_ws_config + .flashblock_builder_ws_max_reconnect_ms, + flashblock_builder_ws_ping_interval_ms: self + .flashblocks + .flashblocks_ws_config + .flashblock_builder_ws_ping_interval_ms, + flashblock_builder_ws_pong_timeout_ms: self + .flashblocks + .flashblocks_ws_config + .flashblock_builder_ws_pong_timeout_ms, + }, + }, + ), + } + } +} diff --git a/kona/bin/node/src/flags/globals.rs b/kona/bin/node/src/flags/globals.rs new file mode 100644 index 0000000000000..6d49247887113 --- /dev/null +++ b/kona/bin/node/src/flags/globals.rs @@ -0,0 +1,114 @@ +//! Global arguments for the CLI. + +use alloy_primitives::Address; +use clap::Parser; +use kona_cli::{LogArgs, MetricsArgs}; +use kona_genesis::RollupConfig; +use kona_registry::OPCHAINS; + +/// Global arguments for the CLI. +#[derive(Parser, Default, Clone, Debug)] +pub struct GlobalArgs { + /// Logging arguments. + #[command(flatten)] + pub log_args: LogArgs, + /// The L2 chain ID to use. + #[arg( + long = "chain", + alias = "l2-chain-id", + short = 'c', + global = true, + default_value = "10", + env = "KONA_NODE_L2_CHAIN_ID", + help = "The L2 chain ID to use" + )] + pub l2_chain_id: alloy_chains::Chain, + /// Embed the override flags globally to provide override values adjacent to the configs. + #[command(flatten)] + pub override_args: super::OverrideArgs, + /// Prometheus CLI arguments. + #[command(flatten)] + pub metrics: MetricsArgs, +} + +impl GlobalArgs { + /// Applies the specified overrides to the given rollup config. + /// + /// Transforms the rollup config and returns the updated config with the overrides applied. + pub fn apply_overrides(&self, config: RollupConfig) -> RollupConfig { + self.override_args.apply(config) + } + + /// Returns the signer [`Address`] from the rollup config for the given l2 chain id. + pub fn genesis_signer(&self) -> anyhow::Result
{ + let id = self.l2_chain_id; + OPCHAINS + .get(&id.id()) + .ok_or(anyhow::anyhow!("No chain config found for chain ID: {id}"))? + .roles + .as_ref() + .ok_or(anyhow::anyhow!("No roles found for chain ID: {id}"))? + .unsafe_block_signer + .ok_or(anyhow::anyhow!("No unsafe block signer found for chain ID: {id}")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use rstest::rstest; + + #[rstest] + #[case::numeric_optimism("10", 10)] + #[case::numeric_ethereum("1", 1)] + #[case::numeric_base("8453", 8453)] + #[case::numeric_unknown("999999", 999999)] + #[case::string_optimism("optimism", 10)] + #[case::string_mainnet("mainnet", 1)] + #[case::string_base("base", 8453)] + fn test_l2_chain_id_parse_valid(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "--l2-chain-id", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::invalid_string("invalid_chain")] + fn test_l2_chain_id_parse_invalid(#[case] invalid_value: &str) { + let result = GlobalArgs::try_parse_from(["test", "--l2-chain-id", invalid_value]); + assert!(result.is_err()); + + // The error should be related to parsing + let err = result.unwrap_err(); + assert!(err.to_string().to_lowercase().contains("invalid")); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_short_flag(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "-c", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_env_var(#[case] env_value: &str, #[case] expected_id: u64) { + unsafe { + std::env::set_var("KONA_NODE_L2_CHAIN_ID", env_value); + } + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + unsafe { + std::env::remove_var("KONA_NODE_L2_CHAIN_ID"); + } + } + + #[test] + fn test_l2_chain_id_default() { + // Test that the default value is chain ID 10 (Optimism) + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), 10); + } +} diff --git a/kona/bin/node/src/flags/metrics.rs b/kona/bin/node/src/flags/metrics.rs new file mode 100644 index 0000000000000..16b3edecfbbdc --- /dev/null +++ b/kona/bin/node/src/flags/metrics.rs @@ -0,0 +1,62 @@ +//! Prometheus metrics CLI args +//! +//! Specifies the available flags for prometheus metric configuration inside CLI + +use crate::metrics::VersionInfo; +use kona_cli::MetricsArgs; + +/// Initializes metrics for a Kona application, including Prometheus and node-specific metrics. +/// Initialize the tracing stack and Prometheus metrics recorder. +/// +/// This function should be called at the beginning of the program. +pub fn init_unified_metrics(args: &MetricsArgs) -> anyhow::Result<()> { + args.init_metrics()?; + if args.enabled { + kona_gossip::Metrics::init(); + kona_disc::Metrics::init(); + kona_engine::Metrics::init(); + kona_node_service::Metrics::init(); + kona_derive::Metrics::init(); + kona_providers_alloy::Metrics::init(); + VersionInfo::from_build().register_version_metrics(); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use std::net::IpAddr; + + /// A mock command that uses the MetricsArgs. + #[derive(Parser, Debug, Clone)] + #[command(about = "Mock command")] + struct MockCommand { + /// Metrics CLI Flags + #[clap(flatten)] + pub metrics: MetricsArgs, + } + + #[test] + fn test_metrics_args_listen_enabled() { + let args = MockCommand::parse_from(["test", "--metrics.enabled"]); + assert!(args.metrics.enabled); + + let args = MockCommand::parse_from(["test"]); + assert!(!args.metrics.enabled); + } + + #[test] + fn test_metrics_args_listen_ip() { + let args = MockCommand::parse_from(["test", "--metrics.addr", "127.0.0.1"]); + let expected: IpAddr = "127.0.0.1".parse().unwrap(); + assert_eq!(args.metrics.addr, expected); + } + + #[test] + fn test_metrics_args_listen_port() { + let args = MockCommand::parse_from(["test", "--metrics.port", "1234"]); + assert_eq!(args.metrics.port, 1234); + } +} diff --git a/kona/bin/node/src/flags/mod.rs b/kona/bin/node/src/flags/mod.rs new file mode 100644 index 0000000000000..07580fa969000 --- /dev/null +++ b/kona/bin/node/src/flags/mod.rs @@ -0,0 +1,28 @@ +//! CLI Flags + +mod globals; +pub use globals::GlobalArgs; + +mod p2p; +pub use p2p::P2PArgs; + +mod rpc; +pub use rpc::RpcArgs; + +mod overrides; +pub use overrides::OverrideArgs; + +mod metrics; +pub use metrics::init_unified_metrics; + +mod sequencer; +pub use sequencer::SequencerArgs; + +mod signer; +pub use signer::{SignerArgs, SignerArgsParseError}; + +mod engine; +pub use engine::{ + BuilderClientArgs, FlashblocksFlags, FlashblocksWebsocketFlags, L1ClientArgs, L2ClientArgs, + RollupBoostFlags, +}; diff --git a/kona/bin/node/src/flags/overrides.rs b/kona/bin/node/src/flags/overrides.rs new file mode 100644 index 0000000000000..15305850b4e8e --- /dev/null +++ b/kona/bin/node/src/flags/overrides.rs @@ -0,0 +1,168 @@ +//! Flags that allow overriding derived values. + +use clap::Parser; +use kona_genesis::RollupConfig; + +/// Override Flags. +#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] +pub struct OverrideArgs { + /// Manually specify the timestamp for the Canyon fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_CANYON")] + pub canyon_override: Option, + /// Manually specify the timestamp for the Delta fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_DELTA")] + pub delta_override: Option, + /// Manually specify the timestamp for the Ecotone fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_ECOTONE")] + pub ecotone_override: Option, + /// Manually specify the timestamp for the Fjord fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_FJORD")] + pub fjord_override: Option, + /// Manually specify the timestamp for the Granite fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_GRANITE")] + pub granite_override: Option, + /// Manually specify the timestamp for the Holocene fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_HOLOCENE")] + pub holocene_override: Option, + /// Manually specify the timestamp for the Isthmus fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_ISTHMUS")] + pub isthmus_override: Option, + /// Manually specify the timestamp for the Jovian fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_JOVIAN")] + pub jovian_override: Option, + /// Manually specify the timestamp for the pectra blob schedule, overriding the bundled + /// setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_PECTRA_BLOB_SCHEDULE")] + pub pectra_blob_schedule_override: Option, + /// Manually specify the timestamp for the Interop fork, overriding the bundled setting. + #[arg(long, env = "KONA_NODE_OVERRIDE_INTEROP")] + pub interop_override: Option, +} + +impl Default for OverrideArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl OverrideArgs { + /// Applies the override args to the given rollup config. + pub fn apply(&self, config: RollupConfig) -> RollupConfig { + let hardforks = kona_genesis::HardForkConfig { + regolith_time: config.hardforks.regolith_time, + canyon_time: self.canyon_override.map(Some).unwrap_or(config.hardforks.canyon_time), + delta_time: self.delta_override.map(Some).unwrap_or(config.hardforks.delta_time), + ecotone_time: self.ecotone_override.map(Some).unwrap_or(config.hardforks.ecotone_time), + fjord_time: self.fjord_override.map(Some).unwrap_or(config.hardforks.fjord_time), + granite_time: self.granite_override.map(Some).unwrap_or(config.hardforks.granite_time), + holocene_time: self + .holocene_override + .map(Some) + .unwrap_or(config.hardforks.holocene_time), + pectra_blob_schedule_time: self + .pectra_blob_schedule_override + .map(Some) + .unwrap_or(config.hardforks.pectra_blob_schedule_time), + isthmus_time: self.isthmus_override.map(Some).unwrap_or(config.hardforks.isthmus_time), + jovian_time: self.jovian_override.map(Some).unwrap_or(config.hardforks.jovian_time), + interop_time: self.interop_override.map(Some).unwrap_or(config.hardforks.interop_time), + }; + RollupConfig { hardforks, ..config } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A mock command that uses the override args. + #[derive(Parser, Debug, Clone)] + #[command(about = "Mock command")] + struct MockCommand { + /// Override flags. + #[clap(flatten)] + pub override_flags: OverrideArgs, + } + + #[test] + fn test_apply_overrides() { + let args = MockCommand::parse_from([ + "test", + "--canyon-override", + "1699981200", + "--delta-override", + "1703203200", + "--ecotone-override", + "1708534800", + "--fjord-override", + "1716998400", + "--granite-override", + "1723478400", + "--holocene-override", + "1732633200", + "--pectra-blob-schedule-override", + "1745000000", + "--isthmus-override", + "1740000000", + "--jovian-override", + "1745000001", + "--interop-override", + "1750000000", + ]); + let config = RollupConfig::default(); + let updated_config = args.override_flags.apply(config); + assert_eq!( + updated_config.hardforks, + kona_genesis::HardForkConfig { + regolith_time: Default::default(), + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: Some(1745000000), + isthmus_time: Some(1740000000), + jovian_time: Some(1745000001), + interop_time: Some(1750000000), + } + ); + } + + #[test] + fn test_apply_default_overrides() { + // Use OP Mainnet rollup config. + let config = kona_registry::ROLLUP_CONFIGS + .get(&10) + .expect("No config found for chain ID 10") + .clone(); + let init_forks = config.hardforks; + let args = MockCommand::parse_from(["test"]); + let updated_config = args.override_flags.apply(config); + assert_eq!(updated_config.hardforks, init_forks); + } + + #[test] + fn test_default_override_flags() { + let args = MockCommand::parse_from(["test"]); + assert_eq!( + args.override_flags, + OverrideArgs { + canyon_override: None, + delta_override: None, + ecotone_override: None, + fjord_override: None, + granite_override: None, + holocene_override: None, + pectra_blob_schedule_override: None, + isthmus_override: None, + jovian_override: None, + interop_override: None, + } + ); + // Sanity check that the default impl matches the expected default values. + assert_eq!(args.override_flags, OverrideArgs::default()); + } +} diff --git a/kona/bin/node/src/flags/p2p.rs b/kona/bin/node/src/flags/p2p.rs new file mode 100644 index 0000000000000..5db9b2d6d3bea --- /dev/null +++ b/kona/bin/node/src/flags/p2p.rs @@ -0,0 +1,630 @@ +//! P2P CLI Flags +//! +//! These are based on p2p flags from the [`op-node`][op-node] CLI. +//! +//! [op-node]: https://github.com/ethereum-optimism/optimism/blob/develop/op-node/flags/p2p_flags.go + +use crate::flags::{GlobalArgs, SignerArgs}; +use alloy_primitives::{B256, b256}; +use alloy_provider::Provider; +use alloy_signer_local::PrivateKeySigner; +use anyhow::Result; +use clap::Parser; +use discv5::enr::k256; +use kona_derive::ChainProvider; +use kona_disc::LocalNode; +use kona_genesis::RollupConfig; +use kona_gossip::GaterConfig; +use kona_node_service::NetworkConfig; +use kona_peers::{BootNode, BootStoreFile, PeerMonitoring, PeerScoreLevel}; +use kona_providers_alloy::AlloyChainProvider; +use libp2p::identity::Keypair; +use std::{ + net::{IpAddr, SocketAddr}, + num::ParseIntError, + path::PathBuf, + str::FromStr, +}; +use tokio::time::Duration; +use url::Url; + +/// P2P CLI Flags +#[derive(Parser, Clone, Debug, PartialEq, Eq)] +pub struct P2PArgs { + /// Disable Discv5 (node discovery). + #[arg(long = "p2p.no-discovery", default_value = "false", env = "KONA_NODE_P2P_NO_DISCOVERY")] + pub no_discovery: bool, + /// Read the hex-encoded 32-byte private key for the peer ID from this txt file. + /// Created if not already exists. Important to persist to keep the same network identity after + /// restarting, maintaining the previous advertised identity. + #[arg(long = "p2p.priv.path", env = "KONA_NODE_P2P_PRIV_PATH")] + pub priv_path: Option, + /// The hex-encoded 32-byte private key for the peer ID. + #[arg(long = "p2p.priv.raw", env = "KONA_NODE_P2P_PRIV_RAW")] + pub private_key: Option, + + /// IP to advertise to external peers from Discv5. + /// Optional argument. Use the `p2p.listen.ip` if not set. + /// + /// Technical note: if this argument is set, the dynamic ENR updates from the discovery layer + /// will be disabled. This is to allow the advertised IP to be static (to use in a network + /// behind a NAT for instance). + #[arg(long = "p2p.advertise.ip", env = "KONA_NODE_P2P_ADVERTISE_IP")] + pub advertise_ip: Option, + /// TCP port to advertise to external peers from the discovery layer. Same as `p2p.listen.tcp` + /// if set to zero. + #[arg(long = "p2p.advertise.tcp", env = "KONA_NODE_P2P_ADVERTISE_TCP_PORT")] + pub advertise_tcp_port: Option, + /// UDP port to advertise to external peers from the discovery layer. + /// Same as `p2p.listen.udp` if set to zero. + #[arg(long = "p2p.advertise.udp", env = "KONA_NODE_P2P_ADVERTISE_UDP_PORT")] + pub advertise_udp_port: Option, + + /// IP to bind LibP2P/Discv5 to. + #[arg(long = "p2p.listen.ip", default_value = "0.0.0.0", env = "KONA_NODE_P2P_LISTEN_IP")] + pub listen_ip: IpAddr, + /// TCP port to bind LibP2P to. Any available system port if set to 0. + #[arg(long = "p2p.listen.tcp", default_value = "9222", env = "KONA_NODE_P2P_LISTEN_TCP_PORT")] + pub listen_tcp_port: u16, + /// UDP port to bind Discv5 to. Same as TCP port if left 0. + #[arg(long = "p2p.listen.udp", default_value = "9223", env = "KONA_NODE_P2P_LISTEN_UDP_PORT")] + pub listen_udp_port: u16, + /// Low-tide peer count. The node actively searches for new peer connections if below this + /// amount. + #[arg(long = "p2p.peers.lo", default_value = "20", env = "KONA_NODE_P2P_PEERS_LO")] + pub peers_lo: u32, + /// High-tide peer count. The node starts pruning peer connections slowly after reaching this + /// number. + #[arg(long = "p2p.peers.hi", default_value = "30", env = "KONA_NODE_P2P_PEERS_HI")] + pub peers_hi: u32, + /// Grace period to keep a newly connected peer around, if it is not misbehaving. + #[arg( + long = "p2p.peers.grace", + default_value = "30", + env = "KONA_NODE_P2P_PEERS_GRACE", + value_parser = |arg: &str| -> Result {Ok(Duration::from_secs(arg.parse()?))} + )] + pub peers_grace: Duration, + /// Configure GossipSub topic stable mesh target count. + /// Aka: The desired outbound degree (numbers of peers to gossip to). + #[arg(long = "p2p.gossip.mesh.d", default_value = "8", env = "KONA_NODE_P2P_GOSSIP_MESH_D")] + pub gossip_mesh_d: usize, + /// Configure GossipSub topic stable mesh low watermark. + /// Aka: The lower bound of outbound degree. + #[arg(long = "p2p.gossip.mesh.lo", default_value = "6", env = "KONA_NODE_P2P_GOSSIP_MESH_DLO")] + pub gossip_mesh_dlo: usize, + /// Configure GossipSub topic stable mesh high watermark. + /// Aka: The upper bound of outbound degree (additional peers will not receive gossip). + #[arg( + long = "p2p.gossip.mesh.dhi", + default_value = "12", + env = "KONA_NODE_P2P_GOSSIP_MESH_DHI" + )] + pub gossip_mesh_dhi: usize, + /// Configure GossipSub gossip target. + /// Aka: The target degree for gossip only (not messaging like p2p.gossip.mesh.d, just + /// announcements of IHAVE). + #[arg( + long = "p2p.gossip.mesh.dlazy", + default_value = "6", + env = "KONA_NODE_P2P_GOSSIP_MESH_DLAZY" + )] + pub gossip_mesh_dlazy: usize, + /// Configure GossipSub to publish messages to all known peers on the topic, outside of the + /// mesh. Also see Dlazy as less aggressive alternative. + #[arg( + long = "p2p.gossip.mesh.floodpublish", + default_value = "false", + env = "KONA_NODE_P2P_GOSSIP_FLOOD_PUBLISH" + )] + pub gossip_flood_publish: bool, + /// Sets the peer scoring strategy for the P2P stack. + /// Can be one of: none or light. + #[arg(long = "p2p.scoring", default_value = "light", env = "KONA_NODE_P2P_SCORING")] + pub scoring: PeerScoreLevel, + + /// Allows to ban peers based on their score. + /// + /// Peers are banned based on a ban threshold (see `p2p.ban.threshold`). + /// If a peer's score is below the threshold, it gets automatically banned. + #[arg(long = "p2p.ban.peers", default_value = "false", env = "KONA_NODE_P2P_BAN_PEERS")] + pub ban_enabled: bool, + + /// The threshold used to ban peers. + /// + /// For peers to be banned, the `p2p.ban.peers` flag must be set to `true`. + /// By default, peers are banned if their score is below -100. This follows the `op-node` default ``. + #[arg(long = "p2p.ban.threshold", default_value = "-100", env = "KONA_NODE_P2P_BAN_THRESHOLD")] + pub ban_threshold: i64, + + /// The duration in minutes to ban a peer for. + /// + /// For peers to be banned, the `p2p.ban.peers` flag must be set to `true`. + /// By default peers are banned for 1 hour. This follows the `op-node` default ``. + #[arg(long = "p2p.ban.duration", default_value = "60", env = "KONA_NODE_P2P_BAN_DURATION")] + pub ban_duration: u64, + + /// The interval in seconds to find peers using the discovery service. + /// Defaults to 5 seconds. + #[arg( + long = "p2p.discovery.interval", + default_value = "5", + env = "KONA_NODE_P2P_DISCOVERY_INTERVAL" + )] + pub discovery_interval: u64, + /// The directory to store the bootstore. + #[arg(long = "p2p.bootstore", env = "KONA_NODE_P2P_BOOTSTORE")] + pub bootstore: Option, + /// Disables the bootstore. + #[arg(long = "p2p.no-bootstore", env = "KONA_NODE_P2P_NO_BOOTSTORE")] + pub disable_bootstore: bool, + /// Peer Redialing threshold is the maximum amount of times to attempt to redial a peer that + /// disconnects. By default, peers are *not* redialed. If set to 0, the peer will be + /// redialed indefinitely. + #[arg(long = "p2p.redial", env = "KONA_NODE_P2P_REDIAL", default_value = "500")] + pub peer_redial: Option, + + /// The duration in minutes of the peer dial period. + /// When the last time a peer was dialed is longer than the dial period, the number of peer + /// dials is reset to 0, allowing the peer to be dialed again. + #[arg(long = "p2p.redial.period", env = "KONA_NODE_P2P_REDIAL_PERIOD", default_value = "60")] + pub redial_period: u64, + + /// An optional list of bootnode ENRs or node records to start the node with. + #[arg(long = "p2p.bootnodes", value_delimiter = ',', env = "KONA_NODE_P2P_BOOTNODES")] + pub bootnodes: Vec, + + /// Optionally enable topic scoring. + /// + /// Topic scoring is a mechanism to score peers based on their behavior in the gossip network. + /// Historically, topic scoring was only enabled for the v1 topic on the OP Stack p2p network + /// in the `op-node`. This was a silent bug, and topic scoring is actively being + /// [phased out of the `op-node`][out]. + /// + /// This flag is only presented for backwards compatibility and debugging purposes. + /// + /// [out]: https://github.com/ethereum-optimism/optimism/pull/15719 + #[arg( + long = "p2p.topic-scoring", + default_value = "false", + env = "KONA_NODE_P2P_TOPIC_SCORING" + )] + pub topic_scoring: bool, + + /// An optional unsafe block signer address. + /// + /// By default, this is fetched from the chain config in the superchain-registry using the + /// specified L2 chain ID. + #[arg(long = "p2p.unsafe.block.signer", env = "KONA_NODE_P2P_UNSAFE_BLOCK_SIGNER")] + pub unsafe_block_signer: Option, + + /// An optional flag to remove random peers from discovery to rotate the peer set. + /// + /// This is the number of seconds to wait before removing a peer from the discovery + /// service. By default, peers are not removed from the discovery service. + /// + /// This is useful for discovering a wider set of peers. + #[arg(long = "p2p.discovery.randomize", env = "KONA_NODE_P2P_DISCOVERY_RANDOMIZE")] + pub discovery_randomize: Option, + + /// Specify optional remote signer configuration. Note that this argument is mutually exclusive + /// with `p2p.sequencer.key` that specifies a local sequencer signer. + #[command(flatten)] + pub signer: SignerArgs, +} + +impl Default for P2PArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl P2PArgs { + fn check_ports_inner(ip_addr: IpAddr, tcp_port: u16, udp_port: u16) -> Result<()> { + if tcp_port == 0 { + return Ok(()); + } + if udp_port == 0 { + return Ok(()); + } + let tcp_socket = std::net::TcpListener::bind((ip_addr, tcp_port)); + let udp_socket = std::net::UdpSocket::bind((ip_addr, udp_port)); + if let Err(e) = tcp_socket { + tracing::error!(target: "p2p::flags", tcp_port, "Error binding TCP socket: {e}"); + anyhow::bail!("Error binding TCP socket on port {tcp_port}: {e}"); + } + if let Err(e) = udp_socket { + tracing::error!(target: "p2p::flags", udp_port, "Error binding UDP socket: {e}"); + anyhow::bail!("Error binding UDP socket on port {udp_port}: {e}"); + } + + Ok(()) + } + + /// Checks if the listen ports are available on the system. + /// + /// If either of the ports are `0`, this check is skipped. + /// + /// ## Errors + /// + /// - If the TCP port is already in use. + /// - If the UDP port is already in use. + pub fn check_ports(&self) -> Result<()> { + Self::check_ports_inner(self.listen_ip, self.listen_tcp_port, self.listen_udp_port) + } + + /// Returns the private key as specified in the raw cli flag or via file path. + pub fn private_key(&self) -> Option { + if let Some(key) = self.private_key { + match PrivateKeySigner::from_bytes(&key) { + Ok(signer) => return Some(signer), + Err(e) => { + tracing::error!(target: "p2p::flags", "Failed to parse private key: {}", e); + return None; + } + } + } + + if let Some(path) = self.priv_path.as_ref() { + if path.exists() { + let contents = std::fs::read_to_string(path).ok()?; + let decoded = B256::from_str(&contents).ok()?; + match PrivateKeySigner::from_bytes(&decoded) { + Ok(signer) => return Some(signer), + Err(e) => { + tracing::error!(target: "p2p::flags", "Failed to parse private key from file: {}", e); + return None; + } + } + } + } + + None + } + + /// Returns the unsafe block signer from the CLI arguments. + pub async fn unsafe_block_signer( + &self, + args: &GlobalArgs, + rollup_config: &RollupConfig, + l1_eth_rpc: Option, + ) -> anyhow::Result { + if let Some(l1_eth_rpc) = l1_eth_rpc { + /// The storage slot that the unsafe block signer address is stored at. + /// Computed as: `bytes32(uint256(keccak256("systemconfig.unsafeblocksigner")) - 1)` + const UNSAFE_BLOCK_SIGNER_ADDRESS_STORAGE_SLOT: B256 = + b256!("0x65a7ed542fb37fe237fdfbdd70b31598523fe5b32879e307bae27a0bd9581c08"); + + let mut provider = AlloyChainProvider::new_http(l1_eth_rpc, 1024); + let latest_block_num = provider.latest_block_number().await?; + let block_info = provider.block_info_by_number(latest_block_num).await?; + + // Fetch the unsafe block signer address from the system config. + let unsafe_block_signer_address = provider + .inner + .get_storage_at( + rollup_config.l1_system_config_address, + UNSAFE_BLOCK_SIGNER_ADDRESS_STORAGE_SLOT.into(), + ) + .hash(block_info.hash) + .await?; + + // Convert the unsafe block signer address to the correct type. + return Ok(alloy_primitives::Address::from_slice( + &unsafe_block_signer_address.to_be_bytes_vec()[12..], + )); + } + + // Otherwise use the genesis signer or the configured unsafe block signer. + args.genesis_signer().or_else(|_| { + self.unsafe_block_signer.ok_or(anyhow::anyhow!("Unsafe block signer not provided")) + }) + } + + /// Constructs kona's P2P network [`NetworkConfig`] from CLI arguments. + /// + /// ## Parameters + /// + /// - [`GlobalArgs`]: required to fetch the genesis unsafe block signer. + /// + /// Errors if the genesis unsafe block signer isn't available for the specified L2 Chain ID. + pub async fn config( + self, + config: &RollupConfig, + args: &GlobalArgs, + l1_rpc: Option, + ) -> anyhow::Result { + // Note: the advertised address is contained in the ENR for external peers from the + // discovery layer to use. + + // Fallback to the listen ip if the advertise ip is not specified + let advertise_ip = self.advertise_ip.unwrap_or(self.listen_ip); + + // If the advertise ip is set, we will disable the dynamic ENR updates. + let static_ip = self.advertise_ip.is_some(); + + // If the advertise tcp port is null, use the listen tcp port + let advertise_tcp_port = match self.advertise_tcp_port { + None => self.listen_tcp_port, + Some(port) => port, + }; + + let advertise_udp_port = match self.advertise_udp_port { + None => self.listen_udp_port, + Some(port) => port, + }; + + let keypair = self.keypair().unwrap_or_else(|_| Keypair::generate_secp256k1()); + let secp256k1_key = keypair.clone().try_into_secp256k1() + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to secp256k1. This is a bug since we only support secp256k1 keys: {e}"))? + .secret().to_bytes(); + let local_node_key = k256::ecdsa::SigningKey::from_bytes(&secp256k1_key.into()) + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to k256 signing key. This is a bug since we only support secp256k1 keys: {e}"))?; + + let discovery_address = + LocalNode::new(local_node_key, advertise_ip, advertise_tcp_port, advertise_udp_port); + let gossip_config = kona_gossip::default_config_builder() + .mesh_n(self.gossip_mesh_d) + .mesh_n_low(self.gossip_mesh_dlo) + .mesh_n_high(self.gossip_mesh_dhi) + .gossip_lazy(self.gossip_mesh_dlazy) + .flood_publish(self.gossip_flood_publish) + .build()?; + + let monitor_peers = self.ban_enabled.then_some(PeerMonitoring { + ban_duration: Duration::from_secs(60 * self.ban_duration), + ban_threshold: self.ban_threshold as f64, + }); + + let discovery_listening_address = SocketAddr::new(self.listen_ip, self.listen_udp_port); + let discovery_config = + NetworkConfig::discv5_config(discovery_listening_address.into(), static_ip); + + let mut gossip_address = libp2p::Multiaddr::from(self.listen_ip); + gossip_address.push(libp2p::multiaddr::Protocol::Tcp(self.listen_tcp_port)); + + let unsafe_block_signer = self.unsafe_block_signer(args, config, l1_rpc).await?; + + let bootstore = if self.disable_bootstore { + None + } else { + Some(self.bootstore.map_or( + BootStoreFile::Default { chain_id: args.l2_chain_id.into() }, + BootStoreFile::Custom, + )) + }; + + let bootnodes = self + .bootnodes + .iter() + .map(|bootnode| BootNode::parse_bootnode(bootnode)) + .collect::>() + .into(); + + Ok(NetworkConfig { + discovery_config, + discovery_interval: Duration::from_secs(self.discovery_interval), + discovery_address, + discovery_randomize: self.discovery_randomize.map(Duration::from_secs), + enr_update: !static_ip, + gossip_address, + keypair, + unsafe_block_signer, + gossip_config, + scoring: self.scoring, + monitor_peers, + bootstore, + topic_scoring: self.topic_scoring, + gater_config: GaterConfig { + peer_redialing: self.peer_redial, + dial_period: Duration::from_secs(60 * self.redial_period), + }, + bootnodes, + rollup_config: config.clone(), + gossip_signer: self.signer.config(args)?, + }) + } + + /// Returns the [`Keypair`] from the cli inputs. + /// + /// If the raw private key is empty and the specified file is empty, + /// this method will generate a new private key and write it out to the file. + /// + /// If neither a file is specified, nor a raw private key input, this method + /// will error. + pub fn keypair(&self) -> Result { + // Attempt the parse the private key if specified. + if let Some(mut private_key) = self.private_key { + return kona_cli::SecretKeyLoader::parse(&mut private_key.0) + .map_err(|e| anyhow::anyhow!(e)); + } + + let Some(ref key_path) = self.priv_path else { + anyhow::bail!("Neither a raw private key nor a private key file path was provided."); + }; + + kona_cli::SecretKeyLoader::load(key_path).map_err(|e| anyhow::anyhow!(e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::b256; + use clap::Parser; + use kona_peers::NodeRecord; + + /// A mock command that uses the P2PArgs. + #[derive(Parser, Debug, Clone)] + #[command(about = "Mock command")] + struct MockCommand { + /// P2P CLI Flags + #[clap(flatten)] + pub p2p: P2PArgs, + } + + #[test] + fn test_p2p_args_keypair_missing_both() { + let args = MockCommand::parse_from(["test"]); + assert!(args.p2p.keypair().is_err()); + } + + #[test] + fn test_p2p_args_keypair_raw_private_key() { + let args = MockCommand::parse_from([ + "test", + "--p2p.priv.raw", + "1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be", + ]); + assert!(args.p2p.keypair().is_ok()); + } + + #[test] + fn test_p2p_args_keypair_from_path() { + // Create a temporary directory. + let dir = std::env::temp_dir(); + let mut source_path = dir.clone(); + assert!(std::env::set_current_dir(dir).is_ok()); + + // Write a private key to a file. + let key = b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + let hex = alloy_primitives::hex::encode(key.0); + source_path.push("test.txt"); + std::fs::write(&source_path, &hex).unwrap(); + + // Parse the keypair from the file. + let args = + MockCommand::parse_from(["test", "--p2p.priv.path", source_path.to_str().unwrap()]); + assert!(args.p2p.keypair().is_ok()); + } + + #[test] + fn test_p2p_args() { + let args = MockCommand::parse_from(["test"]); + assert_eq!(args.p2p, P2PArgs::default()); + } + + #[test] + fn test_p2p_args_randomized() { + let args = MockCommand::parse_from(["test", "--p2p.discovery.randomize", "10"]); + assert_eq!(args.p2p.discovery_randomize, Some(10)); + let args = MockCommand::parse_from(["test"]); + assert_eq!(args.p2p.discovery_randomize, None); + } + + #[test] + fn test_p2p_args_no_discovery() { + let args = MockCommand::parse_from(["test", "--p2p.no-discovery"]); + assert!(args.p2p.no_discovery); + } + + #[test] + fn test_p2p_args_priv_path() { + let args = MockCommand::parse_from(["test", "--p2p.priv.path", "test.txt"]); + assert_eq!(args.p2p.priv_path, Some(PathBuf::from("test.txt"))); + } + + #[test] + fn test_p2p_args_private_key() { + let args = MockCommand::parse_from([ + "test", + "--p2p.priv.raw", + "1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be", + ]); + let key = b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + assert_eq!(args.p2p.private_key, Some(key)); + } + + #[test] + fn test_p2p_args_sequencer_key() { + let args = MockCommand::parse_from([ + "test", + "--p2p.sequencer.key", + "bcc617ea05150ff60490d3c6058630ba94ae9f12a02a87efd291349ca0e54e0a", + ]); + let key = b256!("bcc617ea05150ff60490d3c6058630ba94ae9f12a02a87efd291349ca0e54e0a"); + assert_eq!(args.p2p.signer.sequencer_key, Some(key)); + } + + #[test] + fn test_p2p_args_listen_ip() { + let args = MockCommand::parse_from(["test", "--p2p.listen.ip", "127.0.0.1"]); + let expected: IpAddr = "127.0.0.1".parse().unwrap(); + assert_eq!(args.p2p.listen_ip, expected); + } + + #[test] + fn test_p2p_args_listen_tcp_port() { + let args = MockCommand::parse_from(["test", "--p2p.listen.tcp", "1234"]); + assert_eq!(args.p2p.listen_tcp_port, 1234); + } + + #[test] + fn test_p2p_args_listen_udp_port() { + let args = MockCommand::parse_from(["test", "--p2p.listen.udp", "1234"]); + assert_eq!(args.p2p.listen_udp_port, 1234); + } + + #[test] + fn test_p2p_args_bootnodes() { + let args = MockCommand::parse_from([ + "test", + "--p2p.bootnodes", + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305", + ]); + assert_eq!( + args.p2p.bootnodes, + vec![ + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305", + ] + ); + + // Parse the bootnodes. + let bootnodes = args + .p2p + .bootnodes + .iter() + .map(|bootnode| BootNode::parse_bootnode(bootnode)) + .collect::>(); + + // Otherwise, attempt to use the Node Record format. + let record = NodeRecord::from_str( + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305").unwrap(); + let expected_bootnode = vec![BootNode::from_unsigned(record).unwrap()]; + + assert_eq!(bootnodes, expected_bootnode); + } + + #[test] + fn test_p2p_args_bootnodes_multiple() { + let args = MockCommand::parse_from([ + "test", + "--p2p.bootnodes", + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305,enode://dd751a9ef8912be1bfa7a5e34e2c3785cc5253110bd929f385e07ba7ac19929fb0e0c5d93f77827291f4da02b2232240fbc47ea7ce04c46e333e452f8656b667@34.65.107.0:30305", + ]); + assert_eq!( + args.p2p.bootnodes, + vec![ + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305", + "enode://dd751a9ef8912be1bfa7a5e34e2c3785cc5253110bd929f385e07ba7ac19929fb0e0c5d93f77827291f4da02b2232240fbc47ea7ce04c46e333e452f8656b667@34.65.107.0:30305", + ] + ); + } + + #[test] + fn test_p2p_args_bootnode_enr() { + let args = MockCommand::parse_from([ + "test", + "--p2p.bootnodes", + "enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPSOGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg", + ]); + assert_eq!( + args.p2p.bootnodes, + vec![ + "enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPSOGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg", + ] + ); + } +} diff --git a/kona/bin/node/src/flags/rpc.rs b/kona/bin/node/src/flags/rpc.rs new file mode 100644 index 0000000000000..435d7bb3c0c22 --- /dev/null +++ b/kona/bin/node/src/flags/rpc.rs @@ -0,0 +1,87 @@ +//! Rpc CLI Arguments +//! +//! Flags for configuring the RPC server. + +use clap::Parser; +use kona_rpc::RpcBuilder; +use std::{ + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; + +/// RPC CLI Arguments +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct RpcArgs { + /// Whether to disable the rpc server. + #[arg(long = "rpc.disabled", default_value = "false", env = "KONA_NODE_RPC_DISABLED")] + pub rpc_disabled: bool, + /// Prevent the RPC server from attempting to restart. + #[arg(long = "rpc.no-restart", default_value = "false", env = "KONA_NODE_RPC_NO_RESTART")] + pub no_restart: bool, + /// RPC listening address. + #[arg(long = "rpc.addr", default_value = "0.0.0.0", env = "KONA_NODE_RPC_ADDR")] + pub listen_addr: IpAddr, + /// RPC listening port. + #[arg(long = "port", alias = "rpc.port", default_value = "9545", env = "KONA_NODE_RPC_PORT")] + pub listen_port: u16, + /// Enable the admin API. + #[arg(long = "rpc.enable-admin", env = "KONA_NODE_RPC_ENABLE_ADMIN")] + pub enable_admin: bool, + /// File path used to persist state changes made via the admin API so they persist across + /// restarts. Disabled if not set. + #[arg(long = "rpc.admin-state", env = "KONA_NODE_RPC_ADMIN_STATE")] + pub admin_persistence: Option, + /// Enables websocket rpc server to track block production + #[arg(long = "rpc.ws-enabled", default_value = "false", env = "KONA_NODE_RPC_WS_ENABLED")] + pub ws_enabled: bool, + /// Enables development RPC endpoints for engine state introspection + #[arg(long = "rpc.dev-enabled", default_value = "false", env = "KONA_NODE_RPC_DEV_ENABLED")] + pub dev_enabled: bool, +} + +impl Default for RpcArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl From for Option { + fn from(args: RpcArgs) -> Self { + if args.rpc_disabled { + return None; + } + Some(RpcBuilder { + no_restart: args.no_restart, + socket: SocketAddr::new(args.listen_addr, args.listen_port), + enable_admin: args.enable_admin, + admin_persistence: args.admin_persistence, + ws_enabled: args.ws_enabled, + dev_enabled: args.dev_enabled, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use std::net::Ipv4Addr; + + #[rstest] + #[case::disable_rpc(&["--rpc.disabled"], |args: &mut RpcArgs| { args.rpc_disabled = true; })] + #[case::no_restart(&["--rpc.no-restart"], |args: &mut RpcArgs| { args.no_restart = true; })] + #[case::disable_rpc(&["--rpc.addr", "1.1.1.1"], |args: &mut RpcArgs| { args.listen_addr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); })] + #[case::disable_rpc(&["--port", "8743"], |args: &mut RpcArgs| { args.listen_port = 8743; })] + #[case::disable_rpc_alias(&["--rpc.port", "8743"], |args: &mut RpcArgs| { args.listen_port = 8743; })] + #[case::disable_rpc(&["--rpc.enable-admin"], |args: &mut RpcArgs| { args.enable_admin = true; })] + #[case::disable_rpc(&["--rpc.admin-state", "/"], |args: &mut RpcArgs| { args.admin_persistence = Some(PathBuf::from("/")); })] + fn test_parse_rpc_args(#[case] args: &[&str], #[case] mutate: impl Fn(&mut RpcArgs)) { + let args = [&["kona-node"], args].concat(); + let cli = RpcArgs::parse_from(args); + let mut expected = RpcArgs::default(); + mutate(&mut expected); + assert_eq!(cli, expected); + } +} diff --git a/kona/bin/node/src/flags/sequencer.rs b/kona/bin/node/src/flags/sequencer.rs new file mode 100644 index 0000000000000..6080d852cd87f --- /dev/null +++ b/kona/bin/node/src/flags/sequencer.rs @@ -0,0 +1,78 @@ +//! Sequencer CLI Flags +//! +//! These are based on sequencer flags from the [`op-node`][op-node] CLI. +//! +//! [op-node]: https://github.com/ethereum-optimism/optimism/blob/develop/op-node/flags/flags.go#L233-L265 + +use clap::Parser; +use kona_node_service::SequencerConfig; +use std::{num::ParseIntError, time::Duration}; +use url::Url; + +/// Sequencer CLI Flags +#[derive(Parser, Clone, Debug, PartialEq, Eq)] +pub struct SequencerArgs { + /// Initialize the sequencer in a stopped state. The sequencer can be started using the + /// admin_startSequencer RPC. + #[arg( + long = "sequencer.stopped", + default_value = "false", + env = "KONA_NODE_SEQUENCER_STOPPED" + )] + pub stopped: bool, + + /// Maximum number of L2 blocks for restricting the distance between L2 safe and unsafe. + /// Disabled if 0. + #[arg( + long = "sequencer.max-safe-lag", + default_value = "0", + env = "KONA_NODE_SEQUENCER_MAX_SAFE_LAG" + )] + pub max_safe_lag: u64, + + /// Number of L1 blocks to keep distance from the L1 head as a sequencer for picking an L1 + /// origin. + #[arg(long = "sequencer.l1-confs", default_value = "4", env = "KONA_NODE_SEQUENCER_L1_CONFS")] + pub l1_confs: u64, + + /// Forces the sequencer to strictly prepare the next L1 origin and create empty L2 blocks + #[arg( + long = "sequencer.recover", + default_value = "false", + env = "KONA_NODE_SEQUENCER_RECOVER" + )] + pub recover: bool, + + /// Conductor service rpc endpoint. Providing this value will enable the conductor service. + #[arg(long = "conductor.rpc", env = "KONA_NODE_CONDUCTOR_RPC")] + pub conductor_rpc: Option, + + /// Conductor service rpc timeout. + #[arg( + long = "conductor.rpc.timeout", + default_value = "1", + env = "KONA_NODE_CONDUCTOR_RPC_TIMEOUT", + value_parser = |arg: &str| -> Result {Ok(Duration::from_secs(arg.parse()?))} + )] + pub conductor_rpc_timeout: Duration, +} + +impl Default for SequencerArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl SequencerArgs { + /// Creates a [`SequencerConfig`] from the [`SequencerArgs`]. + pub fn config(&self) -> SequencerConfig { + SequencerConfig { + sequencer_stopped: self.stopped, + sequencer_recovery_mode: self.recover, + conductor_rpc_url: self.conductor_rpc.clone(), + l1_conf_delay: self.l1_confs, + } + } +} diff --git a/kona/bin/node/src/flags/signer.rs b/kona/bin/node/src/flags/signer.rs new file mode 100644 index 0000000000000..c747116bf1070 --- /dev/null +++ b/kona/bin/node/src/flags/signer.rs @@ -0,0 +1,232 @@ +use std::path::PathBuf; + +use alloy_primitives::{Address, B256}; +use alloy_signer::{Signer, k256::ecdsa}; +use alloy_signer_local::PrivateKeySigner; +use clap::{Parser, arg}; +use kona_cli::SecretKeyLoader; +use kona_sources::{BlockSigner, ClientCert, RemoteSigner}; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use std::str::FromStr; +use url::Url; + +use crate::flags::GlobalArgs; + +/// Signer CLI Flags +#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)] +pub struct SignerArgs { + /// An optional flag to specify a local private key for the sequencer to sign unsafe blocks. + #[arg( + long = "p2p.sequencer.key", + env = "KONA_NODE_P2P_SEQUENCER_KEY", + conflicts_with = "endpoint" + )] + pub sequencer_key: Option, + /// An optional path to a file containing the sequencer private key. + /// This is mutually exclusive with `p2p.sequencer.key`. + #[arg( + long = "p2p.sequencer.key.path", + env = "KONA_NODE_P2P_SEQUENCER_KEY_PATH", + conflicts_with = "sequencer_key" + )] + pub sequencer_key_path: Option, + /// The URL of the remote signer endpoint. If not provided, remote signer will be disabled. + /// This is mutually exclusive with `p2p.sequencer.key`. + /// This is required if any of the other signer flags are provided. + #[arg( + long = "p2p.signer.endpoint", + env = "KONA_NODE_P2P_SIGNER_ENDPOINT", + requires = "address" + )] + pub endpoint: Option, + /// The address to sign transactions for. Required if `signer.endpoint` is provided. + #[arg( + long = "p2p.signer.address", + env = "KONA_NODE_P2P_SIGNER_ADDRESS", + requires = "endpoint" + )] + pub address: Option
, + /// Headers to pass to the remote signer. Format `key=value`. Value can contain any character + /// allowed in a HTTP header. When using env vars, split with commas. When using flags one + /// key value pair per flag. + #[arg(long = "p2p.signer.header", env = "KONA_NODE_P2P_SIGNER_HEADER", requires = "endpoint")] + pub header: Vec, + /// An optional path to CA certificates to be used for the remote signer. + #[arg(long = "p2p.signer.tls.ca", env = "KONA_NODE_P2P_SIGNER_TLS_CA", requires = "endpoint")] + pub ca_cert: Option, + /// An optional path to the client certificate for the remote signer. If specified, + /// `signer.tls.key` must also be specified. + #[arg( + long = "p2p.signer.tls.cert", + env = "KONA_NODE_P2P_SIGNER_TLS_CERT", + requires = "key", + requires = "endpoint" + )] + pub cert: Option, + /// An optional path to the client key for the remote signer. If specified, + /// `signer.tls.cert` must also be specified. + #[arg( + long = "p2p.signer.tls.key", + env = "KONA_NODE_P2P_SIGNER_TLS_KEY", + requires = "cert", + requires = "endpoint" + )] + pub key: Option, +} + +/// Errors that can occur when parsing the signer arguments. +#[derive(Debug, thiserror::Error)] +pub enum SignerArgsParseError { + /// The local sequencer key and remote signer cannot be specified at the same time. + #[error("A local sequencer key and a remote signer cannot be specified at the same time.")] + LocalAndRemoteSigner, + /// Both sequencer key and sequencer key path cannot be specified at the same time. + #[error( + "Both sequencer key and sequencer key path cannot be specified at the same time. Use either --p2p.sequencer.key or --p2p.sequencer.key.path." + )] + ConflictingSequencerKeyInputs, + /// The sequencer key is invalid. + #[error("The sequencer key is invalid.")] + SequencerKeyInvalid(#[from] ecdsa::Error), + /// Failed to load sequencer key from file. + #[error("Failed to load sequencer key from file")] + SequencerKeyFileError(#[from] kona_cli::KeypairError), + /// The address is required if `signer.endpoint` is provided. + #[error("The address is required if `signer.endpoint` is provided.")] + AddressRequired, + /// The header is invalid. + #[error("The header is invalid.")] + InvalidHeader, + /// The private key field is required if `signer.tls.cert` is provided. + #[error("The private key field is required if `signer.tls.cert` is provided.")] + KeyRequired, + /// The header name is invalid. + #[error("The header name is invalid.")] + InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName), + /// The header value is invalid. + #[error("The header value is invalid.")] + InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), +} + +impl SignerArgs { + /// Creates a [`BlockSigner`] from the [`SignerArgs`]. + pub fn config(self, args: &GlobalArgs) -> Result, SignerArgsParseError> { + // First, resolve the sequencer key from either raw input or file + let sequencer_key = self.resolve_sequencer_key()?; + + // The sequencer signer obtained from the CLI arguments. + let gossip_signer: Option = match (sequencer_key, self.config_remote()?) { + (Some(_), Some(_)) => return Err(SignerArgsParseError::LocalAndRemoteSigner), + (Some(key), None) => { + let signer: BlockSigner = PrivateKeySigner::from_bytes(&key)? + .with_chain_id(Some(args.l2_chain_id.into())) + .into(); + Some(signer) + } + (None, Some(signer)) => Some(signer.into()), + (None, None) => None, + }; + + Ok(gossip_signer) + } + + /// Resolves the sequencer key from either the raw key or the key file. + fn resolve_sequencer_key(&self) -> Result, SignerArgsParseError> { + match (self.sequencer_key, &self.sequencer_key_path) { + (Some(key), None) => Ok(Some(key)), + (None, Some(path)) => { + let keypair = SecretKeyLoader::load(path)?; + // Extract the private key bytes from the secp256k1 keypair + keypair.try_into_secp256k1().map_or_else( + |_| Err(SignerArgsParseError::SequencerKeyInvalid(ecdsa::Error::new())), + |secp256k1_keypair| { + let private_key_bytes = secp256k1_keypair.secret().to_bytes(); + let key = B256::from_slice(&private_key_bytes); + Ok(Some(key)) + }, + ) + } + (Some(_), Some(_)) => Err(SignerArgsParseError::ConflictingSequencerKeyInputs), + (None, None) => Ok(None), + } + } + + /// Creates a [`RemoteSigner`] from the [`SignerArgs`]. + fn config_remote(self) -> Result, SignerArgsParseError> { + let Some(endpoint) = self.endpoint else { + return Ok(None); + }; + + let Some(address) = self.address else { + return Err(SignerArgsParseError::AddressRequired); + }; + + let headers = self + .header + .iter() + .map(|h| { + let (key, value) = h.split_once('=').ok_or(SignerArgsParseError::InvalidHeader)?; + Ok((HeaderName::from_str(key)?, HeaderValue::from_str(value)?)) + }) + .collect::>()?; + + let client_cert = self + .cert + .clone() + .map(|cert| { + Ok::<_, SignerArgsParseError>(ClientCert { + cert, + key: self.key.clone().ok_or(SignerArgsParseError::KeyRequired)?, + }) + }) + .transpose()?; + + Ok(Some(RemoteSigner { + address, + endpoint, + ca_cert: self.ca_cert.clone(), + client_cert, + headers, + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::b256; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_resolve_sequencer_key_from_file() { + // Create a temporary file with a private key + let mut temp_file = NamedTempFile::new().unwrap(); + let key = b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + let hex = alloy_primitives::hex::encode(key.0); + write!(temp_file, "{hex}").unwrap(); + + let signer_args = SignerArgs { + sequencer_key: None, + sequencer_key_path: Some(temp_file.path().to_path_buf()), + ..Default::default() + }; + + let resolved = signer_args.resolve_sequencer_key().unwrap(); + assert_eq!(resolved, Some(key)); + } + + #[test] + fn test_resolve_sequencer_key_conflicting_inputs() { + let signer_args = SignerArgs { + sequencer_key: Some(b256!( + "bcc617ea05150ff60490d3c6058630ba94ae9f12a02a87efd291349ca0e54e0a" + )), + sequencer_key_path: Some(PathBuf::from("/path/to/key.txt")), + ..Default::default() + }; + + let result = signer_args.resolve_sequencer_key(); + assert!(matches!(result, Err(SignerArgsParseError::ConflictingSequencerKeyInputs))); + } +} diff --git a/kona/bin/node/src/main.rs b/kona/bin/node/src/main.rs new file mode 100644 index 0000000000000..70333e112b8b0 --- /dev/null +++ b/kona/bin/node/src/main.rs @@ -0,0 +1,26 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod cli; +pub mod commands; +pub mod flags; +pub mod metrics; + +pub(crate) mod version; + +fn main() { + use clap::Parser; + + kona_cli::sigsegv_handler::install(); + kona_cli::backtrace::enable(); + + if let Err(err) = cli::Cli::parse().run() { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } +} diff --git a/kona/bin/node/src/metrics/cli_opts.rs b/kona/bin/node/src/metrics/cli_opts.rs new file mode 100644 index 0000000000000..420e280875f3d --- /dev/null +++ b/kona/bin/node/src/metrics/cli_opts.rs @@ -0,0 +1,106 @@ +//! CLI Options + +use kona_genesis::RollupConfig; + +/// Metrics to record various CLI options. +#[derive(Debug, Clone, PartialEq)] +pub struct CliMetrics; + +impl CliMetrics { + /// The identifier for the cli metrics gauge. + pub const IDENTIFIER: &'static str = "kona_cli_opts"; + + /// The P2P Scoring level (disabled if "off"). + pub const P2P_PEER_SCORING_LEVEL: &'static str = "kona_node_peer_scoring_level"; + + /// Whether P2P Topic Scoring is enabled. + pub const P2P_TOPIC_SCORING_ENABLED: &'static str = "kona_node_topic_scoring_enabled"; + + /// Whether P2P banning is enabled. + pub const P2P_BANNING_ENABLED: &'static str = "kona_node_banning_enabled"; + + /// The value for peer redialing. + pub const P2P_PEER_REDIALING: &'static str = "kona_node_peer_redialing"; + + /// Whether flood publishing is enabled. + pub const P2P_FLOOD_PUBLISH: &'static str = "kona_node_flood_publish"; + + /// The interval to send FINDNODE requests through discv5. + pub const P2P_DISCOVERY_INTERVAL: &'static str = "kona_node_discovery_interval"; + + /// The IP to advertise via P2P. + pub const P2P_ADVERTISE_IP: &'static str = "kona_node_advertise_ip"; + + /// The advertised tcp port via P2P. + pub const P2P_ADVERTISE_TCP_PORT: &'static str = "kona_node_advertise_tcp"; + + /// The advertised udp port via P2P. + pub const P2P_ADVERTISE_UDP_PORT: &'static str = "kona_node_advertise_udp"; + + /// The low-tide peer count. + pub const P2P_PEERS_LO: &'static str = "kona_node_peers_lo"; + + /// The high-tide peer count. + pub const P2P_PEERS_HI: &'static str = "kona_node_peers_hi"; + + /// The gossip mesh d option. + pub const P2P_GOSSIP_MESH_D: &'static str = "kona_node_gossip_mesh_d"; + + /// The gossip mesh d lo option. + pub const P2P_GOSSIP_MESH_D_LO: &'static str = "kona_node_gossip_mesh_d_lo"; + + /// The gossip mesh d hi option. + pub const P2P_GOSSIP_MESH_D_HI: &'static str = "kona_node_gossip_mesh_d_hi"; + + /// The gossip mesh d lazy option. + pub const P2P_GOSSIP_MESH_D_LAZY: &'static str = "kona_node_gossip_mesh_d_lazy"; + + /// The duration to ban peers. + pub const P2P_BAN_DURATION: &'static str = "kona_node_ban_duration"; + + /// Hardfork activation times. + pub const HARDFORK_ACTIVATION_TIMES: &'static str = "kona_node_hardforks"; + + /// Top-level rollup config settings. + pub const ROLLUP_CONFIG: &'static str = "kona_node_rollup_config"; +} + +/// Initializes metrics for the rollup config. +pub fn init_rollup_config_metrics(config: &RollupConfig) { + metrics::describe_gauge!( + CliMetrics::ROLLUP_CONFIG, + "Rollup configuration settings for the OP Stack" + ); + metrics::describe_gauge!( + CliMetrics::HARDFORK_ACTIVATION_TIMES, + "Activation times for hardforks in the OP Stack" + ); + + metrics::gauge!( + CliMetrics::ROLLUP_CONFIG, + &[ + ("l1_genesis_block_num", config.genesis.l1.number.to_string()), + ("l2_genesis_block_num", config.genesis.l2.number.to_string()), + ("genesis_l2_time", config.genesis.l2_time.to_string()), + ("l1_chain_id", config.l1_chain_id.to_string()), + ("l2_chain_id", config.l2_chain_id.to_string()), + ("block_time", config.block_time.to_string()), + ("max_sequencer_drift", config.max_sequencer_drift.to_string()), + ("sequencer_window_size", config.seq_window_size.to_string()), + ("channel_timeout", config.channel_timeout.to_string()), + ("granite_channel_timeout", config.granite_channel_timeout.to_string()), + ("batch_inbox_address", config.batch_inbox_address.to_string()), + ("deposit_contract_address", config.deposit_contract_address.to_string()), + ("l1_system_config_address", config.l1_system_config_address.to_string()), + ("protocol_versions_address", config.protocol_versions_address.to_string()), + ] + ) + .set(1); + + for (fork_name, activation_time) in config.hardforks.iter() { + // Set the value of the metric for the given hardfork, using `-1` as a signal that the + // fork is not scheduled. + metrics::gauge!(CliMetrics::HARDFORK_ACTIVATION_TIMES, "fork" => fork_name) + .set(activation_time.map(|t| t as f64).unwrap_or(-1f64)); + } +} diff --git a/kona/bin/node/src/metrics/mod.rs b/kona/bin/node/src/metrics/mod.rs new file mode 100644 index 0000000000000..2d3cbffe9a35a --- /dev/null +++ b/kona/bin/node/src/metrics/mod.rs @@ -0,0 +1,7 @@ +//! Global metrics for `kona-node` + +mod cli_opts; +pub use cli_opts::{CliMetrics, init_rollup_config_metrics}; + +mod version; +pub use version::VersionInfo; diff --git a/kona/bin/node/src/metrics/version.rs b/kona/bin/node/src/metrics/version.rs new file mode 100644 index 0000000000000..b0fdc93029ccc --- /dev/null +++ b/kona/bin/node/src/metrics/version.rs @@ -0,0 +1,59 @@ +//! [`VersionInfo`] metrics +//! +//! Derived from [`reth-node-core`'s type][reth-version-info] +//! +//! [reth-version-info]: https://github.com/paradigmxyz/reth/blob/805fb1012cd1601c3b4fe9e8ca2d97c96f61355b/crates/node/metrics/src/version.rs#L6 + +use metrics::gauge; + +/// Contains version information for the application and allows for exposing the contained +/// information as a prometheus metric. +#[derive(Debug, Clone)] +pub struct VersionInfo { + /// The version of the application. + pub version: &'static str, + /// The build timestamp of the application. + pub build_timestamp: &'static str, + /// The cargo features enabled for the build. + pub cargo_features: &'static str, + /// The Git SHA of the build. + pub git_sha: &'static str, + /// The target triple for the build. + pub target_triple: &'static str, + /// The build profile (e.g., debug or release). + pub build_profile: &'static str, +} + +impl VersionInfo { + /// Creates a new instance of [`VersionInfo`] from the constants defined in [`crate::version`] + /// at compile time. + pub const fn from_build() -> Self { + Self { + version: crate::version::CARGO_PKG_VERSION, + build_timestamp: crate::version::VERGEN_BUILD_TIMESTAMP, + cargo_features: crate::version::VERGEN_CARGO_FEATURES, + git_sha: crate::version::VERGEN_GIT_SHA, + target_triple: crate::version::VERGEN_CARGO_TARGET_TRIPLE, + build_profile: crate::version::BUILD_PROFILE_NAME, + } + } + + /// Exposes kona-node's version information over prometheus. + pub fn register_version_metrics(&self) { + // If no features are enabled, the string will be empty, and the metric will not be + // reported. Report "none" if the string is empty. + let features = if self.cargo_features.is_empty() { "none" } else { self.cargo_features }; + + let labels: [(&str, &str); 6] = [ + ("version", self.version), + ("build_timestamp", self.build_timestamp), + ("cargo_features", features), + ("git_sha", self.git_sha), + ("target_triple", self.target_triple), + ("build_profile", self.build_profile), + ]; + + let gauge = gauge!("kona_node_info", &labels); + gauge.set(1); + } +} diff --git a/kona/bin/node/src/version.rs b/kona/bin/node/src/version.rs new file mode 100644 index 0000000000000..484ee9d2f7e65 --- /dev/null +++ b/kona/bin/node/src/version.rs @@ -0,0 +1,35 @@ +//! Version information for kona-node. + +/// The latest version from Cargo.toml. +pub(crate) const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The 8 character short SHA of the latest commit. +pub(crate) const VERGEN_GIT_SHA: &str = env!("VERGEN_GIT_SHA_SHORT"); + +/// The build timestamp. +pub(crate) const VERGEN_BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP"); + +/// The target triple. +pub(crate) const VERGEN_CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE"); + +/// The build features. +pub(crate) const VERGEN_CARGO_FEATURES: &str = env!("VERGEN_CARGO_FEATURES"); + +/// The short version information for kona-node. +pub(crate) const SHORT_VERSION: &str = env!("KONA_NODE_SHORT_VERSION"); + +/// The long version information for kona-node. +pub(crate) const LONG_VERSION: &str = concat!( + env!("KONA_NODE_LONG_VERSION_0"), + "\n", + env!("KONA_NODE_LONG_VERSION_1"), + "\n", + env!("KONA_NODE_LONG_VERSION_2"), + "\n", + env!("KONA_NODE_LONG_VERSION_3"), + "\n", + env!("KONA_NODE_LONG_VERSION_4") +); + +/// The build profile name. +pub(crate) const BUILD_PROFILE_NAME: &str = env!("KONA_NODE_BUILD_PROFILE"); diff --git a/kona/bin/supervisor/Architecture.md b/kona/bin/supervisor/Architecture.md new file mode 100644 index 0000000000000..379050dc1d52c --- /dev/null +++ b/kona/bin/supervisor/Architecture.md @@ -0,0 +1,127 @@ +# Kona Supervisor System Architecture + +## System Module Diagram + +```mermaid +flowchart TD + SVC["Supervisor Service"] + DBF["ChainDbFactory"] + L1W["L1Watcher"] + RH["ReorgHandler"] + SRPC["SupervisorRpcActor"] + SRPCS["Supervisor RPC Server"] + + subgraph PerChain["Modules Per Chain"] + subgraph ManagedNodeGroup["Managed Node Modules"] + MNODE["ManagedNode"] + MN["ManagedNodeActor"] + end + subgraph ChainProcessorGroup["Chain Processor Modules"] + CPA["ChainProcessorActor"] + CP["ChainProcessor"] + end + CSCJ_SAFE["CrossSafetyCheckerJob (Safe)"] + CSCJ_UNSAFE["CrossSafetyCheckerJob (Unsafe)"] + CHAN["ChainEvent Channel"] + CMDCHAN["ManagedNodeCommand Channel"] + end + + SVC --> DBF + SVC --> L1W + SVC --> SRPC + SRPC --> SRPCS + SVC --> PerChain + SRPCS --> DBF + + L1W --> RH + + CPA --> CP + CP --> MNODE + CP --> DBF + + MN --> MNODE + + CSCJ_SAFE --> DBF + CSCJ_UNSAFE --> DBF + + %% Producers send events to the ChainEvent channel + MNODE --> CHAN + L1W --> CHAN + CSCJ_SAFE --> CHAN + CSCJ_UNSAFE --> CHAN + + %% ChainEvent channel delivers events to ChainProcessorActor + CHAN --> CPA + + %% ChainProcessorActor sends ManagedNodeCommand + CP --> CMDCHAN + + %% Reorg Handler sends ManagedNodeCommand + RH --> CMDCHAN + + %% ManagedNodeCommand channel delivers commands to ManagedNodeActor + CMDCHAN --> MN +``` + +--- + +## Module Descriptions + +### **Global Modules** + +- **Supervisor Service (`SVC`)** + The main orchestrator. Initializes, manages, and coordinates all subsystems and per-chain modules. + +- **ChainDbFactory (`DBF`)** + Responsible for creating and managing per-chain databases, providing storage and state management. + +- **L1Watcher (`L1W`)** + Monitors the Layer 1 chain for finalized and new blocks, detects reorgs and broadcasts relevant events. + +- **ReorgHandler (`RH`)** + Handles chain reorganizations detected by the L1Watcher, ensuring system consistency. + +- **SupervisorRpcActor (`SRPC`)** + Exposes the supervisor’s API endpoints for external control and monitoring. + +- **Supervisor RPC Server (`SRPCS`)** + The actual RPC server implementation, serving requests and interacting with the supervisor service and database. + +--- + +### **Per-Chain Modules** + +- **Managed Node Modules** + - **ManagedNode (`MNODE`)** + Represents a node for a specific chain, handling chain-specific logic and state. + - **ManagedNodeActor (`MN`)** + The actor responsible for executing commands and managing the lifecycle of the ManagedNode. + +- **Chain Processor modules** + - **ChainProcessorActor (`CPA`)** + Listens for chain events and delegates processing to the ChainProcessor. + - **ChainProcessor (`CP`)** + Processes chain events, interacts with the ManagedNode and ChainDbFactory, and issues commands. + +- **CrossSafetyCheckerJob (Safe/Unsafe) (`CSCJ_SAFE`, `CSCJ_UNSAFE`)** + Periodically promotes safety levels for each chain, ensuring cross-chain consistency. + +- **ChainEvent Channel (`CHAN`)** + Event bus for chain events. Receives events from producers (ManagedNode, L1Watcher, CrossSafetyCheckerJobs) and delivers them to the ChainProcessorActor. + +- **ManagedNodeCommand Channel (`CMDCHAN`)** + Command bus for ManagedNode commands. Receives commands from ChainProcessor and ReorgHandler, and delivers them to ManagedNodeActor. + +--- + +## **Event and Command Flow** + +- **Chain events** are produced by ManagedNode, L1Watcher, and CrossSafetyCheckerJobs, sent to the ChainEvent Channel, and consumed by ChainProcessorActor. +- **ManagedNode commands** are produced by ChainProcessor and ReorgHandler, sent to the ManagedNodeCommand Channel, and consumed by ManagedNodeActor. + +--- + +## **Summary** + +This architecture provides a modular, event-driven system for managing multiple chains, ensuring robust coordination, safety, and extensibility. +Each module is clearly separated, with explicit channels for event and command communication, making the system easy to reason about and maintain. diff --git a/kona/bin/supervisor/Cargo.toml b/kona/bin/supervisor/Cargo.toml new file mode 100644 index 0000000000000..124c650dbca80 --- /dev/null +++ b/kona/bin/supervisor/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "kona-supervisor" +version = "0.1.0" +description = "Kona Supervisor" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +# Workspace +kona-supervisor-service.workspace = true +kona-supervisor-core.workspace = true +kona-cli.workspace = true +kona-interop.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true + +alloy-network.workspace = true +alloy-provider.workspace = true +alloy-rpc-types-engine.workspace = true + +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = [ "full", "macros"] } +anyhow = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } +tracing = { workspace = true } +serde.workspace = true +serde_json.workspace = true +glob.workspace = true +reqwest.workspace = true +metrics.workspace = true + +[dev-dependencies] +tempfile.workspace = true +kona-registry.workspace = true + +[build-dependencies] +vergen = { workspace = true, features = ["build", "cargo", "emit_and_set"] } +vergen-git2.workspace = true + +[lints] +workspace = true diff --git a/kona/bin/supervisor/README.md b/kona/bin/supervisor/README.md new file mode 100644 index 0000000000000..b091b06078767 --- /dev/null +++ b/kona/bin/supervisor/README.md @@ -0,0 +1,51 @@ +# `kona-supervisor` + +A supervisor implementation for the OP stack built in rust. + +## Installation + +Build from source + +``` +cargo build --profile release-perf --bin kona-supervisor +``` + +### Usage + +Run the `kona-supervisor` using the following command + +```bash +kona-supervisor \ + --metrics.enabled \ + --metrics.port 9090 \ + --metrics.addr 127.0.0.1 \ + --l1-rpc http://localhost:8545 \ + --l2-consensus.nodes http://node1:8551,http://node2:8551 \ + --l2-consensus.jwt-secret secret1,secret2 \ + --datadir /supervisor_data \ + --dependency-set /path/to/deps.json \ + --rollup-config-paths /configs/rollup-*.json +``` + +### Configuration via Environment Variables + +Many configuration options can be set via environment variables: + +- `L1_RPC` - L1 RPC source +- `L2_CONSENSUS_NODES` - L2 consensus rollup node RPC addresses. +- `L2_CONSENSUS_JWT_SECRET` - JWT secrets for L2 consensus nodes. +- `DEPENDENCY_SET` - Path to the dependency-set JSON config file. +- `DATADIR` - Directory to store supervisor data. +- `ROLLUP_CONFIG_PATHS` - Path pattern to op-node rollup.json configs to load as a rollup config set. + +### Help and Documentation + +Use the `--help` flag to see all available options: + +``` +kona-supervisor --help +``` + +## Advanced Configuration + +Coming soon diff --git a/kona/bin/supervisor/build.rs b/kona/bin/supervisor/build.rs new file mode 100644 index 0000000000000..4391459be7dbb --- /dev/null +++ b/kona/bin/supervisor/build.rs @@ -0,0 +1,37 @@ +//! Used for generating build information for the supervisor service. + +use std::{env, error::Error}; +use vergen::{BuildBuilder, CargoBuilder, Emitter}; +use vergen_git2::Git2Builder; + +fn main() -> Result<(), Box> { + let mut emitter = Emitter::default(); + + let build_builder = BuildBuilder::default().build_timestamp(true).build()?; + + // Add build timestamp information. + emitter.add_instructions(&build_builder)?; + + let cargo_builder = CargoBuilder::default().features(true).target_triple(true).build()?; + + // Add cargo features and target information. + emitter.add_instructions(&cargo_builder)?; + + let git_builder = + Git2Builder::default().describe(false, true, None).dirty(true).sha(false).build()?; + + // Add commit information. + emitter.add_instructions(&git_builder)?; + + emitter.emit_and_set()?; + + // Need to print in order to set the environment variables. + let sha = env::var("VERGEN_GIT_SHA")?; + println!("cargo:rustc-env=VERGEN_GIT_SHA_SHORT={}", &sha[..8]); + + let out_dir = env::var("OUT_DIR").unwrap(); + let profile = out_dir.rsplit(std::path::MAIN_SEPARATOR).nth(3).unwrap(); + println!("cargo:rustc-env=KONA_SUPERVISOR_BUILD_PROFILE={profile}"); + + Ok(()) +} diff --git a/kona/bin/supervisor/src/cli.rs b/kona/bin/supervisor/src/cli.rs new file mode 100644 index 0000000000000..326d19acb8667 --- /dev/null +++ b/kona/bin/supervisor/src/cli.rs @@ -0,0 +1,80 @@ +//! Contains the supervisor CLI. + +use crate::{flags::SupervisorArgs, metrics::VersionInfo}; +use anyhow::Result; +use clap::Parser; +use kona_cli::{LogArgs, LogConfig, MetricsArgs, cli_styles}; +use kona_supervisor_service::Service; +use tracing::{error, info}; + +/// CLI for the Rust implementation of the OP Supervisor. +#[derive(Parser, Debug)] +#[command(name = "op-supervisor", about = "Rust implementation of the OP Supervisor", styles = cli_styles())] +pub struct Cli { + /// Global args + #[command(flatten)] + pub global: LogArgs, + + /// Prometheus metrics args + #[command(flatten)] + pub metrics: MetricsArgs, + + /// Supervisor args + #[command(flatten)] + pub supervisor: SupervisorArgs, +} + +impl Cli { + /// Runs the CLI. + pub fn run(self) -> Result<()> { + self.metrics.init_metrics()?; + // Register build metrics + VersionInfo::from_build().register_version_metrics(); + + self.init_logs(&self.global)?; + + Self::run_until_ctrl_c(async move { + let config = self.supervisor.init_config().await?; + let mut service = Service::new(config); + + tokio::select! { + res = service.run() => { + if let Err(err) = res { + error!(target: "supervisor", %err, "Error running supervisor service"); + } + } + _ = tokio::signal::ctrl_c() => { + info!(target: "supervisor", "Ctrl+C received, initiating service shutdown..."); + } + } + + service.shutdown().await?; // Call shutdown on the service instance itself + info!(target: "supervisor", "Supervisor service shut down gracefully."); + Ok(()) + }) + } + + /// Run until ctrl-c is pressed. + pub fn run_until_ctrl_c(fut: F) -> Result<()> + where + F: std::future::Future>, + { + let rt = Self::tokio_runtime().map_err(|e| anyhow::anyhow!(e))?; + rt.block_on(fut) + } + + /// Creates a new default tokio multi-thread [`Runtime`](tokio::runtime::Runtime) with all + /// features enabled + pub fn tokio_runtime() -> Result { + tokio::runtime::Builder::new_multi_thread().enable_all().build() + } + + /// Initializes the telemetry stack and Prometheus metrics recorder. + pub fn init_logs(&self, args: &LogArgs) -> anyhow::Result<()> { + // Filter out discovery warnings since they're very very noisy. + let filter = tracing_subscriber::EnvFilter::from_default_env(); + + LogConfig::new(args.clone()).init_tracing_subscriber(Some(filter))?; + Ok(()) + } +} diff --git a/kona/bin/supervisor/src/flags/mod.rs b/kona/bin/supervisor/src/flags/mod.rs new file mode 100644 index 0000000000000..a27930e643065 --- /dev/null +++ b/kona/bin/supervisor/src/flags/mod.rs @@ -0,0 +1,4 @@ +//! CLI Flags + +mod supervisor; +pub use supervisor::SupervisorArgs; diff --git a/kona/bin/supervisor/src/flags/supervisor.rs b/kona/bin/supervisor/src/flags/supervisor.rs new file mode 100644 index 0000000000000..2f0d20eb8d789 --- /dev/null +++ b/kona/bin/supervisor/src/flags/supervisor.rs @@ -0,0 +1,777 @@ +use alloy_network::Ethereum; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_types_engine::JwtSecret; +use anyhow::{Context as _, Ok, Result, anyhow}; +use clap::Args; +use glob::glob; +use kona_genesis::RollupConfig; +use kona_interop::DependencySet; +use kona_protocol::BlockInfo; +use kona_supervisor_core::{ + config::{Config, RollupConfigSet}, + syncnode::ClientConfig, +}; +use serde::de::DeserializeOwned; +use std::{ + net::{IpAddr, SocketAddr}, + path::{Path, PathBuf}, +}; +use tokio::{fs::File, io::AsyncReadExt}; + +/// Supervisor configuration arguments. +#[derive(Args, Debug)] +pub struct SupervisorArgs { + /// L1 RPC source + #[arg(long, env = "L1_RPC")] + pub l1_rpc: String, + + /// L2 consensus rollup node RPC addresses. + #[arg(long = "l2-consensus.nodes", env = "L2_CONSENSUS_NODES", value_delimiter = ',')] + pub l2_consensus_nodes: Vec, + + /// JWT secrets for L2 consensus nodes. + #[arg( + long = "l2-consensus.jwt-secret", + env = "L2_CONSENSUS_JWT_SECRET", + value_delimiter = ',' + )] + pub l2_consensus_jwt_secret: Vec, + + /// Directory to store supervisor data. + #[arg(long, env = "DATADIR")] + pub datadir: PathBuf, + + /// Optional endpoint to sync data from another supervisor. + #[arg(long = "datadir.sync-endpoint", env = "DATADIR_SYNC_ENDPOINT")] + pub datadir_sync_endpoint: Option, + + /// Path to the dependency-set JSON config file. + #[arg(long = "dependency-set", env = "DEPENDENCY_SET")] + pub dependency_set: PathBuf, + + /// Path pattern to op-node rollup.json configs to load as a rollup config set. + /// The pattern should use the glob syntax, e.g. '/configs/rollup-*.json' + /// When using this flag, the L1 timestamps are loaded from the provided L1 RPC. + #[arg(long = "rollup-config-paths", env = "ROLLUP_CONFIG_PATHS")] + pub rollup_config_paths: PathBuf, + + /// IP address for the Supervisor RPC server to listen on. + #[arg(long = "rpc.addr", env = "RPC_ADDR", default_value = "0.0.0.0")] + pub rpc_address: IpAddr, + + /// Port for the Supervisor RPC server to listen on. + #[arg(long = "rpc.port", env = "RPC_PORT", default_value_t = 8545)] + pub rpc_port: u16, + + /// Enable the Supervisor Admin API. + #[arg(long = "rpc.enable-admin", env = "RPC_ENABLE_ADMIN", default_value_t = false)] + pub enable_admin_api: bool, +} + +impl SupervisorArgs { + async fn read_json_file(path: &Path) -> Result { + let mut file = File::open(path) + .await + .with_context(|| format!("Failed to open '{}'", path.display()))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .await + .with_context(|| format!("Failed to read '{}'", path.display()))?; + let value = serde_json::from_str(&contents) + .with_context(|| format!("Failed to parse JSON from '{}'", path.display()))?; + Ok(value) + } + + /// initialise and return the [`DependencySet`]. + pub async fn init_dependency_set(&self) -> Result { + Self::read_json_file(&self.dependency_set).await + } + + async fn get_rollup_configs(&self) -> Result> { + let pattern = self + .rollup_config_paths + .to_str() + .ok_or_else(|| anyhow::anyhow!("rollup_config_paths contains invalid UTF-8"))?; + if pattern.is_empty() { + return Err(anyhow::anyhow!("rollup_config_paths pattern is empty")); + } + + let mut rollup_configs = Vec::new(); + for entry in glob(pattern)? { + let path = entry?; + let rollup_config = Self::read_json_file(&path).await?; + rollup_configs.push(rollup_config); + } + Ok(rollup_configs) + } + + /// Initialise and return the rollup config set. + pub async fn init_rollup_config_set(&self) -> Result { + let l1_url = self + .l1_rpc + .parse() + .with_context(|| format!("Failed to parse L1 RPC URL '{}'", &self.l1_rpc))?; + let provider = RootProvider::::new_http(l1_url); + + let mut rollup_config_set = RollupConfigSet::default(); + + // Use the helper to get all configs + let rollup_configs = self.get_rollup_configs().await?; + + for rollup_config in rollup_configs { + let chain_id = rollup_config.l2_chain_id; + + let l1_genesis = provider + .get_block_by_hash(rollup_config.genesis.l1.hash) + .await + .with_context(|| { + format!( + "Failed to fetch L1 genesis block for hash {}", + rollup_config.genesis.l1.hash + ) + })?; + + let l1_genesis = l1_genesis.ok_or_else(|| { + anyhow::anyhow!( + "L1 genesis block not found for hash {}", + rollup_config.genesis.l1.hash + ) + })?; + + rollup_config_set + .add_from_rollup_config( + chain_id.id(), + rollup_config, + BlockInfo::new( + l1_genesis.header.hash, + l1_genesis.header.number, + l1_genesis.header.parent_hash, + l1_genesis.header.timestamp, + ), + ) + .map_err(|err| anyhow!(err))?; + } + + Ok(rollup_config_set) + } + + /// initialise and return the managed nodes configuration. + pub fn init_managed_nodes_config(&self) -> Result> { + let nodes: Vec = self + .l2_consensus_nodes + .iter() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + if nodes.is_empty() { + return Ok(Vec::new()); + } + + let mut managed_nodes = Vec::with_capacity(nodes.len()); + let default_secret_path = self + .l2_consensus_jwt_secret + .first() + .ok_or_else(|| anyhow::anyhow!("No JWT secrets provided"))?; + for (i, rpc_url) in nodes.iter().enumerate() { + let secret_path = self.l2_consensus_jwt_secret.get(i).unwrap_or(default_secret_path); + + let secret = std::fs::read_to_string(secret_path).map_err(|err| { + anyhow::anyhow!("Failed to read JWT secret from '{secret_path}': {err}") + })?; + + let jwt_secret = JwtSecret::from_hex(secret).map_err(|err| { + anyhow::anyhow!("Failed to parse JWT secret from '{secret_path}': {err}") + })?; + + managed_nodes.push(ClientConfig { url: rpc_url.clone(), jwt_secret }); + } + Ok(managed_nodes) + } + + /// initialise and return the Supervisor [`Config`]. + pub async fn init_config(&self) -> Result { + let dependency_set = self.init_dependency_set().await?; + let rollup_config_set = self.init_rollup_config_set().await?; + + let rpc_addr = SocketAddr::new(self.rpc_address, self.rpc_port); + let managed_nodes_config = self.init_managed_nodes_config()?; + + Ok(Config { + l1_rpc: self.l1_rpc.clone(), + l2_consensus_nodes_config: managed_nodes_config, + datadir: self.datadir.clone(), + rpc_addr, + enable_admin_api: self.enable_admin_api, + dependency_set, + rollup_config_set, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use kona_interop::{ChainDependency, DependencySet}; + use kona_registry::HashMap; + use std::{fs::File, io::Write, net::Ipv4Addr}; + use tempfile::{NamedTempFile, tempdir}; + + // Helper struct to parse SupervisorArgs within a test CLI structure + #[derive(Parser, Debug)] + struct TestCli { + #[command(flatten)] + supervisor: SupervisorArgs, + } + + #[test] + fn test_supervisor_args_from_cli_required_only() { + let cli = TestCli::parse_from([ + "test_app", + "--l1-rpc", + "http://localhost:8545", + "--l2-consensus.nodes", + "http://node1:8551,http://node2:8551", + "--l2-consensus.jwt-secret", + "secret1,secret2", + "--datadir", + "/tmp/supervisor_data", + "--dependency-set", + "/path/to/deps.json", + "--rollup-config-paths", + "/configs/rollup-*.json", + ]); + + assert_eq!(cli.supervisor.l1_rpc, "http://localhost:8545"); + assert_eq!( + cli.supervisor.l2_consensus_nodes, + vec!["http://node1:8551".to_string(), "http://node2:8551".to_string()] + ); + assert_eq!( + cli.supervisor.l2_consensus_jwt_secret, + vec!["secret1".to_string(), "secret2".to_string()] + ); + assert_eq!(cli.supervisor.datadir, PathBuf::from("/tmp/supervisor_data")); + assert_eq!(cli.supervisor.datadir_sync_endpoint, None); + assert_eq!(cli.supervisor.dependency_set, PathBuf::from("/path/to/deps.json")); + assert_eq!(cli.supervisor.rollup_config_paths, PathBuf::from("/configs/rollup-*.json")); + assert_eq!(cli.supervisor.rpc_address, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); + assert_eq!(cli.supervisor.rpc_port, 8545); + } + + #[test] + fn test_supervisor_args_from_cli_all_args() { + let cli = TestCli::parse_from([ + "test_app", + "--l1-rpc", + "http://l1.example.com", + "--l2-consensus.nodes", + "http://consensus1", + "--l2-consensus.jwt-secret", + "jwt_secret_value", + "--datadir", + "/data", + "--datadir.sync-endpoint", + "http://sync.example.com", + "--dependency-set", + "/path/to/deps.json", + "--rollup-config-paths", + "/configs/rollup-*.json", + "--rpc.addr", + "192.168.1.100", + "--rpc.port", + "9001", + ]); + + assert_eq!(cli.supervisor.l1_rpc, "http://l1.example.com"); + assert_eq!(cli.supervisor.l2_consensus_nodes, vec!["http://consensus1".to_string()]); + assert_eq!(cli.supervisor.l2_consensus_jwt_secret, vec!["jwt_secret_value".to_string()]); + assert_eq!(cli.supervisor.datadir, PathBuf::from("/data")); + assert_eq!( + cli.supervisor.datadir_sync_endpoint, + Some("http://sync.example.com".to_string()) + ); + assert_eq!(cli.supervisor.dependency_set, PathBuf::from("/path/to/deps.json")); + assert_eq!(cli.supervisor.rollup_config_paths, PathBuf::from("/configs/rollup-*.json")); + assert_eq!(cli.supervisor.rpc_address, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))); + assert_eq!(cli.supervisor.rpc_port, 9001); + } + + #[tokio::test] + async fn test_init_dependency_set_success() -> anyhow::Result<()> { + let mut temp_file = NamedTempFile::new()?; + let json_content = r#" + { + "dependencies": { + "1": { + "chainIndex": 10, + "activationTime": 1678886400, + "historyMinTime": 1609459200 + }, + "2": { + "chainIndex": 20, + "activationTime": 1678886401, + "historyMinTime": 1609459201 + } + }, + "overrideMessageExpiryWindow": 3600 + } + "#; + temp_file.write_all(json_content.as_bytes())?; + + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: temp_file.path().to_path_buf(), + rollup_config_paths: PathBuf::from("dummy/rollup_config_*.json"), + rpc_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + rpc_port: 8545, + enable_admin_api: false, + }; + + let result = args.init_dependency_set().await; + assert!(result.is_ok(), "init_dependency_set should succeed"); + + let loaded_depset = result.unwrap(); + let mut expected_dependencies = HashMap::default(); + expected_dependencies.insert(1, ChainDependency {}); + expected_dependencies.insert(2, ChainDependency {}); + + let expected_depset = DependencySet { + dependencies: expected_dependencies, + override_message_expiry_window: Some(3600), + }; + + assert_eq!(loaded_depset, expected_depset); + Ok(()) + } + + #[tokio::test] + async fn test_init_dependency_set_file_not_found() -> anyhow::Result<()> { + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("/path/to/non_existent_file.json"), + rollup_config_paths: PathBuf::from("dummy/rollup_config_*.json"), + rpc_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + rpc_port: 8545, + enable_admin_api: false, + }; + + let result = args.init_dependency_set().await; + let err = result.expect_err("init_dependency_set should have failed due to file not found"); + let io_error = err.downcast_ref::(); + assert!(io_error.is_some(), "Error should be an std::io::Error, but was: {err:?}"); + assert_eq!(io_error.unwrap().kind(), std::io::ErrorKind::NotFound); + Ok(()) + } + + #[tokio::test] + async fn test_init_dependency_set_invalid_json() -> anyhow::Result<()> { + let mut temp_file = NamedTempFile::new()?; + temp_file.write_all(b"{ \"invalid_json\": ")?; // Malformed JSON + + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: temp_file.path().to_path_buf(), + rollup_config_paths: PathBuf::from("dummy/rollup_config_*.json"), + rpc_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + rpc_port: 8545, + enable_admin_api: false, + }; + + let result = args.init_dependency_set().await; + let err = result.expect_err("init_dependency_set should have failed due to invalid JSON"); + let json_error = err.downcast_ref::(); + assert!(json_error.is_some(), "Error should be a serde_json::Error, but was: {err:?}"); + Ok(()) + } + + #[tokio::test] + async fn test_get_rollup_configs_success() -> anyhow::Result<()> { + let dir = tempdir()?; + let config_path = dir.path().join("rollup-1.json"); + let mut file = File::create(&config_path)?; + let json_content = r#" + { + "genesis": { + "l1": { + "hash": "0x6c61a74b17fc1b6dc8ae9a2197e83871a20f57d1adf9c9acbf920bc44225744b", + "number": 18 + }, + "l2": { + "hash": "0xcde85e0f40c4c9921d40f2d4ee1a8794e76d615044a1176ae71fff0ee8cb2f40", + "number": 0 + }, + "l2_time": 1748932228, + "system_config": { + "batcherAddr": "0xd3f2c5afb2d76f5579f326b0cd7da5f5a4126c35", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "eip1559Params": "0x0000000000000000", + "operatorFeeParams": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "block_time": 2, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 3151908, + "l2_chain_id": 2151908, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 0, + "interop_time": 0, + "batch_inbox_address": "0x00a4fe4c6aaa0729d7699c387e7f281dd64afa2a", + "deposit_contract_address": "0xea0a3ca38bca6eb69cb7463b3fda7aa1616f9e09", + "l1_system_config_address": "0x67872e274ce2d6f2dc937196f8ec9f7af82fae7e", + "protocol_versions_address": "0xb74bb6ae1a1804d283d17e95620da9b9b0e6e0da", + "chain_op_config": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + } + } + "#; + file.write_all(json_content.as_bytes())?; + + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy".to_string()), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: dir.path().join("rollup-*.json"), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let configs = args.get_rollup_configs().await?; + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].l2_chain_id, 2151908); + Ok(()) + } + + #[tokio::test] + async fn test_get_rollup_configs_no_files() -> anyhow::Result<()> { + let dir = tempdir()?; + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy".to_string()), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: dir.path().join("rollup-*.json"), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let configs = args.get_rollup_configs().await?; + assert!(configs.is_empty()); + Ok(()) + } + + #[tokio::test] + async fn test_get_rollup_configs_invalid_json() -> anyhow::Result<()> { + let dir = tempdir()?; + let config_path = dir.path().join("rollup-1.json"); + let mut file = File::create(&config_path)?; + file.write_all(b"{ invalid json }")?; + + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy".to_string()), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: dir.path().join("rollup-*.json"), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let result = args.get_rollup_configs().await; + assert!(result.is_err(), "Should fail on invalid JSON"); + Ok(()) + } + + #[tokio::test] + async fn test_get_rollup_configs_empty_pattern() -> anyhow::Result<()> { + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec![], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + let result = args.get_rollup_configs().await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("pattern is empty"),); + Ok(()) + } + + #[test] + fn test_init_managed_nodes_config_no_jwt_secret() { + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + l2_consensus_nodes: vec!["http://node1:8551".to_string()], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from("dummy/rollup_config_*.json"), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + let result = args.init_managed_nodes_config(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No JWT secrets provided"),); + } + + #[test] + fn test_init_managed_nodes_config_success_single() { + let dir = tempdir().unwrap(); + let secret_path = dir.path().join("s1"); + std::fs::write( + &secret_path, + "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .unwrap(); + + let args = SupervisorArgs { + l1_rpc: "dummy".into(), + l2_consensus_nodes: vec!["http://node1:8551".into()], + l2_consensus_jwt_secret: vec![secret_path.to_string_lossy().into()], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let res = args.init_managed_nodes_config(); + assert!(res.is_ok()); + let cfgs = res.unwrap(); + assert_eq!(cfgs.len(), 1); + assert_eq!(cfgs[0].url, "http://node1:8551"); + } + + #[test] + fn test_init_managed_nodes_config_multiple_nodes_single_secret_uses_default() { + let dir = tempdir().unwrap(); + let secret_path = dir.path().join("s1"); + std::fs::write( + &secret_path, + "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + .unwrap(); + + let args = SupervisorArgs { + l1_rpc: "dummy".into(), + l2_consensus_nodes: vec!["http://n1:8551".into(), "http://n2:8551".into()], + l2_consensus_jwt_secret: vec![secret_path.to_string_lossy().into()], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let res = args.init_managed_nodes_config().unwrap(); + assert_eq!(res.len(), 2); + assert_eq!(res[0].url, "http://n1:8551"); + assert_eq!(res[1].url, "http://n2:8551"); + } + + #[test] + fn test_init_managed_nodes_config_missing_secret_file() { + let args = SupervisorArgs { + l1_rpc: "dummy".into(), + l2_consensus_nodes: vec!["http://node1:8551".into()], + l2_consensus_jwt_secret: vec!["/non/existent/path".into()], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let err = args.init_managed_nodes_config().unwrap_err(); + assert!(err.to_string().contains("Failed to read JWT secret")); + } + + #[test] + fn test_init_managed_nodes_config_invalid_jwt_hex() { + let dir = tempdir().unwrap(); + let secret_path = dir.path().join("bad"); + std::fs::write(&secret_path, "not-hex").unwrap(); + + let args = SupervisorArgs { + l1_rpc: "dummy".into(), + l2_consensus_nodes: vec!["http://node1:8551".into()], + l2_consensus_jwt_secret: vec![secret_path.to_string_lossy().into()], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let err = args.init_managed_nodes_config().unwrap_err(); + assert!(err.to_string().contains("Failed to parse JWT secret")); + } + + #[test] + fn test_init_managed_nodes_config_empty_nodes_returns_empty() { + let args = SupervisorArgs { + l1_rpc: "dummy".to_string(), + // clap/env may produce [""] — ensure it's filtered to empty + l2_consensus_nodes: vec!["".to_string()], + l2_consensus_jwt_secret: vec![], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: PathBuf::from("dummy.json"), + rollup_config_paths: PathBuf::from(""), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + let res = args.init_managed_nodes_config(); + assert!(res.is_ok()); + assert!(res.unwrap().is_empty()); + } + + #[tokio::test] + async fn test_init_config_success() -> anyhow::Result<()> { + use std::{fs::File as StdFile, io::Write}; + + // Create a temp dependency set file + let mut dep_file = NamedTempFile::new()?; + let dep_json = r#" + { + "dependencies": { + "1": { + "chainIndex": 10, + "activationTime": 1678886400, + "historyMinTime": 1609459200 + } + }, + "overrideMessageExpiryWindow": 3600 + } + "#; + dep_file.write_all(dep_json.as_bytes())?; + + // Create a temp dir and rollup config file + let rollup_dir = tempdir()?; + let rollup_path = rollup_dir.path().join("rollup-1.json"); + let mut rollup_file = StdFile::create(&rollup_path)?; + let rollup_json = r#" + { + "genesis": { + "l1": { + "hash": "0x6c61a74b17fc1b6dc8ae9a2197e83871a20f57d1adf9c9acbf920bc44225744b", + "number": 18 + }, + "l2": { + "hash": "0xcde85e0f40c4c9921d40f2d4ee1a8794e76d615044a1176ae71fff0ee8cb2f40", + "number": 0 + }, + "l2_time": 1748932228, + "system_config": { + "batcherAddr": "0xd3f2c5afb2d76f5579f326b0cd7da5f5a4126c35", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "eip1559Params": "0x0000000000000000", + "operatorFeeParams": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "block_time": 2, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 3151908, + "l2_chain_id": 2151908, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 0, + "interop_time": 0, + "batch_inbox_address": "0x00a4fe4c6aaa0729d7699c387e7f281dd64afa2a", + "deposit_contract_address": "0xea0a3ca38bca6eb69cb7463b3fda7aa1616f9e09", + "l1_system_config_address": "0x67872e274ce2d6f2dc937196f8ec9f7af82fae7e", + "protocol_versions_address": "0xb74bb6ae1a1804d283d17e95620da9b9b0e6e0da", + "chain_op_config": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + } + } + "#; + rollup_file.write_all(rollup_json.as_bytes())?; + + let args = SupervisorArgs { + l1_rpc: "http://localhost:8545".to_string(), + l2_consensus_nodes: vec!["http://node1:8551".to_string()], + l2_consensus_jwt_secret: vec!["secret1".to_string()], + datadir: PathBuf::from("dummy"), + datadir_sync_endpoint: None, + dependency_set: dep_file.path().to_path_buf(), + rollup_config_paths: rollup_dir.path().join("rollup-*.json"), + rpc_address: "127.0.0.1".parse().unwrap(), + rpc_port: 8545, + enable_admin_api: false, + }; + + // This will fail at the L1 RPC call unless you mock RootProvider. + // So, for a pure unit test, you may want to mock or skip the L1 RPC part. + let result = args.init_config().await; + assert!(result.is_err() || result.is_ok(), "Should not panic"); + + // If you want to check up to the point before the L1 RPC, you can test init_dependency_set + // and get_rollup_configs separately. + + Ok(()) + } +} diff --git a/kona/bin/supervisor/src/main.rs b/kona/bin/supervisor/src/main.rs new file mode 100644 index 0000000000000..96dc086800185 --- /dev/null +++ b/kona/bin/supervisor/src/main.rs @@ -0,0 +1,24 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod cli; +pub mod flags; +pub mod metrics; +pub(crate) mod version; + +use clap::Parser; + +fn main() { + kona_cli::sigsegv_handler::install(); + kona_cli::backtrace::enable(); + + if let Err(err) = cli::Cli::parse().run() { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } +} diff --git a/kona/bin/supervisor/src/metrics/mod.rs b/kona/bin/supervisor/src/metrics/mod.rs new file mode 100644 index 0000000000000..4257338cec0e8 --- /dev/null +++ b/kona/bin/supervisor/src/metrics/mod.rs @@ -0,0 +1,4 @@ +//! Metrics module + +mod version; +pub use version::VersionInfo; diff --git a/kona/bin/supervisor/src/metrics/version.rs b/kona/bin/supervisor/src/metrics/version.rs new file mode 100644 index 0000000000000..13fa2c273cf2a --- /dev/null +++ b/kona/bin/supervisor/src/metrics/version.rs @@ -0,0 +1,59 @@ +//! [`VersionInfo`] metrics +//! +//! Derived from [`reth-node-core`'s type][reth-version-info] +//! +//! [reth-version-info]: https://github.com/paradigmxyz/reth/blob/805fb1012cd1601c3b4fe9e8ca2d97c96f61355b/crates/node/metrics/src/version.rs#L6 + +use metrics::gauge; + +/// Contains version information for the application and allows for exposing the contained +/// information as a prometheus metric. +#[derive(Debug, Clone)] +pub struct VersionInfo { + /// The version of the application. + pub version: &'static str, + /// The build timestamp of the application. + pub build_timestamp: &'static str, + /// The cargo features enabled for the build. + pub cargo_features: &'static str, + /// The Git SHA of the build. + pub git_sha: &'static str, + /// The target triple for the build. + pub target_triple: &'static str, + /// The build profile (e.g., debug or release). + pub build_profile: &'static str, +} + +impl VersionInfo { + /// Creates a new instance of [`VersionInfo`] from the constants defined in [`crate::version`] + /// at compile time. + pub const fn from_build() -> Self { + Self { + version: crate::version::CARGO_PKG_VERSION, + build_timestamp: crate::version::VERGEN_BUILD_TIMESTAMP, + cargo_features: crate::version::VERGEN_CARGO_FEATURES, + git_sha: crate::version::VERGEN_GIT_SHA, + target_triple: crate::version::VERGEN_CARGO_TARGET_TRIPLE, + build_profile: crate::version::BUILD_PROFILE_NAME, + } + } + + /// Exposes kona-supervisor's version information over prometheus. + pub fn register_version_metrics(&self) { + // If no features are enabled, the string will be empty, and the metric will not be + // reported. Report "none" if the string is empty. + let features = if self.cargo_features.is_empty() { "none" } else { self.cargo_features }; + + let labels: [(&str, &str); 6] = [ + ("version", self.version), + ("build_timestamp", self.build_timestamp), + ("cargo_features", features), + ("git_sha", self.git_sha), + ("target_triple", self.target_triple), + ("build_profile", self.build_profile), + ]; + + let gauge = gauge!("kona_supervisor_info", &labels); + gauge.set(1); + } +} diff --git a/kona/bin/supervisor/src/version.rs b/kona/bin/supervisor/src/version.rs new file mode 100644 index 0000000000000..6fa1e8b73d0f2 --- /dev/null +++ b/kona/bin/supervisor/src/version.rs @@ -0,0 +1,19 @@ +//! Version information for kona-supervisor. + +/// The latest version from Cargo.toml. +pub(crate) const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The 8 character short SHA of the latest commit. +pub(crate) const VERGEN_GIT_SHA: &str = env!("VERGEN_GIT_SHA_SHORT"); + +/// The build timestamp. +pub(crate) const VERGEN_BUILD_TIMESTAMP: &str = env!("VERGEN_BUILD_TIMESTAMP"); + +/// The target triple. +pub(crate) const VERGEN_CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE"); + +/// The build features. +pub(crate) const VERGEN_CARGO_FEATURES: &str = env!("VERGEN_CARGO_FEATURES"); + +/// The build profile name. +pub(crate) const BUILD_PROFILE_NAME: &str = env!("KONA_SUPERVISOR_BUILD_PROFILE"); diff --git a/kona/clippy.toml b/kona/clippy.toml new file mode 100644 index 0000000000000..f3322b5fd242c --- /dev/null +++ b/kona/clippy.toml @@ -0,0 +1 @@ +msrv = "1.88" diff --git a/kona/crates/batcher/comp/Cargo.toml b/kona/crates/batcher/comp/Cargo.toml new file mode 100644 index 0000000000000..f9277bb166b22 --- /dev/null +++ b/kona/crates/batcher/comp/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "kona-comp" +version = "0.4.5" +description = "Compression types for the OP Stack" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-protocol.workspace = true +kona-genesis.workspace = true + +# OP Alloy +op-alloy-consensus.workspace = true + +# Alloy +alloy-primitives = { workspace = true, features = ["map"] } +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-engine.workspace = true + +# Misc +rand = { workspace = true, features = ["small_rng"] } +tracing.workspace = true +thiserror.workspace = true +async-trait.workspace = true +unsigned-varint.workspace = true + +# Compression +brotli.workspace = true +miniz_oxide.workspace = true +alloc-no-stdlib.workspace = true + +# `arbitrary` feature +arbitrary = { workspace = true, features = ["derive"], optional = true } + +# `serde` feature +serde = { workspace = true, optional = true } +alloy-serde = { workspace = true, optional = true } + +# `test-utils` feature +spin = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["fmt"], optional = true } + +[dev-dependencies] +brotli = { workspace = true, features = ["std"] } +spin.workspace = true +rand = { workspace = true, features = ["std", "std_rng"] } +proptest.workspace = true +serde_json.workspace = true +alloy-sol-types.workspace = true +arbitrary = { workspace = true, features = ["derive"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } +alloy-primitives = { workspace = true, features = ["arbitrary"] } +op-alloy-consensus.workspace = true + +[features] +default = [] +std = [ + "alloy-consensus/std", + "alloy-eips/std", + "alloy-primitives/std", + "alloy-rlp/std", + "alloy-rpc-types-engine/std", + "alloy-serde?/std", + "brotli/std", + "kona-genesis/std", + "kona-protocol/std", + "miniz_oxide/std", + "op-alloy-consensus/std", + "rand/std", + "serde?/std", + "spin?/std", + "thiserror/std", + "tracing/std", + "unsigned-varint/std", +] +test-utils = [ "kona-protocol/test-utils" ] +serde = [ + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-primitives/serde", + "alloy-rpc-types-engine/serde", + "dep:alloy-serde", + "dep:serde", + "kona-genesis/serde", + "kona-protocol/serde", + "miniz_oxide/serde", + "op-alloy-consensus/serde", + "rand/serde", + "tracing-subscriber?/serde", +] +arbitrary = [ + "alloy-consensus/arbitrary", + "alloy-eips/arbitrary", + "alloy-primitives/arbitrary", + "alloy-rpc-types-engine/arbitrary", + "alloy-serde?/arbitrary", + "dep:arbitrary", + "kona-genesis/arbitrary", + "kona-protocol/arbitrary", + "op-alloy-consensus/arbitrary", +] diff --git a/kona/crates/batcher/comp/README.md b/kona/crates/batcher/comp/README.md new file mode 100644 index 0000000000000..533d737d0f267 --- /dev/null +++ b/kona/crates/batcher/comp/README.md @@ -0,0 +1,8 @@ +## `kona-comp` + +CI +kona-comp crate +MIT License +Docs + +Compression types for the OP Stack. diff --git a/kona/crates/batcher/comp/examples/batch_to_frames.rs b/kona/crates/batcher/comp/examples/batch_to_frames.rs new file mode 100644 index 0000000000000..3a41d15a4d10d --- /dev/null +++ b/kona/crates/batcher/comp/examples/batch_to_frames.rs @@ -0,0 +1,111 @@ +//! An example encoding and decoding a [SingleBatch]. +//! +//! This example demonstrates EIP-2718 encoding a [SingleBatch] +//! through a [ChannelOut] and into individual [Frame]s. +//! +//! Notice, the raw batch is first _encoded_. +//! Once encoded, it is compressed into raw data that the channel is constructed with. +//! +//! The [ChannelOut] then outputs frames individually using the maximum frame size, +//! in this case hardcoded to 100, to construct the frames. +//! +//! Finally, once [Frame]s are built from the [ChannelOut], they are encoded and ready +//! to be batch-submitted to the data availability layer. + +#[cfg(feature = "std")] +fn main() { + use alloy_primitives::BlockHash; + use kona_comp::{ChannelOut, CompressionAlgo, VariantCompressor}; + use kona_genesis::RollupConfig; + use kona_protocol::{Batch, ChannelId, SingleBatch}; + + // Use the example transaction + let transactions = example_transactions(); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + let single_batch = SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + let batch = Batch::Single(single_batch); + + // Create a new channel. + let id = ChannelId::default(); + let config = RollupConfig::default(); + let compressor: VariantCompressor = CompressionAlgo::Brotli10.into(); + let mut channel_out = ChannelOut::new(id, &config, compressor); + + // Add the compressed batch to the `ChannelOut`. + channel_out.add_batch(batch).unwrap(); + + // Output frames + while channel_out.ready_bytes() > 0 { + let frame = channel_out.output_frame(100).expect("outputs frame"); + println!("Frame: {}", alloy_primitives::hex::encode(frame.encode())); + if channel_out.ready_bytes() <= 100 { + channel_out.close(); + } + } + + assert!(channel_out.closed); + println!("Successfully encoded Batch to frames"); +} + +#[cfg(feature = "std")] +fn example_transactions() -> Vec { + use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope}; + use alloy_eips::eip2718::{Decodable2718, Encodable2718}; + use alloy_primitives::{Address, Signature, U256}; + + let mut transactions = Vec::new(); + + // First Transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[6]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + // Second transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[7]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + transactions +} + +#[cfg(not(feature = "std"))] +fn main() { + /* not implemented for no_std */ +} diff --git a/kona/crates/batcher/comp/src/brotli.rs b/kona/crates/batcher/comp/src/brotli.rs new file mode 100644 index 0000000000000..3302d7b210bf7 --- /dev/null +++ b/kona/crates/batcher/comp/src/brotli.rs @@ -0,0 +1,183 @@ +//! Contains brotli compression utilities. + +use crate::{ChannelCompressor, CompressorError, CompressorResult, CompressorWriter}; +use std::vec::Vec; + +/// The brotli encoding level used in Optimism. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrotliLevel { + /// The fastest compression level. + Brotli9 = 9, + /// The default compression level. + Brotli10 = 10, + /// The highest compression level. + Brotli11 = 11, +} + +impl From for u32 { + fn from(level: BrotliLevel) -> Self { + level as Self + } +} + +/// A Brotli Compression Error. +#[derive(thiserror::Error, Debug)] +pub enum BrotliCompressionError { + /// Unimplemented in no_std environments. + #[error("brotli compression is not supported in no_std environments")] + NoStd, + /// An error returned by the `std` brotli compression method. + #[error("Error from Brotli compression: {0}")] + CompressionError(#[from] std::io::Error), +} + +/// The brotli compressor. +#[derive(Debug, Clone)] +pub struct BrotliCompressor { + /// The compressed bytes. + compressed: Vec, + /// The raw bytes (need to store on reset). + raw: Vec, + /// Marks that the compressor is closed. + closed: bool, + /// The compression level. + pub level: BrotliLevel, +} + +impl BrotliCompressor { + /// Creates a new brotli compressor with the given compression level. + pub fn new(level: impl Into) -> Self { + let level = level.into(); + Self { compressed: Vec::new(), raw: Vec::new(), closed: false, level } + } +} + +impl From for BrotliCompressor { + fn from(level: BrotliLevel) -> Self { + Self::new(level) + } +} + +/// Compresses the given bytes data using the Brotli compressor implemented +/// in the [`brotli`](https://crates.io/crates/brotli) crate. +/// +/// Note: The level must be between 0 and 11. In Optimism, the levels 9, 10, and 11 are used. +/// By default, [BrotliLevel::Brotli10] is used. +#[allow(unused_variables)] +#[allow(unused_mut)] +pub fn compress_brotli( + mut input: &[u8], + level: BrotliLevel, +) -> Result, BrotliCompressionError> { + use brotli::enc::{BrotliCompress, BrotliEncoderParams}; + let mut output = alloc::vec![]; + BrotliCompress( + &mut input, + &mut output, + &BrotliEncoderParams { quality: level as i32, ..Default::default() }, + )?; + Ok(output) +} + +impl CompressorWriter for BrotliCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + if self.closed { + return Err(CompressorError::Brotli); + } + + // First append the new data to the raw buffer. + self.raw.extend_from_slice(data); + + // Compress the raw buffer. + self.compressed = + compress_brotli(&self.raw, self.level).map_err(|_| CompressorError::Brotli)?; + + Ok(data.len()) + } + + fn flush(&mut self) -> CompressorResult<()> { + Ok(()) + } + + fn close(&mut self) -> CompressorResult<()> { + self.flush()?; + self.closed = true; + Ok(()) + } + + fn reset(&mut self) { + self.closed = false; + self.raw.clear(); + self.compressed.clear(); + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + let len = self.compressed.len().min(buf.len()); + buf[..len].copy_from_slice(&self.compressed[..len]); + Ok(len) + } + + fn len(&self) -> usize { + self.compressed.len() + } +} + +impl ChannelCompressor for BrotliCompressor { + fn get_compressed(&self) -> Vec { + self.compressed.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_primitives::hex; + use kona_genesis::MAX_RLP_BYTES_PER_CHANNEL_FJORD; + use kona_protocol::decompress_brotli; + + #[test] + fn test_compress_brotli() { + let expected = hex!("8b048075ed184249e9bc19675e03"); + let decompressed = hex!("75ed184249e9bc19675e"); + + let mut compressor = BrotliCompressor::new(BrotliLevel::Brotli11); + compressor.write(&decompressed).unwrap(); + compressor.close().unwrap(); + let compressed = compressor.get_compressed(); + assert_eq!(compressed, expected); + } + + #[test] + fn test_compress_batch_brotli() { + let raw_batch_decompressed = hex!( + "b930d700f930d3a0a8d01076e1235e0c33674a449c13fc37ee57f9ea065bf41af3aa03d5981f1432833bd0b0a0652a19cd927ae4a22e8f8069385002252d78e1c3cc91a59ac188708b7074449184766cbcf3f93085b903ee02f903ea82014d884062b70d4e215ee885019d47a37c8543ae9f382a8310c97b9451294f5cd6e52c003ecfb412ca8b42705c618d29883782dace9d900000b903690d669b0cd98174ac3b57393839029ac04ad36454109851443b4f6580664fe06766a7dea5b1ed31e14e7c11aa738eecb86e979f874873cd3d7ca9481681b4b17d134316e7bbe828ef69339ef85c6f0e9dcdfe1dc85309effb487569383d5464b519bdc1c85fffc72bfe93d4081a3e1b75e5dd39f95a91df0997a22d8fbdeca57a8b35b4f0e277ec8502cc55581a94eec1d1000b2921b4d7c3985ace205713641d03c3975e4049e13b3d2c5926b224684e38beb3b8d2e5d4060b109aafc3f2d144783aadf6086aa1d5a931d21282711484a9c0537bd4981fc222444f2c057211708e70dc4223063cbf39e4af0b795d3ec0dfba32391611d151145c1b6bb33d53ce2bb7983bd7b6c1516f7a1a719fd876f4b20910aba76c16dbfc57199a60e2ab938bc285613c3802c17aa03cb9654f5142d607bac01293c9aaf4e58b422c543f7e5e458af0b7cf57f33109558bef71e8b5506da723d996eb8e2c265b1cae43dba571d07d3ea1bcfdcb73089597e3744344e049bf21b4244d5aff60d559010b69a6335f4bb21178de504f50808204da652c7767dbf11f2a34b4fb710e6df9ad8810aa75dcdb2c99dfe9bf898912817e490b4982d44fe09f8adb43e0da2a0c824a9069ce8cc36b5fb0074c2db895ee92d92fa6b7efdf5c97ae05ae27556bc07ddc9d9d6261a53e3a10c350c3b1da26b27b345768e17da7dabfe6e30e019c88ef4a0e8df840bbd3fbbb639edf775449d8be7510cc811564789b861372fe97f7b5b1389f20c9872517634e9225669ee80cf077f9c8606cdbad53819a875ecd9f7b6d778c1dc302ca19ae67ffb054eb99206fc90eacbac8177712d0b4c72700df3f5e2c88fb4e9c8284cefa66390a78605ad9320aee34f72f3cb263020204393d9359a65f48b0e6e942b016a1f2c5bd6579f0a65997635ab15fa38db76ae8a5d3be516441499819bfaf730ebaec389db082e41443660dcc6280315154888b9e726b971237fae5e06b01958aac081398c814e446a003039dd090c0efa5d39735ed0ab46c7b4e4c960ae414b045fd19117089e65aaf3779cc9045d6e62538b1b75c2689d23ba3c08ceed46d4fdf9b969b34a1903ebd96a3a6b091842480e638b095c1ec11bb5c599668ea1b0a5a714d13462edb39dfd992b569897ac8f45c587182770631c262fc459afa6f23d5670eee2aac2ddaa89314607d30c6bfd408980c082749ad6b48a5310ac75b880cc080a00b5d23a075615f50233ce278d11b7b0ba0ad6a01486dbf31c54aae096f0f066aa02d9feeb4771b5a37d1247a4cc58a64d392f3916b5602d9d41d97b52b391ffd47b9011801f9011482014d88a793ab3f17510b308821f5d9030532aae9831708c1940b6f262f685c8d0ff7dfc9ba9686d8f75b78923c80b89f7644852b70713a788b69f191c54ec8368a7f2675623b2369f9078516605d0d4550ff9f5b92b9da2147fa3a24cc17605f30cccedc5bacafb2bb86e2640db6654a514b8eb13d3c3ab6b5e344498de0c709dd9bef58a8af16d3efcd2c0b2cb69d6089d0af8d42baab434dea885253e42050aeec01f233e64289b2e894c680fbab4f25a653745dbd89edb19d97e35bdd4293794c69503b0e60ed9cffe7e9ab3cbbc080a0dd08ebab0802fc61ccf26c357b638a55cbcd6b366251c17e2fa52d328d9d59e5a027d334772553048d6b76fc39ddee5f85363810c235219356cb4c5c3dbf9661d5b90298f9029588e383f18817bb0d1c882c58aa6b12de88f3830a7831945c1c1314ed944220436fad3742023cba2a71c4a2886124fee993bc0000b90219fb039c014cd76a327bb9b3f59e8176f377249385e67cb1681f8eacff1dee5a5a949511438ce370f8ad6618f3af81cb1f775a0b365546dd7791b0ad71fb1f2f29154265a8175b7e518580732a5a46dae3752e1234ff779d4eb614af2c66beec964181ecd0cfd1640bb2ca2b860649c41930a60de0cc754884a780488f05d1d5833a381670b368c85bf08d6650e26122f6714056382a006fcd5f9c97f55a98d68dd9293bb1be24823eaa8cb007481dc78a7a670123976e7b6e81fc223f42637759a0c933b73ba89a1d902c0874fedeb0a97dfab298972a18378539c2894ca6df9c0a423c2e98df4c133e5e808809849785b069e323640bf93d4b82a0917aaea8fda9a3072ab9a00a4b8b9b7b3a3eb326e54231d0f6a064cdf4a1fc06c961e5087359c029b13e229fb477d6651bad52c75e503ac45002a803a7457488966cc16bbc9be5c1c9a797d0377710c028e4f05a6cb929cc1fd4018912929252e04e107ffbcbd4c81ba01ab4b11faa90be0f9f9a6a22c87257e4a2aa8283e6f71d7b9e03b5308b16525c4d79705bb0906be0e947e8075ac6ce2235356aa0a66bec39e918e47a6220b322e326bf8fd65e47778e14074c47cb62b7ef8ef956c996097d2919df7aac8ea2ed69c1fd9f1d96b6b82b411c524cacec0f4a4269821fd6766d24954b8870fb1d85f5cda0528ae18419915a8b30b25baf6a162978a4bec86009cece83017d50667a202b3fad18f8ed8b5140c97fa74e91be608fdb788202bea05f469660e363ec580825d1e2bf753c01db044279f862720a27831744b91494f5a050fa7445e0e6156dfdb712a647ef73a2dd35b73d5cc988430c831352d4ac7e8bb90458f9045588a106e4c16d06833a881973c4c642fba1bb83068f2294050c84206ba9d32d93d144884644e5bd36fc92d0883782dace9d900000b903d9b303f8efb68766822d7eea21ca4b7c5dd79dce832c4893247f6784fe47cd7a18caea7b5b4d8bdf02da0276aca185add01fa2d16c2f1188ff7cbf6fb8c6308999037b2b92d725094d8faed86f0b1a45b55de4f36dbb71dcbf4be12fe624077213e0c170afbbbb546a343ac3f2a1333a7a7a7db7be46640a73d61b3aabc805b022be416198d809b62f99d26cf4a3bf555d40686f4b8970ec15386462bec5f2b728de0da047d6b3f3ea51f571507f32f047322fa204f0c5697cbb56b4b5c7792acaa40f02926651fa715a40e1f212c78cd4ecca285ada2c8cbb6e5dcfa3823725b44e29aacbeb9b6224f90fbc895a5980d63da46688832e9776b0666e90deacbcf8a4c559b625cf004cd04c686aaf9d7d6e2d394f5d36311f7afdcec5033daccc63c0540935f59514c9aa8ac3c2aeff48f624f2dbd38062fcd046651e92fc7ffce4dd914bb0dae704e5b26a8b73b3baef8ea022881e15666fada8e43fd621793713cb8c867775b9cdcf3b066582fc9baa705a0e1dc61a4b33b1b33ad3ba3bd0cc41b5850cadc04654dec222178709910209c6ac3db9054ef91facae2d729d7ee54898a18411b6d20d599a3de14d5375e5a9c90f3bce78479cb0f20afca895e40b576940e063587f451a8828ec2dd4a8538b4bebc39f72a6c54e379a07b7d5e0c02ccd57dbff13729bbfe5e78498c01cea12e830944fd0a123b7383fdcda97d8d9cc831e542ab6d9b36774d540b180c2bd52d46ca7f0e17d400cf3cd559b1b4e51ba93cd954777ba27a9f0327eb6c68aafe74fabca4610210db7498aecffd3164c5eef8cede655e1b42d5f54f5a52b4f5fe9698a4463f30f20693263d41074d0403a737c4d4986f0ee7fee828fb7072a80603613fb4d6c219dfa47adad433af6b437dd199f3bbc651487718b2e6d42728034c242672a98a9f36fab6d4162f4e8eb7bf2a9868cead8ad657a67f0aa50286113db972936260323d7b11353328151e80691d551bbe1f7f11774e15db4f175aeac5b91668a712c3c2399a977abb9fd9c2b53c5ba68f2c0ea353028416b36a47028f78918e2b205bf9b3bce6f1a08bd4448abc3f12a240482b4be98dcb77c74fff47e92d833735e802465e50b79d51de5a7fe45a95b650b051c61a529d5f51cd0c603a2de67a3123be1c52263e1c9167765b13ad1e01cfb27531c9203f39e8913fe0cab9d8c14b17bad0100b76c41d41d68ae3b7aeef5f6af4f66d113fd29eb9c4bf994f04decad13880d9d1eb3865a30e2540e86923b36369c121ef2a6a43a618aa4b15560fa806601a85be361468bd09c6dca39ad7ec44809adc0907dd0458177343a7c23330605b802f3ffd3ae61b3be952ca2effae8222e9ed0b6ea4240728a7800e4882efa7dd1ef8202bea05db690cab7dc8c52c2c375428c0aa9ead02bf44e2b1f8ee06e1cf7af25eecc13a07d967fb12e1f0073adac46e0676a6006b30d780e6a1387afec76cbd1f07016e3b9012401f9012082014d88df6f092495b7f4148840c5b5541d013c63830408e194aef36f2041e560a641af89e0ba2799ea630a9592881bc16d674ec80000b8a3afb9380f9228224c1aa59eab115ed4172b471aa2ee11b3d4ac93f4b6a33518007a798170801f4f582e188b489005d8f108e2a4acd6f7ac28852580e73b6a1590ea1af1443666f1d14affb0a9d0655a5c57cd4190b2a00c07276054641ee4204ed8a806ded2b3aaa7453c24e442992434d060b51d2255c1cc2a002264b5dadb32057f4a5d52626e0ff453e2f05f1e0d8294614916c00110853462d51d9ab7e03b7019c6c001a06028ddc42f0d3e1cd6cb1ed7377d518480626d56c80e6d15eacd42ecf2f30957a03f6e1098b300b6329997bacc5e667eeed72a38f6c4e1db7199483bc9a18267d8b90222f9021f88c0988653bce0e07388fbc67f04e5c6772e8311bd5c94eeecd6da1ee441093ef70d8c86a26f4dc4da11588853444835ec580000b901a349e745c1cca19957c43f15309935f7bf49547884332dfe6d5b8b9d61542dd88ecc61187fda813a7f700ca96e8847a33bf8552690d91ec8e8fa70c21b380c9c681b54e859add36c3c19e7fda3075ec1a3cf47ed39c89241bb73f206d7497f93c47db9a85be7135948e19809c195ccd4c9a379ed464bf77ec562e360c52b9225f103d323364a72e8a725ad2b34a355928acc6aa563b67d120ddf54cf68f710624499ddeb30b0c94b8722ef2d641ae49f17f4a916d54350ec483ec5bcfd9748e0a228c3e73cee9ea248ad85060ac51b3e6834e1f771f725a466affa28453ad3726d794caab223fa76c8b994ac5d3a1e8ee830e4fadfe0786174364af3109c04d7d607aca17933c4366d44d9c5376ca34febaaa612707eec4e2fc5c6b1668b3450340938d17e5552df96ae84a905d069f9e3455bccab30640a0720f9b4598d8f82ebd19bd32b7e82165303123a0ed80c57375174c08d32ad3ae354251c97316b2977f3a2fdf2dba1c595093c88275badc54e3aad65f77c56f55d04b1e6d668406058ea01da2364fc207659b028d9c55371c776f732e63255dd177b95f857e3cbdb4c66fabd8202bda060830662664d96755362addcc0908287c99c60761cf9c7a613058894eab6e599a059cd2461d4a89458dc68adf287fee71a783dab0aaa05587a21b4aba1ca4f5efeb9017801f9017482014d88d15c09b7ee8f9562880ae58585f383aacc831e72f6808853444835ec580000b9010a2e818d2c4fa7a974f5c3acf3c0f9439f4c83721b2bb9df4fa290c7fa57bc1f9f77e4b80866845a8bbbf8030b707b1f07a54a0ab901188eb2e1262a45618a08517f943cb032eeec926e4343d5d3089c145da1d53128ae901ce91a813c205c615bc1ce9b8658a9da4c2d258fe36f6ffb6289df910566386dd1a9f73b44053bb64523d8faf7b9055c592695fc426c360479c1e2d1f68ca5c7965dd20b6879989606cea7c0db28f27ead4a591ee264f755b7358146586c6a1a8530ec463dd754f100fac603ec3360c0440874c12bb179c43a23e40957bd446f2573af413f3314e9f0668af2491de96156a9bf35bc469d51935305f4df051580b84e98ec8395fbd42fc0c3f3e7410ac4719af4c080a09a774db7e3a26966edb91c1f7956a091425044ead1589f435c8d04aac9533764a04325d5543464929773cc6ac555f5ce1830c997f4d26f2dad5a7e056db6f0a2e6b9032d02f9032982014d88828a67bc288355d78498c2cc318542aa1a60df8305fbb6808853444835ec580000b902bd082cb3f3fa41ebf06fbb17afeed9ccdcf3d2999e2fdd1e1171e0b1549c06de17dffc4ee7785232184a698311c7487fdf090e34b9954a41affc0d0ad44104f70750f6a896b1b2b5ff1024de66ba877c5494e67735cdfd45f9ec0df1c198b357b60e4d840abaa72c5667074c43bfa5e1f07b5970f018820db6fc2bf84341cd024cefe455c92426f876e51aec0fedded8d4aa4003aaf6970c48d898d8d82a8411990e73c8ec792a2cc4a129e526d0fa34a54c37ac13ecf4e3c597304cdbd327704fc97f2ba0b110afee78da5c3f46d3354bd20f56cb91b7ba8d302422428082748faf8b4828ba925ab1a02ba695e686da4d1e759b6456b0388ac8fd769f3b726332be36d3153ebee040b5d822fe62d73b629a6251c8e49a988cdfe599762759df03c9100db5f7a87ce7102ddd21831e0736924f230ffe6aaf6b012423e351627e118f2bc12736a3694b5468858ec6310017b10de24fe75ff0abc060b1e60271dc5274b4bbf0b755a0a617bc23f57ee2286c805086d5824ca4bb6297545c5c1ccaf03be03b7df33c953ddb183730313f09c88392e4bdf688f1d2b730318cc9b148e488c2f1e383505a383672755a221ee7dffec5a4f77e7efe66043d686a126480ea01a8ef0f72f9a5799e03e863a85b7aa56c88b7575d6ebb9df809a240969d3a2b2e086e742130e38cfe7870db79bbd281849912fa611e04b8dd0dea9b7da5d16a66969e54ab9def159b9c1d351d719a93821c40ad6c6014644c5f77374cbd486d6a7cfe75d7d849ce240ac86a1c0843aab27fba4d317c725eb101752803ea67d3e12b784bb424eee6f766e33d6664ca113af63c54ba27b8a8e904c572dc3fd09848cca3499c403a1c601db77a7f36d244024ceacfd9d6ae494b7e7e0f92fa5f83458d5da139eb127709e3dd75c88fd5f75244e15f1bb8cdbd3056bfa56139442c0bacbf3263f29ef34946e928b9a4f1c085e5df3b09f31c6e87397bd939c001a08b9ac3bc299eff8eedc51ed3ff077e49da6fb145a0c495f430964581fd4d230ba05fef2837a800e231a3178226f59a981d2c4bcebc4b4cfba9680371da1e2c1a61b9042bf904288821c649ab1ae8ea668896d6c78054ad7a6583121a8994e3294b628e98892fc56ae3fcbce852265aa657e7884563918244f40000b903ac0177c66fecad5135344e89f45ec7e083130a3e5eab1abb75bab0aa357cf044c0582542047a3f9985d3439a6f850466061142af44a9208656e278b7ad1bd0e03539cc019d6ebf8758bde3e0489ba540c523f178a0b055c1fedc3627fee427467ab67545c154106bb9e0c12a7120c175d66f9e3eb9183ae5c7640d4cb4bd3dc94c7b4e0c9fe70e692c3fd027e0ebb46bb32b73a269037a76731a9f114343ea0584c3f7e9cb4530d086609b59ab6b72e7dc6c2c0c95699091e06a33af5ba200a168ef483fe11056330e84da4f2a59db72d5d697d262b9565fe81a738a48d24a9f1c8c49a671101bb7db5eb64deb454a117eb00f4ccc31bc93c061e975ab6d375967544a2a06ff8b9d59bfe1ecb1dc47d5536c645d764028c5de77f3f34d6c7999785b70b187d9ec4631e83cc69499a4ff8ace98a6f17b77f648ab7a07d5ee0558a8efc19d4601573156a0264d2e6574e867c1eca423eac1fdbfe0967bb8f02524cc2d9933141acf619ffe99483305fbdd6913f1e1feb78a17fc6b81c705c81eb08d5602b097ddec64f6c334509caeed7525e3e34845b21e56e4424aa9609f4df8bb13f31c5448b6bdede84d9a9aeba9fcc38a3c8eb1f3f31b80918e045266c7d69b252c86f8b5711b2cf7136e2c3d86d1301608c7c16655c3ffe6d04014dfd55a9563c2a307525088fd017486ffeaeed45873013a7940a7a91442b975065c765c32546aee9b001ba78d8563e039c8edc24a92f9f457ae28172eb29e16cc588d52c8e75a565aad1a8f9d6d341189a24718c26c19a83c6cfe1bbec2f4b878759a7dbeb4ffc0568b902b1dfb18af00c7014f2822965ddfb56d7aec508822531834ad2c869affba1f95bf3dfdf1d1dd1c2994d904b9c5133900962c8137d7fce9f0b9a7d0474dff9173edbcefb4bf355539dfa791241031e90770c8f09af595eb1aa0d083bac4fb9b929ad7e23c0fc8d3ecc7458a0790929cf7588cc255916a6c16811f09d0c972b294dee6e1f739c5e9d3eab8016b565c8570e41bcddeef2dfbbf95910ae6a46a2834919742ec599b9ed204d1f86ce6baa534039ed308d8be0d289824303deb54af5f9f50d88807134b8f42485cec121432e58b83c8aecb32fc62623b06c39c3f1e0e921b1bb880d2eb017578e5f33a25a335a813f02259e1b12b8a76a90a65d015bb214032a095cd8918b78003d310a06a246ac95c126188911bda8a6623407c0dad308e25a438f78c7409267b729413b7d248a6a88cd64c73118999f00981aa4f6b639e4252d39b1706c686c7763ae9c41aea7b46fdd48bc490502ae876175e5aff8361ccc530ad8202bea0b0209fabc8a5c0e2a5bd08e9a6b532d51670f41513cf007781f27e49b070ccdba0795755f4fe231840196d847d100e7cf1e5650ae172890c469428269cb105c16cb9031ef9031b882565c357c3279f0c88e90114422a470a4682e988808829a2241af62c0000b902b424fb91666edaa16addea67f72c9e0bc7a8053bda59776ede2a0ec3f7c78ffac0eee97ff259f92b21378193aeeadd0253b08897a14f10ab537db63202a4c9f78eb4b399d55c5a256a8414f58f45b109e6228a75ed1eb09627f44b56eb539c334df412b30ee6f4ea39a04aa671aee9e7157b9cb69aad4ab1d9d75c6d90f3488342b29bb59c97ecfd2bec4f991b095038b9e20eeb591b641f64e32e5020130f8a8daf7c51caf93ca460a4e60132835119f99d0484529cf541ab9f922bf15a782521a0f6739c1edb8d4bc26a07e63790087b4c098e4df74534340bf7815039326d1bdcafa53932deeaff03a31e97c6733cc702cdd42be18e4716dd0d014f3e916b0cee3a16bd52cf717f5efb59fb7e41c8e4c0d7eee8ba92ee5b293b25612ee9a3b0043664e918a2aa2b602accd357c8f22f382b16f637b57f2fedb7d8f66172f22e67cc04f230e28ec96b928f449fba63b7862bc3102181d6c7bf063d9376363b8be8200169aa88c46732c5ab1e19dcbd8abeb34f1e1cbc632484d9864e630c4567c0f04a2bf5895d3cafae1b0e70e4c1ea28d4d9578a82611f09ddb22c3c4440e8236be2bf9cecd3fa64b19930af8664d78d6f10aa9c913be537bf2b539e3a9042d5744eb3d1bbc16d98564488a51ba45edb2713b466beac560789c4eda3c0961bab002b95eba9f512108dee2e39a8759c04b18a923f2f2aab2e1ca30ec7361b25ae71923027c950c089469820a4ec3ec60529f1509b92ef04fb7fac70f25d3e5ea5c6a28226fe19317bd4d0f42085884020a2b22dcb0ed8e5600ac969b4f910e54f617597a84b05774776d694ba38ccd3d1055a7245334cddb1ca20d7e001285a57001d03b2fc1ff893ab044612dba9b311247528d7490a9a7f3e7c3ed8531844d3b829de3604e8546ee8d4c3d7a308d32035159aecfa20ae4660e6dc94b6a155aa78150a01fb0e6c48b660a0f051ab59accaf4508202bda080d51bfef036fd4c4ebe7151b2755d6606122e565323878701113b84fc86548fa06fb34b02deb66359ae8095d3c339673ab2a8b138fcf9aed2d4276c8a16435a60b88801f88582014d88bbd39acc70c3229d884ec80fa5565439d283119a84942d89ae04c33fcbd75e3c6c43b826b266625b854f883782dace9d9000008911d1f14d3a721904f1c001a046bf61e70c69943c277ef7d09ce5e779a10e3671cfec81423e0f951254dfaad2a012fa75748afaa79673d94a17d35666009001775a2b868b9b839c77065649bbebb90143f9014088e1cba06e2ce482dc8804b98caf86fcf0898305c61980880de0b6b3a7640000b8d9854e530ac567b7d29eedd91690a0d2397591c6a1b1f5068bc292b740f6aa5d38003a933c0560971d4701b31d537fb7c1ff68c40ef07221089f37671b101309000e0eccbc42284732aa002f2cb3197def9947c2b2fe47d3fea2efc71b1f3cd681082d043dbc1471a56a5d0a5c757b8c115277a2af2e044e56e5e3c2cf8756dbe51a347096a4ead46fe53f4c03fc100fe0009f6b2fd6ade28fc89230602e9221962f4512740857b87f415f134a224c5149e374fe22f3048f0620f1bddbc9acdc268a5de1296d265bac65fc2650b3de55e6bcbc26bc4d01dbf7548202bda03e35d4429ee24e44134f7f51b32fb69691a16c60a0347d9283a8e593d5a095baa01c590af4c1fcd3aca728bb5aaf03f48aca22c756a87607b4153a5ac6be59ebb5b9029002f9028c82014d88aab881c6fe3d0b7484b0da2b368542c231bfe483115994808829a2241af62c0000b90220a8317aae8cca53d039d79f09934b9c5d0b07bf13ceeffacf1011fda22a85505eb7c717168c18d8fb230a7a3f166a4e93326fa82884ad3093b5e07b4edee095d98bb92f357fd4a98201be26960d4253da6fcd09874b364595a47b95d2b50f8cd45921931469a302be9699779775b59f27deea2aaae41a010a47b825a46103b7d355f1c154b3422b4fbe4e62c71c5b6b98b627beb82014ad990bda2b6c06ddd237543b3652c7a029928153a8cec540311406260fd3a55cc5788610321d66c29f168ffe5d93f92378359231ff89492db2bd2e90a4d9c28263d75b77842584d253fd7316e61c27f71771ac7e7a3c8ae6921ff2280c459c36348e0a098fe8da94c1546c15db7968d6b2821b24edced45a7ca8f2bfb2b9bb7a497b950bdaaf771bd777e918887c0d2d6ad3b72c168228f49fae155862e0baef308ace6952606a660beee10da3fd2d29b5ac31f2d55e34da94a4274e1bd679fa42bccc5db074a070b899e28948680d82c7229223d846a1a2c19143dd99c78bc42c33490b85be5067a25f6361d6b803b315519de254191557ec691967ccc3d087b8799dfa5888ad748b7a6e164da0c726bc1f916110b6fe6a013ce0e28b79bee045d250657a70211dc11a5dee69a2c05e9eedde536a9911883e5ef2ee76729ff8fbc3aae0fa13a36daf01199a7ac60b21c7fcac00d7c6a80f5ce10b79f4666d69a1a45b3ec864a57f1f6fd492223c539351326d7a25b18bcfd8697f55e972607b9675b1d40dea3ba4c0b3c080a0e69a3802e5dbe5284f817eaa05c76127a3898633d4524f3da9ba8d7e7b98af23a05a2672729a0136c572a68b494cdd49ce47c2c0e33582b601632b3a1d15f3cc38b9016001f9015c82014d889e607b89f9d2717488ee3a5d83a713a9fa831ab7e68080b8fb754cefe26136c37abae044d7be8e1a3b8aa3ff230de4579b08bf12020e9ea66a2f282ef549cd7f72d056ded10c2fa21fe339fe56715960a4bacb65525bde1671a0a691f44c0ed582e64d3799c4ee453a4fbb700cc130eef66cc66913d919b6a96bd31efc3d77e4accf3a7c695275188ed2e5a76526e4706bea7df44cf6a36fb9e43d0e37cf5d6e3c5b984062e57ceeb1c5e6a9d0c418a5a83b77c4c99e8799fba27bd884e51d5df3db1562fa0b13cb1051ef5d5269b4215078384fa84cbcdd93cd7e67d166ebfb88eadc77cfab6a09fd1ea8f82f530ecf62d60d176d3bdf4f2eebf57b45b532ba6471fb53312e32c3452ac69c7b0ce227a61e69cac080a0434df311dffabb4af9df6fd81f48814ad8f5363567d421c5466423bf3bdacc05a0032341e2314432f05701cb222c2868894039e6e156ee6872ebc8739a4c45a43db9027d01f9027982014d880843386325d71bf988456fca4e1ec42cda830601c994c5e72917d21e4aa0f724ed1cbe014171f1be66ff80b90203e082cfea48d8bbd73dc4f299c37a26fcfe1286a62d17e6bfd13084a47fbccd302a44770baa03092d7aa3bf8f15281bde3418b5a6f610199a7ca97fc11df8058de81fdc05527047d32e0e4527db10cddaa2e1a190d7dde1987c0501a200df8eea07d61ea0028930e7422451b44295ce91f79de155d6169bd64c0cadae791e59b67544023e5fcde77eb509d6418daa17dba99d0f09c23c7df78d609f4af7c1ad95b01c26edae2080556b8e63ac632d78b87eb57ef23791c2336775ccf12f62dba46b65a5b5c7017068194fd2b7bff11923ac2dba3ba0d7e28c1ed2ef1c5d2069e189c09bc51efb571c63f2891acacd6a327dc810180290f9699541f4b65bdd8935e074f80887d3f6f4c3ecd75a54c95476b26b42f02964c16ae02532433d48fb5b5f779562224d1bc099f51d332c67cecb1e619bcda1aee26011a463952719987f705b12fbbbf34e3989d6b5c5182bddc569fb545de391ef10031bf1b0f673f0ea1a9763f652624852bee8f09dd517250da77dd194f8310086ba52032212ed38e014a9bb3f47d8a16cd463a977a443ee02d5548ebb5c518e5a0125c6645f2ad2d52f99aec5c88cf4aba79167cb8f7012386916fe2b863da27d16a7c3c350442ebf9b54a569ccfcfe4f4e64853fd810e6a5b3b3cba9ac8525a260505d12492b99437309f94b91dd68c7658291052e2c4d414f87c1d7b7bde565791fdf99004316f02ef4d7c001a05044b928ccada6036e32565da0b9ac1b51d4a0eb5d702efb781a832c120665aca027befe34f4cf0deb37ef259882c20be1af0efa2ab726e06eb33736ab2f0b34e5b90186f90183881a09a2f1c8cde2c488c2eb098e1a51326d83159c2580884563918244f40000b9011b643c223acabd55c37efc426850758db45eb7a0ccb908d9e2ab6a122d812921618aaf4e30c377ed8c7c5b829846b473702496e87f2fac0a78fe92a7602239414117ba9d42c354b05e5561f234e4fc76ecf8285abc17060e980e1713a3f0ab031a53c6757c972e363485581436b20fcb4aa524281e6765ae59362fe284cb6c9c26e3980cec0a9b2f61d1446e9a1679fd055fca089b838872a26f866cb09ceaa5a57a061440ba3a342807d83a5a83589a7297afba2c456c628954a3daa451cb42207f9de22fd5dad066647b8e8ed43fccd3f335298291601fd8737a2ed69cb89e0573fc8eef594568c236f8f976870f2da93c65f77aeda9ae17d812e16dae936ca069e489d3d820580c636f12164c73795e287db92ddcc73dd6b341408202bda0b8ad8ad3d5218e0e27145286459b952ffce119c42b7b143d3ae68f08991c6198a07bd60b6dd3efcb39d42fbd3b15f2f65f9561ed6106484285f3a9d235d2962c2cb903a9f903a6883c0753f96351f096886eb111ddc0775d1c8308a6ae80881bc16d674ec80000b9033e6cc26ae2edabe8f726535a61e77b09496c76d81407ade4466993d4785c16ae669c39a5f9ee18875389a6004576a39465d66329e18646036b9ff5657ba1ec659bb2acedda2862458a642949d15f2108c9c9a712216e2d9d13077a134a69c64daa48018d835b542cfa7861a12febf7b79023af48f860377d4d8bf99639ba627ae9844ddd982438e2a508b6cb89c87d4b78f31e42f842f62af9cd59a69f4e899720156f7a2adf1d348e9b665481165af600a3f781aceea0589215f06dc022fd28fc6025ff85e3d4b7c25c358f35ed5f5f025eb2b0ec5511634494515a197f3e06f4e8a2fef699f33f58ab71376581b455cbf592e1e657115448db5237d010399045e023d0d69797131720de65ffba81c41037657951db3bd5fcc555b8bf6944a67f1fc0ae9ddecbdbb955743a86d2ca82b6239a47f0d37759cb3bcca9d95d7ad084bd8269d06f6cee9effb2173096ef22875db79714328f2d80beac6cff4b3f8fbde3ea1a1040b6885d86bc92390ed2efa52181d3fcf6b761c0a14b8417ea3878d311d3690f93258e57848e926364fc0a60dcaa161a1cd9ea4fda657c5e868f59bc6d2ded1e264a100ff752fbc32d30728f13d74f60a1931cf1cd302aec02f4ca94541335c0f0717cda44c966db4c2c1e522794e0cc5a9dd84ed6355f979c4931231225096d3f651aa1970fd8a6de80325a6b7b3362b11eeeb3401df138bf8742bb94fca940ed45f8b4937d1645c98adad12836b19e09b59dd1e4cf020a2d4efeae49aff02a0c92537dfbcd4a560e876d0a3da71a38302efd5986e70a0592c02c4a8e5638869db811e47ce514bbe71acb864580d9f3be29e73f8af1584130a448b85c0a4a790d750a3d67a4f1c3e52b0db1c7ec28b891c66570c894b9955f0914981f28efef48616b004ca747fcdb448d0a1b6d7196e2ca002e17cfe65e7bb08027b95bea17ba0dd5b9a479726b5cd32a0fe24052c2afb163e60733e6ab77f8d1d2f606de15a31a2db1c8b7827434b64f794b808287f612854c7df802822340442cb00b8c508eb8d74a6334da415319557d4a8cb58247a7e65c74ef2238843fd02d24d6a859f02c547fab6e35903f69394659a2b1bb02fb89a613733cce7c4af817f6b8cf2ce38f425fa8b59b3fea76273664b8215d0503198393443c926b578202bda0115d2f3409265aaa2d214d11e19f314193884ce34c3274f4258d5f09a97172fca0418e2cf579d94373b0a81e66636160ad2f1de4597445af60d0ec37e9a97770deb882f880880f511ab07ca9dce1889745de5325aa780e8311fec19424eb7935928d6e5fc275944276ee070e90b9619e8853444835ec58000086428a36f8feba8202bda0d3d221e5abc91d1bf4721d9f51100bdb7e25f4e1b2eb363d200aa1b0c09727bba07688424185824dde9b365f31e258987ffcdbf3c850f9992ed80d0e71e54712ffb902d702f902d382014d88e4400f9aa703b1f98501db23a8d88543ec7b3d868309954b94e59842fa49a842609ce51ec1a4e9f75a00da8e1280b9025a30fadb0cd19a05ca7d20dbd28ffd1ec743d59a1169a730091be383f6c571c51a8514f9ddf9961a588f38bd388786c9e7efc5d0e71ca89e7f24a73201839f40e9378e5305f4174752c6eef07273a2c51009f04350abed1b6dbfff400ac6f790013028b56aa08f5090e4483b7bfd1b08042b8651dfb27520b3167e9b912e37bbefe7f13153571ef8ae23f2034df09ae737e672bd09d896bb01cc035322407ab3ca2a026f1d8d5beab70178c580a650874a57787d92b6f31f7f86ee939bf8fac22b23c6b6666b5e0241fb55dd4d397f1c78fe6da9fc3e66c2e34058e223a4567d259e3e1a3560bae9f5e2e3e7df1b7384b6af9a4155f1eeb61a6bf4b5e149db22109c635cbe9a4266ef48c211fe1236becc472cb7869906e27166f3f017ce75d188fa708e037fe1a5729b43892460458478cdaa91af1f9367cd1164204b240212101e631cbd027c814efd1e46368b37041836964dc6a76701c38810f36cc02ae93eddd5ebe83c24527244a55eceec6d47ec8df4b158fd1166a7d0d7bbee043632852ecd8e5aab24d71717a232eae9facb45b534f75103fc57f5cd8f978a362249a16e6b3783443bc5100bd1d8bbbd45144b7c63393f5d8169c4381f645bbbabc899e022d58e7b4293125d6c4d7ef75436b4542618636fb247b48ff823f52f416348fb767f6146c1f443147baeea5c6ca7fdcfe3795e09112224301f87c5667027b74b54dcc0f3c4e149a1e67aa6f8a940e1f2891980a6e565821a1f06d522eee5803650f6c0b8c8f5452804f9c456550cb8f1d4827c7fd1c8fe77b71aca3aef9be16494a4bf7d40b274d28ed9cd92a2169b6de5fdfa3ed1b6ef8318c080a008c406d42212f12e384b8f8bb7bb40d0c4660b67026646436ca589d143edc5a9a055fb6596377274cd6af52d95a127c503c0af5b7df6df59ec493d2bf15cf02bcbb9046102f9045d82014d8822e3c64dba5192b7843cffd35685424e576804831aa2e894b002add3a6fe3cfc260c378a187213b6bac436f3887ce66c50e2840000b903dd35dffee48e5855b9f4e7d47630f215334f242c738b2aaccc6e4a815ad70d29a94bd5fea67cd0cc855835ab9bf81c789806e311f744dfc370960d5246099d70e509571437c3c61e11c2971782d7ebbe3dd231c3025966d5ae37fea256ab601339db76c325884b7939ac8e772ff54c8196d35cb823cd42287ccad89e0f1a8092caae92612bc897cee16c73c18a39a5b1ba5bc5df73beb108cf5c896a420837ff53f6e601052ec017e75d3554c0ada83b7874ded4edab8b1a25e39c56c4666ae2812fe82f65f5f7d423ab3a173261ff29495a5ed0851171d1c261129b2062fffa4fc682cb41394f5ebe335bc2220abe7e950d9afa85f305eac439eec8eba9227352f592804f5b47208c262b220c1eb39d6ef89a92ec3ef051e9cca642658a8d8e55b35e78583d7a6cfc01bc5b9d579a1514c201d34230684e4385a1774f8b5f38b5191682a8b91b536ccd3821ee409028180d0f5eabf6e1e2e3dcbeeae0d92cd83e52ae68842bf781824cb7dc8c1507361d7d03b03bb15f7f7a0a9bf12171e01408f60b35722a5a819d7d9107fcea1b94184160cd9890f1f510207d47752fc27f58729ca8490b81ea720d5fcae71db92a9b140099047f45526d26af5da8bfe3e41beffe14d5d1cbe31bd1e50b9c38b9b393ef4b1b5514050e4a934d9501fc70d9ee3720a22fe18533b420cda21aea8c483e5bd3cb4786d6ce2d0f97d1a653253efd1c0283772e8ae43013dba4990bb6c7d9c7087c0d9b2fd3b79decd9a775989c81b87ccbb1e2d6b3c4df6dbe1b7e3a147dd8ff6998a0dcbe3f517899f2dbbbc788d5004d2de3d23224268406d02fecb0ba553123528c6b41f6f55aeaf8f32aa767a9f3113ca91d92e2dcf656cdef77f966a6b2cba83340658aa5c26aa0cb8ce54ae3a55b1eaafef66763ff4de971cd6a0b65a680169837dac945b0a7f13864795670922c99dfc6b5a5465e5043ad1b3205e4579cfc0e037f0b4e0a8b22b5d6ddba7d24b31388620d4aba83f84c5a1334261955d52294bd8b56d7175afbae015933ab1e0ef91e8161468f8eaa76a6f7a9bb8c8fc1195b9d8ff5dc4a51ff73a74b0640999bebcecb6036ef676c65e9fa5b1be22872082989c55a789fc4c2252452f786a13c4e868b85fbcd09bab689bb66dfae14c2ea7024647ad97728deed03314b007dbe461c1836e97f928308d39e5afc43ee3ae22ff47fff183553f56711880cc5ef72c5d66b4e2c6f651c57311d48fcc0aec762fae6444a5be11793be04c85ba97450673687734e681a1f3c64699686880d32d4cf87202b49ce13fbc8771fcf30d5593b41ffa61462c64061449b2c0a24ad8a03d280500bc86049bd55a27a05d70b12c7fd700454dbf3869b329a1ffa9994ecc2a6ec9572e3adaa0056c080a013fed42f6ecae05ccdb9bd8dc88ed44579b6a8871118710058f72c29f6db3b8ea03d200c0fb3e4416a51538d2ba41be88cfe830fa74c280e8b4b66cc3fad24ec06" + ); + let raw_batch = hex!( + "1bd930e08e94a89daf73710d130fc039db221fa427e3e9d10b5ff602fca4577fc203ad9313f493c51668a017c2a4ed1260401ae0dd8967eb390d13f2fab12f43bdb0cf432a6630bc76a84c50bedb2a48e562bff35eeabe9cc219de13de55412f6692e1708609ce3440ac1909a693fdf68b581342ecf8d480342c3e3b435349a5d903609718170fa9a4702fc7df772fec119dd097c017e8531040192c66d18eaa4261721c01c8932d0e8890ac2be0630cc398f04f556750355a3a608612f9d782f52746c2c5c83c8e01cc0b5afb9b97080505da0ed526076535d4a34650979f8f1f98ddaf306fa58591a92e25a86a1d62a3ba6d6b53be59da78c1b1a3128059e51e7fdef133a3e0979cfbb47040a51c6e684b6320b624ee51f731fd95ddf7fc672367b4bce94f92714dc4ab37394f3b3e612dab56829e8171d3af31a6cf940504421122cf830dfe1783a42dc48c2296849ef352bf18ee96eb5deff308e094b61e61eae5c02c14320345cbf250a6c15f725d6c2b12e8a10c1331f91d4161667dda26ea1f2a7cbdcd1d73070b70c818d9f543b7b3523e02b58f08f6858b951c735820579cf0ca7e4dff854cb2414a29556658374c977897ff125470427dfcfbf5c8bec622fd5b5d9cfcb898b3ea3846440ecdc29a7f99da330597db06d49dfd085d0b56bcee9b1031aacb1d71d7df7509b2cd76ab53620623cc85f880037e10a14e6b55758925f8ae7eac9489aafb831809662dd12013e9e8ebf67fba771c88da3157aec7ad6a4ee554abe967f1ccb486c47592eba5ae33812285bf3f26dd11d232f63c24a5b6e5fb285aa8950dbecc16f501c87665df4d159b307d36d554d54240306bd6ccdeb6eb37648c5c2d6fae684e2fb5608c2acfffebcc595b277d515158a141f2c8f2a005d5ed82e875c9ed3546149042a2dddfd82107d3067825968eb4cbe455b6b2f6ab2da38c3ad83a3a6d87fec0ff797916e6a5220218436a438d6bb44dfe5cba3f7602cbd7fa0ef7d000b9e02b05b4b867b1eef9b76ecbfc2d6f2df9955e4f8ca9d06f563e3991d86e9f194fad8d7c05e413bf68f02c5592696cf28f51aebd5fc6cd1cd76b3543b37f994c17f83b79c7920c01ff10d4d97e35689d65913b4fa0d5748de37963cdb48cd1416d899a3083df547241e17f5f6df8917ccc0c5639912eb99ed8849a2c8140187ee114fd3253b986c3138906dcc2db911e6bdfeb32fd0c4b8346d3e2b876fbe3d2f95e752b71f94c82be7a77b4ae73bebc06d03e8ea40dea94450887ba163826dfcd21038bf7f560db0190165d83809d398eb32f038186ce9b49ecbf2a9dcfe0be406a71f457514a47dac76990fe20c074893a34a8e7f59d4a945e3aa4e16b6c37a28d9a132cee8fbd5c7052ddca49cfe12a4c14e9492f2e6b480aa70e39e46b481b38c7ec36d24fff714a8464e0aa8c2dc3bacebfb59adc6a17e5377e6fa4e70af286e318b47897ce7e75a65ab445bb64ac6159ab48c1310b641fed5b40c84441a093af75902be5401a3304a3f48740908da9209ee6a66a5442bb3eb344fec8905a7b809c531fc788421da2333a9c3d84a5e0b2c59bc8807796da4f6924da6a3ef92ec94107b8ba4092d1cac44ff621db09c007bc007040006570794ab5289e3a323b98e261151a96b3ea240c0f612015d99996ed87511cfad3d644577ae4ca93a14fb250484781975404938bab804f8cdd4dd288ca384f7430ada7852095dd0b7c04ae9931aab4da57816172e71a85ecab00f5149e9929fbd4dfff8635f54ddd91bb56a86dd60aea8af18dc242026dad7b52f271db63881b39577a15f5b8f357d3ccc8cc6d79665133f571125dd592caa7600dcd7d72b5ba73c0edf74389a8a6e3d4d190b76a559a324d0fe39ea88bc6bc8c3dc30d89145f253b354134b38bdcafa3936aa1eefe10c806c2593502f0dd7cead691dbdf325a7b72da81c7427d2088ad9485332e4fff004237cfe54da30913e7e0f5cebf71691ac1c38731c84d91a233a96424dc976ebed809cc7c01a681f7c26ec078dda8c46066bd2a07ac4df05d18920f47aa113136ce45aa04b9a4732daf0450a88bd175b8086c4efd7992f21b0a0a90e00d3a17a0b46ccfe9dfd9fc901fea75e74d9d127118d0f8832cbee68be4d2c020350d533276cfe5b9d606ffae3e7492ccdb0099475b66c33ba9a1d6f58d8c8de19b8475059e61907a44883ba381ccda9e272b16d797779e4a1b4e3db34def79ba78e8f9ccbf592be4a63f4c9170f2c304ec65a8db539e72e1e5217209b0b38b61027cb82ecd3fc60dafe36cd476cd291f5dc574f818a19ca74d73331e0c3297e25619041b7ba9412255b10df0722463d17eb600aa8c9ffe3f43df2945252cbdf52113dfdb052bb2491299113c3e371b2a035f9b323318f17923f807a394cab6729124845833b794b0454c42c088e119110d767b5456c82fc28a2048925f5dc54765313c632704493126c75f40a499f6408263e61162357d5ff80e37617e80e0aedfcfd0284259d0e2bd644d54ab3166a22630ac06ac802e97f600a73b0e38fcce39189828cf98e1f5c6e8a7dfbf3670ec6498225b00446125276b6cab6004bf4d2e8c1341085b1ac9aa127bd10bb2ed29c7dd74f78baa4061874f24fef9d0adec31b81a46cabe2e860d890edb27b2c7f006a37f29b9b9ed21650ee7fc27f8fb7e16e4cd947bb47d094b26b2def138f04ab29316ed57f12f3a13e988810c045b7e35f1451776031f0524e96d1d4ce2c41a4a35e7e80a127620b2252f27ea3445b0cb1b49c4c33444237a279c20c92086bdc9b0de1e97c1a7a477dc0cf1efdf3040a09a8d1f3993682dfef3458cbad84470b94a52af59c2ba0f08d80b31954937dbb33cd743a099ddedf31402acc348f83e5bb821d185e14975e2a43e40d45e3da4b70fbf397db46395c95eb9176d70b70b1b4d802551c2b035166a82623a61f45e60b4c18570fb034e7061026002f7e15189b7c2ee30b804ca545894707287ca7996945929b08cd4410fcf7bf28c385be9abcdd0cf576dbf6c402c41a7147f14038c97f3fe8631cba55007db867fca4efbe1ff39f537548ed902ae01bd6a0a236a67c88a661dd930c15f017dce1da3ec5159d0fe4cc9cb3488ca09752bcec884d2adc6fb774eddaefffb1477d80ea9e1ddb0b7075ceabbbbb5ecb904866e0bbf0bf8f905b6f7ca5821b92f1109548fc33650f68a9b67ae20b6b165cd39de17f7691b8bfd70568c7239ffc66765d13b72db4ebf890a915d6abe3b557f70550be6bc96e5642b82b91eb10be8d669691df365fc53820e4cb6517f753510dbb9c51a8b5d38ff436fb0c61cdbfdd3f85f318897a64585a16af22cc782fa05fd7794817ec89270890d388c35c3abc1e667e266cdefe79211fd369a7f504a334a3fecebf3027fb2f0ab1af37090f97dfc1d8116ae99b2ecd742e47e48c399a88a1e1aacfb927ba4be5d9f0fb1789f91b1264d7e0f7edfdf48526c583b823968b28f716feeba8a87508249bfd938d756ec8b2e51f8f2624fc6467a7b764eff1384b306bde754b918a0918c122a7e6f6c1698ef129c99126f8d40a9ed97d1da1ca4c4fb859804441cad11ee84557921aba96371cb0b3a90cb2c0cc76c9b43d5cf16de51d6f43ca89c4017fceb239bdb708bf45e91b68fac6b27b66da9172c4d08a63f6759a8d08c513c1b2a702b1b51e1cd866f5fdcee679ed65dffc276cbe93b380acfec273ec53a664f559d29a46ae713fdbf96b1b23a1546aac5d8b6da6cebb128d61832d8a3b1e0587ebd1328867237ad9d43a4a2de95329d26ebdd455779cd19d4361a5d7fa45afd47068302b55d3efafc6b1e57c9e42af6e2507ba785c554eba19449d5f4c42e5acaf20e9ddc8ed37201c363464cc03d40593ef2fa32f81294d00ecf1862c683fda6ec4891f72a5b5b2b29f0d8c2bb415020f8db1ae7976b0cab93845b08d7a0842d6366e59d73b593b8c5fdf199ff6d6564ece94aadb59fed75951abd39f67a06030f2d34d57223b62667a8fa315cd2a27af7ced30d9ec78e71cb8d675d8d61924db42bb3105556a57775e7472e93e648d78fdbfe536e767a71079e1217faa728fcdd26d8be1cc1bfce84083d5272d543378cd430a096deccffed011e5ff741c92bdfdd4d42a8ad0f907d17490eca3fa52b0dad916189cd4b19161f886746a18b366d8bb1047746282d772670bdad1b0566b789dfe8348993a1eff2a3b03f51aaf362711afd6b0150ed8ee20b243fea04fd2e1f1eeb556d66b13f18ce72155f52af95cf6bb1c1a879a4cd9106ecbb5a6891c9823c3cb958a4b7652502e6d1258dda66af2136800ac33d739998995ca73ffcb541c37288b5fd898133d2a1de5c020154dfe1603b80775ff375e6cdbd69cc4557afc794acf9336da712626ed13e50fb60d6d7c0d92b10b01762dc96f8a7fd7facc6e090a7442c52e5e90cd3bd0a1359fcf64fe2a77a9acb296c48607a70232b19947b6d8dccb6adbd195c33aa0f9a3df6affa73afc9d96b17dcbd4e0035e005400e022883b79c11a9d3daef71c06223ad5a240021cb3018849dd4ba3b6772f103b332f1faa8ed2ebaac534ba4b46430d18093adca381454c5f59d7ce8c9f4944a84a5f9d598260b784cb284459798cd0b3529f76dc5dcf8507ebea12e2164aa7aacf8317289b02b3708bb25354b4f35f41134214782f6df124f096fa4786c6e6615be1a2a67ac0d8c74a7c5139b2028f074665a56a4fbe42a2b15709b73cd55e5d242d4fb1259d45c3366ad2494da03538c509456ad6beb9cb0c10ac61a163fd1ef3577af4d495141a9e6f2b8fd008c082e8b4592ecf66d411782d17e00c48c7e63980d5584786992749937503d3cc4c249671ccde9dbe9b4c4f9ed1da22e44f427466633541b675646d794894dd0e53223dfe3f0ceba6b969ce04421c876a51348f9022403f767466afedede7607bf8d06c31c8c7ab38661f618a55e9e2fad91ee8b238a3ca1c64616392b0faf61ea8135a5e4b8cff5a0a0008ae58fa407a60ab3748745bfb167713ff5c96bf9847f67f974328cc933d76259899f32c70f5e0b15087641a9fc09962d167cd6a64d5c251d3f7e751924e243c9fd41a475ac5f3bef284470f4510c6f3250fc4ff6827f3c59bcdbfd166e593e386538b0b3c2f0085b5f6e271371206d6a61a2d8f74246f12968c462cf6c842999e6067a9e8a47c1edb89ca69689ab583b397acabed4b22d100b754bebdf8f270c0ba9ac8d33f68609c55f94572c5684fb0578f795b88b926ae7722223bf3f32e4b68be8878e842ef38be46a23e0904688447e70ed3cb93ed194d8d4bfd24b0bccbb39f92a553551bd7a8d77a6d6180b90c61fb3efbf6e6dfb987bf028dc61e4c22c2fc1d714fa7e1fe671925a1de1752c563dab2ac372093a57611b196db489e152e342e49b0dd2d6d84aaf0baf849db17bd993369caa66b74282277f69d18f4b009dcde6cc3305817035a1b104d056507479d53dfae3386b05f6b4688833381c18bcef8a3e6ed70b47d21085c07486b5232a02b5d64f013a0fc6308d874b3fc4ccf44e016b5456efe45efa0df4ab239aae635e4f9c879cda1b78fe69cfba7b93eb4a36af3d20600fc42c0ccec24639dd53d3a2f67f7f22e8d744ae9917f1cb5819362c38f5b4ed200ba23f4d6dbe5091aaf7ff47ededafcf23421fe16aa42a583d3f8a96eac23faa269f9d001fc00bc003045006cf1a21b65f26a45980910e2222eec2aaa6c248dd1e433ae25f22b186c631ab96577a3c0cd5dcf5bf48162885b91131756ea916258ebdeafe262bf0deef40b0093788e97e864676f127832f5540ea04e0c737edd0324a9b4723a807a70a35705e9e27ff94945c9c47c8c5312e5ce4a0af4b243e210c15223732371cf89b13a957b9a6c44293b0e7ecfc6611b595046bc3e7345bf92428052bd8264db5f2fad4096ba44f9bf62ee1c803e33bb03bfb185b3a966e3c87fcc337331dee6f79ff3afd6d50ad823ee9aed593763b77a88c9ea33d6104fbb98cf0b2d60dd4eb28f4f977b37e29048f01a646df6101aa7d44dd1e29671af77a71d1ef3827d736d1b7f22427e63a957ddcbf65f2d4533461efb760bf8574a8649e87a5bd2db0f50fdd1d89230dbb66dff78740b2bd95dbf78aec6c2e3a89c97c752049126a52a7b37a059246713055139abc5610499a452d2eabe40cf729fb11ed87bff8ec1319f773ce2cb50641b04e6dd745879dd02cc01768061040190c8ab6fd4d1fa6bd1c9e3938c51121514568b61506fbd696f91b12600f0273f3ddabf8d9b573375efde5ead4ffbbb9ac7cb60d524cd7ed46ad5cb84dbcad7795231f0d4e7c05bb30cc31b9e02d4434aece3405f1fa7754a40571982778b5c78af4c6a6d62f0cea4d9bae5f015aa987dedcd31fd22fe7a8370399cdba6d68cae1485de5cc3ab6f04a927da53bd7fefa2ed7f820d4b677a66749f169a0d2d5bef60435edb3d701e139fac5e6ca42951874d563068adc4ae6ca0a633866169afbf8b92f23f37021c301edcc2b57a9126f0df6f9fdde4806bbd2fa3c9d8bea443013a411a3fed267cd4854669e5b710e5d6732a9bd2b8e9d9a522204e491501f2347df956cd008612a4b3b8c5c5326f5ccb1d269e08b1efff02a1074b3e4ece599ff26d2bb2dd6ba42f969b12c68916da13ebe9f9d19bb7590e545a7bf053d8181dafa54117084c1b24111460acf93ac4a85fe695fef00a0a6da53b708c24c601aa0e329b653d4fa11113fca0185d788baab7a647a5ddd6fd6780874fdafe1d1d27dddae0d29c3fb4df510b44bef18a216b908522ae9b6c8d0323222fe732db82d1878279426bc8ecfcbce218a381e96bcdff308be996b67e7889d6894db070fdeec85a919f0f1b8791a50921e6d7d8e943c05057ddac008ffb0c7b20a3905545ca1bbbd94fae6431f5b5618fa953a82db758d7f76e73d231689a5e70930b122fcf4a060df8bfdf47159f7ed9e0b0dcfc27a352785e9d8403dcd092c9db5b749cfd7aacebbfa96934bc24de29a9d022216ab7534c3b15232f5e655ea9173b20ff8f45c5e91ff4b8d346e4f8c2059d514dca5cc11e066d208f0a4873eb59ddf61f2516ca1be3c7cb2d913b6b1fa8329f028a4d545d751710233e2f65f7426536eaa583e574c80d88ca4dd2f98674e0aa874fa6f75a94e5e3128083df9d5344c3aceb890ff0ccb1b716fc3733c61f149436ac794a863ba875da7afd49c5f8a19b9a68fd3f236ff4e5ee684beb3e4a63fe2604b10f18ef8e72f7eff55fe7e0024267be83743fe57fcc508e9fc177c90fc9a73a3346438ed9e3d5d3af443990a19627a45cf5b01b5cb518c07a27dc8ce246156fcfb5b51e9adf207b4eb1a2933a179270cd30b0c3d986254be9af0f8d4069cbe3416a255eb671d86451895bac7a068119f19c53662bff7fefb5883d6a04cf7082c6d990492ba8782025d03f01e753eaf55e7e65289ba3719db0ec3461231a926ecf6ec6aa8e20eb896ead7a39180f113cd8a9897cc768e80b181c394a897aa248fd4d9f569af259ad9e6e69f02e4fdecfba5d7b3b72d97532a364275e30369d01ef8fccf43f7b94f27e3d7e6293da085e1d0b93dc0e84a3ee0b9e49c2fd2892f70306685aad4d2233ca1e4af8252708466c72c3a43b77dd6e2d0cce45e6407ede7e54e58802929790a1b3ef4743229cd3e136996a35fede076f4df911925cd2e3169dbfe7bbd611154e18f2b39d11d0c9def68e16baa8cfaeb6e8b4b1973169d3aa6c784eed172730a05c4b1f265ae1844edeb266dca67d20a98410de84a531cbf53facd4f3cab9d78f56db51418e1be62f2f4fb76ce1bffcb2e6a3a5a197b89d18f6c7adfdd293bfa66f918ba34fe5a3d97e138161a4dcd2af98afe9b5976e3effd2857ed07bf7809ab577135902703d0e5d081d02ab35a7b1cdb0e9c97509d0e7cf46da7fb775cd3504fb1647dc721fc675ef09925f71df66dc30efb66e7b33d1aefdd21740c769cb4214e07d890b1716ef538c4a5965b77e149b3b72727dd44aab32fa1506956a0fcdc8d7d47ac25d7d67371ac9c9d7d56f93e142d14df7877471492140fa36133b69443c31cf9dcea4ac4fc84fd93593872961d17616cc0467be8eb70460c676bd120cd72b0185e430dfc01f088fc3abd5cd0730708f88a9557e248747ac2197919716ad95fe6401195c745586ef38f5f0c2a24bfdcebd6d1e3b136e5e34ee9c5698c1f19e818d41226e43971614615c9e20f3a125408397e12f50ede77f8786607f6b67cf5ebc4243291bce1d7438d0154e929d38db75a9dfcced5c0949af85cb5cc91d95f5d64697dc21f37b31bc40ca9ab309d23d8fc50e9cca1bccbef27d79de533b2ca5f49ee17bfaf5afab8b5f9b7ca93a831384ee05dc6afb31fd2ce082133615dc36f39c9d9cbbb42e8e3f3e763d2d1f089c9b94f7ab183da49f68eb8a1648833136e4da99b873b4ffc2327f3a71d00071da308977da2e9cd2b96b7beb424a4c3127b7aaea40c8973fd9cfc3998d967c7c3ae522ee8fc7984955e54fa4c6a76e133ad7ad302b515303cb66282849cd139160ee7414cd878dd24e7bb858520dc50ae28295a32115147c8dc19c0d3e7e04e80a698bb02fb9a527fa79129daab12c97ae65b37851827246d3a0abf3d047a1e03624f6d3f6184650e4e225a8bb6a1120b40ad658fa729e17b8af540a4f5774bc56e9f932bab885d5272c78ccaba460cad5275b0cc97d098cfc1831b8d1cf3123819263cf597f95888194e54633cf6c23331f80a339f1a61af05017b210de405d5e3a5fdbba53d082765ad9c8bb82ef7dfb0ff417987de06c937b84cad437c75b5ef3fa9f0c5089cc20331d0026e0eac9176dca2506452e969731b61071c3ba1495fa089c034d643ba43740528e013008e04a32c920ce8041c026628a2267c648682026ca17e4bb2f9b95668bf716afbc49c8f3c56012bb8a6effd7393116de8692ce1b5fff224f856ff8589823734a5ee7403a8d900ce2854c5a8d60c6ce304964c3cc5b734672d1a19d0d887e33c244837221e52467b5e9036a4d3dd2bea9c69e67e57bec76a463bbc3fe5872894b9d69d1c7df3cf6dcbae55685c5d36724abc930b9368ca69cdcd38ff603a57cd224254e3ebdc453bd327b222b3da635523c7468f8eab0f50fff3225462567208e00c532778c98309d7c87d10af2e4866ba31f0a1a1803cbae792aec7290edac31ff22622f82b21c62b3f497371213f85aaf1733a11fdaf2fe5e7dc3cfd822e26cd1875171a034e2f30edc4cbe26ea0025445921c502e05707b34feb9069bbce9bf05898feff72f5f1e77255f1a3208d298b39e1437c0f0589de017553199314ecdeb8edfb2f131e13ef2b606b35db4af3c9abbddb2da4ec1dcb2efc64f38242748157459b647320d6150842e8df5c109a778f108c61c9303ecaac0c3b69c23d5a404ff7ba27b9b5549897f5b5287af46aa58a248dbf65f2b303d44190bd5d711a1b9ec0f9cd22facea4683ccc910379a9885a4c48ea91c76fb72cfe75fad1ab3d8eb23ad96e39c31aa7293040f78fb9834f3051225163d549a67bb079c3275a4c0a5442526eb74d82d36bd353af051c317c5fde944d5504c6967950f58187197acfc159eb9308dd1c9a26c8cd5acc4c568633c443475aec9ec74136afd513299e425e722a3b00c376a39c957306fc1352eb7c62226a5a34520da4f020eff85997bf208b018795113cb24daca8119d2845dcb0bc681aab967468522acb7acd7526a17dfd4fc2cac819bf477a58dc63fe4cbdb007a035d2812e8a677b2e7946a1819acf5ca664c6a4ffa6579a4ec60910091154d7ca9f90e864d1e9863ad9fc70b43cbc508f3e4dcdfb2cf5fc9eb64cc0effa7b6156a57f97c4302bca139cab59941aed5abf56bfedcab81803d909045a2cf6b9e0f25955e57f5264f631b382c561d4daa5fbf009882e1ef915a0910e76645e0669ba57e5d48dafc10bfad40534523dffb4bdddc029d6334aea481590718f01c01022883bbe7b3a75c8628f3c02ae3e8a53a5afc736198d9e1a92c51753043a293cb26428e921db44d36168611aaffb96e38e6ec8db2801b01cc4d3f0022d3677e8462972a4417f434937b70e45b88c6e3faef3c5442043d0d4b6bab6a0e82f5eae911fd5a9eeebeaa8037af63039508f036608a8cc909cbf586d391ef3eeb0448be00c4c03b93909fccfba0ff6098ced8fac8f7eba830d851821030ea765b73b9151454ab112a9a4823b6ed73f917abb88990397ecfa4d1c2c607b898c1e476b1c72a633e2881142158b30c12594033896670fbc0d78f61b46b370a84025e5b220c6c442834b4a9df12f4b29c55506ccccd04815759b2834d9fb2f39f4557634464424ba1082c30c2bf715c4bed8d918c3cfd633135bc8bea596154740ef606fffdd2593f20e472492f395d703e1055827ec740df862a70605baadd4d184f6637634da6486793c6f240d0ed081637c556a0545297dff3f8a4bc83498023bfe9599fa8f94f1b6dbcc3e0446b5863fd4eabd6bca97df8fb37ed6f65c0fa9356316944b81724f27755a4b05583d59bd9dad2930a1dcd205c81c9611507298b90b42e08b13ed2fdc0fb7c4d397db7413df47df41fca319d0a2ff8966f0206a3bdfa67ad9dc044e00b301699aa8ed0d14f61648ff08635269e0889418ebe7d04fdd4a1e711915770f8d5c5fed19ce15f2e404c51cc354686efc3fe7bf5fa0f03f3a3883142cda47d0c0f37167fe58d0ac94f14d75e2585d3b9823ebc963da575db5f65733b6d35a6938d3b78a11204e8a54d4795d05c739e46fca5c8239d56e29f36d78a1ebba04f918263570cec4dcff2cef06c2da0db3c65acda270420c976d0949843b7cf6bdf0c68354b30a9f6aa588c111b3a64b6d1f57690e3d46621af3139c26dccf16a09f2688c41189243352bfe8e8871e0c0d1a2cf971a8df844627092d80c16e0267c1aa7bc50f97027737c45c9b334f5b02696ac0e822c970dbdc369c2e7343fdc710f89e99ef05c6b82fc84a20f93c96ee951a47379b8e29110138ed75207b41cbfadfbfe586a0211515ffc5d3008a8b8ed6beaecf693f74f435eabf7265af63ec10707a0b2d8cfb733e382e8ef0beabee9596c775db147ffb5d330b3b741bedc412dfe606168ade1b85d34e15a4b2da153215af27d95f83d65dce00171e9c8da8d92fb810a1aa34ca65a91292c1a4892dcc81a8b1966fe2e8f1cd1ab665b646a69bed401ddfc4d3d6f578be09beeb91d81edd4d0ccaf0edfbc573d70ad478cdf4f5c65c818ed6fb224738cb64f7b80d0f66e8c6fefb6f49c9ab59f0b05c900a1f1a55d51bf49fec5a6a67d162658c4e4f6d2cbade0f96da86afb15bbd8a91e4090ed378a4c31f65e03b53c5a816eb483ec7b6fe36457586228326e551a4ce6bc904c29a499a2cee9e447d318f36fc52e58fbc4cdcc3fddb37101f554b0a4bfb93f047298cd073c583fa0570d0daa821d33a72b8e8afcbea0a12a5cd91517e49f594f0531a07573cb06f08cb895c5c82b6dc8ff951decdfe306b5012c990448bedd4df17502cc002f00231040199c6fe61ff532e7ea01be14300e2bc7ae7c0d236c3f0c09e978f354bada35e719f2ebda965aee8d27148c2efea242ddd7cd41e8c302a2e597d9b3d1b33ec84f07bbcde86ccea2b01591edf17feab8ea9b95744d5a6b186ee2ba42ca92ee95d0164187cabc59c397d202aadd2e2803ec978f8ea376d8ab046d950ef3a4efc2defb35ff0402ec343cf1e3e70ecaad69f75d1c4e03ef951e8b9d3bf785d178ff19ff1432cc14b33808b86c1c39ac9c19c62fad10f41e9ec8ff95f556e4bf127e40627cb7fecde215197b1243fdca58c3ae8542cd874fb542e9f746ca7490edaccdd91bf8f4bff7bf6a7bc40fd28a67364db47f164ba8784e825baaac670ffed2ad9c5d56ae6f9a1cac9c43d28ca3b9fe28bb7465a4767ffa432092ca77985bafdd0a2f5bce2b6472a10a2b0f3cdcb60b14233256547d826b53b010682af15d0e29ce6b5dc0242533fd8f2831fec31d9dcb1f6e67e3eed94ad225c29dc040c00bd0170450062348f259d22c13bd80a59dffa8900e33af85c1012652478e18f0e64815204fd417c4bd0071d79b5e9baf904e20f436e8dfa9dc4af7b2f0f06dd6901fedfb275664190bde61df2c7b7849f0ef697646296fe42a684416bfe2be846ff4449b5cf8ad658f1804d90195c10324cfb071c764ff61355c64e759d1a4e9d631a6d78a760a139737763203600145505bf1a7f04ba4106014fb9104b57a8b44dadfa4a7bc1ded25dc9c252594da3a5f52fd364f29a088e9f451502be292785c15de7a651e3ae2a050e0539c5981c2d3406d5a0331ed451d7988b643bc658d258b4f47506bd02d6fd2e0775bcfa91b368bf51207ebd2d63180cb0f02d5b9f6be1b02aa1a962e41c8f26e2ce9dc15b131b9dd4e547fce08e99b2eb1e56d14e19f697bcec0710c7c60e28b5d9af87d9be14614f7b6c733c2aff9c7fba1f36503ad092daf2607896b06ab01fb6d1a4e4961b9374353ef340b4a65a2feac0792efccb67f2749cd73a60beb76cd304b2cd3e80832835d0cb1debaef54f8a3965a47f0993646ecdeb48cf792ae30a0896e1a1eddaf4f09332c1f352ce4347a8faff316f16850cb0367539f39a022bb34a029b12ef8b6712abb4565570a1d172c2bbd4b242f818b5af1dd46eaf106009a512b53c6b945b6acba91f1d8fbeaf224dbc904172c3e3bdc4fa648e1a240dcf2a1213529d6be1cad52bba9f74f5515ab08d1158cc3d2e6e6c9ca9a089a223335632c79a62c4977c417c5a48d1f63d6a0245856666571d55f03cbed3d07d6be645b595092b8d7acf7cbfd00889a5427fd546d19f44f4e1d6348670d91fa02e4ccd885f5cd87308c190bceba0642d7fbc975ff0ff58cf78a26133488bc538ff6cad84ebbfdb39997a79a0d99eba01310f9020803132216dd8c4fef0e8307cf10e309d5399dc2bee5d2845cdfd30320b212a214f8d3a33d14f42ef143cd33aec5a41d32d589b0ba5b6d8fc512ca40611e5dafc23ee47d111b6008ca94697177a14e3f0e66ab41f2f94c2e37a3e41717c7ebca9318d26a30d136bfe5da7ff73a7fa637f88d0787968986875d7c5d0d4da839ea1990c1cac315a187c3d3843ea9504a4d4f6a6b5da7cfc3b61b3ee9984bbb9789728e94c3663e2bf5331bd7f703d6f40f424e18d8adc839d2b121f7b4b4d40f0e47ac4b808b1e7e45c0204c2fdb2da3be8b59dad1224aab78ad447d52823c386f976d716dc6c6ca3f3e7e41746afe8e9b01946446b6e2eec7ba94db910febfe1e7fa52ffd6390e7f9c5eb173a4ba590f593df45651dde0ad68e535d8a23c46e3f6a7e855c0fc5d2190b57c9ddd7843eb093e5ff98f052b3b81808d803e9a88d9e5fa48847a3c3d18894ce49637bdf211866f2c71116384c40c82236cf84f82f213bcd5f4df22fb0f5087ebb7d344d33bf3087939388b8ab9ce39e4b6766ee84ae7c812e030bc16bbe58aa5fa7837f36626ef47b1b13872194d585381f3b17b488e3a0fdee45f5f113ada681f9913fa2bca3d70a0e7cedbe8c5dca828f116e9d4d2e7fe9cb25fe2fcda8322869afe254eb254b869d4819688782076bffc273eb9ca69a8b357a0be9682b06f959530a848989722c8a9f2c79d8f07ed80a76a3ca557280b830432de6571ff342b6b3a7f644aa7ab96733de40e5989774fe2e0bbf9370e58d4c6abedd5284b6c9a8f140459f3b5289678947c69769fdb20295a80cf26fce7e8c5b245cf139365aa1b8068f50c54fa1359710bde46fd74efd941ff4ef66345f117b0bed8346dc09dc17a6a64093a686736d4c4ba9547503826011b8fbe3a2cfc7ac3b51bd6a575eebd016edc987e49d435d4c2db9750dde190cfe44e96daa99a58c0c6c3cf24debafaed0610c5e6b5ff575c1c5f711ed8e3e3ba9b260b2febabccdc9f54e6d0f81c8b93fa036aa6cd9eb796ffd49c1a9ca5563bf99f01e90dd8cb3e365fe57131e7bfa15e52fe3f60c1cce049690de1ac72f45d1849ebd53420cb136b071e4ef647eb4b9ab528ad0f9f7c776f3fe42c570bc533e9f79cc793bf4149a78ddf0f8644bab7724b3ff55b884c4aba7aa6bd601269109fabf9d582e48691530d09f34de8658d8dd12b09755cd0a53886fa6d919cf81f77f52b5b0b9fbaf5d03d6a4266cd695984935e852b7a70cf2565496b84d3372e539a8b068109d44ad090b33d057ca3643380d1cfcbf5b34925e368b91dcf0f5fd92e84e7daf14bb907e6e4909c4959e885e9ba5e769cc476ccd9bddff07446251f9ac93afbf664449d60c7c9b56d041fbe584245c4ec8c7cefaa11f7984049bdccfac10afc31799d781b91f7080d37d443819291db27ad7cb70241a7da327ce5e22d76184a4e08bea89246c5b723374c084da38764edf91346aed329eda99668a889349467f752567ee00a5542efbbe2e158744e4e49abefb078a15efdfa1897f43085da7e1295e17ea626789af9b83d13c23faae98c6607da3e521fcf7c36aefe7d9b947b8cd6fc5842c8de3ed200fae36555fc510d0af47ac08a5c06720884a4c8ab90139562dbe6359c1926b4d5c93403b5021b615245b7e68e47145c9172e3ac342bd54a17fcdac155cfd933b51d48e5f46bfa8b11bf8165586ed2ef43740e119efb1e31ff35e828469456b8ee8a9171d8f550785312c3441588a9450b2832e08d803c13466e342a435a862a150c6dc29e0104f012bed29717adcd3c4992256bababd43c4e3991f7c5725dd1b2a486d2ccdcd6ad948f6f53da4ebcd66ce794f2abd5d363b40cf21607475c28680caff3be00ce94d4d9a2a1fb430cccf8154ea335feee4b89fa6839ea9125e97f068899de6f916004e229ae7f9b32b009af9398a83ea0912a27b379202750ce4f5209afb9da6331e6172a4c286fe0cba6e881758423c02a4bc99c363cab1ab9719fcbe7e37aef692c5ba828ae67a208bf5d5095c06be00e7b786da7d31f4ec72e8c69708c03a55c54a84e4b9bf706418629a62ea41a6c4ab7c858459ee01e940c9c99301d45a3c16b5c980fd751d65361bf64f20f9fa5cc207e998e46236a65d393b22d15ed8e388eb086104cecbc64b3aa15e025f0fdfafc889a3ab919923e28afc60ea724e405881d8096fbc26ec3d9eacc6e8e5b59b7bd10806e2b5a45af5059153df9718d2e85322809dbed51e92a096b82f27e8ff418400c314a9bed5e449de8c1b9493c4cadb7d7574c3a1a94bfa5c47750bad7c9d7dc47be8d683b892de0d9882d6a414f36bfec308742689333c6d07f88c8494a9fd52d0f5094c6ebb230b8ebc4cc9797e1d64a21a6db37130018abf696f28f30713fb7c3a55be8bd80cee89e9ed5295e804d2e48b10729b759d3cecee1d6d11b987b7d5678b6bdf5cb6113adcb7eec8af5362e45a1af5664bd85cc90d627e62f1f44ec932b74766c1edbadadc5de3fdf59c05bfbac44307ef94bae54846519b4987fb4c5725d593a5d84e635a912b5203b130482d897b8001a12a1fa4323c31bc30f83ce9caa3e5b6802130c69d633fe389c8c6e2d9b110b5869b54a9c9df7327d9f3b8fb46bd0f4c9bf299e5ee4b181ece08d6e978836aea653cbc22ced393d749e956ae2775e877dd87e9848c681e4af9c29f0ebc6152822318c8b32bdd3dd2388a0196fca2c6a176c37c645686ddd5e359db948bf1fe122958c68eca414f5a3c2d5ab4f896ce4db22d09cf540f6ce296726f5ec1e63203f79238fe75a7468ee51ffff67c4d103129a9d9c97e8dd8b8d0b52b6afdefbf1bde912f3cf42b7bae14dbb98d2208293bb0061192c12d525e1e84f0a83df6778c3f48d3c3bc0ceb68a374dc2c80028267c73fbddb09ff085ce5ec58a596f4058a3579ccc5af4e2717e1e6381d7cbc8accb65d85e1f787401086e11628b16e58f9141362dabbc566866d906d813632928ea551b39217239510ed37eb745e378f69fb0796b442ba11e8fe7ac3c0c72dfd737961a61ba36ba6c94e1873e00b8c3108a00ca6dc1b55ee524f6e0f17fa9ad7899050d1fd01134658749cda00ac9d2ffb147aa745e18dc677c36eeb1ae6b903071c3aaaed860ba4c06f706f7deec7eb6977de1f2d78b1df7efbae4acdec1ec35833f55321d4601995a15271f1b32c60662a428fbb3ae799d827136e0ff3496a6bc8251d55430631cfe500511787776894147330030fb47cd62b3cc73104d4b759ace3fd2cd2a936c3e65ff71aa2012bfaf2d7c47bb33d2885a6cf1b75504d4bd007fc59c947270c49fe53976cce349ef177c7d17d209abfb4b1cb7064cbbdb711e19f5194bd0402ef97e6e3210096b51fefc8985babbfb642d0c76373e1a23a8690662f5767d8c67e3794ed98cdfae16981aa5a008fd3fc8b41dec0642602d37576d01c2b87dce2eb5575143429ceaf6ad2fbdd709012a937280d35fb35e20ef67498ff72fcac92d25de3213944d550963c9696891285b439efe77376f2b9c8b5fac954998475745cbc76b3898f9eb09fe33be0b7619e6ef6379c41c0bc04fb0f15c988426fd51853be56025c50452791a6e3341fc5a558223d3e2aba49f5e3ceeedeffded3ed55e615118dba1fe14c4fa120a5f6ffb1dfe0794802a11b041b4d83fd90726e285cb771101e91b9dd180d42f30293e0df4f8952f1c5cda633136f1e30c803653dd90683f5dc722be491434fdd504dff1c917432e6e04065c1044b6b38d1d61b57f4eded135d7de22cce4eee11cd1e20e7f27536a75c291269e3c0a229a428a701de5d562f79c98bd87622beb7904f17119ec6ca8918ce4fca462efd6541cf982dd3a411f920068679b346efb363af976421b78dad8e2104a0e6b0cdb7e79daf967b66e68676044c36ee2e350f6f39f5120509e004ae7cd96542fef78aaafb64ddae778f8117a19459f6e638a969c3e166d8ce1bbab439a834621dc41f1f0c4e9fef18cb6d2bef30852a499277ff3fea4c5f79bfd894354d567c17b38e2e1db4874cc61e28ec951a92567d3eda5a7e299fb84edb235b9785e066f2ae4d483794dff059f9eab82433676d8db696ca98a849d61271c2eeebe6bea3410723ab20c550b62e6d7405523763832d5015bac29e950cb0b96809b41729537b627496f10cdeea00fadfab49d15e4843cd6512e2abbcca9e2abb631306080cf3121efe2ba87fb9972bc28965e59cfd9d34e3b9b275b43e793524daa5774360881a31f029181ea4a1d6788a2c1452898c89789b46ec6a8beab6d9aac3193c75a1b6f25bf5a6dfbc80e650840aec9521c6e739094e0e4398cf377897bc14d865bb0ffec2e7d67cb0c504a38c5cb98d2c39a8303b5b13eb7290c8bdf7d78d59bc1e4a2918eee0a5f4c28c1b567aa8d9f2fc7257f94148266465971e0946e55cf8f78b9c49fe2ff6dbb837d93ff6457d41f1af321c8b513173a91c6624eada68e8b91035e47133f91eed223fd86564acb10f1718adf5bbc81cce6cb2d7acd4f3c1b2f334b7bdda2a289dfe1008f6e702dbcf3fdb46d39d3d71e3f10bd2be6d15bf30f15da1f49e98191ed705e321c2e428e8cdfbe2f6ea9a714c2544c7b19f61e8e54af468318a3653a2b5d4e770" + ); + + let mut compressor = BrotliCompressor::new(BrotliLevel::Brotli10); + compressor.write(&raw_batch_decompressed).unwrap(); + compressor.close().unwrap(); + let compressed = compressor.get_compressed(); + assert_eq!(compressed, raw_batch); + } + + #[test] + fn test_brotli_roundtrip() { + let raw_batch_decompressed = hex!( + "b930d700f930d3a0a8d01076e1235e0c33674a449c13fc37ee57f9ea065bf41af3aa03d5981f1432833bd0b0a0652a19cd927ae4a22e8f8069385002252d78e1c3cc91a59ac188708b7074449184766cbcf3f93085b903ee02f903ea82014d884062b70d4e215ee885019d47a37c8543ae9f382a8310c97b9451294f5cd6e52c003ecfb412ca8b42705c618d29883782dace9d900000b903690d669b0cd98174ac3b57393839029ac04ad36454109851443b4f6580664fe06766a7dea5b1ed31e14e7c11aa738eecb86e979f874873cd3d7ca9481681b4b17d134316e7bbe828ef69339ef85c6f0e9dcdfe1dc85309effb487569383d5464b519bdc1c85fffc72bfe93d4081a3e1b75e5dd39f95a91df0997a22d8fbdeca57a8b35b4f0e277ec8502cc55581a94eec1d1000b2921b4d7c3985ace205713641d03c3975e4049e13b3d2c5926b224684e38beb3b8d2e5d4060b109aafc3f2d144783aadf6086aa1d5a931d21282711484a9c0537bd4981fc222444f2c057211708e70dc4223063cbf39e4af0b795d3ec0dfba32391611d151145c1b6bb33d53ce2bb7983bd7b6c1516f7a1a719fd876f4b20910aba76c16dbfc57199a60e2ab938bc285613c3802c17aa03cb9654f5142d607bac01293c9aaf4e58b422c543f7e5e458af0b7cf57f33109558bef71e8b5506da723d996eb8e2c265b1cae43dba571d07d3ea1bcfdcb73089597e3744344e049bf21b4244d5aff60d559010b69a6335f4bb21178de504f50808204da652c7767dbf11f2a34b4fb710e6df9ad8810aa75dcdb2c99dfe9bf898912817e490b4982d44fe09f8adb43e0da2a0c824a9069ce8cc36b5fb0074c2db895ee92d92fa6b7efdf5c97ae05ae27556bc07ddc9d9d6261a53e3a10c350c3b1da26b27b345768e17da7dabfe6e30e019c88ef4a0e8df840bbd3fbbb639edf775449d8be7510cc811564789b861372fe97f7b5b1389f20c9872517634e9225669ee80cf077f9c8606cdbad53819a875ecd9f7b6d778c1dc302ca19ae67ffb054eb99206fc90eacbac8177712d0b4c72700df3f5e2c88fb4e9c8284cefa66390a78605ad9320aee34f72f3cb263020204393d9359a65f48b0e6e942b016a1f2c5bd6579f0a65997635ab15fa38db76ae8a5d3be516441499819bfaf730ebaec389db082e41443660dcc6280315154888b9e726b971237fae5e06b01958aac081398c814e446a003039dd090c0efa5d39735ed0ab46c7b4e4c960ae414b045fd19117089e65aaf3779cc9045d6e62538b1b75c2689d23ba3c08ceed46d4fdf9b969b34a1903ebd96a3a6b091842480e638b095c1ec11bb5c599668ea1b0a5a714d13462edb39dfd992b569897ac8f45c587182770631c262fc459afa6f23d5670eee2aac2ddaa89314607d30c6bfd408980c082749ad6b48a5310ac75b880cc080a00b5d23a075615f50233ce278d11b7b0ba0ad6a01486dbf31c54aae096f0f066aa02d9feeb4771b5a37d1247a4cc58a64d392f3916b5602d9d41d97b52b391ffd47b9011801f9011482014d88a793ab3f17510b308821f5d9030532aae9831708c1940b6f262f685c8d0ff7dfc9ba9686d8f75b78923c80b89f7644852b70713a788b69f191c54ec8368a7f2675623b2369f9078516605d0d4550ff9f5b92b9da2147fa3a24cc17605f30cccedc5bacafb2bb86e2640db6654a514b8eb13d3c3ab6b5e344498de0c709dd9bef58a8af16d3efcd2c0b2cb69d6089d0af8d42baab434dea885253e42050aeec01f233e64289b2e894c680fbab4f25a653745dbd89edb19d97e35bdd4293794c69503b0e60ed9cffe7e9ab3cbbc080a0dd08ebab0802fc61ccf26c357b638a55cbcd6b366251c17e2fa52d328d9d59e5a027d334772553048d6b76fc39ddee5f85363810c235219356cb4c5c3dbf9661d5b90298f9029588e383f18817bb0d1c882c58aa6b12de88f3830a7831945c1c1314ed944220436fad3742023cba2a71c4a2886124fee993bc0000b90219fb039c014cd76a327bb9b3f59e8176f377249385e67cb1681f8eacff1dee5a5a949511438ce370f8ad6618f3af81cb1f775a0b365546dd7791b0ad71fb1f2f29154265a8175b7e518580732a5a46dae3752e1234ff779d4eb614af2c66beec964181ecd0cfd1640bb2ca2b860649c41930a60de0cc754884a780488f05d1d5833a381670b368c85bf08d6650e26122f6714056382a006fcd5f9c97f55a98d68dd9293bb1be24823eaa8cb007481dc78a7a670123976e7b6e81fc223f42637759a0c933b73ba89a1d902c0874fedeb0a97dfab298972a18378539c2894ca6df9c0a423c2e98df4c133e5e808809849785b069e323640bf93d4b82a0917aaea8fda9a3072ab9a00a4b8b9b7b3a3eb326e54231d0f6a064cdf4a1fc06c961e5087359c029b13e229fb477d6651bad52c75e503ac45002a803a7457488966cc16bbc9be5c1c9a797d0377710c028e4f05a6cb929cc1fd4018912929252e04e107ffbcbd4c81ba01ab4b11faa90be0f9f9a6a22c87257e4a2aa8283e6f71d7b9e03b5308b16525c4d79705bb0906be0e947e8075ac6ce2235356aa0a66bec39e918e47a6220b322e326bf8fd65e47778e14074c47cb62b7ef8ef956c996097d2919df7aac8ea2ed69c1fd9f1d96b6b82b411c524cacec0f4a4269821fd6766d24954b8870fb1d85f5cda0528ae18419915a8b30b25baf6a162978a4bec86009cece83017d50667a202b3fad18f8ed8b5140c97fa74e91be608fdb788202bea05f469660e363ec580825d1e2bf753c01db044279f862720a27831744b91494f5a050fa7445e0e6156dfdb712a647ef73a2dd35b73d5cc988430c831352d4ac7e8bb90458f9045588a106e4c16d06833a881973c4c642fba1bb83068f2294050c84206ba9d32d93d144884644e5bd36fc92d0883782dace9d900000b903d9b303f8efb68766822d7eea21ca4b7c5dd79dce832c4893247f6784fe47cd7a18caea7b5b4d8bdf02da0276aca185add01fa2d16c2f1188ff7cbf6fb8c6308999037b2b92d725094d8faed86f0b1a45b55de4f36dbb71dcbf4be12fe624077213e0c170afbbbb546a343ac3f2a1333a7a7a7db7be46640a73d61b3aabc805b022be416198d809b62f99d26cf4a3bf555d40686f4b8970ec15386462bec5f2b728de0da047d6b3f3ea51f571507f32f047322fa204f0c5697cbb56b4b5c7792acaa40f02926651fa715a40e1f212c78cd4ecca285ada2c8cbb6e5dcfa3823725b44e29aacbeb9b6224f90fbc895a5980d63da46688832e9776b0666e90deacbcf8a4c559b625cf004cd04c686aaf9d7d6e2d394f5d36311f7afdcec5033daccc63c0540935f59514c9aa8ac3c2aeff48f624f2dbd38062fcd046651e92fc7ffce4dd914bb0dae704e5b26a8b73b3baef8ea022881e15666fada8e43fd621793713cb8c867775b9cdcf3b066582fc9baa705a0e1dc61a4b33b1b33ad3ba3bd0cc41b5850cadc04654dec222178709910209c6ac3db9054ef91facae2d729d7ee54898a18411b6d20d599a3de14d5375e5a9c90f3bce78479cb0f20afca895e40b576940e063587f451a8828ec2dd4a8538b4bebc39f72a6c54e379a07b7d5e0c02ccd57dbff13729bbfe5e78498c01cea12e830944fd0a123b7383fdcda97d8d9cc831e542ab6d9b36774d540b180c2bd52d46ca7f0e17d400cf3cd559b1b4e51ba93cd954777ba27a9f0327eb6c68aafe74fabca4610210db7498aecffd3164c5eef8cede655e1b42d5f54f5a52b4f5fe9698a4463f30f20693263d41074d0403a737c4d4986f0ee7fee828fb7072a80603613fb4d6c219dfa47adad433af6b437dd199f3bbc651487718b2e6d42728034c242672a98a9f36fab6d4162f4e8eb7bf2a9868cead8ad657a67f0aa50286113db972936260323d7b11353328151e80691d551bbe1f7f11774e15db4f175aeac5b91668a712c3c2399a977abb9fd9c2b53c5ba68f2c0ea353028416b36a47028f78918e2b205bf9b3bce6f1a08bd4448abc3f12a240482b4be98dcb77c74fff47e92d833735e802465e50b79d51de5a7fe45a95b650b051c61a529d5f51cd0c603a2de67a3123be1c52263e1c9167765b13ad1e01cfb27531c9203f39e8913fe0cab9d8c14b17bad0100b76c41d41d68ae3b7aeef5f6af4f66d113fd29eb9c4bf994f04decad13880d9d1eb3865a30e2540e86923b36369c121ef2a6a43a618aa4b15560fa806601a85be361468bd09c6dca39ad7ec44809adc0907dd0458177343a7c23330605b802f3ffd3ae61b3be952ca2effae8222e9ed0b6ea4240728a7800e4882efa7dd1ef8202bea05db690cab7dc8c52c2c375428c0aa9ead02bf44e2b1f8ee06e1cf7af25eecc13a07d967fb12e1f0073adac46e0676a6006b30d780e6a1387afec76cbd1f07016e3b9012401f9012082014d88df6f092495b7f4148840c5b5541d013c63830408e194aef36f2041e560a641af89e0ba2799ea630a9592881bc16d674ec80000b8a3afb9380f9228224c1aa59eab115ed4172b471aa2ee11b3d4ac93f4b6a33518007a798170801f4f582e188b489005d8f108e2a4acd6f7ac28852580e73b6a1590ea1af1443666f1d14affb0a9d0655a5c57cd4190b2a00c07276054641ee4204ed8a806ded2b3aaa7453c24e442992434d060b51d2255c1cc2a002264b5dadb32057f4a5d52626e0ff453e2f05f1e0d8294614916c00110853462d51d9ab7e03b7019c6c001a06028ddc42f0d3e1cd6cb1ed7377d518480626d56c80e6d15eacd42ecf2f30957a03f6e1098b300b6329997bacc5e667eeed72a38f6c4e1db7199483bc9a18267d8b90222f9021f88c0988653bce0e07388fbc67f04e5c6772e8311bd5c94eeecd6da1ee441093ef70d8c86a26f4dc4da11588853444835ec580000b901a349e745c1cca19957c43f15309935f7bf49547884332dfe6d5b8b9d61542dd88ecc61187fda813a7f700ca96e8847a33bf8552690d91ec8e8fa70c21b380c9c681b54e859add36c3c19e7fda3075ec1a3cf47ed39c89241bb73f206d7497f93c47db9a85be7135948e19809c195ccd4c9a379ed464bf77ec562e360c52b9225f103d323364a72e8a725ad2b34a355928acc6aa563b67d120ddf54cf68f710624499ddeb30b0c94b8722ef2d641ae49f17f4a916d54350ec483ec5bcfd9748e0a228c3e73cee9ea248ad85060ac51b3e6834e1f771f725a466affa28453ad3726d794caab223fa76c8b994ac5d3a1e8ee830e4fadfe0786174364af3109c04d7d607aca17933c4366d44d9c5376ca34febaaa612707eec4e2fc5c6b1668b3450340938d17e5552df96ae84a905d069f9e3455bccab30640a0720f9b4598d8f82ebd19bd32b7e82165303123a0ed80c57375174c08d32ad3ae354251c97316b2977f3a2fdf2dba1c595093c88275badc54e3aad65f77c56f55d04b1e6d668406058ea01da2364fc207659b028d9c55371c776f732e63255dd177b95f857e3cbdb4c66fabd8202bda060830662664d96755362addcc0908287c99c60761cf9c7a613058894eab6e599a059cd2461d4a89458dc68adf287fee71a783dab0aaa05587a21b4aba1ca4f5efeb9017801f9017482014d88d15c09b7ee8f9562880ae58585f383aacc831e72f6808853444835ec580000b9010a2e818d2c4fa7a974f5c3acf3c0f9439f4c83721b2bb9df4fa290c7fa57bc1f9f77e4b80866845a8bbbf8030b707b1f07a54a0ab901188eb2e1262a45618a08517f943cb032eeec926e4343d5d3089c145da1d53128ae901ce91a813c205c615bc1ce9b8658a9da4c2d258fe36f6ffb6289df910566386dd1a9f73b44053bb64523d8faf7b9055c592695fc426c360479c1e2d1f68ca5c7965dd20b6879989606cea7c0db28f27ead4a591ee264f755b7358146586c6a1a8530ec463dd754f100fac603ec3360c0440874c12bb179c43a23e40957bd446f2573af413f3314e9f0668af2491de96156a9bf35bc469d51935305f4df051580b84e98ec8395fbd42fc0c3f3e7410ac4719af4c080a09a774db7e3a26966edb91c1f7956a091425044ead1589f435c8d04aac9533764a04325d5543464929773cc6ac555f5ce1830c997f4d26f2dad5a7e056db6f0a2e6b9032d02f9032982014d88828a67bc288355d78498c2cc318542aa1a60df8305fbb6808853444835ec580000b902bd082cb3f3fa41ebf06fbb17afeed9ccdcf3d2999e2fdd1e1171e0b1549c06de17dffc4ee7785232184a698311c7487fdf090e34b9954a41affc0d0ad44104f70750f6a896b1b2b5ff1024de66ba877c5494e67735cdfd45f9ec0df1c198b357b60e4d840abaa72c5667074c43bfa5e1f07b5970f018820db6fc2bf84341cd024cefe455c92426f876e51aec0fedded8d4aa4003aaf6970c48d898d8d82a8411990e73c8ec792a2cc4a129e526d0fa34a54c37ac13ecf4e3c597304cdbd327704fc97f2ba0b110afee78da5c3f46d3354bd20f56cb91b7ba8d302422428082748faf8b4828ba925ab1a02ba695e686da4d1e759b6456b0388ac8fd769f3b726332be36d3153ebee040b5d822fe62d73b629a6251c8e49a988cdfe599762759df03c9100db5f7a87ce7102ddd21831e0736924f230ffe6aaf6b012423e351627e118f2bc12736a3694b5468858ec6310017b10de24fe75ff0abc060b1e60271dc5274b4bbf0b755a0a617bc23f57ee2286c805086d5824ca4bb6297545c5c1ccaf03be03b7df33c953ddb183730313f09c88392e4bdf688f1d2b730318cc9b148e488c2f1e383505a383672755a221ee7dffec5a4f77e7efe66043d686a126480ea01a8ef0f72f9a5799e03e863a85b7aa56c88b7575d6ebb9df809a240969d3a2b2e086e742130e38cfe7870db79bbd281849912fa611e04b8dd0dea9b7da5d16a66969e54ab9def159b9c1d351d719a93821c40ad6c6014644c5f77374cbd486d6a7cfe75d7d849ce240ac86a1c0843aab27fba4d317c725eb101752803ea67d3e12b784bb424eee6f766e33d6664ca113af63c54ba27b8a8e904c572dc3fd09848cca3499c403a1c601db77a7f36d244024ceacfd9d6ae494b7e7e0f92fa5f83458d5da139eb127709e3dd75c88fd5f75244e15f1bb8cdbd3056bfa56139442c0bacbf3263f29ef34946e928b9a4f1c085e5df3b09f31c6e87397bd939c001a08b9ac3bc299eff8eedc51ed3ff077e49da6fb145a0c495f430964581fd4d230ba05fef2837a800e231a3178226f59a981d2c4bcebc4b4cfba9680371da1e2c1a61b9042bf904288821c649ab1ae8ea668896d6c78054ad7a6583121a8994e3294b628e98892fc56ae3fcbce852265aa657e7884563918244f40000b903ac0177c66fecad5135344e89f45ec7e083130a3e5eab1abb75bab0aa357cf044c0582542047a3f9985d3439a6f850466061142af44a9208656e278b7ad1bd0e03539cc019d6ebf8758bde3e0489ba540c523f178a0b055c1fedc3627fee427467ab67545c154106bb9e0c12a7120c175d66f9e3eb9183ae5c7640d4cb4bd3dc94c7b4e0c9fe70e692c3fd027e0ebb46bb32b73a269037a76731a9f114343ea0584c3f7e9cb4530d086609b59ab6b72e7dc6c2c0c95699091e06a33af5ba200a168ef483fe11056330e84da4f2a59db72d5d697d262b9565fe81a738a48d24a9f1c8c49a671101bb7db5eb64deb454a117eb00f4ccc31bc93c061e975ab6d375967544a2a06ff8b9d59bfe1ecb1dc47d5536c645d764028c5de77f3f34d6c7999785b70b187d9ec4631e83cc69499a4ff8ace98a6f17b77f648ab7a07d5ee0558a8efc19d4601573156a0264d2e6574e867c1eca423eac1fdbfe0967bb8f02524cc2d9933141acf619ffe99483305fbdd6913f1e1feb78a17fc6b81c705c81eb08d5602b097ddec64f6c334509caeed7525e3e34845b21e56e4424aa9609f4df8bb13f31c5448b6bdede84d9a9aeba9fcc38a3c8eb1f3f31b80918e045266c7d69b252c86f8b5711b2cf7136e2c3d86d1301608c7c16655c3ffe6d04014dfd55a9563c2a307525088fd017486ffeaeed45873013a7940a7a91442b975065c765c32546aee9b001ba78d8563e039c8edc24a92f9f457ae28172eb29e16cc588d52c8e75a565aad1a8f9d6d341189a24718c26c19a83c6cfe1bbec2f4b878759a7dbeb4ffc0568b902b1dfb18af00c7014f2822965ddfb56d7aec508822531834ad2c869affba1f95bf3dfdf1d1dd1c2994d904b9c5133900962c8137d7fce9f0b9a7d0474dff9173edbcefb4bf355539dfa791241031e90770c8f09af595eb1aa0d083bac4fb9b929ad7e23c0fc8d3ecc7458a0790929cf7588cc255916a6c16811f09d0c972b294dee6e1f739c5e9d3eab8016b565c8570e41bcddeef2dfbbf95910ae6a46a2834919742ec599b9ed204d1f86ce6baa534039ed308d8be0d289824303deb54af5f9f50d88807134b8f42485cec121432e58b83c8aecb32fc62623b06c39c3f1e0e921b1bb880d2eb017578e5f33a25a335a813f02259e1b12b8a76a90a65d015bb214032a095cd8918b78003d310a06a246ac95c126188911bda8a6623407c0dad308e25a438f78c7409267b729413b7d248a6a88cd64c73118999f00981aa4f6b639e4252d39b1706c686c7763ae9c41aea7b46fdd48bc490502ae876175e5aff8361ccc530ad8202bea0b0209fabc8a5c0e2a5bd08e9a6b532d51670f41513cf007781f27e49b070ccdba0795755f4fe231840196d847d100e7cf1e5650ae172890c469428269cb105c16cb9031ef9031b882565c357c3279f0c88e90114422a470a4682e988808829a2241af62c0000b902b424fb91666edaa16addea67f72c9e0bc7a8053bda59776ede2a0ec3f7c78ffac0eee97ff259f92b21378193aeeadd0253b08897a14f10ab537db63202a4c9f78eb4b399d55c5a256a8414f58f45b109e6228a75ed1eb09627f44b56eb539c334df412b30ee6f4ea39a04aa671aee9e7157b9cb69aad4ab1d9d75c6d90f3488342b29bb59c97ecfd2bec4f991b095038b9e20eeb591b641f64e32e5020130f8a8daf7c51caf93ca460a4e60132835119f99d0484529cf541ab9f922bf15a782521a0f6739c1edb8d4bc26a07e63790087b4c098e4df74534340bf7815039326d1bdcafa53932deeaff03a31e97c6733cc702cdd42be18e4716dd0d014f3e916b0cee3a16bd52cf717f5efb59fb7e41c8e4c0d7eee8ba92ee5b293b25612ee9a3b0043664e918a2aa2b602accd357c8f22f382b16f637b57f2fedb7d8f66172f22e67cc04f230e28ec96b928f449fba63b7862bc3102181d6c7bf063d9376363b8be8200169aa88c46732c5ab1e19dcbd8abeb34f1e1cbc632484d9864e630c4567c0f04a2bf5895d3cafae1b0e70e4c1ea28d4d9578a82611f09ddb22c3c4440e8236be2bf9cecd3fa64b19930af8664d78d6f10aa9c913be537bf2b539e3a9042d5744eb3d1bbc16d98564488a51ba45edb2713b466beac560789c4eda3c0961bab002b95eba9f512108dee2e39a8759c04b18a923f2f2aab2e1ca30ec7361b25ae71923027c950c089469820a4ec3ec60529f1509b92ef04fb7fac70f25d3e5ea5c6a28226fe19317bd4d0f42085884020a2b22dcb0ed8e5600ac969b4f910e54f617597a84b05774776d694ba38ccd3d1055a7245334cddb1ca20d7e001285a57001d03b2fc1ff893ab044612dba9b311247528d7490a9a7f3e7c3ed8531844d3b829de3604e8546ee8d4c3d7a308d32035159aecfa20ae4660e6dc94b6a155aa78150a01fb0e6c48b660a0f051ab59accaf4508202bda080d51bfef036fd4c4ebe7151b2755d6606122e565323878701113b84fc86548fa06fb34b02deb66359ae8095d3c339673ab2a8b138fcf9aed2d4276c8a16435a60b88801f88582014d88bbd39acc70c3229d884ec80fa5565439d283119a84942d89ae04c33fcbd75e3c6c43b826b266625b854f883782dace9d9000008911d1f14d3a721904f1c001a046bf61e70c69943c277ef7d09ce5e779a10e3671cfec81423e0f951254dfaad2a012fa75748afaa79673d94a17d35666009001775a2b868b9b839c77065649bbebb90143f9014088e1cba06e2ce482dc8804b98caf86fcf0898305c61980880de0b6b3a7640000b8d9854e530ac567b7d29eedd91690a0d2397591c6a1b1f5068bc292b740f6aa5d38003a933c0560971d4701b31d537fb7c1ff68c40ef07221089f37671b101309000e0eccbc42284732aa002f2cb3197def9947c2b2fe47d3fea2efc71b1f3cd681082d043dbc1471a56a5d0a5c757b8c115277a2af2e044e56e5e3c2cf8756dbe51a347096a4ead46fe53f4c03fc100fe0009f6b2fd6ade28fc89230602e9221962f4512740857b87f415f134a224c5149e374fe22f3048f0620f1bddbc9acdc268a5de1296d265bac65fc2650b3de55e6bcbc26bc4d01dbf7548202bda03e35d4429ee24e44134f7f51b32fb69691a16c60a0347d9283a8e593d5a095baa01c590af4c1fcd3aca728bb5aaf03f48aca22c756a87607b4153a5ac6be59ebb5b9029002f9028c82014d88aab881c6fe3d0b7484b0da2b368542c231bfe483115994808829a2241af62c0000b90220a8317aae8cca53d039d79f09934b9c5d0b07bf13ceeffacf1011fda22a85505eb7c717168c18d8fb230a7a3f166a4e93326fa82884ad3093b5e07b4edee095d98bb92f357fd4a98201be26960d4253da6fcd09874b364595a47b95d2b50f8cd45921931469a302be9699779775b59f27deea2aaae41a010a47b825a46103b7d355f1c154b3422b4fbe4e62c71c5b6b98b627beb82014ad990bda2b6c06ddd237543b3652c7a029928153a8cec540311406260fd3a55cc5788610321d66c29f168ffe5d93f92378359231ff89492db2bd2e90a4d9c28263d75b77842584d253fd7316e61c27f71771ac7e7a3c8ae6921ff2280c459c36348e0a098fe8da94c1546c15db7968d6b2821b24edced45a7ca8f2bfb2b9bb7a497b950bdaaf771bd777e918887c0d2d6ad3b72c168228f49fae155862e0baef308ace6952606a660beee10da3fd2d29b5ac31f2d55e34da94a4274e1bd679fa42bccc5db074a070b899e28948680d82c7229223d846a1a2c19143dd99c78bc42c33490b85be5067a25f6361d6b803b315519de254191557ec691967ccc3d087b8799dfa5888ad748b7a6e164da0c726bc1f916110b6fe6a013ce0e28b79bee045d250657a70211dc11a5dee69a2c05e9eedde536a9911883e5ef2ee76729ff8fbc3aae0fa13a36daf01199a7ac60b21c7fcac00d7c6a80f5ce10b79f4666d69a1a45b3ec864a57f1f6fd492223c539351326d7a25b18bcfd8697f55e972607b9675b1d40dea3ba4c0b3c080a0e69a3802e5dbe5284f817eaa05c76127a3898633d4524f3da9ba8d7e7b98af23a05a2672729a0136c572a68b494cdd49ce47c2c0e33582b601632b3a1d15f3cc38b9016001f9015c82014d889e607b89f9d2717488ee3a5d83a713a9fa831ab7e68080b8fb754cefe26136c37abae044d7be8e1a3b8aa3ff230de4579b08bf12020e9ea66a2f282ef549cd7f72d056ded10c2fa21fe339fe56715960a4bacb65525bde1671a0a691f44c0ed582e64d3799c4ee453a4fbb700cc130eef66cc66913d919b6a96bd31efc3d77e4accf3a7c695275188ed2e5a76526e4706bea7df44cf6a36fb9e43d0e37cf5d6e3c5b984062e57ceeb1c5e6a9d0c418a5a83b77c4c99e8799fba27bd884e51d5df3db1562fa0b13cb1051ef5d5269b4215078384fa84cbcdd93cd7e67d166ebfb88eadc77cfab6a09fd1ea8f82f530ecf62d60d176d3bdf4f2eebf57b45b532ba6471fb53312e32c3452ac69c7b0ce227a61e69cac080a0434df311dffabb4af9df6fd81f48814ad8f5363567d421c5466423bf3bdacc05a0032341e2314432f05701cb222c2868894039e6e156ee6872ebc8739a4c45a43db9027d01f9027982014d880843386325d71bf988456fca4e1ec42cda830601c994c5e72917d21e4aa0f724ed1cbe014171f1be66ff80b90203e082cfea48d8bbd73dc4f299c37a26fcfe1286a62d17e6bfd13084a47fbccd302a44770baa03092d7aa3bf8f15281bde3418b5a6f610199a7ca97fc11df8058de81fdc05527047d32e0e4527db10cddaa2e1a190d7dde1987c0501a200df8eea07d61ea0028930e7422451b44295ce91f79de155d6169bd64c0cadae791e59b67544023e5fcde77eb509d6418daa17dba99d0f09c23c7df78d609f4af7c1ad95b01c26edae2080556b8e63ac632d78b87eb57ef23791c2336775ccf12f62dba46b65a5b5c7017068194fd2b7bff11923ac2dba3ba0d7e28c1ed2ef1c5d2069e189c09bc51efb571c63f2891acacd6a327dc810180290f9699541f4b65bdd8935e074f80887d3f6f4c3ecd75a54c95476b26b42f02964c16ae02532433d48fb5b5f779562224d1bc099f51d332c67cecb1e619bcda1aee26011a463952719987f705b12fbbbf34e3989d6b5c5182bddc569fb545de391ef10031bf1b0f673f0ea1a9763f652624852bee8f09dd517250da77dd194f8310086ba52032212ed38e014a9bb3f47d8a16cd463a977a443ee02d5548ebb5c518e5a0125c6645f2ad2d52f99aec5c88cf4aba79167cb8f7012386916fe2b863da27d16a7c3c350442ebf9b54a569ccfcfe4f4e64853fd810e6a5b3b3cba9ac8525a260505d12492b99437309f94b91dd68c7658291052e2c4d414f87c1d7b7bde565791fdf99004316f02ef4d7c001a05044b928ccada6036e32565da0b9ac1b51d4a0eb5d702efb781a832c120665aca027befe34f4cf0deb37ef259882c20be1af0efa2ab726e06eb33736ab2f0b34e5b90186f90183881a09a2f1c8cde2c488c2eb098e1a51326d83159c2580884563918244f40000b9011b643c223acabd55c37efc426850758db45eb7a0ccb908d9e2ab6a122d812921618aaf4e30c377ed8c7c5b829846b473702496e87f2fac0a78fe92a7602239414117ba9d42c354b05e5561f234e4fc76ecf8285abc17060e980e1713a3f0ab031a53c6757c972e363485581436b20fcb4aa524281e6765ae59362fe284cb6c9c26e3980cec0a9b2f61d1446e9a1679fd055fca089b838872a26f866cb09ceaa5a57a061440ba3a342807d83a5a83589a7297afba2c456c628954a3daa451cb42207f9de22fd5dad066647b8e8ed43fccd3f335298291601fd8737a2ed69cb89e0573fc8eef594568c236f8f976870f2da93c65f77aeda9ae17d812e16dae936ca069e489d3d820580c636f12164c73795e287db92ddcc73dd6b341408202bda0b8ad8ad3d5218e0e27145286459b952ffce119c42b7b143d3ae68f08991c6198a07bd60b6dd3efcb39d42fbd3b15f2f65f9561ed6106484285f3a9d235d2962c2cb903a9f903a6883c0753f96351f096886eb111ddc0775d1c8308a6ae80881bc16d674ec80000b9033e6cc26ae2edabe8f726535a61e77b09496c76d81407ade4466993d4785c16ae669c39a5f9ee18875389a6004576a39465d66329e18646036b9ff5657ba1ec659bb2acedda2862458a642949d15f2108c9c9a712216e2d9d13077a134a69c64daa48018d835b542cfa7861a12febf7b79023af48f860377d4d8bf99639ba627ae9844ddd982438e2a508b6cb89c87d4b78f31e42f842f62af9cd59a69f4e899720156f7a2adf1d348e9b665481165af600a3f781aceea0589215f06dc022fd28fc6025ff85e3d4b7c25c358f35ed5f5f025eb2b0ec5511634494515a197f3e06f4e8a2fef699f33f58ab71376581b455cbf592e1e657115448db5237d010399045e023d0d69797131720de65ffba81c41037657951db3bd5fcc555b8bf6944a67f1fc0ae9ddecbdbb955743a86d2ca82b6239a47f0d37759cb3bcca9d95d7ad084bd8269d06f6cee9effb2173096ef22875db79714328f2d80beac6cff4b3f8fbde3ea1a1040b6885d86bc92390ed2efa52181d3fcf6b761c0a14b8417ea3878d311d3690f93258e57848e926364fc0a60dcaa161a1cd9ea4fda657c5e868f59bc6d2ded1e264a100ff752fbc32d30728f13d74f60a1931cf1cd302aec02f4ca94541335c0f0717cda44c966db4c2c1e522794e0cc5a9dd84ed6355f979c4931231225096d3f651aa1970fd8a6de80325a6b7b3362b11eeeb3401df138bf8742bb94fca940ed45f8b4937d1645c98adad12836b19e09b59dd1e4cf020a2d4efeae49aff02a0c92537dfbcd4a560e876d0a3da71a38302efd5986e70a0592c02c4a8e5638869db811e47ce514bbe71acb864580d9f3be29e73f8af1584130a448b85c0a4a790d750a3d67a4f1c3e52b0db1c7ec28b891c66570c894b9955f0914981f28efef48616b004ca747fcdb448d0a1b6d7196e2ca002e17cfe65e7bb08027b95bea17ba0dd5b9a479726b5cd32a0fe24052c2afb163e60733e6ab77f8d1d2f606de15a31a2db1c8b7827434b64f794b808287f612854c7df802822340442cb00b8c508eb8d74a6334da415319557d4a8cb58247a7e65c74ef2238843fd02d24d6a859f02c547fab6e35903f69394659a2b1bb02fb89a613733cce7c4af817f6b8cf2ce38f425fa8b59b3fea76273664b8215d0503198393443c926b578202bda0115d2f3409265aaa2d214d11e19f314193884ce34c3274f4258d5f09a97172fca0418e2cf579d94373b0a81e66636160ad2f1de4597445af60d0ec37e9a97770deb882f880880f511ab07ca9dce1889745de5325aa780e8311fec19424eb7935928d6e5fc275944276ee070e90b9619e8853444835ec58000086428a36f8feba8202bda0d3d221e5abc91d1bf4721d9f51100bdb7e25f4e1b2eb363d200aa1b0c09727bba07688424185824dde9b365f31e258987ffcdbf3c850f9992ed80d0e71e54712ffb902d702f902d382014d88e4400f9aa703b1f98501db23a8d88543ec7b3d868309954b94e59842fa49a842609ce51ec1a4e9f75a00da8e1280b9025a30fadb0cd19a05ca7d20dbd28ffd1ec743d59a1169a730091be383f6c571c51a8514f9ddf9961a588f38bd388786c9e7efc5d0e71ca89e7f24a73201839f40e9378e5305f4174752c6eef07273a2c51009f04350abed1b6dbfff400ac6f790013028b56aa08f5090e4483b7bfd1b08042b8651dfb27520b3167e9b912e37bbefe7f13153571ef8ae23f2034df09ae737e672bd09d896bb01cc035322407ab3ca2a026f1d8d5beab70178c580a650874a57787d92b6f31f7f86ee939bf8fac22b23c6b6666b5e0241fb55dd4d397f1c78fe6da9fc3e66c2e34058e223a4567d259e3e1a3560bae9f5e2e3e7df1b7384b6af9a4155f1eeb61a6bf4b5e149db22109c635cbe9a4266ef48c211fe1236becc472cb7869906e27166f3f017ce75d188fa708e037fe1a5729b43892460458478cdaa91af1f9367cd1164204b240212101e631cbd027c814efd1e46368b37041836964dc6a76701c38810f36cc02ae93eddd5ebe83c24527244a55eceec6d47ec8df4b158fd1166a7d0d7bbee043632852ecd8e5aab24d71717a232eae9facb45b534f75103fc57f5cd8f978a362249a16e6b3783443bc5100bd1d8bbbd45144b7c63393f5d8169c4381f645bbbabc899e022d58e7b4293125d6c4d7ef75436b4542618636fb247b48ff823f52f416348fb767f6146c1f443147baeea5c6ca7fdcfe3795e09112224301f87c5667027b74b54dcc0f3c4e149a1e67aa6f8a940e1f2891980a6e565821a1f06d522eee5803650f6c0b8c8f5452804f9c456550cb8f1d4827c7fd1c8fe77b71aca3aef9be16494a4bf7d40b274d28ed9cd92a2169b6de5fdfa3ed1b6ef8318c080a008c406d42212f12e384b8f8bb7bb40d0c4660b67026646436ca589d143edc5a9a055fb6596377274cd6af52d95a127c503c0af5b7df6df59ec493d2bf15cf02bcbb9046102f9045d82014d8822e3c64dba5192b7843cffd35685424e576804831aa2e894b002add3a6fe3cfc260c378a187213b6bac436f3887ce66c50e2840000b903dd35dffee48e5855b9f4e7d47630f215334f242c738b2aaccc6e4a815ad70d29a94bd5fea67cd0cc855835ab9bf81c789806e311f744dfc370960d5246099d70e509571437c3c61e11c2971782d7ebbe3dd231c3025966d5ae37fea256ab601339db76c325884b7939ac8e772ff54c8196d35cb823cd42287ccad89e0f1a8092caae92612bc897cee16c73c18a39a5b1ba5bc5df73beb108cf5c896a420837ff53f6e601052ec017e75d3554c0ada83b7874ded4edab8b1a25e39c56c4666ae2812fe82f65f5f7d423ab3a173261ff29495a5ed0851171d1c261129b2062fffa4fc682cb41394f5ebe335bc2220abe7e950d9afa85f305eac439eec8eba9227352f592804f5b47208c262b220c1eb39d6ef89a92ec3ef051e9cca642658a8d8e55b35e78583d7a6cfc01bc5b9d579a1514c201d34230684e4385a1774f8b5f38b5191682a8b91b536ccd3821ee409028180d0f5eabf6e1e2e3dcbeeae0d92cd83e52ae68842bf781824cb7dc8c1507361d7d03b03bb15f7f7a0a9bf12171e01408f60b35722a5a819d7d9107fcea1b94184160cd9890f1f510207d47752fc27f58729ca8490b81ea720d5fcae71db92a9b140099047f45526d26af5da8bfe3e41beffe14d5d1cbe31bd1e50b9c38b9b393ef4b1b5514050e4a934d9501fc70d9ee3720a22fe18533b420cda21aea8c483e5bd3cb4786d6ce2d0f97d1a653253efd1c0283772e8ae43013dba4990bb6c7d9c7087c0d9b2fd3b79decd9a775989c81b87ccbb1e2d6b3c4df6dbe1b7e3a147dd8ff6998a0dcbe3f517899f2dbbbc788d5004d2de3d23224268406d02fecb0ba553123528c6b41f6f55aeaf8f32aa767a9f3113ca91d92e2dcf656cdef77f966a6b2cba83340658aa5c26aa0cb8ce54ae3a55b1eaafef66763ff4de971cd6a0b65a680169837dac945b0a7f13864795670922c99dfc6b5a5465e5043ad1b3205e4579cfc0e037f0b4e0a8b22b5d6ddba7d24b31388620d4aba83f84c5a1334261955d52294bd8b56d7175afbae015933ab1e0ef91e8161468f8eaa76a6f7a9bb8c8fc1195b9d8ff5dc4a51ff73a74b0640999bebcecb6036ef676c65e9fa5b1be22872082989c55a789fc4c2252452f786a13c4e868b85fbcd09bab689bb66dfae14c2ea7024647ad97728deed03314b007dbe461c1836e97f928308d39e5afc43ee3ae22ff47fff183553f56711880cc5ef72c5d66b4e2c6f651c57311d48fcc0aec762fae6444a5be11793be04c85ba97450673687734e681a1f3c64699686880d32d4cf87202b49ce13fbc8771fcf30d5593b41ffa61462c64061449b2c0a24ad8a03d280500bc86049bd55a27a05d70b12c7fd700454dbf3869b329a1ffa9994ecc2a6ec9572e3adaa0056c080a013fed42f6ecae05ccdb9bd8dc88ed44579b6a8871118710058f72c29f6db3b8ea03d200c0fb3e4416a51538d2ba41be88cfe830fa74c280e8b4b66cc3fad24ec06" + ); + + let mut compressor = BrotliCompressor::new(BrotliLevel::Brotli11); + compressor.write(&raw_batch_decompressed).unwrap(); + compressor.close().unwrap(); + let compressed = compressor.get_compressed(); + + let decompressed = + decompress_brotli(&compressed, MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize).unwrap(); + assert_eq!(decompressed, raw_batch_decompressed); + } +} diff --git a/kona/crates/batcher/comp/src/channel_out.rs b/kona/crates/batcher/comp/src/channel_out.rs new file mode 100644 index 0000000000000..d4fce73c26111 --- /dev/null +++ b/kona/crates/batcher/comp/src/channel_out.rs @@ -0,0 +1,299 @@ +//! Contains the `ChannelOut` primitive for Optimism. + +use crate::{ChannelCompressor, CompressorError}; +use alloc::{vec, vec::Vec}; +use kona_genesis::RollupConfig; +use kona_protocol::{Batch, ChannelId, Frame}; +use rand::{RngCore, SeedableRng, rngs::SmallRng}; + +/// The frame overhead. +const FRAME_V0_OVERHEAD: usize = 23; + +/// An error returned by the [ChannelOut] when adding single batches. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum ChannelOutError { + /// The channel is closed. + #[error("The channel is already closed")] + ChannelClosed, + /// The max frame size is too small. + #[error("The max frame size is too small")] + MaxFrameSizeTooSmall, + /// Missing compressed batch data. + #[error("Missing compressed batch data")] + MissingData, + /// An error from compression. + #[error("Error from compression")] + Compression(#[from] CompressorError), + /// An error encoding the `Batch`. + #[error("Error encoding the batch")] + BatchEncoding, + /// The encoded batch exceeds the max RLP bytes per channel. + #[error("The encoded batch exceeds the max RLP bytes per channel")] + ExceedsMaxRlpBytesPerChannel, +} + +/// [ChannelOut] constructs a channel from compressed, encoded batch data. +#[allow(missing_debug_implementations)] +pub struct ChannelOut<'a, C> +where + C: ChannelCompressor, +{ + /// The unique identifier for the channel. + pub id: ChannelId, + /// A reference to the [RollupConfig] used to + /// check the max RLP bytes per channel when + /// encoding and accepting the batch. + pub config: &'a RollupConfig, + /// The rlp length of the channel. + pub rlp_length: u64, + /// Whether the channel is closed. + pub closed: bool, + /// The frame number. + pub frame_number: u16, + /// The compressor. + pub compressor: C, +} + +impl<'a, C> ChannelOut<'a, C> +where + C: ChannelCompressor, +{ + /// Creates a new [ChannelOut] with the given [ChannelId]. + pub const fn new(id: ChannelId, config: &'a RollupConfig, compressor: C) -> Self { + Self { id, config, rlp_length: 0, frame_number: 0, closed: false, compressor } + } + + /// Resets the [ChannelOut] to its initial state. + pub fn reset(&mut self) { + self.rlp_length = 0; + self.frame_number = 0; + self.closed = false; + self.compressor.reset(); + // `getrandom` isn't available for wasm and risc targets + // Thread-based RNGs are not available for no_std + // So we must use a seeded RNG. + let mut small_rng = SmallRng::seed_from_u64(43); + SmallRng::fill_bytes(&mut small_rng, &mut self.id); + } + + /// Accepts the given [Batch] data into the [ChannelOut], compressing it + /// into frames. + pub fn add_batch(&mut self, batch: Batch) -> Result<(), ChannelOutError> { + if self.closed { + return Err(ChannelOutError::ChannelClosed); + } + + // Encode the batch. + let mut buf = vec![]; + batch.encode(&mut buf).map_err(|_| ChannelOutError::BatchEncoding)?; + + // Validate that the RLP length is within the channel's limits. + let max_rlp_bytes_per_channel = self.config.max_rlp_bytes_per_channel(batch.timestamp()); + if self.rlp_length + buf.len() as u64 > max_rlp_bytes_per_channel { + return Err(ChannelOutError::ExceedsMaxRlpBytesPerChannel); + } + + self.compressor.write(&buf)?; + self.rlp_length += buf.len() as u64; + + Ok(()) + } + + /// Returns the total amount of rlp-encoded input bytes. + pub const fn input_bytes(&self) -> u64 { + self.rlp_length + } + + /// Returns the number of bytes ready to be output to a frame. + pub fn ready_bytes(&self) -> usize { + self.compressor.len() + } + + /// Flush the internal compressor. + pub fn flush(&mut self) -> Result<(), ChannelOutError> { + self.compressor.flush()?; + Ok(()) + } + + /// Closes the channel if not already closed. + pub const fn close(&mut self) { + self.closed = true; + } + + /// Outputs a [Frame] from the [ChannelOut]. + pub fn output_frame(&mut self, max_size: usize) -> Result { + if max_size < FRAME_V0_OVERHEAD { + return Err(ChannelOutError::MaxFrameSizeTooSmall); + } + + // Construct an empty frame. + let mut frame = + Frame { id: self.id, number: self.frame_number, is_last: self.closed, data: vec![] }; + + let mut max_size = max_size - FRAME_V0_OVERHEAD; + if max_size > self.ready_bytes() { + max_size = self.ready_bytes(); + } + + // Read `max_size` bytes from the compressed data. + let mut data = Vec::with_capacity(max_size); + self.compressor.read(&mut data).map_err(ChannelOutError::Compression)?; + frame.data.extend_from_slice(data.as_slice()); + + // Update the compressed data. + self.frame_number += 1; + Ok(frame) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CompressorWriter, test_utils::MockCompressor}; + use alloy_primitives::Bytes; + use kona_protocol::{SingleBatch, SpanBatch}; + + #[test] + fn test_output_frame_max_size_too_small() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + assert_eq!(channel.output_frame(0), Err(ChannelOutError::MaxFrameSizeTooSmall)); + } + + #[test] + fn test_channel_out_output_frame_no_data() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new( + ChannelId::default(), + &config, + MockCompressor { read_error: true, compressed: Some(Default::default()) }, + ); + let err = channel.output_frame(FRAME_V0_OVERHEAD).unwrap_err(); + assert_eq!(err, ChannelOutError::Compression(CompressorError::Full)); + } + + #[test] + fn test_channel_out_output() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new( + ChannelId::default(), + &config, + MockCompressor { compressed: Some(Default::default()), ..Default::default() }, + ); + let frame = channel.output_frame(FRAME_V0_OVERHEAD).unwrap(); + assert_eq!(frame.id, ChannelId::default()); + assert_eq!(frame.number, 0); + assert!(!frame.is_last); + } + + #[test] + fn test_channel_out_reset() { + let config = RollupConfig::default(); + let mut channel = ChannelOut { + id: ChannelId::default(), + config: &config, + rlp_length: 10, + closed: true, + frame_number: 11, + compressor: MockCompressor::default(), + }; + channel.reset(); + assert_eq!(channel.rlp_length, 0); + assert_eq!(channel.frame_number, 0); + // The odds of a randomized channel id being equal to the + // default are so astronomically low, this test will always pass. + // The randomized [u8; 16] is about 1/255^16. + assert!(channel.id != ChannelId::default()); + assert!(!channel.closed); + } + + #[test] + fn test_channel_out_ready_bytes_empty() { + let config = RollupConfig::default(); + let channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + assert_eq!(channel.ready_bytes(), 0); + } + + #[test] + fn test_channel_out_ready_bytes_some() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + channel.compressor.write(&[1, 2, 3]).unwrap(); + assert_eq!(channel.ready_bytes(), 3); + } + + #[test] + fn test_channel_out_close() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + assert!(!channel.closed); + + channel.close(); + assert!(channel.closed); + } + + #[test] + fn test_channel_out_add_batch_closed() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + channel.close(); + + let batch = Batch::Single(SingleBatch::default()); + assert_eq!(channel.add_batch(batch), Err(ChannelOutError::ChannelClosed)); + } + + #[test] + fn test_channel_out_empty_span_batch_decode_error() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + + let batch = Batch::Span(SpanBatch::default()); + assert_eq!(channel.add_batch(batch), Err(ChannelOutError::BatchEncoding)); + } + + #[test] + fn test_channel_out_max_rlp_bytes_per_channel() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + + let batch = Batch::Single(SingleBatch::default()); + channel.rlp_length = config.max_rlp_bytes_per_channel(batch.timestamp()); + + assert_eq!(channel.add_batch(batch), Err(ChannelOutError::ExceedsMaxRlpBytesPerChannel)); + } + + #[test] + fn test_channel_out_add_batch() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + + let batch = Batch::Single(SingleBatch::default()); + assert_eq!(channel.add_batch(batch), Ok(())); + } + + #[test] + fn test_channel_out_add_batch_enforces_cumulative_rlp_limit() { + let config = RollupConfig::default(); + let mut channel = ChannelOut::new(ChannelId::default(), &config, MockCompressor::default()); + + let timestamp = 0; + let max_rlp = config.max_rlp_bytes_per_channel(timestamp); + let payload_size = (max_rlp / 2 + 1) as usize; + + let large_batch = Batch::Single(SingleBatch { + timestamp, + transactions: vec![Bytes::from(vec![0u8; payload_size])], + ..Default::default() + }); + + let mut encoded = Vec::new(); + large_batch.encode(&mut encoded).expect("test batch should encode"); + assert!(encoded.len() as u64 <= max_rlp, "test batch should fit within per-channel limit"); + + channel.add_batch(large_batch.clone()).expect("first batch should fit"); + assert_eq!(channel.rlp_length, encoded.len() as u64); + + let err = channel.add_batch(large_batch).unwrap_err(); + assert_eq!(err, ChannelOutError::ExceedsMaxRlpBytesPerChannel); + } +} diff --git a/kona/crates/batcher/comp/src/config.rs b/kona/crates/batcher/comp/src/config.rs new file mode 100644 index 0000000000000..ea9be16364e95 --- /dev/null +++ b/kona/crates/batcher/comp/src/config.rs @@ -0,0 +1,22 @@ +//! Compression configuration. + +use crate::{CompressionAlgo, CompressorType}; + +/// Config for the compressor itself. +#[derive(Debug, Clone)] +pub struct Config { + /// TargetOutputSize is the target size that the compressed data should reach. + /// The shadow compressor guarantees that the compressed data stays below + /// this bound. The ratio compressor might go over. + pub target_output_size: u64, + /// ApproxComprRatio to assume (only ratio compressor). Should be slightly smaller + /// than average from experiments to avoid the chances of creating a small + /// additional leftover frame. + pub approx_compr_ratio: f64, + /// Kind of compressor to use. Must be one of KindKeys. If unset, NewCompressor + /// will default to RatioKind. + pub kind: CompressorType, + + /// Type of compression algorithm to use. Must be one of [zlib, brotli-(9|10|11)] + pub compression_algo: CompressionAlgo, +} diff --git a/kona/crates/batcher/comp/src/lib.rs b/kona/crates/batcher/comp/src/lib.rs new file mode 100644 index 0000000000000..d406f52e8190e --- /dev/null +++ b/kona/crates/batcher/comp/src/lib.rs @@ -0,0 +1,48 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod channel_out; +pub use channel_out::{ChannelOut, ChannelOutError}; + +mod traits; +pub use traits::{ChannelCompressor, CompressorWriter}; + +mod config; +pub use config::Config; + +mod types; +pub use types::{CompressionAlgo, CompressorError, CompressorResult, CompressorType}; + +mod zlib; +pub use zlib::{ZlibCompressor, compress_zlib, decompress_zlib}; + +#[cfg(feature = "std")] +mod brotli; +#[cfg(feature = "std")] +pub use brotli::{BrotliCompressionError, BrotliCompressor, BrotliLevel, compress_brotli}; + +#[cfg(feature = "std")] +mod variant; +#[cfg(feature = "std")] +pub use variant::VariantCompressor; + +#[cfg(feature = "std")] +mod shadow; +#[cfg(feature = "std")] +pub use shadow::ShadowCompressor; + +#[cfg(feature = "std")] +mod ratio; +#[cfg(feature = "std")] +pub use ratio::RatioCompressor; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/kona/crates/batcher/comp/src/mod.rs b/kona/crates/batcher/comp/src/mod.rs new file mode 100644 index 0000000000000..c90ff206dd039 --- /dev/null +++ b/kona/crates/batcher/comp/src/mod.rs @@ -0,0 +1,32 @@ +//! Contains compression and decompression primitives for Optimism. + +#[cfg(feature = "std")] +mod variant; +#[cfg(feature = "std")] +pub use variant::VariantCompressor; + +mod config; +pub use config::Config; + +mod types; +pub use types::{CompressionAlgo, CompressorError, CompressorResult, CompressorType}; + +mod zlib; +pub use zlib::{ZlibCompressor, compress_zlib, decompress_zlib}; + +mod brotli; +#[cfg(feature = "std")] +pub use brotli::{BrotliCompressionError, compress_brotli, BrotliCompressor}; + +mod traits; +pub use traits::{ChannelCompressor, CompressorWriter}; + +#[cfg(feature = "std")] +mod shadow; +#[cfg(feature = "std")] +pub use shadow::ShadowCompressor; + +#[cfg(feature = "std")] +mod ratio; +#[cfg(feature = "std")] +pub use ratio::RatioCompressor; diff --git a/kona/crates/batcher/comp/src/ratio.rs b/kona/crates/batcher/comp/src/ratio.rs new file mode 100644 index 0000000000000..c9704c951bed9 --- /dev/null +++ b/kona/crates/batcher/comp/src/ratio.rs @@ -0,0 +1,137 @@ +//! Contains the ratio compressor for Optimism. +//! +//! This is a port of the [RatioCompressor][rc] from the op-batcher. +//! +//! [rc]: https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/compressor/ratio_compressor.go#L7 + +use crate::{CompressorResult, CompressorWriter, Config, VariantCompressor}; + +/// Ratio Compressor +/// +/// The ratio compressor uses the target size and a compression ration parameter +/// to determine how much data can be written to the compressor before it's +/// considered full. The full calculation is as follows: +/// +/// full = uncompressedLength * approxCompRatio >= targetFrameSize * targetNumFrames +/// +/// The ratio compressor wraps a [VariantCompressor] which dispatches to the +/// appropriate compression algorithm (ZLIB or Brotli). +#[derive(Debug, Clone)] +pub struct RatioCompressor { + /// The compressor configuration. + config: Config, + /// The amount of data currently in the compressor. + lake: u64, + /// The inner [VariantCompressor] that will be used to compress the data. + compressor: VariantCompressor, +} + +impl RatioCompressor { + /// Create a new [RatioCompressor] with the given [VariantCompressor]. + pub const fn new(config: Config, compressor: VariantCompressor) -> Self { + Self { config, lake: 0, compressor } + } + + /// Calculates the input threshold in bytes. + pub fn input_threshold(&self) -> usize { + let target_frame_size = self.config.target_output_size; + let approx_comp_ratio = self.config.approx_compr_ratio; + + (target_frame_size as f64 / approx_comp_ratio) as usize + } + + /// Returns if the compressor is full (exceeds the input threshold). + pub fn is_full(&self) -> bool { + self.lake >= self.input_threshold() as u64 + } +} + +impl From for RatioCompressor { + fn from(config: Config) -> Self { + let compressor = VariantCompressor::from(config.compression_algo); + Self::new(config, compressor) + } +} + +impl CompressorWriter for RatioCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + match self.compressor.write(data) { + Ok(n) => { + self.lake += n as u64; + Ok(n) + } + Err(e) => Err(e), + } + } + + fn flush(&mut self) -> CompressorResult<()> { + self.compressor.flush() + } + + fn close(&mut self) -> CompressorResult<()> { + self.compressor.close() + } + + fn reset(&mut self) { + self.compressor.reset(); + self.lake = 0; + } + + fn len(&self) -> usize { + self.compressor.len() + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + self.compressor.read(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CompressionAlgo, CompressorType}; + + #[test] + fn test_input_threshold() { + let config = Config { + target_output_size: 1024, + approx_compr_ratio: 0.5, + compression_algo: CompressionAlgo::Zlib, + kind: CompressorType::Ratio, + }; + + let inner = VariantCompressor::from(CompressionAlgo::Zlib); + let compressor = RatioCompressor::new(config, inner); + assert_eq!(compressor.input_threshold(), 2048); + } + + #[test] + fn test_ratio_compressor() { + let config = Config { + target_output_size: 1024, + approx_compr_ratio: 0.5, + compression_algo: CompressionAlgo::Zlib, + kind: CompressorType::Ratio, + }; + + let inner = VariantCompressor::from(CompressionAlgo::Zlib); + let mut compressor = RatioCompressor::new(config, inner); + + assert!(!compressor.is_full()); + compressor.write(&[0; 2048]).unwrap(); + assert!(compressor.is_full()); + assert_eq!(compressor.len(), 18); + + let mut buf = []; + compressor.read(&mut buf).unwrap(); + assert_eq!(buf.len(), 0); + + compressor.flush().unwrap(); + + compressor.reset(); + assert!(!compressor.is_full()); + assert_eq!(compressor.len(), 0); + + compressor.close().unwrap(); + } +} diff --git a/kona/crates/batcher/comp/src/shadow.rs b/kona/crates/batcher/comp/src/shadow.rs new file mode 100644 index 0000000000000..735521f208806 --- /dev/null +++ b/kona/crates/batcher/comp/src/shadow.rs @@ -0,0 +1,117 @@ +//! Contains the shadow compressor for Optimism. +//! +//! This is a port of the [ShadowCompressor][sc] from the op-batcher. +//! +//! [sc]: https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/compressor/shadow_compressor.go#L18 + +use crate::{CompressorError, CompressorResult, CompressorWriter, Config, VariantCompressor}; + +/// The largest potential blow-up in bytes we expect to see when compressing +/// arbitrary (e.g. random) data. Here we account for a 2 byte header, 4 byte +/// digest, 5 byte eof indicator, and then 5 byte flate block header for each 16k of potential +/// data. Assuming frames are max 128k size (the current max blob size) this is 2+4+5+(5*8) = 51 +/// bytes. If we start using larger frames (e.g. should max blob size increase) a larger blowup +/// might be possible, but it would be highly unlikely, and the system still works if our +/// estimate is wrong -- we just end up writing one more tx for the overflow. +const SAFE_COMPRESSION_OVERHEAD: u64 = 51; + +// The number of final bytes a `zlib.Writer` call writes to the output buffer. +const CLOSE_OVERHEAD_ZLIB: u64 = 9; + +/// Shadow Compressor +/// +/// The shadow compressor contains two compression buffers, one for size estimation, and +/// one for the final compressed data. The first compression buffer is flushed on every +/// write, and the second isn't, which means the final compressed data is always at least +/// smaller than the size estimation. +/// +/// One exception to the rule is when the first write to the buffer is not checked against +/// the target. This allows individual blocks larger than the target to be included. +/// Notice, this will be split across multiple channel frames. +#[derive(Debug, Clone)] +pub struct ShadowCompressor { + /// The compressor configuration. + config: Config, + /// The inner [VariantCompressor] that will be used to compress the data. + compressor: VariantCompressor, + /// The shadow compressor. + shadow: VariantCompressor, + + /// Flags that the buffer is full. + is_full: bool, + /// An upper bound on the size of the compressed data. + bound: u64, +} + +impl ShadowCompressor { + /// Creates a new [ShadowCompressor] with the given [VariantCompressor]. + pub const fn new( + config: Config, + compressor: VariantCompressor, + shadow: VariantCompressor, + ) -> Self { + Self { config, is_full: false, compressor, shadow, bound: SAFE_COMPRESSION_OVERHEAD } + } +} + +impl From for ShadowCompressor { + fn from(config: Config) -> Self { + let compressor = VariantCompressor::from(config.compression_algo); + let shadow = VariantCompressor::from(config.compression_algo); + Self::new(config, compressor, shadow) + } +} + +impl CompressorWriter for ShadowCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + // If the buffer is full, error so the user can flush. + if self.is_full { + return Err(CompressorError::Full); + } + + // Write to the shadow compressor. + self.shadow.write(data)?; + + // The new bound increases by the length of the compressed data. + let mut newbound = data.len() as u64; + if newbound > self.config.target_output_size { + // Don't flush the buffer if there's a chance we're over the size limit. + self.shadow.flush()?; + newbound = self.shadow.len() as u64 + CLOSE_OVERHEAD_ZLIB; + if newbound > self.config.target_output_size { + self.is_full = true; + // Only error if the buffer has been written to. + if self.compressor.len() > 0 { + return Err(CompressorError::Full); + } + } + } + + // Update the bound and compress. + self.bound = newbound; + self.compressor.write(data) + } + + fn len(&self) -> usize { + self.compressor.len() + } + + fn flush(&mut self) -> CompressorResult<()> { + self.shadow.flush() + } + + fn close(&mut self) -> CompressorResult<()> { + self.shadow.close() + } + + fn reset(&mut self) { + self.compressor.reset(); + self.shadow.reset(); + self.is_full = false; + self.bound = SAFE_COMPRESSION_OVERHEAD; + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + self.compressor.read(buf) + } +} diff --git a/kona/crates/batcher/comp/src/test_utils.rs b/kona/crates/batcher/comp/src/test_utils.rs new file mode 100644 index 0000000000000..9168ad65f94a0 --- /dev/null +++ b/kona/crates/batcher/comp/src/test_utils.rs @@ -0,0 +1,54 @@ +//! Test Utilities for the compression crate. + +use crate::{ChannelCompressor, CompressorError, CompressorResult, CompressorWriter}; +use alloc::vec::Vec; +use alloy_primitives::Bytes; + +/// A Mock compressor for testing. +#[derive(Debug, Clone, Default)] +pub struct MockCompressor { + /// Compressed bytes + pub compressed: Option, + /// Whether to throw a read error. + pub read_error: bool, +} + +impl CompressorWriter for MockCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + let data = data.to_vec(); + let written = data.len(); + self.compressed = Some(Bytes::from(data)); + Ok(written) + } + + fn flush(&mut self) -> CompressorResult<()> { + Ok(()) + } + + fn close(&mut self) -> CompressorResult<()> { + Ok(()) + } + + fn reset(&mut self) { + self.compressed = None; + } + + fn len(&self) -> usize { + self.compressed.as_ref().map(|b| b.len()).unwrap_or(0) + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + if self.read_error { + return Err(CompressorError::Full); + } + let len = self.compressed.as_ref().map(|b| b.len()).unwrap_or(0); + buf[..len].copy_from_slice(self.compressed.as_ref().unwrap()); + Ok(len) + } +} + +impl ChannelCompressor for MockCompressor { + fn get_compressed(&self) -> Vec { + self.compressed.as_ref().unwrap().to_vec() + } +} diff --git a/kona/crates/batcher/comp/src/traits.rs b/kona/crates/batcher/comp/src/traits.rs new file mode 100644 index 0000000000000..50ef1a32942f6 --- /dev/null +++ b/kona/crates/batcher/comp/src/traits.rs @@ -0,0 +1,39 @@ +//! Contains the core `Compressor` trait. + +use crate::CompressorResult; +use alloc::vec::Vec; + +/// Compressor Writer +/// +/// A trait that expands the standard library `Write` trait to include +/// compression-specific methods and return [CompressorResult] instead of +/// standard library `Result`. +#[allow(clippy::len_without_is_empty)] +pub trait CompressorWriter { + /// Writes the given data to the compressor. + fn write(&mut self, data: &[u8]) -> CompressorResult; + + /// Flushes the buffer. + fn flush(&mut self) -> CompressorResult<()>; + + /// Closes the compressor. + fn close(&mut self) -> CompressorResult<()>; + + /// Resets the compressor. + fn reset(&mut self); + + /// Returns the length of the compressed data. + fn len(&self) -> usize; + + /// Reads the compressed data into the given buffer. + /// Returns the number of bytes read. + fn read(&mut self, buf: &mut [u8]) -> CompressorResult; +} + +/// Channel Compressor +/// +/// A compressor for channels. +pub trait ChannelCompressor: CompressorWriter { + /// Returns the compressed data buffer. + fn get_compressed(&self) -> Vec; +} diff --git a/kona/crates/batcher/comp/src/types.rs b/kona/crates/batcher/comp/src/types.rs new file mode 100644 index 0000000000000..afdc05f587c2b --- /dev/null +++ b/kona/crates/batcher/comp/src/types.rs @@ -0,0 +1,52 @@ +//! Compression types. + +/// The result from compressing data. +pub type CompressorResult = Result; + +/// An error returned by the compressor. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +pub enum CompressorError { + /// Thrown when the compressor is full. + #[error("compressor is full")] + Full, + /// Brotli compression failed. + #[error("brotli compression failed")] + Brotli, +} + +/// The type of compressor to use. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompressorType { + /// The ratio compression. + Ratio, + /// The shadow compression. + Shadow, +} + +/// The compression algorithm type. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompressionAlgo { + /// The fastest brotli compression level. + Brotli9, + /// The default brotli compression level. + Brotli10, + /// The best brotli compression level. + Brotli11, + /// The zlib compression. + Zlib, +} + +#[cfg(feature = "std")] +impl> From for crate::BrotliLevel { + fn from(algo: A) -> Self { + match algo.borrow() { + CompressionAlgo::Brotli9 => Self::Brotli9, + CompressionAlgo::Brotli11 => Self::Brotli11, + _ => Self::Brotli10, + } + } +} diff --git a/kona/crates/batcher/comp/src/variant.rs b/kona/crates/batcher/comp/src/variant.rs new file mode 100644 index 0000000000000..969aecaf813ea --- /dev/null +++ b/kona/crates/batcher/comp/src/variant.rs @@ -0,0 +1,92 @@ +//! A variant over the different implementations of [ChannelCompressor]. + +use crate::{ + BrotliCompressor, ChannelCompressor, CompressionAlgo, CompressorResult, CompressorWriter, + ZlibCompressor, +}; +use kona_genesis::RollupConfig; + +/// The channel compressor wraps the brotli and zlib compressor types, +/// implementing the [ChannelCompressor] trait itself. +#[derive(Debug, Clone)] +pub enum VariantCompressor { + /// The brotli compressor. + Brotli(BrotliCompressor), + /// The zlib compressor. + Zlib(ZlibCompressor), +} + +impl VariantCompressor { + /// Constructs a [VariantCompressor] using the given [RollupConfig] and timestamp. + pub fn from_timestamp(config: &RollupConfig, timestamp: u64) -> Self { + if config.is_fjord_active(timestamp) { + Self::Brotli(BrotliCompressor::new(CompressionAlgo::Brotli10)) + } else { + Self::Zlib(ZlibCompressor::new()) + } + } +} + +impl CompressorWriter for VariantCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + match self { + Self::Brotli(compressor) => compressor.write(data), + Self::Zlib(compressor) => compressor.write(data), + } + } + + fn flush(&mut self) -> CompressorResult<()> { + match self { + Self::Brotli(compressor) => compressor.flush(), + Self::Zlib(compressor) => compressor.flush(), + } + } + + fn close(&mut self) -> CompressorResult<()> { + match self { + Self::Brotli(compressor) => compressor.close(), + Self::Zlib(compressor) => compressor.close(), + } + } + + fn reset(&mut self) { + match self { + Self::Brotli(compressor) => compressor.reset(), + Self::Zlib(compressor) => compressor.reset(), + } + } + + fn len(&self) -> usize { + match self { + Self::Brotli(compressor) => compressor.len(), + Self::Zlib(compressor) => compressor.len(), + } + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + match self { + Self::Brotli(compressor) => compressor.read(buf), + Self::Zlib(compressor) => compressor.read(buf), + } + } +} + +impl ChannelCompressor for VariantCompressor { + fn get_compressed(&self) -> Vec { + match self { + Self::Brotli(compressor) => compressor.get_compressed(), + Self::Zlib(compressor) => compressor.get_compressed(), + } + } +} + +impl From for VariantCompressor { + fn from(algo: CompressionAlgo) -> Self { + match algo { + lvl @ CompressionAlgo::Brotli9 => Self::Brotli(BrotliCompressor::new(lvl)), + lvl @ CompressionAlgo::Brotli10 => Self::Brotli(BrotliCompressor::new(lvl)), + lvl @ CompressionAlgo::Brotli11 => Self::Brotli(BrotliCompressor::new(lvl)), + CompressionAlgo::Zlib => Self::Zlib(ZlibCompressor::new()), + } + } +} diff --git a/kona/crates/batcher/comp/src/zlib.rs b/kona/crates/batcher/comp/src/zlib.rs new file mode 100644 index 0000000000000..cf2521072f6f9 --- /dev/null +++ b/kona/crates/batcher/comp/src/zlib.rs @@ -0,0 +1,73 @@ +//! Contains ZLIB compression and decompression primitives for Optimism. + +use crate::{ChannelCompressor, CompressorResult, CompressorWriter}; +use alloc::vec::Vec; +use miniz_oxide::inflate::DecompressError; + +/// The best compression. +const BEST_ZLIB_COMPRESSION: u8 = 9; + +/// Method to compress data using ZLIB. +pub fn compress_zlib(data: &[u8]) -> Vec { + miniz_oxide::deflate::compress_to_vec(data, BEST_ZLIB_COMPRESSION) +} + +/// Method to decompress data using ZLIB. +pub fn decompress_zlib(data: &[u8]) -> Result, DecompressError> { + miniz_oxide::inflate::decompress_to_vec(data) +} + +/// The ZLIB compressor. +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct ZlibCompressor { + /// Holds a non-compressed buffer. + buffer: Vec, + /// The compressed buffer. + compressed: Vec, +} + +impl ZlibCompressor { + /// Create a new ZLIB compressor. + pub const fn new() -> Self { + Self { buffer: Vec::new(), compressed: Vec::new() } + } +} + +impl CompressorWriter for ZlibCompressor { + fn write(&mut self, data: &[u8]) -> CompressorResult { + self.buffer.extend_from_slice(data); + self.compressed.clear(); + self.compressed.extend_from_slice(&compress_zlib(&self.buffer)); + Ok(data.len()) + } + + fn flush(&mut self) -> CompressorResult<()> { + Ok(()) + } + + fn close(&mut self) -> CompressorResult<()> { + Ok(()) + } + + fn reset(&mut self) { + self.buffer.clear(); + self.compressed.clear(); + } + + fn len(&self) -> usize { + self.compressed.len() + } + + fn read(&mut self, buf: &mut [u8]) -> CompressorResult { + let len = self.compressed.len().min(buf.len()); + buf[..len].copy_from_slice(&self.compressed[..len]); + Ok(len) + } +} + +impl ChannelCompressor for ZlibCompressor { + fn get_compressed(&self) -> Vec { + self.compressed.clone() + } +} diff --git a/kona/crates/node/disc/Cargo.toml b/kona/crates/node/disc/Cargo.toml new file mode 100644 index 0000000000000..91044182af2fc --- /dev/null +++ b/kona/crates/node/disc/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "kona-disc" +version = "0.1.2" +description = "Discovery service for the OP Stack" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# Kona +kona-peers.workspace = true +kona-macros.workspace = true +kona-genesis.workspace = true + +# Alloy +alloy-rlp.workspace = true + +# Networking +discv5 = { workspace = true, features = ["libp2p"] } +libp2p.workspace = true + +# Misc +tokio.workspace = true +tracing.workspace = true +thiserror.workspace = true +derive_more.workspace = true +backon = { workspace = true, features = ["std", "tokio", "tokio-sleep"] } +rand = { workspace = true, features = ["thread_rng"] } + +# `metrics` feature +metrics = { workspace = true, optional = true } + +[dev-dependencies] +tempfile.workspace = true +serde_json.workspace = true +kona-cli.workspace = true + +[features] +default = [] +metrics = [ "dep:metrics", "libp2p/metrics" ] diff --git a/kona/crates/node/disc/src/builder.rs b/kona/crates/node/disc/src/builder.rs new file mode 100644 index 0000000000000..1886045446f9c --- /dev/null +++ b/kona/crates/node/disc/src/builder.rs @@ -0,0 +1,216 @@ +//! Contains a builder for the discovery service. + +use discv5::{Config, Discv5, Enr, enr::k256}; +use kona_peers::{BootNodes, BootStoreFile, OpStackEnr}; +use std::net::IpAddr; +use tokio::time::Duration; + +use crate::{Discv5BuilderError, Discv5Driver}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// The local node information exposed by the discovery service to the network. +pub struct LocalNode { + /// The keypair to use for the local node. Should match the keypair used by the + /// gossip service. + pub signing_key: k256::ecdsa::SigningKey, + /// The IP address to advertise. + pub ip: IpAddr, + /// The TCP port to advertise. + pub tcp_port: u16, + /// Fallback UDP port. + pub udp_port: u16, +} + +impl From<&LocalNode> for discv5::ListenConfig { + fn from(local_node: &LocalNode) -> Self { + match local_node.ip { + IpAddr::V4(ip) => Self::Ipv4 { ip, port: local_node.tcp_port }, + IpAddr::V6(ip) => Self::Ipv6 { ip, port: local_node.tcp_port }, + } + } +} + +impl LocalNode { + /// Creates a new [`LocalNode`] instance. + pub const fn new( + signing_key: k256::ecdsa::SigningKey, + ip: IpAddr, + tcp_port: u16, + udp_port: u16, + ) -> Self { + Self { signing_key, ip, tcp_port, udp_port } + } +} + +impl LocalNode { + /// Build the local node ENR. This should contain the information we wish to + /// broadcast to the other nodes in the network. See + /// [the op-node implementation](https://github.com/ethereum-optimism/optimism/blob/174e55f0a1e73b49b80a561fd3fedd4fea5770c6/op-node/p2p/discovery.go#L61-L97) + /// for the go equivalent + fn build_enr(self, chain_id: u64) -> Result { + let opstack = OpStackEnr::from_chain_id(chain_id); + let mut opstack_data = Vec::new(); + use alloy_rlp::Encodable; + opstack.encode(&mut opstack_data); + + let mut enr_builder = Enr::builder(); + enr_builder.add_value_rlp(OpStackEnr::OP_CL_KEY, opstack_data.into()); + match self.ip { + IpAddr::V4(addr) => { + enr_builder.ip4(addr).tcp4(self.tcp_port).udp4(self.udp_port); + } + IpAddr::V6(addr) => { + enr_builder.ip6(addr).tcp6(self.tcp_port).udp6(self.udp_port); + } + } + + enr_builder.build(&self.signing_key.into()) + } +} + +/// Discovery service builder. +#[derive(Debug, Clone)] +pub struct Discv5Builder { + /// The node information advertised by the discovery service. + local_node: LocalNode, + /// The chain ID of the network. + chain_id: u64, + /// The discovery config for the discovery service. + discovery_config: Config, + /// The interval to find peers. + interval: Option, + /// The interval to randomize discovery peers. + randomize: Option, + /// An optional path to the bootstore. + bootstore: Option, + /// Additional bootnodes to manually add to the initial bootstore + bootnodes: BootNodes, + /// The interval to store the bootnodes to disk. + store_interval: Option, + /// Whether or not to forward the initial set of valid ENRs to the gossip layer. + forward: bool, +} + +impl Discv5Builder { + /// Creates a new [`Discv5Builder`] instance. + pub fn new(local_node: LocalNode, chain_id: u64, discovery_config: Config) -> Self { + Self { + local_node, + chain_id, + discovery_config, + interval: None, + randomize: None, + bootstore: None, + bootnodes: BootNodes::default(), + store_interval: None, + forward: true, + } + } + + /// Sets a bootstore file. + pub fn with_bootstore_file(mut self, bootstore: Option) -> Self { + self.bootstore = bootstore; + self + } + + /// Sets the initial bootnodes to add to the bootstore. + pub fn with_bootnodes(mut self, bootnodes: BootNodes) -> Self { + self.bootnodes = bootnodes; + self + } + + /// Sets the interval to store the bootnodes to disk. + pub const fn with_store_interval(mut self, store_interval: Duration) -> Self { + self.store_interval = Some(store_interval); + self + } + + /// Sets the discovery service advertised local node information. + pub fn with_local_node(mut self, local_node: LocalNode) -> Self { + self.local_node = local_node; + self + } + + /// Sets the chain ID of the network. + pub const fn with_chain_id(mut self, chain_id: u64) -> Self { + self.chain_id = chain_id; + self + } + + /// Sets the interval to find peers. + pub const fn with_interval(mut self, interval: Duration) -> Self { + self.interval = Some(interval); + self + } + + /// Sets the discovery config for the discovery service. + pub fn with_discovery_config(mut self, config: Config) -> Self { + self.discovery_config = config; + self + } + + /// Sets the interval to randomize discovery peers. + pub const fn with_discovery_randomize(mut self, interval: Option) -> Self { + self.randomize = interval; + self + } + + /// Disables forwarding of the initial set of valid ENRs to the gossip layer. + pub const fn disable_forward(mut self) -> Self { + self.forward = false; + self + } + + /// Builds a [`Discv5Driver`]. + pub fn build(self) -> Result { + let chain_id = self.chain_id; + + let config = self.discovery_config; + + let local_node = self.local_node; + let key = local_node.signing_key.clone(); + + let enr = local_node.build_enr(chain_id).map_err(|_| Discv5BuilderError::EnrBuildFailed)?; + + let interval = self.interval.unwrap_or(Duration::from_secs(5)); + let disc = Discv5::new(enr, key.into(), config) + .map_err(|e| Discv5BuilderError::Discv5CreationFailed(e.to_string()))?; + let mut driver = + Discv5Driver::new(disc, interval, chain_id, self.bootstore, self.bootnodes) + .map_err(|e| Discv5BuilderError::Discv5CreationFailed(e.to_string()))?; + driver.store_interval = self.store_interval.unwrap_or(Duration::from_secs(60)); + driver.forward = self.forward; + driver.remove_interval = self.randomize; + Ok(driver) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use discv5::{ConfigBuilder, ListenConfig, enr::CombinedKey}; + use kona_peers::EnrValidation; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn test_builds_valid_enr() { + let CombinedKey::Secp256k1(k256_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let addr = LocalNode::new(k256_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099, 9099); + let mut builder = Discv5Builder::new( + addr, + 10, + ConfigBuilder::new(ListenConfig::Ipv4 { ip: Ipv4Addr::UNSPECIFIED, port: 9099 }) + .build(), + ); + builder = builder.with_discovery_config( + ConfigBuilder::new(ListenConfig::Ipv4 { ip: Ipv4Addr::UNSPECIFIED, port: 9099 }) + .build(), + ); + let driver = builder.build().unwrap(); + let enr = driver.disc.local_enr(); + assert!(EnrValidation::validate(&enr, 10).is_valid()); + } +} diff --git a/kona/crates/node/disc/src/driver.rs b/kona/crates/node/disc/src/driver.rs new file mode 100644 index 0000000000000..269711e6684f8 --- /dev/null +++ b/kona/crates/node/disc/src/driver.rs @@ -0,0 +1,545 @@ +//! Discovery Module. + +use backon::{ExponentialBuilder, RetryableWithContext}; +use derive_more::Debug; +use discv5::{Config, Discv5, Enr, enr::NodeId}; +use kona_peers::{BootNode, BootNodes, BootStore, BootStoreFile, EnrValidation, enr_to_multiaddr}; +use tokio::{ + sync::mpsc::channel, + time::{Duration, sleep}, +}; + +use crate::{Discv5Builder, Discv5Handler, HandlerRequest, LocalNode}; + +/// The [`Discv5Driver`] drives the discovery service. +/// +/// Calling [`Discv5Driver::start`] spawns a new [`Discv5`] +/// discovery service in a new tokio task and returns a +/// [`Discv5Handler`]. +/// +/// Channels are used to communicate between the [`Discv5Handler`] +/// and the spawned task containing the [`Discv5`] service. +/// +/// Since some requested operations are asynchronous, this pattern of message +/// passing is used as opposed to wrapping the [`Discv5`] in an `Arc>`. +/// If an `Arc>` were used, a lock held across the operation's future +/// would be needed since some asynchronous operations require a mutable +/// reference to the [`Discv5`] service. +#[derive(Debug)] +pub struct Discv5Driver { + /// The [`Discv5`] discovery service. + #[debug(skip)] + pub disc: Discv5, + /// The optional [`BootStoreFile`] to use for the bootstore. + pub bootstore: Option, + /// Bootnodes used to bootstrap the discovery service. + pub bootnodes: BootNodes, + /// The chain ID of the network. + pub chain_id: u64, + /// The interval to discovery random nodes. + pub interval: Duration, + /// Whether to forward ENRs to the enr receiver on startup. + pub forward: bool, + /// The interval at which to store the ENRs in the bootstore. + /// This is set to 60 seconds by default. + pub store_interval: Duration, + /// The frequency at which to remove random nodes from the discovery table. + /// This is not enabled (`None`) by default. + pub remove_interval: Option, +} + +impl Discv5Driver { + /// Returns a new [`Discv5Builder`] instance. + pub fn builder( + local_node: LocalNode, + chain_id: u64, + discovery_config: Config, + ) -> Discv5Builder { + Discv5Builder::new(local_node, chain_id, discovery_config) + } + + /// Instantiates a new [`Discv5Driver`]. + pub const fn new( + disc: Discv5, + interval: Duration, + chain_id: u64, + bootstore: Option, + bootnodes: BootNodes, + ) -> Result { + Ok(Self { + disc, + chain_id, + bootnodes, + interval, + forward: true, + remove_interval: None, + store_interval: Duration::from_secs(60), + bootstore, + }) + } + + /// Starts the inner [`Discv5`] service. + async fn init(self) -> Result { + let (s, res) = { + |mut v: Self| async { + let res = v.disc.start().await; + (v, res) + } + } + .retry(ExponentialBuilder::default()) + .context(self) + .notify(|err: &discv5::Error, dur: Duration| { + warn!(target: "discovery", ?err, "Failed to start discovery service [Duration: {:?}]", dur); + }) + .await; + res.map(|_| s) + } + + /// Bootstraps the [`Discv5`] table with bootnodes. + async fn bootstrap_peers( + bootstore: Option, + bootnodes: BootNodes, + chain_id: u64, + disc: &Discv5, + ) -> BootStore { + // Note: if the bootstore file cannot be created, we use a default bootstore. + let mut store = bootstore + .map_or_else(BootStore::default, |bootstore| bootstore.try_into().unwrap_or_default()); + + let initial_store_length = store.len(); + + for bn in bootnodes.0.into_iter().chain(BootNodes::from_chain_id(chain_id).0.into_iter()) { + let res = match bn { + BootNode::Enr(enr) => Ok(enr.clone()), + BootNode::Enode(enode) => disc.request_enr(enode.clone()).await, + }; + + let Ok(enr) = res else { + debug!(target: "discovery::bootstrap", ?res, "Failed to add boot node ENR to discovery table"); + continue; + }; + + let validation = EnrValidation::validate(&enr, chain_id); + if validation.is_invalid() { + trace!(target: "discovery::bootstrap", "Ignoring Invalid Bootnode ENR: {:?}. {:?}", enr, validation); + continue; + } + + if let Err(e) = disc.add_enr(enr.clone()) { + debug!(target: "discovery::bootstrap", "Failed to add enr: {:?}", e); + continue; + } + + store.add_enr(enr); + } + + let new_store_len = store.len(); + + debug!(target: "discovery::bootstrap", + added=%(new_store_len - initial_store_length), + total=%new_store_len, + "Added new ENRs to discv5 bootstore" + ); + + store + } + + /// Spawns a new [`Discv5`] discovery service in a new tokio task. + /// + /// Returns a [`Discv5Handler`] to communicate with the spawned task. + pub fn start(mut self) -> (Discv5Handler, tokio::sync::mpsc::Receiver) { + let chain_id = self.chain_id; + let (req_sender, mut req_recv) = channel::(1024); + let (enr_sender, enr_recv) = channel::(1024); + + tokio::spawn(async move { + let remove = self.remove_interval.is_some(); + let remove_dur = self.remove_interval.unwrap_or(std::time::Duration::from_secs(600)); + let mut removal_interval = tokio::time::interval(remove_dur); + let mut interval = tokio::time::interval(self.interval); + let mut store_interval = tokio::time::interval(self.store_interval); + + // Step 1: Start the discovery service. + let Ok(s) = self.init().await else { + error!(target: "discovery", "Failed to start discovery service"); + return; + }; + self = s; + trace!(target: "discovery", "Discv5 Initialized"); + + // Step 2: Bootstrap the discovery table with bootnodes. + let mut store = + Self::bootstrap_peers(self.bootstore, self.bootnodes, chain_id, &self.disc).await; + + let enrs = self.disc.table_entries_enr(); + info!(target: "discovery", "Discv5 Started with {} ENRs", enrs.len()); + + // Step 3: Forward ENRs in the bootstore to the enr receiver. + if self.forward { + for enr in store.valid_peers_with_chain_id(self.chain_id) { + if let Err(e) = enr_sender.send(enr.clone()).await { + debug!(target: "discovery", "Failed to forward enr: {:?}", e); + } + } + } + + // Continuously attempt to start the event stream with a retry limit and shutdown + // signal. + let mut retries = 0; + let max_retries = 10; // Maximum number of retries before giving up. + let mut event_stream = loop { + if retries >= max_retries { + error!(target: "discovery", "Exceeded maximum retries for event stream startup. Aborting..."); + return; // Exit the task if the retry limit is reached. + } + match self.disc.event_stream().await { + Ok(event_stream) => { + break event_stream; + } + Err(e) => { + warn!(target: "discovery", "Failed to start event stream: {:?}", e); + retries += 1; + sleep(Duration::from_secs(2)).await; + info!(target: "discovery", "Retrying event stream startup... (Attempt {}/{})", retries, max_retries); + } + } + }; + + // Step 4: Run the core driver loop. + loop { + tokio::select! { + msg = req_recv.recv() => { + match msg { + Some(msg) => match msg { + HandlerRequest::Metrics(tx) => { + let metrics = self.disc.metrics(); + if let Err(e) = tx.send(metrics) { + warn!(target: "discovery", "Failed to send metrics: {:?}", e); + } + } + HandlerRequest::PeerCount(tx) => { + let peers = self.disc.connected_peers(); + if let Err(e) = tx.send(peers) { + warn!(target: "discovery", "Failed to send peer count: {:?}", e); + } + } + HandlerRequest::LocalEnr(tx) => { + let enr = self.disc.local_enr().clone(); + if let Err(e) = tx.send(enr.clone()) { + warn!(target: "discovery", "Failed to send local enr: {:?}", e); + } + } + HandlerRequest::AddEnr(enr) => { + let _ = self.disc.add_enr(enr); + } + HandlerRequest::RequestEnr{out, addr} => { + let enr = self.disc.request_enr(addr).await; + if let Err(e) = out.send(enr) { + warn!(target: "discovery", "Failed to send request enr: {:?}", e); + } + } + HandlerRequest::TableEnrs(tx) => { + let enrs = self.disc.table_entries_enr(); + if let Err(e) = tx.send(enrs) { + warn!(target: "discovery", "Failed to send table enrs: {:?}", e); + } + }, + HandlerRequest::TableInfos(tx) => { + let infos = self.disc.table_entries(); + if let Err(e) = tx.send(infos) { + warn!(target: "discovery", "Failed to send table infos: {:?}", e); + } + }, + HandlerRequest::BanAddrs{addrs_to_ban, ban_duration} => { + let enrs = self.disc.table_entries_enr(); + + for enr in enrs { + let Some(multi_addr) = enr_to_multiaddr(&enr) else { + continue; + }; + + if addrs_to_ban.contains(&multi_addr) { + self.disc.ban_node(&enr.node_id(), Some(ban_duration)); + } + } + }, + } + None => { + trace!(target: "discovery", "Receiver `None` peer enr"); + } + } + } + event = event_stream.recv() => { + let Some(event) = event else { + trace!(target: "discovery", "Received `None` event"); + continue; + }; + match event { + discv5::Event::Discovered(enr) => { + if EnrValidation::validate(&enr, chain_id).is_valid() { + debug!(target: "discovery", "Valid ENR discovered, forwarding to swarm: {:?}", enr); + kona_macros::inc!(gauge, crate::Metrics::DISCOVERY_EVENT, "type" => "discovered"); + store.add_enr(enr.clone()); + let sender = enr_sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(enr).await { + debug!(target: "discovery", "Failed to send enr: {:?}", e); + } + }); + } + } + discv5::Event::SessionEstablished(enr, addr) => { + if EnrValidation::validate(&enr, chain_id).is_valid() { + debug!(target: "discovery", "Session established with valid ENR, forwarding to swarm. Address: {:?}, ENR: {:?}", addr, enr); + kona_macros::inc!(gauge, crate::Metrics::DISCOVERY_EVENT, "type" => "session_established"); + store.add_enr(enr.clone()); + let sender = enr_sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(enr).await { + debug!(target: "discovery", "Failed to send enr: {:?}", e); + } + }); + } + } + discv5::Event::UnverifiableEnr { enr, .. } => { + if EnrValidation::validate(&enr, chain_id).is_valid() { + debug!(target: "discovery", "Valid ENR discovered, forwarding to swarm: {:?}", enr); + kona_macros::inc!(gauge, crate::Metrics::DISCOVERY_EVENT, "type" => "unverifiable_enr"); + store.add_enr(enr.clone()); + let sender = enr_sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(enr).await { + debug!(target: "discovery", "Failed to send enr: {:?}", e); + } + }); + } + + } + _ => {} + } + } + _ = interval.tick() => { + let id = NodeId::random(); + trace!(target: "discovery", "Finding random node: {}", id); + kona_macros::inc!(gauge, crate::Metrics::FIND_NODE_REQUEST, "find_node" => "find_node"); + let fut = self.disc.find_node(id); + let enr_sender = enr_sender.clone(); + tokio::spawn(async move { + match fut.await { + Ok(nodes) => { + let enrs = nodes.into_iter().filter(|node| EnrValidation::validate(node, chain_id).is_valid()); + for enr in enrs { + _ = enr_sender.send(enr).await; + } + } + Err(err) => { + info!(target: "discovery", "Failed to find node: {:?}", err); + } + } + }); + } + _ = store_interval.tick() => { + let start = std::time::Instant::now(); + let enrs = self.disc.table_entries_enr(); + store.merge(enrs); + + if let Err(e) = store.sync() { + warn!(target: "discovery", "Failed to sync bootstore: {:?}", e); + } + + let elapsed = start.elapsed(); + debug!(target: "discovery", "Bootstore ENRs stored in {:?}", elapsed); + kona_macros::record!(histogram, crate::Metrics::ENR_STORE_TIME, "store_time", "store_time", elapsed.as_secs_f64()); + kona_macros::set!(gauge, crate::Metrics::DISCOVERY_PEER_COUNT, self.disc.connected_peers() as f64); + } + _ = removal_interval.tick() => { + if remove { + let enrs = self.disc.table_entries_enr(); + if enrs.len() > 20 { + let mut rng = rand::rng(); + let index = rand::Rng::random_range(&mut rng, 0..enrs.len()); + let enr = enrs[index].clone(); + debug!(target: "removal", "Removing random ENR: {:?}", enr); + self.disc.remove_node(&enr.node_id()); + } + } + } + } + } + }); + + (Discv5Handler::new(chain_id, req_sender), enr_recv) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::LocalNode; + use discv5::{ + ConfigBuilder, + enr::{CombinedKey, CombinedPublicKey}, + handler::NodeContact, + }; + use kona_genesis::{OP_MAINNET_CHAIN_ID, OP_SEPOLIA_CHAIN_ID}; + use tempfile::tempdir; + + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + + #[tokio::test] + async fn test_online_discv5_driver() { + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + let discovery = Discv5Driver::builder( + LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0, 0), + OP_SEPOLIA_CHAIN_ID, + ConfigBuilder::new(socket.into()).build(), + ) + .build() + .expect("Failed to build discovery service"); + let (handle, _) = discovery.start(); + assert_eq!(handle.chain_id, OP_SEPOLIA_CHAIN_ID); + } + + #[tokio::test] + async fn test_online_discv5_driver_bootstrap_testnet() { + // Use a test file to make sure bootstore + // doesn't conflict with a local bootstore. + let file = tempdir().unwrap(); + let file = file.path().join("bootstore.json"); + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + let mut discovery = Discv5Driver::builder( + LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0, 0), + OP_SEPOLIA_CHAIN_ID, + ConfigBuilder::new(socket.into()).build(), + ) + .build() + .expect("Failed to build discovery service"); + discovery.bootstore = Some(BootStoreFile::Custom(file)); + + discovery = discovery.init().await.expect("Failed to initialize discovery service"); + + // There are no ENRs for `OP_SEPOLIA_CHAIN_ID` in the bootstore. + // If an ENR is added, this check will fail. + Discv5Driver::bootstrap_peers( + discovery.bootstore, + discovery.bootnodes, + OP_SEPOLIA_CHAIN_ID, + &discovery.disc, + ) + .await; + assert!( + discovery.disc.table_entries_enr().len() >= 5, + "Discovery table should have at least 5 ENRs" + ); + + // It should have the same number of entries as the testnet table. + let testnet = BootNodes::testnet(); + + // Filter out testnet ENRs that are not valid. + let testnet: Vec = testnet + .iter() + .filter_map(|node| { + if let BootNode::Enr(enr) = node { + // Check that the ENR is valid for the testnet. + if EnrValidation::validate(enr, OP_SEPOLIA_CHAIN_ID).is_invalid() { + return None; + } + } + let node_contact = + NodeContact::try_from_multiaddr(node.to_multiaddr().unwrap()).unwrap(); + + Some(node_contact.public_key()) + }) + .collect(); + + // There should be 8 valid ENRs for the testnet. + assert_eq!(testnet.len(), 8); + + // Those 8 ENRs should be in the discovery table. + let disc_enrs = discovery.disc.table_entries_enr(); + for public_key in testnet { + assert!( + disc_enrs.iter().any(|enr| enr.public_key() == public_key), + "Discovery table does not contain testnet ENR: {public_key:?}" + ); + } + } + + #[tokio::test] + async fn test_online_discv5_driver_bootstrap_mainnet() { + kona_cli::init_test_tracing(); + + // Use a test file to make sure bootstore + // doesn't conflict with a local bootstore. + let file = tempdir().unwrap(); + let file = file.path().join("bootstore.json"); + + // Filter out ENRs that are not valid. + let mainnet = BootNodes::mainnet(); + let mainnet: Vec = mainnet + .iter() + .filter_map(|node| { + if let BootNode::Enr(enr) = node { + if EnrValidation::validate(enr, OP_MAINNET_CHAIN_ID).is_invalid() { + return None; + } + } + let node_contact = + NodeContact::try_from_multiaddr(node.to_multiaddr().unwrap()).unwrap(); + + Some(node_contact.public_key()) + }) + .collect(); + + // There should be 16 valid ENRs for the mainnet. + assert_eq!(mainnet.len(), 16); + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let mut discovery = Discv5Driver::builder( + LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0, 0), + OP_MAINNET_CHAIN_ID, + ConfigBuilder::new(socket.into()).build(), + ) + .build() + .expect("Failed to build discovery service"); + discovery.bootstore = Some(BootStoreFile::Custom(file)); + + discovery = discovery.init().await.expect("Failed to initialize discovery service"); + + // There are no ENRs for op mainnet in the bootstore. + // If an ENR is added, this check will fail. + Discv5Driver::bootstrap_peers( + discovery.bootstore, + discovery.bootnodes, + OP_MAINNET_CHAIN_ID, + &discovery.disc, + ) + .await; + assert!( + discovery.disc.table_entries_enr().len() >= 10, + "Discovery table should have at least 10 ENRs" + ); + + // Those ENRs should be in the mainnet bootnodes. + let disc_enrs = discovery.disc.table_entries_enr(); + for enr in disc_enrs { + assert!( + mainnet.iter().any(|pub_key| pub_key == &enr.public_key()), + "Discovery table does not contain mainnet ENR: {enr:?}" + ); + } + } +} diff --git a/kona/crates/node/disc/src/error.rs b/kona/crates/node/disc/src/error.rs new file mode 100644 index 0000000000000..c1eff246f506a --- /dev/null +++ b/kona/crates/node/disc/src/error.rs @@ -0,0 +1,15 @@ +//! Error type when building the discovery service. + +use derive_more::From; +use thiserror::Error; + +/// An error that can occur when building the discovery service. +#[derive(Debug, Clone, PartialEq, From, Eq, Error)] +pub enum Discv5BuilderError { + /// Could not create the discovery service. + #[error("could not create discovery service: {0}")] + Discv5CreationFailed(String), + /// Failed to build the ENR. + #[error("failed to build ENR")] + EnrBuildFailed, +} diff --git a/kona/crates/node/disc/src/handler.rs b/kona/crates/node/disc/src/handler.rs new file mode 100644 index 0000000000000..35169c02dfc15 --- /dev/null +++ b/kona/crates/node/disc/src/handler.rs @@ -0,0 +1,178 @@ +//! Handler to the [`discv5::Discv5`] service spawned in a thread. + +use discv5::{Enr, RequestError, enr::NodeId, kbucket::NodeStatus, metrics::Metrics}; +use libp2p::Multiaddr; +use std::{collections::HashSet, string::String, sync::Arc, time::Duration}; +use tokio::sync::mpsc::Sender; + +/// Request message for communicating with the Discv5 discovery service. +/// +/// These requests are sent from the main application thread to the discovery +/// service running in a separate task, enabling asynchronous operations on +/// the discovery table and peer management. +#[derive(Debug)] +pub enum HandlerRequest { + /// Request current metrics from the discovery service. + /// + /// Returns performance and operational statistics including query counts, + /// success rates, and table population metrics. + Metrics(tokio::sync::oneshot::Sender), + + /// Get the current number of connected peers in the discovery table. + /// + /// Returns the count of peers currently maintained in the routing table, + /// which indicates the health and connectivity of the discovery service. + PeerCount(tokio::sync::oneshot::Sender), + + /// Add an ENR to the discovery service's routing table. + /// + /// Manually inserts a peer record into the table, typically used for + /// adding bootstrap nodes or peers discovered through other channels. + AddEnr(Enr), + + /// Request an ENR from a specific network address. + /// + /// Initiates a discovery query to retrieve the ENR for a peer at the + /// given address. Used for peer verification and metadata retrieval. + RequestEnr { + /// Channel to receive the result of the ENR request. + out: tokio::sync::oneshot::Sender>, + /// Network address to query for the ENR. + addr: String, + }, + + /// Get the local node's ENR. + /// + /// Returns the ENR that represents this node in the discovery network, + /// including its network address, capabilities, and cryptographic identity. + LocalEnr(tokio::sync::oneshot::Sender), + + /// Get all ENRs currently stored in the routing table. + /// + /// Returns a complete dump of peer records known to the discovery service, + /// useful for debugging and network analysis. + TableEnrs(tokio::sync::oneshot::Sender>), + + /// Get detailed information about nodes in the routing table. + /// + /// Returns comprehensive information including node IDs, ENRs, and status + /// for all peers in the discovery table. + TableInfos(tokio::sync::oneshot::Sender>), + + /// Ban specific network addresses for a duration. + /// + /// Prevents the discovery service from interacting with the specified + /// addresses, useful for blocking malicious or problematic peers. + BanAddrs { + /// Set of network addresses to ban. + addrs_to_ban: Arc>, + /// Duration for which the addresses should be banned. + ban_duration: Duration, + }, +} + +/// Handler to the spawned [`discv5::Discv5`] service. +/// +/// Provides a lock-free way to access the spawned `discv5::Discv5` service +/// by using message-passing to relay requests and responses through +/// a channel. +#[derive(Debug, Clone)] +pub struct Discv5Handler { + /// Sends [`HandlerRequest`]s to the spawned [`discv5::Discv5`] service. + pub sender: Sender, + /// The chain id. + pub chain_id: u64, +} + +impl Discv5Handler { + /// Creates a new [`Discv5Handler`] service. + pub const fn new(chain_id: u64, sender: Sender) -> Self { + Self { sender, chain_id } + } + + /// Blocking request for the ENRs of the discovery service. + /// + /// Returns `None` if the request could not be sent or received. + pub fn table_enrs(&self) -> tokio::sync::oneshot::Receiver> { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(HandlerRequest::TableEnrs(tx)).await { + warn!(target: "discovery", err = ?e, "Failed to send table ENRs request"); + } + }); + rx + } + + /// Returns a [`tokio::sync::oneshot::Receiver`] that contains a vector of information about + /// the nodes in the discv5 table. + pub fn table_infos(&self) -> tokio::sync::oneshot::Receiver> { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(HandlerRequest::TableInfos(tx)).await { + warn!(target: "discv5_handler", "Failed to send table infos request: {:?}", e); + } + }); + rx + } + + /// Blocking request for the local ENR of the node. + /// + /// Returns `None` if the request could not be sent or received. + pub fn local_enr(&self) -> tokio::sync::oneshot::Receiver { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(HandlerRequest::LocalEnr(tx)).await { + warn!(target: "discovery", err = ?e, "Failed to send local ENR request"); + } + }); + rx + } + + /// Requests an [`Enr`] from the discv5 service given a [`Multiaddr`]. + pub fn request_enr( + &self, + addr: Multiaddr, + ) -> tokio::sync::oneshot::Receiver> { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = + sender.send(HandlerRequest::RequestEnr { out: tx, addr: addr.to_string() }).await + { + warn!(target: "discv5_handler", "Failed to send request ENR request: {:?}", e); + } + }); + rx + } + + /// Blocking request for the metrics of the discovery service. + /// + /// Returns `None` if the request could not be sent or received. + pub fn metrics(&self) -> tokio::sync::oneshot::Receiver { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(HandlerRequest::Metrics(tx)).await { + warn!(target: "discovery", err = ?e, "Failed to send metrics request"); + } + }); + rx + } + + /// Blocking request for the discovery service peer count. + /// + /// Returns `None` if the request could not be sent or received. + pub fn peer_count(&self) -> tokio::sync::oneshot::Receiver { + let (tx, rx) = tokio::sync::oneshot::channel(); + let sender = self.sender.clone(); + tokio::spawn(async move { + if let Err(e) = sender.send(HandlerRequest::PeerCount(tx)).await { + warn!(target: "discovery", err = ?e, "Failed to send peer count request"); + } + }); + rx + } +} diff --git a/kona/crates/node/disc/src/lib.rs b/kona/crates/node/disc/src/lib.rs new file mode 100644 index 0000000000000..335f76371b6b1 --- /dev/null +++ b/kona/crates/node/disc/src/lib.rs @@ -0,0 +1,72 @@ +//! Discovery service for the OP Stack. +//! +//! This crate provides decentralized peer discovery capabilities using the Discv5 distributed +//! hash table (DHT) protocol, as defined in the Ethereum networking specifications. +//! +//! ## Overview +//! +//! The discovery service enables OP Stack nodes to find and connect to other network +//! participants without relying on centralized infrastructure. It maintains a local +//! view of the network through ENRs (Ethereum Node Records) and facilitates peer +//! connections for the gossip layer. +//! +//! ## Key Components +//! +//! - [`Discv5Driver`]: Main service driver that manages the discovery process +//! - [`Discv5Builder`]: Builder pattern for configuring discovery service parameters +//! - [`Discv5Handler`]: Handle for interacting with the discovery service +//! - [`LocalNode`]: Represents the local node's discovery information +//! +//! ## Discovery Process +//! +//! 1. **Bootstrap**: Connect to known bootstrap nodes to join the network +//! 2. **Table Population**: Discover peers through DHT queries and populate the routing table +//! 3. **Peer Maintenance**: Periodically refresh peer information and prune stale entries +//! 4. **ENR Updates**: Keep local ENR information current and propagate changes +//! +//! ## ENR Management +//! +//! ENRs (Ethereum Node Records) contain essential information about network peers: +//! - Node identity and cryptographic proof +//! - Network address and port information +//! - Protocol capabilities and version +//! - Chain-specific information (chain ID, etc.) +//! +//! ## Persistent Storage +//! +//! The service maintains a persistent bootstore that caches discovered peers across +//! restarts, reducing bootstrap time and improving network resilience. +//! +//! ## Configuration +//! +//! Key configuration parameters include: +//! - Discovery interval for random peer queries +//! - Bootstrap node list +//! - Storage location for persistent peer cache +//! - Network interface and port bindings + +#![doc(html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/kona-logo.png")] +#![doc(issue_tracker_base_url = "https://github.com/op-rs/kona/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +// Logging +#[macro_use] +extern crate tracing; +// Used in tests +use kona_genesis as _; + +mod builder; +pub use builder::{Discv5Builder, LocalNode}; + +mod error; +pub use error::Discv5BuilderError; + +mod driver; +pub use driver::Discv5Driver; + +mod handler; +pub use handler::{Discv5Handler, HandlerRequest}; + +mod metrics; +pub use metrics::Metrics; diff --git a/kona/crates/node/disc/src/metrics.rs b/kona/crates/node/disc/src/metrics.rs new file mode 100644 index 0000000000000..ef4ce0fcf1a30 --- /dev/null +++ b/kona/crates/node/disc/src/metrics.rs @@ -0,0 +1,62 @@ +//! Metrics for the discovery service. + +/// Container for discovery metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for discv5 events. + pub const DISCOVERY_EVENT: &str = "kona_node_discovery_events"; + + /// Counter for the number of FIND_NODE requests. + pub const FIND_NODE_REQUEST: &str = "kona_node_find_node_requests"; + + /// Timer for the time taken to store ENRs in the bootstore. + pub const ENR_STORE_TIME: &str = "kona_node_enr_store_time"; + + /// Identifier for the gauge that tracks the number of peers in the discovery service. + pub const DISCOVERY_PEER_COUNT: &str = "kona_node_discovery_peer_count"; + + /// Initializes metrics for the discovery service. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in the discovery service. + #[cfg(feature = "metrics")] + pub fn describe() { + metrics::describe_gauge!(Self::DISCOVERY_EVENT, "Events received by the discv5 service"); + metrics::describe_histogram!( + Self::ENR_STORE_TIME, + "Observations of elapsed time to store ENRs in the on-disk bootstore" + ); + metrics::describe_gauge!( + Self::DISCOVERY_PEER_COUNT, + "Number of peers connected to the discv5 service" + ); + metrics::describe_gauge!( + Self::FIND_NODE_REQUEST, + "Requests made to find a node through the discv5 peer discovery service" + ); + } + + /// Initializes metrics to `0` so they can be queried immediately by consumers of prometheus + /// metrics. + #[cfg(feature = "metrics")] + pub fn zero() { + // Discovery Event + kona_macros::set!(gauge, Self::DISCOVERY_EVENT, "type", "discovered", 0); + kona_macros::set!(gauge, Self::DISCOVERY_EVENT, "type", "session_established", 0); + kona_macros::set!(gauge, Self::DISCOVERY_EVENT, "type", "unverifiable_enr", 0); + + // Peer Counts + kona_macros::set!(gauge, Self::DISCOVERY_PEER_COUNT, 0); + kona_macros::set!(gauge, Self::FIND_NODE_REQUEST, 0); + } +} diff --git a/kona/crates/node/engine/Cargo.toml b/kona/crates/node/engine/Cargo.toml new file mode 100644 index 0000000000000..6d33b66cc51ee --- /dev/null +++ b/kona/crates/node/engine/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "kona-engine" +description = "An implementation of the OP Stack engine client" +version = "0.1.2" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# workspace +kona-genesis.workspace = true +kona-macros.workspace = true +kona-protocol = {workspace = true, features = ["serde", "std"]} + +# alloy +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-json-rpc.workspace = true +alloy-network.workspace = true +alloy-transport.workspace = true +alloy-primitives.workspace = true +alloy-provider = { workspace = true, features = ["ipc", "reqwest", "reqwest-rustls-tls", "engine-api"] } +alloy-rpc-client.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } +alloy-transport-http = { workspace = true, features = ["reqwest", "hyper", "jwt-auth"] } + +# op-alloy +op-alloy-network.workspace = true +op-alloy-consensus.workspace = true +op-alloy-provider.workspace = true +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true + +# general +serde.workspace = true +tokio.workspace = true +tracing.workspace = true +async-trait.workspace = true +thiserror.workspace = true +url.workspace = true +tower.workspace = true +http-body-util.workspace = true +derive_more = { workspace = true, features = ["display", "deref", "from_str", "constructor"] } +serde_json.workspace = true +jsonrpsee-types.workspace = true + +# metrics +metrics = { workspace = true, optional = true } + +# rollup boost +rollup-boost.workspace = true +http.workspace = true + +[dev-dependencies] +kona-registry.workspace = true +rand = {workspace = true, features = ["thread_rng"]} +arbitrary.workspace = true +op-alloy-rpc-types = {workspace = true, features = ["arbitrary", "k256"]} +metrics-exporter-prometheus.workspace = true +rstest.workspace = true + +[features] +metrics = [ "dep:metrics" ] +test-utils = [] diff --git a/kona/crates/node/engine/README.md b/kona/crates/node/engine/README.md new file mode 100644 index 0000000000000..a9062e98ed27a --- /dev/null +++ b/kona/crates/node/engine/README.md @@ -0,0 +1,50 @@ +# `kona-engine` + +CI +Kona Engine +License +Codecov + +An extensible implementation of the [OP Stack][op-stack] rollup node engine client. + +## Overview + +The `kona-engine` crate provides a task-based engine client for interacting with Ethereum execution layers. It implements the Engine API specification and manages the execution layer state through a priority-driven task queue system. + +## Key Components + +- **[`Engine`](crate::Engine)** - Main task queue processor that executes engine operations atomically +- **[`EngineClient`](crate::EngineClient)** - HTTP client for Engine API communication with JWT authentication +- **[`EngineState`](crate::EngineState)** - Tracks the current state of the execution layer +- **Task Types** - Specialized tasks for different engine operations: + - [`InsertTask`](crate::InsertTask) - Insert new payloads into the execution engine + - [`BuildTask`](crate::BuildTask) - Build new payloads with automatic forkchoice synchronization + - [`ConsolidateTask`](crate::ConsolidateTask) - Consolidate unsafe payloads to advance the safe chain + - [`FinalizeTask`](crate::FinalizeTask) - Finalize safe payloads on L1 confirmation + - [`SynchronizeTask`](crate::SynchronizeTask) - Internal task for execution layer forkchoice synchronization + +## Architecture + +The engine implements a task-driven architecture where forkchoice synchronization is handled automatically: + +- **Automatic Forkchoice Handling**: The [`BuildTask`](crate::BuildTask) automatically performs forkchoice updates during block building, eliminating the need for explicit forkchoice management in user code. +- **Internal Synchronization**: [`SynchronizeTask`](crate::SynchronizeTask) handles internal execution layer synchronization and is primarily used by other tasks rather than directly by users. +- **Priority-Based Execution**: Tasks are executed in priority order to ensure optimal sequencer performance and block processing efficiency. + +## Engine API Compatibility + +The crate supports multiple Engine API versions with automatic version selection based on the rollup configuration: + +- **Engine Forkchoice Updated**: V2, V3 +- **Engine New Payload**: V2, V3, V4 +- **Engine Get Payload**: V2, V3, V4 + +Version selection follows Optimism hardfork activation times (Bedrock, Canyon, Delta, Ecotone, Isthmus). + +## Features + +- `metrics` - Enable Prometheus metrics collection (optional) + + + +[op-stack]: https://specs.optimism.io diff --git a/kona/crates/node/engine/src/attributes.rs b/kona/crates/node/engine/src/attributes.rs new file mode 100644 index 0000000000000..5571bd6b26d8f --- /dev/null +++ b/kona/crates/node/engine/src/attributes.rs @@ -0,0 +1,1004 @@ +//! Contains a utility method to check if attributes match a block. + +use alloy_eips::{Decodable2718, eip1559::BaseFeeParams}; +use alloy_network::TransactionResponse; +use alloy_primitives::{Address, B256, Bytes}; +use alloy_rpc_types_eth::{Block, BlockTransactions, Withdrawals}; +use kona_genesis::RollupConfig; +use kona_protocol::OpAttributesWithParent; +use op_alloy_consensus::{ + EIP1559ParamError, OpTxEnvelope, decode_holocene_extra_data, decode_jovian_extra_data, +}; +use op_alloy_rpc_types::Transaction; + +/// Result of validating payload attributes against an execution layer block. +/// +/// Used to verify that proposed payload attributes match the actual executed block, +/// ensuring consistency between the rollup derivation process and execution layer. +/// Validation includes withdrawals, transactions, fees, and other block properties. +/// +/// # Examples +/// +/// ```rust,ignore +/// use kona_engine::AttributesMatch; +/// use kona_genesis::RollupConfig; +/// use kona_protocol::OpAttributesWithParent; +/// +/// let config = RollupConfig::default(); +/// let match_result = AttributesMatch::check_withdrawals(&config, &attributes, &block); +/// +/// if match_result.is_match() { +/// println!("Attributes are valid for this block"); +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AttributesMatch { + /// The payload attributes are consistent with the block. + Match, + /// The attributes do not match the block (contains mismatch details). + Mismatch(AttributesMismatch), +} + +impl AttributesMatch { + /// Returns true if the attributes match the block. + pub const fn is_match(&self) -> bool { + matches!(self, Self::Match) + } + + /// Returns true if the attributes do not match the block. + pub const fn is_mismatch(&self) -> bool { + matches!(self, Self::Mismatch(_)) + } + + /// Checks that withdrawals for a block and attributes match. + pub fn check_withdrawals( + config: &RollupConfig, + attributes: &OpAttributesWithParent, + block: &Block, + ) -> Self { + let attr_withdrawals = attributes.attributes().payload_attributes.withdrawals.as_ref(); + let attr_withdrawals = attr_withdrawals.map(|w| Withdrawals::new(w.to_vec())); + let block_withdrawals = block.withdrawals.as_ref(); + + if config.is_canyon_active(block.header.timestamp) { + // In canyon, the withdrawals list should be some and empty + if attr_withdrawals.is_none_or(|w| !w.is_empty()) { + return Self::Mismatch(AttributesMismatch::CanyonWithdrawalsNotEmpty); + } + if block_withdrawals.is_none_or(|w| !w.is_empty()) { + return Self::Mismatch(AttributesMismatch::CanyonWithdrawalsNotEmpty); + } + if !config.is_isthmus_active(block.header.timestamp) { + // In canyon, the withdrawals root should be set to the empty value + let empty_hash = alloy_consensus::EMPTY_ROOT_HASH; + if block.header.inner.withdrawals_root != Some(empty_hash) { + return Self::Mismatch(AttributesMismatch::CanyonNotEmptyHash); + } + } + } else { + // In bedrock, the withdrawals list should be None + if attr_withdrawals.is_some() { + return Self::Mismatch(AttributesMismatch::BedrockWithdrawals); + } + } + + if config.is_isthmus_active(block.header.timestamp) { + // In isthmus, the withdrawals root must be set + if block.header.inner.withdrawals_root.is_none() { + return Self::Mismatch(AttributesMismatch::IsthmusMissingWithdrawalsRoot); + } + } + + Self::Match + } + + /// Checks the attributes and block transaction list for consolidation. + /// We start by checking that there are the same number of transactions in both the attribute + /// payload and the block. Then we compare their contents + fn check_transactions(attributes_txs: &[Bytes], block: &Block) -> Self { + // Before checking the number of transactions, we have to make sure that the block + // has the right transactions format. We need to have access to the + // full transactions to be able to compare their contents. + let block_txs = match block.transactions { + BlockTransactions::Hashes(_) | BlockTransactions::Full(_) + if attributes_txs.is_empty() && block.transactions.is_empty() => + { + // We early return when both attributes and blocks are empty. This is for ergonomics + // because the default [`BlockTransactions`] format is + // [`BlockTransactions::Hash`], which may cause + // the [`BlockTransactions`] format check to fail right below. We may want to be a + // bit more flexible and not reject the hash format if both the + // attributes and the block are empty. + return Self::Match; + } + BlockTransactions::Uncle => { + // This can never be uncle transactions + error!( + "Invalid format for the block transactions. The `Uncle` transaction format is not relevant in that context and should not get used here. This is a bug" + ); + + return AttributesMismatch::MalformedBlockTransactions.into(); + } + BlockTransactions::Hashes(_) => { + // We can't have hash transactions with non empty blocks + error!( + "Invalid format for the block transactions. The `Hash` transaction format is not relevant in that context and should not get used here. This is a bug." + ); + + return AttributesMismatch::MalformedBlockTransactions.into(); + } + BlockTransactions::Full(ref block_txs) => block_txs, + }; + + let attributes_txs_len = attributes_txs.len(); + let block_txs_len = block_txs.len(); + + if attributes_txs_len != block_txs_len { + return AttributesMismatch::TransactionLen(attributes_txs_len, block_txs_len).into(); + } + + // Then we need to check that the content of the encoded transactions match + // Note that it is safe to zip both iterators because we checked their length + // beforehand. + for (attr_tx_bytes, block_tx) in attributes_txs.iter().zip(block_txs) { + trace!( + target: "engine", + ?attr_tx_bytes, + block_tx_hash = %block_tx.tx_hash(), + "Checking attributes transaction against block transaction", + ); + // Let's try to deserialize the attributes transaction + let Ok(attr_tx) = OpTxEnvelope::decode_2718(&mut &attr_tx_bytes[..]) else { + error!( + "Impossible to deserialize transaction from attributes. If we have stored these attributes it means the transactions where well formatted. This is a bug" + ); + + return AttributesMismatch::MalformedAttributesTransaction.into(); + }; + + if &attr_tx != block_tx.inner.inner.inner() { + warn!(target: "engine", ?attr_tx, ?block_tx, "Transaction mismatch in derived attributes"); + return AttributesMismatch::TransactionContent(attr_tx.tx_hash(), block_tx.tx_hash()) + .into() + } + } + + Self::Match + } + + /// Validates and compares EIP1559 parameters for consolidation. + fn check_eip1559( + config: &RollupConfig, + attributes: &OpAttributesWithParent, + block: &Block, + ) -> Self { + // We can assume that the EIP-1559 params are set iff holocene is active. + // Note here that we don't need to check for the attributes length because of type-safety. + let (ae, ad): (u128, u128) = match attributes.attributes().decode_eip_1559_params() { + None => { + // Holocene is active but the eip1559 are not set. This is a bug! + // Note: we checked the timestamp match above, so we can assume that both the + // attributes and the block have the same stamps + if config.is_holocene_active(block.header.timestamp) { + error!( + "EIP1559 parameters for attributes not set while holocene is active. This is a bug" + ); + return AttributesMismatch::MissingAttributesEIP1559.into(); + } + + // If the attributes are not specified, that means we can just early return. + return Self::Match; + } + Some((0, e)) if e != 0 => { + error!( + "Holocene EIP1559 params cannot have a 0 denominator unless elasticity is also 0. This is a bug" + ); + return AttributesMismatch::InvalidEIP1559ParamsCombination.into(); + } + // We need to translate (0, 0) parameters to pre-holocene protocol constants. + // Since holocene is supposed to be active, canyon should be as well. We take the canyon + // base fee params. + Some((0, 0)) => { + let BaseFeeParams { max_change_denominator, elasticity_multiplier } = + config.chain_op_config.as_canyon_base_fee_params(); + + (elasticity_multiplier, max_change_denominator) + } + Some((ae, ad)) => (ae.into(), ad.into()), + }; + + let extra_data_decoded = if config.is_jovian_active(block.header.timestamp) { + decode_jovian_extra_data(&block.header.extra_data).map(|(be, bd, _)| (be, bd)) + } else if config.is_holocene_active(block.header.timestamp) { + decode_holocene_extra_data(&block.header.extra_data) + } else { + return AttributesMismatch::MissingBlockEIP1559.into(); + }; + + // We decode the extra data stemming from the block header. + let (be, bd): (u128, u128) = match extra_data_decoded { + Ok((be, bd)) => (be.into(), bd.into()), + Err(EIP1559ParamError::NoEIP1559Params) => { + error!( + "EIP1559 parameters for the block not set while holocene is active. This is a bug" + ); + return AttributesMismatch::MissingBlockEIP1559.into(); + } + Err(EIP1559ParamError::InvalidVersion(v)) => { + error!( + version = v, + "The version in the extra data EIP1559 payload is incorrect. Should be 0. This is a bug", + ); + return AttributesMismatch::InvalidExtraDataVersion.into(); + } + Err(e) => { + error!(err = ?e, "An unknown extra data decoding error occurred. This is a bug",); + + return AttributesMismatch::UnknownExtraDataDecodingError(e).into(); + } + }; + + // We now have to check that both parameters match + if ae != be || ad != bd { + return AttributesMismatch::EIP1559Parameters( + BaseFeeParams { max_change_denominator: ad, elasticity_multiplier: ae }, + BaseFeeParams { max_change_denominator: bd, elasticity_multiplier: be }, + ) + .into() + } + + Self::Match + } + + /// Checks if the specified [`OpAttributesWithParent`] matches the specified [`Block`]. + /// Returns [`AttributesMatch::Match`] if they match, otherwise returns + /// [`AttributesMatch::Mismatch`]. + pub fn check( + config: &RollupConfig, + attributes: &OpAttributesWithParent, + block: &Block, + ) -> Self { + if attributes.parent.block_info.hash != block.header.inner.parent_hash { + return AttributesMismatch::ParentHash( + attributes.parent.block_info.hash, + block.header.inner.parent_hash, + ) + .into(); + } + + if attributes.attributes().payload_attributes.timestamp != block.header.inner.timestamp { + return AttributesMismatch::Timestamp( + attributes.attributes().payload_attributes.timestamp, + block.header.inner.timestamp, + ) + .into(); + } + + let mix_hash = block.header.inner.mix_hash; + if attributes.attributes().payload_attributes.prev_randao != mix_hash { + return AttributesMismatch::PrevRandao( + attributes.attributes().payload_attributes.prev_randao, + mix_hash, + ) + .into(); + } + + // Let's extract the list of attribute transactions + let default_vec = vec![]; + let attributes_txs = attributes + .attributes() + .transactions + .as_ref() + .map_or_else(|| &default_vec, |attrs| attrs); + + // Check transactions + if let mismatch @ Self::Mismatch(_) = Self::check_transactions(attributes_txs, block) { + return mismatch + } + + let Some(gas_limit) = attributes.attributes().gas_limit else { + return AttributesMismatch::MissingAttributesGasLimit.into(); + }; + + if gas_limit != block.header.inner.gas_limit { + return AttributesMismatch::GasLimit(gas_limit, block.header.inner.gas_limit).into(); + } + + if let m @ Self::Mismatch(_) = Self::check_withdrawals(config, attributes, block) { + return m; + } + + if attributes.attributes().payload_attributes.parent_beacon_block_root != + block.header.inner.parent_beacon_block_root + { + return AttributesMismatch::ParentBeaconBlockRoot( + attributes.attributes().payload_attributes.parent_beacon_block_root, + block.header.inner.parent_beacon_block_root, + ) + .into(); + } + + if attributes.attributes().payload_attributes.suggested_fee_recipient != + block.header.inner.beneficiary + { + return AttributesMismatch::FeeRecipient( + attributes.attributes().payload_attributes.suggested_fee_recipient, + block.header.inner.beneficiary, + ) + .into(); + } + + // Check the EIP-1559 parameters in a separate helper method + if let m @ Self::Mismatch(_) = Self::check_eip1559(config, attributes, block) { + return m; + } + + Self::Match + } +} + +/// An enum over the type of mismatch between [`OpAttributesWithParent`] +/// and a [`Block`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AttributesMismatch { + /// The parent hash of the block does not match the parent hash of the attributes. + ParentHash(B256, B256), + /// The timestamp of the block does not match the timestamp of the attributes. + Timestamp(u64, u64), + /// The prev randao of the block does not match the prev randao of the attributes. + PrevRandao(B256, B256), + /// The block contains malformed transactions. This is a bug - the transaction format + /// should be checked before the consolidation step. + MalformedBlockTransactions, + /// There is a malformed transaction inside the attributes. This is a bug - the transaction + /// format should be checked before the consolidation step. + MalformedAttributesTransaction, + /// A mismatch in the number of transactions contained in the attributes and the block. + TransactionLen(usize, usize), + /// A mismatch in the content of some transactions contained in the attributes and the block. + TransactionContent(B256, B256), + /// The EIP1559 payload for the [`OpAttributesWithParent`] is missing when holocene is active. + MissingAttributesEIP1559, + /// The EIP1559 payload for the block is missing when holocene is active. + MissingBlockEIP1559, + /// The version in the extra data EIP1559 payload is incorrect. Should be 0. + InvalidExtraDataVersion, + /// An unknown extra data decoding error occurred. + UnknownExtraDataDecodingError(EIP1559ParamError), + /// Holocene EIP1559 params cannot have a 0 denominator unless elasticity is also 0 + InvalidEIP1559ParamsCombination, + /// The EIP1559 base fee parameters of the attributes and the block don't match + EIP1559Parameters(BaseFeeParams, BaseFeeParams), + /// Transactions mismatch. + Transactions(u64, u64), + /// The gas limit of the block does not match the gas limit of the attributes. + GasLimit(u64, u64), + /// The gas limit for the [`OpAttributesWithParent`] is missing. + MissingAttributesGasLimit, + /// The fee recipient of the block does not match the fee recipient of the attributes. + FeeRecipient(Address, Address), + /// A mismatch in the parent beacon block root. + ParentBeaconBlockRoot(Option, Option), + /// After the canyon hardfork, withdrawals cannot be empty. + CanyonWithdrawalsNotEmpty, + /// After the canyon hardfork, the withdrawals root must be the empty hash. + CanyonNotEmptyHash, + /// In the bedrock hardfork, the attributes must has empty withdrawals. + BedrockWithdrawals, + /// In the isthmus hardfork, the withdrawals root must be set. + IsthmusMissingWithdrawalsRoot, +} + +impl From for AttributesMatch { + fn from(mismatch: AttributesMismatch) -> Self { + Self::Mismatch(mismatch) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::AttributesMismatch::EIP1559Parameters; + use alloy_consensus::EMPTY_ROOT_HASH; + use alloy_primitives::{Bytes, FixedBytes, address, b256}; + use alloy_rpc_types_eth::BlockTransactions; + use arbitrary::{Arbitrary, Unstructured}; + use kona_protocol::{BlockInfo, L2BlockInfo}; + use kona_registry::ROLLUP_CONFIGS; + use op_alloy_consensus::encode_holocene_extra_data; + use op_alloy_rpc_types_engine::OpPayloadAttributes; + + fn default_attributes() -> OpAttributesWithParent { + OpAttributesWithParent { + attributes: OpPayloadAttributes::default(), + parent: L2BlockInfo::default(), + derived_from: Some(BlockInfo::default()), + is_last_in_span: true, + } + } + + fn default_rollup_config() -> &'static RollupConfig { + let opm = 10; + ROLLUP_CONFIGS.get(&opm).expect("default rollup config should exist") + } + + #[test] + fn test_attributes_match_parent_hash_mismatch() { + let cfg = default_rollup_config(); + let attributes = default_attributes(); + let mut block = Block::::default(); + block.header.inner.parent_hash = + b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::ParentHash( + attributes.parent.block_info.hash, + block.header.inner.parent_hash, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_check_timestamp() { + let cfg = default_rollup_config(); + let attributes = default_attributes(); + let mut block = Block::::default(); + block.header.inner.timestamp = 1234567890; + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::Timestamp( + attributes.attributes().payload_attributes.timestamp, + block.header.inner.timestamp, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_check_prev_randao() { + let cfg = default_rollup_config(); + let attributes = default_attributes(); + let mut block = Block::::default(); + block.header.inner.mix_hash = + b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::PrevRandao( + attributes.attributes().payload_attributes.prev_randao, + block.header.inner.mix_hash, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_missing_gas_limit() { + let cfg = default_rollup_config(); + let attributes = default_attributes(); + let mut block = Block::::default(); + block.header.inner.gas_limit = 123456; + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::MissingAttributesGasLimit.into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_check_gas_limit() { + let cfg = default_rollup_config(); + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(123457); + let mut block = Block::::default(); + block.header.inner.gas_limit = 123456; + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::GasLimit( + attributes.attributes().gas_limit.unwrap_or_default(), + block.header.inner.gas_limit, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_check_parent_beacon_block_root() { + let cfg = default_rollup_config(); + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(0); + attributes.attributes.payload_attributes.parent_beacon_block_root = + Some(b256!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")); + let block = Block::::default(); + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::ParentBeaconBlockRoot( + attributes.attributes().payload_attributes.parent_beacon_block_root, + block.header.inner.parent_beacon_block_root, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match_check_fee_recipient() { + let cfg = default_rollup_config(); + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(0); + let mut block = Block::::default(); + block.header.inner.beneficiary = address!("1234567890abcdef1234567890abcdef12345678"); + let check = AttributesMatch::check(cfg, &attributes, &block); + let expected: AttributesMatch = AttributesMismatch::FeeRecipient( + attributes.attributes().payload_attributes.suggested_fee_recipient, + block.header.inner.beneficiary, + ) + .into(); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + fn generate_txs(num_txs: usize) -> Vec { + // Simulate some random data + let mut data = vec![0; 1024]; + let mut rng = rand::rng(); + + (0..num_txs) + .map(|_| { + rand::Rng::fill(&mut rng, &mut data[..]); + + // Create unstructured data with the random bytes + let u = Unstructured::new(&data); + + // Generate a random instance of MyStruct + Transaction::arbitrary_take_rest(u).expect("Impossible to generate arbitrary tx") + }) + .collect() + } + + fn test_transactions_match_helper() -> (OpAttributesWithParent, Block) { + const NUM_TXS: usize = 10; + + let transactions = generate_txs(NUM_TXS); + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(0); + attributes.attributes.transactions = Some( + transactions + .iter() + .map(|tx| { + use alloy_eips::Encodable2718; + let mut buf = vec![]; + tx.inner.inner.inner().encode_2718(&mut buf); + Bytes::from(buf) + }) + .collect::>(), + ); + + let block = Block:: { + transactions: BlockTransactions::Full(transactions), + ..Default::default() + }; + + (attributes, block) + } + + #[test] + fn test_attributes_match_check_transactions() { + let cfg = default_rollup_config(); + let (attributes, block) = test_transactions_match_helper(); + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + } + + #[test] + fn test_attributes_mismatch_check_transactions_len() { + let cfg = default_rollup_config(); + let (mut attributes, block) = test_transactions_match_helper(); + attributes.attributes = OpPayloadAttributes { + transactions: attributes.attributes.transactions.map(|mut txs| { + txs.pop(); + txs + }), + ..attributes.attributes + }; + + let block_txs_len = block.transactions.len(); + + let expected: AttributesMatch = + AttributesMismatch::TransactionLen(block_txs_len - 1, block_txs_len).into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_mismatch_check_transaction_content() { + let cfg = default_rollup_config(); + let (attributes, mut block) = test_transactions_match_helper(); + let BlockTransactions::Full(block_txs) = &mut block.transactions else { + unreachable!("The helper should build a full list of transactions") + }; + + let first_tx = block_txs.last().unwrap().clone(); + let first_tx_hash = first_tx.tx_hash(); + + // We set the last tx to be the same as the first transaction. + // Since the transactions are generated randomly and there are more than one transaction, + // there is a very high likelihood that any pair of transactions is distinct. + let last_tx = block_txs.first_mut().unwrap(); + let last_tx_hash = last_tx.tx_hash(); + *last_tx = first_tx; + + let expected: AttributesMatch = + AttributesMismatch::TransactionContent(last_tx_hash, first_tx_hash).into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + /// Checks the edge case where the attributes array is empty. + #[test] + fn test_attributes_mismatch_empty_tx_attributes() { + let cfg = default_rollup_config(); + let (mut attributes, block) = test_transactions_match_helper(); + attributes.attributes = OpPayloadAttributes { transactions: None, ..attributes.attributes }; + + let block_txs_len = block.transactions.len(); + + let expected: AttributesMatch = AttributesMismatch::TransactionLen(0, block_txs_len).into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + /// Checks the edge case where the transactions contained in the block have the wrong + /// format. + #[test] + fn test_block_transactions_wrong_format() { + let cfg = default_rollup_config(); + let (attributes, mut block) = test_transactions_match_helper(); + block.transactions = BlockTransactions::Uncle; + + let expected: AttributesMatch = AttributesMismatch::MalformedBlockTransactions.into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + /// Checks the edge case where the transactions contained in the attributes have the wrong + /// format. + #[test] + fn test_attributes_transactions_wrong_format() { + let cfg = default_rollup_config(); + let (mut attributes, block) = test_transactions_match_helper(); + let txs = attributes.attributes.transactions.as_mut().unwrap(); + let first_tx_bytes = txs.first_mut().unwrap(); + *first_tx_bytes = Bytes::copy_from_slice(&[0, 1, 2]); + + let expected: AttributesMatch = AttributesMismatch::MalformedAttributesTransaction.into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + assert!(check.is_mismatch()); + } + + // Test that the check pass if the transactions obtained from the attributes have the format + // `Some(vec![])`, ie an empty vector inside a `Some` option. + #[test] + fn test_attributes_and_block_transactions_empty() { + let cfg = default_rollup_config(); + let (mut attributes, mut block) = test_transactions_match_helper(); + + attributes.attributes = + OpPayloadAttributes { transactions: Some(vec![]), ..attributes.attributes }; + + block.transactions = BlockTransactions::Full(vec![]); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + + // Edge case: if the block transactions and the payload attributes are empty, we can also + // use the hash format (this is the default value of `BlockTransactions`). + attributes.attributes = OpPayloadAttributes { transactions: None, ..attributes.attributes }; + block.transactions = BlockTransactions::Hashes(vec![]); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + } + + // Edge case: if the payload attributes has the format `Some(vec![])`, we can still + // use the hash format. + #[test] + fn test_attributes_and_block_transactions_empty_hash_format() { + let cfg = default_rollup_config(); + let (mut attributes, mut block) = test_transactions_match_helper(); + + attributes.attributes = + OpPayloadAttributes { transactions: Some(vec![]), ..attributes.attributes }; + + block.transactions = BlockTransactions::Hashes(vec![]); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + } + + // Test that the check fails if the block format is incorrect and the attributes are empty + #[test] + fn test_attributes_empty_and_block_uncle() { + let cfg = default_rollup_config(); + let (mut attributes, mut block) = test_transactions_match_helper(); + + attributes.attributes = + OpPayloadAttributes { transactions: Some(vec![]), ..attributes.attributes }; + + block.transactions = BlockTransactions::Uncle; + + let expected: AttributesMatch = AttributesMismatch::MalformedBlockTransactions.into(); + + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, expected); + } + + fn eip1559_test_setup() -> (RollupConfig, OpAttributesWithParent, Block) { + let mut cfg = default_rollup_config().clone(); + + // We need to activate holocene to make sure it works! We set the activation time to zero to + // make sure that it is activated by default. + cfg.hardforks.holocene_time = Some(0); + + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(0); + // For canyon and above we need to specify the withdrawals + attributes.attributes.payload_attributes.withdrawals = Some(vec![]); + + // For canyon and above we also need to specify the withdrawal headers + let block = Block { + withdrawals: Some(Withdrawals(vec![])), + header: alloy_rpc_types_eth::Header { + inner: alloy_consensus::Header { + withdrawals_root: Some(EMPTY_ROOT_HASH), + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + (cfg, attributes, block) + } + + /// Ensures that we have to set the EIP1559 parameters for holocene and above. + #[test] + fn test_eip1559_parameters_not_specified_holocene() { + let (cfg, attributes, block) = eip1559_test_setup(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Mismatch(AttributesMismatch::MissingAttributesEIP1559)); + assert!(check.is_mismatch()); + } + + /// Ensures that we have to set the EIP1559 parameters for holocene and above. + #[test] + fn test_eip1559_parameters_specified_attributes_but_not_block() { + let (cfg, mut attributes, block) = eip1559_test_setup(); + + attributes.attributes.eip_1559_params = Some(Default::default()); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!( + check, + AttributesMatch::Mismatch(AttributesMismatch::UnknownExtraDataDecodingError( + EIP1559ParamError::InvalidExtraDataLength + )) + ); + assert!(check.is_mismatch()); + } + + /// Check that, when the eip1559 params are specified and empty, the check fails because we + /// fallback on canyon params for the attributes but not for the block (edge case). + #[test] + fn test_eip1559_parameters_specified_both_and_empty() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + attributes.attributes.eip_1559_params = Some(Default::default()); + block.header.extra_data = vec![0; 9].into(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!( + check, + AttributesMatch::Mismatch(EIP1559Parameters( + BaseFeeParams { max_change_denominator: 250, elasticity_multiplier: 6 }, + BaseFeeParams { max_change_denominator: 0, elasticity_multiplier: 0 } + )) + ); + assert!(check.is_mismatch()); + } + + #[test] + fn test_eip1559_parameters_empty_for_attr_only() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + attributes.attributes.eip_1559_params = Some(Default::default()); + block.header.extra_data = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 250, elasticity_multiplier: 6 }, + ) + .unwrap(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + assert!(check.is_match()); + } + + #[test] + fn test_eip1559_parameters_custom_values_match() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + let eip1559_extra_params = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 }, + ) + .unwrap(); + let eip1559_params: FixedBytes<8> = + eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap(); + + attributes.attributes.eip_1559_params = Some(eip1559_params); + block.header.extra_data = eip1559_extra_params; + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + assert!(check.is_match()); + } + + #[test] + fn test_eip1559_parameters_custom_values_mismatch() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + let eip1559_extra_params = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 }, + ) + .unwrap(); + + let eip1559_params: FixedBytes<8> = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 99, elasticity_multiplier: 2 }, + ) + .unwrap() + .split_off(1) + .as_ref() + .try_into() + .unwrap(); + + attributes.attributes.eip_1559_params = Some(eip1559_params); + block.header.extra_data = eip1559_extra_params; + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!( + check, + AttributesMatch::Mismatch(AttributesMismatch::EIP1559Parameters( + BaseFeeParams { max_change_denominator: 99, elasticity_multiplier: 2 }, + BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 } + )) + ); + assert!(check.is_mismatch()); + } + + /// Edge case: if the elasticity multiplier is 0, the max change denominator cannot be 0 as well + #[test] + fn test_eip1559_parameters_combination_mismatch() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + let eip1559_extra_params = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 5, elasticity_multiplier: 0 }, + ) + .unwrap(); + let eip1559_params: FixedBytes<8> = + eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap(); + + attributes.attributes.eip_1559_params = Some(eip1559_params); + block.header.extra_data = eip1559_extra_params; + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!( + check, + AttributesMatch::Mismatch(AttributesMismatch::InvalidEIP1559ParamsCombination) + ); + assert!(check.is_mismatch()); + } + + /// Check that the version of the extra block data must be zero. + #[test] + fn test_eip1559_parameters_invalid_version() { + let (cfg, mut attributes, mut block) = eip1559_test_setup(); + + let eip1559_extra_params = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 }, + ) + .unwrap(); + let eip1559_params: FixedBytes<8> = + eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap(); + + let mut raw_extra_params_bytes = eip1559_extra_params.to_vec(); + raw_extra_params_bytes[0] = 10; + + attributes.attributes.eip_1559_params = Some(eip1559_params); + block.header.extra_data = raw_extra_params_bytes.into(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Mismatch(AttributesMismatch::InvalidExtraDataVersion)); + assert!(check.is_mismatch()); + } + + /// Try to encode jovian extra data with the holocene encoding function. + #[test] + fn test_eip1559_parameters_invalid_jovian_encoding() { + let (mut cfg, mut attributes, mut block) = eip1559_test_setup(); + + cfg.hardforks.jovian_time = Some(0); + + let eip1559_extra_params = encode_holocene_extra_data( + Default::default(), + BaseFeeParams { max_change_denominator: 100, elasticity_multiplier: 2 }, + ) + .unwrap(); + let eip1559_params: FixedBytes<8> = + eip1559_extra_params.clone().split_off(1).as_ref().try_into().unwrap(); + + let raw_extra_params_bytes = eip1559_extra_params.to_vec(); + + attributes.attributes.eip_1559_params = Some(eip1559_params); + block.header.extra_data = raw_extra_params_bytes.into(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + assert_eq!( + check, + AttributesMatch::Mismatch(AttributesMismatch::UnknownExtraDataDecodingError( + EIP1559ParamError::InvalidExtraDataLength + )) + ); + assert!(check.is_mismatch()); + } + + /// The default parameters can't overflow the u32 byte representation of the base fee params! + #[test] + fn test_eip1559_default_param_cant_overflow() { + let (mut cfg, mut attributes, mut block) = eip1559_test_setup(); + cfg.chain_op_config.eip1559_denominator_canyon = u64::MAX; + cfg.chain_op_config.eip1559_elasticity = u64::MAX; + + attributes.attributes.eip_1559_params = Some(Default::default()); + block.header.extra_data = vec![0; 9].into(); + + let check = AttributesMatch::check(&cfg, &attributes, &block); + + // Note that in this case we *always* have a mismatch because there isn't enough bytes in + // the default representation of the extra params to represent a u128 + assert_eq!( + check, + AttributesMatch::Mismatch(EIP1559Parameters( + BaseFeeParams { + max_change_denominator: u64::MAX as u128, + elasticity_multiplier: u64::MAX as u128 + }, + BaseFeeParams { max_change_denominator: 0, elasticity_multiplier: 0 } + )) + ); + assert!(check.is_mismatch()); + } + + #[test] + fn test_attributes_match() { + let cfg = default_rollup_config(); + let mut attributes = default_attributes(); + attributes.attributes.gas_limit = Some(0); + let block = Block::::default(); + let check = AttributesMatch::check(cfg, &attributes, &block); + assert_eq!(check, AttributesMatch::Match); + assert!(check.is_match()); + } +} diff --git a/kona/crates/node/engine/src/client.rs b/kona/crates/node/engine/src/client.rs new file mode 100644 index 0000000000000..1647f0cad3234 --- /dev/null +++ b/kona/crates/node/engine/src/client.rs @@ -0,0 +1,501 @@ +//! An Engine API Client. + +use crate::{Metrics, RollupBoostServer, RollupBoostServerArgs, RollupBoostServerLike}; +use alloy_eips::{BlockId, eip1898::BlockNumberOrTag}; +use alloy_network::{Ethereum, Network}; +use alloy_primitives::{Address, B256, BlockHash, Bytes, StorageKey}; +use alloy_provider::{EthGetBlock, Provider, RootProvider, RpcWithBlock, ext::EngineApi}; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_engine::{ + ClientVersionV1, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadInputV2, + ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, JwtSecret, + PayloadId, PayloadStatus, +}; +use alloy_rpc_types_eth::{Block, EIP1186AccountProofResponse}; +use alloy_transport::{RpcError, TransportErrorKind, TransportResult}; +use alloy_transport_http::{ + AuthLayer, AuthService, Http, HyperClient, + hyper_util::{ + client::legacy::{Client, connect::HttpConnector}, + rt::TokioExecutor, + }, +}; +use async_trait::async_trait; +use http::uri::InvalidUri; +use http_body_util::Full; +use kona_genesis::RollupConfig; +use kona_protocol::{FromBlockError, L2BlockInfo}; +use op_alloy_network::Optimism; +use op_alloy_provider::ext::engine::OpEngineApi; +use op_alloy_rpc_types::Transaction; +use op_alloy_rpc_types_engine::{ + OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, + OpPayloadAttributes, ProtocolVersion, +}; +use rollup_boost::{ + Flashblocks, FlashblocksService, FlashblocksWebsocketConfig, Probes, RpcClientError, +}; +use std::{ + future::Future, + net::{AddrParseError, IpAddr, SocketAddr}, + str::FromStr, + sync::Arc, + time::{Duration, Instant}, +}; +use thiserror::Error; +use tower::ServiceBuilder; +use url::Url; + +/// An error that occurred in the [`EngineClient`]. +#[derive(Error, Debug)] +pub enum EngineClientError { + /// An RPC error occurred + #[error("An RPC error occurred: {0}")] + RpcError(#[from] RpcError), + + /// An error occurred while decoding the payload + #[error("An error occurred while decoding the payload: {0}")] + BlockInfoDecodeError(#[from] FromBlockError), +} +/// A Hyper HTTP client with a JWT authentication layer. +pub type HyperAuthClient> = HyperClient>>; + +/// Engine API client used to communicate with L1/L2 ELs and optional rollup-boost. +/// EngineClient trait that is very coupled to its only implementation. +/// The main reason this exists is for mocking/unit testing. +#[async_trait] +pub trait EngineClient: OpEngineApi> + Send + Sync { + /// Returns a reference to the inner [`RollupConfig`]. + fn cfg(&self) -> &RollupConfig; + + /// Fetches the L1 block with the provided `BlockId`. + fn get_l1_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse>; + + /// Fetches the L2 block with the provided `BlockId`. + fn get_l2_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse>; + + /// Get the account and storage values of the specified account including the merkle proofs. + /// This call can be used to verify that the data has not been tampered with. + fn get_proof( + &self, + address: Address, + keys: Vec, + ) -> RpcWithBlock<(Address, Vec), EIP1186AccountProofResponse>; + + /// Sends the given payload to the execution layer client, as specified for the Paris fork. + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> TransportResult; + + /// Fetches the [`Block`] for the given [`BlockNumberOrTag`]. + async fn l2_block_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result>, EngineClientError>; + + /// Fetches the [L2BlockInfo] by [BlockNumberOrTag]. + async fn l2_block_info_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result, EngineClientError>; +} + +/// An Engine API client that provides authenticated HTTP communication with an execution layer. +/// +/// The [`OpEngineClient`] handles JWT authentication and manages connections to both L1 and L2 +/// execution layers. It automatically selects the appropriate Engine API version based on the +/// rollup configuration and block timestamps. +/// +/// Engine API client used to communicate with L1/L2 ELs and optional rollup-boost. +#[derive(Clone, Debug)] +pub struct OpEngineClient +where + L1Provider: Provider, + L2Provider: Provider, +{ + /// The L2 engine provider for Engine API calls. + engine: L2Provider, + /// The L1 chain provider for reading L1 data. + l1_provider: L1Provider, + /// The [`RollupConfig`] for determining Engine API versions based on hardfork activations. + cfg: Arc, + /// The rollup boost server + pub rollup_boost: Arc, +} + +impl OpEngineClient +where + L1Provider: Provider, + L2Provider: Provider, +{ + /// Creates a new RPC client for the given address and JWT secret. + pub fn rpc_client(addr: Url, jwt: JwtSecret) -> RootProvider { + let hyper_client = Client::builder(TokioExecutor::new()).build_http::>(); + let auth_layer = AuthLayer::new(jwt); + let service = ServiceBuilder::new().layer(auth_layer).service(hyper_client); + let layer_transport = HyperClient::with_service(service); + let http_hyper = Http::with_client(layer_transport, addr); + let rpc_client = RpcClient::new(http_hyper, false); + RootProvider::::new(rpc_client) + } +} + +/// The builder for the [`OpEngineClient`]. +#[derive(Debug, Clone)] +pub struct EngineClientBuilder { + /// The builder URL. + pub builder: Url, + /// The builder JWT secret. + pub builder_jwt: JwtSecret, + /// The builder timeout. + pub builder_timeout: Duration, + /// The L2 Engine API endpoint URL. + pub l2: Url, + /// The L2 JWT secret. + pub l2_jwt: JwtSecret, + /// The L2 timeout. + pub l2_timeout: Duration, + /// The L1 RPC URL. + pub l1_rpc: Url, + /// The [`RollupConfig`] for determining Engine API versions based on hardfork activations. + pub cfg: Arc, + /// The rollup boost arguments. + pub rollup_boost: RollupBoostServerArgs, +} + +/// An error that occurred in the [`EngineClientBuilder`]. +#[derive(Error, Debug)] +pub enum EngineClientBuilderError { + /// An error occurred while parsing the URL + #[error("An error occurred while parsing the URL: {0}")] + UrlParseError(#[from] InvalidUri), + /// An error occurred while parsing the IP address + #[error("An error occurred while parsing the IP address: {0}")] + IpAddrParseError(#[from] AddrParseError), + /// An error occurred while creating the RPC client + #[error("An error occurred while creating the RPC client: {0}")] + RpcClientError(#[from] RpcClientError), + /// An error occurred while creating the Flashblocks service + #[error("An error occurred while creating the Flashblocks service: {0}")] + FlashblocksError(String), +} + +impl EngineClientBuilder { + /// Creates a new [`OpEngineClient`] with authenticated HTTP connections. + /// + /// Sets up JWT-authenticated connections to the Engine API endpoint through the rollup-boost + /// server along with an unauthenticated connection to the L1 chain. + /// + /// # FIXME(@theochap, ``, ``): + /// This method can be simplified/improved in a few ways: + /// - Unify kona's and rollup-boost's RPC client creation + /// - Removed the `dyn RollupBoostServerLike` type erasure. + pub fn build( + self, + ) -> Result>, EngineClientBuilderError> + { + let probes = Arc::new(Probes::default()); + let l2_client = rollup_boost::RpcClient::new( + http::Uri::from_str(self.l2.to_string().as_str())?, + self.l2_jwt, + self.l2_timeout.as_millis() as u64, + rollup_boost::PayloadSource::L2, + )?; + let builder_client = rollup_boost::RpcClient::new( + http::Uri::from_str(self.builder.to_string().as_str())?, + self.builder_jwt, + self.builder_timeout.as_millis() as u64, + rollup_boost::PayloadSource::Builder, + )?; + + let rollup_boost_server: Box = + match self.rollup_boost.flashblocks { + Some(flashblocks) => { + let inbound_url = flashblocks.flashblocks_builder_url; + let outbound_addr = SocketAddr::new( + IpAddr::from_str(&flashblocks.flashblocks_host)?, + flashblocks.flashblocks_port, + ); + + let ws_config = flashblocks.flashblocks_ws_config; + + let builder_client = Arc::new( + Flashblocks::run( + builder_client, + inbound_url, + outbound_addr, + FlashblocksWebsocketConfig { + flashblock_builder_ws_initial_reconnect_ms: ws_config + .flashblock_builder_ws_initial_reconnect_ms, + flashblock_builder_ws_max_reconnect_ms: ws_config + .flashblock_builder_ws_max_reconnect_ms, + flashblock_builder_ws_ping_interval_ms: ws_config + .flashblock_builder_ws_ping_interval_ms, + flashblock_builder_ws_pong_timeout_ms: ws_config + .flashblock_builder_ws_pong_timeout_ms, + }, + ) + .map_err(|e| EngineClientBuilderError::FlashblocksError(e.to_string()))?, + ); + Box::new(rollup_boost::RollupBoostServer::::new( + l2_client, + builder_client, + self.rollup_boost.initial_execution_mode, + self.rollup_boost.block_selection_policy, + probes.clone(), + self.rollup_boost.external_state_root, + self.rollup_boost.ignore_unhealthy_builders, + )) + } + None => Box::new(rollup_boost::RollupBoostServer::::new( + l2_client, + Arc::new(builder_client), + self.rollup_boost.initial_execution_mode, + self.rollup_boost.block_selection_policy, + probes.clone(), + self.rollup_boost.external_state_root, + self.rollup_boost.ignore_unhealthy_builders, + )), + }; + + let rollup_boost = Arc::new(RollupBoostServer { server: rollup_boost_server, probes }); + + // TODO(ethereum-optimism/optimism#18656): remove this client, upstream the remaining + // EngineApiExt methods to the RollupBoostServer + let engine = OpEngineClient::>::rpc_client::( + self.l2, + self.l2_jwt, + ); + + let l1_provider = RootProvider::new_http(self.l1_rpc); + + Ok(OpEngineClient { engine, l1_provider, cfg: self.cfg, rollup_boost }) + } +} + +#[async_trait] +impl EngineClient for OpEngineClient +where + L1Provider: Provider, + L2Provider: Provider, +{ + fn cfg(&self) -> &RollupConfig { + self.cfg.as_ref() + } + + fn get_l1_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + self.l1_provider.get_block(block) + } + + fn get_l2_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + self.engine.get_block(block) + } + + fn get_proof( + &self, + address: Address, + keys: Vec, + ) -> RpcWithBlock<(Address, Vec), EIP1186AccountProofResponse> { + self.engine.get_proof(address, keys) + } + + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> TransportResult { + self.engine.new_payload_v1(payload).await + } + + async fn l2_block_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result>, EngineClientError> { + Ok(self.engine.get_block_by_number(numtag).full().await?) + } + + async fn l2_block_info_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result, EngineClientError> { + let block = self.engine.get_block_by_number(numtag).full().await?; + let Some(block) = block else { + return Ok(None); + }; + Ok(Some(L2BlockInfo::from_block_and_genesis(&block.into_consensus(), &self.cfg.genesis)?)) + } +} + +#[async_trait::async_trait] +impl OpEngineApi> + for OpEngineClient +where + L1Provider: Provider, + L2Provider: Provider, +{ + async fn new_payload_v2( + &self, + payload: ExecutionPayloadInputV2, + ) -> TransportResult { + let call = >>::new_payload_v2( + &self.engine, + payload, + ); + + record_call_time(call, Metrics::NEW_PAYLOAD_METHOD).await + } + + async fn new_payload_v3( + &self, + payload: ExecutionPayloadV3, + parent_beacon_block_root: B256, + ) -> TransportResult { + let call = + self.rollup_boost.server.new_payload_v3(payload, vec![], parent_beacon_block_root); + + record_call_time(call, Metrics::NEW_PAYLOAD_METHOD).await.map_err(Into::into) + } + + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + parent_beacon_block_root: B256, + ) -> TransportResult { + let call = self.rollup_boost.server.new_payload_v4( + payload.clone(), + vec![], + parent_beacon_block_root, + vec![], + ); + + record_call_time(call, Metrics::NEW_PAYLOAD_METHOD).await.map_err(Into::into) + } + + async fn fork_choice_updated_v2( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult { + let call = + >>::fork_choice_updated_v2( + &self.engine, + fork_choice_state, + payload_attributes, + ); + + record_call_time(call, Metrics::FORKCHOICE_UPDATE_METHOD).await + } + + async fn fork_choice_updated_v3( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult { + let call = + self.rollup_boost.server.fork_choice_updated_v3(fork_choice_state, payload_attributes); + + record_call_time(call, Metrics::FORKCHOICE_UPDATE_METHOD).await.map_err(Into::into) + } + + async fn get_payload_v2( + &self, + payload_id: PayloadId, + ) -> TransportResult { + let call = >>::get_payload_v2( + &self.engine, + payload_id, + ); + + record_call_time(call, Metrics::GET_PAYLOAD_METHOD).await + } + + async fn get_payload_v3( + &self, + payload_id: PayloadId, + ) -> TransportResult { + let call = self.rollup_boost.server.get_payload_v3(payload_id); + + record_call_time(call, Metrics::GET_PAYLOAD_METHOD).await.map_err(Into::into) + } + + async fn get_payload_v4( + &self, + payload_id: PayloadId, + ) -> TransportResult { + let call = self.rollup_boost.server.get_payload_v4(payload_id); + + record_call_time(call, Metrics::GET_PAYLOAD_METHOD).await.map_err(Into::into) + } + + async fn get_payload_bodies_by_hash_v1( + &self, + block_hashes: Vec, + ) -> TransportResult { + >>::get_payload_bodies_by_hash_v1( + &self.engine, + block_hashes, + ) + .await + } + + async fn get_payload_bodies_by_range_v1( + &self, + start: u64, + count: u64, + ) -> TransportResult { + , + >>::get_payload_bodies_by_range_v1(&self.engine, start, count).await + } + + async fn get_client_version_v1( + &self, + client_version: ClientVersionV1, + ) -> TransportResult> { + >>::get_client_version_v1( + &self.engine, + client_version, + ) + .await + } + + async fn signal_superchain_v1( + &self, + recommended: ProtocolVersion, + required: ProtocolVersion, + ) -> TransportResult { + >>::signal_superchain_v1( + &self.engine, + recommended, + required, + ) + .await + } + + async fn exchange_capabilities( + &self, + capabilities: Vec, + ) -> TransportResult> { + >>::exchange_capabilities( + &self.engine, + capabilities, + ) + .await + } +} + +/// Wrapper to record the time taken for a call to the engine API and log the result as a metric. +async fn record_call_time( + f: impl Future>, + metric_label: &'static str, +) -> Result { + // Await on the future and track its duration. + let start = Instant::now(); + let result = f.await?; + let duration = start.elapsed(); + + // Record the call duration. + kona_macros::record!( + histogram, + Metrics::ENGINE_METHOD_REQUEST_DURATION, + "method", + metric_label, + duration.as_secs_f64() + ); + Ok(result) +} diff --git a/kona/crates/node/engine/src/kinds.rs b/kona/crates/node/engine/src/kinds.rs new file mode 100644 index 0000000000000..6739e78937925 --- /dev/null +++ b/kona/crates/node/engine/src/kinds.rs @@ -0,0 +1,52 @@ +//! Contains the different kinds of execution engine clients that can be used. + +use derive_more::{Display, FromStr}; + +/// Identifies the type of execution layer client for behavior customization. +/// +/// Different execution clients may have slight variations in API behavior +/// or supported features. This enum allows the engine to adapt its behavior +/// accordingly, though as of v0.1.0, behavior is equivalent across all types. +/// +/// # Examples +/// +/// ```rust +/// use kona_engine::EngineKind; +/// use std::str::FromStr; +/// +/// // Parse from string +/// let kind = EngineKind::from_str("geth").unwrap(); +/// assert_eq!(kind, EngineKind::Geth); +/// +/// // Display as string +/// assert_eq!(EngineKind::Reth.to_string(), "reth"); +/// ``` +#[derive(Debug, Display, FromStr, Clone, Copy, PartialEq, Eq)] +pub enum EngineKind { + /// Geth execution client. + #[display("geth")] + Geth, + /// Reth execution client. + #[display("reth")] + Reth, + /// Erigon execution client. + #[display("erigon")] + Erigon, +} + +impl EngineKind { + /// Contains all valid engine client kinds. + pub const KINDS: [Self; 3] = [Self::Geth, Self::Reth, Self::Erigon]; + + /// Returns whether the engine client kind supports post finalization EL sync. + #[deprecated( + since = "0.1.0", + note = "Node behavior is now equivalent across all engine client types." + )] + pub const fn supports_post_finalization_elsync(self) -> bool { + match self { + Self::Geth => false, + Self::Erigon | Self::Reth => true, + } + } +} diff --git a/kona/crates/node/engine/src/lib.rs b/kona/crates/node/engine/src/lib.rs new file mode 100644 index 0000000000000..0140f708ede85 --- /dev/null +++ b/kona/crates/node/engine/src/lib.rs @@ -0,0 +1,84 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +//! ## Architecture +//! +//! The engine operates as a task-driven system where operations are queued and executed atomically: +//! +//! ```text +//! ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +//! │ Engine │◄───┤ Task Queue │◄───┤ Engine │ +//! │ Client │ │ (Priority) │ │ Tasks │ +//! └─────────────┘ └──────────────┘ └─────────────┘ +//! │ │ │ +//! ▼ ▼ ▼ +//! ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +//! │ Engine API │ │ Engine State │ │ Rollup │ +//! │ (HTTP/JWT) │ │ Updates │ │ Config │ +//! └─────────────┘ └──────────────┘ └─────────────┘ +//! ``` +//! +//! ## Module Organization +//! +//! - **Task Queue** - Core engine task queue and execution logic via [`Engine`] +//! - **Client** - HTTP client for Engine API communication via [`EngineClient`] +//! - **State** - Engine state management and synchronization via [`EngineState`] +//! - **Versions** - Engine API version selection via [`EngineForkchoiceVersion`], +//! [`EngineNewPayloadVersion`], [`EngineGetPayloadVersion`] +//! - **Attributes** - Payload attribute validation via [`AttributesMatch`] +//! - **Kinds** - Engine client type identification via [`EngineKind`] +//! - **Query** - Engine query interface via [`EngineQueries`] +//! - **Metrics** - Optional Prometheus metrics collection via [`Metrics`] + +#[macro_use] +extern crate tracing; + +mod task_queue; +pub use task_queue::{ + BuildTask, BuildTaskError, ConsolidateTask, ConsolidateTaskError, Engine, EngineBuildError, + EngineResetError, EngineTask, EngineTaskError, EngineTaskErrorSeverity, EngineTaskErrors, + EngineTaskExt, FinalizeTask, FinalizeTaskError, InsertTask, InsertTaskError, SealTask, + SealTaskError, SynchronizeTask, SynchronizeTaskError, +}; + +mod attributes; +pub use attributes::{AttributesMatch, AttributesMismatch}; + +mod client; +pub use client::{ + EngineClient, EngineClientBuilder, EngineClientBuilderError, EngineClientError, + HyperAuthClient, OpEngineClient, +}; + +mod rollup_boost; +pub use rollup_boost::{ + FlashblocksClientArgs, FlashblocksWebsocketConfig, RollupBoostServer, RollupBoostServerArgs, + RollupBoostServerError, RollupBoostServerLike, +}; + +mod versions; +pub use versions::{EngineForkchoiceVersion, EngineGetPayloadVersion, EngineNewPayloadVersion}; + +mod state; +pub use state::{EngineState, EngineSyncState, EngineSyncStateUpdate}; + +mod kinds; +pub use kinds::EngineKind; + +mod query; +pub use query::{EngineQueries, EngineQueriesError, EngineQuerySender}; + +mod metrics; +pub use metrics::Metrics; + +mod sync; +pub use sync::{L2ForkchoiceState, SyncStartError, find_starting_forkchoice}; + +#[cfg(any(test, feature = "test-utils"))] +/// Utilities that are useful when creating unit tests using structs within this library. +pub mod test_utils; diff --git a/kona/crates/node/engine/src/metrics/mod.rs b/kona/crates/node/engine/src/metrics/mod.rs new file mode 100644 index 0000000000000..87a65e372e6c8 --- /dev/null +++ b/kona/crates/node/engine/src/metrics/mod.rs @@ -0,0 +1,131 @@ +//! Prometheus metrics collection for engine operations. +//! +//! Provides metric identifiers and labels for monitoring engine performance, +//! task execution, and block progression through safety levels. + +/// Metrics container with constants for Prometheus metric collection. +/// +/// Contains identifiers for gauges, counters, and histograms used to monitor +/// engine operations when the `metrics` feature is enabled. Metrics track: +/// +/// - Block progression through safety levels (unsafe → finalized) +/// - Task execution success/failure rates by type +/// - Engine API method call latencies +/// +/// # Usage +/// +/// ```rust,ignore +/// use metrics::{counter, gauge, histogram}; +/// use kona_engine::Metrics; +/// +/// // Track successful task execution +/// counter!(Metrics::ENGINE_TASK_SUCCESS, "task" => Metrics::INSERT_TASK_LABEL); +/// +/// // Record block height at safety level +/// gauge!(Metrics::BLOCK_LABELS, block_num as f64, "level" => Metrics::SAFE_BLOCK_LABEL); +/// +/// // Time Engine API calls +/// histogram!(Metrics::ENGINE_METHOD_REQUEST_DURATION, duration.as_secs_f64()); +/// ``` +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the gauge that tracks block labels. + pub const BLOCK_LABELS: &str = "kona_node_block_labels"; + /// Unsafe block label. + pub const UNSAFE_BLOCK_LABEL: &str = "unsafe"; + /// Cross-unsafe block label. + pub const CROSS_UNSAFE_BLOCK_LABEL: &str = "cross-unsafe"; + /// Local-safe block label. + pub const LOCAL_SAFE_BLOCK_LABEL: &str = "local-safe"; + /// Safe block label. + pub const SAFE_BLOCK_LABEL: &str = "safe"; + /// Finalized block label. + pub const FINALIZED_BLOCK_LABEL: &str = "finalized"; + + /// Identifier for the counter that records engine task counts. + pub const ENGINE_TASK_SUCCESS: &str = "kona_node_engine_task_count"; + /// Identifier for the counter that records engine task counts. + pub const ENGINE_TASK_FAILURE: &str = "kona_node_engine_task_failure"; + + /// Insert task label. + pub const INSERT_TASK_LABEL: &str = "insert"; + /// Consolidate task label. + pub const CONSOLIDATE_TASK_LABEL: &str = "consolidate"; + /// Forkchoice task label. + pub const FORKCHOICE_TASK_LABEL: &str = "forkchoice-update"; + /// Build task label. + pub const BUILD_TASK_LABEL: &str = "build"; + /// Seal task label. + pub const SEAL_TASK_LABEL: &str = "seal"; + /// Finalize task label. + pub const FINALIZE_TASK_LABEL: &str = "finalize"; + + /// Identifier for the histogram that tracks engine method call time. + pub const ENGINE_METHOD_REQUEST_DURATION: &str = "kona_node_engine_method_request_duration"; + /// `engine_forkchoiceUpdatedV` label + pub const FORKCHOICE_UPDATE_METHOD: &str = "engine_forkchoiceUpdated"; + /// `engine_newPayloadV` label. + pub const NEW_PAYLOAD_METHOD: &str = "engine_newPayload"; + /// `engine_getPayloadV` label. + pub const GET_PAYLOAD_METHOD: &str = "engine_getPayload"; + + /// Identifier for the counter that tracks the number of times the engine has been reset. + pub const ENGINE_RESET_COUNT: &str = "kona_node_engine_reset_count"; + + /// Initializes metrics for the engine. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in [`kona_engine`][crate]. + #[cfg(feature = "metrics")] + pub fn describe() { + // Block labels + metrics::describe_gauge!(Self::BLOCK_LABELS, "Blockchain head labels"); + + // Engine task counts + metrics::describe_counter!(Self::ENGINE_TASK_SUCCESS, "Engine tasks successfully executed"); + metrics::describe_counter!(Self::ENGINE_TASK_FAILURE, "Engine tasks failed"); + + // Engine method request duration histogram + metrics::describe_histogram!( + Self::ENGINE_METHOD_REQUEST_DURATION, + metrics::Unit::Seconds, + "Engine method request duration" + ); + + // Engine reset counter + metrics::describe_counter!( + Self::ENGINE_RESET_COUNT, + metrics::Unit::Count, + "Engine reset count" + ); + } + + /// Initializes metrics to `0` so they can be queried immediately by consumers of prometheus + /// metrics. + #[cfg(feature = "metrics")] + pub fn zero() { + // Engine task counts + kona_macros::set!(counter, Self::ENGINE_TASK_SUCCESS, Self::INSERT_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_SUCCESS, Self::CONSOLIDATE_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_SUCCESS, Self::BUILD_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_SUCCESS, Self::FINALIZE_TASK_LABEL, 0); + + kona_macros::set!(counter, Self::ENGINE_TASK_FAILURE, Self::INSERT_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_FAILURE, Self::CONSOLIDATE_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_FAILURE, Self::BUILD_TASK_LABEL, 0); + kona_macros::set!(counter, Self::ENGINE_TASK_FAILURE, Self::FINALIZE_TASK_LABEL, 0); + + // Engine reset count + kona_macros::set!(counter, Self::ENGINE_RESET_COUNT, 0); + } +} diff --git a/kona/crates/node/engine/src/query.rs b/kona/crates/node/engine/src/query.rs new file mode 100644 index 0000000000000..e30c36ae3146e --- /dev/null +++ b/kona/crates/node/engine/src/query.rs @@ -0,0 +1,143 @@ +//! Engine query interface for external communication. +//! +//! Provides a channel-based API for querying engine state and configuration +//! from external actors. Uses oneshot channels for responses to maintain +//! clean async communication patterns. + +use std::sync::Arc; + +use alloy_eips::BlockNumberOrTag; +use alloy_transport::{RpcError, TransportErrorKind}; +use kona_genesis::RollupConfig; +use kona_protocol::{L2BlockInfo, OutputRoot, Predeploys}; +use tokio::sync::oneshot::Sender; + +use crate::{EngineClient, EngineClientError, EngineState}; + +/// Channel sender for submitting [`EngineQueries`] to the engine. +pub type EngineQuerySender = tokio::sync::mpsc::Sender; + +/// Query types supported by the engine for external communication. +/// +/// Each variant includes a oneshot sender for the response, enabling +/// async request-response patterns. The engine processes these queries +/// and sends responses back through the provided channels. +#[derive(Debug)] +pub enum EngineQueries { + /// Request the current rollup configuration. + Config(Sender), + /// Request the current [`EngineState`] snapshot. + State(Sender), + /// Request the L2 output root for a specific block. + /// + /// Returns a tuple of block info, output root, and engine state at the requested block. + OutputAtBlock { + /// The block number or tag to retrieve the output for. + block: BlockNumberOrTag, + /// Response channel for (block_info, output_root, engine_state). + sender: Sender<(L2BlockInfo, OutputRoot, EngineState)>, + }, + /// Subscribe to engine state updates via a watch channel receiver. + StateReceiver(Sender>), + /// Development API: Subscribe to task queue length updates. + QueueLengthReceiver(Sender>), + /// Development API: Get the current number of pending tasks in the queue. + TaskQueueLength(Sender), +} + +/// An error that can occur when querying the engine. +#[derive(Debug, thiserror::Error)] +pub enum EngineQueriesError { + /// The output channel was closed unexpectedly. Impossible to send query response. + #[error("Output channel closed unexpectedly. Impossible to send query response")] + OutputChannelClosed, + /// Failed to retrieve the L2 block by label. + #[error("Failed to retrieve L2 block by label: {0}")] + BlockRetrievalFailed(#[from] EngineClientError), + /// No block withdrawals root while Isthmus is active. + #[error("No block withdrawals root while Isthmus is active")] + NoWithdrawalsRoot, + /// No L2 block found for block number or tag. + #[error("No L2 block found for block number or tag: {0}")] + NoL2BlockFound(BlockNumberOrTag), + /// Impossible to retrieve L2 withdrawals root from state. + #[error("Impossible to retrieve L2 withdrawals root from state. {0}")] + FailedToRetrieveWithdrawalsRoot(#[from] RpcError), +} + +impl EngineQueries { + /// Handles the engine query request. + pub async fn handle( + self, + state_recv: &tokio::sync::watch::Receiver, + queue_length_recv: &tokio::sync::watch::Receiver, + client: &Arc, + rollup_config: &Arc, + ) -> Result<(), EngineQueriesError> { + let state = *state_recv.borrow(); + + match self { + Self::Config(sender) => sender + .send((**rollup_config).clone()) + .map_err(|_| EngineQueriesError::OutputChannelClosed), + Self::State(sender) => { + sender.send(state).map_err(|_| EngineQueriesError::OutputChannelClosed) + } + Self::OutputAtBlock { block, sender } => { + let output_block = client.l2_block_by_label(block).await?; + let output_block = output_block.ok_or(EngineQueriesError::NoL2BlockFound(block))?; + // Cloning the l2 block below is cheaper than sending a network request to get the + // l2 block info. Querying the `L2BlockInfo` from the client ends up + // fetching the full l2 block again. + let consensus_block = output_block.clone().into_consensus(); + let output_block_info = + L2BlockInfo::from_block_and_genesis::( + &consensus_block.map_transactions(|tx| tx.inner.inner.into_inner()), + &rollup_config.genesis, + ) + .map_err(|_| EngineQueriesError::NoL2BlockFound(block))?; + + let state_root = output_block.header.state_root; + + let message_passer_storage_root = + if rollup_config.is_isthmus_active(output_block.header.timestamp) { + output_block + .header + .withdrawals_root + .ok_or(EngineQueriesError::NoWithdrawalsRoot)? + } else { + // Fetch the storage root for the L2 head block. + let l2_to_l1_message_passer = client + .get_proof(Predeploys::L2_TO_L1_MESSAGE_PASSER, Default::default()) + .block_id(block.into()) + .await?; + + l2_to_l1_message_passer.storage_hash + }; + + let output_response_v0 = OutputRoot::from_parts( + state_root, + message_passer_storage_root, + output_block.header.hash, + ); + + sender + .send((output_block_info, output_response_v0, state)) + .map_err(|_| EngineQueriesError::OutputChannelClosed) + } + Self::StateReceiver(subscription) => subscription + .send(state_recv.clone()) + .map_err(|_| EngineQueriesError::OutputChannelClosed), + Self::QueueLengthReceiver(subscription) => subscription + .send(queue_length_recv.clone()) + .map_err(|_| EngineQueriesError::OutputChannelClosed), + Self::TaskQueueLength(sender) => { + let queue_length = *queue_length_recv.borrow(); + if sender.send(queue_length).is_err() { + warn!(target: "engine", "Failed to send task queue length response"); + } + Ok(()) + } + } + } +} diff --git a/kona/crates/node/engine/src/rollup_boost.rs b/kona/crates/node/engine/src/rollup_boost.rs new file mode 100644 index 0000000000000..9c507cfcc793a --- /dev/null +++ b/kona/crates/node/engine/src/rollup_boost.rs @@ -0,0 +1,222 @@ +//! Rollup-boost abstraction used by the engine client. + +use alloy_json_rpc::{ErrorPayload, RpcError}; +use alloy_primitives::{B256, Bytes}; +use alloy_rpc_types_engine::{ + ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus, +}; +use alloy_transport::TransportErrorKind; +use op_alloy_rpc_types_engine::{ + OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, + OpPayloadAttributes, +}; +use rollup_boost::{EngineApiExt, EngineApiServer, ExecutionMode, Health, Probes}; +use std::{fmt::Debug, sync::Arc}; + +use rollup_boost::BlockSelectionPolicy; +use url::Url; + +/// Configuration for the rollup-boost server. +#[derive(Clone, Debug)] +pub struct RollupBoostServerArgs { + /// The initial execution mode of the rollup-boost server. + pub initial_execution_mode: ExecutionMode, + /// The block selection policy of the rollup-boost server. + pub block_selection_policy: Option, + /// Whether to use the l2 client for computing state root. + pub external_state_root: bool, + /// Allow all engine API calls to builder even when marked as unhealthy + /// This is default true assuming no builder CL set up + pub ignore_unhealthy_builders: bool, + /// Flashblocks configuration + pub flashblocks: Option, +} + +/// Configuration for the Flashblocks client. +#[derive(Clone, Debug)] +pub struct FlashblocksClientArgs { + /// Flashblocks Builder WebSocket URL + pub flashblocks_builder_url: Url, + + /// Flashblocks WebSocket host for outbound connections + pub flashblocks_host: String, + + /// Flashblocks WebSocket port for outbound connections + pub flashblocks_port: u16, + + /// Websocket connection configuration + pub flashblocks_ws_config: FlashblocksWebsocketConfig, +} + +/// Configuration for the Flashblocks WebSocket connection. +#[derive(Debug, Clone, Copy)] +pub struct FlashblocksWebsocketConfig { + /// Minimum time for exponential backoff for timeout if builder disconnected + pub flashblock_builder_ws_initial_reconnect_ms: u64, + + /// Maximum time for exponential backoff for timeout if builder disconnected + pub flashblock_builder_ws_max_reconnect_ms: u64, + + /// Interval in milliseconds between ping messages sent to upstream servers to detect + /// unresponsive connections + pub flashblock_builder_ws_ping_interval_ms: u64, + + /// Timeout in milliseconds to wait for pong responses from upstream servers before considering + /// the connection dead + pub flashblock_builder_ws_pong_timeout_ms: u64, +} + +/// An error that occurred in the rollup-boost server. +#[derive(Debug, thiserror::Error)] +pub enum RollupBoostServerError { + /// JSON-RPC error. + #[error("Rollup boost server error: {0}")] + Jsonrpsee(#[from] jsonrpsee_types::ErrorObjectOwned), +} + +impl From for RpcError { + fn from(error: RollupBoostServerError) -> Self { + match error { + RollupBoostServerError::Jsonrpsee(error) => Self::ErrorResp(ErrorPayload { + code: error.code().into(), + message: error.message().to_string().into(), + data: None, + }), + } + } +} + +/// Trait object used to erase the concrete rollup-boost server type. +#[async_trait::async_trait] +pub trait RollupBoostServerLike: Debug + Send + Sync { + /// Sets the execution mode. + fn set_execution_mode(&self, execution_mode: ExecutionMode); + + /// Gets the execution mode. + fn get_execution_mode(&self) -> ExecutionMode; + + /// Creates a new payload v3. + async fn new_payload_v3( + &self, + payload: ExecutionPayloadV3, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + ) -> Result; + + /// Creates a new payload v4. + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + execution_requests: Vec, + ) -> Result; + + /// Performs a fork choice updated v3. + async fn fork_choice_updated_v3( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> Result; + + /// Gets a payload v3. + async fn get_payload_v3( + &self, + payload_id: PayloadId, + ) -> Result; + + /// Gets a payload v4. + async fn get_payload_v4( + &self, + payload_id: PayloadId, + ) -> Result; +} + +#[async_trait::async_trait] +impl RollupBoostServerLike + for rollup_boost::RollupBoostServer +{ + fn set_execution_mode(&self, execution_mode: ExecutionMode) { + *self.execution_mode.lock() = execution_mode; + } + + fn get_execution_mode(&self) -> ExecutionMode { + *self.execution_mode.lock() + } + + async fn new_payload_v3( + &self, + payload: ExecutionPayloadV3, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + ) -> Result { + EngineApiServer::new_payload_v3(self, payload, versioned_hashes, parent_beacon_block_root) + .await + .map_err(RollupBoostServerError::from) + } + + async fn new_payload_v4( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + execution_requests: Vec, + ) -> Result { + EngineApiServer::new_payload_v4( + self, + payload, + versioned_hashes, + parent_beacon_block_root, + execution_requests, + ) + .await + .map_err(RollupBoostServerError::from) + } + + async fn fork_choice_updated_v3( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> Result { + EngineApiServer::fork_choice_updated_v3(self, fork_choice_state, payload_attributes) + .await + .map_err(RollupBoostServerError::from) + } + + async fn get_payload_v3( + &self, + payload_id: PayloadId, + ) -> Result { + EngineApiServer::get_payload_v3(self, payload_id) + .await + .map_err(RollupBoostServerError::from) + } + + async fn get_payload_v4( + &self, + payload_id: PayloadId, + ) -> Result { + EngineApiServer::get_payload_v4(self, payload_id) + .await + .map_err(RollupBoostServerError::from) + } +} + +/// Structure that wraps a rollup boost server and its probes. +/// +/// TODO(op-rs/kona#3053): remove this wrapper and use the RollupBoostServer directly +/// Remove the dynamic dispatch and use generics instead. +#[derive(Debug)] +pub struct RollupBoostServer { + /// The rollup boost server implementation + pub server: Box, + /// Rollup boost probes + pub probes: Arc, +} + +impl RollupBoostServer { + /// Gets the health of the rollup boost server. + pub fn get_health(&self) -> Health { + self.probes.health() + } +} diff --git a/kona/crates/node/engine/src/state/core.rs b/kona/crates/node/engine/src/state/core.rs new file mode 100644 index 0000000000000..6f4de6b43b649 --- /dev/null +++ b/kona/crates/node/engine/src/state/core.rs @@ -0,0 +1,258 @@ +//! The internal state of the engine controller. + +use crate::Metrics; +use alloy_rpc_types_engine::ForkchoiceState; +use kona_protocol::L2BlockInfo; +use serde::{Deserialize, Serialize}; + +/// The synchronization state of the execution layer across different safety levels. +/// +/// Tracks block progression through various stages of verification and finalization, +/// from initial unsafe blocks received via P2P to fully finalized blocks derived from +/// finalized L1 data. Each level represents increasing confidence in the block's validity. +/// +/// # Safety Levels +/// +/// The state tracks blocks at different safety levels, listed from least to most safe: +/// +/// 1. **Unsafe** - Most recent blocks from P2P network (unverified) +/// 2. **Cross-unsafe** - Unsafe blocks with cross-layer verification +/// 3. **Local-safe** - Derived from L1 data, completed span-batch +/// 4. **Safe** - Cross-verified with safe L1 dependencies +/// 5. **Finalized** - Derived from finalized L1 data only +/// +/// See the [OP Stack specifications](https://specs.optimism.io) for detailed safety definitions. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub struct EngineSyncState { + /// Most recent block found on the P2P network (lowest safety level). + unsafe_head: L2BlockInfo, + /// Cross-verified unsafe head (equal to unsafe_head pre-interop). + cross_unsafe_head: L2BlockInfo, + /// Derived from L1 data as a completed span-batch, but not yet cross-verified. + local_safe_head: L2BlockInfo, + /// Derived from L1 data and cross-verified to have safe L1 dependencies. + safe_head: L2BlockInfo, + /// Derived from finalized L1 data with only finalized dependencies (highest safety level). + finalized_head: L2BlockInfo, +} + +impl EngineSyncState { + /// Returns the current unsafe head. + pub const fn unsafe_head(&self) -> L2BlockInfo { + self.unsafe_head + } + + /// Returns the current cross-verified unsafe head. + pub const fn cross_unsafe_head(&self) -> L2BlockInfo { + self.cross_unsafe_head + } + + /// Returns the current local safe head. + pub const fn local_safe_head(&self) -> L2BlockInfo { + self.local_safe_head + } + + /// Returns the current safe head. + pub const fn safe_head(&self) -> L2BlockInfo { + self.safe_head + } + + /// Returns the current finalized head. + pub const fn finalized_head(&self) -> L2BlockInfo { + self.finalized_head + } + + /// Creates a `ForkchoiceState` + /// + /// - `head_block` = `unsafe_head` + /// - `safe_block` = `safe_head` + /// - `finalized_block` = `finalized_head` + /// + /// If the block info is not yet available, the default values are used. + pub const fn create_forkchoice_state(&self) -> ForkchoiceState { + ForkchoiceState { + head_block_hash: self.unsafe_head.hash(), + safe_block_hash: self.safe_head.hash(), + finalized_block_hash: self.finalized_head.hash(), + } + } + + /// Applies the update to the provided sync state, using the current state values if the update + /// is not specified. Returns the new sync state. + pub fn apply_update(self, sync_state_update: EngineSyncStateUpdate) -> Self { + if let Some(unsafe_head) = sync_state_update.unsafe_head { + Self::update_block_label_metric( + Metrics::UNSAFE_BLOCK_LABEL, + unsafe_head.block_info.number, + ); + } + if let Some(cross_unsafe_head) = sync_state_update.cross_unsafe_head { + Self::update_block_label_metric( + Metrics::CROSS_UNSAFE_BLOCK_LABEL, + cross_unsafe_head.block_info.number, + ); + } + if let Some(local_safe_head) = sync_state_update.local_safe_head { + Self::update_block_label_metric( + Metrics::LOCAL_SAFE_BLOCK_LABEL, + local_safe_head.block_info.number, + ); + } + if let Some(safe_head) = sync_state_update.safe_head { + Self::update_block_label_metric(Metrics::SAFE_BLOCK_LABEL, safe_head.block_info.number); + } + if let Some(finalized_head) = sync_state_update.finalized_head { + Self::update_block_label_metric( + Metrics::FINALIZED_BLOCK_LABEL, + finalized_head.block_info.number, + ); + } + + Self { + unsafe_head: sync_state_update.unsafe_head.unwrap_or(self.unsafe_head), + cross_unsafe_head: sync_state_update + .cross_unsafe_head + .unwrap_or(self.cross_unsafe_head), + local_safe_head: sync_state_update.local_safe_head.unwrap_or(self.local_safe_head), + safe_head: sync_state_update.safe_head.unwrap_or(self.safe_head), + finalized_head: sync_state_update.finalized_head.unwrap_or(self.finalized_head), + } + } + + /// Updates a block label metric, keyed by the label. + #[inline] + fn update_block_label_metric(label: &'static str, number: u64) { + kona_macros::set!(gauge, Metrics::BLOCK_LABELS, "label", label, number as f64); + } +} + +/// Specifies how to update the sync state of the engine. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EngineSyncStateUpdate { + /// Most recent block found on the p2p network + pub unsafe_head: Option, + /// Cross-verified unsafe head, always equal to the unsafe head pre-interop + pub cross_unsafe_head: Option, + /// Derived from L1, and known to be a completed span-batch, + /// but not cross-verified yet. + pub local_safe_head: Option, + /// Derived from L1 and cross-verified to have cross-safe dependencies. + pub safe_head: Option, + /// Derived from finalized L1 data, + /// and cross-verified to only have finalized dependencies. + pub finalized_head: Option, +} + +/// The chain state viewed by the engine controller. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub struct EngineState { + /// The sync state of the engine. + pub sync_state: EngineSyncState, + + /// Whether or not the EL has finished syncing. + pub el_sync_finished: bool, + + /// Track when the rollup node changes the forkchoice to restore previous + /// known unsafe chain. e.g. Unsafe Reorg caused by Invalid span batch. + /// This update does not retry except engine returns non-input error + /// because engine may forgot backupUnsafeHead or backupUnsafeHead is not part + /// of the chain. + pub need_fcu_call_backup_unsafe_reorg: bool, +} + +impl EngineState { + /// Returns if consolidation is needed. + /// + /// [Consolidation] is only performed by a rollup node when the unsafe head + /// is ahead of the safe head. When the two are equal, consolidation isn't + /// required and the [`crate::BuildTask`] can be used to build the block. + /// + /// [Consolidation]: https://specs.optimism.io/protocol/derivation.html#l1-consolidation-payload-attributes-matching + pub fn needs_consolidation(&self) -> bool { + self.sync_state.safe_head() != self.sync_state.unsafe_head() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Metrics; + use kona_protocol::BlockInfo; + use metrics_exporter_prometheus::PrometheusBuilder; + use rstest::rstest; + + impl EngineState { + /// Set the unsafe head. + pub fn set_unsafe_head(&mut self, unsafe_head: L2BlockInfo) { + self.sync_state.apply_update(EngineSyncStateUpdate { + unsafe_head: Some(unsafe_head), + ..Default::default() + }); + } + + /// Set the cross-verified unsafe head. + pub fn set_cross_unsafe_head(&mut self, cross_unsafe_head: L2BlockInfo) { + self.sync_state.apply_update(EngineSyncStateUpdate { + cross_unsafe_head: Some(cross_unsafe_head), + ..Default::default() + }); + } + + /// Set the local safe head. + pub fn set_local_safe_head(&mut self, local_safe_head: L2BlockInfo) { + self.sync_state.apply_update(EngineSyncStateUpdate { + local_safe_head: Some(local_safe_head), + ..Default::default() + }); + } + + /// Set the safe head. + pub fn set_safe_head(&mut self, safe_head: L2BlockInfo) { + self.sync_state.apply_update(EngineSyncStateUpdate { + safe_head: Some(safe_head), + ..Default::default() + }); + } + + /// Set the finalized head. + pub fn set_finalized_head(&mut self, finalized_head: L2BlockInfo) { + self.sync_state.apply_update(EngineSyncStateUpdate { + finalized_head: Some(finalized_head), + ..Default::default() + }); + } + } + + #[rstest] + #[case::set_unsafe(EngineState::set_unsafe_head, Metrics::UNSAFE_BLOCK_LABEL, 1)] + #[case::set_cross_unsafe( + EngineState::set_cross_unsafe_head, + Metrics::CROSS_UNSAFE_BLOCK_LABEL, + 2 + )] + #[case::set_local_safe(EngineState::set_local_safe_head, Metrics::LOCAL_SAFE_BLOCK_LABEL, 3)] + #[case::set_safe_head(EngineState::set_safe_head, Metrics::SAFE_BLOCK_LABEL, 4)] + #[case::set_finalized_head(EngineState::set_finalized_head, Metrics::FINALIZED_BLOCK_LABEL, 5)] + #[cfg(feature = "metrics")] + fn test_chain_label_metrics( + #[case] set_fn: impl Fn(&mut EngineState, L2BlockInfo), + #[case] label_name: &str, + #[case] number: u64, + ) { + let handle = PrometheusBuilder::new().install_recorder().unwrap(); + crate::Metrics::init(); + + let mut state = EngineState::default(); + set_fn( + &mut state, + L2BlockInfo { + block_info: BlockInfo { number, ..Default::default() }, + ..Default::default() + }, + ); + + assert!(handle.render().contains( + format!("kona_node_block_labels{{label=\"{label_name}\"}} {number}").as_str() + )); + } +} diff --git a/kona/crates/node/engine/src/state/mod.rs b/kona/crates/node/engine/src/state/mod.rs new file mode 100644 index 0000000000000..b61100917e612 --- /dev/null +++ b/kona/crates/node/engine/src/state/mod.rs @@ -0,0 +1,4 @@ +//! Engine State + +mod core; +pub use core::{EngineState, EngineSyncState, EngineSyncStateUpdate}; diff --git a/kona/crates/node/engine/src/sync/error.rs b/kona/crates/node/engine/src/sync/error.rs new file mode 100644 index 0000000000000..d98b69d642b2e --- /dev/null +++ b/kona/crates/node/engine/src/sync/error.rs @@ -0,0 +1,41 @@ +//! Contains the error types used for finding the starting forkchoice state. + +use alloy_eips::BlockId; +use alloy_primitives::B256; +use alloy_transport::{RpcError, TransportErrorKind}; +use kona_protocol::FromBlockError; +use thiserror::Error; + +/// An error that can occur during the sync start process. +#[derive(Error, Debug)] +pub enum SyncStartError { + /// An rpc error occurred + #[error("An RPC error occurred: {0}")] + RpcError(#[from] RpcError), + /// An error occurred while converting a block to [`L2BlockInfo`]. + /// + /// [`L2BlockInfo`]: kona_protocol::L2BlockInfo + #[error(transparent)] + FromBlock(#[from] FromBlockError), + /// A block could not be found. + #[error("Block not found: {0}")] + BlockNotFound(BlockId), + /// Invalid L1 genesis hash. + #[error("Invalid L1 genesis hash. Expected {0}, Got {1}")] + InvalidL1GenesisHash(B256, B256), + /// Invalid L2 genesis hash. + #[error("Invalid L2 genesis hash. Expected {0}, Got {1}")] + InvalidL2GenesisHash(B256, B256), + /// Finalized block mismatch + #[error("Finalized block mismatch. Expected {0}, Got {1}")] + MismatchedFinalizedBlock(B256, B256), + /// L1 origin mismatch. + #[error("L1 origin mismatch")] + L1OriginMismatch, + /// Non-zero sequence number. + #[error("Non-zero sequence number for block with different L1 origin")] + NonZeroSequenceNumber, + /// Inconsistent sequence number. + #[error("Inconsistent sequence number; Must monotonically increase.")] + InconsistentSequenceNumber, +} diff --git a/kona/crates/node/engine/src/sync/forkchoice.rs b/kona/crates/node/engine/src/sync/forkchoice.rs new file mode 100644 index 0000000000000..c1c604d46cbc7 --- /dev/null +++ b/kona/crates/node/engine/src/sync/forkchoice.rs @@ -0,0 +1,102 @@ +//! Contains the forkchoice state for the L2. + +use crate::{EngineClient, SyncStartError}; +use alloy_eips::{BlockId, BlockNumberOrTag}; +use alloy_provider::Network; +use alloy_transport::TransportResult; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; +use op_alloy_network::Optimism; +use std::fmt::Display; + +/// An unsafe, safe, and finalized [L2BlockInfo] returned by the [crate::find_starting_forkchoice] +/// function. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct L2ForkchoiceState { + /// The unsafe L2 block. + pub un_safe: L2BlockInfo, + /// The safe L2 block. + pub safe: L2BlockInfo, + /// The finalized L2 block. + pub finalized: L2BlockInfo, +} + +impl Display for L2ForkchoiceState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "FINALIZED: {} (#{}) | SAFE: {} (#{}) | UNSAFE: {} (#{})", + self.finalized.block_info.hash, + self.finalized.block_info.number, + self.safe.block_info.hash, + self.safe.block_info.number, + self.un_safe.block_info.hash, + self.un_safe.block_info.number, + ) + } +} + +impl L2ForkchoiceState { + /// Fetches the current forkchoice state of the L2 execution layer. + /// + /// - The finalized block may not always be available. If it is not, we fall back to genesis. + /// - The safe block may not always be available. If it is not, we fall back to the finalized + /// block. + /// - The unsafe block is always assumed to be available. + pub async fn current( + cfg: &RollupConfig, + engine_client: &EngineClient_, + ) -> Result { + let finalized = { + let rpc_block = + match get_block_compat(engine_client, BlockNumberOrTag::Finalized.into()).await { + Ok(Some(block)) => block, + Ok(None) => engine_client + .get_l2_block(cfg.genesis.l2.number.into()) + .full() + .await? + .ok_or(SyncStartError::BlockNotFound(cfg.genesis.l2.number.into()))?, + Err(e) => return Err(e.into()), + } + .into_consensus(); + + L2BlockInfo::from_block_and_genesis(&rpc_block, &cfg.genesis)? + }; + let safe = match get_block_compat(engine_client, BlockNumberOrTag::Safe.into()).await { + Ok(Some(block)) => { + L2BlockInfo::from_block_and_genesis(&block.into_consensus(), &cfg.genesis)? + } + Ok(None) => finalized, + Err(e) => return Err(e.into()), + }; + let un_safe = { + let rpc_block = get_block_compat(engine_client, BlockNumberOrTag::Latest.into()) + .await? + .ok_or(SyncStartError::BlockNotFound(BlockNumberOrTag::Latest.into()))?; + L2BlockInfo::from_block_and_genesis(&rpc_block.into_consensus(), &cfg.genesis)? + }; + + Ok(Self { un_safe, safe, finalized }) + } +} + +/// Wrapper function around [`EngineClient::get_l2_block`] to handle compatibility issues with geth +/// and erigon. When serving a block-by-number request, these clients will return non-standard +/// errors for the safe and finalized heads when the chain has just started and nothing is marked as +/// safe or finalized yet. +async fn get_block_compat( + engine_client: &EngineClient_, + block_id: BlockId, +) -> TransportResult::BlockResponse>> { + match engine_client.get_l2_block(block_id).full().await { + Err(e) => { + let err_str = e.to_string(); + if err_str.contains("block not found") || err_str.contains("Unknown block") { + Ok(None) + } else { + Err(e) + } + } + r => r, + } +} diff --git a/kona/crates/node/engine/src/sync/mod.rs b/kona/crates/node/engine/src/sync/mod.rs new file mode 100644 index 0000000000000..3d798f2146a82 --- /dev/null +++ b/kona/crates/node/engine/src/sync/mod.rs @@ -0,0 +1,150 @@ +//! Sync start algorithm for the OP Stack rollup node. + +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; + +mod forkchoice; +pub use forkchoice::L2ForkchoiceState; + +mod error; +pub use error::SyncStartError; + +use tracing::info; + +use crate::EngineClient; + +/// Searches for the latest [`L2ForkchoiceState`] that we can use to start the sync process with. +/// +/// - The *unsafe L2 block*: This is the highest L2 block whose L1 origin is a *plausible* +/// extension of the canonical L1 chain (as known to the rollup node). +/// - The *safe L2 block*: This is the highest L2 block whose epoch's sequencing window is +/// complete within the canonical L1 chain (as known to the rollup node). +/// - The *finalized L2 block*: This is the L2 block which is known to be fully derived from +/// finalized L1 block data. +/// +/// Plausible: meaning that the blockhash of the L2 block's L1 origin +/// (as reported in the L1 Attributes deposit within the L2 block) is not canonical at another +/// height in the L1 chain, and the same holds for all its ancestors. +pub async fn find_starting_forkchoice( + cfg: &RollupConfig, + engine_client: &EngineClient_, +) -> Result { + let mut current_fc = L2ForkchoiceState::current(cfg, engine_client).await?; + info!( + target: "sync_start", + unsafe = %current_fc.un_safe.block_info.number, + safe = %current_fc.safe.block_info.number, + finalized = %current_fc.finalized.block_info.number, + "Loaded current L2 EL forkchoice state" + ); + + // Search for the highest `unsafe` block, relative to the initial `unsafe` block's L1 origin, + loop { + let l1_origin = + engine_client.get_l1_block(current_fc.un_safe.l1_origin.hash.into()).await?; + info!( + target: "sync_start", + l1_origin = %current_fc.un_safe.l1_origin.number, + l2_unsafe = %current_fc.un_safe.block_info.number, + "Searching for L2 unsafe block with canonical L1 origin" + ); + + match l1_origin { + Some(_) => { + // Unsafe block has existing L1 origin. Continue with this head. + info!( + target: "sync_start", + l2_unsafe = %current_fc.un_safe.block_info.number, + "Found L2 unsafe block with canonical L1 origin" + ); + break; + } + None => { + let l2_parent_hash = current_fc.un_safe.block_info.parent_hash.into(); + let l2_parent = engine_client + .get_l2_block(l2_parent_hash) + .full() + .await? + .ok_or(SyncStartError::BlockNotFound(l2_parent_hash))?; + + current_fc.un_safe = + L2BlockInfo::from_block_and_genesis(&l2_parent.into_consensus(), &cfg.genesis)?; + } + } + } + + // Search for the highest `safe` block that's L1 origin is at least older than the sequencing + // window, relative to the L1 origin of the `unsafe` block. + let mut safe_cursor = current_fc.un_safe; + loop { + info!( + target: "sync_start", + l1_origin = %safe_cursor.l1_origin.number, + l2_safe = %safe_cursor.block_info.number, + "Searching for L2 safe block beyond sequencing window" + ); + + let is_behind_sequence_window = + current_fc.un_safe.l1_origin.number.saturating_sub(cfg.seq_window_size) > + safe_cursor.l1_origin.number; + let is_finalized = safe_cursor.block_info.hash == current_fc.finalized.block_info.hash; + let is_genesis = safe_cursor.block_info.hash == cfg.genesis.l2.hash; + if is_behind_sequence_window || is_finalized || is_genesis { + info!( + target: "sync_start", + l2_safe = %safe_cursor.block_info.number, + is_behind_sequence_window, + is_finalized, + is_genesis, + "Found suitable L2 safe block" + ); + current_fc.safe = safe_cursor; + break; + } else { + let block = engine_client + .get_l2_block(safe_cursor.block_info.parent_hash.into()) + .full() + .await? + .ok_or(SyncStartError::BlockNotFound(safe_cursor.block_info.parent_hash.into()))?; + safe_cursor = + L2BlockInfo::from_block_and_genesis(&block.into_consensus(), &cfg.genesis)?; + } + } + + // Leave the finalized block as-is, and return the current forkchoice. + Ok(current_fc) +} + +#[cfg(test)] +mod test { + use alloy_provider::Network; + use alloy_rpc_types_eth::Block; + use kona_protocol::L2BlockInfo; + use kona_registry::ROLLUP_CONFIGS; + use op_alloy_network::Optimism; + + const OP_SEPOLIA_CHAIN_ID: u64 = 11155420; + const OP_SEPOLIA_GENESIS_RPC_RESPONSE: &str = "{\"hash\":\"0x102de6ffb001480cc9b8b548fd05c34cd4f46ae4aa91759393db90ea0409887d\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"sha3Uncles\":\"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\"miner\":\"0x4200000000000000000000000000000000000011\",\"stateRoot\":\"0x06787a17a3ed87c339a39dbbeeb311578a0c83ed29daa2db95da62b28efce8a9\",\"transactionsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"receiptsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"difficulty\":\"0x0\",\"number\":\"0x0\",\"gasLimit\":\"0x1c9c380\",\"gasUsed\":\"0x0\",\"timestamp\":\"0x64d6dbac\",\"extraData\":\"0x424544524f434b\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"nonce\":\"0x0000000000000000\",\"baseFeePerGas\":\"0x3b9aca00\",\"size\":\"0x209\",\"uncles\":[],\"transactions\":[]}"; + + /// Sanity regression test - `alloy_rpc_types`' `Block::into_consensus` failed to saturate the + /// header of the `alloy_consensus::Header` type on an old version. This test covers the + /// conversion to ensure an OP genesis block's conversion to the consensus type works for + /// the sake of `L2BlockInfo::from_block_and_genesis`. + #[tokio::test] + async fn test_genesis_block_hash() { + let rollup_config = ROLLUP_CONFIGS.get(&OP_SEPOLIA_CHAIN_ID).unwrap(); + let genesis_block: Block<::TransactionResponse> = + serde_json::from_str(OP_SEPOLIA_GENESIS_RPC_RESPONSE).unwrap(); + + let rpc_reported_hash = genesis_block.header.hash; + let consensus_block = genesis_block.into_consensus(); + + // Check that the genesis block's RPC-reported hash is equal to the manually computed hash. + assert_eq!(rpc_reported_hash, consensus_block.hash_slow()); + + // Convert to `L2BlockInfo` and check the same. + let l2_block_info = + L2BlockInfo::from_block_and_genesis(&consensus_block, &rollup_config.genesis).unwrap(); + assert_eq!(rpc_reported_hash, l2_block_info.block_info.hash); + } +} diff --git a/kona/crates/node/engine/src/task_queue/core.rs b/kona/crates/node/engine/src/task_queue/core.rs new file mode 100644 index 0000000000000..50d8f910bb678 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/core.rs @@ -0,0 +1,182 @@ +//! The [`Engine`] is a task queue that receives and executes [`EngineTask`]s. + +use super::EngineTaskExt; +use crate::{ + EngineClient, EngineState, EngineSyncStateUpdate, EngineTask, EngineTaskError, + EngineTaskErrorSeverity, Metrics, SyncStartError, SynchronizeTask, SynchronizeTaskError, + find_starting_forkchoice, task_queue::EngineTaskErrors, +}; +use alloy_rpc_types_eth::Transaction; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BlockInfo, L2BlockInfo, OpBlockConversionError, to_system_config}; +use op_alloy_consensus::OpTxEnvelope; +use std::{collections::BinaryHeap, sync::Arc}; +use thiserror::Error; +use tokio::sync::watch::Sender; + +/// The [`Engine`] task queue. +/// +/// Tasks of a shared [`EngineTask`] variant are processed in FIFO order, providing synchronization +/// guarantees for the L2 execution layer and other actors. A priority queue, ordered by +/// [`EngineTask`]'s [`Ord`] implementation, is used to prioritize tasks executed by the +/// [`Engine::drain`] method. +/// +/// Because tasks are executed one at a time, they are considered to be atomic operations over the +/// [`EngineState`], and are given exclusive access to the engine state during execution. +/// +/// Tasks within the queue are also considered fallible. If they fail with a temporary error, +/// they are not popped from the queue, the error is returned, and they are retried on the +/// next call to [`Engine::drain`]. +#[derive(Debug)] +pub struct Engine { + /// The state of the engine. + state: EngineState, + /// A sender that can be used to notify the engine actor of state changes. + state_sender: Sender, + /// A sender that can be used to notify the engine actor of task queue length changes. + task_queue_length: Sender, + /// The task queue. + tasks: BinaryHeap>, +} + +impl Engine { + /// Creates a new [`Engine`] with an empty task queue and the passed initial [`EngineState`]. + pub fn new( + initial_state: EngineState, + state_sender: Sender, + task_queue_length: Sender, + ) -> Self { + Self { state: initial_state, state_sender, task_queue_length, tasks: BinaryHeap::default() } + } + + /// Returns a reference to the inner [`EngineState`]. + pub const fn state(&self) -> &EngineState { + &self.state + } + + /// Returns a receiver that can be used to listen to engine state updates. + pub fn state_subscribe(&self) -> tokio::sync::watch::Receiver { + self.state_sender.subscribe() + } + + /// Returns a receiver that can be used to listen to engine queue length updates. + pub fn queue_length_subscribe(&self) -> tokio::sync::watch::Receiver { + self.task_queue_length.subscribe() + } + + /// Enqueues a new [`EngineTask`] for execution. + /// Updates the queue length and notifies listeners of the change. + pub fn enqueue(&mut self, task: EngineTask) { + self.tasks.push(task); + self.task_queue_length.send_replace(self.tasks.len()); + } + + /// Resets the engine by finding a plausible sync starting point via + /// [`find_starting_forkchoice`]. The state will be updated to the starting point, and a + /// forkchoice update will be enqueued in order to reorg the execution layer. + pub async fn reset( + &mut self, + client: Arc, + config: Arc, + ) -> Result<(L2BlockInfo, BlockInfo, SystemConfig), EngineResetError> { + // Clear any outstanding tasks to prepare for the reset. + self.clear(); + + let mut start = find_starting_forkchoice(&config, client.as_ref()).await?; + + // Retry to synchronize the engine until we succeeds or a critical error occurs. + while let Err(err) = SynchronizeTask::new( + client.clone(), + config.clone(), + EngineSyncStateUpdate { + unsafe_head: Some(start.un_safe), + cross_unsafe_head: Some(start.un_safe), + local_safe_head: Some(start.safe), + safe_head: Some(start.safe), + finalized_head: Some(start.finalized), + }, + ) + .execute(&mut self.state) + .await + { + match err.severity() { + EngineTaskErrorSeverity::Temporary | + EngineTaskErrorSeverity::Flush | + EngineTaskErrorSeverity::Reset => { + warn!(target: "engine", ?err, "Forkchoice update failed during reset. Trying again..."); + start = find_starting_forkchoice(&config, client.as_ref()).await?; + } + EngineTaskErrorSeverity::Critical => { + return Err(EngineResetError::Forkchoice(err)); + } + } + } + + // Find the new safe head's L1 origin and SystemConfig. + let origin_block = start + .safe + .l1_origin + .number + .saturating_sub(config.channel_timeout(start.safe.block_info.timestamp)); + let l1_origin_info: BlockInfo = client + .get_l1_block(origin_block.into()) + .await + .map_err(SyncStartError::RpcError)? + .ok_or(SyncStartError::BlockNotFound(origin_block.into()))? + .into_consensus() + .into(); + let l2_safe_block = client + .get_l2_block(start.safe.block_info.hash.into()) + .full() + .await + .map_err(SyncStartError::RpcError)? + .ok_or(SyncStartError::BlockNotFound(origin_block.into()))? + .into_consensus() + .map_transactions(|t| as Clone>::clone(&t).into_inner()); + let system_config = to_system_config(&l2_safe_block, &config)?; + + kona_macros::inc!(counter, Metrics::ENGINE_RESET_COUNT); + + Ok((start.safe, l1_origin_info, system_config)) + } + + /// Clears the task queue. + pub fn clear(&mut self) { + self.tasks.clear(); + } + + /// Attempts to drain the queue by executing all [`EngineTask`]s in-order. If any task returns + /// an error along the way, it is not popped from the queue (in case it must be retried) and + /// the error is returned. + pub async fn drain(&mut self) -> Result<(), EngineTaskErrors> { + // Drain tasks in order of priority, halting on errors for a retry to be attempted. + while let Some(task) = self.tasks.peek() { + // Execute the task + task.execute(&mut self.state).await?; + + // Update the state and notify the engine actor. + self.state_sender.send_replace(self.state); + + // Pop the task from the queue now that it's been executed. + self.tasks.pop(); + + self.task_queue_length.send_replace(self.tasks.len()); + } + + Ok(()) + } +} + +/// An error occurred while attempting to reset the [`Engine`]. +#[derive(Debug, Error)] +pub enum EngineResetError { + /// An error that occurred while updating the forkchoice state. + #[error(transparent)] + Forkchoice(#[from] SynchronizeTaskError), + /// An error occurred while traversing the L1 for the sync starting point. + #[error(transparent)] + SyncStart(#[from] SyncStartError), + /// An error occurred while constructing the SystemConfig for the new safe head. + #[error(transparent)] + SystemConfigConversion(#[from] OpBlockConversionError), +} diff --git a/kona/crates/node/engine/src/task_queue/mod.rs b/kona/crates/node/engine/src/task_queue/mod.rs new file mode 100644 index 0000000000000..57fd6ee99073f --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/mod.rs @@ -0,0 +1,7 @@ +//! The [`Engine`] task queue and the [`EngineTask`]s it can execute. + +mod core; +pub use core::{Engine, EngineResetError}; + +mod tasks; +pub use tasks::*; diff --git a/kona/crates/node/engine/src/task_queue/tasks/build/error.rs b/kona/crates/node/engine/src/task_queue/tasks/build/error.rs new file mode 100644 index 0000000000000..dc1c634b45fcb --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/build/error.rs @@ -0,0 +1,80 @@ +//! Contains error types for the [crate::SynchronizeTask]. + +use crate::{EngineTaskError, task_queue::tasks::task::EngineTaskErrorSeverity}; +use alloy_rpc_types_engine::{PayloadId, PayloadStatusEnum}; +use alloy_transport::{RpcError, TransportErrorKind}; +use thiserror::Error; +use tokio::sync::mpsc; + +/// An error that occurs during payload building within the engine. +/// +/// This error type is specific to the block building process and represents failures +/// that can occur during the automatic forkchoice update phase of [`BuildTask`]. +/// Unlike [`BuildTaskError`], which handles higher-level build orchestration errors, +/// `EngineBuildError` focuses on low-level engine API communication failures. +/// +/// ## Error Categories +/// +/// - **State Validation**: Errors related to inconsistent chain state +/// - **Engine Communication**: RPC failures during forkchoice updates +/// - **Payload Validation**: Invalid payload status responses from the execution layer +/// +/// [`BuildTask`]: crate::BuildTask +#[derive(Debug, Error)] +pub enum EngineBuildError { + /// The finalized head is ahead of the unsafe head. + #[error("Finalized head is ahead of unsafe head")] + FinalizedAheadOfUnsafe(u64, u64), + /// The forkchoice update call to the engine api failed. + #[error("Failed to build payload attributes in the engine. Forkchoice RPC error: {0}")] + AttributesInsertionFailed(#[from] RpcError), + /// The inserted payload is invalid. + #[error("The inserted payload is invalid: {0}")] + InvalidPayload(String), + /// The inserted payload status is unexpected. + #[error("The inserted payload status is unexpected: {0}")] + UnexpectedPayloadStatus(PayloadStatusEnum), + /// The payload ID is missing. + #[error("The inserted payload ID is missing")] + MissingPayloadId, + /// The engine is syncing. + #[error("The engine is syncing")] + EngineSyncing, +} + +/// An error that occurs when running the [crate::BuildTask]. +#[derive(Debug, Error)] +pub enum BuildTaskError { + /// An error occurred when building the payload attributes in the engine. + #[error("An error occurred when building the payload attributes to the engine.")] + EngineBuildError(EngineBuildError), + /// Error sending the built payload envelope. + #[error(transparent)] + MpscSend(#[from] Box>), +} + +impl EngineTaskError for BuildTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::EngineBuildError(EngineBuildError::FinalizedAheadOfUnsafe(_, _)) => { + EngineTaskErrorSeverity::Critical + } + Self::EngineBuildError(EngineBuildError::AttributesInsertionFailed(_)) => { + EngineTaskErrorSeverity::Temporary + } + Self::EngineBuildError(EngineBuildError::InvalidPayload(_)) => { + EngineTaskErrorSeverity::Temporary + } + Self::EngineBuildError(EngineBuildError::UnexpectedPayloadStatus(_)) => { + EngineTaskErrorSeverity::Temporary + } + Self::EngineBuildError(EngineBuildError::MissingPayloadId) => { + EngineTaskErrorSeverity::Temporary + } + Self::EngineBuildError(EngineBuildError::EngineSyncing) => { + EngineTaskErrorSeverity::Temporary + } + Self::MpscSend(_) => EngineTaskErrorSeverity::Critical, + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/build/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/build/mod.rs new file mode 100644 index 0000000000000..8b4b322e1ae92 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/build/mod.rs @@ -0,0 +1,10 @@ +//! Task and its associated types for building and importing a new block. + +mod task; +pub use task::BuildTask; + +mod error; +pub use error::{BuildTaskError, EngineBuildError}; + +#[cfg(test)] +mod task_test; diff --git a/kona/crates/node/engine/src/task_queue/tasks/build/task.rs b/kona/crates/node/engine/src/task_queue/tasks/build/task.rs new file mode 100644 index 0000000000000..8fc4ecda7575e --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/build/task.rs @@ -0,0 +1,184 @@ +//! A task for building a new block and importing it. +use super::BuildTaskError; +use crate::{ + EngineClient, EngineForkchoiceVersion, EngineState, EngineTaskExt, + state::EngineSyncStateUpdate, task_queue::tasks::build::error::EngineBuildError, +}; +use alloy_rpc_types_engine::{PayloadId, PayloadStatusEnum}; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_genesis::RollupConfig; +use kona_protocol::OpAttributesWithParent; +use std::{sync::Arc, time::Instant}; +use tokio::sync::mpsc; + +/// Task for building new blocks with automatic forkchoice synchronization. +/// +/// The [`BuildTask`] only performs the `engine_forkchoiceUpdated` call within the block building +/// workflow. It makes this call with the provided attributes to initiate block building on the +/// execution layer and, if successful, sends the new [`PayloadId`] via the configured sender. +/// +/// ## Error Handling +/// +/// The task uses [`EngineBuildError`] for build-specific failures during the forkchoice update +/// phase. +/// +/// [`EngineBuildError`]: crate::EngineBuildError +#[derive(Debug, Clone, Constructor)] +pub struct BuildTask { + /// The engine API client. + pub engine: Arc, + /// The [`RollupConfig`]. + pub cfg: Arc, + /// The [`OpAttributesWithParent`] to instruct the execution layer to build. + pub attributes: OpAttributesWithParent, + /// The optional sender through which [`PayloadId`] will be sent after the + /// block build has been started. + pub payload_id_tx: Option>, +} + +impl BuildTask { + /// Validates the provided [PayloadStatusEnum] according to the rules listed below. + /// + /// ## Observed [PayloadStatusEnum] Variants + /// - `VALID`: Returns Ok(()) - forkchoice update was successful + /// - `INVALID`: Returns error with validation details + /// - `SYNCING`: Returns temporary error - EL is syncing + /// - Other: Returns error for unexpected status codes + fn validate_forkchoice_status(status: PayloadStatusEnum) -> Result<(), BuildTaskError> { + match status { + PayloadStatusEnum::Valid => Ok(()), + PayloadStatusEnum::Invalid { validation_error } => { + error!(target: "engine_builder", "Forkchoice update failed: {}", validation_error); + Err(BuildTaskError::EngineBuildError(EngineBuildError::InvalidPayload( + validation_error, + ))) + } + PayloadStatusEnum::Syncing => { + warn!(target: "engine_builder", "Forkchoice update failed temporarily: EL is syncing"); + Err(BuildTaskError::EngineBuildError(EngineBuildError::EngineSyncing)) + } + PayloadStatusEnum::Accepted => { + // Other codes are never returned by `engine_forkchoiceUpdate` + Err(BuildTaskError::EngineBuildError(EngineBuildError::UnexpectedPayloadStatus( + status, + ))) + } + } + } + + /// Starts the block building process by sending an initial `engine_forkchoiceUpdate` call with + /// the payload attributes to build. + /// + /// ### Success (`VALID`) + /// If the build is successful, the [PayloadId] is returned for sealing and the successful + /// forkchoice update identifier is relayed via the stored `payload_id_tx` sender. + /// + /// ### Failure (`INVALID`) + /// If the forkchoice update fails, the [BuildTaskError]. + /// + /// ### Syncing (`SYNCING`) + /// If the EL is syncing, the payload attributes are buffered and the function returns early. + /// This is a temporary state, and the function should be called again later. + /// + /// Note: This is `pub(super)` to allow testing via the `tests` submodule. + pub(super) async fn start_build( + &self, + state: &EngineState, + engine_client: &EngineClient_, + attributes_envelope: OpAttributesWithParent, + ) -> Result { + // Sanity check if the head is behind the finalized head. If it is, this is a critical + // error. + if state.sync_state.unsafe_head().block_info.number < + state.sync_state.finalized_head().block_info.number + { + return Err(BuildTaskError::EngineBuildError(EngineBuildError::FinalizedAheadOfUnsafe( + state.sync_state.unsafe_head().block_info.number, + state.sync_state.finalized_head().block_info.number, + ))); + } + + // When inserting a payload, we advertise the parent's unsafe head as the current unsafe + // head to build on top of. + let new_forkchoice = state + .sync_state + .apply_update(EngineSyncStateUpdate { + unsafe_head: Some(attributes_envelope.parent), + ..Default::default() + }) + .create_forkchoice_state(); + + let forkchoice_version = EngineForkchoiceVersion::from_cfg( + &self.cfg, + attributes_envelope.attributes.payload_attributes.timestamp, + ); + let update = match forkchoice_version { + EngineForkchoiceVersion::V3 => { + engine_client + .fork_choice_updated_v3(new_forkchoice, Some(attributes_envelope.attributes)) + .await + } + EngineForkchoiceVersion::V2 => { + engine_client + .fork_choice_updated_v2(new_forkchoice, Some(attributes_envelope.attributes)) + .await + } + } + .map_err(|e| { + error!(target: "engine_builder", "Forkchoice update failed: {}", e); + BuildTaskError::EngineBuildError(EngineBuildError::AttributesInsertionFailed(e)) + })?; + + Self::validate_forkchoice_status(update.payload_status.status)?; + + debug!( + target: "engine_builder", + unsafe_hash = new_forkchoice.head_block_hash.to_string(), + safe_hash = new_forkchoice.safe_block_hash.to_string(), + finalized_hash = new_forkchoice.finalized_block_hash.to_string(), + "Forkchoice update with attributes successful" + ); + + // Fetch the payload ID from the FCU. If no payload ID was returned, something went wrong - + // the block building job on the EL should have been initiated. + update + .payload_id + .ok_or(BuildTaskError::EngineBuildError(EngineBuildError::MissingPayloadId)) + } +} + +#[async_trait] +impl EngineTaskExt for BuildTask { + type Output = PayloadId; + + type Error = BuildTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result { + debug!( + target: "engine_builder", + txs = self.attributes.attributes().transactions.as_ref().map_or(0, |txs| txs.len()), + is_deposits = self.attributes.is_deposits_only(), + "Starting new build job" + ); + + // Start the build by sending an FCU call with the current forkchoice and the input + // payload attributes. + let fcu_start_time = Instant::now(); + let payload_id = self.start_build(state, &self.engine, self.attributes.clone()).await?; + let fcu_duration = fcu_start_time.elapsed(); + + info!( + target: "engine_builder", + fcu_duration = ?fcu_duration, + "block build started" + ); + + // If a channel was provided, send the payload ID to it. + if let Some(tx) = &self.payload_id_tx { + tx.send(payload_id).await.map_err(Box::new)?; + } + + Ok(payload_id) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/build/task_test.rs b/kona/crates/node/engine/src/task_queue/tasks/build/task_test.rs new file mode 100644 index 0000000000000..f7af1b11af915 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/build/task_test.rs @@ -0,0 +1,167 @@ +//! Tests for BuildTask::execute + +use crate::{ + BuildTask, BuildTaskError, EngineBuildError, EngineClient, EngineForkchoiceVersion, + EngineState, EngineTaskExt, + test_utils::{ + MockEngineClientBuilder, TestAttributesBuilder, TestEngineStateBuilder, test_block_info, + test_engine_client_builder, + }, +}; +use alloy_primitives::FixedBytes; +use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadId, PayloadStatus, PayloadStatusEnum}; +use kona_genesis::RollupConfig; +use rstest::rstest; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::mpsc; + +fn fcu_for_payload(payload_id: Option, status: PayloadStatusEnum) -> ForkchoiceUpdated { + ForkchoiceUpdated { + payload_status: PayloadStatus { status, latest_valid_hash: Some(FixedBytes([2u8; 32])) }, + payload_id, + } +} + +fn configure_fcu( + b: MockEngineClientBuilder, + fcu_version: EngineForkchoiceVersion, + fcu_response: ForkchoiceUpdated, + cfg: &mut RollupConfig, + attributes_timestamp: u64, +) -> MockEngineClientBuilder { + match fcu_version { + EngineForkchoiceVersion::V2 => { + // Ecotone not yet active + cfg.hardforks.ecotone_time = Some(attributes_timestamp + 1); + b.with_fork_choice_updated_v2_response(fcu_response) + } + EngineForkchoiceVersion::V3 => { + // Ecotone is active + cfg.hardforks.ecotone_time = Some(attributes_timestamp); + b.with_fork_choice_updated_v3_response(fcu_response) + } + } +} + +#[derive(Debug, Error, PartialEq, Eq)] +enum TestErr { + #[error("AttributesInsertionFailed.")] + AttributesInsertionFailed, + #[error("EngineSyncing.")] + EngineSyncing, + #[error("FinalizedAheadOfUnsafe.")] + FinalizedAheadOfUnsafe, + #[error("InvalidPayload.")] + InvalidPayload, + #[error("MissingPayloadId.")] + MissingPayloadId, + #[error("UnexpectedPayloadStatus.")] + Unexpected, + #[error("MpscSend.")] + MpscSend, +} + +// Wraps real errors, ignoring details so we can easily match on results. +async fn wrapped_execute( + task: &mut BuildTask, + state: &mut EngineState, +) -> Result { + match task.execute(state).await { + Ok(payload_id) => Ok(payload_id), + Err(BuildTaskError::EngineBuildError(e)) => match e { + EngineBuildError::AttributesInsertionFailed(_) => { + Err(TestErr::AttributesInsertionFailed) + } + EngineBuildError::EngineSyncing => Err(TestErr::EngineSyncing), + EngineBuildError::FinalizedAheadOfUnsafe(_, _) => Err(TestErr::FinalizedAheadOfUnsafe), + EngineBuildError::InvalidPayload(_) => Err(TestErr::InvalidPayload), + EngineBuildError::MissingPayloadId => Err(TestErr::MissingPayloadId), + EngineBuildError::UnexpectedPayloadStatus(_) => Err(TestErr::Unexpected), + }, + Err(BuildTaskError::MpscSend(_)) => Err(TestErr::MpscSend), + } +} + +#[rstest] +#[case::success(Some(PayloadStatusEnum::Valid), true, None)] +#[case::missing_id(Some(PayloadStatusEnum::Valid), false, Some(TestErr::MissingPayloadId))] +#[case::fcu_fail(None, false, Some(TestErr::AttributesInsertionFailed))] +#[case::fcu_status_fail(Some(PayloadStatusEnum::Invalid{validation_error: "".to_string()}), false, Some(TestErr::InvalidPayload))] +#[case::fcu_status_fail(Some(PayloadStatusEnum::Syncing), false, Some(TestErr::EngineSyncing))] +#[case::fcu_status_fail(Some(PayloadStatusEnum::Accepted), false, Some(TestErr::Unexpected))] +#[tokio::test] +async fn test_execute_variants( + // NB: none = failure + #[case] fcu_status: Option, + // NB: none = failure + #[case] payload_id_present: bool, + // NB: none = success + #[case] expected_err: Option, + #[values(true, false)] with_channel: bool, + #[values(EngineForkchoiceVersion::V2, EngineForkchoiceVersion::V3)] + fcu_version: EngineForkchoiceVersion, +) { + let payload_id = if payload_id_present { Some(PayloadId::new([1u8; 8])) } else { None }; + + let parent_block = test_block_info(0); + let unsafe_block = test_block_info(1); + let attributes_timestamp = unsafe_block.block_info.timestamp; + + let mut cfg = RollupConfig::default(); + + // Configure client with FCU response. If none, it will err on call, which is also a test case. + let engine_client = fcu_status + .map_or(test_engine_client_builder(), |status| { + configure_fcu( + test_engine_client_builder(), + fcu_version, + fcu_for_payload(payload_id, status), + &mut cfg, + attributes_timestamp, + ) + }) + .build(); + + let attributes = TestAttributesBuilder::new() + .with_parent(parent_block) + .with_timestamp(attributes_timestamp) + .build(); + + let (tx, mut rx) = mpsc::channel(1); + + let mut task = BuildTask::new( + Arc::new(engine_client.clone()), + Arc::new(cfg), + attributes.clone(), + if with_channel { Some(tx) } else { None }, + ); + + let mut state = TestEngineStateBuilder::new() + .with_unsafe_head(unsafe_block) + .with_safe_head(parent_block) + .with_finalized_head(parent_block) + .build(); + + // Execute: Call execute + let result = wrapped_execute(&mut task, &mut state).await; + + if expected_err.is_some() { + assert_eq!(expected_err, result.err()); + } else { + assert!(result.is_ok()); + assert!(payload_id.is_some(), "Payload id none when it should be some."); + assert_eq!(result.unwrap(), payload_id.unwrap(), "Should return the correct payload ID"); + + // test channel payload send + if task.payload_id_tx.is_some() { + let res = rx.recv().await; + assert!(res.is_some(), "channel result is None"); + assert_eq!( + res.unwrap(), + payload_id.unwrap(), + "channel should have received correct payload id" + ); + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/consolidate/error.rs b/kona/crates/node/engine/src/task_queue/tasks/consolidate/error.rs new file mode 100644 index 0000000000000..b4210ce22abe1 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/consolidate/error.rs @@ -0,0 +1,48 @@ +//! Contains error types for the [`crate::ConsolidateTask`]. + +use crate::{ + BuildTaskError, EngineTaskError, SealTaskError, SynchronizeTaskError, + task_queue::tasks::{BuildAndSealError, task::EngineTaskErrorSeverity}, +}; +use thiserror::Error; + +/// An error that occurs when running the [`crate::ConsolidateTask`]. +#[derive(Debug, Error)] +pub enum ConsolidateTaskError { + /// The unsafe L2 block is missing. + #[error("Unsafe L2 block is missing {0}")] + MissingUnsafeL2Block(u64), + /// Failed to fetch the unsafe L2 block. + #[error("Failed to fetch the unsafe L2 block")] + FailedToFetchUnsafeL2Block, + /// The build task failed. + #[error(transparent)] + BuildTaskFailed(#[from] BuildTaskError), + /// The seal task failed. + #[error(transparent)] + SealTaskFailed(#[from] SealTaskError), + /// The consolidation forkchoice update call to the engine api failed. + #[error(transparent)] + ForkchoiceUpdateFailed(#[from] SynchronizeTaskError), +} + +impl From for ConsolidateTaskError { + fn from(err: BuildAndSealError) -> Self { + match err { + BuildAndSealError::Build(e) => Self::BuildTaskFailed(e), + BuildAndSealError::Seal(e) => Self::SealTaskFailed(e), + } + } +} + +impl EngineTaskError for ConsolidateTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::MissingUnsafeL2Block(_) => EngineTaskErrorSeverity::Reset, + Self::FailedToFetchUnsafeL2Block => EngineTaskErrorSeverity::Temporary, + Self::BuildTaskFailed(inner) => inner.severity(), + Self::SealTaskFailed(inner) => inner.severity(), + Self::ForkchoiceUpdateFailed(inner) => inner.severity(), + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/consolidate/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/consolidate/mod.rs new file mode 100644 index 0000000000000..2beff9e61004a --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/consolidate/mod.rs @@ -0,0 +1,7 @@ +//! Consolidation Task + +mod error; +pub use error::ConsolidateTaskError; + +mod task; +pub use task::ConsolidateTask; diff --git a/kona/crates/node/engine/src/task_queue/tasks/consolidate/task.rs b/kona/crates/node/engine/src/task_queue/tasks/consolidate/task.rs new file mode 100644 index 0000000000000..8d7ff6a30f239 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/consolidate/task.rs @@ -0,0 +1,173 @@ +//! A task to consolidate the engine state. + +use crate::{ + ConsolidateTaskError, EngineClient, EngineState, EngineTaskExt, SynchronizeTask, + state::EngineSyncStateUpdate, task_queue::build_and_seal, +}; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_genesis::RollupConfig; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; +use std::{sync::Arc, time::Instant}; + +/// The [`ConsolidateTask`] attempts to consolidate the engine state +/// using the specified payload attributes and the oldest unsafe head. +/// +/// If consolidation fails, payload attributes processing is attempted using `build_and_seal`. +#[derive(Debug, Clone, Constructor)] +pub struct ConsolidateTask { + /// The engine client. + pub client: Arc, + /// The [`RollupConfig`]. + pub cfg: Arc, + /// The [`OpAttributesWithParent`] to instruct the execution layer to build. + pub attributes: OpAttributesWithParent, + /// Whether or not the payload was derived, or created by the sequencer. + pub is_attributes_derived: bool, +} + +impl ConsolidateTask { + /// This is used when the [`ConsolidateTask`] fails to consolidate the engine state. + async fn execute_build_and_seal_tasks( + &self, + state: &mut EngineState, + ) -> Result<(), ConsolidateTaskError> { + build_and_seal( + state, + self.client.clone(), + self.cfg.clone(), + self.attributes.clone(), + self.is_attributes_derived, + ) + .await?; + + Ok(()) + } + + /// Attempts consolidation on the engine state. + pub async fn consolidate(&self, state: &mut EngineState) -> Result<(), ConsolidateTaskError> { + let global_start = Instant::now(); + + // Fetch the unsafe l2 block after the attributes parent. + let block_num = self.attributes.block_number(); + let fetch_start = Instant::now(); + let block = match self.client.l2_block_by_label(block_num.into()).await { + Ok(Some(block)) => block, + Ok(None) => { + warn!(target: "engine", "Received `None` block for {}", block_num); + return Err(ConsolidateTaskError::MissingUnsafeL2Block(block_num)); + } + Err(_) => { + warn!(target: "engine", "Failed to fetch unsafe l2 block for consolidation"); + return Err(ConsolidateTaskError::FailedToFetchUnsafeL2Block); + } + }; + let block_fetch_duration = fetch_start.elapsed(); + + // Attempt to consolidate the unsafe head. + // If this is successful, the forkchoice change synchronizes. + // Otherwise, the attributes need to be processed. + let block_hash = block.header.hash; + if crate::AttributesMatch::check(&self.cfg, &self.attributes, &block).is_match() { + trace!( + target: "engine", + attributes = ?self.attributes, + block_hash = %block_hash, + "Consolidating engine state", + ); + + match L2BlockInfo::from_block_and_genesis(&block.into_consensus(), &self.cfg.genesis) { + // Only issue a forkchoice update if the attributes are the last in the span + // batch. This is an optimization to avoid sending a FCU + // call for every block in the span batch. + Ok(block_info) if !self.attributes.is_last_in_span => { + let total_duration = global_start.elapsed(); + + // Apply a transient update to the safe head. + state.sync_state = state.sync_state.apply_update(EngineSyncStateUpdate { + safe_head: Some(block_info), + local_safe_head: Some(block_info), + ..Default::default() + }); + + info!( + target: "engine", + hash = %block_info.block_info.hash, + number = block_info.block_info.number, + ?total_duration, + ?block_fetch_duration, + "Updated safe head via L1 consolidation" + ); + + return Ok(()); + } + Ok(block_info) => { + let fcu_start = Instant::now(); + + SynchronizeTask::new( + Arc::clone(&self.client), + self.cfg.clone(), + EngineSyncStateUpdate { + safe_head: Some(block_info), + local_safe_head: Some(block_info), + ..Default::default() + }, + ) + .execute(state) + .await + .map_err(|e| { + warn!(target: "engine", ?e, "Consolidation failed"); + e + })?; + + let fcu_duration = fcu_start.elapsed(); + + let total_duration = global_start.elapsed(); + + info!( + target: "engine", + hash = %block_info.block_info.hash, + number = block_info.block_info.number, + ?total_duration, + ?block_fetch_duration, + fcu_duration = ?fcu_duration, + "Updated safe head via L1 consolidation" + ); + + return Ok(()); + } + Err(e) => { + // Continue on to build the block since we failed to construct the block info. + warn!(target: "engine", ?e, "Failed to construct L2BlockInfo, proceeding to build task"); + } + } + } + + // Otherwise, the attributes need to be processed. + debug!( + target: "engine", + attributes = ?self.attributes, + block_hash = %block_hash, + "Attributes mismatch! Executing build task to initiate reorg", + ); + self.execute_build_and_seal_tasks(state).await + } +} + +#[async_trait] +impl EngineTaskExt for ConsolidateTask { + type Output = (); + + type Error = ConsolidateTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result<(), ConsolidateTaskError> { + // Skip to building the payload attributes if consolidation is not needed. + if state.sync_state.safe_head().block_info.number < + state.sync_state.unsafe_head().block_info.number + { + self.consolidate(state).await + } else { + self.execute_build_and_seal_tasks(state).await + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/finalize/error.rs b/kona/crates/node/engine/src/task_queue/tasks/finalize/error.rs new file mode 100644 index 0000000000000..60a11b4da96e4 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/finalize/error.rs @@ -0,0 +1,42 @@ +//! Contains error types for the [crate::FinalizeTask]. + +use crate::{ + EngineTaskError, SynchronizeTaskError, task_queue::tasks::task::EngineTaskErrorSeverity, +}; +use alloy_transport::{RpcError, TransportErrorKind}; +use kona_protocol::FromBlockError; +use thiserror::Error; + +/// An error that occurs when running the [crate::FinalizeTask]. +#[derive(Debug, Error)] +pub enum FinalizeTaskError { + /// The block is not safe, and therefore cannot be finalized. + #[error("Attempted to finalize a block that is not yet safe")] + BlockNotSafe, + /// The block to finalize was not found. + #[error("The block to finalize was not found: Number {0}")] + BlockNotFound(u64), + /// An error occurred while transforming the RPC block into [`L2BlockInfo`]. + /// + /// [`L2BlockInfo`]: kona_protocol::L2BlockInfo + #[error(transparent)] + FromBlock(#[from] FromBlockError), + /// A temporary RPC failure. + #[error(transparent)] + TransportError(#[from] RpcError), + /// The forkchoice update call to finalize the block failed. + #[error(transparent)] + ForkchoiceUpdateFailed(#[from] SynchronizeTaskError), +} + +impl EngineTaskError for FinalizeTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::BlockNotSafe => EngineTaskErrorSeverity::Critical, + Self::BlockNotFound(_) => EngineTaskErrorSeverity::Critical, + Self::FromBlock(_) => EngineTaskErrorSeverity::Critical, + Self::TransportError(_) => EngineTaskErrorSeverity::Temporary, + Self::ForkchoiceUpdateFailed(inner) => inner.severity(), + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/finalize/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/finalize/mod.rs new file mode 100644 index 0000000000000..dccec06202517 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/finalize/mod.rs @@ -0,0 +1,7 @@ +//! Task and its associated types for finalizing an L2 block. + +mod task; +pub use task::FinalizeTask; + +mod error; +pub use error::FinalizeTaskError; diff --git a/kona/crates/node/engine/src/task_queue/tasks/finalize/task.rs b/kona/crates/node/engine/src/task_queue/tasks/finalize/task.rs new file mode 100644 index 0000000000000..4b26aeb7b1f23 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/finalize/task.rs @@ -0,0 +1,72 @@ +//! A task for finalizing an L2 block. + +use crate::{ + EngineClient, EngineState, EngineTaskExt, FinalizeTaskError, SynchronizeTask, + state::EngineSyncStateUpdate, +}; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; +use std::{sync::Arc, time::Instant}; + +/// The [`FinalizeTask`] fetches the [`L2BlockInfo`] at `block_number`, updates the [`EngineState`], +/// and dispatches a forkchoice update to finalize the block. +#[derive(Debug, Clone, Constructor)] +pub struct FinalizeTask { + /// The engine client. + pub client: Arc, + /// The rollup config. + pub cfg: Arc, + /// The number of the L2 block to finalize. + pub block_number: u64, +} + +#[async_trait] +impl EngineTaskExt for FinalizeTask { + type Output = (); + + type Error = FinalizeTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result<(), FinalizeTaskError> { + // Sanity check that the block that is being finalized is at least safe. + if state.sync_state.safe_head().block_info.number < self.block_number { + return Err(FinalizeTaskError::BlockNotSafe); + } + + let block_fetch_start = Instant::now(); + let block = self + .client + .get_l2_block(self.block_number.into()) + .full() + .await + .map_err(FinalizeTaskError::TransportError)? + .ok_or(FinalizeTaskError::BlockNotFound(self.block_number))? + .into_consensus(); + let block_info = L2BlockInfo::from_block_and_genesis(&block, &self.client.cfg().genesis) + .map_err(FinalizeTaskError::FromBlock)?; + let block_fetch_duration = block_fetch_start.elapsed(); + + // Dispatch a forkchoice update. + let fcu_start = Instant::now(); + SynchronizeTask::new( + self.client.clone(), + self.cfg.clone(), + EngineSyncStateUpdate { finalized_head: Some(block_info), ..Default::default() }, + ) + .execute(state) + .await?; + let fcu_duration = fcu_start.elapsed(); + + info!( + target: "engine", + hash = %block_info.block_info.hash, + number = block_info.block_info.number, + ?block_fetch_duration, + ?fcu_duration, + "Updated finalized head" + ); + + Ok(()) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/insert/error.rs b/kona/crates/node/engine/src/task_queue/tasks/insert/error.rs new file mode 100644 index 0000000000000..8d0cfc021640a --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/insert/error.rs @@ -0,0 +1,45 @@ +//! Contains the error types for the [InsertTask]. +//! +//! [InsertTask]: crate::InsertTask + +use crate::{ + EngineTaskError, SynchronizeTaskError, task_queue::tasks::task::EngineTaskErrorSeverity, +}; +use alloy_rpc_types_engine::PayloadStatusEnum; +use alloy_transport::{RpcError, TransportErrorKind}; +use kona_protocol::FromBlockError; +use op_alloy_rpc_types_engine::OpPayloadError; + +/// An error that occurs when running the [InsertTask]. +/// +/// [InsertTask]: crate::InsertTask +#[derive(Debug, thiserror::Error)] +pub enum InsertTaskError { + /// Error converting a payload into a block. + #[error(transparent)] + FromBlockError(#[from] OpPayloadError), + /// Failed to insert new payload. + #[error("Failed to insert new payload: {0}")] + InsertFailed(RpcError), + /// Unexpected payload status + #[error("Unexpected payload status: {0}")] + UnexpectedPayloadStatus(PayloadStatusEnum), + /// Error converting the payload + chain genesis into an L2 block info. + #[error(transparent)] + L2BlockInfoConstruction(#[from] FromBlockError), + /// The forkchoice update call to consolidate the block into the engine state failed. + #[error(transparent)] + ForkchoiceUpdateFailed(#[from] SynchronizeTaskError), +} + +impl EngineTaskError for InsertTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::FromBlockError(_) => EngineTaskErrorSeverity::Critical, + Self::InsertFailed(_) => EngineTaskErrorSeverity::Temporary, + Self::UnexpectedPayloadStatus(_) => EngineTaskErrorSeverity::Temporary, + Self::L2BlockInfoConstruction(_) => EngineTaskErrorSeverity::Critical, + Self::ForkchoiceUpdateFailed(inner) => inner.severity(), + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/insert/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/insert/mod.rs new file mode 100644 index 0000000000000..397e2e5513150 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/insert/mod.rs @@ -0,0 +1,7 @@ +//! Task to insert an unsafe payload into the execution engine. + +mod task; +pub use task::InsertTask; + +mod error; +pub use error::InsertTaskError; diff --git a/kona/crates/node/engine/src/task_queue/tasks/insert/task.rs b/kona/crates/node/engine/src/task_queue/tasks/insert/task.rs new file mode 100644 index 0000000000000..6b9aeea29952f --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/insert/task.rs @@ -0,0 +1,155 @@ +//! A task to insert an unsafe payload into the execution engine. + +use crate::{ + EngineClient, EngineState, EngineTaskExt, InsertTaskError, SynchronizeTask, + state::EngineSyncStateUpdate, +}; +use alloy_eips::eip7685::EMPTY_REQUESTS_HASH; +use alloy_rpc_types_engine::{ + CancunPayloadFields, ExecutionPayloadInputV2, PayloadStatusEnum, PraguePayloadFields, +}; +use async_trait::async_trait; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; +use op_alloy_consensus::OpBlock; +use op_alloy_rpc_types_engine::{ + OpExecutionPayload, OpExecutionPayloadEnvelope, OpExecutionPayloadSidecar, +}; +use std::{sync::Arc, time::Instant}; + +/// The task to insert a payload into the execution engine. +#[derive(Debug, Clone)] +pub struct InsertTask { + /// The engine client. + client: Arc, + /// The rollup config. + rollup_config: Arc, + /// The network payload envelope. + envelope: OpExecutionPayloadEnvelope, + /// If the payload is safe this is true. + /// A payload is safe if it is derived from a safe block. + is_payload_safe: bool, +} + +impl InsertTask { + /// Creates a new insert task. + pub const fn new( + client: Arc, + rollup_config: Arc, + envelope: OpExecutionPayloadEnvelope, + is_attributes_derived: bool, + ) -> Self { + Self { client, rollup_config, envelope, is_payload_safe: is_attributes_derived } + } + + /// Checks the response of the `engine_newPayload` call. + const fn check_new_payload_status(&self, status: &PayloadStatusEnum) -> bool { + matches!(status, PayloadStatusEnum::Valid | PayloadStatusEnum::Syncing) + } +} + +#[async_trait] +impl EngineTaskExt for InsertTask { + type Output = (); + + type Error = InsertTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result<(), InsertTaskError> { + let time_start = Instant::now(); + + // Insert the new payload. + // Form the new unsafe block ref from the execution payload. + let parent_beacon_block_root = self.envelope.parent_beacon_block_root.unwrap_or_default(); + let insert_time_start = Instant::now(); + let (response, block): (_, OpBlock) = match self.envelope.execution_payload.clone() { + OpExecutionPayload::V1(payload) => ( + self.client.new_payload_v1(payload).await, + self.envelope + .execution_payload + .clone() + .try_into_block() + .map_err(InsertTaskError::FromBlockError)?, + ), + OpExecutionPayload::V2(payload) => { + let payload_input = ExecutionPayloadInputV2 { + execution_payload: payload.payload_inner, + withdrawals: Some(payload.withdrawals), + }; + ( + self.client.new_payload_v2(payload_input).await, + self.envelope + .execution_payload + .clone() + .try_into_block() + .map_err(InsertTaskError::FromBlockError)?, + ) + } + OpExecutionPayload::V3(payload) => ( + self.client.new_payload_v3(payload, parent_beacon_block_root).await, + self.envelope + .execution_payload + .clone() + .try_into_block_with_sidecar(&OpExecutionPayloadSidecar::v3( + CancunPayloadFields::new(parent_beacon_block_root, vec![]), + )) + .map_err(InsertTaskError::FromBlockError)?, + ), + OpExecutionPayload::V4(payload) => ( + self.client.new_payload_v4(payload, parent_beacon_block_root).await, + self.envelope + .execution_payload + .clone() + .try_into_block_with_sidecar(&OpExecutionPayloadSidecar::v4( + CancunPayloadFields::new(parent_beacon_block_root, vec![]), + PraguePayloadFields::new(EMPTY_REQUESTS_HASH), + )) + .map_err(InsertTaskError::FromBlockError)?, + ), + }; + + // Check the `engine_newPayload` response. + let response = match response { + Ok(resp) => resp, + Err(e) => { + warn!(target: "engine", "Failed to insert new payload: {e}"); + return Err(InsertTaskError::InsertFailed(e)); + } + }; + if !self.check_new_payload_status(&response.status) { + return Err(InsertTaskError::UnexpectedPayloadStatus(response.status)); + } + let insert_duration = insert_time_start.elapsed(); + + let new_unsafe_ref = + L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) + .map_err(InsertTaskError::L2BlockInfoConstruction)?; + + // Send a FCU to canonicalize the imported block. + SynchronizeTask::new( + Arc::clone(&self.client), + self.rollup_config.clone(), + EngineSyncStateUpdate { + cross_unsafe_head: Some(new_unsafe_ref), + unsafe_head: Some(new_unsafe_ref), + local_safe_head: self.is_payload_safe.then_some(new_unsafe_ref), + safe_head: self.is_payload_safe.then_some(new_unsafe_ref), + ..Default::default() + }, + ) + .execute(state) + .await?; + + let total_duration = time_start.elapsed(); + + info!( + target: "engine", + hash = %new_unsafe_ref.block_info.hash, + number = new_unsafe_ref.block_info.number, + total_duration = ?total_duration, + insert_duration = ?insert_duration, + "Inserted new unsafe block" + ); + + Ok(()) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/mod.rs new file mode 100644 index 0000000000000..d24d75fdf3232 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/mod.rs @@ -0,0 +1,27 @@ +//! Tasks to update the engine state. + +mod task; +pub use task::{ + EngineTask, EngineTaskError, EngineTaskErrorSeverity, EngineTaskErrors, EngineTaskExt, +}; + +mod synchronize; +pub use synchronize::{SynchronizeTask, SynchronizeTaskError}; + +mod insert; +pub use insert::{InsertTask, InsertTaskError}; + +mod build; +pub use build::{BuildTask, BuildTaskError, EngineBuildError}; + +mod seal; +pub use seal::{SealTask, SealTaskError}; + +mod consolidate; +pub use consolidate::{ConsolidateTask, ConsolidateTaskError}; + +mod finalize; +pub use finalize::{FinalizeTask, FinalizeTaskError}; + +mod util; +pub(super) use util::{BuildAndSealError, build_and_seal}; diff --git a/kona/crates/node/engine/src/task_queue/tasks/seal/error.rs b/kona/crates/node/engine/src/task_queue/tasks/seal/error.rs new file mode 100644 index 0000000000000..736fa24aea2a0 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/seal/error.rs @@ -0,0 +1,67 @@ +//! Contains error types for the [crate::SynchronizeTask]. + +use crate::{EngineTaskError, InsertTaskError, task_queue::tasks::task::EngineTaskErrorSeverity}; +use alloy_transport::{RpcError, TransportErrorKind}; +use kona_protocol::FromBlockError; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use thiserror::Error; +use tokio::sync::mpsc; + +/// An error that occurs when running the [crate::SealTask]. +#[derive(Debug, Error)] +pub enum SealTaskError { + /// Impossible to insert the payload into the engine. + #[error(transparent)] + PayloadInsertionFailed(#[from] Box), + /// The get payload call to the engine api failed. + #[error(transparent)] + GetPayloadFailed(RpcError), + /// A deposit-only payload failed to import. + #[error("Deposit-only payload failed to import")] + DepositOnlyPayloadFailed, + /// Failed to re-attempt payload import with deposit-only payload. + #[error("Failed to re-attempt payload import with deposit-only payload")] + DepositOnlyPayloadReattemptFailed, + /// The payload is invalid, and the derivation pipeline must + /// be flushed post-holocene. + #[error("Invalid payload, must flush post-holocene")] + HoloceneInvalidFlush, + /// Failed to convert a [`OpExecutionPayload`] to a [`L2BlockInfo`]. + /// + /// [`OpExecutionPayload`]: op_alloy_rpc_types_engine::OpExecutionPayload + /// [`L2BlockInfo`]: kona_protocol::L2BlockInfo + #[error(transparent)] + FromBlock(#[from] FromBlockError), + /// Error sending the built payload envelope. + #[error(transparent)] + MpscSend( + #[from] Box>>, + ), + /// The clock went backwards. + #[error("The clock went backwards")] + ClockWentBackwards, + /// Unsafe head changed between build and seal. This likely means that there was some race + /// condition between the previous seal updating the unsafe head and the build attributes + /// being created. This build has been invalidated. + /// + /// If not propagated to the original caller for handling (i.e. there was no original caller), + /// this should not happen and is a critical error. + #[error("Unsafe head changed between build and seal")] + UnsafeHeadChangedSinceBuild, +} + +impl EngineTaskError for SealTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::PayloadInsertionFailed(inner) => inner.severity(), + Self::GetPayloadFailed(_) => EngineTaskErrorSeverity::Temporary, + Self::HoloceneInvalidFlush => EngineTaskErrorSeverity::Flush, + Self::DepositOnlyPayloadReattemptFailed => EngineTaskErrorSeverity::Critical, + Self::DepositOnlyPayloadFailed => EngineTaskErrorSeverity::Critical, + Self::FromBlock(_) => EngineTaskErrorSeverity::Critical, + Self::MpscSend(_) => EngineTaskErrorSeverity::Critical, + Self::ClockWentBackwards => EngineTaskErrorSeverity::Critical, + Self::UnsafeHeadChangedSinceBuild => EngineTaskErrorSeverity::Critical, + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/seal/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/seal/mod.rs new file mode 100644 index 0000000000000..45d94e001366d --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/seal/mod.rs @@ -0,0 +1,7 @@ +//! Task and its associated types for importing a block that has been started. + +mod task; +pub use task::SealTask; + +mod error; +pub use error::SealTaskError; diff --git a/kona/crates/node/engine/src/task_queue/tasks/seal/task.rs b/kona/crates/node/engine/src/task_queue/tasks/seal/task.rs new file mode 100644 index 0000000000000..86885c0e4d183 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/seal/task.rs @@ -0,0 +1,285 @@ +//! A task for importing a block that has already been started. +use super::SealTaskError; +use crate::{ + EngineClient, EngineGetPayloadVersion, EngineState, EngineTaskExt, InsertTask, + InsertTaskError::{self}, + task_queue::build_and_seal, +}; +use alloy_rpc_types_engine::{ExecutionPayload, PayloadId}; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_genesis::RollupConfig; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; +use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadEnvelope}; +use std::{sync::Arc, time::Instant}; +use tokio::sync::mpsc; + +/// Task for block sealing and canonicalization. +/// +/// The [`SealTask`] handles the following parts of the block building workflow: +/// +/// 1. **Payload Construction**: Retrieves the built payload using `engine_getPayload` +/// 2. **Block Import**: Imports the payload using [`InsertTask`] for canonicalization +/// +/// ## Error Handling +/// +/// The task delegates to [`InsertTaskError`] for payload import failures. +/// +/// [`InsertTask`]: crate::InsertTask +/// [`InsertTaskError`]: crate::InsertTaskError +#[derive(Debug, Clone, Constructor)] +pub struct SealTask { + /// The engine API client. + pub engine: Arc, + /// The [`RollupConfig`]. + pub cfg: Arc, + /// The [`PayloadId`] being sealed. + pub payload_id: PayloadId, + /// The [`OpAttributesWithParent`] to instruct the execution layer to build. + pub attributes: OpAttributesWithParent, + /// Whether or not the payload was derived, or created by the sequencer. + pub is_attributes_derived: bool, + /// An optional sender to convey success/failure result of the built + /// [`OpExecutionPayloadEnvelope`] after the block has been built, imported, and canonicalized + /// or the [`SealTaskError`] that occurred during processing. + pub result_tx: Option>>, +} + +impl SealTask { + /// Seals the execution payload in the EL, returning the execution envelope. + /// + /// ## Engine Method Selection + /// The method used to fetch the payload from the EL is determined by the payload timestamp. The + /// method used to import the payload into the engine is determined by the payload version. + /// + /// - `engine_getPayloadV2` is used for payloads with a timestamp before the Ecotone fork. + /// - `engine_getPayloadV3` is used for payloads with a timestamp after the Ecotone fork. + /// - `engine_getPayloadV4` is used for payloads with a timestamp after the Isthmus fork. + async fn seal_payload( + &self, + cfg: &RollupConfig, + engine: &EngineClient_, + payload_id: PayloadId, + payload_attrs: OpAttributesWithParent, + ) -> Result { + let payload_timestamp = payload_attrs.attributes().payload_attributes.timestamp; + + debug!( + target: "engine", + payload_id = payload_id.to_string(), + l2_time = payload_timestamp, + "Sealing payload" + ); + + let get_payload_version = EngineGetPayloadVersion::from_cfg(cfg, payload_timestamp); + let payload_envelope = match get_payload_version { + EngineGetPayloadVersion::V4 => { + let payload = engine.get_payload_v4(payload_id).await.map_err(|e| { + error!(target: "engine", "Payload fetch failed: {e}"); + SealTaskError::GetPayloadFailed(e) + })?; + + OpExecutionPayloadEnvelope { + parent_beacon_block_root: Some(payload.parent_beacon_block_root), + execution_payload: OpExecutionPayload::V4(payload.execution_payload), + } + } + EngineGetPayloadVersion::V3 => { + let payload = engine.get_payload_v3(payload_id).await.map_err(|e| { + error!(target: "engine", "Payload fetch failed: {e}"); + SealTaskError::GetPayloadFailed(e) + })?; + + OpExecutionPayloadEnvelope { + parent_beacon_block_root: Some(payload.parent_beacon_block_root), + execution_payload: OpExecutionPayload::V3(payload.execution_payload), + } + } + EngineGetPayloadVersion::V2 => { + let payload = engine.get_payload_v2(payload_id).await.map_err(|e| { + error!(target: "engine", "Payload fetch failed: {e}"); + SealTaskError::GetPayloadFailed(e) + })?; + + OpExecutionPayloadEnvelope { + parent_beacon_block_root: None, + execution_payload: match payload.execution_payload.into_payload() { + ExecutionPayload::V1(payload) => OpExecutionPayload::V1(payload), + ExecutionPayload::V2(payload) => OpExecutionPayload::V2(payload), + _ => unreachable!("the response should be a V1 or V2 payload"), + }, + } + } + }; + + Ok(payload_envelope) + } + + /// Inserts a payload into the engine with Holocene fallback support. + /// + /// This function handles: + /// 1. Executing the InsertTask to import the payload + /// 2. Handling deposits-only payload failures + /// 3. Holocene fallback via build_and_seal if needed + /// + /// Returns Ok(()) if the payload is successfully inserted, or an error if insertion fails. + async fn insert_payload( + &self, + state: &mut EngineState, + new_payload: OpExecutionPayloadEnvelope, + ) -> Result<(), SealTaskError> { + // Insert the new block into the engine. + match InsertTask::new( + Arc::clone(&self.engine), + self.cfg.clone(), + new_payload.clone(), + self.is_attributes_derived, + ) + .execute(state) + .await + { + Err(InsertTaskError::UnexpectedPayloadStatus(e)) + if self.attributes.is_deposits_only() => + { + error!(target: "engine", error = ?e, "Critical: Deposit-only payload import failed"); + return Err(SealTaskError::DepositOnlyPayloadFailed); + } + Err(InsertTaskError::UnexpectedPayloadStatus(e)) + if self.cfg.is_holocene_active( + self.attributes.attributes().payload_attributes.timestamp, + ) => + { + warn!(target: "engine", error = ?e, "Re-attempting payload import with deposits only."); + + // HOLOCENE: Re-attempt payload import with deposits only + // First build the deposits-only payload, then seal it + let deposits_only_attrs = self.attributes.as_deposits_only(); + + return match build_and_seal( + state, + self.engine.clone(), + self.cfg.clone(), + deposits_only_attrs.clone(), + self.is_attributes_derived, + ) + .await + { + Ok(_) => { + info!(target: "engine", "Successfully imported deposits-only payload"); + Err(SealTaskError::HoloceneInvalidFlush) + } + Err(_) => Err(SealTaskError::DepositOnlyPayloadReattemptFailed), + } + } + Err(e) => { + error!(target: "engine", "Payload import failed: {e}"); + return Err(Box::new(e).into()); + } + Ok(_) => { + info!(target: "engine", "Successfully imported payload") + } + } + + Ok(()) + } + + /// Seals and canonicalizes the block by fetching the payload and importing it. + /// + /// This function handles: + /// 1. Fetching the execution payload from the EL + /// 2. Importing the payload into the engine with Holocene fallback support + /// 3. Sending the payload to the optional channel + async fn seal_and_canonicalize_block( + &self, + state: &mut EngineState, + ) -> Result { + // Fetch the payload just inserted from the EL and import it into the engine. + let block_import_start_time = Instant::now(); + let new_payload = self + .seal_payload(&self.cfg, &self.engine, self.payload_id, self.attributes.clone()) + .await?; + + let new_block_ref = L2BlockInfo::from_payload_and_genesis( + new_payload.execution_payload.clone(), + self.attributes.attributes().payload_attributes.parent_beacon_block_root, + &self.cfg.genesis, + ) + .map_err(SealTaskError::FromBlock)?; + + // Insert the payload into the engine. + self.insert_payload(state, new_payload.clone()).await?; + + let block_import_duration = block_import_start_time.elapsed(); + + info!( + target: "engine", + l2_number = new_block_ref.block_info.number, + l2_time = new_block_ref.block_info.timestamp, + block_import_duration = ?block_import_duration, + "Built and imported new {} block", + if self.is_attributes_derived { "safe" } else { "unsafe" }, + ); + + Ok(new_payload) + } + + /// Sends the provided result via the `result_tx` sender if one exists, returning the + /// appropriate error if it does not. + /// + /// This allows the original caller to handle errors, removing that burden from the engine, + /// which may not know the caller's intent or retry preferences. If the original caller did not + /// provide a mechanism to get notified of updates, handle the error in the default manner in + /// the task queue logic. + async fn send_channel_result_or_get_error( + &self, + res: Result, + ) -> Result<(), SealTaskError> { + // NB: If a response channel was provided, that channel will receive success/failure info, + // and this task will always succeed. If not, task failure will be relayed to the caller. + if let Some(tx) = &self.result_tx { + tx.send(res).await.map_err(|e| SealTaskError::MpscSend(Box::new(e)))?; + } else if let Err(x) = res { + return Err(x) + } + + Ok(()) + } +} + +#[async_trait] +impl EngineTaskExt for SealTask { + type Output = (); + + type Error = SealTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result<(), SealTaskError> { + debug!( + target: "engine", + txs = self.attributes.attributes().transactions.as_ref().map_or(0, |txs| txs.len()), + is_deposits = self.attributes.is_deposits_only(), + "Starting new seal job" + ); + + let unsafe_block_info = state.sync_state.unsafe_head().block_info; + let parent_block_info = self.attributes.parent.block_info; + + let res = if unsafe_block_info.hash != parent_block_info.hash || + unsafe_block_info.number != parent_block_info.number + { + info!( + target: "engine", + unsafe_block_info = ?unsafe_block_info, + parent_block_info = ?parent_block_info, + "Seal attributes parent does not match unsafe head, returning rebuild error" + ); + Err(SealTaskError::UnsafeHeadChangedSinceBuild) + } else { + // Seal the block and import it into the engine. + self.seal_and_canonicalize_block(state).await + }; + + self.send_channel_result_or_get_error(res).await?; + + Ok(()) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/synchronize/error.rs b/kona/crates/node/engine/src/task_queue/tasks/synchronize/error.rs new file mode 100644 index 0000000000000..2b3b971204283 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/synchronize/error.rs @@ -0,0 +1,34 @@ +//! Contains error types for the [crate::SynchronizeTask]. + +use crate::{EngineTaskError, task_queue::tasks::task::EngineTaskErrorSeverity}; +use alloy_rpc_types_engine::PayloadStatusEnum; +use alloy_transport::{RpcError, TransportErrorKind}; +use thiserror::Error; + +/// An error that occurs when running the [crate::SynchronizeTask]. +#[derive(Debug, Error)] +pub enum SynchronizeTaskError { + /// The forkchoice update call to the engine api failed. + #[error("Forkchoice update engine api call failed due to an RPC error: {0}")] + ForkchoiceUpdateFailed(RpcError), + /// The finalized head is behind the unsafe head. + #[error("Invalid forkchoice state: unsafe head {0} is ahead of finalized head {1}")] + FinalizedAheadOfUnsafe(u64, u64), + /// The forkchoice state is invalid. + #[error("Invalid forkchoice state")] + InvalidForkchoiceState, + /// The payload status is unexpected. + #[error("Unexpected payload status: {0}")] + UnexpectedPayloadStatus(PayloadStatusEnum), +} + +impl EngineTaskError for SynchronizeTaskError { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::FinalizedAheadOfUnsafe(_, _) => EngineTaskErrorSeverity::Critical, + Self::ForkchoiceUpdateFailed(_) => EngineTaskErrorSeverity::Temporary, + Self::UnexpectedPayloadStatus(_) => EngineTaskErrorSeverity::Temporary, + Self::InvalidForkchoiceState => EngineTaskErrorSeverity::Reset, + } + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/synchronize/mod.rs b/kona/crates/node/engine/src/task_queue/tasks/synchronize/mod.rs new file mode 100644 index 0000000000000..0c4cf2ccd8a88 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/synchronize/mod.rs @@ -0,0 +1,7 @@ +//! Task and its associated types for the forkchoice engine update. + +mod task; +pub use task::SynchronizeTask; + +mod error; +pub use error::SynchronizeTaskError; diff --git a/kona/crates/node/engine/src/task_queue/tasks/synchronize/task.rs b/kona/crates/node/engine/src/task_queue/tasks/synchronize/task.rs new file mode 100644 index 0000000000000..951d396168c6b --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/synchronize/task.rs @@ -0,0 +1,156 @@ +//! A task for the `engine_forkchoiceUpdated` method, with no attributes. + +use crate::{ + EngineClient, EngineState, EngineTaskExt, SynchronizeTaskError, state::EngineSyncStateUpdate, +}; +use alloy_rpc_types_engine::{INVALID_FORK_CHOICE_STATE_ERROR, PayloadStatusEnum}; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_genesis::RollupConfig; +use std::sync::Arc; +use tokio::time::Instant; + +/// Internal task for execution layer forkchoice synchronization. +/// +/// The [`SynchronizeTask`] performs `engine_forkchoiceUpdated` calls to synchronize +/// the execution layer's forkchoice state with the rollup node's view. This task +/// operates without payload attributes and is primarily used internally by other +/// engine tasks rather than being directly enqueued by users. +/// +/// ## Usage Patterns +/// +/// - **Internal Synchronization**: Called by [`InsertTask`], [`ConsolidateTask`], and +/// [`FinalizeTask`] +/// - **Engine Reset**: Used during engine resets to establish initial forkchoice state +/// - **Safe Head Updates**: Synchronizes safe and finalized head changes +/// +/// ## Automatic Integration +/// +/// Unlike the legacy `ForkchoiceTask`, forkchoice updates during block building are now +/// explicitly handled within [`BuildTask`], eliminating the need for explicit +/// forkchoice management in most user scenarios. +/// +/// [`InsertTask`]: crate::InsertTask +/// [`ConsolidateTask`]: crate::ConsolidateTask +/// [`FinalizeTask`]: crate::FinalizeTask +/// [`BuildTask`]: crate::BuildTask +#[derive(Debug, Clone, Constructor)] +pub struct SynchronizeTask { + /// The engine client. + pub client: Arc, + /// The rollup config. + pub rollup: Arc, + /// The sync state update to apply to the engine state. + pub state_update: EngineSyncStateUpdate, +} + +impl SynchronizeTask { + /// Checks the response of the `engine_forkchoiceUpdated` call, and updates the sync status if + /// necessary. + fn check_forkchoice_updated_status( + &self, + state: &mut EngineState, + status: &PayloadStatusEnum, + ) -> Result<(), SynchronizeTaskError> { + match status { + PayloadStatusEnum::Valid => { + if !state.el_sync_finished { + info!( + target: "engine", + "Finished execution layer sync." + ); + state.el_sync_finished = true; + } + + Ok(()) + } + PayloadStatusEnum::Syncing => { + // If we're not building a new payload, we're driving EL sync. + debug!(target: "engine", "Attempting to update forkchoice state while EL syncing"); + Ok(()) + } + s => { + // Other codes are not expected. + Err(SynchronizeTaskError::UnexpectedPayloadStatus(s.clone())) + } + } + } +} + +#[async_trait] +impl EngineTaskExt for SynchronizeTask { + type Output = (); + type Error = SynchronizeTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result { + // Apply the sync state update to the engine state. + let new_sync_state = state.sync_state.apply_update(self.state_update); + + // Check if a forkchoice update is not needed, return early. + // A forkchoice update is not needed if... + // 1. The engine state is not default (initial forkchoice state has been emitted), and + // 2. The new sync state is the same as the current sync state (no changes to the sync + // state). + // + // NOTE: + // We shouldn't retry the synchronize task there. Since the `sync_state` is only updated + // inside the `SynchronizeTask` (except inside the ConsolidateTask, when the block is not + // the last in the batch) - the engine will get stuck retrying the `SynchronizeTask` + if state.sync_state != Default::default() && state.sync_state == new_sync_state { + debug!(target: "engine", ?new_sync_state, "No forkchoice update needed"); + return Ok(()); + } + + // Check if the head is behind the finalized head. + if new_sync_state.unsafe_head().block_info.number < + new_sync_state.finalized_head().block_info.number + { + return Err(SynchronizeTaskError::FinalizedAheadOfUnsafe( + new_sync_state.unsafe_head().block_info.number, + new_sync_state.finalized_head().block_info.number, + )); + } + + let fcu_time_start = Instant::now(); + + // Send the forkchoice update through the input. + let forkchoice = new_sync_state.create_forkchoice_state(); + + // Handle the forkchoice update result. + // NOTE: it doesn't matter which version we use here, because we're not sending any + // payload attributes. The forkchoice updated call is version agnostic if no payload + // attributes are provided. + let response = self.client.fork_choice_updated_v3(forkchoice, None).await; + + let valid_response = response.map_err(|e| { + // Fatal forkchoice update error. + let error = e + .as_error_resp() + .and_then(|e| { + (e.code == INVALID_FORK_CHOICE_STATE_ERROR as i64) + .then_some(SynchronizeTaskError::InvalidForkchoiceState) + }) + .unwrap_or_else(|| SynchronizeTaskError::ForkchoiceUpdateFailed(e)); + + debug!(target: "engine", error = ?error, "Unexpected forkchoice update error"); + + error + })?; + + self.check_forkchoice_updated_status(state, &valid_response.payload_status.status)?; + + // Apply the new sync state to the engine state. + state.sync_state = new_sync_state; + + let fcu_duration = fcu_time_start.elapsed(); + debug!( + target: "engine", + fcu_duration = ?fcu_duration, + forkchoice = ?forkchoice, + response = ?valid_response, + "Forkchoice updated" + ); + + Ok(()) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/task.rs b/kona/crates/node/engine/src/task_queue/tasks/task.rs new file mode 100644 index 0000000000000..5a60cc63c75a0 --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/task.rs @@ -0,0 +1,242 @@ +//! Tasks sent to the [`Engine`] for execution. +//! +//! [`Engine`]: crate::Engine + +use super::{BuildTask, ConsolidateTask, FinalizeTask, InsertTask}; +use crate::{ + BuildTaskError, ConsolidateTaskError, EngineClient, EngineState, FinalizeTaskError, + InsertTaskError, + task_queue::{SealTask, SealTaskError}, +}; +use async_trait::async_trait; +use derive_more::Display; +use std::cmp::Ordering; +use thiserror::Error; +use tokio::task::yield_now; + +/// The severity of an engine task error. +/// +/// This is used to determine how to handle the error when draining the engine task queue. +#[derive(Debug, PartialEq, Eq, Display, Clone, Copy)] +pub enum EngineTaskErrorSeverity { + /// The error is temporary and the task is retried. + #[display("temporary")] + Temporary, + /// The error is critical and is propagated to the engine actor. + #[display("critical")] + Critical, + /// The error indicates that the engine should be reset. + #[display("reset")] + Reset, + /// The error indicates that the engine should be flushed. + #[display("flush")] + Flush, +} + +/// The interface for an engine task error. +/// +/// An engine task error should have an associated severity level to specify how to handle the error +/// when draining the engine task queue. +pub trait EngineTaskError { + /// The severity of the error. + fn severity(&self) -> EngineTaskErrorSeverity; +} + +/// The interface for an engine task. +#[async_trait] +pub trait EngineTaskExt { + /// The output type of the task. + type Output; + + /// The error type of the task. + type Error: EngineTaskError; + + /// Executes the task, taking a shared lock on the engine state and `self`. + async fn execute(&self, state: &mut EngineState) -> Result; +} + +/// An error that may occur during an [`EngineTask`]'s execution. +#[derive(Error, Debug)] +pub enum EngineTaskErrors { + /// An error that occurred while inserting a block into the engine. + #[error(transparent)] + Insert(#[from] InsertTaskError), + /// An error that occurred while building a block. + #[error(transparent)] + Build(#[from] BuildTaskError), + /// An error that occurred while sealing a block. + #[error(transparent)] + Seal(#[from] SealTaskError), + /// An error that occurred while consolidating the engine state. + #[error(transparent)] + Consolidate(#[from] ConsolidateTaskError), + /// An error that occurred while finalizing an L2 block. + #[error(transparent)] + Finalize(#[from] FinalizeTaskError), +} + +impl EngineTaskError for EngineTaskErrors { + fn severity(&self) -> EngineTaskErrorSeverity { + match self { + Self::Insert(inner) => inner.severity(), + Self::Build(inner) => inner.severity(), + Self::Seal(inner) => inner.severity(), + Self::Consolidate(inner) => inner.severity(), + Self::Finalize(inner) => inner.severity(), + } + } +} + +/// Tasks that may be inserted into and executed by the [`Engine`]. +/// +/// [`Engine`]: crate::Engine +#[derive(Debug, Clone)] +pub enum EngineTask { + /// Inserts a payload into the execution engine. + Insert(Box>), + /// Begins building a new block with the given attributes, producing a new payload ID. + Build(Box>), + /// Seals the block with the given payload ID and attributes, inserting it into the execution + /// engine. + Seal(Box>), + /// Performs consolidation on the engine state, reverting to payload attribute processing + /// via the [`BuildTask`] if consolidation fails. + Consolidate(Box>), + /// Finalizes an L2 block + Finalize(Box>), +} + +impl EngineTask { + /// Executes the task without consuming it. + async fn execute_inner(&self, state: &mut EngineState) -> Result<(), EngineTaskErrors> { + match self { + Self::Insert(task) => task.execute(state).await?, + Self::Seal(task) => task.execute(state).await?, + Self::Consolidate(task) => task.execute(state).await?, + Self::Finalize(task) => task.execute(state).await?, + Self::Build(task) => { + task.execute(state).await?; + } + }; + + Ok(()) + } + + const fn task_metrics_label(&self) -> &'static str { + match self { + Self::Insert(_) => crate::Metrics::INSERT_TASK_LABEL, + Self::Consolidate(_) => crate::Metrics::CONSOLIDATE_TASK_LABEL, + Self::Build(_) => crate::Metrics::BUILD_TASK_LABEL, + Self::Seal(_) => crate::Metrics::SEAL_TASK_LABEL, + Self::Finalize(_) => crate::Metrics::FINALIZE_TASK_LABEL, + } + } +} + +impl PartialEq for EngineTask { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Self::Insert(_), Self::Insert(_)) | + (Self::Build(_), Self::Build(_)) | + (Self::Seal(_), Self::Seal(_)) | + (Self::Consolidate(_), Self::Consolidate(_)) | + (Self::Finalize(_), Self::Finalize(_)) + ) + } +} + +impl Eq for EngineTask {} + +impl PartialOrd for EngineTask { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EngineTask { + fn cmp(&self, other: &Self) -> Ordering { + // Order (descending): BuildBlock -> InsertUnsafe -> Consolidate -> Finalize + // + // https://specs.optimism.io/protocol/derivation.html#forkchoice-synchronization + // + // - Block building jobs are prioritized above all other tasks, to give priority to the + // sequencer. BuildTask handles forkchoice updates automatically. + // - InsertUnsafe tasks are prioritized over Consolidate tasks, to ensure that unsafe block + // gossip is imported promptly. + // - Consolidate tasks are prioritized over Finalize tasks, as they advance the safe chain + // via derivation. + // - Finalize tasks have the lowest priority, as they only update finalized status. + match (self, other) { + // Same variant cases + (Self::Insert(_), Self::Insert(_)) => Ordering::Equal, + (Self::Consolidate(_), Self::Consolidate(_)) => Ordering::Equal, + (Self::Build(_), Self::Build(_)) => Ordering::Equal, + (Self::Seal(_), Self::Seal(_)) => Ordering::Equal, + (Self::Finalize(_), Self::Finalize(_)) => Ordering::Equal, + + // SealBlock tasks are prioritized over all others + (Self::Seal(_), _) => Ordering::Greater, + (_, Self::Seal(_)) => Ordering::Less, + + // BuildBlock tasks are prioritized over InsertUnsafe and Consolidate tasks + (Self::Build(_), _) => Ordering::Greater, + (_, Self::Build(_)) => Ordering::Less, + + // InsertUnsafe tasks are prioritized over Consolidate and Finalize tasks + (Self::Insert(_), _) => Ordering::Greater, + (_, Self::Insert(_)) => Ordering::Less, + + // Consolidate tasks are prioritized over Finalize tasks + (Self::Consolidate(_), _) => Ordering::Greater, + (_, Self::Consolidate(_)) => Ordering::Less, + } + } +} + +#[async_trait] +impl EngineTaskExt for EngineTask { + type Output = (); + + type Error = EngineTaskErrors; + + async fn execute(&self, state: &mut EngineState) -> Result<(), Self::Error> { + // Retry the task until it succeeds or a critical error occurs. + while let Err(e) = self.execute_inner(state).await { + let severity = e.severity(); + + kona_macros::inc!( + counter, + crate::Metrics::ENGINE_TASK_FAILURE, + self.task_metrics_label() => severity.to_string() + ); + + match severity { + EngineTaskErrorSeverity::Temporary => { + trace!(target: "engine", "{e}"); + + // Yield the task to allow other tasks to execute to avoid starvation. + yield_now().await; + + continue; + } + EngineTaskErrorSeverity::Critical => { + error!(target: "engine", "{e}"); + return Err(e); + } + EngineTaskErrorSeverity::Reset => { + warn!(target: "engine", "Engine requested derivation reset"); + return Err(e); + } + EngineTaskErrorSeverity::Flush => { + warn!(target: "engine", "Engine requested derivation flush"); + return Err(e); + } + } + } + + kona_macros::inc!(counter, crate::Metrics::ENGINE_TASK_SUCCESS, self.task_metrics_label()); + + Ok(()) + } +} diff --git a/kona/crates/node/engine/src/task_queue/tasks/util.rs b/kona/crates/node/engine/src/task_queue/tasks/util.rs new file mode 100644 index 0000000000000..835725529fc8e --- /dev/null +++ b/kona/crates/node/engine/src/task_queue/tasks/util.rs @@ -0,0 +1,59 @@ +//! Utility functions for task execution. + +use super::{BuildTask, BuildTaskError, EngineTaskExt, SealTask, SealTaskError}; +use crate::{EngineClient, EngineState}; +use kona_genesis::RollupConfig; +use kona_protocol::OpAttributesWithParent; +use std::sync::Arc; + +/// Error type for build and seal operations. +#[derive(Debug, thiserror::Error)] +pub(in crate::task_queue) enum BuildAndSealError { + /// An error occurred during the build phase. + #[error(transparent)] + Build(#[from] BuildTaskError), + /// An error occurred during the seal phase. + #[error(transparent)] + Seal(#[from] SealTaskError), +} + +/// Builds and seals a payload in sequence. +/// +/// This is a utility function that: +/// 1. Creates and executes a [`BuildTask`] to initiate block building +/// 2. Creates and executes a [`SealTask`] to seal the block, referencing the initiated payload +/// +/// This pattern is commonly used for Holocene deposits-only fallback and other scenarios +/// where a build-then-seal workflow is needed. +/// +/// # Arguments +/// +/// * `state` - Mutable reference to the engine state +/// * `engine` - The engine client +/// * `cfg` - The rollup configuration +/// * `attributes` - The payload attributes to build +/// * `is_attributes_derived` - Whether the attributes were derived or created by the sequencer +pub(in crate::task_queue) async fn build_and_seal( + state: &mut EngineState, + engine: Arc, + cfg: Arc, + attributes: OpAttributesWithParent, + is_attributes_derived: bool, +) -> Result<(), BuildAndSealError> { + // Execute the build task + let payload_id = BuildTask::new( + engine.clone(), + cfg.clone(), + attributes.clone(), + None, // Build task doesn't send the payload yet + ) + .execute(state) + .await?; + + // Execute the seal task with the payload ID from the build + SealTask::new(engine, cfg, payload_id, attributes, is_attributes_derived, None) + .execute(state) + .await?; + + Ok(()) +} diff --git a/kona/crates/node/engine/src/test_utils/attributes.rs b/kona/crates/node/engine/src/test_utils/attributes.rs new file mode 100644 index 0000000000000..09c2cbe8143b8 --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/attributes.rs @@ -0,0 +1,111 @@ +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, b256}; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// Builder for creating test OpAttributesWithParent instances with sensible defaults +#[derive(Debug)] +pub struct TestAttributesBuilder { + timestamp: u64, + prev_randao: B256, + suggested_fee_recipient: alloy_primitives::Address, + withdrawals: Option>, + parent_beacon_block_root: Option, + transactions: Option>, + no_tx_pool: Option, + gas_limit: Option, + eip_1559_params: Option, + min_base_fee: Option, + parent: L2BlockInfo, + derived_from: Option, + is_last_in_span: bool, +} + +impl TestAttributesBuilder { + /// Creates a new builder with default values + pub fn new() -> Self { + let parent = L2BlockInfo { + block_info: BlockInfo { + number: 0, + hash: b256!("1111111111111111111111111111111111111111111111111111111111111111"), + parent_hash: B256::ZERO, + timestamp: 1000, + }, + l1_origin: BlockNumHash::default(), + seq_num: 0, + }; + + Self { + timestamp: 2000, + prev_randao: b256!("2222222222222222222222222222222222222222222222222222222222222222"), + suggested_fee_recipient: alloy_primitives::Address::ZERO, + withdrawals: None, + parent_beacon_block_root: Some(B256::ZERO), + transactions: None, + no_tx_pool: Some(false), + gas_limit: Some(30_000_000), + eip_1559_params: None, + min_base_fee: None, + parent, + derived_from: None, + is_last_in_span: false, + } + } + + /// Sets the timestamp + pub const fn with_timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Sets the parent block + pub const fn with_parent(mut self, parent: L2BlockInfo) -> Self { + self.parent = parent; + self + } + + /// Sets the transactions + #[allow(dead_code)] + pub fn with_transactions(mut self, txs: Vec) -> Self { + self.transactions = Some(txs); + self + } + + /// Sets the gas limit + #[allow(dead_code)] + pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + /// Builds the OpAttributesWithParent + pub fn build(self) -> OpAttributesWithParent { + let attributes = OpPayloadAttributes { + payload_attributes: alloy_rpc_types_engine::PayloadAttributes { + timestamp: self.timestamp, + prev_randao: self.prev_randao, + suggested_fee_recipient: self.suggested_fee_recipient, + withdrawals: self.withdrawals, + parent_beacon_block_root: self.parent_beacon_block_root, + }, + transactions: self.transactions, + no_tx_pool: self.no_tx_pool, + gas_limit: self.gas_limit, + eip_1559_params: self.eip_1559_params, + min_base_fee: self.min_base_fee, + }; + + OpAttributesWithParent::new( + attributes, + self.parent, + self.derived_from, + self.is_last_in_span, + ) + } +} + +impl Default for TestAttributesBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/kona/crates/node/engine/src/test_utils/engine_client.rs b/kona/crates/node/engine/src/test_utils/engine_client.rs new file mode 100644 index 0000000000000..5fedc431b5695 --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/engine_client.rs @@ -0,0 +1,803 @@ +//! Mock implementations for testing engine client functionality. + +use crate::{EngineClient, HyperAuthClient}; +use alloy_eips::{BlockId, eip1898::BlockNumberOrTag}; +use alloy_network::{Ethereum, Network}; +use alloy_primitives::{Address, B256, BlockHash, StorageKey}; +use alloy_provider::{EthGetBlock, ProviderCall, RpcWithBlock}; +use alloy_rpc_types_engine::{ + ClientVersionV1, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadInputV2, + ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, + PayloadStatus, +}; +use alloy_rpc_types_eth::{Block, EIP1186AccountProofResponse, Transaction as EthTransaction}; +use alloy_transport::{TransportError, TransportErrorKind, TransportResult}; +use alloy_transport_http::Http; +use async_trait::async_trait; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; +use op_alloy_network::Optimism; +use op_alloy_provider::ext::engine::OpEngineApi; +use op_alloy_rpc_types::Transaction as OpTransaction; +use op_alloy_rpc_types_engine::{ + OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, + OpPayloadAttributes, ProtocolVersion, +}; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::RwLock; + +use crate::EngineClientError; + +/// Builder for creating test MockEngineClient instances with sensible defaults +pub fn test_engine_client_builder() -> MockEngineClientBuilder { + MockEngineClientBuilder::new().with_config(Arc::new(RollupConfig::default())) +} + +/// Mock storage for engine client responses. +/// +/// Each API method has version-specific storage to allow tests to verify +/// which specific version was called and return different responses per version. +#[derive(Debug, Clone, Default)] +pub struct MockEngineStorage { + /// Storage for block responses by tag. + pub l2_blocks_by_label: HashMap>, + /// Storage for block info responses by tag. + pub block_info_by_tag: HashMap, + + // Version-specific new_payload responses + /// Storage for new_payload_v1 responses. + pub new_payload_v1_response: Option, + /// Storage for new_payload_v2 responses. + pub new_payload_v2_response: Option, + /// Storage for new_payload_v3 responses. + pub new_payload_v3_response: Option, + /// Storage for new_payload_v4 responses. + pub new_payload_v4_response: Option, + + // Version-specific fork_choice_updated responses + /// Storage for fork_choice_updated_v2 responses. + pub fork_choice_updated_v2_response: Option, + /// Storage for fork_choice_updated_v3 responses. + pub fork_choice_updated_v3_response: Option, + + // Version-specific get_payload responses + /// Storage for execution payload envelope v2 responses. + pub execution_payload_v2: Option, + /// Storage for OP execution payload envelope v3 responses. + pub execution_payload_v3: Option, + /// Storage for OP execution payload envelope v4 responses. + pub execution_payload_v4: Option, + + // Version-specific get_payload_bodies responses + /// Storage for get_payload_bodies_by_hash_v1 responses. + pub get_payload_bodies_by_hash_v1_response: Option, + /// Storage for get_payload_bodies_by_range_v1 responses. + pub get_payload_bodies_by_range_v1_response: Option, + + // Non-versioned responses + /// Storage for client version responses. + pub client_versions: Option>, + /// Storage for protocol version responses. + pub protocol_version: Option, + /// Storage for capabilities responses. + pub capabilities: Option>, + + // Storage for get_l1_block, get_l2_block, and get_proof + /// Storage for L1 blocks by stringified BlockId. + /// L1 blocks use standard Ethereum transactions. + pub l1_blocks_by_id: HashMap>, + /// Storage for L2 blocks by stringified BlockId. + /// L2 blocks use OP Stack transactions. + pub l2_blocks_by_id: HashMap>, + /// Storage for proofs by (address, stringified BlockId) key. + pub proofs_by_address: HashMap<(Address, String), EIP1186AccountProofResponse>, +} + +/// Builder for constructing a [`MockEngineClient`] with pre-configured responses. +/// +/// This builder allows you to set up mock responses before creating the client, +/// making it easier to write concise tests. +/// +/// # Example +/// +/// ```rust,ignore +/// use kona_engine::test_utils::{MockEngineClient}; +/// use alloy_rpc_types_engine::{PayloadStatus, PayloadStatusEnum}; +/// use std::sync::Arc; +/// +/// let mock = MockEngineClient::builder() +/// .with_config(Arc::new(RollupConfig::default())) +/// .with_payload_status(PayloadStatus { +/// status: PayloadStatusEnum::Valid, +/// latest_valid_hash: Some(B256::ZERO), +/// }) +/// .build(); +/// ``` +#[derive(Debug)] +pub struct MockEngineClientBuilder { + cfg: Option>, + storage: MockEngineStorage, +} + +impl MockEngineClientBuilder { + /// Creates a new builder with default values. + pub fn new() -> Self { + Self { cfg: None, storage: MockEngineStorage::default() } + } + + /// Sets the rollup configuration. + pub fn with_config(mut self, cfg: Arc) -> Self { + self.cfg = Some(cfg); + self + } + + /// Sets a block response for a specific tag. + pub fn with_l2_block_by_label( + mut self, + tag: BlockNumberOrTag, + block: Block, + ) -> Self { + self.storage.l2_blocks_by_label.insert(tag, block); + self + } + + /// Sets a block info response for a specific tag. + pub fn with_block_info_by_tag(mut self, tag: BlockNumberOrTag, info: L2BlockInfo) -> Self { + self.storage.block_info_by_tag.insert(tag, info); + self + } + + /// Sets the new_payload_v1 response. + pub fn with_new_payload_v1_response(mut self, status: PayloadStatus) -> Self { + self.storage.new_payload_v1_response = Some(status); + self + } + + /// Sets the new_payload_v2 response. + pub fn with_new_payload_v2_response(mut self, status: PayloadStatus) -> Self { + self.storage.new_payload_v2_response = Some(status); + self + } + + /// Sets the new_payload_v3 response. + pub fn with_new_payload_v3_response(mut self, status: PayloadStatus) -> Self { + self.storage.new_payload_v3_response = Some(status); + self + } + + /// Sets the new_payload_v4 response. + pub fn with_new_payload_v4_response(mut self, status: PayloadStatus) -> Self { + self.storage.new_payload_v4_response = Some(status); + self + } + + /// Sets the fork_choice_updated_v2 response. + pub fn with_fork_choice_updated_v2_response(mut self, response: ForkchoiceUpdated) -> Self { + self.storage.fork_choice_updated_v2_response = Some(response); + self + } + + /// Sets the fork_choice_updated_v3 response. + pub fn with_fork_choice_updated_v3_response(mut self, response: ForkchoiceUpdated) -> Self { + self.storage.fork_choice_updated_v3_response = Some(response); + self + } + + /// Sets the execution payload v2 response. + pub fn with_execution_payload_v2(mut self, payload: ExecutionPayloadEnvelopeV2) -> Self { + self.storage.execution_payload_v2 = Some(payload); + self + } + + /// Sets the execution payload v3 response. + pub fn with_execution_payload_v3(mut self, payload: OpExecutionPayloadEnvelopeV3) -> Self { + self.storage.execution_payload_v3 = Some(payload); + self + } + + /// Sets the execution payload v4 response. + pub fn with_execution_payload_v4(mut self, payload: OpExecutionPayloadEnvelopeV4) -> Self { + self.storage.execution_payload_v4 = Some(payload); + self + } + + /// Sets the get_payload_bodies_by_hash_v1 response. + pub fn with_payload_bodies_by_hash_response( + mut self, + bodies: ExecutionPayloadBodiesV1, + ) -> Self { + self.storage.get_payload_bodies_by_hash_v1_response = Some(bodies); + self + } + + /// Sets the get_payload_bodies_by_range_v1 response. + pub fn with_payload_bodies_by_range_response( + mut self, + bodies: ExecutionPayloadBodiesV1, + ) -> Self { + self.storage.get_payload_bodies_by_range_v1_response = Some(bodies); + self + } + + /// Sets the client versions response. + pub fn with_client_versions(mut self, versions: Vec) -> Self { + self.storage.client_versions = Some(versions); + self + } + + /// Sets the protocol version response. + pub const fn with_protocol_version(mut self, version: ProtocolVersion) -> Self { + self.storage.protocol_version = Some(version); + self + } + + /// Sets the capabilities response. + pub fn with_capabilities(mut self, capabilities: Vec) -> Self { + self.storage.capabilities = Some(capabilities); + self + } + + /// Sets an L1 block response for a specific BlockId. + pub fn with_l1_block(mut self, block_id: BlockId, block: Block) -> Self { + let key = block_id_to_key(&block_id); + self.storage.l1_blocks_by_id.insert(key, block); + self + } + + /// Sets an L2 block response for a specific BlockId. + pub fn with_l2_block(mut self, block_id: BlockId, block: Block) -> Self { + let key = block_id_to_key(&block_id); + self.storage.l2_blocks_by_id.insert(key, block); + self + } + + /// Sets a proof response for a specific address and BlockId. + pub fn with_proof( + mut self, + address: Address, + block_id: BlockId, + proof: EIP1186AccountProofResponse, + ) -> Self { + let key = block_id_to_key(&block_id); + self.storage.proofs_by_address.insert((address, key), proof); + self + } + + /// Builds the [`MockEngineClient`] with the configured values. + /// + /// # Panics + /// + /// Panics if any required fields (cfg) are not set. + pub fn build(self) -> MockEngineClient { + let cfg = self.cfg.expect("cfg must be set"); + + MockEngineClient { cfg, storage: Arc::new(RwLock::new(self.storage)) } + } +} + +impl Default for MockEngineClientBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Mock implementation of the EngineClient trait for testing. +/// +/// This mock allows tests to configure expected responses for all EngineClient +/// and OpEngineApi methods. All responses are stored in a shared [`MockEngineStorage`] +/// protected by an RwLock for thread-safe access. +#[derive(Debug, Clone)] +pub struct MockEngineClient { + /// The rollup configuration. + cfg: Arc, + /// Shared storage for mock responses. + storage: Arc>, +} + +impl MockEngineClient { + /// Creates a new mock engine client with the given config. + pub fn new(cfg: Arc) -> Self { + Self { cfg, storage: Arc::new(RwLock::new(MockEngineStorage::default())) } + } + + /// Creates a builder for constructing a mock engine client. + pub fn builder() -> MockEngineClientBuilder { + MockEngineClientBuilder::new() + } + + /// Returns a reference to the mock storage for configuring responses. + pub fn storage(&self) -> Arc> { + Arc::clone(&self.storage) + } + + /// Sets a block response for a specific tag. + pub async fn set_l2_block_by_label(&self, tag: BlockNumberOrTag, block: Block) { + self.storage.write().await.l2_blocks_by_label.insert(tag, block); + } + + /// Sets a block info response for a specific tag. + pub async fn set_block_info_by_tag(&self, tag: BlockNumberOrTag, info: L2BlockInfo) { + self.storage.write().await.block_info_by_tag.insert(tag, info); + } + + /// Sets the new_payload_v1 response. + pub async fn set_new_payload_v1_response(&self, status: PayloadStatus) { + self.storage.write().await.new_payload_v1_response = Some(status); + } + + /// Sets the new_payload_v2 response. + pub async fn set_new_payload_v2_response(&self, status: PayloadStatus) { + self.storage.write().await.new_payload_v2_response = Some(status); + } + + /// Sets the new_payload_v3 response. + pub async fn set_new_payload_v3_response(&self, status: PayloadStatus) { + self.storage.write().await.new_payload_v3_response = Some(status); + } + + /// Sets the new_payload_v4 response. + pub async fn set_new_payload_v4_response(&self, status: PayloadStatus) { + self.storage.write().await.new_payload_v4_response = Some(status); + } + + /// Sets the fork_choice_updated_v2 response. + pub async fn set_fork_choice_updated_v2_response(&self, response: ForkchoiceUpdated) { + self.storage.write().await.fork_choice_updated_v2_response = Some(response); + } + + /// Sets the fork_choice_updated_v3 response. + pub async fn set_fork_choice_updated_v3_response(&self, response: ForkchoiceUpdated) { + self.storage.write().await.fork_choice_updated_v3_response = Some(response); + } + + /// Sets the execution payload v2 response. + pub async fn set_execution_payload_v2(&self, payload: ExecutionPayloadEnvelopeV2) { + self.storage.write().await.execution_payload_v2 = Some(payload); + } + + /// Sets the execution payload v3 response. + pub async fn set_execution_payload_v3(&self, payload: OpExecutionPayloadEnvelopeV3) { + self.storage.write().await.execution_payload_v3 = Some(payload); + } + + /// Sets the execution payload v4 response. + pub async fn set_execution_payload_v4(&self, payload: OpExecutionPayloadEnvelopeV4) { + self.storage.write().await.execution_payload_v4 = Some(payload); + } + + /// Sets the get_payload_bodies_by_hash_v1 response. + pub async fn set_payload_bodies_by_hash_response(&self, bodies: ExecutionPayloadBodiesV1) { + self.storage.write().await.get_payload_bodies_by_hash_v1_response = Some(bodies); + } + + /// Sets the get_payload_bodies_by_range_v1 response. + pub async fn set_payload_bodies_by_range_response(&self, bodies: ExecutionPayloadBodiesV1) { + self.storage.write().await.get_payload_bodies_by_range_v1_response = Some(bodies); + } + + /// Sets the client versions response. + pub async fn set_client_versions(&self, versions: Vec) { + self.storage.write().await.client_versions = Some(versions); + } + + /// Sets the protocol version response. + pub async fn set_protocol_version(&self, version: ProtocolVersion) { + self.storage.write().await.protocol_version = Some(version); + } + + /// Sets the capabilities response. + pub async fn set_capabilities(&self, capabilities: Vec) { + self.storage.write().await.capabilities = Some(capabilities); + } + + /// Sets an L1 block response for a specific BlockId. + pub async fn set_l1_block(&self, block_id: BlockId, block: Block) { + let key = block_id_to_key(&block_id); + self.storage.write().await.l1_blocks_by_id.insert(key, block); + } + + /// Sets an L2 block response for a specific BlockId. + pub async fn set_l2_block(&self, block_id: BlockId, block: Block) { + let key = block_id_to_key(&block_id); + self.storage.write().await.l2_blocks_by_id.insert(key, block); + } + + /// Sets a proof response for a specific address and BlockId. + pub async fn set_proof( + &self, + address: Address, + block_id: BlockId, + proof: EIP1186AccountProofResponse, + ) { + let key = block_id_to_key(&block_id); + self.storage.write().await.proofs_by_address.insert((address, key), proof); + } +} + +#[async_trait] +impl EngineClient for MockEngineClient { + fn cfg(&self) -> &RollupConfig { + self.cfg.as_ref() + } + + fn get_l1_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + let storage = Arc::clone(&self.storage); + let block_key = block_id_to_key(&block); + + EthGetBlock::new_provider( + block, + Box::new(move |_kind| { + let storage = Arc::clone(&storage); + let block_key = block_key.clone(); + + ProviderCall::BoxedFuture(Box::pin(async move { + let storage_guard = storage.read().await; + Ok(storage_guard.l1_blocks_by_id.get(&block_key).cloned()) + })) + }), + ) + } + + fn get_l2_block(&self, block: BlockId) -> EthGetBlock<::BlockResponse> { + let storage = Arc::clone(&self.storage); + let block_key = block_id_to_key(&block); + + EthGetBlock::new_provider( + block, + Box::new(move |_kind| { + let storage = Arc::clone(&storage); + let block_key = block_key.clone(); + + ProviderCall::BoxedFuture(Box::pin(async move { + let storage_guard = storage.read().await; + Ok(storage_guard.l2_blocks_by_id.get(&block_key).cloned()) + })) + }), + ) + } + + fn get_proof( + &self, + address: Address, + _keys: Vec, + ) -> RpcWithBlock<(Address, Vec), EIP1186AccountProofResponse> { + let storage = Arc::clone(&self.storage); + + RpcWithBlock::new_provider(move |block_id| { + let storage = Arc::clone(&storage); + let block_key = block_id_to_key(&block_id); + let address = address; + + ProviderCall::BoxedFuture(Box::pin(async move { + let storage_guard = storage.read().await; + storage_guard.proofs_by_address.get(&(address, block_key)).cloned().ok_or_else( + || { + TransportError::from(TransportErrorKind::custom_str( + "No proof configured for this address and block. \ + Use with_proof() or set_proof() to set a response.", + )) + }, + ) + })) + }) + } + + async fn new_payload_v1(&self, _payload: ExecutionPayloadV1) -> TransportResult { + let storage = self.storage.read().await; + storage.new_payload_v1_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "new_payload_v1 was called but no v1 response configured. \ + Use with_new_payload_v1_response() or set_new_payload_v1_response() to set a response." + )) + }) + } + + async fn l2_block_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result>, EngineClientError> { + let storage = self.storage.read().await; + Ok(storage.l2_blocks_by_label.get(&numtag).cloned()) + } + + async fn l2_block_info_by_label( + &self, + numtag: BlockNumberOrTag, + ) -> Result, EngineClientError> { + let storage = self.storage.read().await; + Ok(storage.block_info_by_tag.get(&numtag).cloned()) + } +} + +#[async_trait] +impl OpEngineApi> for MockEngineClient { + async fn new_payload_v2( + &self, + _payload: ExecutionPayloadInputV2, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.new_payload_v2_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "new_payload_v2 was called but no v2 response configured. \ + Use with_new_payload_v2_response() or set_new_payload_v2_response() to set a response." + )) + }) + } + + async fn new_payload_v3( + &self, + _payload: ExecutionPayloadV3, + _parent_beacon_block_root: B256, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.new_payload_v3_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "new_payload_v3 was called but no v3 response configured. \ + Use with_new_payload_v3_response() or set_new_payload_v3_response() to set a response." + )) + }) + } + + async fn new_payload_v4( + &self, + _payload: OpExecutionPayloadV4, + _parent_beacon_block_root: B256, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.new_payload_v4_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "new_payload_v4 was called but no v4 response configured. \ + Use with_new_payload_v4_response() or set_new_payload_v4_response() to set a response." + )) + }) + } + + async fn fork_choice_updated_v2( + &self, + _fork_choice_state: ForkchoiceState, + _payload_attributes: Option, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.fork_choice_updated_v2_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "fork_choice_updated_v2 was called but no v2 response configured. \ + Use with_fork_choice_updated_v2_response() or set_fork_choice_updated_v2_response() to set a response." + )) + }) + } + + async fn fork_choice_updated_v3( + &self, + _fork_choice_state: ForkchoiceState, + _payload_attributes: Option, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.fork_choice_updated_v3_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "fork_choice_updated_v3 was called but no v3 response configured. \ + Use with_fork_choice_updated_v3_response() or set_fork_choice_updated_v3_response() to set a response." + )) + }) + } + + async fn get_payload_v2( + &self, + _payload_id: PayloadId, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.execution_payload_v2.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "No execution payload v2 set in mock", + )) + }) + } + + async fn get_payload_v3( + &self, + _payload_id: PayloadId, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.execution_payload_v3.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "No execution payload v3 set in mock", + )) + }) + } + + async fn get_payload_v4( + &self, + _payload_id: PayloadId, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.execution_payload_v4.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "No execution payload v4 set in mock", + )) + }) + } + + async fn get_payload_bodies_by_hash_v1( + &self, + _block_hashes: Vec, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.get_payload_bodies_by_hash_v1_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "get_payload_bodies_by_hash_v1 was called but no response configured. \ + Use with_payload_bodies_by_hash_response() or set_payload_bodies_by_hash_response() to set a response." + )) + }) + } + + async fn get_payload_bodies_by_range_v1( + &self, + _start: u64, + _count: u64, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.get_payload_bodies_by_range_v1_response.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str( + "get_payload_bodies_by_range_v1 was called but no response configured. \ + Use with_payload_bodies_by_range_response() or set_payload_bodies_by_range_response() to set a response." + )) + }) + } + + async fn get_client_version_v1( + &self, + _client_version: ClientVersionV1, + ) -> TransportResult> { + let storage = self.storage.read().await; + storage.client_versions.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str("No client versions set in mock")) + }) + } + + async fn signal_superchain_v1( + &self, + _recommended: ProtocolVersion, + _required: ProtocolVersion, + ) -> TransportResult { + let storage = self.storage.read().await; + storage.protocol_version.ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str("No protocol version set in mock")) + }) + } + + async fn exchange_capabilities( + &self, + _capabilities: Vec, + ) -> TransportResult> { + let storage = self.storage.read().await; + storage.capabilities.clone().ok_or_else(|| { + TransportError::from(TransportErrorKind::custom_str("No capabilities set in mock")) + }) + } +} + +/// Helper function to convert BlockId to a string key for HashMap storage. +/// This is necessary because BlockId doesn't implement Hash. +fn block_id_to_key(block_id: &BlockId) -> String { + match block_id { + BlockId::Hash(hash) => format!("hash:{}", hash.block_hash), + BlockId::Number(num) => format!("number:{num}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_rpc_types_engine::PayloadStatusEnum; + + #[tokio::test] + async fn test_mock_engine_client_creation() { + let cfg = Arc::new(RollupConfig::default()); + + let mock = MockEngineClient::new(cfg.clone()); + + // Verify the config was set correctly + assert_eq!(mock.cfg().block_time, cfg.block_time); + } + + #[tokio::test] + async fn test_mock_payload_status() { + let cfg = Arc::new(RollupConfig::default()); + + let mock = MockEngineClient::new(cfg); + + let status = + PayloadStatus { status: PayloadStatusEnum::Valid, latest_valid_hash: Some(B256::ZERO) }; + + mock.set_new_payload_v2_response(status.clone()).await; + + // Create a minimal ExecutionPayloadInputV2 for testing + use alloy_primitives::{Bytes, U256}; + use alloy_rpc_types_engine::ExecutionPayloadV1; + let payload = ExecutionPayloadInputV2 { + execution_payload: ExecutionPayloadV1 { + parent_hash: B256::ZERO, + fee_recipient: Default::default(), + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Default::default(), + prev_randao: B256::ZERO, + block_number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + block_hash: B256::ZERO, + transactions: vec![], + }, + withdrawals: None, + }; + + let result = mock.new_payload_v2(payload).await.unwrap(); + + assert_eq!(result.status, status.status); + } + + #[tokio::test] + async fn test_mock_forkchoice_updated() { + let cfg = Arc::new(RollupConfig::default()); + + let mock = MockEngineClient::new(cfg); + + let fcu = ForkchoiceUpdated { + payload_status: PayloadStatus { + status: PayloadStatusEnum::Valid, + latest_valid_hash: Some(B256::ZERO), + }, + payload_id: None, + }; + + mock.set_fork_choice_updated_v2_response(fcu.clone()).await; + + let result = mock.fork_choice_updated_v2(ForkchoiceState::default(), None).await.unwrap(); + + assert_eq!(result.payload_status.status, fcu.payload_status.status); + } + + #[tokio::test] + async fn test_builder_pattern() { + let cfg = Arc::new(RollupConfig::default()); + let status = + PayloadStatus { status: PayloadStatusEnum::Valid, latest_valid_hash: Some(B256::ZERO) }; + + let mock = MockEngineClient::builder() + .with_config(cfg.clone()) + .with_new_payload_v2_response(status.clone()) + .build(); + + // Verify the config was set + assert_eq!(mock.cfg().block_time, cfg.block_time); + + // Create a minimal ExecutionPayloadInputV2 for testing + use alloy_primitives::{Bytes, U256}; + use alloy_rpc_types_engine::ExecutionPayloadV1; + let payload = ExecutionPayloadInputV2 { + execution_payload: ExecutionPayloadV1 { + parent_hash: B256::ZERO, + fee_recipient: Default::default(), + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Default::default(), + prev_randao: B256::ZERO, + block_number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + block_hash: B256::ZERO, + transactions: vec![], + }, + withdrawals: None, + }; + + // Verify the pre-configured response is returned + let result = mock.new_payload_v2(payload).await.unwrap(); + assert_eq!(result.status, status.status); + } +} diff --git a/kona/crates/node/engine/src/test_utils/engine_state.rs b/kona/crates/node/engine/src/test_utils/engine_state.rs new file mode 100644 index 0000000000000..214c998672191 --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/engine_state.rs @@ -0,0 +1,96 @@ +use crate::{EngineState, EngineSyncStateUpdate}; +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, b256}; +use kona_protocol::{BlockInfo, L2BlockInfo}; + +/// Builder for creating test EngineState instances with sensible defaults +#[derive(Debug)] +pub struct TestEngineStateBuilder { + unsafe_head: L2BlockInfo, + cross_unsafe_head: Option, + local_safe_head: Option, + safe_head: Option, + finalized_head: Option, + el_sync_finished: bool, +} + +impl TestEngineStateBuilder { + /// Creates a new builder with default values. + /// Default: all heads set to genesis block (block 0) + pub fn new() -> Self { + let genesis = L2BlockInfo { + block_info: BlockInfo { + number: 0, + hash: b256!("0000000000000000000000000000000000000000000000000000000000000000"), + parent_hash: B256::ZERO, + timestamp: 0, + }, + l1_origin: BlockNumHash::default(), + seq_num: 0, + }; + + Self { + unsafe_head: genesis, + cross_unsafe_head: None, + local_safe_head: None, + safe_head: None, + finalized_head: None, + el_sync_finished: true, + } + } + + /// Sets the unsafe head + pub const fn with_unsafe_head(mut self, block: L2BlockInfo) -> Self { + self.unsafe_head = block; + self + } + + /// Sets the cross-unsafe head + #[allow(dead_code)] + pub const fn with_cross_unsafe_head(mut self, block: L2BlockInfo) -> Self { + self.cross_unsafe_head = Some(block); + self + } + + /// Sets the safe head + pub const fn with_safe_head(mut self, block: L2BlockInfo) -> Self { + self.safe_head = Some(block); + self + } + + /// Sets the finalized head + pub const fn with_finalized_head(mut self, block: L2BlockInfo) -> Self { + self.finalized_head = Some(block); + self + } + + /// Sets whether EL sync is finished + #[allow(dead_code)] + pub const fn with_el_sync_finished(mut self, finished: bool) -> Self { + self.el_sync_finished = finished; + self + } + + /// Builds the EngineState + pub fn build(self) -> EngineState { + let mut state = EngineState::default(); + + // Set unsafe head (required) + state.sync_state = state.sync_state.apply_update(EngineSyncStateUpdate { + unsafe_head: Some(self.unsafe_head), + cross_unsafe_head: Some(self.cross_unsafe_head.unwrap_or(self.unsafe_head)), + local_safe_head: Some(self.local_safe_head.unwrap_or(self.unsafe_head)), + safe_head: Some(self.safe_head.unwrap_or(self.unsafe_head)), + finalized_head: Some(self.finalized_head.unwrap_or(self.unsafe_head)), + }); + + state.el_sync_finished = self.el_sync_finished; + state + } +} + +impl Default for TestEngineStateBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/kona/crates/node/engine/src/test_utils/misc.rs b/kona/crates/node/engine/src/test_utils/misc.rs new file mode 100644 index 0000000000000..f3e826f63a05a --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/misc.rs @@ -0,0 +1,17 @@ +use alloy_eips::BlockNumHash; +use alloy_primitives::B256; +use kona_protocol::{BlockInfo, L2BlockInfo}; + +/// Helper to create a test L2BlockInfo at a specific block number +pub fn test_block_info(number: u64) -> L2BlockInfo { + L2BlockInfo { + block_info: BlockInfo { + number, + hash: B256::random(), + parent_hash: B256::random(), + timestamp: number * 2, + }, + l1_origin: BlockNumHash::default(), + seq_num: 0, + } +} diff --git a/kona/crates/node/engine/src/test_utils/mod.rs b/kona/crates/node/engine/src/test_utils/mod.rs new file mode 100644 index 0000000000000..3a38fd42eb538 --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/mod.rs @@ -0,0 +1,16 @@ +mod attributes; +pub use attributes::TestAttributesBuilder; + +mod engine_client; +pub use engine_client::{ + MockEngineClient, MockEngineClientBuilder, MockEngineStorage, test_engine_client_builder, +}; + +mod engine_state; +pub use engine_state::TestEngineStateBuilder; + +mod misc; +pub use misc::test_block_info; + +mod provider; +pub use provider::{MockL1Provider, MockL2Provider}; diff --git a/kona/crates/node/engine/src/test_utils/provider.rs b/kona/crates/node/engine/src/test_utils/provider.rs new file mode 100644 index 0000000000000..d7d4adc51ac84 --- /dev/null +++ b/kona/crates/node/engine/src/test_utils/provider.rs @@ -0,0 +1,32 @@ +use alloy_network::Ethereum; +use alloy_provider::Provider; +use async_trait::async_trait; +use op_alloy_network::Optimism; + +/// Mock L1 Provider that implements the Provider trait for testing. +/// +/// This is a minimal no-op provider that satisfies the trait bounds required +/// by [`MockEngineClient`]. All provider methods return empty/default values. +#[derive(Debug, Clone)] +pub struct MockL1Provider; + +#[async_trait] +impl Provider for MockL1Provider { + fn root(&self) -> &alloy_provider::RootProvider { + unimplemented!("MockL1Provider does not support root()") + } +} + +/// Mock L2 Provider that implements the Provider trait for Optimism network. +/// +/// This is a minimal no-op provider that satisfies the trait bounds required +/// by [`MockEngineClient`]. All provider methods return empty/default values. +#[derive(Debug, Clone)] +pub struct MockL2Provider; + +#[async_trait] +impl Provider for MockL2Provider { + fn root(&self) -> &alloy_provider::RootProvider { + unimplemented!("MockL2Provider does not support root()") + } +} diff --git a/kona/crates/node/engine/src/versions.rs b/kona/crates/node/engine/src/versions.rs new file mode 100644 index 0000000000000..79317a4917110 --- /dev/null +++ b/kona/crates/node/engine/src/versions.rs @@ -0,0 +1,104 @@ +//! Engine API version selection based on Optimism hardfork activations. +//! +//! Automatically selects the appropriate Engine API method versions based on +//! the rollup configuration and block timestamps. Different Optimism hardforks +//! require different Engine API versions to support new features. +//! +//! # Version Mapping +//! +//! - **Bedrock, Canyon, Delta** → V2 methods +//! - **Ecotone (Cancun)** → V3 methods +//! - **Isthmus** → V4 methods +//! +//! Adapted from the [OP Node version providers](https://github.com/ethereum-optimism/optimism/blob/develop/op-node/rollup/types.go#L546). + +use kona_genesis::RollupConfig; + +/// Engine API version for `engine_forkchoiceUpdated` method calls. +/// +/// Selects between V2 and V3 based on hardfork activation. V3 is required +/// for Ecotone/Cancun and later hardforks to support new consensus features. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EngineForkchoiceVersion { + /// Version 2: Used for Bedrock, Canyon, and Delta hardforks. + V2, + /// Version 3: Required for Ecotone/Cancun and later hardforks. + V3, +} + +impl EngineForkchoiceVersion { + /// Returns the appropriate [`EngineForkchoiceVersion`] for the chain at the given attributes. + /// + /// Uses the [`RollupConfig`] to check which hardfork is active at the given timestamp. + pub fn from_cfg(cfg: &RollupConfig, timestamp: u64) -> Self { + if cfg.is_ecotone_active(timestamp) { + // Cancun+ + Self::V3 + } else { + // Bedrock, Canyon, Delta + Self::V2 + } + } +} + +/// Engine API version for `engine_newPayload` method calls. +/// +/// Progressive version selection based on hardfork activation: +/// - V2: Basic payload processing +/// - V3: Adds Cancun/Ecotone support +/// - V4: Adds Isthmus hardfork features +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EngineNewPayloadVersion { + /// Version 2: Basic payload processing for early hardforks. + V2, + /// Version 3: Adds Cancun/Ecotone consensus features. + V3, + /// Version 4: Adds Isthmus hardfork support. + V4, +} + +impl EngineNewPayloadVersion { + /// Returns the appropriate [`EngineNewPayloadVersion`] for the chain at the given timestamp. + /// + /// Uses the [`RollupConfig`] to check which hardfork is active at the given timestamp. + pub fn from_cfg(cfg: &RollupConfig, timestamp: u64) -> Self { + if cfg.is_isthmus_active(timestamp) { + Self::V4 + } else if cfg.is_ecotone_active(timestamp) { + // Cancun + Self::V3 + } else { + Self::V2 + } + } +} + +/// Engine API version for `engine_getPayload` method calls. +/// +/// Matches the payload version used for retrieval with the version +/// used during payload construction, ensuring API compatibility. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EngineGetPayloadVersion { + /// Version 2: Basic payload retrieval. + V2, + /// Version 3: Enhanced payload data for Cancun/Ecotone. + V3, + /// Version 4: Extended payload format for Isthmus. + V4, +} + +impl EngineGetPayloadVersion { + /// Returns the appropriate [`EngineGetPayloadVersion`] for the chain at the given timestamp. + /// + /// Uses the [`RollupConfig`] to check which hardfork is active at the given timestamp. + pub fn from_cfg(cfg: &RollupConfig, timestamp: u64) -> Self { + if cfg.is_isthmus_active(timestamp) { + Self::V4 + } else if cfg.is_ecotone_active(timestamp) { + // Cancun + Self::V3 + } else { + Self::V2 + } + } +} diff --git a/kona/crates/node/gossip/Cargo.toml b/kona/crates/node/gossip/Cargo.toml new file mode 100644 index 0000000000000..1ebba37ccd682 --- /dev/null +++ b/kona/crates/node/gossip/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "kona-gossip" +version = "0.1.2" +description = "Gossip protocol implementation for the OP Stack" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# Kona +kona-peers.workspace = true +kona-macros.workspace = true +kona-genesis.workspace = true +kona-disc.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-primitives = { workspace = true, features = ["k256", "getrandom"] } + +# Op Alloy +op-alloy-consensus = { workspace = true, features = ["k256"] } +op-alloy-rpc-types-engine = { workspace = true, features = ["std", "serde"] } + +# Networking +snap.workspace = true +futures.workspace = true +libp2p-stream.workspace = true +discv5 = { workspace = true, features = ["libp2p"] } +openssl = { workspace = true, features = ["vendored"] } +libp2p-identity = { workspace = true, features = ["secp256k1"] } +libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "noise", "gossipsub", "ping", "yamux", "identify"] } +ipnet = { workspace = true, features = ["serde"] } + +# Misc +serde.workspace = true +tokio.workspace = true +tracing.workspace = true +thiserror.workspace = true +serde_repr.workspace = true +lazy_static.workspace = true +derive_more = { workspace = true, features = ["display", "deref", "debug"] } + +# `metrics` feature +metrics = { workspace = true, optional = true } + +[dev-dependencies] +tempfile.workspace = true +multihash.workspace = true +serde_json.workspace = true +alloy-eips.workspace = true +alloy-chains.workspace = true + +rand = { workspace = true, features = ["thread_rng"] } +arbitrary = { workspace = true, features = ["derive"] } +alloy-primitives = { workspace = true, features = ["arbitrary"] } +alloy-rpc-types-engine = { workspace = true, features = ["std"] } +alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] } +op-alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] } + +[features] +default = [] +metrics = [ "dep:metrics", "kona-disc/metrics", "libp2p/metrics" ] diff --git a/kona/crates/node/gossip/src/behaviour.rs b/kona/crates/node/gossip/src/behaviour.rs new file mode 100644 index 0000000000000..49869f031ebab --- /dev/null +++ b/kona/crates/node/gossip/src/behaviour.rs @@ -0,0 +1,132 @@ +//! Network Behaviour Module. + +use derive_more::Debug; +use libp2p::{ + gossipsub::{Config, IdentTopic, MessageAuthenticity}, + swarm::NetworkBehaviour, +}; + +use crate::{Event, Handler}; + +/// An error that can occur when creating a [`Behaviour`]. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum BehaviourError { + /// The gossipsub behaviour creation failed. + #[error("gossipsub behaviour creation failed")] + GossipsubCreationFailed, + /// Subscription failed. + #[error("subscription failed")] + SubscriptionFailed, + /// Failed to set the peer score on the gossipsub. + #[error("{0}")] + PeerScoreFailed(String), +} + +/// Specifies the [`NetworkBehaviour`] of the node +#[derive(NetworkBehaviour, Debug)] +#[behaviour(out_event = "Event")] +pub struct Behaviour { + /// Responds to inbound pings and send outbound pings. + #[debug(skip)] + pub ping: libp2p::ping::Behaviour, + /// Enables gossipsub as the routing layer. + pub gossipsub: libp2p::gossipsub::Behaviour, + /// Enables the identify protocol. + #[debug(skip)] + pub identify: libp2p::identify::Behaviour, + /// Enables the sync request/response protocol. + /// See `` + #[debug(skip)] + pub sync_req_resp: libp2p_stream::Behaviour, +} + +impl Behaviour { + /// Configures the swarm behaviors, subscribes to the gossip topics, and returns a new + /// [`Behaviour`]. + pub fn new( + public_key: libp2p::identity::PublicKey, + cfg: Config, + handlers: &[Box], + ) -> Result { + let ping = libp2p::ping::Behaviour::default(); + + let mut gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, cfg) + .map_err(|_| BehaviourError::GossipsubCreationFailed)?; + + let identify = libp2p::identify::Behaviour::new( + libp2p::identify::Config::new("".to_string(), public_key) + .with_agent_version("kona".to_string()), + ); + + let sync_req_resp = libp2p_stream::Behaviour::new(); + + let subscriptions = handlers + .iter() + .flat_map(|handler| { + handler + .topics() + .iter() + .map(|topic| { + let topic = IdentTopic::new(topic.to_string()); + gossipsub + .subscribe(&topic) + .map_err(|_| BehaviourError::SubscriptionFailed)?; + Ok(topic.to_string()) + }) + .collect::>() + }) + .collect::, BehaviourError>>()?; + + if !subscriptions.is_empty() { + tracing::info!(target: "gossip", "Subscribed to topics:"); + } + for topic in subscriptions { + tracing::info!(target: "gossip", "-> {}", topic); + } + + Ok(Self { identify, ping, gossipsub, sync_req_resp }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{config, handler::BlockHandler}; + use alloy_chains::Chain; + use alloy_primitives::Address; + use kona_genesis::RollupConfig; + use libp2p::gossipsub::{IdentTopic, TopicHash}; + + fn op_mainnet_topics() -> Vec { + vec![ + IdentTopic::new("/optimism/10/0/blocks").hash(), + IdentTopic::new("/optimism/10/1/blocks").hash(), + IdentTopic::new("/optimism/10/2/blocks").hash(), + IdentTopic::new("/optimism/10/3/blocks").hash(), + ] + } + + #[test] + fn test_behaviour_no_handlers() { + let key = libp2p::identity::Keypair::generate_secp256k1(); + let cfg = config::default_config(); + let handlers = vec![]; + let _ = Behaviour::new(key.public(), cfg, &handlers).unwrap(); + } + + #[test] + fn test_behaviour_with_handlers() { + let key = libp2p::identity::Keypair::generate_secp256k1(); + let cfg = config::default_config(); + let (_, recv) = tokio::sync::watch::channel(Address::default()); + let block_handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + recv, + ); + let handlers: Vec> = vec![Box::new(block_handler)]; + let behaviour = Behaviour::new(key.public(), cfg, &handlers).unwrap(); + let mut topics = behaviour.gossipsub.topics().cloned().collect::>(); + topics.sort(); + assert_eq!(topics, op_mainnet_topics()); + } +} diff --git a/kona/crates/node/gossip/src/block_validity.rs b/kona/crates/node/gossip/src/block_validity.rs new file mode 100644 index 0000000000000..49aa85f04c958 --- /dev/null +++ b/kona/crates/node/gossip/src/block_validity.rs @@ -0,0 +1,1011 @@ +#[cfg(feature = "metrics")] +use std::time::Instant; +use std::time::SystemTime; + +use alloy_consensus::Block; +use alloy_eips::eip7685::EMPTY_REQUESTS_HASH; +use alloy_primitives::{Address, B256}; +use alloy_rpc_types_engine::{ExecutionPayloadV3, PayloadError}; +use kona_genesis::RollupConfig; +use libp2p::gossipsub::MessageAcceptance; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_rpc_types_engine::{ + OpExecutionPayload, OpExecutionPayloadV4, OpNetworkPayloadEnvelope, OpPayloadError, +}; + +use super::BlockHandler; +#[cfg(feature = "metrics")] +use crate::Metrics; + +/// Error that can occur when validating a block. +#[derive(Debug, thiserror::Error)] +pub enum BlockInvalidError { + /// The block has an invalid timestamp. + #[error("Invalid timestamp. Current: {current}, Received: {received}")] + Timestamp { + /// The current timestamp. + current: u64, + /// The received timestamp. + received: u64, + }, + /// The block has an invalid base fee per gas. + #[error("Base fee per gas overflow")] + BaseFeePerGasOverflow(#[from] PayloadError), + /// The block has an invalid hash. + #[error("Invalid block hash. Expected: {expected}, Received: {received}")] + BlockHash { + /// The expected block hash. + expected: B256, + /// The received block hash. + received: B256, + }, + /// The block has an invalid signature. + #[error("Invalid signature.")] + Signature, + /// The block has an invalid signer. + #[error("Invalid signer, expected: {expected}, received: {received}")] + Signer { + /// The expected signer. + expected: Address, + /// The received signer. + received: Address, + }, + /// Invalid block. + #[error(transparent)] + InvalidBlock(#[from] OpPayloadError), + /// The block has an invalid parent beacon block root. + #[error("Payload is on v3+ topic, but has empty parent beacon root")] + ParentBeaconRoot, + /// The block has an invalid blob gas used. + #[error("Payload is on v3+ topic, but has non-zero blob gas used")] + BlobGasUsed, + /// The block has an invalid excess blob gas. + #[error("Payload is on v3+ topic, but has non-zero excess blob gas")] + ExcessBlobGas, + /// The block has an invalid withdrawals root. + #[error("Payload is on v4+ topic, but has non-empty withdrawals root")] + WithdrawalsRoot, + /// Too many blocks were validated for the same height. + #[error("Too many blocks seen for height {height}")] + TooManyBlocks { + /// The height of the block. + height: u64, + }, + /// The block has already been seen. + #[error("Block seen before")] + BlockSeen { + /// The hash of the block. + block_hash: B256, + }, +} + +impl From for MessageAcceptance { + fn from(value: BlockInvalidError) -> Self { + // We only want to ignore blocks that we have already seen. + match value { + BlockInvalidError::BlockSeen { block_hash: _ } => Self::Ignore, + _ => Self::Reject, + } + } +} + +impl BlockHandler { + /// The maximum number of blocks to keep in the seen hashes map. + /// + /// Note: this value must be high enough to ensure we prevent replay attacks. + /// Ie, the entries pruned must be old enough blocks to be considered invalid + /// if new blocks for that height are received. + /// + /// This value is chosen to match `op-node` validator's lru cache size. + /// See: + pub const SEEN_HASH_CACHE_SIZE: usize = 1_000; + + /// The maximum number of blocks to keep per height. + /// This value is chosen according to the optimism specs: + /// + const MAX_BLOCKS_TO_KEEP: usize = 5; + + /// Determines if a block is valid. + /// + /// We validate the block according to the rules defined here: + /// + /// + /// The block encoding/compression are assumed to be valid at this point (they are first checked + /// in the handle). + pub fn block_valid( + &mut self, + envelope: &OpNetworkPayloadEnvelope, + ) -> Result<(), BlockInvalidError> { + // Start timing for the validation duration + #[cfg(feature = "metrics")] + let validation_start = Instant::now(); + + // Record total validation attempts + #[cfg(feature = "metrics")] + kona_macros::inc!(counter, Metrics::BLOCK_VALIDATION_TOTAL); + + // Record block version distribution + #[cfg(feature = "metrics")] + { + let version = match &envelope.payload { + OpExecutionPayload::V1(_) => "v1", + OpExecutionPayload::V2(_) => "v2", + OpExecutionPayload::V3(_) => "v3", + OpExecutionPayload::V4(_) => "v4", + }; + kona_macros::inc!(counter, Metrics::BLOCK_VERSION, "version" => version); + } + + let validation_result = self.validate_block_internal(envelope); + + // Record validation duration + #[cfg(feature = "metrics")] + { + let duration = validation_start.elapsed(); + kona_macros::record!( + histogram, + Metrics::BLOCK_VALIDATION_DURATION_SECONDS, + duration.as_secs_f64() + ); + } + + // Record success/failure metrics + match &validation_result { + Ok(()) => { + #[cfg(feature = "metrics")] + kona_macros::inc!(counter, Metrics::BLOCK_VALIDATION_SUCCESS); + } + Err(_err) => { + #[cfg(feature = "metrics")] + { + let reason = match _err { + BlockInvalidError::Timestamp { current, received } => { + if *received > *current + 5 { + "timestamp_future" + } else { + "timestamp_past" + } + } + BlockInvalidError::BlockHash { .. } => "invalid_hash", + BlockInvalidError::Signature => "invalid_signature", + BlockInvalidError::Signer { .. } => "invalid_signer", + BlockInvalidError::TooManyBlocks { .. } => "too_many_blocks", + BlockInvalidError::BlockSeen { .. } => "block_seen", + BlockInvalidError::InvalidBlock(_) => "invalid_block", + BlockInvalidError::ParentBeaconRoot => "parent_beacon_root", + BlockInvalidError::BlobGasUsed => "blob_gas_used", + BlockInvalidError::ExcessBlobGas => "excess_blob_gas", + BlockInvalidError::WithdrawalsRoot => "withdrawals_root", + BlockInvalidError::BaseFeePerGasOverflow(_) => "invalid_block", + }; + kona_macros::inc!(counter, Metrics::BLOCK_VALIDATION_FAILED, "reason" => reason); + } + } + } + + validation_result + } + + /// Internal validation logic extracted for cleaner metrics instrumentation. + fn validate_block_internal( + &mut self, + envelope: &OpNetworkPayloadEnvelope, + ) -> Result<(), BlockInvalidError> { + let current_timestamp = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + + // The timestamp is at most 5 seconds in the future. + let is_future = envelope.payload.timestamp() > current_timestamp + 5; + // The timestamp is at most 60 seconds in the past. + let is_past = envelope.payload.timestamp() < current_timestamp - 60; + + // CHECK: The timestamp is not too far in the future or past. + if is_future || is_past { + return Err(BlockInvalidError::Timestamp { + current: current_timestamp, + received: envelope.payload.timestamp(), + }); + } + + // CHECK: Ensure the block hash is valid. + let expected = envelope.payload.block_hash(); + let mut block: Block = envelope.payload.clone().try_into_block()?; + block.header.parent_beacon_block_root = envelope.parent_beacon_block_root; + // If isthmus is active, set the requests hash to the empty hash. + if self.rollup_config.is_isthmus_active(envelope.payload.timestamp()) { + block.header.requests_hash = Some(EMPTY_REQUESTS_HASH); + } + let received = block.header.hash_slow(); + if received != expected { + return Err(BlockInvalidError::BlockHash { expected, received }); + } + + // CHECK: The payload is valid for the specific version of this block. + self.validate_version_specific_payload(envelope)?; + + if let Some(seen_hashes_at_height) = + self.seen_hashes.get_mut(&envelope.payload.block_number()) + { + // CHECK: If more than [`Self::MAX_BLOCKS_TO_KEEP`] different blocks have been received + // for the same height, reject the block. + if seen_hashes_at_height.len() > Self::MAX_BLOCKS_TO_KEEP { + return Err(BlockInvalidError::TooManyBlocks { + height: envelope.payload.block_number(), + }); + } + + // CHECK: If the block has already been seen, ignore it. + if seen_hashes_at_height.contains(&envelope.payload.block_hash()) { + return Err(BlockInvalidError::BlockSeen { + block_hash: envelope.payload.block_hash(), + }); + } + } + + // CHECK: The signature is valid. + let msg = envelope.payload_hash.signature_message(self.rollup_config.l2_chain_id.id()); + let block_signer = *self.signer_recv.borrow(); + + // The block has a valid signature. + let Ok(msg_signer) = envelope.signature.recover_address_from_prehash(&msg) else { + return Err(BlockInvalidError::Signature); + }; + + // The block is signed by the expected signer (the unsafe block signer). + if msg_signer != block_signer { + return Err(BlockInvalidError::Signer { expected: block_signer, received: msg_signer }); + } + + self.seen_hashes + .entry(envelope.payload.block_number()) + .or_default() + .insert(envelope.payload.block_hash()); + + // Mark the block as seen. + if self.seen_hashes.len() >= Self::SEEN_HASH_CACHE_SIZE { + self.seen_hashes.pop_first(); + } + + Ok(()) + } + + /// Validate version specific contents of the payload. + fn validate_version_specific_payload( + &self, + envelope: &OpNetworkPayloadEnvelope, + ) -> Result<(), BlockInvalidError> { + // Validation for v1 payloads are mostly ensured by type-safety, by decoding the + // payload to the ExecutionPayloadV1 type: + // 1. The block should not have any withdrawals + // 2. The block should not have any withdrawals list + // 3. The block should not have any blob gas used + // 4. The block should not have any excess blob gas + // 5. The block should not have any withdrawals root + // 6. The block should not have any parent beacon block root (validated because ignored by + // the decoder, this causes a hash mismatch. See tests) + + // Same as v1, except: + // 1. The block should have an empty withdrawals list. This is checked during the call to + // [`OpExecutionPayload::try_into_block`]. + + // Same as v2, except: + // 1. The block should have a zero blob gas used + // 2. The block should have a zero excess blob gas + // 3. The block should have a non empty parent beacon block root + fn validate_v3( + rollup_config: &RollupConfig, + block: &ExecutionPayloadV3, + parent_beacon_block_root: Option, + ) -> Result<(), BlockInvalidError> { + // If Jovian is not active, the blob gas used should be zero. + if !rollup_config.is_jovian_active(block.timestamp()) && block.blob_gas_used != 0 { + return Err(BlockInvalidError::BlobGasUsed); + } + + if block.excess_blob_gas != 0 { + return Err(BlockInvalidError::ExcessBlobGas); + } + + if parent_beacon_block_root.is_none() { + return Err(BlockInvalidError::ParentBeaconRoot); + } + + Ok(()) + } + + // Same as v3, except: + // 1. The block should have an non-empty withdrawals root (checked by type-safety) + fn validate_v4( + rollup_config: &RollupConfig, + block: &OpExecutionPayloadV4, + parent_beacon_block_root: Option, + ) -> Result<(), BlockInvalidError> { + validate_v3(rollup_config, &block.payload_inner, parent_beacon_block_root) + } + + match &envelope.payload { + OpExecutionPayload::V1(_) => Ok(()), + OpExecutionPayload::V2(_) => Ok(()), + OpExecutionPayload::V3(payload) => { + validate_v3(&self.rollup_config, payload, envelope.parent_beacon_block_root) + } + OpExecutionPayload::V4(payload) => { + validate_v4(&self.rollup_config, payload, envelope.parent_beacon_block_root) + } + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use alloy_chains::Chain; + use alloy_consensus::{Block, EMPTY_OMMER_ROOT_HASH}; + use alloy_eips::{eip2718::Encodable2718, eip4895::Withdrawal}; + use alloy_primitives::{Address, B256, Bytes, Signature}; + use alloy_rlp::BufMut; + use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3}; + use arbitrary::{Arbitrary, Unstructured}; + use kona_genesis::RollupConfig; + use op_alloy_consensus::OpTxEnvelope; + use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadV4, PayloadHash}; + + fn valid_block() -> Block { + // Simulate some random data + let mut data = vec![0; 1024 * 1024]; + let mut rng = rand::rng(); + rand::Rng::fill(&mut rng, &mut data[..]); + + // Create unstructured data with the random bytes + let u = Unstructured::new(&data); + + // Generate a random instance of MyStruct + let mut block: Block = Block::arbitrary_take_rest(u).unwrap(); + + let transactions: Vec = + block.body.transactions().map(|tx| tx.encoded_2718().into()).collect(); + + let transactions_root = + alloy_consensus::proofs::ordered_trie_root_with_encoder(&transactions, |item, buf| { + buf.put_slice(item) + }); + + block.header.transactions_root = transactions_root; + + // We always need to set the base fee per gas to a positive value to ensure the block is + // valid. + block.header.base_fee_per_gas = + Some(block.header.base_fee_per_gas.unwrap_or_default().saturating_add(1)); + + let current_timestamp = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + block.header.timestamp = current_timestamp; + + block + } + + /// Make the block v1 compatible + fn v1_valid_block() -> Block { + let mut block = valid_block(); + block.header.withdrawals_root = None; + block.header.blob_gas_used = None; + block.header.excess_blob_gas = None; + block.header.parent_beacon_block_root = None; + block.header.requests_hash = None; + block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH; + block.header.difficulty = Default::default(); + block.header.nonce = Default::default(); + + block + } + + /// Make the block v2 compatible + pub(crate) fn v2_valid_block() -> Block { + let mut block = v1_valid_block(); + + block.body.withdrawals = Some(vec![].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + + block.header.withdrawals_root = Some(withdrawals_root); + + block + } + + /// Make the block v3 compatible + pub(crate) fn v3_valid_block() -> Block { + let mut block = valid_block(); + + block.body.withdrawals = Some(vec![].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + block.header.withdrawals_root = Some(withdrawals_root); + + block.header.blob_gas_used = Some(0); + block.header.excess_blob_gas = Some(0); + block.header.parent_beacon_block_root = + Some(block.header.parent_beacon_block_root.unwrap_or_default()); + + block.header.requests_hash = None; + block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH; + block.header.difficulty = Default::default(); + block.header.nonce = Default::default(); + + block + } + + /// Make the block v4 compatible + pub(crate) fn v4_valid_block() -> Block { + v3_valid_block() + } + + /// Generates a random valid block and ensure it is v1 compatible + #[test] + fn test_block_valid() { + let block = v1_valid_block(); + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + } + + /// Generates a random block with an invalid timestamp and ensure it is rejected + #[test] + fn test_block_invalid_timestamp_early() { + let mut block = v1_valid_block(); + + block.header.timestamp -= 61; + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Timestamp { .. }))); + } + + /// Generates a random block with an invalid timestamp and ensure it is rejected + #[test] + fn test_block_invalid_timestamp_too_far() { + let mut block = v1_valid_block(); + + block.header.timestamp += 6; + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Timestamp { .. }))); + } + + /// Generates a random block with an invalid hash and ensure it is rejected + #[test] + fn test_block_invalid_hash() { + let block = v1_valid_block(); + + let mut v1 = ExecutionPayloadV1::from_block_slow(&block); + + v1.block_hash = B256::ZERO; + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::BlockHash { .. }))); + } + + #[test] + fn test_cannot_validate_same_block_twice() { + let block = v1_valid_block(); + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::BlockSeen { .. }))); + } + + #[test] + fn test_cannot_have_too_many_blocks_for_the_same_height() { + let first_block = v1_valid_block(); + + let initial_height = first_block.header.number; + + let v1 = ExecutionPayloadV1::from_block_slow(&first_block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + + let next_payloads = (0..=BlockHandler::MAX_BLOCKS_TO_KEEP) + .map(|_| { + let mut block = v1_valid_block(); + // The blocks have the same height + block.header.number = initial_height; + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + } + }) + .collect::>(); + + for envelope in next_payloads[..next_payloads.len() - 1].iter() { + assert!(handler.block_valid(envelope).is_ok()); + } + + // The last envelope should fail + assert!(matches!( + handler.block_valid(next_payloads.last().unwrap()), + Err(BlockInvalidError::TooManyBlocks { .. }) + )); + } + + /// Blocks with invalid signatures should be rejected. + #[test] + fn test_invalid_signature() { + let block = v1_valid_block(); + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let mut envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + let mut signature_bytes = envelope.signature.as_bytes(); + signature_bytes[0] = !signature_bytes[0]; + envelope.signature = Signature::from_raw_array(&signature_bytes).unwrap(); + + assert!(handler.seen_hashes.is_empty()); + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Signature))); + } + + /// Blocks with invalid signers should be rejected. + #[test] + fn test_invalid_signer() { + let block = v1_valid_block(); + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let (_, unsafe_signer) = tokio::sync::watch::channel(Address::default()); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Signer { .. }))); + } + + /// If we specify a non empty parent beacon block root for blocks with v1/v2 payloads we + /// get a hash mismatch error because the decoder enforces that these versions of the execution + /// payload don't contain the parent beacon block root. + #[test] + fn test_v1_v2_block_invalid_parent_beacon_block_root() { + let block = v1_valid_block(); + + let v1 = ExecutionPayloadV1::from_block_slow(&block); + + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some(B256::ZERO), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::BlockHash { .. }))); + + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some(B256::ZERO), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::BlockHash { .. }))); + } + + #[test] + fn test_v2_block() { + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + } + + #[test] + fn test_v2_non_empty_withdrawals() { + let mut block = v2_valid_block(); + block.body.withdrawals = Some(vec![Withdrawal::default()].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + block.header.withdrawals_root = Some(withdrawals_root); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!( + handler.block_valid(&envelope), + Err(BlockInvalidError::InvalidBlock(OpPayloadError::NonEmptyL1Withdrawals)) + )); + } + + #[test] + fn test_v3_block() { + let block = v3_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + } + + #[test] + fn test_v3_non_empty_withdrawals() { + let mut block = v3_valid_block(); + block.body.withdrawals = Some(vec![Withdrawal::default()].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + block.header.withdrawals_root = Some(withdrawals_root); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!( + handler.block_valid(&envelope), + Err(BlockInvalidError::InvalidBlock(OpPayloadError::NonEmptyL1Withdrawals)) + )); + } + + #[test] + fn test_v3_gas_params() { + let mut block = v3_valid_block(); + block.header.blob_gas_used = Some(1); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::BlobGasUsed))); + + block.header.blob_gas_used = Some(0); + block.header.excess_blob_gas = Some(1); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::ExcessBlobGas))); + } + + #[test] + fn test_v4_block() { + let block = v4_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + let v4 = OpExecutionPayloadV4::from_v3_with_withdrawals_root( + v3, + block.withdrawals_root.unwrap(), + ); + + let payload = OpExecutionPayload::V4(v4); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + assert!(handler.block_valid(&envelope).is_ok()); + } + + #[test] + #[cfg(feature = "metrics")] + fn test_metrics_instrumentation() { + // This test verifies that metrics code compiles and doesn't panic + // The actual metric values would require a metrics registry setup in a real test + // environment + + use crate::Metrics; + + // Initialize metrics (this should not panic) + Metrics::init(); + + let block = v1_valid_block(); + let v1 = ExecutionPayloadV1::from_block_slow(&block); + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // Test successful validation metrics + assert!(handler.block_valid(&envelope).is_ok()); + + // Test failed validation metrics + let mut invalid_block = v1_valid_block(); + invalid_block.header.timestamp = 0; // Invalid timestamp + + let v1_invalid = ExecutionPayloadV1::from_block_slow(&invalid_block); + let payload_invalid = OpExecutionPayload::V1(v1_invalid); + let envelope_invalid = OpNetworkPayloadEnvelope { + payload: payload_invalid, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + // This should increment failure metrics + assert!(handler.block_valid(&envelope_invalid).is_err()); + } + + #[test] + fn test_metrics_feature_gating() { + // Verify the code compiles and runs without panics even when metrics feature is disabled + let block = v1_valid_block(); + let v1 = ExecutionPayloadV1::from_block_slow(&block); + let payload = OpExecutionPayload::V1(v1); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // Should work regardless of metrics feature + assert!(handler.block_valid(&envelope).is_ok()); + } +} diff --git a/kona/crates/node/gossip/src/builder.rs b/kona/crates/node/gossip/src/builder.rs new file mode 100644 index 0000000000000..c78cd13e63c2f --- /dev/null +++ b/kona/crates/node/gossip/src/builder.rs @@ -0,0 +1,218 @@ +//! A builder for the [`GossipDriver`]. + +use alloy_primitives::Address; +use kona_genesis::RollupConfig; +use kona_peers::{PeerMonitoring, PeerScoreLevel}; +use libp2p::{ + Multiaddr, StreamProtocol, SwarmBuilder, gossipsub::Config, identity::Keypair, + noise::Config as NoiseConfig, tcp::Config as TcpConfig, yamux::Config as YamuxConfig, +}; +use std::time::Duration; +use tokio::sync::watch::{self}; + +use crate::{Behaviour, BlockHandler, GaterConfig, GossipDriver, GossipDriverBuilderError}; + +/// A builder for the [`GossipDriver`]. +#[derive(Debug)] +pub struct GossipDriverBuilder { + /// The [`RollupConfig`] for the network. + rollup_config: RollupConfig, + /// The [`Keypair`] for the node. + keypair: Keypair, + /// The [`Multiaddr`] for the gossip driver to listen on. + gossip_addr: Multiaddr, + /// Unsafe block signer [`Address`]. + signer: Address, + /// The idle connection timeout as a [`Duration`]. + timeout: Option, + /// Sets the [`PeerScoreLevel`] for the [`Behaviour`]. + scoring: Option, + /// The [`Config`] for the [`Behaviour`]. + config: Option, + /// If set, the gossip layer will monitor peer scores and ban peers that are below a given + /// threshold. + peer_monitoring: Option, + /// The configuration for the connection gater. + gater_config: Option, + /// Topic scoring. Disabled by default. + topic_scoring: bool, +} + +impl GossipDriverBuilder { + /// Creates a new [`GossipDriverBuilder`]. + pub const fn new( + rollup_config: RollupConfig, + signer: Address, + gossip_addr: Multiaddr, + keypair: Keypair, + ) -> Self { + Self { + timeout: None, + keypair, + gossip_addr, + signer, + scoring: None, + config: None, + peer_monitoring: None, + gater_config: None, + rollup_config, + topic_scoring: false, + } + } + + /// Sets the configuration for the connection gater. + pub const fn with_gater_config(mut self, config: GaterConfig) -> Self { + self.gater_config = Some(config); + self + } + + /// Sets the [`RollupConfig`] for the network. + /// This is used to determine the topic to publish to. + pub fn with_rollup_config(mut self, rollup_config: RollupConfig) -> Self { + self.rollup_config = rollup_config; + self + } + + /// Sets topic scoring. + /// This is disabled by default. + pub const fn with_topic_scoring(mut self, topic_scoring: bool) -> Self { + self.topic_scoring = topic_scoring; + self + } + + /// Sets the [`PeerScoreLevel`] for the [`Behaviour`]. + pub const fn with_peer_scoring(mut self, level: PeerScoreLevel) -> Self { + self.scoring = Some(level); + self + } + + /// Sets the [`PeerMonitoring`] configuration for the gossip driver. + pub const fn with_peer_monitoring(mut self, peer_monitoring: Option) -> Self { + self.peer_monitoring = peer_monitoring; + self + } + + /// Sets the unsafe block signer [`Address`]. + pub const fn with_unsafe_block_signer_receiver(mut self, signer: Address) -> Self { + self.signer = signer; + self + } + + /// Sets the [`Keypair`] for the node. + pub fn with_keypair(mut self, keypair: Keypair) -> Self { + self.keypair = keypair; + self + } + + /// Sets the swarm's idle connection timeout. + pub const fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Sets the [`Multiaddr`] for the gossip driver to listen on. + pub fn with_address(mut self, addr: Multiaddr) -> Self { + self.gossip_addr = addr; + self + } + + /// Sets the [`Config`] for the [`Behaviour`]. + pub fn with_config(mut self, config: Config) -> Self { + self.config = Some(config); + self + } + + /// Builds the [`GossipDriver`]. + pub fn build( + mut self, + ) -> Result< + (GossipDriver, watch::Sender
), + GossipDriverBuilderError, + > { + // Extract builder arguments + let timeout = self.timeout.take().unwrap_or(Duration::from_secs(60)); + let keypair = self.keypair; + let addr = self.gossip_addr; + let signer_recv = self.signer; + let rollup_config = self.rollup_config; + let l2_chain_id = rollup_config.l2_chain_id; + let block_time = rollup_config.block_time; + + let (signer_tx, signer_rx) = watch::channel(signer_recv); + + // Block Handler setup + let handler = BlockHandler::new(rollup_config, signer_rx); + + // Construct the gossip behaviour + let config = self.config.unwrap_or(crate::default_config()); + info!( + target: "gossip", + "CONFIG: [Mesh D: {}] [Mesh L: {}] [Mesh H: {}] [Gossip Lazy: {}] [Flood Publish: {}]", + config.mesh_n(), + config.mesh_n_low(), + config.mesh_n_high(), + config.gossip_lazy(), + config.flood_publish() + ); + info!( + target: "gossip", + "CONFIG: [Heartbeat: {}] [Floodsub: {}] [Validation: {:?}] [Max Transmit: {} bytes]", + config.heartbeat_interval().as_secs(), + config.support_floodsub(), + config.validation_mode(), + config.max_transmit_size() + ); + let mut behaviour = Behaviour::new(keypair.public(), config, &[Box::new(handler.clone())])?; + + // If peer scoring is configured, set it on the behaviour. + match self.scoring { + None => info!(target: "scoring", "Peer scoring not enabled"), + Some(PeerScoreLevel::Off) => { + info!(target: "scoring", level = ?PeerScoreLevel::Off, "Peer scoring explicitly disabled") + } + Some(level) => { + use crate::handler::Handler; + let params = level + .to_params(handler.topics(), self.topic_scoring, block_time) + .unwrap_or_default(); + match behaviour.gossipsub.with_peer_score(params, PeerScoreLevel::thresholds()) { + Ok(_) => debug!(target: "scoring", "Peer scoring enabled successfully"), + Err(e) => warn!(target: "scoring", "Peer scoring failed: {}", e), + } + } + } + + // Let's setup the sync request/response protocol stream. + let mut sync_handler = behaviour.sync_req_resp.new_control(); + + let protocol = format!("/opstack/req/payload_by_number/{l2_chain_id}/0/"); + let sync_protocol_name = StreamProtocol::try_from_owned(protocol) + .map_err(|_| GossipDriverBuilderError::SetupSyncReqRespError)?; + let sync_protocol = sync_handler + .accept(sync_protocol_name) + .map_err(|_| GossipDriverBuilderError::SyncReqRespAlreadyAccepted)?; + + // Build the swarm. + debug!(target: "gossip", "Building Swarm with Peer ID: {}", keypair.public().to_peer_id()); + let swarm = SwarmBuilder::with_existing_identity(keypair) + .with_tokio() + .with_tcp( + TcpConfig::default().nodelay(true), + |i: &Keypair| { + debug!(target: "gossip", "Noise Config Peer ID: {}", i.public().to_peer_id()); + NoiseConfig::new(i) + }, + YamuxConfig::default, + ) + .map_err(|_| GossipDriverBuilderError::TcpError)? + .with_behaviour(|_| behaviour) + .map_err(|_| GossipDriverBuilderError::WithBehaviourError)? + .with_swarm_config(|c| c.with_idle_connection_timeout(timeout)) + .build(); + + let gater_config = self.gater_config.take().unwrap_or_default(); + let gate = crate::ConnectionGater::new(gater_config); + + Ok((GossipDriver::new(swarm, addr, handler, sync_handler, sync_protocol, gate), signer_tx)) + } +} diff --git a/kona/crates/node/gossip/src/config.rs b/kona/crates/node/gossip/src/config.rs new file mode 100644 index 0000000000000..0193d9d8f0088 --- /dev/null +++ b/kona/crates/node/gossip/src/config.rs @@ -0,0 +1,164 @@ +//! Gossipsub Config + +use lazy_static::lazy_static; +use libp2p::gossipsub::{Config, ConfigBuilder, Message, MessageId}; +use openssl::sha::sha256; +use snap::raw::Decoder; +use std::time::Duration; + +//////////////////////////////////////////////////////////////////////////////////////////////// +// GossipSub Constants +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// The maximum gossip size. +/// Limits the total size of gossip RPC containers as well as decompressed individual messages. +pub const MAX_GOSSIP_SIZE: usize = 10 * (1 << 20); + +/// The minimum gossip size. +/// Used to make sure that there is at least some data to validate the signature against. +pub const MIN_GOSSIP_SIZE: usize = 66; + +/// The maximum outbound queue. +pub const MAX_OUTBOUND_QUEUE: usize = 256; + +/// The maximum validate queue. +pub const MAX_VALIDATE_QUEUE: usize = 256; + +/// The global validate throttle. +pub const GLOBAL_VALIDATE_THROTTLE: usize = 512; + +/// The default mesh D. +pub const DEFAULT_MESH_D: usize = 8; + +/// The default mesh D low. +pub const DEFAULT_MESH_DLO: usize = 6; + +/// The default mesh D high. +pub const DEFAULT_MESH_DHI: usize = 12; + +/// The default mesh D lazy. +pub const DEFAULT_MESH_DLAZY: usize = 6; + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Duration Constants +//////////////////////////////////////////////////////////////////////////////////////////////// + +lazy_static! { + /// The gossip heartbeat. + pub static ref GOSSIP_HEARTBEAT: Duration = Duration::from_millis(500); + + /// The seen messages TTL. + /// Limits the duration that message IDs are remembered for gossip deduplication purposes. + pub static ref SEEN_MESSAGES_TTL: Duration = 130 * *GOSSIP_HEARTBEAT; + + /// The peer score inspect frequency. + /// The frequency at which peer scores are inspected. + pub static ref PEER_SCORE_INSPECT_FREQUENCY: Duration = 15 * Duration::from_secs(1); +} + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Config Building +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// Builds the default gossipsub configuration. +/// +/// Notable defaults: +/// - flood_publish: false (call `.flood_publish(true)` on the [ConfigBuilder] to enable) +/// - backoff_slack: 1 +/// - heart beat interval: 1 second +/// - peer exchange is disabled +/// - maximum byte size for gossip messages: 2048 bytes +/// +/// # Returns +/// +/// A [`ConfigBuilder`] with the default gossipsub configuration already set. +/// Call `.build()` on the returned builder to get the final [libp2p::gossipsub::Config]. +pub fn default_config_builder() -> ConfigBuilder { + let mut builder = ConfigBuilder::default(); + builder + .mesh_n(DEFAULT_MESH_D) + .mesh_n_low(DEFAULT_MESH_DLO) + .mesh_n_high(DEFAULT_MESH_DHI) + .gossip_lazy(DEFAULT_MESH_DLAZY) + .heartbeat_interval(*GOSSIP_HEARTBEAT) + .fanout_ttl(Duration::from_secs(60)) + .history_length(12) + .history_gossip(3) + .flood_publish(false) + .support_floodsub() + .max_transmit_size(MAX_GOSSIP_SIZE) + .duplicate_cache_time(Duration::from_secs(120)) + .validation_mode(libp2p::gossipsub::ValidationMode::None) + .validate_messages() + .message_id_fn(compute_message_id); + + builder +} + +/// Returns the default [Config] for gossipsub. +pub fn default_config() -> Config { + default_config_builder().build().expect("default gossipsub config must be valid") +} + +/// Computes the [MessageId] of a `gossipsub` message. +fn compute_message_id(msg: &Message) -> MessageId { + let mut decoder = Decoder::new(); + let id = decoder.decompress_vec(&msg.data).map_or_else( + |_| { + warn!(target: "cfg", "Failed to decompress message, using invalid snappy"); + let domain_invalid_snappy: Vec = vec![0x0, 0x0, 0x0, 0x0]; + sha256([domain_invalid_snappy.as_slice(), msg.data.as_slice()].concat().as_slice()) + [..20] + .to_vec() + }, + |data| { + let domain_valid_snappy: Vec = vec![0x1, 0x0, 0x0, 0x0]; + sha256([domain_valid_snappy.as_slice(), data.as_slice()].concat().as_slice())[..20] + .to_vec() + }, + ); + + MessageId(id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constructs_default_config() { + let cfg = default_config(); + assert_eq!(cfg.mesh_n(), DEFAULT_MESH_D); + assert_eq!(cfg.mesh_n_low(), DEFAULT_MESH_DLO); + assert_eq!(cfg.mesh_n_high(), DEFAULT_MESH_DHI); + } + + #[test] + fn test_compute_message_id_invalid_snappy() { + let msg = Message { + source: None, + data: vec![1, 2, 3, 4, 5], + sequence_number: None, + topic: libp2p::gossipsub::TopicHash::from_raw("test"), + }; + + let id = compute_message_id(&msg); + let hashed = sha256(&[&[0x0, 0x0, 0x0, 0x0], [1, 2, 3, 4, 5].as_slice()].concat()); + assert_eq!(id.0, hashed[..20].to_vec()); + } + + #[test] + fn test_compute_message_id_valid_snappy() { + let compressed = snap::raw::Encoder::new().compress_vec(&[1, 2, 3, 4, 5]).unwrap(); + let msg = Message { + source: None, + data: compressed, + sequence_number: None, + topic: libp2p::gossipsub::TopicHash::from_raw("test"), + }; + + let id = compute_message_id(&msg); + let hashed = sha256(&[&[0x1, 0x0, 0x0, 0x0], [1, 2, 3, 4, 5].as_slice()].concat()); + assert_eq!(id.0, hashed[..20].to_vec()); + } +} diff --git a/kona/crates/node/gossip/src/driver.rs b/kona/crates/node/gossip/src/driver.rs new file mode 100644 index 0000000000000..3dea19e9628cb --- /dev/null +++ b/kona/crates/node/gossip/src/driver.rs @@ -0,0 +1,487 @@ +//! Consensus-layer gossipsub driver for Optimism. + +use alloy_primitives::{Address, hex}; +use derive_more::Debug; +use discv5::Enr; +use futures::{AsyncReadExt, AsyncWriteExt, stream::StreamExt}; +use kona_genesis::RollupConfig; +use kona_peers::{EnrValidation, PeerMonitoring, enr_to_multiaddr}; +use libp2p::{ + Multiaddr, PeerId, Swarm, TransportError, + gossipsub::{IdentTopic, MessageId}, + swarm::SwarmEvent, +}; +use libp2p_identity::Keypair; +use libp2p_stream::IncomingStreams; +use op_alloy_rpc_types_engine::OpNetworkPayloadEnvelope; +use std::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::Mutex; + +use crate::{ + Behaviour, BlockHandler, ConnectionGate, ConnectionGater, Event, GossipDriverBuilder, Handler, + PublishError, +}; + +/// A driver for a [`Swarm`] instance. +/// +/// Connects the swarm to the given [`Multiaddr`] +/// and handles events using the [`BlockHandler`]. +#[derive(Debug)] +pub struct GossipDriver { + /// The [`Swarm`] instance. + #[debug(skip)] + pub swarm: Swarm, + /// A [`Multiaddr`] to listen on. + pub addr: Multiaddr, + /// The [`BlockHandler`]. + pub handler: BlockHandler, + /// A [`libp2p_stream::Control`] instance. Can be used to control the sync request/response + #[debug(skip)] + pub sync_handler: libp2p_stream::Control, + /// The inbound streams for the sync request/response protocol. + /// + /// This is an option to allow to take the underlying value when the gossip driver gets + /// activated. + /// + /// TODO(op-rs/kona#2141): remove the sync-req-resp protocol once the `op-node` phases it out. + #[debug(skip)] + pub sync_protocol: Option, + /// A mapping from [`PeerId`] to [`Multiaddr`]. + pub peerstore: HashMap, + /// If set, the gossip layer will monitor peer scores and ban peers that are below a given + /// threshold. + pub peer_monitoring: Option, + /// Tracks connection start time for peers + pub peer_connection_start: HashMap, + /// The connection gate. + pub connection_gate: G, + /// Tracks ping times for peers. + pub ping: Arc>>, +} + +impl GossipDriver +where + G: ConnectionGate, +{ + /// Returns the [`GossipDriverBuilder`] that can be used to construct the [`GossipDriver`]. + pub const fn builder( + rollup_config: RollupConfig, + signer: Address, + gossip_addr: Multiaddr, + keypair: Keypair, + ) -> GossipDriverBuilder { + GossipDriverBuilder::new(rollup_config, signer, gossip_addr, keypair) + } + + /// Creates a new [`GossipDriver`] instance. + pub fn new( + swarm: Swarm, + addr: Multiaddr, + handler: BlockHandler, + sync_handler: libp2p_stream::Control, + sync_protocol: IncomingStreams, + gate: G, + ) -> Self { + Self { + swarm, + addr, + handler, + peerstore: Default::default(), + peer_monitoring: None, + peer_connection_start: Default::default(), + sync_handler, + sync_protocol: Some(sync_protocol), + connection_gate: gate, + ping: Arc::new(Mutex::new(Default::default())), + } + } + + /// Publishes an unsafe block to gossip. + /// + /// ## Arguments + /// + /// * `topic_selector` - A function that selects the topic for the block. This is expected to be + /// a closure that takes the [`BlockHandler`] and returns the [`IdentTopic`] for the block. + /// * `payload` - The payload to be published. + /// + /// ## Returns + /// + /// Returns the [`MessageId`] of the published message or a [`PublishError`] + /// if the message could not be published. + pub fn publish( + &mut self, + selector: impl FnOnce(&BlockHandler) -> IdentTopic, + payload: Option, + ) -> Result, PublishError> { + let Some(payload) = payload else { + return Ok(None); + }; + let topic = selector(&self.handler); + let topic_hash = topic.hash(); + let data = self.handler.encode(topic, payload)?; + let id = self.swarm.behaviour_mut().gossipsub.publish(topic_hash, data)?; + kona_macros::inc!(gauge, crate::Metrics::UNSAFE_BLOCK_PUBLISHED); + Ok(Some(id)) + } + + /// Handles the sync request/response protocol. + /// + /// This is a mock handler that supports the `payload_by_number` protocol. + /// It always returns: not found (1), version (0). `` + /// + /// ## Note + /// + /// This is used to ensure op-nodes are not penalizing kona-nodes for not supporting it. + /// This feature is being deprecated by the op-node team. Once it is fully removed from the + /// op-node's implementation we will remove this handler. + pub(super) fn sync_protocol_handler(&mut self) { + let Some(mut sync_protocol) = self.sync_protocol.take() else { + return; + }; + + // Spawn a new task to handle the sync request/response protocol. + tokio::spawn(async move { + loop { + let Some((peer_id, mut inbound_stream)) = sync_protocol.next().await else { + warn!(target: "gossip", "The sync protocol stream has ended"); + return; + }; + + info!(target: "gossip", "Received a sync request from {peer_id}, spawning a new task to handle it"); + + tokio::spawn(async move { + let mut buffer = Vec::new(); + let Ok(bytes_received) = inbound_stream.read_to_end(&mut buffer).await else { + error!(target: "gossip", "Failed to read the sync request from {peer_id}"); + return; + }; + + debug!(target: "gossip", bytes_received = bytes_received, peer_id = ?peer_id, payload = ?buffer, "Received inbound sync request"); + + // We return: not found (1), version (0). `` + // Response format: = + // No payload is returned. + const OUTPUT: [u8; 2] = hex!("0100"); + + // We only write that we're not supporting the sync request. + if let Err(e) = inbound_stream.write_all(&OUTPUT).await { + error!(target: "gossip", err = ?e, "Failed to write the sync response to {peer_id}"); + return; + }; + + debug!(target: "gossip", bytes_sent = OUTPUT.len(), peer_id = ?peer_id, "Sent outbound sync response"); + }); + } + }); + } + + /// Starts the libp2p Swarm. + /// + /// - Starts the sync request/response protocol handler. + /// - Tells the swarm to listen on the given [`Multiaddr`]. + /// + /// Waits for the swarm to start listen before returning and connecting to peers. + pub async fn start(&mut self) -> Result> { + // Start the sync request/response protocol handler. + self.sync_protocol_handler(); + + match self.swarm.listen_on(self.addr.clone()) { + Ok(id) => loop { + if let SwarmEvent::NewListenAddr { address, listener_id } = + self.swarm.select_next_some().await + { + if id == listener_id { + info!(target: "gossip", "Swarm now listening on: {address}"); + + self.addr = address.clone(); + + return Ok(address); + } + } + }, + Err(err) => { + error!(target: "gossip", "Fail to listen on {}: {err}", self.addr); + Err(err) + } + } + } + + /// Returns the local peer id. + pub fn local_peer_id(&self) -> &libp2p::PeerId { + self.swarm.local_peer_id() + } + + /// Returns a mutable reference to the Swarm's behaviour. + pub fn behaviour_mut(&mut self) -> &mut Behaviour { + self.swarm.behaviour_mut() + } + + /// Attempts to select the next event from the Swarm. + pub async fn next(&mut self) -> Option> { + self.swarm.next().await + } + + /// Returns the number of connected peers. + pub fn connected_peers(&self) -> usize { + self.swarm.connected_peers().count() + } + + /// Dials the given [`Enr`]. + pub fn dial(&mut self, enr: Enr) { + let validation = EnrValidation::validate(&enr, self.handler.rollup_config.l2_chain_id.id()); + if validation.is_invalid() { + trace!(target: "gossip", "Invalid OP Stack ENR for chain id {}: {}", self.handler.rollup_config.l2_chain_id.id(), validation); + return; + } + let Some(multiaddr) = enr_to_multiaddr(&enr) else { + debug!(target: "gossip", "Failed to extract tcp socket from enr: {:?}", enr); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "invalid_enr"); + return; + }; + self.dial_multiaddr(multiaddr); + } + + /// Dials the given [`Multiaddr`]. + pub fn dial_multiaddr(&mut self, addr: Multiaddr) { + // Check if we're allowed to dial the address. + if let Err(dial_error) = self.connection_gate.can_dial(&addr) { + debug!(target: "gossip", ?dial_error, "unable to dial peer"); + return; + } + + // Extract the peer ID from the address. + let Some(peer_id) = ConnectionGater::peer_id_from_addr(&addr) else { + warn!(target: "gossip", peer=?addr, "Failed to extract PeerId from Multiaddr"); + return; + }; + + if self.swarm.connected_peers().any(|p| p == &peer_id) { + debug!(target: "gossip", peer=?addr, "Already connected to peer, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "already_connected", "peer" => peer_id.to_string()); + return; + } + + // Let the gate know we are dialing the address. + self.connection_gate.dialing(&addr); + + // Dial + match self.swarm.dial(addr.clone()) { + Ok(_) => { + trace!(target: "gossip", peer=?addr, "Dialed peer"); + self.connection_gate.dialed(&addr); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER, "peer" => peer_id.to_string()); + } + Err(e) => { + error!(target: "gossip", "Failed to connect to peer: {:?}", e); + self.connection_gate.remove_dial(&peer_id); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "connection_error", "error" => e.to_string(), "peer" => peer_id.to_string()); + } + } + } + + fn handle_gossip_event(&mut self, event: Event) -> Option { + match event { + Event::Gossipsub(e) => return self.handle_gossipsub_event(*e), + Event::Ping(libp2p::ping::Event { peer, result, .. }) => { + trace!(target: "gossip", ?peer, ?result, "Ping received"); + + // If the peer is connected to gossip, record the connection duration. + if let Some(start_time) = self.peer_connection_start.get(&peer) { + let _ping_duration = start_time.elapsed(); + kona_macros::record!( + histogram, + crate::Metrics::GOSSIP_PEER_CONNECTION_DURATION_SECONDS, + _ping_duration.as_secs_f64() + ); + } + + // Record the peer score in the metrics if available. + if let Some(_peer_score) = self.behaviour_mut().gossipsub.peer_score(&peer) { + kona_macros::record!( + histogram, + crate::Metrics::PEER_SCORES, + "peer", + peer.to_string(), + _peer_score + ); + } + + let pings = Arc::clone(&self.ping); + tokio::spawn(async move { + if let Ok(time) = result { + pings.lock().await.insert(peer, time); + } + }); + } + Event::Identify(e) => self.handle_identify_event(*e), + // Don't do anything with stream events as this should be unreachable code. + Event::Stream => { + error!(target: "gossip", "Stream events should not be emitted!"); + } + }; + + None + } + + fn handle_identify_event(&mut self, event: libp2p::identify::Event) { + match event { + libp2p::identify::Event::Received { connection_id, peer_id, info } => { + debug!(target: "gossip", ?connection_id, ?peer_id, ?info, "Received identify info from peer"); + self.peerstore.insert(peer_id, info); + } + libp2p::identify::Event::Sent { connection_id, peer_id } => { + debug!(target: "gossip", ?connection_id, ?peer_id, "Sent identify info to peer"); + } + libp2p::identify::Event::Pushed { connection_id, peer_id, info } => { + debug!(target: "gossip", ?connection_id, ?peer_id, ?info, "Pushed identify info to peer"); + } + libp2p::identify::Event::Error { connection_id, peer_id, error } => { + error!(target: "gossip", ?connection_id, ?peer_id, ?error, "Error raised while attempting to identify remote"); + } + } + } + + /// Handles a [`libp2p::gossipsub::Event`]. + fn handle_gossipsub_event( + &mut self, + event: libp2p::gossipsub::Event, + ) -> Option { + match event { + libp2p::gossipsub::Event::Message { + propagation_source: src, + message_id: id, + message, + } => { + trace!(target: "gossip", "Received message with topic: {}", message.topic); + kona_macros::inc!(gauge, crate::Metrics::GOSSIP_EVENT, "type" => "message", "topic" => message.topic.to_string()); + if self.handler.topics().contains(&message.topic) { + let (status, payload) = self.handler.handle(message); + _ = self + .swarm + .behaviour_mut() + .gossipsub + .report_message_validation_result(&id, &src, status); + return payload; + } + } + libp2p::gossipsub::Event::Subscribed { peer_id, topic } => { + trace!(target: "gossip", "Peer: {:?} subscribed to topic: {:?}", peer_id, topic); + kona_macros::inc!(gauge, crate::Metrics::GOSSIP_EVENT, "type" => "subscribed", "topic" => topic.to_string()); + } + libp2p::gossipsub::Event::Unsubscribed { peer_id, topic } => { + trace!(target: "gossip", "Peer: {:?} unsubscribed from topic: {:?}", peer_id, topic); + kona_macros::inc!(gauge, crate::Metrics::GOSSIP_EVENT, "type" => "unsubscribed", "topic" => topic.to_string()); + } + libp2p::gossipsub::Event::SlowPeer { peer_id, .. } => { + trace!(target: "gossip", "Slow peer: {:?}", peer_id); + kona_macros::inc!(gauge, crate::Metrics::GOSSIP_EVENT, "type" => "slow_peer", "peer" => peer_id.to_string()); + } + libp2p::gossipsub::Event::GossipsubNotSupported { peer_id } => { + trace!(target: "gossip", "Peer: {:?} does not support gossipsub", peer_id); + kona_macros::inc!(gauge, crate::Metrics::GOSSIP_EVENT, "type" => "not_supported", "peer" => peer_id.to_string()); + } + } + None + } + + /// Handles the [`SwarmEvent`]. + pub fn handle_event(&mut self, event: SwarmEvent) -> Option { + match event { + SwarmEvent::Behaviour(behavior_event) => { + return self.handle_gossip_event(behavior_event) + } + SwarmEvent::ConnectionEstablished { peer_id, .. } => { + let peer_count = self.swarm.connected_peers().count(); + info!(target: "gossip", "Connection established: {:?} | Peer Count: {}", peer_id, peer_count); + kona_macros::inc!( + gauge, + crate::Metrics::GOSSIPSUB_CONNECTION, + "type" => "connected", + "peer" => peer_id.to_string(), + ); + kona_macros::set!(gauge, crate::Metrics::GOSSIP_PEER_COUNT, peer_count as f64); + + self.peer_connection_start.insert(peer_id, Instant::now()); + } + SwarmEvent::OutgoingConnectionError { peer_id: _peer_id, error, .. } => { + debug!(target: "gossip", "Outgoing connection error: {:?}", error); + kona_macros::inc!( + gauge, + crate::Metrics::GOSSIPSUB_CONNECTION, + "type" => "outgoing_error", + "peer" => _peer_id.map(|p| p.to_string()).unwrap_or_default() + ); + } + SwarmEvent::IncomingConnectionError { + error, connection_id: _connection_id, .. + } => { + debug!(target: "gossip", "Incoming connection error: {:?}", error); + kona_macros::inc!( + gauge, + crate::Metrics::GOSSIPSUB_CONNECTION, + "type" => "incoming_error", + "connection_id" => _connection_id.to_string() + ); + } + SwarmEvent::ConnectionClosed { peer_id, cause, .. } => { + let peer_count = self.swarm.connected_peers().count(); + warn!(target: "gossip", ?peer_id, ?cause, peer_count, "Connection closed"); + kona_macros::inc!( + gauge, + crate::Metrics::GOSSIPSUB_CONNECTION, + "type" => "closed", + "peer" => peer_id.to_string() + ); + kona_macros::set!(gauge, crate::Metrics::GOSSIP_PEER_COUNT, peer_count as f64); + + // Record the total connection duration. + if let Some(start_time) = self.peer_connection_start.remove(&peer_id) { + let _peer_duration = start_time.elapsed(); + kona_macros::record!( + histogram, + crate::Metrics::GOSSIP_PEER_CONNECTION_DURATION_SECONDS, + _peer_duration.as_secs_f64() + ); + } + + // Record the peer score in the metrics if available. + if let Some(_peer_score) = self.behaviour_mut().gossipsub.peer_score(&peer_id) { + kona_macros::record!( + histogram, + crate::Metrics::PEER_SCORES, + "peer", + peer_id.to_string(), + _peer_score + ); + } + + let pings = Arc::clone(&self.ping); + tokio::spawn(async move { + pings.lock().await.remove(&peer_id); + }); + + // If the connection was initiated by us, remove the peer from the current dials + // set so that we can dial it again. + self.connection_gate.remove_dial(&peer_id); + } + SwarmEvent::NewListenAddr { listener_id, address } => { + debug!(target: "gossip", reporter_id = ?listener_id, new_address = ?address, "New listen address"); + } + SwarmEvent::Dialing { peer_id, connection_id } => { + debug!(target: "gossip", ?peer_id, ?connection_id, "Dialing peer"); + } + SwarmEvent::NewExternalAddrOfPeer { peer_id, address } => { + debug!(target: "gossip", ?peer_id, ?address, "New external address of peer"); + } + _ => { + debug!(target: "gossip", ?event, "Ignoring non-behaviour in event handler"); + } + }; + + None + } +} diff --git a/kona/crates/node/gossip/src/error.rs b/kona/crates/node/gossip/src/error.rs new file mode 100644 index 0000000000000..36b165af7bd74 --- /dev/null +++ b/kona/crates/node/gossip/src/error.rs @@ -0,0 +1,116 @@ +//! Error types for the gossip networking module. + +use crate::BehaviourError; +use derive_more::From; +use libp2p::{Multiaddr, PeerId}; +use std::net::IpAddr; +use thiserror::Error; + +/// Error encountered when publishing a payload to the gossip network. +/// +/// Represents failures in the payload publishing pipeline, including +/// network-level publishing errors and payload encoding issues. +#[derive(Debug, Error)] +pub enum PublishError { + /// Failed to publish the payload via GossipSub protocol. + /// + /// This can occur due to network connectivity issues, mesh topology + /// problems, or protocol-level errors in the libp2p stack. + #[error("Failed to publish payload: {0}")] + PublishError(#[from] libp2p::gossipsub::PublishError), + + /// Failed to encode the payload before publishing. + /// + /// Indicates an issue with serializing the payload data structure + /// into the binary format expected by the network protocol. + #[error("Failed to encode payload: {0}")] + EncodeError(#[from] HandlerEncodeError), +} + +/// Error encountered when encoding payloads in the block handler. +/// +/// Represents failures in the payload serialization process, typically +/// occurring when converting OP Stack data structures to network format. +#[derive(Debug, Error)] +pub enum HandlerEncodeError { + /// Failed to encode the OP Stack payload envelope. + /// + /// This error indicates issues with serializing the OP Stack network payload + /// structure, which contains the consensus data being gossiped. + #[error("Failed to encode payload: {0}")] + PayloadEncodeError(#[from] op_alloy_rpc_types_engine::PayloadEnvelopeEncodeError), + + /// Attempted to publish to an unknown or unsubscribed topic. + /// + /// This error occurs when trying to publish to a GossipSub topic that + /// is not recognized or that the node is not subscribed to. + #[error("Unknown topic: {0}")] + UnknownTopic(libp2p::gossipsub::TopicHash), +} + +/// An error type for the [`crate::GossipDriverBuilder`]. +#[derive(Debug, Clone, PartialEq, Eq, From, Error)] +pub enum GossipDriverBuilderError { + /// A TCP error. + #[error("TCP error")] + TcpError, + /// An error when setting the behaviour on the swarm builder. + #[error("error setting behaviour on swarm builder")] + WithBehaviourError, + /// An error when building the gossip behaviour. + #[error("error building gossip behaviour")] + BehaviourError(BehaviourError), + /// An error when setting up the sync request/response protocol. + #[error("error setting up sync request/response protocol")] + SetupSyncReqRespError, + /// The sync request/response protocol has already been accepted. + #[error("sync request/response protocol already accepted")] + SyncReqRespAlreadyAccepted, +} + +/// An error type representing reasons why a peer cannot be dialed. +#[derive(Debug, Clone, Error)] +pub enum DialError { + /// Failed to extract PeerId from Multiaddr. + #[error("Failed to extract PeerId from Multiaddr: {addr}")] + InvalidMultiaddr { + /// The multiaddress that failed to be parsed or does not contain a valid PeerId component + addr: Multiaddr, + }, + /// Already dialing this peer. + #[error("Already dialing peer: {peer_id}")] + AlreadyDialing { + /// The PeerId of the peer that is already being dialed + peer_id: PeerId, + }, + /// Dial threshold reached for this peer. + #[error("Dial threshold reached for peer: {addr}")] + ThresholdReached { + /// The multiaddress of the peer that has reached the maximum dial attempts + addr: Multiaddr, + }, + /// Peer is blocked. + #[error("Peer is blocked: {peer_id}")] + PeerBlocked { + /// The PeerId of the peer that is on the blocklist + peer_id: PeerId, + }, + /// Failed to extract IP address from Multiaddr. + #[error("Failed to extract IP address from Multiaddr: {addr}")] + InvalidIpAddress { + /// The multiaddress that does not contain a valid IP address component + addr: Multiaddr, + }, + /// IP address is blocked. + #[error("IP address is blocked: {ip}")] + AddressBlocked { + /// The IP address that is on the blocklist + ip: IpAddr, + }, + /// IP address is in a blocked subnet. + #[error("IP address {ip} is in a blocked subnet")] + SubnetBlocked { + /// The IP address that belongs to a blocked subnet range + ip: IpAddr, + }, +} diff --git a/kona/crates/node/gossip/src/event.rs b/kona/crates/node/gossip/src/event.rs new file mode 100644 index 0000000000000..c8e0c877bf357 --- /dev/null +++ b/kona/crates/node/gossip/src/event.rs @@ -0,0 +1,108 @@ +//! Event Handling Module. + +use libp2p::{gossipsub, identify, ping}; + +/// High-level events emitted by the gossip networking system. +/// +/// This enum wraps the various low-level libp2p events into a unified +/// event type that can be handled by the application layer. Events are +/// generated by the underlying libp2p protocols and bubble up through +/// the networking stack. +#[derive(Debug)] +pub enum Event { + /// Network connectivity check event from the ping protocol. + /// + /// Used to verify peer connectivity and measure round-trip times. + #[allow(dead_code)] + Ping(ping::Event), + + /// GossipSub mesh networking event. + /// + /// Includes message reception, peer subscription changes, and mesh + /// topology updates. This is the primary event type for consensus + /// layer networking. + Gossipsub(Box), + + /// Peer identification protocol event. + /// + /// Contains information about peer capabilities, supported protocols, + /// and network identity. Used for protocol negotiation and compatibility + /// checking. + Identify(Box), + + /// Stream protocol event for request-response communication. + /// + /// Handles direct peer-to-peer communication outside of the gossip mesh, + /// typically used for block synchronization requests. + Stream, +} + +impl From for Event { + /// Converts [ping::Event] to [Event] + fn from(value: ping::Event) -> Self { + Self::Ping(value) + } +} + +impl From for Event { + /// Converts [gossipsub::Event] to [Event] + fn from(value: gossipsub::Event) -> Self { + Self::Gossipsub(Box::new(value)) + } +} + +impl From for Event { + /// Converts [identify::Event] to [Event] + fn from(value: identify::Event) -> Self { + Self::Identify(Box::new(value)) + } +} + +impl From<()> for Event { + /// Converts () to [Event] + fn from(_value: ()) -> Self { + Self::Stream + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_event_conversion() { + let gossipsub_event = libp2p::gossipsub::Event::Message { + propagation_source: libp2p::PeerId::random(), + message_id: libp2p::gossipsub::MessageId(vec![]), + message: libp2p::gossipsub::Message { + source: None, + data: vec![], + sequence_number: None, + topic: libp2p::gossipsub::TopicHash::from_raw("test"), + }, + }; + let event = Event::from(gossipsub_event); + match event { + Event::Gossipsub(e) => { + if !matches!(*e, libp2p::gossipsub::Event::Message { .. }) { + panic!("Event conversion failed"); + } + } + _ => panic!("Event conversion failed"), + } + } + + #[test] + fn test_event_conversion_ping() { + let ping_event = ping::Event { + peer: libp2p::PeerId::random(), + connection: libp2p::swarm::ConnectionId::new_unchecked(0), + result: Ok(core::time::Duration::from_secs(1)), + }; + let event = Event::from(ping_event); + match event { + Event::Ping(_) => {} + _ => panic!("Event conversion failed"), + } + } +} diff --git a/kona/crates/node/gossip/src/gate.rs b/kona/crates/node/gossip/src/gate.rs new file mode 100644 index 0000000000000..9605feeb4666b --- /dev/null +++ b/kona/crates/node/gossip/src/gate.rs @@ -0,0 +1,71 @@ +//! Connection Gate for the libp2p Gossip Swarm. + +use crate::{Connectedness, DialError}; +use ipnet::IpNet; +use libp2p::{Multiaddr, PeerId}; +use std::net::IpAddr; + +/// Connection Gate +/// +/// The connection gate is used to isolate and abstract the +/// logic for which peers are allowed to connect to the +/// gossip swarm. +pub trait ConnectionGate { + /// Checks if a peer is allowed to connect to the gossip swarm. + /// Returns Ok(()) if the peer can be dialed, or Err(DialError) with the reason why not. + fn can_dial(&mut self, peer_id: &Multiaddr) -> Result<(), DialError>; + + /// Returns the [`Connectedness`] for a given peer id. + fn connectedness(&self, peer_id: &PeerId) -> Connectedness; + + /// Marks an address as currently being dialed. + fn dialing(&mut self, addr: &Multiaddr); + + /// Marks an address as dialed. + fn dialed(&mut self, addr: &Multiaddr); + + /// Removes a peer id from the current dials set. + fn remove_dial(&mut self, peer: &PeerId); + + /// Checks if a peer can be removed from the gossip swarm. + /// + /// Since peers can be protected from disconnection, this method + /// checks if the peer is protected or not. + fn can_disconnect(&self, peer_id: &Multiaddr) -> bool; + + /// Blocks a given peer id. + fn block_peer(&mut self, peer_id: &PeerId); + + /// Unblocks a given peer id. + fn unblock_peer(&mut self, peer_id: &PeerId); + + /// Lists the blocked peers. + fn list_blocked_peers(&self) -> Vec; + + /// Blocks a given ip address from connecting to the gossip swarm. + fn block_addr(&mut self, ip: IpAddr); + + /// Unblocks a given ip address, allowing it to connect to the gossip swarm. + fn unblock_addr(&mut self, ip: IpAddr); + + /// Lists all blocked ip addresses. + fn list_blocked_addrs(&self) -> Vec; + + /// Blocks a subnet from connecting to the gossip swarm. + fn block_subnet(&mut self, subnet: IpNet); + + /// Unblocks a subnet, allowing it to connect to the gossip swarm. + fn unblock_subnet(&mut self, subnet: IpNet); + + /// Lists all blocked subnets. + fn list_blocked_subnets(&self) -> Vec; + + /// Protects a peer from being disconnected. + fn protect_peer(&mut self, peer_id: PeerId); + + /// Unprotects a peer, allowing it to be disconnected. + fn unprotect_peer(&mut self, peer_id: PeerId); + + /// Lists all protected peers. + fn list_protected_peers(&self) -> Vec; +} diff --git a/kona/crates/node/gossip/src/gater.rs b/kona/crates/node/gossip/src/gater.rs new file mode 100644 index 0000000000000..3c871e4be447b --- /dev/null +++ b/kona/crates/node/gossip/src/gater.rs @@ -0,0 +1,557 @@ +//! An implementation of the [`ConnectionGate`] trait. + +use crate::{Connectedness, ConnectionGate, DialError}; +use ipnet::IpNet; +use libp2p::{Multiaddr, PeerId}; +use std::{ + collections::{HashMap, HashSet}, + net::{IpAddr, ToSocketAddrs}, + time::Duration, +}; +use tokio::time::Instant; + +/// Dial information tracking for peer connection management. +/// +/// Tracks connection attempt statistics for rate limiting and connection gating. +/// Used to prevent excessive connection attempts to the same peer within a +/// configured time window. +#[derive(Debug, Clone)] +pub struct DialInfo { + /// Number of times the peer has been dialed during the current dial period. + /// This number is reset once the last time the peer was dialed is longer than the dial period. + pub num_dials: u64, + /// The last time the peer was dialed. + pub last_dial: Instant, +} + +impl Default for DialInfo { + fn default() -> Self { + Self { num_dials: 0, last_dial: Instant::now() } + } +} + +/// Configuration parameters for the connection gater. +/// +/// Controls rate limiting, connection management, and peer protection policies +/// to maintain network health and prevent abuse. +#[derive(Debug, Clone)] +pub struct GaterConfig { + /// Maximum number of connection attempts per dial period for a single peer. + /// + /// If set to `None`, unlimited redials are allowed. When set, prevents + /// excessive connection attempts to unresponsive or problematic peers. + pub peer_redialing: Option, + + /// Duration of the rate limiting window for peer connections. + /// + /// A peer cannot be dialed more than `peer_redialing` times during this + /// period. The period resets after this duration has elapsed since the + /// last dial attempt. Default is 1 hour. + pub dial_period: Duration, +} + +impl Default for GaterConfig { + fn default() -> Self { + Self { peer_redialing: None, dial_period: Duration::from_secs(60 * 60) } + } +} + +/// Connection Gater +/// +/// A connection gate that regulates peer connections for the libp2p gossip swarm. +/// +/// An implementation of the [`ConnectionGate`] trait. +#[derive(Default, Debug, Clone)] +pub struct ConnectionGater { + /// The configuration for the connection gater. + config: GaterConfig, + /// A set of [`PeerId`]s that are currently being dialed. + pub current_dials: HashSet, + /// A mapping from [`Multiaddr`] to the dial info for the peer. + pub dialed_peers: HashMap, + /// Holds a map from peer id to connectedness for the given peer id. + pub connectedness: HashMap, + /// A set of protected peers that cannot be disconnected. + /// + /// Protecting a peer prevents the peer from any redial thresholds or peer scoring. + pub protected_peers: HashSet, + /// A set of blocked peer ids. + pub blocked_peers: HashSet, + /// A set of blocked ip addresses that cannot be dialed. + pub blocked_addrs: HashSet, + /// A set of blocked subnets that cannot be connected to. + pub blocked_subnets: HashSet, +} + +impl ConnectionGater { + /// Creates a new instance of the `ConnectionGater`. + pub fn new(config: GaterConfig) -> Self { + Self { + config, + current_dials: HashSet::new(), + dialed_peers: HashMap::new(), + connectedness: HashMap::new(), + protected_peers: HashSet::new(), + blocked_peers: HashSet::new(), + blocked_addrs: HashSet::new(), + blocked_subnets: HashSet::new(), + } + } + + /// Returns if the given [`Multiaddr`] has been dialed the maximum number of times. + pub fn dial_threshold_reached(&self, addr: &Multiaddr) -> bool { + // If the peer has not been dialed yet, the threshold is not reached. + let Some(dialed) = self.dialed_peers.get(addr) else { + return false; + }; + // If the peer has been dialed and the threshold is not set, the threshold is reached. + let Some(redialing) = self.config.peer_redialing else { + return true; + }; + // If the threshold is set to `0`, redial indefinitely. + if redialing == 0 { + return false; + } + if dialed.num_dials >= redialing { + return true; + } + false + } + + fn dial_period_expired(&self, addr: &Multiaddr) -> bool { + let Some(dial_info) = self.dialed_peers.get(addr) else { + return false; + }; + dial_info.last_dial.elapsed() > self.config.dial_period + } + + /// Gets the [`PeerId`] from a given [`Multiaddr`]. + pub fn peer_id_from_addr(addr: &Multiaddr) -> Option { + addr.iter().find_map(|component| match component { + libp2p::multiaddr::Protocol::P2p(peer_id) => Some(peer_id), + _ => None, + }) + } + + /// Constructs the [`IpAddr`] from the given [`Multiaddr`]. + pub fn ip_from_addr(addr: &Multiaddr) -> Option { + addr.iter().find_map(|component| match component { + libp2p::multiaddr::Protocol::Ip4(ip) => Some(IpAddr::V4(ip)), + libp2p::multiaddr::Protocol::Ip6(ip) => Some(IpAddr::V6(ip)), + _ => None, + }) + } + + /// Attempts to resolve a DNS-based [`Multiaddr`] to an [`IpAddr`]. + /// + /// Returns: + /// - `None` if the multiaddr does not contain a DNS component (use [`Self::ip_from_addr`]) + /// - `Some(Err(()))` if DNS resolution failed + /// - `Some(Ok(ip))` if DNS resolution succeeded + /// + /// Respects the DNS protocol type: `dns4` only returns IPv4, `dns6` only returns IPv6. + pub fn try_resolve_dns(addr: &Multiaddr) -> Option> { + // Track which DNS protocol type was used + let (hostname, ipv4_only, ipv6_only) = + addr.iter().find_map(|component| match component { + libp2p::multiaddr::Protocol::Dns(h) | libp2p::multiaddr::Protocol::Dnsaddr(h) => { + Some((h.to_string(), false, false)) + } + libp2p::multiaddr::Protocol::Dns4(h) => Some((h.to_string(), true, false)), + libp2p::multiaddr::Protocol::Dns6(h) => Some((h.to_string(), false, true)), + _ => None, + })?; + + debug!(target: "p2p", %hostname, ipv4_only, ipv6_only, "Resolving DNS hostname"); + + let ip = match format!("{hostname}:0").to_socket_addrs() { + Ok(addrs) => { + // Filter addresses based on DNS protocol type + addrs.map(|socket_addr| socket_addr.ip()).find(|ip| { + if ipv4_only { + ip.is_ipv4() + } else if ipv6_only { + ip.is_ipv6() + } else { + true + } + }) + } + Err(e) => { + warn!(target: "p2p", %hostname, error = %e, "DNS resolution failed"); + return Some(Err(())); + } + }; + + ip.map_or_else( + || { + warn!(target: "p2p", %hostname, "DNS resolution returned no matching addresses"); + Some(Err(())) + }, + |resolved_ip| { + debug!(target: "p2p", %hostname, %resolved_ip, "DNS resolution successful"); + Some(Ok(resolved_ip)) + }, + ) + } + + /// Checks if a given [`IpAddr`] is within any of the `blocked_subnets`. + pub fn check_ip_in_blocked_subnets(&self, ip_addr: &IpAddr) -> bool { + for subnet in &self.blocked_subnets { + if subnet.contains(ip_addr) { + return true; + } + } + false + } +} + +impl ConnectionGate for ConnectionGater { + fn can_dial(&mut self, addr: &Multiaddr) -> Result<(), DialError> { + // Get the peer id from the given multiaddr. + let peer_id = Self::peer_id_from_addr(addr).ok_or_else(|| { + warn!(target: "p2p", peer=?addr, "Failed to extract PeerId from Multiaddr"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "invalid_multiaddr"); + DialError::InvalidMultiaddr { addr: addr.clone() } + })?; + + // Cannot dial a peer that is already being dialed. + if self.current_dials.contains(&peer_id) { + debug!(target: "gossip", peer=?addr, "Already dialing peer, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "already_dialing", "peer" => peer_id.to_string()); + return Err(DialError::AlreadyDialing { peer_id }); + } + + // If the peer is protected, do not apply thresholds. + let protected = self.protected_peers.contains(&peer_id); + + // If the peer is not protected, its dial threshold is reached and dial period is not + // expired, do not dial. + if !protected && self.dial_threshold_reached(addr) && !self.dial_period_expired(addr) { + debug!(target: "gossip", peer=?addr, "Dial threshold reached, not dialing"); + self.connectedness.insert(peer_id, Connectedness::CannotConnect); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "threshold_reached", "peer" => peer_id.to_string()); + return Err(DialError::ThresholdReached { addr: addr.clone() }); + } + + // If the peer is blocked, do not dial. + if self.blocked_peers.contains(&peer_id) { + debug!(target: "gossip", peer=?addr, "Peer is blocked, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_peer", "peer" => peer_id.to_string()); + return Err(DialError::PeerBlocked { peer_id }); + } + + // Get IP address - either directly from multiaddr or by resolving DNS. + let ip_addr = match Self::try_resolve_dns(addr) { + Some(Ok(ip)) => { + debug!(target: "gossip", peer=?addr, resolved_ip=?ip, "Resolved DNS multiaddr"); + ip + } + Some(Err(())) => { + // DNS resolution failed - allow the dial, libp2p will handle it. + debug!(target: "gossip", peer=?addr, "DNS resolution failed, allowing dial"); + return Ok(()); + } + None => Self::ip_from_addr(addr).ok_or_else(|| { + warn!(target: "p2p", peer=?addr, "Failed to extract IpAddr from Multiaddr"); + DialError::InvalidIpAddress { addr: addr.clone() } + })?, + }; + + // If the address is blocked, do not dial. + if self.blocked_addrs.contains(&ip_addr) { + debug!(target: "gossip", peer=?addr, "Address is blocked, not dialing"); + self.connectedness.insert(peer_id, Connectedness::CannotConnect); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_address", "peer" => peer_id.to_string()); + return Err(DialError::AddressBlocked { ip: ip_addr }); + } + + // If address lies in any blocked subnets, do not dial. + if self.check_ip_in_blocked_subnets(&ip_addr) { + debug!(target: "gossip", ip=?ip_addr, "IP address is in a blocked subnet, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_subnet", "peer" => peer_id.to_string()); + return Err(DialError::SubnetBlocked { ip: ip_addr }); + } + + Ok(()) + } + + fn connectedness(&self, peer_id: &PeerId) -> Connectedness { + self.connectedness.get(peer_id).cloned().unwrap_or(Connectedness::NotConnected) + } + + fn list_protected_peers(&self) -> Vec { + self.protected_peers.iter().copied().collect() + } + + fn dialing(&mut self, addr: &Multiaddr) { + if let Some(peer_id) = Self::peer_id_from_addr(addr) { + self.current_dials.insert(peer_id); + self.connectedness.insert(peer_id, Connectedness::Connected); + } else { + warn!(target: "p2p", peer=?addr, "Failed to extract PeerId from Multiaddr when dialing"); + } + } + + fn dialed(&mut self, addr: &Multiaddr) { + let dial_info = self + .dialed_peers + .entry(addr.clone()) + .or_insert(DialInfo { num_dials: 0, last_dial: Instant::now() }); + + // If the last dial was longer than the dial period, reset the number of dials. + if dial_info.last_dial.elapsed() > self.config.dial_period { + dial_info.num_dials = 0; + } + + dial_info.num_dials += 1; + dial_info.last_dial = Instant::now(); + trace!(target: "gossip", peer=?addr, "Dialed peer, current count: {}", dial_info.num_dials); + } + + fn remove_dial(&mut self, peer_id: &PeerId) { + self.current_dials.remove(peer_id); + } + + fn can_disconnect(&self, addr: &Multiaddr) -> bool { + let Some(peer_id) = Self::peer_id_from_addr(addr) else { + warn!(target: "p2p", peer=?addr, "Failed to extract PeerId from Multiaddr when checking disconnect"); + // If we cannot extract the PeerId, disconnection is allowed. + return true; + }; + // If the peer is protected, do not disconnect. + if !self.protected_peers.contains(&peer_id) { + return true; + } + // Peer is protected, cannot disconnect. + false + } + + fn block_peer(&mut self, peer_id: &PeerId) { + self.blocked_peers.insert(*peer_id); + debug!(target: "gossip", peer=?peer_id, "Blocked peer"); + self.connectedness.insert(*peer_id, Connectedness::CannotConnect); + } + + fn unblock_peer(&mut self, peer_id: &PeerId) { + self.blocked_peers.remove(peer_id); + debug!(target: "gossip", peer=?peer_id, "Unblocked peer"); + self.connectedness.insert(*peer_id, Connectedness::NotConnected); + } + + fn list_blocked_peers(&self) -> Vec { + self.blocked_peers.iter().copied().collect() + } + + fn block_addr(&mut self, ip: IpAddr) { + self.blocked_addrs.insert(ip); + debug!(target: "gossip", ?ip, "Blocked ip address"); + } + + fn unblock_addr(&mut self, ip: IpAddr) { + self.blocked_addrs.remove(&ip); + debug!(target: "gossip", ?ip, "Unblocked ip address"); + } + + fn list_blocked_addrs(&self) -> Vec { + self.blocked_addrs.iter().cloned().collect() + } + + fn block_subnet(&mut self, subnet: IpNet) { + self.blocked_subnets.insert(subnet); + debug!(target: "gossip", ?subnet, "Blocked subnet"); + } + + fn unblock_subnet(&mut self, subnet: IpNet) { + self.blocked_subnets.remove(&subnet); + debug!(target: "gossip", ?subnet, "Unblocked subnet"); + } + + fn list_blocked_subnets(&self) -> Vec { + self.blocked_subnets.iter().copied().collect() + } + + fn protect_peer(&mut self, peer_id: PeerId) { + self.protected_peers.insert(peer_id); + debug!(target: "gossip", peer=?peer_id, "Protected peer"); + } + + fn unprotect_peer(&mut self, peer_id: PeerId) { + self.protected_peers.remove(&peer_id); + debug!(target: "gossip", peer=?peer_id, "Unprotected peer"); + } +} + +#[test] +fn test_check_ip_in_blocked_subnets_ipv4() { + use std::str::FromStr; + + let mut gater = ConnectionGater::new(GaterConfig { + peer_redialing: None, + dial_period: Duration::from_secs(60 * 60), + }); + gater.blocked_subnets.insert("192.168.1.0/24".parse::().unwrap()); + gater.blocked_subnets.insert("10.0.0.0/8".parse::().unwrap()); + gater.blocked_subnets.insert("172.16.0.0/16".parse::().unwrap()); + + // IP in blocked subnet + assert!(gater.check_ip_in_blocked_subnets(&IpAddr::from_str("192.168.1.100").unwrap())); + assert!(gater.check_ip_in_blocked_subnets(&IpAddr::from_str("10.0.0.5").unwrap())); + assert!(gater.check_ip_in_blocked_subnets(&IpAddr::from_str("172.16.255.255").unwrap())); + + // IP not in any blocked subnet + assert!(!gater.check_ip_in_blocked_subnets(&IpAddr::from_str("192.168.2.1").unwrap())); + assert!(!gater.check_ip_in_blocked_subnets(&IpAddr::from_str("172.17.0.1").unwrap())); + assert!(!gater.check_ip_in_blocked_subnets(&IpAddr::from_str("8.8.8.8").unwrap())); +} + +#[test] +fn test_dial_error_handling() { + use crate::{ConnectionGate, DialError}; + use std::str::FromStr; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // Test invalid multiaddr (missing peer ID) + let invalid_addr = Multiaddr::from_str("/ip4/127.0.0.1/tcp/8080").unwrap(); + let result = gater.can_dial(&invalid_addr); + assert!(matches!(result, Err(DialError::InvalidMultiaddr { .. }))); + + // Test with valid address + let valid_addr = Multiaddr::from_str( + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + // First dial should succeed + assert!(gater.can_dial(&valid_addr).is_ok()); + + // Mark as dialing + gater.dialing(&valid_addr); + + // Second dial should fail with AlreadyDialing + let result = gater.can_dial(&valid_addr); + assert!(matches!(result, Err(DialError::AlreadyDialing { .. }))); +} + +#[test] +fn test_dns_multiaddr_detection() { + use std::str::FromStr; + + // Test DNS4 multiaddr (try_resolve_dns returns Some for DNS addresses) + let dns4_addr = Multiaddr::from_str( + "/dns4/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns4_addr).is_some()); + + // Test DNS6 multiaddr + let dns6_addr = Multiaddr::from_str( + "/dns6/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns6_addr).is_some()); + + // Test DNS multiaddr (generic) + let dns_addr = Multiaddr::from_str( + "/dns/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns_addr).is_some()); + + // Test dnsaddr multiaddr + let dnsaddr = Multiaddr::from_str( + "/dnsaddr/example.com/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dnsaddr).is_some()); + + // Test IP4 multiaddr (should NOT be detected as DNS - returns None) + let ip4_addr = Multiaddr::from_str( + "/ip4/127.0.0.1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&ip4_addr).is_none()); + + // Test IP6 multiaddr (should NOT be detected as DNS - returns None) + let ip6_addr = Multiaddr::from_str( + "/ip6/::1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&ip6_addr).is_none()); +} + +#[test] +fn test_dns_multiaddr_can_dial() { + use crate::ConnectionGate; + use std::str::FromStr; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // DNS4 multiaddr should be allowed to dial (IP checks skipped) + let dns4_addr = Multiaddr::from_str( + "/dns4/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(gater.can_dial(&dns4_addr).is_ok()); + + // Real-world DNS multiaddr format (like the one from the issue) + let real_world_dns = Multiaddr::from_str( + "/dns4/alfonso-0-opn-reth-a-rpc-1-p2p.primary.infra.dev.oplabs.cloud/tcp/9003/p2p/16Uiu2HAmUSo81N6iNQNKZCiqDAg5Mcmh9gwvPgKmKj1HH6qCR4Kq", + ) + .unwrap(); + assert!(gater.can_dial(&real_world_dns).is_ok()); + + // DNS multiaddr with blocked peer should still be blocked + let peer_id = ConnectionGater::peer_id_from_addr(&dns4_addr).unwrap(); + gater.block_peer(&peer_id); + assert!(gater.can_dial(&dns4_addr).is_err()); +} + +#[test] +fn test_dns_multiaddr_blocked_by_resolved_ip() { + use crate::{ConnectionGate, DialError}; + use std::{net::IpAddr, str::FromStr}; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // localhost resolves to 127.0.0.1 + let dns_localhost = Multiaddr::from_str( + "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + // Should succeed before blocking + assert!(gater.can_dial(&dns_localhost).is_ok()); + + // Block 127.0.0.1 + gater.block_addr(IpAddr::from_str("127.0.0.1").unwrap()); + + // Should now fail because localhost resolves to blocked IP + let result = gater.can_dial(&dns_localhost); + assert!(matches!(result, Err(DialError::AddressBlocked { .. }))); +} + +#[test] +fn test_dns_multiaddr_blocked_by_subnet() { + use crate::{ConnectionGate, DialError}; + use std::str::FromStr; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // localhost resolves to 127.0.0.1 + let dns_localhost = Multiaddr::from_str( + "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + // Should succeed before blocking + assert!(gater.can_dial(&dns_localhost).is_ok()); + + // Block the 127.0.0.0/8 subnet + gater.block_subnet("127.0.0.0/8".parse().unwrap()); + + // Should now fail because localhost resolves to IP in blocked subnet + let result = gater.can_dial(&dns_localhost); + assert!(matches!(result, Err(DialError::SubnetBlocked { .. }))); +} diff --git a/kona/crates/node/gossip/src/handler.rs b/kona/crates/node/gossip/src/handler.rs new file mode 100644 index 0000000000000..368f2e3b7ac9d --- /dev/null +++ b/kona/crates/node/gossip/src/handler.rs @@ -0,0 +1,480 @@ +//! Block Handler + +use crate::HandlerEncodeError; +use alloy_primitives::{Address, B256}; +use kona_genesis::RollupConfig; +use libp2p::gossipsub::{IdentTopic, Message, MessageAcceptance, TopicHash}; +use op_alloy_rpc_types_engine::OpNetworkPayloadEnvelope; +use std::collections::{BTreeMap, HashSet}; +use tokio::sync::watch::Receiver; + +/// This trait defines the functionality required to process incoming messages +/// and determine their acceptance within the network. +/// +/// Implementors of this trait can specify how messages are handled and which +/// topics they are interested in. +pub trait Handler: Send { + /// Manages validation and further processing of messages + /// This is a stateful method, because the handler needs to keep track of seen hashes. + fn handle(&mut self, msg: Message) -> (MessageAcceptance, Option); + + /// Specifies which topics the handler is interested in + fn topics(&self) -> Vec; +} + +/// Responsible for managing blocks received via p2p gossip +#[derive(Debug, Clone)] +pub struct BlockHandler { + /// The rollup config used to validate the block. + pub rollup_config: RollupConfig, + /// A [`Receiver`] to monitor changes to the unsafe block signer. + pub signer_recv: Receiver
, + /// The libp2p topic for pre Canyon/Shangai blocks. + pub blocks_v1_topic: IdentTopic, + /// The libp2p topic for Canyon/Delta blocks. + pub blocks_v2_topic: IdentTopic, + /// The libp2p topic for Ecotone V3 blocks. + pub blocks_v3_topic: IdentTopic, + /// The libp2p topic for V4 blocks. + pub blocks_v4_topic: IdentTopic, + /// A map of seen block height to block hash set. + /// This map is pruned when it contains more than [`Self::SEEN_HASH_CACHE_SIZE`] entries. + pub seen_hashes: BTreeMap>, +} + +impl Handler for BlockHandler { + /// Checks validity of a [`OpNetworkPayloadEnvelope`] received over P2P gossip. + /// If valid, sends the [`OpNetworkPayloadEnvelope`] to the block update channel. + fn handle(&mut self, msg: Message) -> (MessageAcceptance, Option) { + let decoded = if msg.topic == self.blocks_v1_topic.hash() { + OpNetworkPayloadEnvelope::decode_v1(&msg.data) + } else if msg.topic == self.blocks_v2_topic.hash() { + OpNetworkPayloadEnvelope::decode_v2(&msg.data) + } else if msg.topic == self.blocks_v3_topic.hash() { + OpNetworkPayloadEnvelope::decode_v3(&msg.data) + } else if msg.topic == self.blocks_v4_topic.hash() { + OpNetworkPayloadEnvelope::decode_v4(&msg.data) + } else { + warn!(target: "gossip", topic = ?msg.topic, "Received block with unknown topic"); + return (MessageAcceptance::Reject, None); + }; + + match decoded { + Ok(envelope) => match self.block_valid(&envelope) { + Ok(()) => (MessageAcceptance::Accept, Some(envelope)), + Err(err) => { + warn!(target: "gossip", ?err, hash = ?envelope.payload_hash, "Received invalid block"); + (err.into(), None) + } + }, + Err(err) => { + warn!(target: "gossip", ?err, "Failed to decode block"); + (MessageAcceptance::Reject, None) + } + } + } + + /// The gossip topics accepted for new blocks + fn topics(&self) -> Vec { + vec![ + self.blocks_v1_topic.hash(), + self.blocks_v2_topic.hash(), + self.blocks_v3_topic.hash(), + self.blocks_v4_topic.hash(), + ] + } +} + +impl BlockHandler { + /// Creates a new [`BlockHandler`]. + /// + /// Requires the chain ID and a receiver channel for the unsafe block signer. + pub fn new(rollup_config: RollupConfig, signer_recv: Receiver
) -> Self { + let chain_id = rollup_config.l2_chain_id.id(); + Self { + rollup_config, + signer_recv, + blocks_v1_topic: IdentTopic::new(format!("/optimism/{chain_id}/0/blocks")), + blocks_v2_topic: IdentTopic::new(format!("/optimism/{chain_id}/1/blocks")), + blocks_v3_topic: IdentTopic::new(format!("/optimism/{chain_id}/2/blocks")), + blocks_v4_topic: IdentTopic::new(format!("/optimism/{chain_id}/3/blocks")), + seen_hashes: BTreeMap::new(), + } + } + + /// Returns the topic using the specified timestamp and optional [`RollupConfig`]. + /// + /// Reference: + pub fn topic(&self, timestamp: u64) -> IdentTopic { + if self.rollup_config.is_isthmus_active(timestamp) { + self.blocks_v4_topic.clone() + } else if self.rollup_config.is_ecotone_active(timestamp) { + self.blocks_v3_topic.clone() + } else if self.rollup_config.is_canyon_active(timestamp) { + self.blocks_v2_topic.clone() + } else { + self.blocks_v1_topic.clone() + } + } + + /// Encodes a [`OpNetworkPayloadEnvelope`] into a byte array + /// based on the specified topic. + pub fn encode( + &self, + topic: IdentTopic, + envelope: OpNetworkPayloadEnvelope, + ) -> Result, HandlerEncodeError> { + let encoded = match topic.hash() { + hash if hash == self.blocks_v1_topic.hash() => envelope.encode_v1()?, + hash if hash == self.blocks_v2_topic.hash() => envelope.encode_v2()?, + hash if hash == self.blocks_v3_topic.hash() => envelope.encode_v3()?, + hash if hash == self.blocks_v4_topic.hash() => envelope.encode_v4()?, + hash => return Err(HandlerEncodeError::UnknownTopic(hash)), + }; + Ok(encoded) + } +} + +#[cfg(test)] +mod tests { + use alloy_chains::Chain; + use alloy_rpc_types_engine::{ExecutionPayloadV2, ExecutionPayloadV3}; + use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadV4, PayloadHash}; + + use crate::{v2_valid_block, v3_valid_block, v4_valid_block}; + + use super::*; + use alloy_primitives::{B256, Signature}; + + #[test] + fn test_valid_decode() { + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // TRICK: Since the decode method recomputes the payload hash, we need to change the unsafe + // signer in the handler to ensure that the payload won't be rejected for invalid + // signature. + let encoded = handler.encode(handler.blocks_v2_topic.clone(), envelope).unwrap(); + let decoded = OpNetworkPayloadEnvelope::decode_v2(&encoded).unwrap(); + + let msg = decoded.payload_hash.signature_message(10); + let signer = decoded.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + handler.signer_recv = unsafe_signer; + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + topic: handler.blocks_v2_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Accept)); + } + + /// This payload has a wrong hash so the signature won't be valid. + #[test] + fn test_invalid_decode_payload_hash() { + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + topic: handler.blocks_v2_topic.clone().into(), + data: handler.encode(handler.blocks_v2_topic.clone(), envelope).unwrap(), + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Reject)); + } + + /// The message contains a wrong version so the payload won't be properly decoded. + #[test] + fn test_invalid_decode_version_mismatch() { + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: None, + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + let encoded = handler.encode(handler.blocks_v2_topic.clone(), envelope).unwrap(); + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + // Version mismatch! + topic: handler.blocks_v1_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Reject)); + } + + /// The message contains a wrong version so the payload won't be properly decoded. + #[test] + fn test_invalid_decode_version_mismatch_v3_with_v2() { + let block = v3_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + let encoded = handler.encode(handler.blocks_v3_topic.clone(), envelope).unwrap(); + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + // Version mismatch! + topic: handler.blocks_v2_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Reject)); + } + + /// The message contains a wrong version so the payload won't be properly decoded. + #[test] + fn test_invalid_decode_version_mismatch_v2_with_v3() { + let block = v2_valid_block(); + + let v2 = ExecutionPayloadV2::from_block_slow(&block); + + let payload = OpExecutionPayload::V2(v2); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + let encoded = handler.encode(handler.blocks_v2_topic.clone(), envelope).unwrap(); + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + // Version mismatch! + topic: handler.blocks_v3_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Reject)); + } + + /// The message contains a wrong version so the payload won't be properly decoded. + #[test] + fn test_invalid_decode_version_mismatch_v4_with_v3() { + let block = v4_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + let v4 = OpExecutionPayloadV4::from_v3_with_withdrawals_root( + v3, + block.withdrawals_root.unwrap(), + ); + + let payload = OpExecutionPayload::V4(v4); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + let encoded = handler.encode(handler.blocks_v4_topic.clone(), envelope).unwrap(); + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + // Version mismatch! + topic: handler.blocks_v3_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Reject)); + } + + #[test] + fn test_valid_decode_v4() { + let block = v4_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + let v4 = OpExecutionPayloadV4::from_v3_with_withdrawals_root( + v3, + block.withdrawals_root.unwrap(), + ); + + let payload = OpExecutionPayload::V4(v4); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // TRICK: Since the decode method recomputes the payload hash, we need to change the unsafe + // signer in the handler to ensure that the payload won't be rejected for invalid + // signature. + let encoded = handler.encode(handler.blocks_v4_topic.clone(), envelope).unwrap(); + let decoded = OpNetworkPayloadEnvelope::decode_v4(&encoded).unwrap(); + + let msg = decoded.payload_hash.signature_message(10); + let signer = decoded.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + handler.signer_recv = unsafe_signer; + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + topic: handler.blocks_v4_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Accept)); + } + + #[test] + fn test_valid_decode_v3() { + let block = v3_valid_block(); + + let v3 = ExecutionPayloadV3::from_block_slow(&block); + + let payload = OpExecutionPayload::V3(v3); + let envelope = OpNetworkPayloadEnvelope { + payload, + signature: Signature::test_signature(), + payload_hash: PayloadHash(B256::ZERO), + parent_beacon_block_root: Some( + block.header.parent_beacon_block_root.unwrap_or_default(), + ), + }; + + let msg = envelope.payload_hash.signature_message(10); + let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + let mut handler = BlockHandler::new( + RollupConfig { l2_chain_id: Chain::optimism_mainnet(), ..Default::default() }, + unsafe_signer, + ); + + // TRICK: Since the decode method recomputes the payload hash, we need to change the unsafe + // signer in the handler to ensure that the payload won't be rejected for invalid + // signature. + let encoded = handler.encode(handler.blocks_v3_topic.clone(), envelope).unwrap(); + let decoded = OpNetworkPayloadEnvelope::decode_v3(&encoded).unwrap(); + + let msg = decoded.payload_hash.signature_message(10); + let signer = decoded.signature.recover_address_from_prehash(&msg).unwrap(); + let (_, unsafe_signer) = tokio::sync::watch::channel(signer); + handler.signer_recv = unsafe_signer; + + // Let's try to encode a message. + let message = Message { + source: None, + sequence_number: None, + topic: handler.blocks_v3_topic.clone().into(), + data: encoded, + }; + + assert!(matches!(handler.handle(message).0, MessageAcceptance::Accept)); + } +} diff --git a/kona/crates/node/gossip/src/lib.rs b/kona/crates/node/gossip/src/lib.rs new file mode 100644 index 0000000000000..79a695e8c4b54 --- /dev/null +++ b/kona/crates/node/gossip/src/lib.rs @@ -0,0 +1,76 @@ +//! Gossip protocol implementation for the OP Stack. +//! +//! This crate provides a comprehensive gossip networking implementation for the OP Stack, +//! including GossipSub-based consensus layer networking, RPC interfaces for network +//! administration, and metrics collection. +//! +//! ## Key Components +//! +//! - [`GossipDriver`]: Main driver managing the libp2p swarm and event handling +//! - [`Behaviour`]: Custom libp2p behavior combining GossipSub, Ping, and Identify +//! - [`BlockHandler`]: Validates and processes incoming block payloads +//! - [`ConnectionGater`]: Sophisticated connection management and rate limiting +//! - [`P2pRpcRequest`]: RPC interface for network administration +//! - [`Metrics`]: Metrics collection for monitoring and observability + +#![doc(html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/kona-logo.png")] +#![doc(issue_tracker_base_url = "https://github.com/op-rs/kona/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +#[macro_use] +extern crate tracing; +// Used in tests +#[allow(unused_extern_crates)] +extern crate alloy_rlp; + +mod metrics; +pub use metrics::Metrics; + +mod rpc; +pub use rpc::{ + Connectedness, Direction, GossipScores, P2pRpcRequest, PeerCount, PeerDump, PeerInfo, + PeerScores, PeerStats, ReqRespScores, TopicScores, +}; + +mod behaviour; +pub use behaviour::{Behaviour, BehaviourError}; + +mod config; +pub use config::{ + DEFAULT_MESH_D, DEFAULT_MESH_DHI, DEFAULT_MESH_DLAZY, DEFAULT_MESH_DLO, + GLOBAL_VALIDATE_THROTTLE, GOSSIP_HEARTBEAT, MAX_GOSSIP_SIZE, MAX_OUTBOUND_QUEUE, + MAX_VALIDATE_QUEUE, MIN_GOSSIP_SIZE, PEER_SCORE_INSPECT_FREQUENCY, SEEN_MESSAGES_TTL, + default_config, default_config_builder, +}; + +mod gate; +pub use gate::ConnectionGate; // trait + +mod gater; +pub use gater::{ + ConnectionGater, // implementation + DialInfo, + GaterConfig, +}; + +mod builder; +pub use builder::GossipDriverBuilder; + +mod error; +pub use error::{DialError, GossipDriverBuilderError, HandlerEncodeError, PublishError}; + +mod event; +pub use event::Event; + +mod handler; +pub use handler::{BlockHandler, Handler}; + +mod driver; +pub use driver::GossipDriver; + +mod block_validity; +pub use block_validity::BlockInvalidError; + +#[cfg(test)] +pub(crate) use block_validity::tests::*; diff --git a/kona/crates/node/gossip/src/metrics/mod.rs b/kona/crates/node/gossip/src/metrics/mod.rs new file mode 100644 index 0000000000000..733f4df7eb7a5 --- /dev/null +++ b/kona/crates/node/gossip/src/metrics/mod.rs @@ -0,0 +1,207 @@ +//! Metrics for the Gossip stack. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the gauge that tracks gossip events. + pub const GOSSIP_EVENT: &str = "kona_node_gossip_events"; + + /// Identifier for the gauge that tracks libp2p gossipsub events. + pub const GOSSIPSUB_EVENT: &str = "kona_node_gossipsub_events"; + + /// Identifier for the gauge that tracks libp2p gossipsub connections. + pub const GOSSIPSUB_CONNECTION: &str = "kona_node_gossipsub_connection"; + + /// Identifier for the gauge that tracks unsafe blocks published. + pub const UNSAFE_BLOCK_PUBLISHED: &str = "kona_node_unsafe_block_published"; + + /// Identifier for the gauge that tracks the number of connected peers. + pub const GOSSIP_PEER_COUNT: &str = "kona_node_swarm_peer_count"; + + /// Identifier for the gauge that tracks the number of dialed peers. + pub const DIAL_PEER: &str = "kona_node_dial_peer"; + + /// Identifier for the gauge that tracks the number of errors when dialing peers. + pub const DIAL_PEER_ERROR: &str = "kona_node_dial_peer_error"; + + /// Identifier for the gauge that tracks RPC calls. + pub const RPC_CALLS: &str = "kona_node_rpc_calls"; + + /// Identifier for a gauge that tracks the number of banned peers. + pub const BANNED_PEERS: &str = "kona_node_banned_peers"; + + /// Identifier for a histogram that tracks peer scores. + pub const PEER_SCORES: &str = "kona_node_peer_scores"; + + /// Identifier for the gauge that tracks the duration of peer connections in seconds. + pub const GOSSIP_PEER_CONNECTION_DURATION_SECONDS: &str = + "kona_node_gossip_peer_connection_duration_seconds"; + + /// Identifier for the counter that tracks total block validation attempts. + pub const BLOCK_VALIDATION_TOTAL: &str = "kona_node_block_validation_total"; + + /// Identifier for the counter that tracks successful block validations. + pub const BLOCK_VALIDATION_SUCCESS: &str = "kona_node_block_validation_success"; + + /// Identifier for the counter that tracks failed block validations by reason. + pub const BLOCK_VALIDATION_FAILED: &str = "kona_node_block_validation_failed"; + + /// Identifier for the histogram that tracks block validation duration in seconds. + pub const BLOCK_VALIDATION_DURATION_SECONDS: &str = + "kona_node_block_validation_duration_seconds"; + + /// Identifier for the counter that tracks block version distribution. + pub const BLOCK_VERSION: &str = "kona_node_block_version"; + + /// Initializes metrics for the Gossip stack. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in [`kona_gossip`][crate]. + #[cfg(feature = "metrics")] + pub fn describe() { + metrics::describe_gauge!(Self::RPC_CALLS, "Calls made to the Gossip RPC module"); + metrics::describe_gauge!( + Self::GOSSIPSUB_EVENT, + "Events received by the libp2p gossipsub Swarm" + ); + metrics::describe_gauge!(Self::DIAL_PEER, "Number of peers dialed by the libp2p Swarm"); + metrics::describe_gauge!( + Self::UNSAFE_BLOCK_PUBLISHED, + "Number of OpNetworkPayloadEnvelope gossipped out through the libp2p Swarm" + ); + metrics::describe_gauge!( + Self::GOSSIP_PEER_COUNT, + "Number of peers connected to the libp2p gossip Swarm" + ); + metrics::describe_gauge!( + Self::GOSSIPSUB_CONNECTION, + "Connections made to the libp2p Swarm" + ); + metrics::describe_gauge!( + Self::BANNED_PEERS, + "Number of peers banned by kona's gossip stack" + ); + metrics::describe_histogram!( + Self::PEER_SCORES, + "Observations of peer scores in the gossipsub mesh" + ); + metrics::describe_histogram!( + Self::GOSSIP_PEER_CONNECTION_DURATION_SECONDS, + "Duration of peer connections in seconds" + ); + metrics::describe_counter!( + Self::BLOCK_VALIDATION_TOTAL, + "Total number of block validation attempts" + ); + metrics::describe_counter!( + Self::BLOCK_VALIDATION_SUCCESS, + "Number of successful block validations" + ); + metrics::describe_counter!( + Self::BLOCK_VALIDATION_FAILED, + "Number of failed block validations by reason" + ); + metrics::describe_histogram!( + Self::BLOCK_VALIDATION_DURATION_SECONDS, + "Duration of block validation in seconds" + ); + metrics::describe_counter!(Self::BLOCK_VERSION, "Distribution of block versions"); + } + + /// Initializes metrics to `0` so they can be queried immediately by consumers of prometheus + /// metrics. + #[cfg(feature = "metrics")] + pub fn zero() { + // RPC Calls + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_self", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_peerCount", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_peers", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_peerStats", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_discoveryTable", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_blockPeer", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_listBlockedPeers", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_blockAddr", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_unblockAddr", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_listBlockedAddrs", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_blockSubnet", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_unblockSubnet", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_listBlockedSubnets", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_protectPeer", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_unprotectPeer", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_connectPeer", 0); + kona_macros::set!(gauge, Self::RPC_CALLS, "method", "opp2p_disconnectPeer", 0); + + // Gossip Events + kona_macros::set!(gauge, Self::GOSSIP_EVENT, "type", "message", 0); + kona_macros::set!(gauge, Self::GOSSIP_EVENT, "type", "subscribed", 0); + kona_macros::set!(gauge, Self::GOSSIP_EVENT, "type", "unsubscribed", 0); + kona_macros::set!(gauge, Self::GOSSIP_EVENT, "type", "slow_peer", 0); + kona_macros::set!(gauge, Self::GOSSIP_EVENT, "type", "not_supported", 0); + + // Peer dials + kona_macros::set!(gauge, Self::DIAL_PEER, 0); + kona_macros::set!(gauge, Self::DIAL_PEER_ERROR, 0); + + // Unsafe Blocks + kona_macros::set!(gauge, Self::UNSAFE_BLOCK_PUBLISHED, 0); + + // Peer Counts + kona_macros::set!(gauge, Self::GOSSIP_PEER_COUNT, 0); + + // Connection + kona_macros::set!(gauge, Self::GOSSIPSUB_CONNECTION, "type", "connected", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_CONNECTION, "type", "outgoing_error", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_CONNECTION, "type", "incoming_error", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_CONNECTION, "type", "closed", 0); + + // Gossipsub Events + kona_macros::set!(gauge, Self::GOSSIPSUB_EVENT, "type", "subscribed", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_EVENT, "type", "unsubscribed", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_EVENT, "type", "gossipsub_not_supported", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_EVENT, "type", "slow_peer", 0); + kona_macros::set!(gauge, Self::GOSSIPSUB_EVENT, "type", "message_received", 0); + + // Banned Peers + kona_macros::set!(gauge, Self::BANNED_PEERS, 0); + + // Block validation metrics + kona_macros::set!(counter, Self::BLOCK_VALIDATION_TOTAL, 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_SUCCESS, 0); + + // Block validation failures by reason + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "timestamp_future", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "timestamp_past", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "invalid_hash", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "invalid_signature", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "invalid_signer", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "too_many_blocks", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "block_seen", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "invalid_block", 0); + kona_macros::set!( + counter, + Self::BLOCK_VALIDATION_FAILED, + "reason", + "parent_beacon_root", + 0 + ); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "blob_gas_used", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "excess_blob_gas", 0); + kona_macros::set!(counter, Self::BLOCK_VALIDATION_FAILED, "reason", "withdrawals_root", 0); + + // Block versions + kona_macros::set!(counter, Self::BLOCK_VERSION, "version", "v1", 0); + kona_macros::set!(counter, Self::BLOCK_VERSION, "version", "v2", 0); + kona_macros::set!(counter, Self::BLOCK_VERSION, "version", "v3", 0); + kona_macros::set!(counter, Self::BLOCK_VERSION, "version", "v4", 0); + } +} diff --git a/kona/crates/node/gossip/src/mod.rs b/kona/crates/node/gossip/src/mod.rs new file mode 100644 index 0000000000000..d5bbf7566ed8a --- /dev/null +++ b/kona/crates/node/gossip/src/mod.rs @@ -0,0 +1,79 @@ +//! GossipSub-based consensus layer networking for Optimism. +//! +//! This module implements the networking layer for Optimism consensus using libp2p's GossipSub +//! protocol. It handles the propagation and validation of OP Stack network payload messages +//! across the network mesh. +//! +//! ## Key Components +//! +//! - [`GossipDriver`]: The main driver that manages the libp2p swarm and event handling +//! - [`Behaviour`]: Custom libp2p behavior combining GossipSub, Ping, and Identify protocols +//! - [`BlockHandler`]: Validates and processes incoming block payloads +//! - [`ConnectionGater`]: Implements sophisticated connection management and rate limiting +//! - [`Event`]: High-level events emitted by the gossip system +//! +//! ## Network Architecture +//! +//! The gossip network uses a mesh topology where nodes maintain connections to a subset +//! of peers and propagate messages through the mesh. This provides efficient message +//! delivery with built-in redundancy and fault tolerance. +//! +//! ## Message Validation +//! +//! All incoming messages are validated through a multi-stage process: +//! 1. Basic structure validation +//! 2. Signature verification +//! 3. Content validation through [`BlockHandler`] +//! 4. Duplicate detection and caching +//! +//! ## Connection Management +//! +//! The module implements intelligent connection management through: +//! - Rate limiting for incoming connections +//! - IP-based filtering and subnet blocking +//! - Peer protection mechanisms +//! - Automatic connection pruning +//! +//! [`OpNetworkPayloadEnvelope`]: op_alloy_rpc_types_engine::OpNetworkPayloadEnvelope + +mod behaviour; +pub use behaviour::{Behaviour, BehaviourError}; + +mod config; +pub use config::{ + DEFAULT_MESH_D, DEFAULT_MESH_DHI, DEFAULT_MESH_DLAZY, DEFAULT_MESH_DLO, + GLOBAL_VALIDATE_THROTTLE, GOSSIP_HEARTBEAT, MAX_GOSSIP_SIZE, MAX_OUTBOUND_QUEUE, + MAX_VALIDATE_QUEUE, MIN_GOSSIP_SIZE, PEER_SCORE_INSPECT_FREQUENCY, SEEN_MESSAGES_TTL, + default_config, default_config_builder, +}; + +mod gate; +pub use gate::ConnectionGate; // trait + +mod gater; +pub use gater::{ + ConnectionGater, // implementation + DialInfo, + GaterConfig, +}; + +mod builder; +pub use builder::GossipDriverBuilder; + +mod error; +pub use error::{DialError, GossipDriverBuilderError, HandlerEncodeError, PublishError}; + +mod event; +pub use event::Event; + +mod handler; +pub use handler::{BlockHandler, Handler}; + +mod driver; +pub use driver::GossipDriver; + +mod block_validity; +pub use block_validity::BlockInvalidError; + +#[cfg(test)] +pub(crate) use block_validity::tests::*; diff --git a/kona/crates/node/gossip/src/rpc/mod.rs b/kona/crates/node/gossip/src/rpc/mod.rs new file mode 100644 index 0000000000000..9d917fa384b2c --- /dev/null +++ b/kona/crates/node/gossip/src/rpc/mod.rs @@ -0,0 +1,53 @@ +//! RPC API types and request handling for P2P administration. +//! +//! This module provides a JSON-RPC compatible interface for monitoring and controlling +//! the P2P networking stack. It offers comprehensive visibility into network status, +//! peer management, and operational metrics. +//! +//! ## API Categories +//! +//! ### Peer Information +//! - [`PeerInfo`]: Comprehensive peer details including connection status and capabilities +//! - [`PeerStats`]: Connection statistics and performance metrics +//! - [`PeerCount`]: Current peer count across different connection states +//! - [`PeerDump`]: Complete dump of all known peers +//! +//! ### Scoring and Quality +//! - [`PeerScores`]: Peer reputation scores used for mesh maintenance +//! - [`GossipScores`]: GossipSub-specific scoring metrics +//! - [`TopicScores`]: Per-topic scoring information +//! - [`ReqRespScores`]: Request-response protocol scoring +//! +//! ### Connection Management +//! - [`Connectedness`]: Peer connection state enumeration +//! - [`Direction`]: Connection direction (inbound/outbound) +//! +//! ## RPC Methods +//! +//! The [`P2pRpcRequest`] enum defines all available RPC methods, including: +//! - Node identity and status queries +//! - Peer listing and statistics +//! - Connection management (block/unblock peers) +//! - Network address filtering +//! - Discovery table inspection +//! +//! ## Usage +//! +//! The RPC interface is designed to be compatible with existing OP Stack tooling +//! and monitoring systems, providing the same API surface as the reference `op-node` +//! implementation where applicable. +//! +//! ## Security Considerations +//! +//! Administrative RPC methods should be exposed only to authorized clients, as they +//! can affect network connectivity and peer relationships. Consider implementing +//! appropriate authentication and access controls in production deployments. + +mod request; +pub use request::P2pRpcRequest; + +mod types; +pub use types::{ + Connectedness, Direction, GossipScores, PeerCount, PeerDump, PeerInfo, PeerScores, PeerStats, + ReqRespScores, TopicScores, +}; diff --git a/kona/crates/node/gossip/src/rpc/request.rs b/kona/crates/node/gossip/src/rpc/request.rs new file mode 100644 index 0000000000000..5bd36c4e7fa00 --- /dev/null +++ b/kona/crates/node/gossip/src/rpc/request.rs @@ -0,0 +1,662 @@ +//! Contains the p2p RPC request type. + +use std::{net::IpAddr, num::TryFromIntError, sync::Arc}; + +use crate::{GossipDriver, GossipScores}; +use alloy_primitives::map::{HashMap, HashSet}; +use discv5::{ + enr::{NodeId, k256::ecdsa}, + multiaddr::Protocol, +}; +use ipnet::IpNet; +use kona_disc::Discv5Handler; +use kona_peers::OpStackEnr; +use libp2p::{Multiaddr, PeerId, gossipsub::TopicHash}; +use tokio::sync::oneshot::Sender; + +use super::{ + PeerDump, PeerStats, + types::{Connectedness, Direction, PeerInfo, PeerScores}, +}; +use crate::ConnectionGate; + +/// A p2p RPC Request. +#[derive(Debug)] +pub enum P2pRpcRequest { + /// Returns [`PeerInfo`] for the p2p network. + PeerInfo(Sender), + /// Dumps the node's discovery table from the [`kona_disc::Discv5Driver`]. + DiscoveryTable(Sender>), + /// Returns the current peer count for both the + /// - Discovery Service ([`kona_disc::Discv5Driver`]) + /// - Gossip Service ([`crate::GossipDriver`]) + PeerCount(Sender<(Option, usize)>), + /// Returns a [`PeerDump`] containing detailed information about connected peers. + /// If `connected` is true, only returns connected peers. + Peers { + /// The output channel to send the [`PeerDump`] to. + out: Sender, + /// Whether to only return connected peers. + connected: bool, + }, + /// Request to block a peer by its [`PeerId`]. + BlockPeer { + /// The [`PeerId`] of the peer to block. + id: PeerId, + }, + /// Request to unblock a peer by its [`PeerId`]. + UnblockPeer { + /// The [`PeerId`] of the peer to unblock. + id: PeerId, + }, + /// Request to list all blocked peers. + ListBlockedPeers(Sender>), + /// Request to block a given IP Address. + BlockAddr { + /// The IP address to block. + address: IpAddr, + }, + /// Request to unblock a given IP Address. + UnblockAddr { + /// The IP address to unblock. + address: IpAddr, + }, + /// Request to list all blocked IP Addresses. + ListBlockedAddrs(Sender>), + /// Request to block a given Subnet. + BlockSubnet { + /// The Subnet to block. + address: IpNet, + }, + /// Request to unblock a given Subnet. + UnblockSubnet { + /// The Subnet to unblock. + address: IpNet, + }, + + /// Request to connect to a given peer. + ConnectPeer { + /// The [`Multiaddr`] of the peer to connect to. + address: Multiaddr, + }, + /// Request to disconnect the specified peer. + DisconnectPeer { + /// The peer id to disconnect. + peer_id: PeerId, + }, + /// Protects a given peer from disconnection. + ProtectPeer { + /// The id of the peer. + peer_id: PeerId, + }, + /// Unprotects a given peer. + UnprotectPeer { + /// The id of the peer. + peer_id: PeerId, + }, + /// Request to list all blocked Subnets. + ListBlockedSubnets(Sender>), + /// Returns the current peer stats for both the + /// - Discovery Service ([`kona_disc::Discv5Driver`]) + /// - Gossip Service ([`crate::GossipDriver`]) + /// + /// This information can be used to briefly monitor the current state of the p2p network for a + /// given peer. + PeerStats(Sender), +} + +impl P2pRpcRequest { + /// Handles the peer count request. + pub fn handle(self, gossip: &mut GossipDriver, disc: &Discv5Handler) { + match self { + Self::PeerCount(s) => Self::handle_peer_count(s, gossip, disc), + Self::DiscoveryTable(s) => Self::handle_discovery_table(s, disc), + Self::PeerInfo(s) => Self::handle_peer_info(s, gossip, disc), + Self::Peers { out, connected } => Self::handle_peers(out, connected, gossip, disc), + Self::DisconnectPeer { peer_id } => Self::disconnect_peer(peer_id, gossip), + Self::PeerStats(s) => Self::handle_peer_stats(s, gossip, disc), + Self::ConnectPeer { address } => Self::connect_peer(address, gossip), + Self::BlockPeer { id } => Self::block_peer(id, gossip), + Self::UnblockPeer { id } => Self::unblock_peer(id, gossip), + Self::ListBlockedPeers(s) => Self::list_blocked_peers(s, gossip), + Self::BlockAddr { address } => Self::block_addr(address, gossip), + Self::UnblockAddr { address } => Self::unblock_addr(address, gossip), + Self::ListBlockedAddrs(s) => Self::list_blocked_addrs(s, gossip), + Self::ProtectPeer { peer_id } => Self::protect_peer(peer_id, gossip), + Self::UnprotectPeer { peer_id } => Self::unprotect_peer(peer_id, gossip), + Self::BlockSubnet { address } => Self::block_subnet(address, gossip), + Self::UnblockSubnet { address } => Self::unblock_subnet(address, gossip), + Self::ListBlockedSubnets(s) => Self::list_blocked_subnets(s, gossip), + } + } + + fn protect_peer(id: PeerId, gossip: &mut GossipDriver) { + gossip.connection_gate.protect_peer(id); + } + + fn unprotect_peer(id: PeerId, gossip: &mut GossipDriver) { + gossip.connection_gate.unprotect_peer(id); + } + + fn block_addr(address: IpAddr, gossip: &mut GossipDriver) { + gossip.connection_gate.block_addr(address); + } + + fn unblock_addr(address: IpAddr, gossip: &mut GossipDriver) { + gossip.connection_gate.unblock_addr(address); + } + + fn list_blocked_addrs(s: Sender>, gossip: &GossipDriver) { + let blocked_addrs = gossip.connection_gate.list_blocked_addrs(); + if let Err(e) = s.send(blocked_addrs) { + warn!(target: "p2p::rpc", "Failed to send blocked addresses through response channel: {:?}", e); + } + } + + fn block_peer(id: PeerId, gossip: &mut GossipDriver) { + gossip.connection_gate.block_peer(&id); + gossip.swarm.behaviour_mut().gossipsub.blacklist_peer(&id); + } + + fn unblock_peer(id: PeerId, gossip: &mut GossipDriver) { + gossip.connection_gate.unblock_peer(&id); + gossip.swarm.behaviour_mut().gossipsub.remove_blacklisted_peer(&id); + } + + fn list_blocked_peers(s: Sender>, gossip: &GossipDriver) { + let blocked_peers = gossip.connection_gate.list_blocked_peers(); + if let Err(e) = s.send(blocked_peers) { + warn!(target: "p2p::rpc", "Failed to send blocked peers through response channel: {:?}", e); + } + } + + fn block_subnet(address: IpNet, gossip: &mut GossipDriver) { + gossip.connection_gate.block_subnet(address); + } + + fn unblock_subnet(address: IpNet, gossip: &mut GossipDriver) { + gossip.connection_gate.unblock_subnet(address); + } + + fn connect_peer(address: Multiaddr, gossip: &mut GossipDriver) { + gossip.dial_multiaddr(address) + } + + fn disconnect_peer(peer_id: PeerId, gossip: &mut GossipDriver) { + if let Err(e) = gossip.swarm.disconnect_peer_id(peer_id) { + warn!(target: "p2p::rpc", "Failed to disconnect peer {}: {:?}", peer_id, e); + } else { + info!(target: "p2p::rpc", "Disconnected peer {}", peer_id); + // Record the duration of the peer connection. + if let Some(start_time) = gossip.peer_connection_start.remove(&peer_id) { + let _peer_duration = start_time.elapsed(); + kona_macros::record!( + histogram, + crate::Metrics::GOSSIP_PEER_CONNECTION_DURATION_SECONDS, + _peer_duration.as_secs_f64() + ); + } + } + } + + fn list_blocked_subnets(s: Sender>, gossip: &GossipDriver) { + let blocked_subnets = gossip.connection_gate.list_blocked_subnets(); + if let Err(e) = s.send(blocked_subnets) { + warn!(target: "p2p::rpc", "Failed to send blocked subnets through response channel: {:?}", e); + } + } + + fn handle_discovery_table(sender: Sender>, disc: &Discv5Handler) { + let enrs = disc.table_enrs(); + tokio::spawn(async move { + let dt = match enrs.await { + Ok(dt) => dt.into_iter().map(|e| e.to_string()).collect(), + + Err(e) => { + warn!(target: "p2p_rpc", "Failed to receive peer count: {:?}", e); + return; + } + }; + + if let Err(e) = sender.send(dt) { + warn!(target: "p2p_rpc", "Failed to send peer count through response channel: {:?}", e); + } + }); + } + + fn handle_peers( + sender: Sender, + connected: bool, + gossip: &GossipDriver, + disc: &Discv5Handler, + ) { + let Ok(total_connected) = gossip.swarm.network_info().num_peers().try_into() else { + error!(target: "p2p::rpc", "Failed to get total connected peers. The number of connected peers is too large and overflows u32."); + return; + }; + + let peer_ids: Vec = if connected { + gossip.swarm.connected_peers().cloned().collect() + } else { + gossip.peerstore.keys().cloned().collect() + }; + + // Get the set of actually connected peers from the swarm for accurate connectedness + // reporting. + let actually_connected: HashSet = gossip.swarm.connected_peers().cloned().collect(); + + // Get connection gate information. + let banned_subnets = gossip.connection_gate.list_blocked_subnets(); + let banned_ips = gossip.connection_gate.list_blocked_addrs(); + let banned_peers = gossip.connection_gate.list_blocked_peers(); + let protected_peers = gossip.connection_gate.list_protected_peers(); + + // For each peer id, determine connectedness based on actual swarm connection state. + // This fixes the issue where the connection gate's internal state could be stale, + // especially for inbound connections or after connections close. + let connectedness = peer_ids + .iter() + .copied() + .map(|id| { + if actually_connected.contains(&id) { + (id, Connectedness::Connected) + } else if banned_peers.contains(&id) { + (id, Connectedness::CannotConnect) + } else { + (id, Connectedness::NotConnected) + } + }) + .collect::>(); + + // Clone the ping map + let pings = Arc::clone(&gossip.ping); + + #[derive(Default)] + struct PeerMetadata { + protocols: Option>, + addresses: Vec, + user_agent: String, + protocol_version: String, + score: f64, + } + + // Build a map of peer ids to their supported protocols and addresses. + let mut peer_metadata: HashMap = gossip + .peerstore + .iter() + .map(|(id, info)| { + let protocols = if info.protocols.is_empty() { + None + } else { + Some( + info.protocols + .iter() + .map(|protocol| protocol.to_string()) + .collect::>(), + ) + }; + let addresses = info + .listen_addrs + .iter() + .map(|addr| { + let mut addr = addr.clone(); + addr.push(Protocol::P2p(*id)); + addr.to_string() + }) + .collect::>(); + + let score = gossip.swarm.behaviour().gossipsub.peer_score(id).unwrap_or_default(); + + ( + *id, + PeerMetadata { + protocols, + addresses, + user_agent: info.agent_version.clone(), + protocol_version: info.protocol_version.clone(), + score, + }, + ) + }) + .collect(); + + // We consider that kona-nodes are gossiping blocks if their peers are subscribed to any of + // the blocks topics. + // This is the same heuristic as the one used in the op-node (``). + let peer_gossip_info = gossip + .swarm + .behaviour() + .gossipsub + .all_peers() + .filter_map(|(peer_id, topics)| { + let supported_topics = HashSet::from([ + gossip.handler.blocks_v1_topic.hash(), + gossip.handler.blocks_v2_topic.hash(), + gossip.handler.blocks_v3_topic.hash(), + gossip.handler.blocks_v4_topic.hash(), + ]); + + if topics.iter().any(|topic| supported_topics.contains(topic)) { + Some(*peer_id) + } else { + None + } + }) + .collect::>(); + + let disc_table_infos = disc.table_infos(); + + tokio::spawn(async move { + let Ok(table_infos) = disc_table_infos.await else { + error!(target: "p2p::rpc", "Failed to get table infos. The connection to the gossip driver is closed."); + return; + }; + + let pings = { pings.lock().await.clone() }; + + let node_to_peer_id: HashMap = peer_ids.into_iter().filter_map(|id| + { + let Ok(pubkey) = libp2p_identity::PublicKey::try_decode_protobuf(&id.to_bytes()[2..]) else { + error!(target: "p2p::rpc", peer_id = ?id, "Failed to decode public key from peer id. This is a bug as all the peer ids should be decodable (because they come from secp256k1 public keys)."); + return None; + }; + + let key = + match pubkey.try_into_secp256k1().map_err(|err| err.to_string()).and_then( + |key| ecdsa::VerifyingKey::from_sec1_bytes(key.to_bytes().as_slice()).map_err(|err| err.to_string()) + ) { Ok(key) => key, + Err(err) => { + error!(target: "p2p::rpc", peer_id = ?id, err = ?err, "Failed to convert public key to secp256k1 public key. This is a bug."); + return None; + }}; + let node_id = NodeId::from(key); + Some((node_id, id)) + } + ).collect(); + + // Filter out peers that are not in the gossip network. + let node_to_table_infos = table_infos + .into_iter() + .filter(|(id, _, _)| node_to_peer_id.contains_key(id)) + .map(|(id, enr, status)| (id, (enr, status))) + .collect::>(); + + // Build the peer info map. + let infos: HashMap = node_to_peer_id + .iter() + .map(|(id, peer_id)| { + let (maybe_enr, maybe_status) = node_to_table_infos.get(id).cloned().unzip(); + + let opstack_enr = + maybe_enr.clone().and_then(|enr| OpStackEnr::try_from(&enr).ok()); + + let direction = maybe_status + .map(|status| { + if status.is_incoming() { + Direction::Inbound + } else { + Direction::Outbound + } + }) + .unwrap_or_default(); + + let PeerMetadata { protocols, addresses, user_agent, protocol_version, score } = + peer_metadata.remove(peer_id).unwrap_or_default(); + + let peer_connectedness = + connectedness.get(peer_id).copied().unwrap_or(Connectedness::NotConnected); + + let latency = pings.get(peer_id).map(|d| d.as_secs()).unwrap_or(0); + + let node_id = format!("{:?}", &id); + ( + peer_id.to_string(), + PeerInfo { + peer_id: peer_id.to_string(), + node_id, + user_agent, + protocol_version, + enr: maybe_enr.map(|enr| enr.to_string()), + addresses, + protocols, + connectedness: peer_connectedness, + direction, + // Note: we use the chain id from the ENR if it exists, otherwise we + // use 0 to be consistent with op-node's behavior (``). + chain_id: opstack_enr.map(|enr| enr.chain_id).unwrap_or(0), + gossip_blocks: peer_gossip_info.contains(peer_id), + protected: protected_peers.contains(peer_id), + latency, + peer_scores: PeerScores { + gossip: GossipScores { + total: score, + // Note(@theochap): we don't compute the topic scores + // because we don't + // `rust-libp2p` doesn't expose that information to the + // user-facing API. + // See `` + blocks: Default::default(), + // Note(@theochap): We can't compute the ip colocation + // factor because + // `rust-libp2p` doesn't expose that information to the + // user-facing API + // See `` + ip_colocation_factor: Default::default(), + // Note(@theochap): We can't compute the behavioral penalty + // because + // `rust-libp2p` doesn't expose that information to the + // user-facing API + // See `` + behavioral_penalty: Default::default(), + }, + // We only support a shim implementation for the req/resp + // protocol so we're not + // computing scores for it. + req_resp: Default::default(), + }, + }, + ) + }) + .collect(); + + if let Err(e) = sender.send(PeerDump { + total_connected, + peers: infos, + banned_peers: banned_peers.into_iter().map(|p| p.to_string()).collect(), + banned_ips, + banned_subnets, + }) { + warn!(target: "p2p::rpc", "Failed to send peer info through response channel: {:?}", e); + } + }); + } + + /// Handles a peer info request by spawning a task. + fn handle_peer_info( + sender: Sender, + gossip: &GossipDriver, + disc: &Discv5Handler, + ) { + let peer_id = *gossip.local_peer_id(); + let chain_id = disc.chain_id; + let local_enr = disc.local_enr(); + let mut addresses = gossip + .swarm + .listeners() + .map(|a| { + let mut addr = a.clone(); + addr.push(Protocol::P2p(peer_id)); + addr.to_string() + }) + .collect::>(); + + addresses.append( + &mut gossip.swarm.external_addresses().map(|a| a.to_string()).collect::>(), + ); + + tokio::spawn(async move { + let enr = match local_enr.await { + Ok(enr) => enr, + Err(e) => { + warn!(target: "p2p::rpc", "Failed to receive local ENR: {:?}", e); + return; + } + }; + + // Note: we need to use `Debug` impl here because the `Display` impl of + // `NodeId` strips some part of the hex string and replaces it with "...". + let node_id = format!("{:?}", &enr.node_id()); + + // We need to add the local multiaddr to the list of known addresses. + let peer_info = PeerInfo { + peer_id: peer_id.to_string(), + node_id, + user_agent: "kona".to_string(), + protocol_version: String::new(), + enr: Some(enr.to_string()), + addresses, + protocols: Some(vec![ + "/ipfs/id/push/1.0.0".to_string(), + "/meshsub/1.1.0".to_string(), + "/ipfs/ping/1.0.0".to_string(), + "/meshsub/1.2.0".to_string(), + "/ipfs/id/1.0.0".to_string(), + format!("/opstack/req/payload_by_number/{chain_id}/0/"), + "/meshsub/1.0.0".to_string(), + "/floodsub/1.0.0".to_string(), + ]), + connectedness: Connectedness::Connected, + direction: Direction::Inbound, + protected: false, + chain_id, + latency: 0, + gossip_blocks: true, + peer_scores: PeerScores::default(), + }; + if let Err(e) = sender.send(peer_info) { + warn!(target: "p2p_rpc", "Failed to send peer info through response channel: {:?}", e); + } + }); + } + + fn handle_peer_stats( + sender: Sender, + gossip: &GossipDriver, + disc: &Discv5Handler, + ) { + let peers_known = gossip.peerstore.len(); + let gossip_network_info = gossip.swarm.network_info(); + let table_info = disc.peer_count(); + + let banned_peers = gossip.connection_gate.list_blocked_peers().len(); + + let topics = gossip.swarm.behaviour().gossipsub.topics().collect::>(); + + let topics = topics + .into_iter() + .map(|hash| { + ( + hash.clone(), + gossip + .swarm + .behaviour() + .gossipsub + .all_peers() + .filter(|(_, topics)| topics.contains(&hash)) + .count(), + ) + }) + .collect::>(); + + let v1_topic_hash = gossip.handler.blocks_v1_topic.hash(); + let v2_topic_hash = gossip.handler.blocks_v2_topic.hash(); + let v3_topic_hash = gossip.handler.blocks_v3_topic.hash(); + let v4_topic_hash = gossip.handler.blocks_v4_topic.hash(); + + tokio::spawn(async move { + let Ok(table) = table_info.await else { + error!(target: "p2p::rpc", "failed to get discovery table size. The sender has been dropped. The discv5 service may not be running anymore."); + return; + }; + + let Ok(table) = table.try_into() else { + error!(target: "p2p::rpc", "failed to get discovery table size. Integer overflow. Please ensure that the number of peers in the discovery table fits in a u32."); + return; + }; + + let Ok(connected) = gossip_network_info.num_peers().try_into() else { + error!(target: "p2p::rpc", "failed to get number of connected peers. Integer overflow. Please ensure that the number of connected peers fits in a u32."); + return; + }; + + let Ok(known) = peers_known.try_into() else { + error!(target: "p2p::rpc", "failed to get number of known peers. Integer overflow. Please ensure that the number of known peers fits in a u32."); + return; + }; + + // Given a topic hash, this method: + // - gets the number of peers in the mesh for that topic + // - returns an error if the number of peers in the mesh overflows a u32 + // - returns 0 if there are no peers in the mesh for that topic + let get_topic = |topic: &TopicHash| { + Ok::( + topics + .get(topic) + .cloned() + .map(|v| v.try_into()) + .transpose()? + .unwrap_or_default(), + ) + }; + + let Ok(block_topics) = vec![ + get_topic(&v1_topic_hash), + get_topic(&v2_topic_hash), + get_topic(&v3_topic_hash), + get_topic(&v4_topic_hash), + ] + .into_iter() + .collect::, _>>() else { + error!(target: "p2p::rpc", "failed to get blocks topic. Some topic count overflowed. Make sure that the number of peers for a given topic fits in a u32."); + return; + }; + + let stats = PeerStats { + connected, + table, + blocks_topic: block_topics[0], + blocks_topic_v2: block_topics[1], + blocks_topic_v3: block_topics[2], + blocks_topic_v4: block_topics[3], + banned: banned_peers as u32, + known, + }; + + if let Err(e) = sender.send(stats) { + warn!(target: "p2p_rpc", "Failed to send peer stats through response channel: {:?}", e); + }; + }); + } + + /// Handles a peer count request by spawning a task. + fn handle_peer_count( + sender: Sender<(Option, usize)>, + gossip: &GossipDriver, + disc: &Discv5Handler, + ) { + let pc_req = disc.peer_count(); + let gossip_pc = gossip.connected_peers(); + tokio::spawn(async move { + let pc = match pc_req.await { + Ok(pc) => Some(pc), + Err(e) => { + warn!(target: "p2p_rpc", "Failed to receive peer count: {:?}", e); + None + } + }; + if let Err(e) = sender.send((pc, gossip_pc)) { + warn!(target: "p2p_rpc", "Failed to send peer count through response channel: {:?}", e); + } + }); + } +} diff --git a/kona/crates/node/gossip/src/rpc/types.rs b/kona/crates/node/gossip/src/rpc/types.rs new file mode 100644 index 0000000000000..48cddaf09f63a --- /dev/null +++ b/kona/crates/node/gossip/src/rpc/types.rs @@ -0,0 +1,428 @@ +//! The types used in the p2p RPC API. + +use core::net::IpAddr; +use derive_more::Display; + +use alloy_primitives::{ChainId, map::HashMap}; + +/// The peer info. +/// +/// +#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerInfo { + /// The peer id. + #[serde(rename = "peerID")] + pub peer_id: String, + /// The node id. + #[serde(rename = "nodeID")] + pub node_id: String, + /// The user agent. + pub user_agent: String, + /// The protocol version. + pub protocol_version: String, + /// The enr for the peer. + /// If the peer is not in the discovery table, this will not be set. + #[serde(rename = "ENR")] + #[serde(skip_serializing_if = "Option::is_none")] + pub enr: Option, + /// The peer addresses. + pub addresses: Vec, + /// Peer supported protocols + pub protocols: Option>, + /// 0: "`NotConnected`", + /// 1: "Connected", + /// 2: "`CanConnect`" (gracefully disconnected) + /// 3: "`CannotConnect`" (tried but failed) + pub connectedness: Connectedness, + /// 0: "Unknown", + /// 1: "Inbound" (if the peer contacted us) + /// 2: "Outbound" (if we connected to them) + pub direction: Direction, + /// Whether the peer is protected. + pub protected: bool, + /// The chain id. + #[serde(rename = "chainID")] + pub chain_id: ChainId, + /// The peer latency in nanoseconds + pub latency: u64, + /// Whether the peer gossips + pub gossip_blocks: bool, + /// The peer scores. + #[serde(rename = "scores")] + pub peer_scores: PeerScores, +} + +/// GossipSub topic-specific scoring metrics. +/// +/// Tracks peer performance within specific gossip topics, used by the +/// GossipSub protocol to maintain mesh quality and route messages efficiently. +/// These scores influence peer selection for the gossip mesh topology. +/// +/// Reference: +#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TopicScores { + /// Duration the peer has participated in the topic mesh. + /// + /// Longer participation indicates stability and commitment to the topic, + /// contributing positively to the peer's mesh score. + pub time_in_mesh: f64, + + /// Count of first-time message deliveries from this peer. + /// + /// Measures how often this peer is the first to deliver new messages, + /// indicating their connectivity and responsiveness to the network. + pub first_message_deliveries: f64, + + /// Count of messages delivered while in the mesh topology. + /// + /// Tracks consistent message forwarding behavior while the peer is + /// an active participant in the mesh structure. + pub mesh_message_deliveries: f64, + + /// Count of invalid or malicious messages from this peer. + /// + /// Penalizes peers that send invalid, duplicate, or malformed messages, + /// helping maintain network health and preventing spam. + pub invalid_message_deliveries: f64, +} + +/// Comprehensive GossipSub scoring metrics for peer quality assessment. +/// +/// Aggregates various scoring factors used by the GossipSub protocol to +/// evaluate peer quality and determine mesh topology. Higher scores indicate +/// more reliable and well-behaved peers. +/// +/// Reference: +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GossipScores { + /// Aggregate score across all scoring dimensions. + /// + /// The final computed score that determines this peer's overall + /// reputation in the gossip network. + pub total: f64, + + /// Block-specific topic scores for consensus messages. + /// + /// Tracks peer behavior specifically for block gossip, which is + /// the primary message type in OP Stack networks. + pub blocks: TopicScores, + + /// Penalty for IP address colocation with other peers. + /// + /// Reduces scores for peers sharing IP addresses to prevent + /// eclipse attacks and improve network decentralization. + #[serde(rename = "IPColocationFactor")] + pub ip_colocation_factor: f64, + + /// Penalty for problematic behavior patterns. + /// + /// Applied to peers exhibiting suspicious or harmful behavior + /// that doesn't fit other specific scoring categories. + pub behavioral_penalty: f64, +} + +/// Request-response protocol scoring metrics. +/// +/// Tracks peer performance in direct request-response interactions outside +/// of the gossip mesh, such as block synchronization requests. +/// +/// Reference: +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReqRespScores { + /// Number of valid responses provided by this peer. + /// + /// Counts successful request-response exchanges where the peer + /// provided correct and timely responses to queries. + pub valid_responses: f64, + + /// Number of error responses or failed requests. + /// + /// Tracks cases where the peer returned errors, timeouts, or + /// otherwise failed to properly respond to requests. + pub error_responses: f64, + /// Number of rejected payloads. + pub rejected_payloads: f64, +} + +/// Peer Scores +/// +/// +#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerScores { + /// The gossip scores + pub gossip: GossipScores, + /// The request-response scores. + pub req_resp: ReqRespScores, +} + +/// Peer count data. +#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerCount { + /// The total number of connected peers to the discovery service. + pub connected_discovery: Option, + /// The total number of connected peers to the gossip service. + pub connected_gossip: usize, +} + +/// A raw peer dump. +/// +/// +#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerDump { + /// The total number of connected peers + pub total_connected: u32, + /// A map from peer id to peer info + pub peers: HashMap, + /// A list of banned peers. + pub banned_peers: Vec, + /// A list of banned ip addresses. + #[serde(rename = "bannedIPS")] + pub banned_ips: Vec, + /// The banned subnets + pub banned_subnets: Vec, +} + +/// Peer stats. +/// +/// +#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerStats { + /// The number of connections + pub connected: u32, + /// The table. + pub table: u32, + /// The blocks topic. + #[serde(rename = "blocksTopic")] + pub blocks_topic: u32, + /// The blocks v2 topic. + #[serde(rename = "blocksTopicV2")] + pub blocks_topic_v2: u32, + /// The blocks v3 topic. + #[serde(rename = "blocksTopicV3")] + pub blocks_topic_v3: u32, + /// The blocks v4 topic. + #[serde(rename = "blocksTopicV4")] + pub blocks_topic_v4: u32, + /// The banned count. + pub banned: u32, + /// The known count. + pub known: u32, +} + +/// Represents the connectivity state of a peer in a network, indicating the reachability and +/// interaction status of a node with its peers. +#[derive( + Clone, + Debug, + Display, + PartialEq, + Copy, + Default, + // We need to use `serde_repr` to serialize the enum as an integer to match the `op-node` API. + serde_repr::Serialize_repr, + serde_repr::Deserialize_repr, +)] +#[repr(u8)] +pub enum Connectedness { + /// No current connection to the peer, and no recent history of a successful connection. + #[default] + #[display("Not Connected")] + NotConnected = 0, + + /// An active, open connection to the peer exists. + #[display("Connected")] + Connected = 1, + + /// Connection to the peer is possible but not currently established; usually implies a past + /// successful connection. + #[display("Can Connect")] + CanConnect = 2, + + /// Recent attempts to connect to the peer failed, indicating potential issues in reachability + /// or peer status. + #[display("Cannot Connect")] + CannotConnect = 3, + + /// Connection to the peer is limited; may not have full capabilities. + #[display("Limited")] + Limited = 4, +} + +impl From for Connectedness { + fn from(value: u8) -> Self { + match value { + 0 => Self::NotConnected, + 1 => Self::Connected, + 2 => Self::CanConnect, + 3 => Self::CannotConnect, + 4 => Self::Limited, + _ => Self::NotConnected, + } + } +} +/// Direction represents the direction of a connection. +#[derive(Debug, Clone, Display, Copy, PartialEq, Eq, Default)] +pub enum Direction { + /// Unknown is the default direction when the direction is not specified. + #[default] + #[display("Unknown")] + Unknown = 0, + /// Inbound is for when the remote peer initiated the connection. + #[display("Inbound")] + Inbound = 1, + /// Outbound is for when the local peer initiated the connection. + #[display("Outbound")] + Outbound = 2, +} + +impl serde::Serialize for Direction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(*self as u8) + } +} + +impl<'de> serde::Deserialize<'de> for Direction { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + match value { + 0 => Ok(Self::Unknown), + 1 => Ok(Self::Inbound), + 2 => Ok(Self::Outbound), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(value as u64), + &"a value between 0 and 2", + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connectedness_from_u8() { + assert_eq!(Connectedness::from(0), Connectedness::NotConnected); + assert_eq!(Connectedness::from(1), Connectedness::Connected); + assert_eq!(Connectedness::from(2), Connectedness::CanConnect); + assert_eq!(Connectedness::from(3), Connectedness::CannotConnect); + assert_eq!(Connectedness::from(4), Connectedness::Limited); + assert_eq!(Connectedness::from(5), Connectedness::NotConnected); + } + + #[test] + fn test_direction_display() { + assert_eq!(Direction::Unknown.to_string(), "Unknown"); + assert_eq!(Direction::Inbound.to_string(), "Inbound"); + assert_eq!(Direction::Outbound.to_string(), "Outbound"); + } + + #[test] + fn test_direction_serialization() { + assert_eq!( + serde_json::to_string(&Direction::Unknown).unwrap(), + "0", + "Serialization failed for Direction::Unknown" + ); + assert_eq!( + serde_json::to_string(&Direction::Inbound).unwrap(), + "1", + "Serialization failed for Direction::Inbound" + ); + assert_eq!( + serde_json::to_string(&Direction::Outbound).unwrap(), + "2", + "Serialization failed for Direction::Outbound" + ); + } + + #[test] + fn test_direction_deserialization() { + let unknown: Direction = serde_json::from_str("0").unwrap(); + let inbound: Direction = serde_json::from_str("1").unwrap(); + let outbound: Direction = serde_json::from_str("2").unwrap(); + + assert_eq!(unknown, Direction::Unknown, "Deserialization mismatch for Direction::Unknown"); + assert_eq!(inbound, Direction::Inbound, "Deserialization mismatch for Direction::Inbound"); + assert_eq!( + outbound, + Direction::Outbound, + "Deserialization mismatch for Direction::Outbound" + ); + } + + #[test] + fn test_peer_info_connectedness_serialization() { + let peer_info = PeerInfo { + peer_id: String::from("peer123"), + node_id: String::from("node123"), + user_agent: String::from("MyUserAgent"), + protocol_version: String::from("v1"), + enr: Some(String::from("enr123")), + addresses: [String::from("127.0.0.1")].to_vec(), + protocols: Some([String::from("eth"), String::from("p2p")].to_vec()), + connectedness: Connectedness::Connected, + direction: Direction::Outbound, + protected: true, + chain_id: 1, + latency: 100, + gossip_blocks: true, + peer_scores: PeerScores { + gossip: GossipScores { + total: 1.0, + blocks: TopicScores { + time_in_mesh: 10.0, + first_message_deliveries: 5.0, + mesh_message_deliveries: 2.0, + invalid_message_deliveries: 0.0, + }, + ip_colocation_factor: 0.5, + behavioral_penalty: 0.1, + }, + req_resp: ReqRespScores { + valid_responses: 10.0, + error_responses: 1.0, + rejected_payloads: 0.0, + }, + }, + }; + + let serialized = serde_json::to_string(&peer_info).expect("Serialization failed"); + + let deserialized: PeerInfo = + serde_json::from_str(&serialized).expect("Deserialization failed"); + + assert_eq!(peer_info.peer_id, deserialized.peer_id); + assert_eq!(peer_info.node_id, deserialized.node_id); + assert_eq!(peer_info.user_agent, deserialized.user_agent); + assert_eq!(peer_info.protocol_version, deserialized.protocol_version); + assert_eq!(peer_info.enr, deserialized.enr); + assert_eq!(peer_info.addresses, deserialized.addresses); + assert_eq!(peer_info.protocols, deserialized.protocols); + assert_eq!(peer_info.connectedness, deserialized.connectedness); + assert_eq!(peer_info.direction, deserialized.direction); + assert_eq!(peer_info.protected, deserialized.protected); + assert_eq!(peer_info.chain_id, deserialized.chain_id); + assert_eq!(peer_info.latency, deserialized.latency); + assert_eq!(peer_info.gossip_blocks, deserialized.gossip_blocks); + assert_eq!(peer_info.peer_scores.gossip.total, deserialized.peer_scores.gossip.total); + assert_eq!( + peer_info.peer_scores.req_resp.valid_responses, + deserialized.peer_scores.req_resp.valid_responses + ); + } +} diff --git a/kona/crates/node/peers/Cargo.toml b/kona/crates/node/peers/Cargo.toml new file mode 100644 index 0000000000000..77958721857f0 --- /dev/null +++ b/kona/crates/node/peers/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "kona-peers" +version = "0.1.2" +description = "Network peers library for the OP Stack" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# Kona +kona-registry.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-primitives = { workspace = true, features = ["k256", "getrandom"] } + +# Networking +discv5 = { workspace = true, features = ["libp2p"] } +libp2p-identity = { workspace = true, features = ["secp256k1"] } +libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "noise", "gossipsub", "ping", "yamux", "identify"] } + +# Cryptography +secp256k1.workspace = true + +# Misc +url.workspace = true +dirs.workspace = true +serde.workspace = true +tracing.workspace = true +thiserror.workspace = true +lazy_static.workspace = true +unsigned-varint.workspace = true +serde_json = { workspace = true, features = ["std"] } +derive_more = { workspace = true, features = ["display", "deref", "debug"] } + +# `arbitrary` feature dependencies +arbitrary = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +arbtest.workspace = true +tempfile.workspace = true +multihash.workspace = true +serde_json.workspace = true + +kona-genesis.workspace = true + +arbitrary = { workspace = true, features = ["derive"] } +alloy-primitives = { workspace = true, features = ["arbitrary"] } +alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] } + +[features] +default = [] +arbitrary = [ "alloy-primitives/arbitrary", "dep:arbitrary" ] diff --git a/kona/crates/node/peers/README.md b/kona/crates/node/peers/README.md new file mode 100644 index 0000000000000..e6ab644ad5e67 --- /dev/null +++ b/kona/crates/node/peers/README.md @@ -0,0 +1,42 @@ +# `kona-peers` + +CI +Kona Peers +License +Codecov + +Networking Utilities ported from reth. + +Much of this module is ported from +. + +This module manages and converts Ethereum network entities such as node records, peer IDs, and +Ethereum Node Records (ENRs) + +## Node Record Overview + +Ethereum uses different types of "node records" to represent peers on the network. + +The simplest way to identify a peer is by public key. This is the `PeerId` type, which usually +represents a peer's secp256k1 public key. + +A more complete representation of a peer is the `NodeRecord` type, which includes the peer's +IP address, the ports where it is reachable (TCP and UDP), and the peer's public key. This is +what is returned from discovery v4 queries. + +The most comprehensive node record type is the Ethereum Node Record (`discv5::Enr`), which is +a signed, versioned record that includes the information from a `NodeRecord` along with +additional metadata. This is the data structure returned from discovery v5 queries. + +When we need to deserialize an identifier that could be any of these three types (`PeerId`, +`NodeRecord`, and `discv5::Enr`), we use the `AnyNode` type, which is an enum over the +three types. `AnyNode` is used in reth's `admin_addTrustedPeer` RPC method. + +In short, the types are as follows: +- `PeerId`: A simple public key identifier. +- `NodeRecord`: A more complete representation of a peer, including IP address and ports. +- `discv5::Enr`: An Ethereum Node Record, which is a signed, versioned record that includes + additional metadata. Useful when interacting with discovery v5, or when custom metadata is + required. +- `AnyNode`: An enum over `PeerId`, `NodeRecord`, and `discv5::Enr`, useful in + deserialization when the type of the node record is not known. \ No newline at end of file diff --git a/kona/crates/node/peers/src/any.rs b/kona/crates/node/peers/src/any.rs new file mode 100644 index 0000000000000..ae895435e70f2 --- /dev/null +++ b/kona/crates/node/peers/src/any.rs @@ -0,0 +1,124 @@ +//! Contains the `AnyNode` enum, which can represent a peer in any form. + +use crate::{NodeRecord, PeerId}; +use derive_more::From; +use discv5::{Enr, enr::EnrPublicKey}; +use libp2p::swarm::dial_opts::DialOpts; + +use super::utils::peer_id_to_secp256k1_pubkey; + +/// A peer that can come in [`Enr`] or [`NodeRecord`] form. +#[derive(Debug, Clone, From, Eq, PartialEq, Hash)] +pub enum AnyNode { + /// An "enode:" peer with full ip + NodeRecord(NodeRecord), + /// An "enr:" peer + Enr(Enr), + /// An incomplete "enode" with only a peer id + PeerId(PeerId), +} + +/// An error that can occur when converting an [`AnyNode`] to [`DialOpts`]. +#[derive(Debug, thiserror::Error)] +pub enum DialOptsError { + /// The peer id is not valid and cannot be converted to a secp256k1 public key. + #[error("Invalid peer id. Error: {0}")] + InvalidPeerId(secp256k1::Error), + /// The secp256k1 public key cannot be converted to a libp2p peer id. + #[error("Invalid public key. Error: {0}")] + InvalidPublicKey(#[from] discv5::libp2p_identity::DecodingError), +} + +impl AnyNode { + /// Returns the local peer id of the node. + pub fn peer_id(&self) -> PeerId { + match self { + Self::NodeRecord(record) => record.id, + Self::Enr(enr) => PeerId::from_slice(&enr.public_key().encode_uncompressed()), + Self::PeerId(peer_id) => *peer_id, + } + } + + /// Converts the [`AnyNode`] into [`DialOpts`]. + pub fn as_dial_opts(&self) -> Result { + let pub_key = &peer_id_to_secp256k1_pubkey(self.peer_id()) + .map_err(DialOptsError::InvalidPeerId)? + .serialize(); + + // codecov:ignore-start + // We ignore the code coverage because in theory, the serialization of the public key + // should never fail, but we don't want to panic in case of a bug. + let pub_key: discv5::libp2p_identity::PublicKey = + discv5::libp2p_identity::secp256k1::PublicKey::try_from_bytes(pub_key)?.into(); + // codecov:ignore-end + + let libp2p_id = libp2p::PeerId::from_public_key(&pub_key); + Ok(libp2p_id.into()) + } +} + +#[allow(clippy::from_over_into)] +impl TryInto for AnyNode { + type Error = DialOptsError; + + fn try_into(self) -> Result { + self.as_dial_opts() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::b512; + use std::str::FromStr; + + #[test] + fn test_into_dial_opts() { + let peer_id: PeerId = b512!( + "ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec" + ); + let any_node = AnyNode::from(peer_id); + let _: DialOpts = any_node.try_into().unwrap(); + } + + #[test] + fn test_into_dial_ops_error() { + let peer_id: PeerId = PeerId::ZERO; + let any_node = AnyNode::from(peer_id); + let res: DialOptsError = any_node.as_dial_opts().unwrap_err(); + assert!(matches!(res, DialOptsError::InvalidPeerId(_))); + } + + #[test] + fn test_peer_id_node_record() { + let raw = crate::OP_RAW_BOOTNODES[8]; + let record = NodeRecord::from_str(raw).unwrap(); + let any_node = AnyNode::from(record); + let peer_id = any_node.peer_id(); + let expected = b512!( + "ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec" + ); + assert_eq!(peer_id, expected); + } + + #[test] + fn test_peer_id_enr() { + let enr = Enr::from_str("enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPSOGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg").unwrap(); + let any_node = AnyNode::from(enr); + let peer_id = any_node.peer_id(); + let expected = b512!( + "99e4a7261ef38caac348fa0d3065e8a29783178861a63e48d0eb10514b78bbcb03c757c6bd6db80e8560bd176ce44781ca161dcacd49b54ee02b45d1a7135b18" + ); + assert_eq!(peer_id, expected); + } + + #[test] + fn test_peer_id_from_peer_id() { + let peer_id: PeerId = b512!( + "ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec" + ); + let any_node = AnyNode::from(peer_id); + let parsed = any_node.peer_id(); + assert_eq!(parsed, peer_id); + } +} diff --git a/kona/crates/node/peers/src/boot.rs b/kona/crates/node/peers/src/boot.rs new file mode 100644 index 0000000000000..dec450ebec113 --- /dev/null +++ b/kona/crates/node/peers/src/boot.rs @@ -0,0 +1,133 @@ +//! Contains the [`BootNode`] type which is used to represent a boot node in the network. + +use crate::{NodeRecord, enr_to_multiaddr}; +use derive_more::{Display, From}; +use discv5::{ + Enr, + multiaddr::{Multiaddr, Protocol}, +}; +use serde::{Deserialize, Serialize}; +use std::{net::IpAddr, str::FromStr}; + +use super::utils::{PeerIdConversionError, local_id_to_p2p_id}; + +/// A boot node can be added either as a string in either 'enode' URL scheme or serialized from +/// [`Enr`] type. +#[derive(Clone, Debug, PartialEq, Eq, Hash, From, Display, Serialize, Deserialize)] +pub enum BootNode { + /// An unsigned node record. + #[display("{_0}")] + Enode(Multiaddr), + /// A signed node record. + #[display("{_0:?}")] + Enr(Enr), +} + +impl BootNode { + /// Parses a [`NodeRecord`] and serializes according to CL format. Note: [`discv5`] is + /// originally a CL library hence needs this format to add the node. + pub fn from_unsigned(node_record: NodeRecord) -> Result { + let NodeRecord { address, udp_port, id, tcp_port } = node_record; + let mut multi_address = Multiaddr::empty(); + match address { + IpAddr::V4(ip) => multi_address.push(Protocol::Ip4(ip)), + IpAddr::V6(ip) => multi_address.push(Protocol::Ip6(ip)), + } + + multi_address.push(Protocol::Udp(udp_port)); + multi_address.push(Protocol::Tcp(tcp_port)); + multi_address.push(Protocol::P2p(local_id_to_p2p_id(id)?)); + + Ok(Self::Enode(multi_address)) + } + + /// Converts a [`BootNode`] into a [`Multiaddr`]. + pub fn to_multiaddr(&self) -> Option { + match self { + Self::Enode(addr) => Some(addr.clone()), + Self::Enr(enr) => enr_to_multiaddr(enr), + } + } + + /// Helper method to parse a bootnode from a string. + pub fn parse_bootnode(raw: &str) -> Self { + // If the string starts with "enr:" it is an ENR record. + if raw.starts_with("enr:") { + let enr = Enr::from_str(raw).unwrap(); + return Self::from(enr); + } + // Otherwise, attempt to use the Node Record format. + let record = NodeRecord::from_str(raw).unwrap(); + Self::from_unsigned(record).unwrap() + } +} + +#[cfg(test)] +mod tests { + use discv5::{ + enr::{CombinedPublicKey, k256}, + handler::NodeContact, + }; + + use crate::utils::peer_id_to_secp256k1_pubkey; + + use super::*; + use std::{net::Ipv4Addr, str::FromStr}; + + #[test] + fn test_derive_bootnode_enode_multiaddr() { + let hardcoded_enode = "enode://2bd2e657bb3c8efffb8ff6db9071d9eb7be70d7c6d7d980ff80fc93b2629675c5f750bc0a5ef27cd788c2e491b8795a7e9a4a6e72178c14acc6753c0e5d77ae4@34.65.205.244:30305"; + let node_record = NodeRecord::from_str(hardcoded_enode).unwrap(); + let boot_node = BootNode::from_unsigned(node_record).unwrap(); + + // Get the substring from hardcoded_enode between the second / and before the @ + let peer_id = hardcoded_enode + [hardcoded_enode.find('/').unwrap() + 2..hardcoded_enode.find('@').unwrap()] + .to_string(); + + let peer_id = crate::PeerId::from_str(&peer_id).unwrap(); + let p2p_peer_id = local_id_to_p2p_id(peer_id).unwrap(); + + let expected_multiaddr = Multiaddr::empty() + .with(Protocol::Ip4(Ipv4Addr::new(34, 65, 205, 244))) + .with(Protocol::Udp(30305)) + .with(Protocol::Tcp(30305)) + .with(Protocol::P2p(p2p_peer_id)); + + assert_eq!(boot_node.to_multiaddr(), Some(expected_multiaddr)); + } + + #[test] + fn test_derive_bootnode_enode_multiaddr_back_and_forth() { + let hardcoded_enode = "enode://2bd2e657bb3c8efffb8ff6db9071d9eb7be70d7c6d7d980ff80fc93b2629675c5f750bc0a5ef27cd788c2e491b8795a7e9a4a6e72178c14acc6753c0e5d77ae4@34.65.205.244:30305"; + let node_record = NodeRecord::from_str(hardcoded_enode).unwrap(); + let boot_node = BootNode::from_unsigned(node_record).unwrap(); + let multiaddr = boot_node.to_multiaddr().unwrap(); + + let node_contact = NodeContact::try_from_multiaddr(multiaddr).unwrap(); + + // The public key contained in the NodeContact is used to connect to the + // bootnode. + let contact_pkey = node_contact.public_key(); + + // We get the expected public key from the enode information. + let peer_id = hardcoded_enode + [hardcoded_enode.find('/').unwrap() + 2..hardcoded_enode.find('@').unwrap()] + .to_string(); + let peer_id = crate::PeerId::from_str(&peer_id).unwrap(); + + // The public key from the peer id is using the uncompressed form. + let pkey_secp256k1 = peer_id_to_secp256k1_pubkey(peer_id).unwrap(); + let p2p_public_key: discv5::libp2p_identity::secp256k1::PublicKey = + discv5::libp2p_identity::secp256k1::PublicKey::try_from_bytes( + &pkey_secp256k1.serialize(), + ) + .unwrap(); + + let expected_pkey: CombinedPublicKey = + k256::ecdsa::VerifyingKey::from_sec1_bytes(&p2p_public_key.to_bytes()).unwrap().into(); + + // These two keys should be equal. + assert_eq!(contact_pkey, expected_pkey); + } +} diff --git a/kona/crates/node/peers/src/enr.rs b/kona/crates/node/peers/src/enr.rs new file mode 100644 index 0000000000000..cc8237e732c2c --- /dev/null +++ b/kona/crates/node/peers/src/enr.rs @@ -0,0 +1,182 @@ +//! Contains the Optimism consensus-layer ENR Type. + +use alloy_rlp::{Decodable, Encodable}; +use discv5::Enr; +use unsigned_varint::{decode, encode}; + +/// Validates the [`Enr`] for the OP Stack. +#[derive(Debug, derive_more::Display, Clone, Default, PartialEq, Eq)] +pub enum EnrValidation { + /// Conversion error. + #[display("Conversion error: {_0}")] + ConversionError(OpStackEnrError), + /// Invalid Chain ID. + #[display("Invalid Chain ID: {_0}")] + InvalidChainId(u64), + /// Valid ENR. + #[default] + #[display("Valid ENR")] + Valid, +} + +impl EnrValidation { + /// Validates the [`Enr`] for the OP Stack. + pub fn validate(enr: &Enr, chain_id: u64) -> Self { + let opstack_enr = match OpStackEnr::try_from(enr) { + Ok(opstack_enr) => opstack_enr, + Err(e) => return Self::ConversionError(e), + }; + + if opstack_enr.chain_id != chain_id { + return Self::InvalidChainId(opstack_enr.chain_id); + } + + Self::Valid + } + + /// Returns `true` if the ENR is valid. + pub const fn is_valid(&self) -> bool { + matches!(self, Self::Valid) + } + + /// Returns `true` if the ENR is invalid. + pub const fn is_invalid(&self) -> bool { + !self.is_valid() + } +} + +/// The unique L2 network identifier +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct OpStackEnr { + /// Chain ID + pub chain_id: u64, + /// The version. Always set to 0. + pub version: u64, +} + +/// The error type that can be returned when trying to convert an [`Enr`] to an [`OpStackEnr`]. +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum OpStackEnrError { + /// Missing OP Stack ENR key. + #[error("Missing OP Stack ENR key")] + MissingKey, + /// Failed to decode the OP Stack ENR Value. + #[error("Failed to decode the OP Stack ENR Value: {0}")] + DecodeError(String), + /// Invalid version. + #[error("Invalid version: {0}")] + InvalidVersion(u64), +} + +impl TryFrom<&Enr> for OpStackEnr { + type Error = OpStackEnrError; + fn try_from(enr: &Enr) -> Result { + let Some(mut opstack) = enr.get_raw_rlp(Self::OP_CL_KEY) else { + return Err(OpStackEnrError::MissingKey); + }; + let opstack_enr = + Self::decode(&mut opstack).map_err(|e| OpStackEnrError::DecodeError(e.to_string()))?; + + if opstack_enr.version != 0 { + return Err(OpStackEnrError::InvalidVersion(opstack_enr.version)); + } + + Ok(opstack_enr) + } +} + +impl OpStackEnr { + /// The [`Enr`] key literal string for the consensus layer. + pub const OP_CL_KEY: &str = "opstack"; + + /// Constructs an [`OpStackEnr`] from a chain id. + pub const fn from_chain_id(chain_id: u64) -> Self { + Self { chain_id, version: 0 } + } +} + +impl Encodable for OpStackEnr { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let mut chain_id_buf = encode::u128_buffer(); + let chain_id_slice = encode::u128(self.chain_id as u128, &mut chain_id_buf); + + let mut version_buf = encode::u128_buffer(); + let version_slice = encode::u128(self.version as u128, &mut version_buf); + + let opstack = [chain_id_slice, version_slice].concat(); + alloy_primitives::Bytes::from(opstack).encode(out); + } +} + +impl Decodable for OpStackEnr { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let bytes = alloy_primitives::Bytes::decode(buf)?; + let (chain_id, rest) = decode::u64(&bytes) + .map_err(|_| alloy_rlp::Error::Custom("could not decode chain id"))?; + let (version, _) = + decode::u64(rest).map_err(|_| alloy_rlp::Error::Custom("could not decode chain id"))?; + Ok(Self { chain_id, version }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Bytes, bytes}; + use discv5::enr::CombinedKey; + + #[test] + #[cfg(feature = "arbitrary")] + fn roundtrip_op_stack_enr() { + arbtest::arbtest(|u| { + let op_stack_enr = OpStackEnr::from_chain_id(u.arbitrary()?); + let bytes = alloy_rlp::encode(op_stack_enr).to_vec(); + let decoded = OpStackEnr::decode(&mut &bytes[..]).unwrap(); + assert_eq!(decoded, op_stack_enr); + Ok(()) + }); + } + + #[test] + fn test_enr_validation() { + let key = CombinedKey::generate_secp256k1(); + let mut enr = Enr::builder().build(&key).unwrap(); + let op_stack_enr = OpStackEnr::from_chain_id(10); + let mut op_stack_bytes = Vec::new(); + op_stack_enr.encode(&mut op_stack_bytes); + enr.insert_raw_rlp(OpStackEnr::OP_CL_KEY, op_stack_bytes.into(), &key).unwrap(); + assert!(EnrValidation::validate(&enr, 10).is_valid()); + assert!(EnrValidation::validate(&enr, 11).is_invalid()); + } + + #[test] + fn test_enr_validation_invalid_version() { + let key = CombinedKey::generate_secp256k1(); + let mut enr = Enr::builder().build(&key).unwrap(); + let mut op_stack_enr = OpStackEnr::from_chain_id(10); + op_stack_enr.version = 1; + let mut op_stack_bytes = Vec::new(); + op_stack_enr.encode(&mut op_stack_bytes); + enr.insert_raw_rlp(OpStackEnr::OP_CL_KEY, op_stack_bytes.into(), &key).unwrap(); + assert!(EnrValidation::validate(&enr, 10).is_invalid()); + } + + #[test] + fn test_op_mainnet_enr() { + let op_enr = OpStackEnr::from_chain_id(10); + let bytes = alloy_rlp::encode(op_enr).to_vec(); + assert_eq!(Bytes::from(bytes.clone()), bytes!("820A00")); + let decoded = OpStackEnr::decode(&mut &bytes[..]).unwrap(); + assert_eq!(decoded, op_enr); + } + + #[test] + fn test_base_mainnet_enr() { + let base_enr = OpStackEnr::from_chain_id(8453); + let bytes = alloy_rlp::encode(base_enr).to_vec(); + assert_eq!(Bytes::from(bytes.clone()), bytes!("83854200")); + let decoded = OpStackEnr::decode(&mut &bytes[..]).unwrap(); + assert_eq!(decoded, base_enr); + } +} diff --git a/kona/crates/node/peers/src/lib.rs b/kona/crates/node/peers/src/lib.rs new file mode 100644 index 0000000000000..46f96edd39bc0 --- /dev/null +++ b/kona/crates/node/peers/src/lib.rs @@ -0,0 +1,45 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[macro_use] +extern crate tracing; + +/// Alias for a peer identifier. +/// +/// This is the most primitive secp256k1 public key identifier for a given peer. +pub type PeerId = alloy_primitives::B512; + +mod nodes; +pub use nodes::{BootNodes, OP_RAW_BOOTNODES, OP_RAW_TESTNET_BOOTNODES}; + +mod store; +pub use store::{BootStore, BootStoreFile}; + +mod score; +pub use score::PeerScoreLevel; + +mod enr; +pub use enr::{EnrValidation, OpStackEnr, OpStackEnrError}; + +mod any; +pub use any::{AnyNode, DialOptsError}; + +mod boot; +pub use boot::BootNode; + +mod record; +pub use record::{NodeRecord, NodeRecordParseError}; + +mod utils; +pub use utils::{ + PeerIdConversionError, enr_to_multiaddr, local_id_to_p2p_id, peer_id_to_secp256k1_pubkey, +}; + +mod monitoring; +pub use monitoring::PeerMonitoring; diff --git a/kona/crates/node/peers/src/monitoring.rs b/kona/crates/node/peers/src/monitoring.rs new file mode 100644 index 0000000000000..82062d1f1d587 --- /dev/null +++ b/kona/crates/node/peers/src/monitoring.rs @@ -0,0 +1,12 @@ +//! Specifies peer monitoring configuration. + +use std::time::Duration; + +/// Specifies how to monitor peer scores and ban poorly performing peers. +#[derive(Debug, Clone)] +pub struct PeerMonitoring { + /// The threshold under which a peer should be banned. + pub ban_threshold: f64, + /// The duration of a peer's ban. + pub ban_duration: Duration, +} diff --git a/kona/crates/node/peers/src/nodes.rs b/kona/crates/node/peers/src/nodes.rs new file mode 100644 index 0000000000000..78bed713a4c22 --- /dev/null +++ b/kona/crates/node/peers/src/nodes.rs @@ -0,0 +1,188 @@ +//! Bootnodes for consensus network discovery. + +use crate::BootNode; +use derive_more::Deref; +use lazy_static::lazy_static; + +use kona_registry::CHAINS; + +/// Bootnodes for OP Stack chains. +#[derive(Debug, Clone, Deref, PartialEq, Eq, Default, derive_more::From)] +pub struct BootNodes(pub Vec); + +impl BootNodes { + /// Returns the bootnodes for the given chain id. + /// + /// If the chain id is not recognized, no bootnodes are returned. + pub fn from_chain_id(id: u64) -> Self { + let Some(chain) = CHAINS.get_chain_by_id(id) else { + return Self(vec![]); + }; + match chain.parent.chain_id() { + 1 => Self::mainnet(), + 11155111 => Self::testnet(), + _ => Self(vec![]), + } + } + + /// Returns the bootnodes for the mainnet. + pub fn mainnet() -> Self { + Self(OP_BOOTNODES.clone()) + } + + /// Returns the bootnodes for the testnet. + pub fn testnet() -> Self { + Self(OP_TESTNET_BOOTNODES.clone()) + } + + /// Returns the length of the bootnodes. + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Returns if the bootnodes are empty. + pub const fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +lazy_static! { + /// Default op bootnodes to use. + static ref OP_BOOTNODES: Vec = OP_RAW_BOOTNODES.iter() + .map(|raw| BootNode::parse_bootnode(raw)) + .collect(); + + /// Default op testnet bootnodes to use. + static ref OP_TESTNET_BOOTNODES: Vec = OP_RAW_TESTNET_BOOTNODES.iter() + .map(|raw| BootNode::parse_bootnode(raw)) + .collect(); +} + +/// OP stack mainnet boot nodes. +pub static OP_RAW_BOOTNODES: &[&str] = &[ + // OP Mainnet Bootnodes + "enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPSOGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg", + "enr:-J64QAlTCDa188Hl1OGv5_2Kj2nWCsvxMVc_rEnLtw7RPFbOfqUOV6khXT_PH6cC603I2ynY31rSQ8sI9gLeJbfFGaWGAYYFIrpdgmlkgnY0gmlwhANWgzCHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECkySjcg-2v0uWAsFsZZu43qNHppGr2D5F913Qqs5jDCGDdGNwgiQGg3VkcIIkBg", + "enr:-J24QGEzN4mJgLWNTUNwj7riVJ2ZjRLenOFccl2dbRFxHHOCCZx8SXWzgf-sLzrGs6QgqSFCvGXVgGPBkRkfOWlT1-iGAYe6Cu93gmlkgnY0gmlwhCJBEUSHb3BzdGFja4OkAwCJc2VjcDI1NmsxoQLuYIwaYOHg3CUQhCkS-RsSHmUd1b_x93-9yQ5ItS6udIN0Y3CCIyuDdWRwgiMr", + // Base Mainnet Bootnodes + "enr:-J24QNz9lbrKbN4iSmmjtnr7SjUMk4zB7f1krHZcTZx-JRKZd0kA2gjufUROD6T3sOWDVDnFJRvqBBo62zuF-hYCohOGAYiOoEyEgmlkgnY0gmlwhAPniryHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQKNVFlCxh_B-716tTs-h1vMzZkSs1FTu_OYTNjgufplG4N0Y3CCJAaDdWRwgiQG", + "enr:-J24QH-f1wt99sfpHy4c0QJM-NfmsIfmlLAMMcgZCUEgKG_BBYFc6FwYgaMJMQN5dsRBJApIok0jFn-9CS842lGpLmqGAYiOoDRAgmlkgnY0gmlwhLhIgb2Hb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJ9FTIv8B9myn1MWaC_2lJ-sMoeCDkusCsk4BYHjjCq04N0Y3CCJAaDdWRwgiQG", + "enr:-J24QDXyyxvQYsd0yfsN0cRr1lZ1N11zGTplMNlW4xNEc7LkPXh0NAJ9iSOVdRO95GPYAIc6xmyoCCG6_0JxdL3a0zaGAYiOoAjFgmlkgnY0gmlwhAPckbGHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJwoS7tzwxqXSyFL7g0JM-KWVbgvjfB8JA__T7yY_cYboN0Y3CCJAaDdWRwgiQG", + "enr:-J24QHmGyBwUZXIcsGYMaUqGGSl4CFdx9Tozu-vQCn5bHIQbR7On7dZbU61vYvfrJr30t0iahSqhc64J46MnUO2JvQaGAYiOoCKKgmlkgnY0gmlwhAPnCzSHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQINc4fSijfbNIiGhcgvwjsjxVFJHUstK9L1T8OTKUjgloN0Y3CCJAaDdWRwgiQG", + "enr:-J24QG3ypT4xSu0gjb5PABCmVxZqBjVw9ca7pvsI8jl4KATYAnxBmfkaIuEqy9sKvDHKuNCsy57WwK9wTt2aQgcaDDyGAYiOoGAXgmlkgnY0gmlwhDbGmZaHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQIeAK_--tcLEiu7HvoUlbV52MspE0uCocsx1f_rYvRenIN0Y3CCJAaDdWRwgiQG", + // OP Labs + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305", + "enode://dd751a9ef8912be1bfa7a5e34e2c3785cc5253110bd929f385e07ba7ac19929fb0e0c5d93f77827291f4da02b2232240fbc47ea7ce04c46e333e452f8656b667@34.65.107.0:30305", + "enode://c5d289b56a77b6a2342ca29956dfd07aadf45364dde8ab20d1dc4efd4d1bc6b4655d902501daea308f4d8950737a4e93a4dfedd17b49cd5760ffd127837ca965@34.65.202.239:30305", + // Base + "enode://87a32fd13bd596b2ffca97020e31aef4ddcc1bbd4b95bb633d16c1329f654f34049ed240a36b449fda5e5225d70fe40bc667f53c304b71f8e68fc9d448690b51@3.231.138.188:30301", + "enode://ca21ea8f176adb2e229ce2d700830c844af0ea941a1d8152a9513b966fe525e809c3a6c73a2c18a12b74ed6ec4380edf91662778fe0b79f6a591236e49e176f9@184.72.129.189:30301", + "enode://acf4507a211ba7c1e52cdf4eef62cdc3c32e7c9c47998954f7ba024026f9a6b2150cd3f0b734d9c78e507ab70d59ba61dfe5c45e1078c7ad0775fb251d7735a2@3.220.145.177:30301", + "enode://8a5a5006159bf079d06a04e5eceab2a1ce6e0f721875b2a9c96905336219dbe14203d38f70f3754686a6324f786c2f9852d8c0dd3adac2d080f4db35efc678c5@3.231.11.52:30301", + "enode://cdadbe835308ad3557f9a1de8db411da1a260a98f8421d62da90e71da66e55e98aaa8e90aa7ce01b408a54e4bd2253d701218081ded3dbe5efbbc7b41d7cef79@54.198.153.150:30301", + // Conduit + // "enode://d25ce99435982b04d60c4b41ba256b84b888626db7bee45a9419382300fbe907359ae5ef250346785bff8d3b9d07cd3e017a27e2ee3cfda3bcbb0ba762ac9674@bootnode.conduit.xyz:0?discport=30301", + "enode://2d4e7e9d48f4dd4efe9342706dd1b0024681bd4c3300d021f86fc75eab7865d4e0cbec6fbc883f011cfd6a57423e7e2f6e104baad2b744c3cafaec6bc7dc92c1@34.65.43.171:0?discport=30305", + "enode://9d7a3efefe442351217e73b3a593bcb8efffb55b4807699972145324eab5e6b382152f8d24f6301baebbfb5ecd4127bd3faab2842c04cd432bdf50ba092f6645@34.65.109.126:0?discport=30305", + // Uniswap Labs + "enode://010800c668896c100e8d64abc388ac5a22a8134a96fb0107c5d0c56d79ba7225c12d9e9e012d3cc0ee2701d7f63dd45f8abf0bbcf6f3c541f91742b1d7a99355@3.134.214.169:9222", + "enode://b97abcc7011d06299c4bc44742be4a0e631a1a2925a2992adcfe80ed86bec5ff0ddf1b90d015f2dbb5e305560e12c9873b2dad72d84d131ac4be9f2a4c74b763@52.14.30.39:9222", + "enode://760230a662610620d6d2e4ad846a6dccbceaa4556872dfacf9cdca7c2f5b49e4c66e822ed2e8813debb5fb7391f0519b8d075e565a2a89c79a9e4092e81b3e5b@3.148.100.173:9222", + "enode://b1a743328188dba3b2ed8c06abbb2688fabe64a3251e43bd77d4e5265bbd5cf03eca8ace4cde8ddb0c49c409b90bf941ebf556094638c6203edd6baa5ef0091b@3.134.214.169:30303", + "enode://ea9eaaf695facbe53090beb7a5b0411a81459bbf6e6caac151e587ee77120a1b07f3b9f3a9550f797d73d69840a643b775fd1e40344dea11e7660b6a483fe80e@52.14.30.39:30303", + "enode://77b6b1e72984d5d50e00ae934ffea982902226fe92fa50da42334c2750d8e405b55a5baabeb988c88125368142a64eda5096d0d4522d3b6eef75d166c7d303a9@3.148.100.173:30303", +]; + +/// OP stack testnet boot nodes. +pub static OP_RAW_TESTNET_BOOTNODES: &[&str] = &[ + // OP Labs + "enode://2bd2e657bb3c8efffb8ff6db9071d9eb7be70d7c6d7d980ff80fc93b2629675c5f750bc0a5ef27cd788c2e491b8795a7e9a4a6e72178c14acc6753c0e5d77ae4@34.65.205.244:30305", + "enode://db8e1cab24624cc62fc35dbb9e481b88a9ef0116114cd6e41034c55b5b4f18755983819252333509bd8e25f6b12aadd6465710cd2e956558faf17672cce7551f@34.65.173.88:30305", + "enode://bfda2e0110cfd0f4c9f7aa5bf5ec66e6bd18f71a2db028d36b8bf8b0d6fdb03125c1606a6017b31311d96a36f5ef7e1ad11604d7a166745e6075a715dfa67f8a@34.65.229.245:30305", + // Base + "enode://548f715f3fc388a7c917ba644a2f16270f1ede48a5d88a4d14ea287cc916068363f3092e39936f1a3e7885198bef0e5af951f1d7b1041ce8ba4010917777e71f@18.210.176.114:30301", + "enode://6f10052847a966a725c9f4adf6716f9141155b99a0fb487fea3f51498f4c2a2cb8d534e680ee678f9447db85b93ff7c74562762c3714783a7233ac448603b25f@107.21.251.55:30301", + // Uniswap Labs + "enode://9e138a8ec4291c4f2fe5851aaee44fc73ae67da87fb26b75e3b94183c7ffc15b2795afc816b0aa084151b95b3a3553f1cd0d1e9dd134dcf059a84d4e0b429afc@3.146.117.118:30303", + "enode://34d87d649e5c58a17a43c1d59900a2020bd82d5b12ea39467c3366bee2946aaa9c759c77ede61089624691291fb2129eeb2a47687b50e2463188c78e1f738cf2@52.15.54.8:30303", + "enode://c2405194166fe2c0e6c61ee469745fed1a6802f51c8fc39e1c78c21c9a6a15a7c55304f09ee37e430da9a1ce8117ca085263c6b0f474f6946811e398347611ef@3.146.213.65:30303", +]; + +#[cfg(test)] +mod tests { + use discv5::{Enr, enr::EnrPublicKey}; + use std::str::FromStr; + + use kona_genesis::{BASE_MAINNET_CHAIN_ID, OP_MAINNET_CHAIN_ID, OP_SEPOLIA_CHAIN_ID}; + + use super::*; + + #[test] + fn test_validate_bootnode_lens() { + assert_eq!(OP_RAW_BOOTNODES.len(), 24); + assert_eq!(OP_RAW_TESTNET_BOOTNODES.len(), 8); + } + + #[test] + fn test_parse_raw_bootnodes() { + for raw in OP_RAW_BOOTNODES.iter() { + BootNode::parse_bootnode(raw); + } + + for raw in OP_RAW_TESTNET_BOOTNODES.iter() { + BootNode::parse_bootnode(raw); + } + } + + #[test] + fn test_bootnodes_from_chain_id() { + let mainnet = BootNodes::from_chain_id(OP_MAINNET_CHAIN_ID); + assert_eq!(mainnet.len(), 24); + + let mainnet = BootNodes::from_chain_id(BASE_MAINNET_CHAIN_ID); + assert_eq!(mainnet.len(), 24); + + let mainnet = BootNodes::from_chain_id(130 /* Unichain Mainnet */); + assert_eq!(mainnet.len(), 24); + + let testnet = BootNodes::from_chain_id(OP_SEPOLIA_CHAIN_ID); + assert_eq!(testnet.len(), 8); + + let testnet = BootNodes::from_chain_id(1301 /* Unichain Sepolia */); + assert_eq!(testnet.len(), 8); + + let unknown = BootNodes::from_chain_id(0); + assert!(unknown.is_empty()); + } + + #[test] + fn test_bootnodes_len() { + let bootnodes = BootNodes::mainnet(); + assert_eq!(bootnodes.len(), 24); + + let bootnodes = BootNodes::testnet(); + assert_eq!(bootnodes.len(), 8); + } + + #[test] + fn parse_enr() { + const ENR: &str = "enr:-Jy4QHRgJ9rnWbTs0oOfv8IHt77NDhHE3rwXf3fCh8RRN8sze4gyuQ2MkAapZwneDd_LH77TGCRS5N4wPGm-J5Hh-oCDAQKOgmlkgnY0gmlwhC36_pOHb3BzdGFja4Xkq4MBAIlzZWNwMjU2azGhAqUtGspoH5IzIIAwaqcipQFWripEU12KAiKFqRKDCZWxg3RjcIIjK4N1ZHCCIys"; + const PEER_ID: &str = "16Uiu2HAm6YT98Hd3qAtop3TFM75uXvuyEhZYwPCfZ9mzRckmFkmW"; + + let enr = Enr::from_str(ENR).unwrap(); + let pub_key = enr.public_key(); + let pub_key = + libp2p_identity::secp256k1::PublicKey::try_from_bytes(&pub_key.encode()).unwrap(); + + assert_eq!(PEER_ID, libp2p_identity::PeerId::from_public_key(&pub_key.into()).to_string()); + } + + #[test] + fn test_bootnodes_empty() { + let bootnodes = BootNodes(vec![]); + assert!(bootnodes.is_empty()); + + let bootnodes = BootNodes::from_chain_id(OP_MAINNET_CHAIN_ID); + assert!(!bootnodes.is_empty()); + } +} diff --git a/kona/crates/node/peers/src/record.rs b/kona/crates/node/peers/src/record.rs new file mode 100644 index 0000000000000..3ea22342ff5cd --- /dev/null +++ b/kona/crates/node/peers/src/record.rs @@ -0,0 +1,225 @@ +//! Commonly used `NodeRecord` type for peers. +//! +//! This is a simplified version of the `NodeRecord` type in reth. +//! +//! Adapted from . + +use crate::PeerId; +use core::{ + fmt, + fmt::Write, + net::{IpAddr, SocketAddr}, + num::ParseIntError, + str::FromStr, +}; +use std::net::ToSocketAddrs; + +/// Represents an ENR in discovery. +/// +/// Note: this is only an excerpt of the [`NodeRecord`] data structure. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct NodeRecord { + /// The Address of a node. + pub address: IpAddr, + /// UDP discovery port. + pub udp_port: u16, + /// TCP port of the port that accepts connections. + pub tcp_port: u16, + /// Public key of the discovery service + pub id: PeerId, +} + +impl NodeRecord { + /// Converts the `address` into an [`core::net::Ipv4Addr`] if the `address` is a mapped + /// [`Ipv6Addr`](std::net::Ipv6Addr). + /// + /// Returns `true` if the address was converted. + /// + /// See also [`std::net::Ipv6Addr::to_ipv4_mapped`] + pub fn convert_ipv4_mapped(&mut self) -> bool { + // convert IPv4 mapped IPv6 address + if let IpAddr::V6(v6) = self.address { + if let Some(v4) = v6.to_ipv4_mapped() { + self.address = v4.into(); + return true; + } + } + false + } + + /// Same as [`Self::convert_ipv4_mapped`] but consumes the type + pub fn into_ipv4_mapped(mut self) -> Self { + self.convert_ipv4_mapped(); + self + } + + /// Sets the tcp port + pub const fn with_tcp_port(mut self, port: u16) -> Self { + self.tcp_port = port; + self + } + + /// Sets the udp port + pub const fn with_udp_port(mut self, port: u16) -> Self { + self.udp_port = port; + self + } + + /// Creates a new record from a socket addr and peer id. + pub const fn new(addr: SocketAddr, id: PeerId) -> Self { + Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id } + } + + /// Creates a new record from an ip address and ports. + pub fn new_with_ports( + ip_addr: IpAddr, + tcp_port: u16, + udp_port: Option, + id: PeerId, + ) -> Self { + let udp_port = udp_port.unwrap_or(tcp_port); + Self { address: ip_addr, tcp_port, udp_port, id } + } + + /// The TCP socket address of this node + #[must_use] + pub const fn tcp_addr(&self) -> SocketAddr { + SocketAddr::new(self.address, self.tcp_port) + } + + /// The UDP socket address of this node + #[must_use] + pub const fn udp_addr(&self) -> SocketAddr { + SocketAddr::new(self.address, self.udp_port) + } +} + +impl fmt::Display for NodeRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("enode://")?; + alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?; + f.write_char('@')?; + match self.address { + IpAddr::V4(ip) => { + ip.fmt(f)?; + } + IpAddr::V6(ip) => { + // encapsulate with brackets + f.write_char('[')?; + ip.fmt(f)?; + f.write_char(']')?; + } + } + f.write_char(':')?; + self.tcp_port.fmt(f)?; + if self.tcp_port != self.udp_port { + f.write_str("?discport=")?; + self.udp_port.fmt(f)?; + } + + Ok(()) + } +} + +/// Possible error types when parsing a [`NodeRecord`] +#[derive(Debug, thiserror::Error)] +pub enum NodeRecordParseError { + /// Invalid url + #[error("Failed to parse url: {0}")] + InvalidUrl(String), + /// Invalid id + #[error("Failed to parse id")] + InvalidId(String), + /// Invalid discport + #[error("Failed to discport query: {0}")] + Discport(ParseIntError), +} + +impl FromStr for NodeRecord { + type Err = NodeRecordParseError; + + fn from_str(s: &str) -> Result { + use url::{Host, Url}; + + let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?; + + let port = url + .port() + .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?; + + let address = match url.host() { + Some(Host::Ipv4(ip)) => IpAddr::V4(ip), + Some(Host::Ipv6(ip)) => IpAddr::V6(ip), + Some(Host::Domain(dns)) => format!("{dns}:{port}") + .to_socket_addrs() + .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))? + .next() + .map(|addr| addr.ip()) + .ok_or_else(|| { + NodeRecordParseError::InvalidUrl(format!("no IP found for host: {url:?}")) + })?, + + _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))), + }; + + let udp_port = if let Some(discovery_port) = url + .query_pairs() + .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port)) + { + discovery_port.parse::().map_err(NodeRecordParseError::Discport)? + } else { + port + }; + + let id = url + .username() + .parse::() + .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?; + + Ok(Self { address, id, tcp_port: port, udp_port }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_url_parse_domain() { + let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:30303?discport=30301"; + let node: NodeRecord = url.parse().unwrap(); + let localhost_socket_addr = "localhost:30303".to_socket_addrs().unwrap().next().unwrap(); + assert_eq!(node, NodeRecord { + address: localhost_socket_addr.ip(), + tcp_port: 30303, + udp_port: 30301, + id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(), + }) + } + + #[test] + fn test_url_parse() { + let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301"; + let node: NodeRecord = url.parse().unwrap(); + assert_eq!(node, NodeRecord { + address: IpAddr::V4([10,3,58,6].into()), + tcp_port: 30303, + udp_port: 30301, + id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(), + }) + } + + #[test] + fn test_node_display() { + let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303"; + let node: NodeRecord = url.parse().unwrap(); + assert_eq!(url, &format!("{node}")); + } + + #[test] + fn test_node_display_discport() { + let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301"; + let node: NodeRecord = url.parse().unwrap(); + assert_eq!(url, &format!("{node}")); + } +} diff --git a/kona/crates/node/peers/src/score.rs b/kona/crates/node/peers/src/score.rs new file mode 100644 index 0000000000000..807f9879bda6f --- /dev/null +++ b/kona/crates/node/peers/src/score.rs @@ -0,0 +1,144 @@ +//! Contains peer scoring types. + +use derive_more::{Display, FromStr}; +use libp2p::gossipsub::{PeerScoreParams, PeerScoreThresholds, TopicHash, TopicScoreParams}; +use std::collections::HashMap; + +/// The peer scoring level is used to determine +/// how peers are scored based on their behavior. +#[derive(Debug, FromStr, Display, Default, Clone, Copy, PartialEq, Eq)] +pub enum PeerScoreLevel { + /// No peer scoring is applied. + #[default] + Off, + /// Light peer scoring is applied. + Light, +} + +impl PeerScoreLevel { + /// Decay to zero is the decay factor for a peer's score to zero. + pub const DECAY_TO_ZERO: f64 = 0.01; + + /// Mesh weight is the weight of the mesh delivery topic. + pub const MESH_WEIGHT: f64 = -0.7; + + /// Max in mesh score is the maximum score for being in the mesh. + pub const MAX_IN_MESH_SCORE: f64 = 10.0; + + /// Decay epoch is the number of epochs to decay the score over. + pub const DECAY_EPOCH: f64 = 5.0; + + /// Helper function to calculate the decay factor for a given duration. + /// The decay factor is calculated using the formula: + /// `decay_factor = (1 - decay_to_zero) ^ (duration / slot)`. + pub fn score_decay(duration: std::time::Duration, slot: std::time::Duration) -> f64 { + let num_of_times = duration.as_secs() / slot.as_secs(); + (1.0 - Self::DECAY_TO_ZERO).powf(1.0 / num_of_times as f64) + } + + /// Default peer score thresholds. + pub const DEFAULT_PEER_SCORE_THRESHOLDS: PeerScoreThresholds = PeerScoreThresholds { + gossip_threshold: -10.0, + publish_threshold: -40.0, + graylist_threshold: -40.0, + accept_px_threshold: 20.0, + opportunistic_graft_threshold: 0.05, + }; + + /// Returns a cap on the in mesh score. + /// The cap is calculated based on the slot duration. + /// The formula used is: + /// `cap = (3600 * time.Second) / slot`. + pub fn in_mesh_cap(slot: std::time::Duration) -> f64 { + (3600 * std::time::Duration::from_secs(1)).as_secs_f64() / slot.as_secs_f64() + } + + /// Returns the topic score parameters given the block time. + pub fn topic_score_params(block_time: u64) -> TopicScoreParams { + let slot = std::time::Duration::from_secs(block_time); + let epoch = slot * 6; + let invalid_decay_period = 50 * epoch; + let decay_epoch = + std::time::Duration::from_secs(Self::DECAY_EPOCH as u64 * epoch.as_secs()); + TopicScoreParams { + topic_weight: 0.8, + time_in_mesh_weight: Self::MAX_IN_MESH_SCORE / Self::in_mesh_cap(slot), + time_in_mesh_quantum: slot, + time_in_mesh_cap: Self::in_mesh_cap(slot), + first_message_deliveries_weight: 1.0, + first_message_deliveries_decay: Self::score_decay(20 * epoch, slot), + first_message_deliveries_cap: 23.0, + mesh_message_deliveries_weight: Self::MESH_WEIGHT, + mesh_message_deliveries_decay: Self::score_decay(decay_epoch, slot), + mesh_message_deliveries_cap: (epoch.as_secs() / slot.as_secs()) as f64 * + Self::DECAY_EPOCH, + mesh_message_deliveries_threshold: (epoch.as_secs() / slot.as_secs()) as f64 * + Self::DECAY_EPOCH / + 10.0, + mesh_message_deliveries_window: std::time::Duration::from_secs(2), + mesh_message_deliveries_activation: epoch * 4, + mesh_failure_penalty_weight: Self::MESH_WEIGHT, + mesh_failure_penalty_decay: Self::score_decay(decay_epoch, slot), + invalid_message_deliveries_weight: -140.4475, + invalid_message_deliveries_decay: Self::score_decay(invalid_decay_period, slot), + } + } + + /// Constructs topic scores for the given topics. + pub fn topic_scores( + topics: Vec, + block_time: u64, + ) -> HashMap { + let mut topic_scores = HashMap::with_capacity(topics.len()); + for topic in topics { + debug!(target: "scoring", "Topic scoring enabled on topic: {}", topic); + topic_scores.insert(topic, Self::topic_score_params(block_time)); + } + topic_scores + } + + /// Returns the [`PeerScoreParams`] for the given peer scoring level. + /// + /// # Arguments + /// * `block_time` - The block time in seconds. + pub fn to_params( + &self, + topics: Vec, + topic_scoring: bool, + block_time: u64, + ) -> Option { + let slot = std::time::Duration::from_secs(block_time); + debug!(target: "scoring", "Slot duration: {:?}", slot); + let epoch = slot * 6; + let ten_epochs = epoch * 10; + let one_hundred_epochs = epoch * 100; + let penalty_decay = Self::score_decay(ten_epochs, slot); + let topics = + if topic_scoring { Self::topic_scores(topics, block_time) } else { Default::default() }; + match self { + Self::Off => None, + Self::Light => Some(PeerScoreParams { + topics, + topic_score_cap: 34.0, + app_specific_weight: 1.0, + ip_colocation_factor_weight: -35.0, + ip_colocation_factor_threshold: 10.0, + ip_colocation_factor_whitelist: Default::default(), + behaviour_penalty_weight: -16.0, + behaviour_penalty_threshold: 6.0, + behaviour_penalty_decay: penalty_decay, + decay_interval: slot, + decay_to_zero: Self::DECAY_TO_ZERO, + retain_score: one_hundred_epochs, + slow_peer_weight: -0.2, // default + slow_peer_threshold: 0.0, // default + slow_peer_decay: 0.2, + }), + } + } + + /// Returns the [`PeerScoreThresholds`]. + pub const fn thresholds() -> PeerScoreThresholds { + Self::DEFAULT_PEER_SCORE_THRESHOLDS + } +} diff --git a/kona/crates/node/peers/src/store.rs b/kona/crates/node/peers/src/store.rs new file mode 100644 index 0000000000000..57da34fd69adc --- /dev/null +++ b/kona/crates/node/peers/src/store.rs @@ -0,0 +1,209 @@ +//! Bootnode Store + +use discv5::Enr; +use std::{ + collections::VecDeque, + fs::File, + io::{BufReader, Seek, SeekFrom}, + path::PathBuf, +}; + +/// The maximum number of peers that can be stored in the bootstore. +const MAX_PEERS: usize = 2048; + +/// On-disk storage for [`Enr`]s. +/// +/// The [`BootStore`] is a simple JSON file that holds the list of [`Enr`]s that have been +/// successfully peered. +/// +/// When the number of peers within the [`BootStore`] exceeds `MAX_PEERS`, the oldest peers are +/// removed to make room for new ones. +#[derive(Debug, serde::Serialize, Default)] +pub struct BootStore { + /// The file for the [`BootStore`]. + #[serde(skip)] + pub file: Option, + /// [`Enr`]s for peers. + pub peers: VecDeque, +} + +/// The bootstore caching policy. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BootStoreFile { + /// Default path for the bootstore, ie `~/.kona//bootstore.json`. + Default { + /// The l2 chain ID. + chain_id: u64, + }, + /// A custom bootstore path is used. This must be a valid path to a file. + Custom(PathBuf), +} + +impl From for BootStore { + fn from(file: File) -> Self { + let peers = peers_from_file(&file); + Self { file: Some(file), peers } + } +} + +impl TryInto for BootStoreFile { + type Error = std::io::Error; + + /// Returns a pointer to the bootstore file for the given combination of chain id and bootstore + /// file type. + fn try_into(self) -> Result { + let path = TryInto::::try_into(self)?; + File::options().read(true).write(true).create(true).truncate(false).open(path) + } +} + +impl TryInto for BootStoreFile { + type Error = std::io::Error; + + fn try_into(self) -> Result { + let file = TryInto::::try_into(self)?; + Ok(file.into()) + } +} + +impl TryInto for BootStoreFile { + type Error = std::io::Error; + + fn try_into(self) -> Result { + match self { + Self::Default { chain_id } => { + let mut path = dirs::home_dir() + .ok_or(std::io::Error::other("Failed to get home directory"))?; + path.push(".kona"); + path.push(chain_id.to_string()); + path.push("bootstore.json"); + Ok(path) + } + Self::Custom(path) => Ok(path), + } + } +} + +fn peers_from_file(file: &File) -> VecDeque { + debug!(target: "bootstore", "Reading boot store from disk: {:?}", file); + let reader = BufReader::new(file); + match serde_json::from_reader(reader).map(|s: BootStore| s.peers) { + Ok(peers) => peers, + Err(e) => { + warn!(target: "bootstore", "Failed to read boot store from disk: {:?}", e); + VecDeque::new() + } + } +} + +// This custom implementation of `Deserialize` allows us to ignore +// enrs that have an invalid string format in the store. +impl<'de> serde::Deserialize<'de> for BootStore { + fn deserialize>(deserializer: D) -> Result { + let peers: Vec = serde::Deserialize::deserialize(deserializer)?; + let mut store = Self { file: None, peers: VecDeque::new() }; + for peer in peers { + match serde_json::from_value::(peer) { + Ok(enr) => { + store.peers.push_back(enr); + } + Err(e) => { + warn!(target: "peers_store", "Failed to deserialize ENR: {:?}", e); + } + } + } + Ok(store) + } +} + +impl BootStore { + /// Adds an [`Enr`] to the store. + /// + /// This method will **note** panic on failure to write to disk. Instead, it is the + /// responsibility of the caller to ensure the store is written to disk by calling + /// [`BootStore::sync`] prior to dropping the store. + pub fn add_enr(&mut self, enr: Enr) { + self.add_rotate(enr); + } + + /// Returns the number of peers in the bootstore that + /// have the [`crate::OpStackEnr::OP_CL_KEY`] in the ENR. + pub fn valid_peers(&self) -> Vec<&Enr> { + self.peers + .iter() + .filter(|enr| enr.get_raw_rlp(crate::OpStackEnr::OP_CL_KEY.as_bytes()).is_some()) + .collect() + } + + /// Returns the number of peers that contain the + /// [`crate::OpStackEnr::OP_CL_KEY`] in the ENR *and* + /// have the correct chain id and version. + pub fn valid_peers_with_chain_id(&self, chain_id: u64) -> Vec<&Enr> { + self.peers + .iter() + .filter(|enr| crate::EnrValidation::validate(enr, chain_id).is_valid()) + .collect() + } + + /// Returns the number of peers in the in-memory store. + pub fn len(&self) -> usize { + self.peers.len() + } + + /// Returns if the in-memory store is empty. + pub fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + /// Merges the given list of [`Enr`]s with the current list of peers. + pub fn merge(&mut self, peers: impl IntoIterator) { + peers.into_iter().for_each(|peer| self.add_rotate(peer)); + } + + /// Syncs the [`BootStore`] with the contents on disk. + pub fn sync(&mut self) -> Result<(), std::io::Error> { + if let Some(file) = &mut self.file { + // Reset the file pointer to the beginning of the file to overwrite the file. + // Reset file pointer AND truncate + file.seek(SeekFrom::Start(0))?; + file.set_len(0)?; + + serde_json::to_writer(file, &self.peers)?; + } + Ok(()) + } + + /// Returns all available bootstores for the given data directory. + pub fn available(datadir: Option) -> Vec { + let mut bootstores = Vec::new(); + let path = datadir.unwrap_or_else(|| { + let mut home = dirs::home_dir().expect("Failed to get home directory"); + home.push(".kona"); + home + }); + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries.flatten() { + if let Ok(chain_id) = entry.file_name().to_string_lossy().parse::() { + bootstores.push(chain_id); + } + } + } + bootstores + } + + /// Adds an [`Enr`] to the store, rotating the oldest peer out if necessary. + fn add_rotate(&mut self, enr: Enr) { + if self.peers.contains(&enr) { + return; + } + + debug!(target: "bootstore", "Adding enr to the boot store: {}", enr); + self.peers.push_back(enr); + + // Prune the oldest peer if we exceed the maximum number of peers. + if self.peers.len() > MAX_PEERS { + debug!(target: "bootstore", "Boot store exceeded maximum peers, removing oldest peer"); + self.peers.pop_front(); + } + } +} diff --git a/kona/crates/node/peers/src/utils.rs b/kona/crates/node/peers/src/utils.rs new file mode 100644 index 0000000000000..c76d523ede365 --- /dev/null +++ b/kona/crates/node/peers/src/utils.rs @@ -0,0 +1,205 @@ +//! Utilities to translate types. + +use discv5::{ + Enr, + enr::{CombinedPublicKey, EnrPublicKey}, + multiaddr::Protocol, +}; +use libp2p::Multiaddr; + +use super::PeerId; + +/// Converts an [`Enr`] into a [`Multiaddr`]. +pub fn enr_to_multiaddr(enr: &Enr) -> Option { + let mut addr = if let Some(socket) = enr.tcp4_socket() { + let mut addr = Multiaddr::from(*socket.ip()); + addr.push(Protocol::Tcp(socket.port())); + addr + } else if let Some(socket) = enr.tcp6_socket() { + let mut addr = Multiaddr::from(*socket.ip()); + addr.push(Protocol::Tcp(socket.port())); + addr + } else { + return None; + }; + + let CombinedPublicKey::Secp256k1(pub_key) = enr.public_key() else { + return None; + }; + + let pub_key = libp2p_identity::secp256k1::PublicKey::try_from_bytes(&pub_key.encode()).ok()?; + let pub_key = libp2p_identity::PublicKey::from(pub_key); + + addr.push(Protocol::P2p(libp2p::PeerId::from_public_key(&pub_key))); + + Some(addr) +} + +/// Converts an uncompressed [`PeerId`] to a [`secp256k1::PublicKey`] by prepending the [`PeerId`] +/// bytes with the `SECP256K1_TAG_PUBKEY_UNCOMPRESSED` tag. +pub fn peer_id_to_secp256k1_pubkey(id: PeerId) -> Result { + /// Tags the public key as uncompressed. + /// + /// See: + const SECP256K1_TAG_PUBKEY_UNCOMPRESSED: u8 = 4; + + let mut full_pubkey = [0u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE]; + full_pubkey[0] = SECP256K1_TAG_PUBKEY_UNCOMPRESSED; + full_pubkey[1..].copy_from_slice(id.as_slice()); + secp256k1::PublicKey::from_slice(&full_pubkey) +} + +/// An error that can occur when converting a [`PeerId`] to a [`libp2p::PeerId`]. +#[derive(Debug, thiserror::Error)] +pub enum PeerIdConversionError { + /// The peer id is not valid and cannot be converted to a secp256k1 public key. + #[error("Invalid peer id: {0}")] + InvalidPeerId(secp256k1::Error), + /// The secp256k1 public key cannot be converted to a libp2p peer id. This is a bug. + #[error("Invalid conversion from secp256k1 public key to libp2p peer id: {0}. This is a bug.")] + InvalidPublicKey(#[from] discv5::libp2p_identity::DecodingError), +} + +/// Converts an uncoded [`PeerId`] to a [`libp2p::PeerId`]. These two types represent the same +/// underlying concept (secp256k1 public key) but using different encodings (the local [`PeerId`] is +/// the uncompressed representation of the public key, while the "p2plib" [`libp2p::PeerId`] is a +/// more complex representation, involving protobuf encoding and bitcoin encoding, defined here: ). +pub fn local_id_to_p2p_id(peer_id: PeerId) -> Result { + // The libp2p library works with compressed public keys. + let encoded_pk_bytes = peer_id_to_secp256k1_pubkey(peer_id) + .map_err(PeerIdConversionError::InvalidPeerId)? + .serialize(); + let pk: discv5::libp2p_identity::PublicKey = + discv5::libp2p_identity::secp256k1::PublicKey::try_from_bytes(&encoded_pk_bytes)?.into(); + + Ok(pk.to_peer_id()) +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + + use super::*; + use crate::PeerId; + use alloy_primitives::hex::FromHex; + use discv5::enr::{CombinedKey, Enr, EnrKey}; + + #[test] + fn test_resolve_multiaddr() { + let ip = Ipv4Addr::new(132, 145, 16, 10); + let tcp_port = 9000; + let udp_port = 9001; + let private_key = CombinedKey::generate_secp256k1(); + + let public_key = private_key.public().encode(); + let public_key = + libp2p_identity::secp256k1::PublicKey::try_from_bytes(&public_key).unwrap(); + let peer_id = libp2p::PeerId::from_public_key(&public_key.into()); + + let enr = Enr::builder().ip4(ip).tcp4(tcp_port).udp4(udp_port).build(&private_key).unwrap(); + + let multiaddr = enr_to_multiaddr(&enr).unwrap(); + + let mut received_ip = None; + let mut received_tcp_port = None; + let mut received_p2p_id = None; + + for protocol in multiaddr.iter() { + match protocol { + Protocol::Ip4(ip) => { + received_ip = Some(ip); + } + Protocol::Tcp(port) => { + received_tcp_port = Some(port); + } + Protocol::P2p(id) => { + received_p2p_id = Some(id); + } + _ => { + panic!("Unexpected protocol: {protocol:?}"); + } + } + } + assert_eq!(received_ip, Some(ip)); + assert_eq!(received_tcp_port, Some(tcp_port)); + assert_eq!(received_p2p_id, Some(peer_id)); + } + + #[test] + fn test_resolve_multiaddr_ipv6() { + let ip = Ipv6Addr::new(0x2001, 0xdb8, 0x0a, 0x11, 0x1e, 0x8a, 0x2e, 0x3a); + let tcp_port = 9000; + let udp_port = 9001; + let private_key = CombinedKey::generate_secp256k1(); + + let public_key = private_key.public().encode(); + let public_key = + libp2p_identity::secp256k1::PublicKey::try_from_bytes(&public_key).unwrap(); + let peer_id = libp2p::PeerId::from_public_key(&public_key.into()); + + let enr = Enr::builder().ip6(ip).tcp6(tcp_port).udp6(udp_port).build(&private_key).unwrap(); + + let multiaddr = enr_to_multiaddr(&enr).unwrap(); + + let mut received_ip = None; + let mut received_tcp_port = None; + let mut received_p2p_id = None; + + for protocol in multiaddr.iter() { + match protocol { + Protocol::Ip6(ip) => { + received_ip = Some(ip); + } + Protocol::Tcp(port) => { + received_tcp_port = Some(port); + } + Protocol::P2p(id) => { + received_p2p_id = Some(id); + } + _ => { + panic!("Unexpected protocol: {protocol:?}"); + } + } + } + assert_eq!(received_ip, Some(ip)); + assert_eq!(received_tcp_port, Some(tcp_port)); + assert_eq!(received_p2p_id, Some(peer_id)); + } + + #[test] + fn test_convert_local_peer_id_to_multi_peer_id() { + let p2p_keypair = discv5::libp2p_identity::secp256k1::Keypair::generate(); + let uncompressed = p2p_keypair.public().to_bytes_uncompressed(); + let local_peer_id = PeerId::from_slice(&uncompressed[1..]); + + // We need to convert the local peer id (uncompressed secp256k1 public key) to a libp2p + // peer id (protocol buffer encoded public key). + let peer_id = local_id_to_p2p_id(local_peer_id).unwrap(); + + let p2p_public_key: discv5::libp2p_identity::PublicKey = + p2p_keypair.public().clone().into(); + + assert_eq!(peer_id, p2p_public_key.to_peer_id()); + } + + #[test] + fn test_hardcoded_peer_id() { + const PUB_KEY_STR: &str = "548f715f3fc388a7c917ba644a2f16270f1ede48a5d88a4d14ea287cc916068363f3092e39936f1a3e7885198bef0e5af951f1d7b1041ce8ba4010917777e71f"; + let pub_key = PeerId::from_hex(PUB_KEY_STR).unwrap(); + + // We need to convert the local peer id (uncompressed secp256k1 public key) to a libp2p + // peer id (protocol buffer encoded public key). + let peer_id = local_id_to_p2p_id(pub_key).unwrap(); + + let uncompressed_pub_key = peer_id_to_secp256k1_pubkey(pub_key).unwrap(); + + let p2p_public_key: discv5::libp2p_identity::PublicKey = + discv5::libp2p_identity::secp256k1::PublicKey::try_from_bytes( + &uncompressed_pub_key.serialize(), + ) + .unwrap() + .into(); + + assert_eq!(peer_id, p2p_public_key.to_peer_id()); + } +} diff --git a/kona/crates/node/rpc/Cargo.toml b/kona/crates/node/rpc/Cargo.toml new file mode 100644 index 0000000000000..dadfbeb43c247 --- /dev/null +++ b/kona/crates/node/rpc/Cargo.toml @@ -0,0 +1,84 @@ +[package] +name = "kona-rpc" +version = "0.3.2" +description = "Optimism RPC Types and API" + +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +authors.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-protocol = {workspace = true, features = ["serde", "std"]} +kona-gossip.workspace = true +kona-engine.workspace = true +kona-macros.workspace = true +kona-genesis = {workspace = true, features = ["serde", "std"]} + +# OP Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine = {workspace = true, features = ["serde", "std"]} +op-alloy-rpc-types.workspace = true + +# Alloy +alloy-eips = { workspace = true, features = ["serde", "std"] } +alloy-rpc-types-engine = { workspace = true, features = ["serde", "std"] } +alloy-primitives = { workspace = true, features = ["map", "rlp", "serde", "std"] } + +# Misc +libp2p.workspace = true +tracing.workspace = true +thiserror.workspace = true +derive_more = { workspace = true, default-features = false, features = [ + "display", + "from", + "constructor", + "std", +] } +async-trait.workspace = true +tokio = { workspace = true, features = ["time"] } +ipnet = { workspace = true } +backon = { workspace = true } + +# `serde` +serde = { workspace = true, features = ["std"] } + +# `jsonrpsee` +jsonrpsee = { workspace = true, features = ["macros", "server"] } +getrandom = { workspace = true, features = ["wasm_js"] } # req for wasm32-unknown-unknown +op-alloy-rpc-jsonrpsee.workspace = true + +# `reqwest` feature +alloy-rpc-client = { workspace = true, features = ["reqwest"], optional = true } + +# `metrics` feature +metrics = { workspace = true, optional = true } + +# `rollup-boost` feature +rollup-boost.workspace = true + +[dev-dependencies] +serde_json.workspace = true + +[features] +default = [] +reqwest = [ "client", "dep:alloy-rpc-client" ] +client = [ + "jsonrpsee/async-client", + "jsonrpsee/client", + "op-alloy-rpc-jsonrpsee/client", + "op-alloy-rpc-types/jsonrpsee", +] +metrics = [ + "dep:metrics", + "kona-engine/metrics", + "kona-gossip/metrics", + "libp2p/metrics", +] diff --git a/kona/crates/node/rpc/README.md b/kona/crates/node/rpc/README.md new file mode 100644 index 0000000000000..bb754a8304325 --- /dev/null +++ b/kona/crates/node/rpc/README.md @@ -0,0 +1,9 @@ +## `kona-rpc` + +CI +kona-rpc crate +MIT License +Docs + + +Low-level Optimism JSON-RPC server and client implementations. diff --git a/kona/crates/node/rpc/src/admin.rs b/kona/crates/node/rpc/src/admin.rs new file mode 100644 index 0000000000000..2bec87d3c9f0a --- /dev/null +++ b/kona/crates/node/rpc/src/admin.rs @@ -0,0 +1,280 @@ +//! Admin RPC Module + +use crate::AdminApiServer; +use alloy_primitives::B256; +use async_trait::async_trait; +use core::fmt::Debug; +use jsonrpsee::{ + core::RpcResult, + types::{ErrorCode, ErrorObject}, +}; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use rollup_boost::{ + ExecutionMode, GetExecutionModeResponse, SetExecutionModeRequest, SetExecutionModeResponse, +}; +use thiserror::Error; +use tokio::sync::oneshot; + +/// The query types to the network actor for the admin api. +#[derive(Debug)] +pub enum NetworkAdminQuery { + /// An admin rpc request to post an unsafe payload. + PostUnsafePayload { + /// The payload to post. + payload: OpExecutionPayloadEnvelope, + }, +} + +/// The query types to the rollup boost component of the engine actor. +/// Only set when rollup boost is enabled. +#[derive(Debug)] +pub enum RollupBoostAdminQuery { + /// An admin rpc request to set the execution mode. + SetExecutionMode { + /// The execution mode to set. + execution_mode: ExecutionMode, + }, + /// An admin rpc request to get the execution mode. + GetExecutionMode { + /// The sender to send the execution mode to. + sender: oneshot::Sender, + }, +} + +type NetworkAdminQuerySender = tokio::sync::mpsc::Sender; +type RollupBoostAdminQuerySender = tokio::sync::mpsc::Sender; + +/// The admin rpc server. +#[derive(Debug)] +pub struct AdminRpc { + /// The sequencer admin API client. + pub sequencer_admin_client: Option, + /// The sender to the network actor. + pub network_sender: NetworkAdminQuerySender, + /// The sender to the rollup boost component of the engine actor. + /// Only set when rollup boost is enabled. + pub rollup_boost_sender: Option, +} + +impl AdminRpc { + /// Constructs a new [`AdminRpc`] given the sequencer sender, network sender, and execution + /// mode. + /// + /// # Parameters + /// + /// - `sequencer_sender`: The sender to the sequencer actor. + /// - `network_sender`: The sender to the network actor. + /// - `rollup_boost_sender`: Sender of admin queries to the rollup boost component of the engine + /// actor. + /// + /// # Returns + /// + /// A new [`AdminRpc`] instance. + pub const fn new( + sequencer_admin_client: Option, + network_sender: NetworkAdminQuerySender, + rollup_boost_sender: Option, + ) -> Self { + Self { sequencer_admin_client, network_sender, rollup_boost_sender } + } +} + +#[async_trait] +impl AdminApiServer for AdminRpc { + async fn admin_post_unsafe_payload( + &self, + payload: OpExecutionPayloadEnvelope, + ) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "admin_postUnsafePayload"); + self.network_sender + .send(NetworkAdminQuery::PostUnsafePayload { payload }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_sequencer_active(&self) -> RpcResult { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .is_sequencer_active() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_start_sequencer(&self) -> RpcResult<()> { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .start_sequencer() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_stop_sequencer(&self) -> RpcResult { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .stop_sequencer() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_conductor_enabled(&self) -> RpcResult { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .is_conductor_enabled() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_recover_mode(&self) -> RpcResult { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .is_recovery_mode() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_set_recover_mode(&self, mode: bool) -> RpcResult<()> { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .set_recovery_mode(mode) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn admin_override_leader(&self) -> RpcResult<()> { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .override_leader() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn set_execution_mode( + &self, + request: SetExecutionModeRequest, + ) -> RpcResult { + let Some(ref rollup_boost_sender) = self.rollup_boost_sender else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + rollup_boost_sender + .send(RollupBoostAdminQuery::SetExecutionMode { + execution_mode: request.execution_mode, + }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + Ok(SetExecutionModeResponse { execution_mode: request.execution_mode }) + } + + async fn get_execution_mode(&self) -> RpcResult { + let Some(ref rollup_boost_sender) = self.rollup_boost_sender else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + let (tx, rx) = oneshot::channel(); + + rollup_boost_sender + .send(RollupBoostAdminQuery::GetExecutionMode { sender: tx }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + .map(|execution_mode| GetExecutionModeResponse { execution_mode }) + } + + async fn admin_reset_derivation_pipeline(&self) -> RpcResult<()> { + // If the sequencer is not enabled (mode runs in validator mode), return an error. + let Some(ref sequencer_client) = self.sequencer_admin_client else { + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + }; + + sequencer_client + .reset_derivation_pipeline() + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } +} + +/// The admin API client for the sequencer actor. +#[async_trait] +pub trait SequencerAdminAPIClient: Send + Sync + Debug { + /// Check if the sequencer is active. + async fn is_sequencer_active(&self) -> Result; + + /// Check if the conductor is enabled. + async fn is_conductor_enabled(&self) -> Result; + + /// Check if in recovery mode. + async fn is_recovery_mode(&self) -> Result; + + /// Start the sequencer. + async fn start_sequencer(&self) -> Result<(), SequencerAdminAPIError>; + + /// Stop the sequencer. + async fn stop_sequencer(&self) -> Result; + + /// Set recovery mode. + async fn set_recovery_mode(&self, mode: bool) -> Result<(), SequencerAdminAPIError>; + + /// Override the leader. + async fn override_leader(&self) -> Result<(), SequencerAdminAPIError>; + + /// Reset the derivation pipeline. + async fn reset_derivation_pipeline(&self) -> Result<(), SequencerAdminAPIError>; +} + +/// Errors that can occur when using the sequencer admin API. +#[derive(Debug, Error)] +pub enum SequencerAdminAPIError { + /// Error sending request. + #[error("Error sending request: {0}.")] + RequestError(String), + + /// Error receiving response. + #[error("Error receiving response: {0}.")] + ResponseError(String), + + /// Error stopping sequencer. + #[error("Error stopping sequencer: {0}.")] + StopError(#[from] StopSequencerError), + + /// Error overriding leader. + #[error("Error overriding leader: {0}.")] + LeaderOverrideError(String), +} + +/// Errors that can occur when using the sequencer admin API. +#[derive(Debug, Error)] +pub enum StopSequencerError { + /// Sequencer stopped successfully, followed by some error. + #[error("Sequencer stopped successfully, followed by error: {0}.")] + ErrorAfterSequencerWasStopped(String), +} diff --git a/kona/crates/node/rpc/src/config.rs b/kona/crates/node/rpc/src/config.rs new file mode 100644 index 0000000000000..056e393f54ef8 --- /dev/null +++ b/kona/crates/node/rpc/src/config.rs @@ -0,0 +1,48 @@ +//! Contains the RPC Configuration. + +use std::{net::SocketAddr, path::PathBuf}; + +/// The RPC configuration. +#[derive(Debug, Clone)] +pub struct RpcBuilder { + /// Prevent the rpc server from being restarted. + pub no_restart: bool, + /// The RPC socket address. + pub socket: SocketAddr, + /// Enable the admin API. + pub enable_admin: bool, + /// File path used to persist state changes made via the admin API so they persist across + /// restarts. + pub admin_persistence: Option, + /// Enable the websocket rpc server + pub ws_enabled: bool, + /// Enable development RPC endpoints + pub dev_enabled: bool, +} + +impl RpcBuilder { + /// Returns whether WebSocket RPC endpoint is enabled + pub const fn ws_enabled(&self) -> bool { + self.ws_enabled + } + + /// Returns whether development RPC endpoints are enabled + pub const fn dev_enabled(&self) -> bool { + self.dev_enabled + } + + /// Returns the socket address of the [`RpcBuilder`]. + pub const fn socket(&self) -> SocketAddr { + self.socket + } + + /// Returns the number of times the RPC server will attempt to restart if it stops. + pub const fn restart_count(&self) -> u32 { + if self.no_restart { 0 } else { 3 } + } + + /// Sets the given [`SocketAddr`] on the [`RpcBuilder`]. + pub fn set_addr(self, addr: SocketAddr) -> Self { + Self { socket: addr, ..self } + } +} diff --git a/kona/crates/node/rpc/src/dev.rs b/kona/crates/node/rpc/src/dev.rs new file mode 100644 index 0000000000000..3841e936a6a66 --- /dev/null +++ b/kona/crates/node/rpc/src/dev.rs @@ -0,0 +1,114 @@ +//! Development RPC API for exposing internal engine state and task queue information. +//! +//! This module provides development and debugging endpoints that allow introspection +//! of the engine's internal state, task queue, and operations. + +use async_trait::async_trait; +use jsonrpsee::{ + PendingSubscriptionSink, SubscriptionSink, + core::{RpcResult, SubscriptionResult}, + types::ErrorCode, +}; +use kona_engine::{EngineQueries, EngineQuerySender}; + +use crate::DevEngineApiServer; +use jsonrpsee::core::to_json_raw_value; + +/// Implementation of the development RPC API. +#[derive(Debug)] +pub struct DevEngineRpc { + /// The engine query sender. + engine_query_sender: EngineQuerySender, +} + +impl DevEngineRpc { + /// Creates a new [`DevEngineRpc`] instance. + pub const fn new(engine_query_sender: EngineQuerySender) -> Self { + Self { engine_query_sender } + } + + /// Gets an engine queue length watcher for subscriptions. + async fn engine_queue_length_watcher( + &self, + ) -> Result, jsonrpsee::core::SubscriptionError> { + let (query_tx, query_rx) = tokio::sync::oneshot::channel(); + + if let Err(e) = + self.engine_query_sender.send(EngineQueries::QueueLengthReceiver(query_tx)).await + { + tracing::warn!(target: "rpc::dev", ?e, "Failed to send engine state receiver query. The engine query handler is likely closed."); + return Err(jsonrpsee::core::SubscriptionError::from( + "Internal error. Failed to send engine state receiver query. The engine query handler is likely closed.", + )); + } + + query_rx.await.map_err(|_| jsonrpsee::core::SubscriptionError::from("Internal error. Failed to receive engine task receiver query. The engine query handler is likely closed.")) + } + + async fn send_queue_length_update( + sink: &SubscriptionSink, + queue_length: &usize, + ) -> Result<(), jsonrpsee::core::SubscriptionError> { + sink.send(to_json_raw_value(queue_length).map_err(|_| { + jsonrpsee::core::SubscriptionError::from( + "Internal error. Impossible to convert engine queue length to json", + ) + })?) + .await + .map_err(|_| { + jsonrpsee::core::SubscriptionError::from( + "Failed to send engine queue length update. Subscription likely dropped.", + ) + }) + } +} + +#[async_trait] +impl DevEngineApiServer for DevEngineRpc { + async fn dev_subscribe_engine_queue_length( + &self, + sink: PendingSubscriptionSink, + ) -> SubscriptionResult { + let sink = sink.accept().await?; + + let mut subscription = self.engine_queue_length_watcher().await?; + + let mut current_queue_length = *subscription.borrow(); + + Self::send_queue_length_update(&sink, ¤t_queue_length).await?; + + while let Ok(new_queue_length) = subscription + .wait_for(|queue_length| queue_length != ¤t_queue_length) + .await + .map(|state| *state) + { + Self::send_queue_length_update(&sink, &new_queue_length).await?; + current_queue_length = new_queue_length; + } + + tracing::warn!(target: "rpc::dev::engine_queue_size", "Subscription to engine queue size has been closed."); + Ok(()) + } + + async fn dev_task_queue_length(&self) -> RpcResult { + let (query_tx, query_rx) = tokio::sync::oneshot::channel(); + + self.engine_query_sender.send(EngineQueries::TaskQueueLength(query_tx)).await.map_err( + |_| { + jsonrpsee::types::ErrorObjectOwned::owned( + ErrorCode::InternalError.code(), + "Engine query channel closed", + None::<()>, + ) + }, + )?; + + query_rx.await.map_err(|_| { + jsonrpsee::types::ErrorObjectOwned::owned( + ErrorCode::InternalError.code(), + "Failed to receive task queue length", + None::<()>, + ) + }) + } +} diff --git a/kona/crates/node/rpc/src/health.rs b/kona/crates/node/rpc/src/health.rs new file mode 100644 index 0000000000000..98ba9d105c0b5 --- /dev/null +++ b/kona/crates/node/rpc/src/health.rs @@ -0,0 +1,110 @@ +use async_trait::async_trait; +use jsonrpsee::{ + core::RpcResult, + types::{ErrorCode, ErrorObject}, +}; +use rollup_boost::Health; +use tokio::sync::{mpsc, oneshot}; + +use crate::jsonrpsee::{HealthzApiServer, RollupBoostHealthzApiServer}; + +/// Key for the rollup boost health status. +/// +----------------+-------------------------------+--------------------------------------+-------------------------------+ +/// | Execution Mode | Healthy | PartialContent | Service Unavailable | +/// +----------------+-------------------------------+--------------------------------------+-------------------------------+ +/// | Enabled | - Request-path: L2 succeeds | - Request-path: builder fails/stale | - Request-path: L2 fails | +/// | | (get/new payload) → 200 | while L2 succeeds → 206 | (error from L2) → 503 | +/// | | - Background: builder | - Background: builder fetch fails or | - Background: never sets 503 | +/// | | latest-unsafe is fresh → | latest-unsafe is stale → 206 | | +/// | | 200 | | | +/// +----------------+-------------------------------+--------------------------------------+-------------------------------+ +/// | DryRun | - Request-path: L2 succeeds | - Never set in DryRun | - Request-path: L2 fails | +/// | | (always returns L2) → 200 | (degrade only in Enabled) | (error from L2) → 503 | +/// | | - Background: builder stale | | - Background: never sets 503 | +/// | | ignored (remains 200) | | | +/// +----------------+-------------------------------+--------------------------------------+-------------------------------+ +/// | Disabled | - Request-path: L2 succeeds | - Never set in Disabled | - Request-path: L2 fails | +/// | | (builder skipped) → 200 | (degrade only in Enabled) | (error from L2) → 503 | +/// | | - Background: N/A | | - Background: never sets 503 | +/// +----------------+-------------------------------+--------------------------------------+-------------------------------+ +/// +/// This type is the same as [`Health`], but it implements `serde::Serialize` +/// and `serde::Deserialize`. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum RollupBoostHealth { + /// Rollup boost is healthy. + Healthy, + /// Rollup boost is partially healthy. + PartialContent, + /// Rollup boost service is unavailable. + ServiceUnavailable, +} + +impl From for RollupBoostHealth { + fn from(health: Health) -> Self { + match health { + Health::Healthy => Self::Healthy, + Health::PartialContent => Self::PartialContent, + Health::ServiceUnavailable => Self::ServiceUnavailable, + } + } +} + +/// A healthcheck response for the RPC server. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct HealthzResponse { + /// The application version. + pub version: String, +} + +/// A healthcheck response for the rollup boost health. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct RollupBoostHealthzResponse { + /// The rollup boost health. + pub rollup_boost_health: RollupBoostHealth, +} + +/// A query to get the health of the rollup boost server. +#[derive(Debug)] +pub struct RollupBoostHealthQuery { + /// The sender to send the rollup boost health to. + pub sender: oneshot::Sender, +} + +/// The healthz rpc server. +#[derive(Debug, Clone)] +pub struct HealthzRpc { + /// The rollup boost health. + pub rollup_boost_health: mpsc::Sender, +} + +impl HealthzRpc { + /// Constructs a new [`HealthzRpc`] given the rollup boost health sender. + pub const fn new(rollup_boost_health: mpsc::Sender) -> Self { + Self { rollup_boost_health } + } +} + +#[async_trait] +impl HealthzApiServer for HealthzRpc { + async fn healthz(&self) -> RpcResult { + Ok(HealthzResponse { version: env!("CARGO_PKG_VERSION").to_string() }) + } +} + +#[async_trait] +impl RollupBoostHealthzApiServer for HealthzRpc { + async fn rollup_boost_healthz(&self) -> RpcResult { + let (tx, rx) = oneshot::channel(); + + self.rollup_boost_health + .send(RollupBoostHealthQuery { sender: tx }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let rollup_boost_health = + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + Ok(RollupBoostHealthzResponse { rollup_boost_health }) + } +} diff --git a/kona/crates/node/rpc/src/jsonrpsee.rs b/kona/crates/node/rpc/src/jsonrpsee.rs new file mode 100644 index 0000000000000..c09ca3e4beab6 --- /dev/null +++ b/kona/crates/node/rpc/src/jsonrpsee.rs @@ -0,0 +1,238 @@ +//! The Optimism RPC API using `jsonrpsee` + +use crate::{ + OutputResponse, SafeHeadResponse, + health::{HealthzResponse, RollupBoostHealthzResponse}, +}; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::B256; +use core::net::IpAddr; +use ipnet::IpNet; +use jsonrpsee::{ + core::{RpcResult, SubscriptionResult}, + proc_macros::rpc, +}; +use kona_genesis::RollupConfig; +use kona_gossip::{PeerCount, PeerDump, PeerInfo, PeerStats}; +use kona_protocol::SyncStatus; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use rollup_boost::{GetExecutionModeResponse, SetExecutionModeRequest, SetExecutionModeResponse}; + +#[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), allow(unused_imports))] +use getrandom as _; // required for compiling wasm32-unknown-unknown + +// Re-export apis defined in upstream `op-alloy-rpc-jsonrpsee` +pub use op_alloy_rpc_jsonrpsee::traits::{MinerApiExtServer, OpAdminApiServer}; + +/// Optimism specified rpc interface. +/// +/// https://docs.optimism.io/builders/node-operators/json-rpc +/// https://github.com/ethereum-optimism/optimism/blob/8dd17a7b114a7c25505cd2e15ce4e3d0f7e3f7c1/op-node/node/api.go#L114 +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "optimism"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "optimism"))] +pub trait RollupNodeApi { + /// Get the output root at a specific block. + #[method(name = "outputAtBlock")] + async fn op_output_at_block(&self, block_number: BlockNumberOrTag) + -> RpcResult; + + /// Gets the safe head at an L1 block height. + #[method(name = "safeHeadAtL1Block")] + async fn op_safe_head_at_l1_block( + &self, + block_number: BlockNumberOrTag, + ) -> RpcResult; + + /// Get the synchronization status. + #[method(name = "syncStatus")] + async fn op_sync_status(&self) -> RpcResult; + + /// Get the rollup configuration parameters. + #[method(name = "rollupConfig")] + async fn op_rollup_config(&self) -> RpcResult; + + /// Get the software version. + #[method(name = "version")] + async fn op_version(&self) -> RpcResult; +} + +/// The opp2p namespace handles peer interactions. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "opp2p"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "opp2p"))] +pub trait OpP2PApi { + /// Returns information of node + #[method(name = "self")] + async fn opp2p_self(&self) -> RpcResult; + + /// Returns information of peers + #[method(name = "peerCount")] + async fn opp2p_peer_count(&self) -> RpcResult; + + /// Returns information of peers. If `connected` is true, only returns connected peers. + #[method(name = "peers")] + async fn opp2p_peers(&self, connected: bool) -> RpcResult; + + /// Returns statistics of peers + #[method(name = "peerStats")] + async fn opp2p_peer_stats(&self) -> RpcResult; + + /// Returns the discovery table + #[method(name = "discoveryTable")] + async fn opp2p_discovery_table(&self) -> RpcResult>; + + /// Blocks the given peer + #[method(name = "blockPeer")] + async fn opp2p_block_peer(&self, peer: String) -> RpcResult<()>; + + /// Unblocks the given peer + #[method(name = "unblockPeer")] + async fn opp2p_unblock_peer(&self, peer: String) -> RpcResult<()>; + + /// Lists blocked peers + #[method(name = "listBlockedPeers")] + async fn opp2p_list_blocked_peers(&self) -> RpcResult>; + + /// Blocks the given address + #[method(name = "blocAddr")] + async fn opp2p_block_addr(&self, ip: IpAddr) -> RpcResult<()>; + + /// Unblocks the given address + #[method(name = "unblockAddr")] + async fn opp2p_unblock_addr(&self, ip: IpAddr) -> RpcResult<()>; + + /// Lists blocked addresses + #[method(name = "listBlockedAddrs")] + async fn opp2p_list_blocked_addrs(&self) -> RpcResult>; + + /// Blocks the given subnet + #[method(name = "blockSubnet")] + async fn opp2p_block_subnet(&self, subnet: IpNet) -> RpcResult<()>; + + /// Unblocks the given subnet + #[method(name = "unblockSubnet")] + async fn opp2p_unblock_subnet(&self, subnet: IpNet) -> RpcResult<()>; + + /// Lists blocked subnets + #[method(name = "listBlockedSubnets")] + async fn opp2p_list_blocked_subnets(&self) -> RpcResult>; + + /// Protects the given peer + #[method(name = "protectPeer")] + async fn opp2p_protect_peer(&self, peer: String) -> RpcResult<()>; + + /// Unprotects the given peer + #[method(name = "unprotectPeer")] + async fn opp2p_unprotect_peer(&self, peer: String) -> RpcResult<()>; + + /// Connects to the given peer + #[method(name = "connectPeer")] + async fn opp2p_connect_peer(&self, peer: String) -> RpcResult<()>; + + /// Disconnects from the given peer + #[method(name = "disconnectPeer")] + async fn opp2p_disconnect_peer(&self, peer: String) -> RpcResult<()>; +} + +/// Websockets API for the node. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "ws"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "ws"))] +#[async_trait] +pub trait Ws { + /// Subscribes to the stream of finalized head updates. + #[subscription(name = "subscribe_finalized_head", item = kona_protocol::L2BlockInfo)] + async fn ws_finalized_head_updates(&self) -> SubscriptionResult; + + /// Subscribes to the stream of safe head updates. + #[subscription(name = "subscribe_safe_head", item = kona_protocol::L2BlockInfo)] + async fn ws_safe_head_updates(&self) -> SubscriptionResult; + + /// Subscribes to the stream of unsafe head updates. + #[subscription(name = "subscribe_unsafe_head", item = kona_protocol::L2BlockInfo)] + async fn ws_unsafe_head_updates(&self) -> SubscriptionResult; +} + +/// Development RPC API for engine state introspection. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "dev"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "dev"))] +#[async_trait] +pub trait DevEngineApi { + /// Subscribe to engine queue length updates. + #[subscription(name = "subscribe_engine_queue_size", item = usize)] + async fn dev_subscribe_engine_queue_length(&self) -> SubscriptionResult; + + /// Get the current number of tasks in the engine queue. + #[method(name = "taskQueueLength")] + async fn dev_task_queue_length(&self) -> RpcResult; +} + +/// The admin namespace for the consensus node. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "admin"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "admin"))] +#[async_trait] +pub trait AdminApi { + /// Posts the unsafe payload. + #[method(name = "postUnsafePayload")] + async fn admin_post_unsafe_payload(&self, payload: OpExecutionPayloadEnvelope) + -> RpcResult<()>; + + /// Checks if the sequencer is active. + #[method(name = "sequencerActive")] + async fn admin_sequencer_active(&self) -> RpcResult; + + /// Starts the sequencer. + #[method(name = "startSequencer")] + async fn admin_start_sequencer(&self) -> RpcResult<()>; + + /// Stops the sequencer. + #[method(name = "stopSequencer")] + async fn admin_stop_sequencer(&self) -> RpcResult; + + /// Checks if the conductor is enabled. + #[method(name = "conductorEnabled")] + async fn admin_conductor_enabled(&self) -> RpcResult; + + /// Gets the recover mode. + #[method(name = "adminRecoverMode")] + async fn admin_recover_mode(&self) -> RpcResult; + + /// Sets the recover mode. + #[method(name = "setRecoverMode")] + async fn admin_set_recover_mode(&self, mode: bool) -> RpcResult<()>; + + /// Overrides the leader in the conductor. + #[method(name = "overrideLeader")] + async fn admin_override_leader(&self) -> RpcResult<()>; + + /// Resets the derivation pipeline. + #[method(name = "resetDerivationPipeline")] + async fn admin_reset_derivation_pipeline(&self) -> RpcResult<()>; + + /// Sets the rollup boost execution mode. + #[method(name = "setExecutionMode")] + async fn set_execution_mode( + &self, + request: SetExecutionModeRequest, + ) -> RpcResult; + + /// Gets the rollup boost execution mode. + #[method(name = "getExecutionMode")] + async fn get_execution_mode(&self) -> RpcResult; +} + +/// The admin namespace for the consensus node. +#[cfg_attr(not(feature = "client"), rpc(server))] +#[cfg_attr(feature = "client", rpc(server, client))] +pub trait HealthzApi { + /// Gets the health of the kona-node. + #[method(name = "healthz")] + async fn healthz(&self) -> RpcResult; +} + +/// The rollup boost health namespace. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "kona-rollup-boost"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "kona-rollup-boost"))] +pub trait RollupBoostHealthzApi { + /// Gets the rollup boost health. + #[method(name = "healthz")] + async fn rollup_boost_healthz(&self) -> RpcResult; +} diff --git a/kona/crates/node/rpc/src/l1_watcher.rs b/kona/crates/node/rpc/src/l1_watcher.rs new file mode 100644 index 0000000000000..ca7229c69b616 --- /dev/null +++ b/kona/crates/node/rpc/src/l1_watcher.rs @@ -0,0 +1,43 @@ +use kona_genesis::RollupConfig; +use kona_protocol::BlockInfo; +use tokio::sync::oneshot::Sender; + +/// The L1 watcher state accessible from RPC queries. +#[derive(Debug, Clone)] +pub struct L1State { + /// The current L1 block. + /// + /// This is the L1 block that the derivation process is last idled at. + /// This may not be fully derived into L2 data yet. + /// The safe L2 blocks were produced/included fully from the L1 chain up to _but excluding_ + /// this L1 block. If the node is synced, this matches the `head_l1`, minus the verifier + /// confirmation distance. + pub current_l1: Option, + /// The current L1 finalized block. + /// + /// This is a legacy sync-status attribute. This is deprecated. + /// A previous version of the L1 finalization-signal was updated only after the block was + /// retrieved by number. This attribute just matches `finalized_l1` now. + pub current_l1_finalized: Option, + /// The L1 head block ref. + /// + /// The head is not guaranteed to build on the other L1 sync status fields, + /// as the node may be in progress of resetting to adapt to a L1 reorg. + pub head_l1: Option, + /// The L1 safe head block ref. + pub safe_l1: Option, + /// The finalized L1 block ref. + pub finalized_l1: Option, +} + +/// A sender for L1 watcher queries. +pub type L1WatcherQuerySender = tokio::sync::mpsc::Sender; + +/// The inbound queries to the L1 watcher. +#[derive(Debug)] +pub enum L1WatcherQueries { + /// Get the rollup config from the L1 watcher. + Config(Sender), + /// Get a complete view of the L1 state. + L1State(Sender), +} diff --git a/kona/crates/node/rpc/src/lib.rs b/kona/crates/node/rpc/src/lib.rs new file mode 100644 index 0000000000000..108c5e56eafdc --- /dev/null +++ b/kona/crates/node/rpc/src/lib.rs @@ -0,0 +1,54 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[macro_use] +extern crate tracing; + +mod admin; +pub use admin::{ + AdminRpc, NetworkAdminQuery, RollupBoostAdminQuery, SequencerAdminAPIClient, + SequencerAdminAPIError, StopSequencerError, +}; + +mod config; +pub use config::RpcBuilder; + +mod net; +pub use net::P2pRpc; + +mod p2p; + +mod response; +pub use response::SafeHeadResponse; + +mod output; +pub use output::OutputResponse; + +mod dev; +pub use dev::DevEngineRpc; + +mod jsonrpsee; +pub use jsonrpsee::{ + AdminApiServer, DevEngineApiServer, HealthzApiServer, MinerApiExtServer, OpAdminApiServer, + OpP2PApiServer, RollupBoostHealthzApiServer, RollupNodeApiServer, WsServer, +}; + +mod rollup; +pub use rollup::RollupRpc; + +mod l1_watcher; +pub use l1_watcher::{L1State, L1WatcherQueries, L1WatcherQuerySender}; + +mod ws; +pub use ws::WsRPC; + +mod health; +pub use health::{ + HealthzResponse, HealthzRpc, RollupBoostHealth, RollupBoostHealthQuery, + RollupBoostHealthzResponse, +}; diff --git a/kona/crates/node/rpc/src/net.rs b/kona/crates/node/rpc/src/net.rs new file mode 100644 index 0000000000000..7732b80820b8e --- /dev/null +++ b/kona/crates/node/rpc/src/net.rs @@ -0,0 +1,22 @@ +//! Network types + +use kona_gossip::P2pRpcRequest; + +/// A type alias for the sender of a [`P2pRpcRequest`]. +type P2pReqSender = tokio::sync::mpsc::Sender; + +/// P2pRpc +/// +/// This is a server implementation of [`crate::OpP2PApiServer`]. +#[derive(Debug)] +pub struct P2pRpc { + /// The channel to send [`P2pRpcRequest`]s. + pub sender: P2pReqSender, +} + +impl P2pRpc { + /// Constructs a new [`P2pRpc`] given a sender channel. + pub const fn new(sender: P2pReqSender) -> Self { + Self { sender } + } +} diff --git a/kona/crates/node/rpc/src/output.rs b/kona/crates/node/rpc/src/output.rs new file mode 100644 index 0000000000000..e2dd277ce18fa --- /dev/null +++ b/kona/crates/node/rpc/src/output.rs @@ -0,0 +1,38 @@ +//! Output Types + +use alloy_primitives::B256; +use kona_protocol::{L2BlockInfo, OutputRoot, SyncStatus}; + +/// An [output response][or] for Optimism Rollup. +/// +/// [or]: https://github.com/ethereum-optimism/optimism/blob/f20b92d3eb379355c876502c4f28e72a91ab902f/op-service/eth/output.go#L10-L17 +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutputResponse { + /// The output version. + pub version: B256, + /// The output root hash. + pub output_root: B256, + /// A reference to the L2 block. + pub block_ref: L2BlockInfo, + /// The withdrawal storage root. + pub withdrawal_storage_root: B256, + /// The state root. + pub state_root: B256, + /// The status of the node sync. + pub sync_status: SyncStatus, +} + +impl OutputResponse { + /// Builds an [`OutputResponse`] from its parts. + pub fn from_v0(v0: OutputRoot, sync_status: SyncStatus, block_ref: L2BlockInfo) -> Self { + Self { + version: v0.version(), + output_root: v0.hash(), + block_ref, + withdrawal_storage_root: v0.bridge_storage_root, + state_root: v0.state_root, + sync_status, + } + } +} diff --git a/kona/crates/node/rpc/src/p2p.rs b/kona/crates/node/rpc/src/p2p.rs new file mode 100644 index 0000000000000..0c2bb9ac88a76 --- /dev/null +++ b/kona/crates/node/rpc/src/p2p.rs @@ -0,0 +1,317 @@ +//! RPC Module to serve the P2P API. +//! +//! Kona's P2P RPC API is a JSON-RPC API compatible with the [op-node] API. +//! +//! +//! [op-node]: https://github.com/ethereum-optimism/optimism/blob/7a6788836984996747193b91901a824c39032bd8/op-node/p2p/rpc_api.go#L45 + +use async_trait::async_trait; +use backon::{ExponentialBuilder, Retryable}; +use ipnet::IpNet; +use jsonrpsee::{ + core::RpcResult, + types::{ErrorCode, ErrorObject}, +}; +use kona_gossip::{P2pRpcRequest, PeerCount, PeerDump, PeerInfo, PeerStats}; +use std::{net::IpAddr, str::FromStr, time::Duration}; + +use crate::{OpP2PApiServer, net::P2pRpc}; + +#[async_trait] +impl OpP2PApiServer for P2pRpc { + async fn opp2p_self(&self) -> RpcResult { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_self"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::PeerInfo(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_peer_count(&self) -> RpcResult { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_peerCount"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::PeerCount(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let (connected_discovery, connected_gossip) = + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + Ok(PeerCount { connected_discovery, connected_gossip }) + } + + async fn opp2p_peers(&self, connected: bool) -> RpcResult { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_peers"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::Peers { out: tx, connected }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let dump = rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + Ok(dump) + } + + async fn opp2p_peer_stats(&self) -> RpcResult { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::PeerStats(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let stats = rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + Ok(stats) + } + + async fn opp2p_discovery_table(&self) -> RpcResult> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_discoveryTable"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::DiscoveryTable(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_block_peer(&self, peer_id: String) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_blockPeer"); + let id = libp2p::PeerId::from_str(&peer_id) + .map_err(|_| ErrorObject::from(ErrorCode::InvalidParams))?; + self.sender + .send(P2pRpcRequest::BlockPeer { id }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_unblock_peer(&self, peer_id: String) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_unblockPeer"); + let id = libp2p::PeerId::from_str(&peer_id) + .map_err(|_| ErrorObject::from(ErrorCode::InvalidParams))?; + self.sender + .send(P2pRpcRequest::UnblockPeer { id }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_list_blocked_peers(&self) -> RpcResult> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_listBlockedPeers"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::ListBlockedPeers(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await + .map(|peers| peers.iter().map(|p| p.to_string()).collect()) + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_block_addr(&self, address: IpAddr) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_blockAddr"); + self.sender + .send(P2pRpcRequest::BlockAddr { address }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_unblock_addr(&self, address: IpAddr) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_unblockAddr"); + self.sender + .send(P2pRpcRequest::UnblockAddr { address }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_list_blocked_addrs(&self) -> RpcResult> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_listBlockedAddrs"); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::ListBlockedAddrs(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_block_subnet(&self, subnet: IpNet) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_blockSubnet"); + self.sender + .send(P2pRpcRequest::BlockSubnet { address: subnet }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_unblock_subnet(&self, subnet: IpNet) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_unblockSubnet"); + + self.sender + .send(P2pRpcRequest::UnblockSubnet { address: subnet }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_list_blocked_subnets(&self) -> RpcResult> { + kona_macros::inc!( + gauge, + kona_gossip::Metrics::RPC_CALLS, + "method" => "opp2p_listBlockedSubnets" + ); + let (tx, rx) = tokio::sync::oneshot::channel(); + self.sender + .send(P2pRpcRequest::ListBlockedSubnets(tx)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + rx.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_protect_peer(&self, id: String) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_protectPeer"); + let peer_id = libp2p::PeerId::from_str(&id) + .map_err(|_| ErrorObject::from(ErrorCode::InvalidParams))?; + self.sender + .send(P2pRpcRequest::ProtectPeer { peer_id }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_unprotect_peer(&self, id: String) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_unprotectPeer"); + let peer_id = libp2p::PeerId::from_str(&id) + .map_err(|_| ErrorObject::from(ErrorCode::InvalidParams))?; + self.sender + .send(P2pRpcRequest::UnprotectPeer { peer_id }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + + async fn opp2p_connect_peer(&self, _peer: String) -> RpcResult<()> { + use std::str::FromStr; + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_connectPeer"); + let ma = libp2p::Multiaddr::from_str(&_peer).map_err(|_| { + ErrorObject::borrowed(ErrorCode::InvalidParams.code(), "Invalid multiaddr", None) + })?; + + let peer_id = ma + .iter() + .find_map(|component| match component { + libp2p::multiaddr::Protocol::P2p(peer_id) => Some(peer_id), + _ => None, + }) + .ok_or_else(|| { + ErrorObject::borrowed( + ErrorCode::InvalidParams.code(), + "Impossible to extract peer ID from multiaddr", + None, + ) + })?; + + self.sender.send(P2pRpcRequest::ConnectPeer { address: ma }).await.map_err(|_| { + ErrorObject::borrowed( + ErrorCode::InternalError.code(), + "Failed to send connect peer request", + None, + ) + })?; + + // We need to wait until both peers are connected to each other to return from this method. + // We try with an exponential backoff and return an error if we fail to connect to the peer. + let is_connected = async || { + let (tx, rx) = tokio::sync::oneshot::channel(); + + self.sender + .send(P2pRpcRequest::Peers { out: tx, connected: true }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let peers = rx.await.map_err(|_| { + ErrorObject::borrowed(ErrorCode::InternalError.code(), "Failed to get peers", None) + })?; + + Ok::>(peers.peers.contains_key(&peer_id.to_string())) + }; + + if !is_connected + .retry(ExponentialBuilder::default().with_total_delay(Some(Duration::from_secs(10)))) + .await? + { + return Err(ErrorObject::borrowed( + ErrorCode::InvalidParams.code(), + "Peer not connected", + None, + )); + } + + Ok(()) + } + + async fn opp2p_disconnect_peer(&self, peer_id: String) -> RpcResult<()> { + kona_macros::inc!(gauge, kona_gossip::Metrics::RPC_CALLS, "method" => "opp2p_disconnectPeer"); + let peer_id = match peer_id.parse() { + Ok(id) => id, + Err(err) => { + warn!(target: "rpc", ?err, ?peer_id, "Failed to parse peer ID"); + return Err(ErrorObject::from(ErrorCode::InvalidParams)); + } + }; + + self.sender + .send(P2pRpcRequest::DisconnectPeer { peer_id }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + // We need to wait until both peers are fully disconnected to each other to return from this + // method. We try with an exponential backoff and return an error if we fail to + // disconnect from the peer. + let is_not_connected = async || { + let (tx, rx) = tokio::sync::oneshot::channel(); + + self.sender + .send(P2pRpcRequest::Peers { out: tx, connected: true }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + let peers = rx.await.map_err(|_| { + ErrorObject::borrowed(ErrorCode::InternalError.code(), "Failed to get peers", None) + })?; + + Ok::>(!peers.peers.contains_key(&peer_id.to_string())) + }; + + if !is_not_connected + .retry(ExponentialBuilder::default().with_total_delay(Some(Duration::from_secs(10)))) + .await? + { + return Err(ErrorObject::borrowed( + ErrorCode::InvalidParams.code(), + "Peers are still connected", + None, + )); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_parse_multiaddr_string() { + use std::str::FromStr; + let ma = "/ip4/127.0.0.1/udt"; + let multiaddr = libp2p::Multiaddr::from_str(ma).unwrap(); + let components = multiaddr.iter().collect::>(); + assert_eq!( + components[0], + libp2p::multiaddr::Protocol::Ip4(std::net::Ipv4Addr::new(127, 0, 0, 1)) + ); + assert_eq!(components[1], libp2p::multiaddr::Protocol::Udt); + } +} diff --git a/kona/crates/node/rpc/src/response.rs b/kona/crates/node/rpc/src/response.rs new file mode 100644 index 0000000000000..4455848b6a2a1 --- /dev/null +++ b/kona/crates/node/rpc/src/response.rs @@ -0,0 +1,28 @@ +//! Response to safe head request + +use alloy_eips::BlockNumHash; + +/// The safe head response. +/// +/// +/// Note: the optimism "eth.BlockID" type is number,hash +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SafeHeadResponse { + /// The L1 block. + pub l1_block: BlockNumHash, + /// The safe head. + pub safe_head: BlockNumHash, +} + +#[cfg(test)] +mod tests { + use super::*; + + // + #[test] + fn test_safe_head_response() { + let s = r#"{"l1Block":{"hash":"0x7de331305c2bb3e5642a2adcb9c003cc67cefc7b05a3da5a6a4b12cf3af15407","number":6834391},"safeHead":{"hash":"0xa5e5ec1ade7d6fef209f73861bf0080950cde74c4b0c07823983eb5225e282a8","number":18266679}}"#; + let _response: SafeHeadResponse = serde_json::from_str(s).unwrap(); + } +} diff --git a/kona/crates/node/rpc/src/rollup.rs b/kona/crates/node/rpc/src/rollup.rs new file mode 100644 index 0000000000000..bdc1941c762d4 --- /dev/null +++ b/kona/crates/node/rpc/src/rollup.rs @@ -0,0 +1,152 @@ +//! Implements the rollup client rpc endpoints. These endpoints serve data about the rollup state. +//! +//! Implemented in the op-node in + +use alloy_eips::BlockNumberOrTag; +use async_trait::async_trait; +use jsonrpsee::{ + core::RpcResult, + types::{ErrorCode, ErrorObject}, +}; +use kona_engine::{EngineQueries, EngineQuerySender, EngineState}; +use kona_genesis::RollupConfig; +use kona_protocol::SyncStatus; + +use crate::{ + L1State, L1WatcherQueries, OutputResponse, RollupNodeApiServer, SafeHeadResponse, + l1_watcher::L1WatcherQuerySender, +}; + +/// RollupRpc +/// +/// This is a server implementation of [`crate::RollupNodeApiServer`]. +#[derive(Debug)] +pub struct RollupRpc { + /// The channel to send [`kona_engine::EngineQueries`]s. + pub engine_sender: EngineQuerySender, + /// The channel to send [`crate::L1WatcherQueries`]s. + pub l1_watcher_sender: L1WatcherQuerySender, +} + +impl RollupRpc { + /// The identifier for the Metric that tracks rollup RPC calls. + pub const RPC_IDENT: &'static str = "rollup_rpc"; + + /// Constructs a new [`RollupRpc`] given a sender channel. + pub const fn new( + engine_sender: EngineQuerySender, + l1_watcher_sender: L1WatcherQuerySender, + ) -> Self { + Self { engine_sender, l1_watcher_sender } + } + + // Important note: we zero-out the fields that can't be derived yet to follow op-node's + // behaviour. + fn sync_status_from_actor_queries( + l1_sync_status: L1State, + l2_sync_status: EngineState, + ) -> SyncStatus { + SyncStatus { + current_l1: l1_sync_status.current_l1.unwrap_or_default(), + current_l1_finalized: l1_sync_status.current_l1_finalized.unwrap_or_default(), + head_l1: l1_sync_status.head_l1.unwrap_or_default(), + safe_l1: l1_sync_status.safe_l1.unwrap_or_default(), + finalized_l1: l1_sync_status.finalized_l1.unwrap_or_default(), + unsafe_l2: l2_sync_status.sync_state.unsafe_head(), + cross_unsafe_l2: l2_sync_status.sync_state.cross_unsafe_head(), + local_safe_l2: l2_sync_status.sync_state.local_safe_head(), + safe_l2: l2_sync_status.sync_state.safe_head(), + finalized_l2: l2_sync_status.sync_state.finalized_head(), + } + } +} + +#[async_trait] +impl RollupNodeApiServer for RollupRpc { + async fn op_output_at_block(&self, block_num: BlockNumberOrTag) -> RpcResult { + kona_macros::inc!(gauge, Self::RPC_IDENT, "method" => "op_outputAtBlock"); + + let (output_send, output_recv) = tokio::sync::oneshot::channel(); + let (l1_sync_status_send, l1_sync_status_recv) = tokio::sync::oneshot::channel(); + + let ((l2_block_info, output_root, l2_sync_status), l1_sync_status) = tokio::try_join!( + async { + self.engine_sender + .send(EngineQueries::OutputAtBlock { block: block_num, sender: output_send }) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + output_recv.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + }, + async { + self.l1_watcher_sender + .send(L1WatcherQueries::L1State(l1_sync_status_send)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + l1_sync_status_recv.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + )?; + + let sync_status = Self::sync_status_from_actor_queries(l1_sync_status, l2_sync_status); + + Ok(OutputResponse::from_v0(output_root, sync_status, l2_block_info)) + } + + /// This RPC endpoint is not supported. It is not necessary to track the safe head for every L1 + /// block post-interop anymore so we can remove this method from the rpc interface. + async fn op_safe_head_at_l1_block( + &self, + _block_num: BlockNumberOrTag, + ) -> RpcResult { + kona_macros::inc!(gauge, Self::RPC_IDENT, "method" => "op_safeHeadAtL1Block"); + return Err(ErrorObject::from(ErrorCode::MethodNotFound)); + } + + async fn op_sync_status(&self) -> RpcResult { + kona_macros::inc!(gauge, Self::RPC_IDENT, "method" => "op_syncStatus"); + + let (l1_sync_status_send, l1_sync_status_recv) = tokio::sync::oneshot::channel(); + let (l2_sync_status_send, l2_sync_status_recv) = tokio::sync::oneshot::channel(); + + let (l1_sync_status, l2_sync_status) = tokio::try_join!( + async { + self.l1_watcher_sender + .send(L1WatcherQueries::L1State(l1_sync_status_send)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + l1_sync_status_recv.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + }, + async { + self.engine_sender + .send(EngineQueries::State(l2_sync_status_send)) + .await + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + l2_sync_status_recv.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError)) + } + ) + .map_err(|_| ErrorObject::from(ErrorCode::InternalError))?; + + return Ok(Self::sync_status_from_actor_queries(l1_sync_status, l2_sync_status)); + } + + async fn op_rollup_config(&self) -> RpcResult { + kona_macros::inc!(gauge, Self::RPC_IDENT, "method" => "op_rollupConfig"); + + let (rollup_config_send, rollup_config_recv) = tokio::sync::oneshot::channel(); + let Ok(()) = self.engine_sender.send(EngineQueries::Config(rollup_config_send)).await + else { + return Err(ErrorObject::from(ErrorCode::InternalError)); + }; + + Ok(rollup_config_recv.await.map_err(|_| ErrorObject::from(ErrorCode::InternalError))?) + } + + async fn op_version(&self) -> RpcResult { + kona_macros::inc!(gauge, Self::RPC_IDENT, "method" => "op_version"); + + const RPC_VERSION: &str = env!("CARGO_PKG_VERSION"); + + return Ok(RPC_VERSION.to_string()); + } +} diff --git a/kona/crates/node/rpc/src/ws.rs b/kona/crates/node/rpc/src/ws.rs new file mode 100644 index 0000000000000..7c39857ac3322 --- /dev/null +++ b/kona/crates/node/rpc/src/ws.rs @@ -0,0 +1,122 @@ +//! Custom RPC subscription endpoints to for the kona node to stream internal state/data. + +use jsonrpsee::{ + PendingSubscriptionSink, SubscriptionSink, core::SubscriptionResult, tracing::warn, +}; +use kona_engine::{EngineQueries, EngineQuerySender, EngineState}; +use kona_protocol::L2BlockInfo; + +use jsonrpsee::core::to_json_raw_value; + +use crate::jsonrpsee::WsServer; + +/// An RPC server that handles subscriptions to the node's state. +#[derive(Debug)] +pub struct WsRPC { + /// The engine query sender. + engine_query_sender: EngineQuerySender, +} + +impl WsRPC { + /// Constructs a new [`WsRPC`] instance. + pub const fn new(engine_query_sender: EngineQuerySender) -> Self { + Self { engine_query_sender } + } + + async fn engine_state_watcher( + &self, + ) -> Result, jsonrpsee::core::SubscriptionError> { + let (query_tx, query_rx) = tokio::sync::oneshot::channel(); + + if let Err(e) = self.engine_query_sender.send(EngineQueries::StateReceiver(query_tx)).await + { + warn!(target: "rpc::ws", ?e, "Failed to send engine state receiver query. The engine query handler is likely closed."); + return Err(jsonrpsee::core::SubscriptionError::from( + "Internal error. Failed to send engine state receiver query. The engine query handler is likely closed.", + )); + } + + query_rx.await.map_err(|_| jsonrpsee::core::SubscriptionError::from("Internal error. Failed to receive engine state receiver query. The engine query handler is likely closed.")) + } + + async fn send_state_update( + sink: &SubscriptionSink, + state: L2BlockInfo, + ) -> Result<(), jsonrpsee::core::SubscriptionError> { + sink.send(to_json_raw_value(&state).map_err(|_| { + jsonrpsee::core::SubscriptionError::from( + "Internal error. Impossible to convert l2 block info to json", + ) + })?) + .await + .map_err(|_| { + jsonrpsee::core::SubscriptionError::from( + "Failed to send head update. Subscription likely dropped.", + ) + }) + } +} + +#[async_trait::async_trait] +impl WsServer for WsRPC { + async fn ws_safe_head_updates(&self, sink: PendingSubscriptionSink) -> SubscriptionResult { + let sink = sink.accept().await?; + + let mut subscription = self.engine_state_watcher().await?; + + let mut current_safe_head = subscription.borrow().sync_state.safe_head(); + + while let Ok(new_state) = subscription + .wait_for(|state| state.sync_state.safe_head() != current_safe_head) + .await + .map(|state| *state) + { + info!(target: "rpc::ws", "Sending safe head update: {:?}", new_state.sync_state.safe_head()); + current_safe_head = new_state.sync_state.safe_head(); + Self::send_state_update(&sink, current_safe_head).await?; + } + + warn!(target: "rpc::ws", "Subscription to safe head updates has been closed."); + Ok(()) + } + + async fn ws_finalized_head_updates(&self, sink: PendingSubscriptionSink) -> SubscriptionResult { + let sink = sink.accept().await?; + + let mut subscription = self.engine_state_watcher().await?; + + let mut current_finalized_head = subscription.borrow().sync_state.finalized_head(); + + while let Ok(new_state) = subscription + .wait_for(|state| state.sync_state.finalized_head() != current_finalized_head) + .await + .map(|state| *state) + { + current_finalized_head = new_state.sync_state.finalized_head(); + Self::send_state_update(&sink, current_finalized_head).await?; + } + + warn!(target: "rpc::ws", "Subscription to finalized head updates has been closed."); + Ok(()) + } + + async fn ws_unsafe_head_updates(&self, sink: PendingSubscriptionSink) -> SubscriptionResult { + let sink = sink.accept().await?; + + let mut subscription = self.engine_state_watcher().await?; + + let mut current_unsafe_head = subscription.borrow().sync_state.unsafe_head(); + + while let Ok(new_state) = subscription + .wait_for(|state| state.sync_state.unsafe_head() != current_unsafe_head) + .await + .map(|state| *state) + { + current_unsafe_head = new_state.sync_state.unsafe_head(); + Self::send_state_update(&sink, current_unsafe_head).await?; + } + + warn!(target: "rpc::ws", "Subscription to unsafe head updates has been closed."); + Ok(()) + } +} diff --git a/kona/crates/node/service/Cargo.toml b/kona/crates/node/service/Cargo.toml new file mode 100644 index 0000000000000..b265ce005f425 --- /dev/null +++ b/kona/crates/node/service/Cargo.toml @@ -0,0 +1,97 @@ +[package] +name = "kona-node-service" +description = "An implementation of the OP Stack consensus node service" +version = "0.1.3" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# workspace +kona-gossip.workspace = true +kona-disc.workspace = true +kona-engine.workspace = true +kona-sources.workspace = true +kona-genesis.workspace = true +kona-derive.workspace = true +kona-protocol.workspace = true +kona-providers-alloy.workspace = true +kona-rpc.workspace = true +kona-peers.workspace = true +kona-macros.workspace = true + +# rollup-boost +rollup-boost.workspace = true + +# alloy +alloy-chains.workspace = true +alloy-signer.workspace = true +alloy-signer-local.workspace = true +alloy-primitives.workspace = true +alloy-rpc-client.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } +alloy-provider = { workspace = true, features = ["reqwest", "reqwest-rustls-tls", "hyper", "hyper-tls"] } +alloy-eips.workspace = true +alloy-transport.workspace = true +alloy-transport-http = { workspace = true, features = ["reqwest", "reqwest-rustls-tls", "hyper", "hyper-tls", "jwt-auth"] } + +# op-alloy +op-alloy-network.workspace = true +op-alloy-rpc-types-engine = { workspace = true, features = ["std"] } +op-alloy-provider.workspace = true + +# general +url.workspace = true +libp2p.workspace = true +libp2p-stream.workspace = true +discv5.workspace = true +futures.workspace = true +tracing.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +async-trait.workspace = true +async-stream.workspace = true +tokio-stream.workspace = true +strum = { workspace = true, features = ["derive"] } +backon.workspace = true +derive_more = { workspace = true, features = ["debug"] } +jsonrpsee = { workspace = true, features = ["server"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tower.workspace = true +http-body-util.workspace = true + +# metrics +metrics = { workspace = true, optional = true } + +[dev-dependencies] +rstest.workspace = true +arbitrary.workspace = true +rand.workspace = true +anyhow.workspace = true +backon.workspace = true +http = "1" +mockall.workspace = true +alloy-primitives = { workspace = true, features = ["k256"] } +alloy-rpc-types-engine = { workspace = true, features = ["arbitrary"] } +alloy-consensus = { workspace = true, features = ["arbitrary"] } +op-alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] } +kona-derive = {workspace = true, features = ["test-utils"]} + +[features] +default = [] +metrics = [ + "dep:metrics", + "kona-derive/metrics", + "kona-disc/metrics", + "kona-engine/metrics", + "kona-gossip/metrics", + "kona-providers-alloy/metrics", + "kona-rpc/metrics", + "libp2p/metrics", +] diff --git a/kona/crates/node/service/README.md b/kona/crates/node/service/README.md new file mode 100644 index 0000000000000..2effabfac5791 --- /dev/null +++ b/kona/crates/node/service/README.md @@ -0,0 +1,10 @@ +# `kona-node-service` + +CI +kona-node-service crate +MIT License +Docs + +An implementation of the OP Stack [RollupNode][rn-spec] service. + +[rn-spec]: https://specs.optimism.io/protocol/rollup-node.html diff --git a/kona/crates/node/service/src/actors/derivation.rs b/kona/crates/node/service/src/actors/derivation.rs new file mode 100644 index 0000000000000..c3b38ef19948c --- /dev/null +++ b/kona/crates/node/service/src/actors/derivation.rs @@ -0,0 +1,529 @@ +//! [NodeActor] implementation for the derivation sub-routine. + +use std::sync::Arc; + +use crate::{ + InteropMode, Metrics, NodeActor, + actors::{CancellableContext, engine::ResetRequest}, +}; +use alloy_provider::RootProvider; +use async_trait::async_trait; +use kona_derive::{ + ActivationSignal, Pipeline, PipelineError, PipelineErrorKind, ResetError, ResetSignal, Signal, + SignalReceiver, StepResult, +}; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use kona_providers_alloy::{ + AlloyChainProvider, AlloyL2ChainProvider, OnlineBeaconClient, OnlineBlobProvider, + OnlinePipeline, +}; +use op_alloy_network::Optimism; +use thiserror::Error; +use tokio::{ + select, + sync::{mpsc, oneshot, watch}, +}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; + +/// The [NodeActor] for the derivation sub-routine. +/// +/// This actor is responsible for receiving messages from [NodeActor]s and stepping the +/// derivation pipeline forward to produce new payload attributes. The actor then sends the payload +/// to the [NodeActor] responsible for the execution sub-routine. +#[derive(Debug)] +pub struct DerivationActor +where + B: PipelineBuilder, +{ + /// The state for the derivation actor. + state: B, + + /// The receiver for L1 head update notifications. + l1_head_updates: watch::Receiver>, + /// The receiver for L2 safe head update notifications. + engine_l2_safe_head: watch::Receiver, + /// A receiver used by the engine to signal derivation to begin. Completing EL sync consumes + /// the instance. + el_sync_complete_rx: oneshot::Receiver<()>, + /// A receiver that sends a [`Signal`] to the derivation pipeline. + /// + /// The derivation actor steps over the derivation pipeline to generate + /// [`OpAttributesWithParent`]. These attributes then need to be executed + /// via the engine api, which is done by sending them through the + /// [`DerivationContext::derived_attributes_tx`] channel. + /// + /// When the engine api receives an `INVALID` response for a new block ( + /// the new [`OpAttributesWithParent`]) during block building, the payload + /// is reduced to "deposits-only". When this happens, the channel and + /// remaining buffered batches need to be flushed out of the derivation + /// pipeline. + /// + /// This channel allows the engine to send a [`Signal::FlushChannel`] + /// message back to the derivation pipeline when an `INVALID` response + /// occurs. + /// + /// Specs: + derivation_signal_rx: mpsc::Receiver, +} + +/// The state for the derivation actor. +#[derive(Debug)] +pub struct DerivationState

+where + P: Pipeline + SignalReceiver, +{ + /// The derivation pipeline. + pub pipeline: P, + /// A flag indicating whether or not derivation is idle. Derivation is considered idle when it + /// has yielded to wait for more data on the DAL. + pub derivation_idle: bool, + /// A flag indicating whether or not derivation is waiting for a signal. When waiting for a + /// signal, derivation cannot process any incoming events. + pub waiting_for_signal: bool, +} + +/// The size of the cache used in the derivation pipeline's providers. +const DERIVATION_PROVIDER_CACHE_SIZE: usize = 1024; + +/// A trait for building derivation pipelines. +#[async_trait] +pub trait PipelineBuilder: Send + Sync + 'static { + /// The type of pipeline to build. + type Pipeline: Pipeline + SignalReceiver + Send + Sync + 'static; + + /// Builds the derivation pipeline. + async fn build(self) -> DerivationState; +} + +/// The configuration necessary to build the derivation actor. +#[derive(Debug)] +pub struct DerivationBuilder { + /// The L1 provider. + pub l1_provider: RootProvider, + /// Whether to trust the L1 RPC. + pub l1_trust_rpc: bool, + /// The L1 beacon client. + pub l1_beacon: OnlineBeaconClient, + /// The L2 provider. + pub l2_provider: RootProvider, + /// Whether to trust the L2 RPC. + pub l2_trust_rpc: bool, + /// The rollup config. + pub rollup_config: Arc, + /// The L1 chain configuration. + pub l1_config: Arc, + /// The interop mode. + pub interop_mode: InteropMode, +} + +#[async_trait] +impl PipelineBuilder for DerivationBuilder { + type Pipeline = OnlinePipeline; + + async fn build(self) -> DerivationState { + // Create the caching L1/L2 EL providers for derivation. + let l1_derivation_provider = AlloyChainProvider::new_with_trust( + self.l1_provider.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l1_trust_rpc, + ); + let l2_derivation_provider = AlloyL2ChainProvider::new_with_trust( + self.l2_provider.clone(), + self.rollup_config.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l2_trust_rpc, + ); + + let pipeline = match self.interop_mode { + InteropMode::Polled => OnlinePipeline::new_polled( + self.rollup_config.clone(), + self.l1_config.clone(), + OnlineBlobProvider::init(self.l1_beacon.clone()).await, + l1_derivation_provider, + l2_derivation_provider, + ), + InteropMode::Indexed => OnlinePipeline::new_indexed( + self.rollup_config.clone(), + self.l1_config.clone(), + OnlineBlobProvider::init(self.l1_beacon.clone()).await, + l1_derivation_provider, + l2_derivation_provider, + ), + }; + + DerivationState::new(pipeline) + } +} + +/// The inbound channels for the derivation actor. +/// These channels are used to send messages to the derivation actor by other actors. +#[derive(Debug)] +pub struct DerivationInboundChannels { + /// The sender for L1 head update notifications. + pub l1_head_updates_tx: watch::Sender>, + /// The sender for L2 safe head update notifications. + pub engine_l2_safe_head_tx: watch::Sender, + /// A sender used by the engine to signal derivation to begin. Completing EL sync consumes the + /// instance. + pub el_sync_complete_tx: oneshot::Sender<()>, + /// A sender that sends a [`Signal`] to the derivation pipeline. + /// + /// This channel should be used by the engine actor to send [`Signal`]s to the derivation + /// pipeline. The signals are received by `DerivationActor::derivation_signal_rx`. + pub derivation_signal_tx: mpsc::Sender, +} + +/// The communication context used by the derivation actor. +#[derive(Debug)] +pub struct DerivationContext { + /// The cancellation token, shared between all tasks. + pub cancellation: CancellationToken, + /// Sends the derived [`OpAttributesWithParent`]s produced by the actor. + pub derived_attributes_tx: mpsc::Sender, + /// The reset request sender, used to handle [`PipelineErrorKind::Reset`] events and forward + /// them to the engine. + pub reset_request_tx: mpsc::Sender, +} + +impl CancellableContext for DerivationContext { + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} + +impl

DerivationState

+where + P: Pipeline + SignalReceiver, +{ + /// Creates a new instance of the [DerivationState]. + pub const fn new(pipeline: P) -> Self { + Self { pipeline, derivation_idle: true, waiting_for_signal: false } + } + + /// Handles a [`Signal`] received over the derivation signal receiver channel. + async fn signal(&mut self, signal: Signal) { + if let Signal::Reset(ResetSignal { l1_origin, .. }) = signal { + kona_macros::set!(counter, Metrics::DERIVATION_L1_ORIGIN, l1_origin.number); + } + + match self.pipeline.signal(signal).await { + Ok(_) => info!(target: "derivation", ?signal, "[SIGNAL] Executed Successfully"), + Err(e) => { + error!(target: "derivation", ?e, ?signal, "Failed to signal derivation pipeline") + } + } + } + + /// Attempts to step the derivation pipeline forward as much as possible in order to produce the + /// next safe payload. + async fn produce_next_attributes( + &mut self, + engine_l2_safe_head: &watch::Receiver, + reset_request_tx: &mpsc::Sender, + ) -> Result { + // As we start the safe head at the disputed block's parent, we step the pipeline until the + // first attributes are produced. All batches at and before the safe head will be + // dropped, so the first payload will always be the disputed one. + loop { + let l2_safe_head = *engine_l2_safe_head.borrow(); + match self.pipeline.step(l2_safe_head).await { + StepResult::PreparedAttributes => { /* continue; attributes will be sent off. */ } + StepResult::AdvancedOrigin => { + let origin = + self.pipeline.origin().ok_or(PipelineError::MissingOrigin.crit())?.number; + + kona_macros::set!(counter, Metrics::DERIVATION_L1_ORIGIN, origin); + debug!(target: "derivation", l1_block = origin, "Advanced L1 origin"); + } + StepResult::OriginAdvanceErr(e) | StepResult::StepFailed(e) => { + match e { + PipelineErrorKind::Temporary(e) => { + // NotEnoughData is transient, and doesn't imply we need to wait for + // more data. We can continue stepping until we receive an Eof. + if matches!(e, PipelineError::NotEnoughData) { + continue; + } + + debug!( + target: "derivation", + "Exhausted data source for now; Yielding until the chain has extended." + ); + return Err(DerivationError::Yield); + } + PipelineErrorKind::Reset(e) => { + warn!(target: "derivation", "Derivation pipeline is being reset: {e}"); + + let system_config = self + .pipeline + .system_config_by_number(l2_safe_head.block_info.number) + .await?; + + if matches!(e, ResetError::HoloceneActivation) { + let l1_origin = self + .pipeline + .origin() + .ok_or(PipelineError::MissingOrigin.crit())?; + + self.pipeline + .signal( + ActivationSignal { + l2_safe_head, + l1_origin, + system_config: Some(system_config), + } + .signal(), + ) + .await?; + } else { + if let ResetError::ReorgDetected(expected, new) = e { + warn!( + target: "derivation", + "L1 reorg detected! Expected: {expected} | New: {new}" + ); + + kona_macros::inc!(counter, Metrics::L1_REORG_COUNT); + } + // send the `reset` signal to the engine actor only when interop is + // not active. + if !self + .pipeline + .rollup_config() + .is_interop_active(l2_safe_head.block_info.timestamp) + { + reset_request_tx.send(ResetRequest{result_tx: None}).await.map_err(|e| { + error!(target: "derivation", ?e, "Failed to send reset request"); + DerivationError::Sender(Box::new(e)) + })?; + } + self.waiting_for_signal = true; + return Err(DerivationError::Yield); + } + } + PipelineErrorKind::Critical(_) => { + error!(target: "derivation", "Critical derivation error: {e}"); + kona_macros::inc!(counter, Metrics::DERIVATION_CRITICAL_ERROR); + return Err(e.into()); + } + } + } + } + + // If there are any new attributes, send them to the execution actor. + if let Some(attrs) = self.pipeline.next() { + return Ok(attrs); + } + } + } + + /// Attempts to process the next payload attributes. + /// + /// There are a few constraints around stepping on the derivation pipeline. + /// - The l2 safe head ([`L2BlockInfo`]) must not be the zero hash. + /// - The pipeline must not be stepped on with the same L2 safe head twice. + /// - Errors must be bubbled up to the caller. + /// + /// In order to achieve this, the channel to receive the L2 safe head + /// [`L2BlockInfo`] from the engine is *only* marked as _seen_ after payload + /// attributes are successfully produced. If the pipeline step errors, + /// the same [`L2BlockInfo`] is used again. If the [`L2BlockInfo`] is the + /// zero hash, the pipeline is not stepped on. + async fn process( + &mut self, + msg: InboundDerivationMessage, + engine_l2_safe_head: &mut watch::Receiver, + el_sync_complete_rx: &oneshot::Receiver<()>, + derived_attributes_tx: &mpsc::Sender, + reset_request_tx: &mpsc::Sender, + ) -> Result<(), DerivationError> { + // Only attempt derivation once the engine finishes syncing. + if !el_sync_complete_rx.is_terminated() { + trace!(target: "derivation", "Engine not ready, skipping derivation"); + return Ok(()); + } else if self.waiting_for_signal { + trace!(target: "derivation", "Waiting to receive a signal, skipping derivation"); + return Ok(()); + } + + // If derivation isn't idle and the message hasn't observed a safe head update already, + // check if the safe head has changed before continuing. This is to prevent attempts to + // progress the pipeline while it is in the middle of processing a channel. + if !(self.derivation_idle || msg == InboundDerivationMessage::SafeHeadUpdated) { + match engine_l2_safe_head.has_changed() { + Ok(true) => { /* Proceed to produce next payload attributes. */ } + Ok(false) => { + trace!(target: "derivation", "Safe head hasn't changed, skipping derivation."); + return Ok(()); + } + Err(e) => { + error!(target: "derivation", ?e, "Failed to check if safe head has changed"); + return Err(DerivationError::L2SafeHeadReceiveFailed); + } + } + } + + // Wait for the engine to initialize unknowns prior to kicking off derivation. + let engine_safe_head = *engine_l2_safe_head.borrow(); + if engine_safe_head.block_info.hash.is_zero() { + warn!(target: "derivation", engine_safe_head = ?engine_safe_head.block_info.number, "Waiting for engine to initialize state prior to derivation."); + return Ok(()); + } + + // Advance the pipeline as much as possible, new data may be available or there still may be + // payloads in the attributes queue. + let payload_attrs = + match self.produce_next_attributes(engine_l2_safe_head, reset_request_tx).await { + Ok(attrs) => attrs, + Err(DerivationError::Yield) => { + // Yield until more data is available. + self.derivation_idle = true; + return Ok(()); + } + Err(e) => { + return Err(e); + } + }; + + // Mark derivation as busy. + self.derivation_idle = false; + + // Mark the L2 safe head as seen. + engine_l2_safe_head.borrow_and_update(); + + // Send payload attributes out for processing. + derived_attributes_tx + .send(payload_attrs) + .await + .map_err(|e| DerivationError::Sender(Box::new(e)))?; + + Ok(()) + } +} + +impl DerivationActor +where + B: PipelineBuilder, +{ + /// Creates a new instance of the [DerivationActor]. + pub fn new(state: B) -> (DerivationInboundChannels, Self) { + let (l1_head_updates_tx, l1_head_updates_rx) = watch::channel(None); + let (engine_l2_safe_head_tx, engine_l2_safe_head_rx) = + watch::channel(L2BlockInfo::default()); + let (el_sync_complete_tx, el_sync_complete_rx) = oneshot::channel(); + let (derivation_signal_tx, derivation_signal_rx) = mpsc::channel(16); + let actor = Self { + state, + l1_head_updates: l1_head_updates_rx, + engine_l2_safe_head: engine_l2_safe_head_rx, + el_sync_complete_rx, + derivation_signal_rx, + }; + + ( + DerivationInboundChannels { + l1_head_updates_tx, + engine_l2_safe_head_tx, + el_sync_complete_tx, + derivation_signal_tx, + }, + actor, + ) + } +} + +#[async_trait] +impl NodeActor for DerivationActor +where + B: PipelineBuilder, +{ + type Error = DerivationError; + type StartData = DerivationContext; + + async fn start( + mut self, + DerivationContext { + derived_attributes_tx, + reset_request_tx, + cancellation, + }: Self::StartData, + ) -> Result<(), Self::Error> { + let mut state = self.state.build().await; + + loop { + select! { + biased; + + _ = cancellation.cancelled() => { + info!( + target: "derivation", + "Received shutdown signal. Exiting derivation task." + ); + return Ok(()); + } + signal = self.derivation_signal_rx.recv() => { + let Some(signal) = signal else { + error!( + target: "derivation", + ?signal, + "DerivationActor failed to receive signal" + ); + return Err(DerivationError::SignalReceiveFailed); + }; + + state.signal(signal).await; + state.waiting_for_signal = false; + } + msg = self.l1_head_updates.changed() => { + if let Err(err) = msg { + error!( + target: "derivation", + ?err, + "L1 head update stream closed without cancellation. Exiting derivation task." + ); + return Ok(()); + } + + state.process(InboundDerivationMessage::NewDataAvailable, &mut self.engine_l2_safe_head, &self.el_sync_complete_rx, &derived_attributes_tx, &reset_request_tx).await?; + } + _ = self.engine_l2_safe_head.changed() => { + state.process(InboundDerivationMessage::SafeHeadUpdated, &mut self.engine_l2_safe_head, &self.el_sync_complete_rx, &derived_attributes_tx, &reset_request_tx).await?; + } + _ = &mut self.el_sync_complete_rx, if !self.el_sync_complete_rx.is_terminated() => { + info!(target: "derivation", "Engine finished syncing, starting derivation."); + // Optimistically process the first message. + state.process(InboundDerivationMessage::NewDataAvailable, &mut self.engine_l2_safe_head, &self.el_sync_complete_rx, &derived_attributes_tx, &reset_request_tx).await?; + } + } + } + } +} + +/// Messages that the [DerivationActor] can receive from other actors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InboundDerivationMessage { + /// New data is potentially available for processing on the data availability layer. + NewDataAvailable, + /// The engine has updated its safe head. An attempt to process the next payload attributes can + /// be made. + SafeHeadUpdated, +} + +/// An error from the [DerivationActor]. +#[derive(Error, Debug)] +pub enum DerivationError { + /// An error originating from the derivation pipeline. + #[error(transparent)] + Pipeline(#[from] PipelineErrorKind), + /// Waiting for more data to be available. + #[error("Waiting for more data to be available")] + Yield, + /// An error originating from the broadcast sender. + #[error("Failed to send event to broadcast sender: {0}")] + Sender(Box), + /// An error from the signal receiver. + #[error("Failed to receive signal")] + SignalReceiveFailed, + /// Unable to receive the L2 safe head to step on the pipeline. + #[error("Failed to receive L2 safe head")] + L2SafeHeadReceiveFailed, +} diff --git a/kona/crates/node/service/src/actors/engine/actor.rs b/kona/crates/node/service/src/actors/engine/actor.rs new file mode 100644 index 0000000000000..28ebcf79dc1a9 --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/actor.rs @@ -0,0 +1,720 @@ +//! The [`EngineActor`]. + +use super::{BlockEngineResult, EngineError, L2Finalizer}; +use crate::{BlockEngineError, NodeActor, NodeMode, actors::CancellableContext}; +use alloy_provider::RootProvider; +use alloy_rpc_types_engine::{JwtSecret, PayloadId}; +use async_trait::async_trait; +use futures::{FutureExt, future::OptionFuture}; +use kona_derive::{ResetSignal, Signal}; +use kona_engine::{ + BuildTask, ConsolidateTask, Engine, EngineClient, EngineClientBuilder, + EngineClientBuilderError, EngineQueries, EngineState as InnerEngineState, EngineTask, + EngineTaskError, EngineTaskErrorSeverity, InsertTask, OpEngineClient, RollupBoostServer, + RollupBoostServerArgs, SealTask, SealTaskError, +}; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use kona_rpc::{RollupBoostAdminQuery, RollupBoostHealthQuery}; +use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use std::{fmt::Debug, sync::Arc, time::Duration}; +use tokio::{ + sync::{mpsc, oneshot, watch}, + task::JoinHandle, +}; +use tokio_util::{ + future::FutureExt as _, + sync::{CancellationToken, WaitForCancellationFuture}, +}; +use url::Url; + +/// A request to build a payload. +/// Contains the attributes to build and a channel to send back the resulting `PayloadId`. +#[derive(Debug)] +pub struct BuildRequest { + /// The [`OpAttributesWithParent`] from which the block build should be started. + pub attributes: OpAttributesWithParent, + /// The channel on which the result, successful or not, will be sent. + pub result_tx: mpsc::Sender, +} + +/// A request to reset the engine forkchoice. +/// Optionally contains a channel to send back the response if the caller would like to know that +/// the request was successfully processed. +#[derive(Debug)] +pub struct ResetRequest { + /// response will be sent to this channel, if `Some`. + pub result_tx: Option>>, +} + +/// A request to seal and canonicalize a payload. +/// Contains the `PayloadId`, attributes, and a channel to send back the result. +#[derive(Debug)] +pub struct SealRequest { + /// The `PayloadId` to seal and canonicalize. + pub payload_id: PayloadId, + /// The attributes necessary for the seal operation. + pub attributes: OpAttributesWithParent, + /// The channel on which the result, successful or not, will be sent. + pub result_tx: mpsc::Sender>, +} + +/// The [`EngineActor`] is responsible for managing the operations sent to the execution layer's +/// Engine API. To accomplish this, it uses the [`Engine`] task queue to order Engine API +/// interactions based off of the [`Ord`] implementation of [`EngineTask`]. +#[derive(Debug)] +pub struct EngineActor { + /// A channel to receive [`OpAttributesWithParent`] from the derivation actor. + attributes_rx: mpsc::Receiver, + /// The [`EngineConfig`] used to build the actor. + builder: EngineConfig, + /// A channel to receive build requests. + /// Upon successful processing of the provided attributes, a `PayloadId` will be sent via the + /// provided sender. + /// ## Note + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + build_request_rx: Option>, + /// The [`L2Finalizer`], used to finalize L2 blocks. + finalizer: L2Finalizer, + /// Handler for inbound queries to the engine. + inbound_queries: mpsc::Receiver, + /// A channel to receive reset requests. + reset_request_rx: mpsc::Receiver, + /// Shared admin query handle (from rollup-boost), exposed for RPC wiring. + /// Only set when rollup boost is enabled. + pub rollup_boost_admin_query_rx: mpsc::Receiver, + /// Shared health handle (from rollup-boost), exposed for RPC wiring. + /// Only set when rollup boost is enabled. + pub rollup_boost_health_query_rx: mpsc::Receiver, + /// A channel to receive seal requests. + /// The success/fail result of the sealing operation will be sent via the provided sender. + /// ## Note + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + seal_request_rx: Option>, + /// A channel to receive [`OpExecutionPayloadEnvelope`] from the network actor. + unsafe_block_rx: mpsc::Receiver, + /// A channel to use to relay the current unsafe head. + /// ## Note + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + unsafe_head_tx: Option>, +} + +/// The outbound data for the [`EngineActor`]. +#[derive(Debug)] +pub struct EngineInboundData { + /// A channel to send [`OpAttributesWithParent`] to the engine actor. + pub attributes_tx: mpsc::Sender, + /// A channel to use to send [`BuildRequest`] payloads to the engine actor. + /// + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + pub build_request_tx: Option>, + /// A channel that sends new finalized L1 blocks intermittently. + pub finalized_l1_block_tx: watch::Sender>, + /// Handler to send inbound queries to the engine. + pub inbound_queries_tx: mpsc::Sender, + /// A channel to send reset requests. + pub reset_request_tx: mpsc::Sender, + /// A channel to send rollup boost admin queries to the engine actor. + pub rollup_boost_admin_query_tx: mpsc::Sender, + /// A channel to send rollup boost health queries to the engine actor. + pub rollup_boost_health_query_tx: mpsc::Sender, + /// A channel to use to send [`SealRequest`] payloads to the engine actor. + /// + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + pub seal_request_tx: Option>, + /// A channel to send [`OpExecutionPayloadEnvelope`] to the engine actor. + /// + /// ## Note + /// The sequencer actor should not need to send [`OpExecutionPayloadEnvelope`]s to the engine + /// actor through that channel. Instead, it should use the `build_request_tx` channel to + /// trigger [`BuildTask`] tasks which should insert the block newly built to the engine + /// state upon completion. + pub unsafe_block_tx: mpsc::Sender, + /// A receiver to use to view the latest unsafe head [`L2BlockInfo`] and await its changes. + /// + /// This is `Some` when the node is in sequencer mode, and `None` when the node is in validator + /// mode. + pub unsafe_head_rx: Option>, +} + +/// Configuration for the Engine Actor. +#[derive(Debug, Clone)] +pub struct EngineConfig { + /// The [`RollupConfig`]. + pub config: Arc, + + /// Builder url. + pub builder_url: Url, + /// Builder jwt secret. + pub builder_jwt_secret: JwtSecret, + /// Builder timeout. + pub builder_timeout: Duration, + + /// The engine rpc url. + pub l2_url: Url, + /// The engine jwt secret. + pub l2_jwt_secret: JwtSecret, + /// The l2 timeout. + pub l2_timeout: Duration, + + /// The L1 rpc url. + pub l1_url: Url, + + /// The mode of operation for the node. + /// When the node is in sequencer mode, the engine actor will receive requests to build blocks + /// from the sequencer actor. + pub mode: NodeMode, + + /// The rollup boost arguments. + pub rollup_boost: RollupBoostServerArgs, +} + +impl EngineConfig { + /// Launches the [`Engine`]. Returns the [`Engine`] and a channel to receive engine state + /// updates. + fn build_state( + self, + ) -> Result< + EngineActorState>>, + EngineClientBuilderError, + > { + let client = EngineClientBuilder { + builder: self.builder_url.clone(), + builder_jwt: self.builder_jwt_secret, + builder_timeout: self.builder_timeout, + l2: self.l2_url.clone(), + l2_jwt: self.l2_jwt_secret, + l2_timeout: self.l2_timeout, + l1_rpc: self.l1_url.clone(), + cfg: self.config.clone(), + rollup_boost: self.rollup_boost.clone(), + } + .build()? + .into(); + + let state = InnerEngineState::default(); + let (engine_state_send, _) = tokio::sync::watch::channel(state); + let (engine_queue_length_send, _) = tokio::sync::watch::channel(0); + + Ok(EngineActorState { + rollup: self.config, + client, + engine: Engine::new(state, engine_state_send, engine_queue_length_send), + }) + } +} + +/// The configuration for the [`EngineActor`]. +#[derive(Debug)] +pub(super) struct EngineActorState { + /// The [`RollupConfig`] used to build tasks. + pub(super) rollup: Arc, + /// An [`OpEngineClient`] used for creating engine tasks. + pub(super) client: Arc, + /// The [`Engine`] task queue. + pub(super) engine: Engine, +} + +/// The communication context used by the engine actor. +#[derive(Debug)] +pub struct EngineContext { + /// The cancellation token, shared between all tasks. + pub cancellation: CancellationToken, + /// The sender for L2 safe head update notifications. + pub engine_l2_safe_head_tx: watch::Sender, + /// A channel to send a signal that EL sync has completed. Informs the derivation actor to + /// start. Because the EL sync state machine within [`InnerEngineState`] can only complete + /// once, this channel is consumed after the first successful send. Future cases where EL + /// sync is re-triggered can occur, but we will not block derivation on it. + pub sync_complete_tx: oneshot::Sender<()>, + /// A way for the engine actor to send a [`Signal`] back to the derivation actor. + pub derivation_signal_tx: mpsc::Sender, +} + +impl CancellableContext for EngineContext { + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} + +struct SequencerChannels { + build_request_rx: Option>, + build_request_tx: Option>, + seal_request_rx: Option>, + seal_request_tx: Option>, + unsafe_head_rx: Option>, + unsafe_head_tx: Option>, +} + +impl EngineActor { + /// Constructs a new [`EngineActor`] from the params. + pub fn new(config: EngineConfig) -> (EngineInboundData, Self) { + let (finalized_l1_block_tx, finalized_l1_block_rx) = watch::channel(None); + let (inbound_queries_tx, inbound_queries_rx) = mpsc::channel(1024); + let (attributes_tx, attributes_rx) = mpsc::channel(1024); + let (unsafe_block_tx, unsafe_block_rx) = mpsc::channel(1024); + let (reset_request_tx, reset_request_rx) = mpsc::channel(1024); + + let sequencer_channels = if config.mode.is_sequencer() { + let (build_request_tx, build_request_rx) = mpsc::channel(1024); + let (seal_request_tx, seal_request_rx) = mpsc::channel(1024); + let (unsafe_head_tx, unsafe_head_rx) = watch::channel(L2BlockInfo::default()); + + SequencerChannels { + build_request_rx: Some(build_request_rx), + build_request_tx: Some(build_request_tx), + seal_request_rx: Some(seal_request_rx), + seal_request_tx: Some(seal_request_tx), + unsafe_head_rx: Some(unsafe_head_rx), + unsafe_head_tx: Some(unsafe_head_tx), + } + } else { + SequencerChannels { + build_request_rx: None, + build_request_tx: None, + seal_request_rx: None, + seal_request_tx: None, + unsafe_head_rx: None, + unsafe_head_tx: None, + } + }; + + let (rollup_boost_admin_query_tx, rollup_boost_admin_query_rx) = mpsc::channel(1024); + let (rollup_boost_health_query_tx, rollup_boost_health_query_rx) = mpsc::channel(1024); + + let actor = Self { + builder: config, + attributes_rx, + unsafe_block_rx, + unsafe_head_tx: sequencer_channels.unsafe_head_tx, + reset_request_rx, + inbound_queries: inbound_queries_rx, + build_request_rx: sequencer_channels.build_request_rx, + seal_request_rx: sequencer_channels.seal_request_rx, + finalizer: L2Finalizer::new(finalized_l1_block_rx), + rollup_boost_admin_query_rx, + rollup_boost_health_query_rx, + }; + + let outbound_data = EngineInboundData { + attributes_tx, + build_request_tx: sequencer_channels.build_request_tx, + finalized_l1_block_tx, + inbound_queries_tx, + reset_request_tx, + rollup_boost_admin_query_tx, + rollup_boost_health_query_tx, + seal_request_tx: sequencer_channels.seal_request_tx, + unsafe_block_tx, + unsafe_head_rx: sequencer_channels.unsafe_head_rx, + }; + + (outbound_data, actor) + } +} + +impl EngineActorState { + /// Starts a task to handle engine queries. + fn start_query_task( + &self, + mut inbound_query_channel: tokio::sync::mpsc::Receiver, + mut rollup_boost_admin_query_rx: tokio::sync::mpsc::Receiver, + mut rollup_boost_health_query_rx: tokio::sync::mpsc::Receiver, + rollup_boost: Arc, + ) -> JoinHandle> { + let state_recv = self.engine.state_subscribe(); + let queue_length_recv = self.engine.queue_length_subscribe(); + let engine_client = self.client.clone(); + let rollup_config = self.rollup.clone(); + + tokio::spawn(async move { + loop { + tokio::select! { + req = inbound_query_channel.recv(), if !inbound_query_channel.is_closed() => { + { + let Some(req) = req else { + error!(target: "engine", "Engine query receiver closed unexpectedly"); + return Err(EngineError::ChannelClosed); + }; + + trace!(target: "engine", ?req, "Received engine query."); + + if let Err(e) = req + .handle(&state_recv, &queue_length_recv, &engine_client, &rollup_config) + .await + { + warn!(target: "engine", err = ?e, "Failed to handle engine query."); + } + } + } + admin_query = rollup_boost_admin_query_rx.recv(), if !rollup_boost_admin_query_rx.is_closed() => { + trace!(target: "engine", ?admin_query, "Received rollup boost admin query."); + + let Some(admin_query) = admin_query else { + warn!(target: "engine", "Received a rollup boost query but no rollup-boost config found"); + continue; + }; + + match admin_query { + RollupBoostAdminQuery::SetExecutionMode { execution_mode } => { + rollup_boost.server.set_execution_mode(execution_mode); + } + RollupBoostAdminQuery::GetExecutionMode { sender } => { + let execution_mode = rollup_boost.server.get_execution_mode(); + sender.send(execution_mode).unwrap(); + } + } + } + health_query = rollup_boost_health_query_rx.recv(), if !rollup_boost_health_query_rx.is_closed() => { + trace!(target: "engine", ?health_query, "Received rollup boost health query."); + + let Some(health_query) = health_query else { + error!(target: "engine", "Rollup boost health query receiver closed unexpectedly"); + return Err(EngineError::ChannelClosed); + }; + + let health = rollup_boost.get_health(); + health_query.sender.send(health.into()).unwrap(); + } + } + } + }) + } + + /// Resets the inner [`Engine`] and propagates the reset to the derivation actor. + pub(super) async fn reset( + &mut self, + derivation_signal_tx: &mpsc::Sender, + engine_l2_safe_head_tx: &watch::Sender, + finalizer: &mut L2Finalizer, + ) -> Result<(), EngineError> { + // Reset the engine. + let (l2_safe_head, l1_origin, system_config) = + self.engine.reset(self.client.clone(), self.rollup.clone()).await?; + + // Attempt to update the safe head following the reset. + // IMPORTANT NOTE: We need to update the safe head BEFORE sending the reset signal to the + // derivation actor. Since the derivation actor receives the safe head via a watch + // channel, updating the safe head after sending the reset signal may cause a race + // condition where the derivation actor receives the pre-reset safe head. + self.maybe_update_safe_head(engine_l2_safe_head_tx); + + // Signal the derivation actor to reset. + let signal = ResetSignal { l2_safe_head, l1_origin, system_config: Some(system_config) }; + match derivation_signal_tx.send(signal.signal()).await { + Ok(_) => info!(target: "engine", "Sent reset signal to derivation actor"), + Err(err) => { + error!(target: "engine", ?err, "Failed to send reset signal to the derivation actor"); + return Err(EngineError::ChannelClosed); + } + } + + // Clear the queue of L2 blocks awaiting finalization. + finalizer.clear(); + + Ok(()) + } + + /// Drains the inner [`Engine`] task queue and attempts to update the safe head. + async fn drain( + &mut self, + derivation_signal_tx: &mpsc::Sender, + sync_complete_tx: &mut Option>, + engine_l2_safe_head_tx: &watch::Sender, + finalizer: &mut L2Finalizer, + ) -> Result<(), EngineError> { + match self.engine.drain().await { + Ok(_) => { + trace!(target: "engine", "[ENGINE] tasks drained"); + } + Err(err) => { + match err.severity() { + EngineTaskErrorSeverity::Critical => { + error!(target: "engine", ?err, "Critical error draining engine tasks"); + return Err(err.into()); + } + EngineTaskErrorSeverity::Reset => { + warn!(target: "engine", ?err, "Received reset request"); + self.reset(derivation_signal_tx, engine_l2_safe_head_tx, finalizer).await?; + } + EngineTaskErrorSeverity::Flush => { + // This error is encountered when the payload is marked INVALID + // by the engine api. Post-holocene, the payload is replaced by + // a "deposits-only" block and re-executed. At the same time, + // the channel and any remaining buffered batches are flushed. + warn!(target: "engine", ?err, "Invalid payload, Flushing derivation pipeline."); + match derivation_signal_tx.send(Signal::FlushChannel).await { + Ok(_) => { + debug!(target: "engine", "Sent flush signal to derivation actor") + } + Err(err) => { + error!(target: "engine", ?err, "Failed to send flush signal to the derivation actor."); + return Err(EngineError::ChannelClosed); + } + } + } + EngineTaskErrorSeverity::Temporary => { + trace!(target: "engine", ?err, "Temporary error draining engine tasks"); + } + } + } + } + + self.maybe_update_safe_head(engine_l2_safe_head_tx); + self.check_el_sync( + derivation_signal_tx, + engine_l2_safe_head_tx, + sync_complete_tx, + finalizer, + ) + .await?; + + Ok(()) + } + + /// Checks if the EL has finished syncing, notifying the derivation actor if it has. + async fn check_el_sync( + &mut self, + derivation_signal_tx: &mpsc::Sender, + engine_l2_safe_head_tx: &watch::Sender, + sync_complete_tx: &mut Option>, + finalizer: &mut L2Finalizer, + ) -> Result<(), EngineError> { + if self.engine.state().el_sync_finished { + let Some(sync_complete_tx) = std::mem::take(sync_complete_tx) else { + return Ok(()); + }; + + // Only reset the engine if the sync state does not already know about a finalized + // block. + if self.engine.state().sync_state.finalized_head() != L2BlockInfo::default() { + return Ok(()); + } + + // If the sync status is finished, we can reset the engine and start derivation. + info!(target: "engine", "Performing initial engine reset"); + self.reset(derivation_signal_tx, engine_l2_safe_head_tx, finalizer).await?; + sync_complete_tx.send(()).ok(); + } + + Ok(()) + } + + /// Attempts to update the safe head via the watch channel. + fn maybe_update_safe_head(&self, engine_l2_safe_head_tx: &watch::Sender) { + let state_safe_head = self.engine.state().sync_state.safe_head(); + let update = |head: &mut L2BlockInfo| { + if head != &state_safe_head { + *head = state_safe_head; + return true; + } + false + }; + let sent = engine_l2_safe_head_tx.send_if_modified(update); + info!(target: "engine", safe_head = ?state_safe_head, ?sent, "Attempted L2 Safe Head Update"); + } +} + +#[async_trait] +impl NodeActor for EngineActor { + type Error = EngineError; + type StartData = EngineContext; + + async fn start( + mut self, + EngineContext { + cancellation, + engine_l2_safe_head_tx, + sync_complete_tx, + derivation_signal_tx, + }: Self::StartData, + ) -> Result<(), Self::Error> { + let mut state = self.builder.build_state()?; + + // Start the engine query server in a separate task to avoid blocking the main task. + let handle = state + .start_query_task( + self.inbound_queries, + self.rollup_boost_admin_query_rx, + self.rollup_boost_health_query_rx, + state.client.rollup_boost.clone(), + ) + .with_cancellation_token(&cancellation) + .then(async |result| { + cancellation.cancel(); + + let Some(result) = result else { + warn!(target: "engine", "Engine query task cancelled"); + return Ok(()); + }; + + let Ok(result) = result else { + error!(target: "engine", ?result, "Engine query task panicked"); + return Err(EngineError::ChannelClosed); + }; + + match result { + Ok(()) => { + info!(target: "engine", "Engine query task completed successfully"); + Ok(()) + } + Err(err) => { + error!(target: "engine", ?err, "Engine query task failed"); + Err(err) + } + } + }); + + // The sync complete tx is consumed after the first successful send. Hence we need to wrap + // it in an `Option` to ensure we satisfy the borrow checker. + let mut sync_complete_tx = Some(sync_complete_tx); + + loop { + tokio::select! { + _ = cancellation.cancelled() => { + warn!(target: "engine", "EngineActor received shutdown signal. Aborting engine query task."); + + handle.await?; + + return Ok(()); + }, + + drain_result = // Attempt to drain all outstanding tasks from the engine queue before adding new ones. + state + .drain( + &derivation_signal_tx, + &mut sync_complete_tx, + &engine_l2_safe_head_tx, + &mut self.finalizer, + ) + => { + if let Err(err) = drain_result { + error!(target: "engine", ?err, "Failed to drain engine tasks"); + cancellation.cancel(); + return Err(err); + } + + // If the unsafe head has updated, propagate it to the outbound channels. + if let Some(unsafe_head_tx) = self.unsafe_head_tx.as_mut() { + unsafe_head_tx.send_if_modified(|val| { + let new_head = state.engine.state().sync_state.unsafe_head(); + (*val != new_head).then(|| *val = new_head).is_some() + }); + } + } + } + + tokio::select! { + biased; + + _ = cancellation.cancelled() => { + warn!(target: "engine", "EngineActor received shutdown signal. Aborting engine query task."); + + return Ok(()); + } + reset = self.reset_request_rx.recv() => { + let Some(ResetRequest{result_tx: result_tx_option}) = reset else { + error!(target: "engine", "Reset request receiver closed unexpectedly"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + }; + + warn!(target: "engine", "Received reset request"); + + let reset_res = state + .reset(&derivation_signal_tx, &engine_l2_safe_head_tx, &mut self.finalizer) + .await; + + // Send the result if there is a channel on which to do so. + if let Some(tx) = result_tx_option { + let response_payload = reset_res.as_ref().map(|_| ()).map_err(|e| BlockEngineError::ResetForkchoiceError(e.to_string())); + if tx.send(response_payload).await.is_err() { + warn!(target: "engine", "Sending reset response failed"); + } + } + + reset_res?; + } + Some(req) = OptionFuture::from(self.seal_request_rx.as_mut().map(|rx| rx.recv())), if self.seal_request_rx.is_some() => { + let Some(SealRequest{payload_id, attributes, result_tx}) = req else { + error!(target: "engine", "Seal request receiver closed unexpectedly while in sequencer mode"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + }; + + let task = EngineTask::Seal(Box::new(SealTask::new( + state.client.clone(), + state.rollup.clone(), + payload_id, + attributes, + // The payload is not derived in this case. + false, + Some(result_tx), + ))); + state.engine.enqueue(task); + } + Some(req) = OptionFuture::from(self.build_request_rx.as_mut().map(|rx| rx.recv())), if self.build_request_rx.is_some() => { + let Some(BuildRequest{attributes, result_tx}) = req else { + error!(target: "engine", "Build request receiver closed unexpectedly while in sequencer mode"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + }; + + let task = EngineTask::Build(Box::new(BuildTask::new( + state.client.clone(), + state.rollup.clone(), + attributes, + Some(result_tx), + ))); + state.engine.enqueue(task); + } + unsafe_block = self.unsafe_block_rx.recv() => { + let Some(envelope) = unsafe_block else { + error!(target: "engine", "Unsafe block receiver closed unexpectedly"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + }; + let task = EngineTask::Insert(Box::new(InsertTask::new( + state.client.clone(), + state.rollup.clone(), + envelope, + false, // The payload is not derived in this case. This is an unsafe block. + ))); + state.engine.enqueue(task); + } + attributes = self.attributes_rx.recv() => { + let Some(attributes) = attributes else { + error!(target: "engine", "Attributes receiver closed unexpectedly"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + }; + self.finalizer.enqueue_for_finalization(&attributes); + + let task = EngineTask::Consolidate(Box::new(ConsolidateTask::new( + state.client.clone(), + state.rollup.clone(), + attributes, + true, + ))); + state.engine.enqueue(task); + } + msg = self.finalizer.new_finalized_block() => { + if let Err(err) = msg { + error!(target: "engine", ?err, "L1 finalized block receiver closed unexpectedly"); + cancellation.cancel(); + return Err(EngineError::ChannelClosed); + } + // Attempt to finalize any L2 blocks that are contained within the finalized L1 + // chain. + self.finalizer.try_finalize_next(&mut state).await; + } + } + } + } +} diff --git a/kona/crates/node/service/src/actors/engine/api.rs b/kona/crates/node/service/src/actors/engine/api.rs new file mode 100644 index 0000000000000..6aa755d336202 --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/api.rs @@ -0,0 +1,154 @@ +use crate::actors::engine::{BuildRequest, SealRequest, actor::ResetRequest}; +use alloy_rpc_types_engine::PayloadId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_engine::{BuildTaskError, SealTaskError}; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use std::fmt::Debug; +use thiserror::Error; +use tokio::sync::{mpsc, watch}; + +/// Trait to be referenced by those interacting with EngineActor for block building +/// operations. The EngineActor requires the use of channels for communication, but +/// this interface allows that to be abstracted from callers and allows easy testing. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait BlockBuildingClient: Debug + Send + Sync { + /// Resets the engine's forkchoice, awaiting confirmation that it succeeded or returning the + /// error in performing the reset. + async fn reset_engine_forkchoice(&self) -> BlockEngineResult<()>; + + /// Starts building a block with the provided attributes. + /// + /// Returns a `PayloadId` that can be used to seal the block later. + async fn start_build_block( + &self, + attributes: OpAttributesWithParent, + ) -> BlockEngineResult; + + /// Seals and canonicalizes a previously started block. + /// + /// Takes a `PayloadId` from a previous `start_build_block` call and returns + /// the finalized execution payload envelope. + async fn seal_and_canonicalize_block( + &self, + payload_id: PayloadId, + attributes: OpAttributesWithParent, + ) -> BlockEngineResult; + + /// Returns the current unsafe head [`L2BlockInfo`]. + async fn get_unsafe_head(&self) -> BlockEngineResult; +} + +/// Queue-based implementation of the [`BlockBuildingClient`] trait. This handles all channel-based +/// operations, providing a nice facade for callers. +#[derive(Constructor, Debug)] +pub struct QueuedBlockBuildingClient { + /// A channel to use to send build requests to the engine. + /// Upon successful processing of the provided attributes, a `PayloadId` will be sent via the + /// provided sender. + pub build_request_tx: mpsc::Sender, + /// A channel to send seal requests to the engine. + /// If provided, the success/fail result of the sealing operation will be sent via the provided + /// sender. + pub seal_request_tx: mpsc::Sender, + /// A channel to send reset requests to the engine. + /// If provided, the success/fail result of the reset operation will be sent via the provided + /// sender. + pub reset_request_tx: mpsc::Sender, + /// A channel to receive the latest unsafe head [`L2BlockInfo`]. + pub unsafe_head_rx: watch::Receiver, +} + +#[async_trait] +impl BlockBuildingClient for QueuedBlockBuildingClient { + async fn get_unsafe_head(&self) -> BlockEngineResult { + Ok(*self.unsafe_head_rx.borrow()) + } + + async fn reset_engine_forkchoice(&self) -> BlockEngineResult<()> { + let (result_tx, mut result_rx) = mpsc::channel(1); + + self.reset_request_tx + .send(ResetRequest { result_tx: Some(result_tx) }) + .await + .map_err(|_| BlockEngineError::RequestError("request channel closed.".to_string()))?; + + result_rx.recv().await.ok_or_else(|| { + error!(target: "block_engine", "Failed to receive built payload"); + BlockEngineError::ResponseError("response channel closed.".to_string()) + })? + } + + async fn start_build_block( + &self, + attributes: OpAttributesWithParent, + ) -> BlockEngineResult { + let (payload_id_tx, mut payload_id_rx) = mpsc::channel(1); + + if self + .build_request_tx + .send(BuildRequest { attributes, result_tx: payload_id_tx }) + .await + .is_err() + { + return Err(BlockEngineError::RequestError("request channel closed.".to_string())); + } + + payload_id_rx.recv().await.ok_or_else(|| { + error!(target: "block_engine", "Failed to receive payload for initiated block build"); + BlockEngineError::ResponseError("response channel closed.".to_string()) + }) + } + + async fn seal_and_canonicalize_block( + &self, + payload_id: PayloadId, + attributes: OpAttributesWithParent, + ) -> BlockEngineResult { + let (result_tx, mut result_rx) = mpsc::channel(1); + + self.seal_request_tx + .send(SealRequest { payload_id, attributes, result_tx }) + .await + .map_err(|_| BlockEngineError::RequestError("request channel closed.".to_string()))?; + + match result_rx.recv().await { + Some(Ok(payload)) => Ok(payload), + Some(Err(err)) => Err(BlockEngineError::SealError(err)), + None => { + error!(target: "block_engine", "Failed to receive built payload"); + Err(BlockEngineError::ResponseError("response channel closed.".to_string())) + } + } + } +} + +/// The result of a [`BlockBuildingClient`] call. +pub type BlockEngineResult = Result; + +/// Error making requests to the BlockEngine. +#[derive(Debug, Error)] +pub enum BlockEngineError { + /// Error making a request to the engine. The request never made it there. + #[error("Error making a request to the engine: {0}.")] + RequestError(String), + + /// Error receiving response from the engine. + /// This means the request may or may not have succeeded. + #[error("Error receiving response from the engine: {0}..")] + ResponseError(String), + + /// An error occurred starting to build a block. + #[error(transparent)] + StartBuildError(#[from] BuildTaskError), + + /// An error occurred sealing a block. + #[error(transparent)] + SealError(#[from] SealTaskError), + + /// An error occurred performing the reset. + #[error("An error occurred performing the reset: {0}.")] + ResetForkchoiceError(String), +} diff --git a/kona/crates/node/service/src/actors/engine/error.rs b/kona/crates/node/service/src/actors/engine/error.rs new file mode 100644 index 0000000000000..1a8055c35ec30 --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/error.rs @@ -0,0 +1,24 @@ +//! Error type for the [`EngineActor`]. +//! +//! [`EngineActor`]: super::EngineActor + +use kona_engine::{EngineClientBuilderError, EngineResetError, EngineTaskErrors}; + +/// An error from the [`EngineActor`]. +/// +/// [`EngineActor`]: super::EngineActor +#[derive(thiserror::Error, Debug)] +pub enum EngineError { + /// Closed channel error. + #[error("a channel has been closed unexpectedly")] + ChannelClosed, + /// Engine reset error. + #[error(transparent)] + EngineReset(#[from] EngineResetError), + /// Engine client builder error. + #[error(transparent)] + EngineClientBuilder(#[from] EngineClientBuilderError), + /// Engine task error. + #[error(transparent)] + EngineTask(#[from] EngineTaskErrors), +} diff --git a/kona/crates/node/service/src/actors/engine/finalizer.rs b/kona/crates/node/service/src/actors/engine/finalizer.rs new file mode 100644 index 0000000000000..16df663b90a44 --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/finalizer.rs @@ -0,0 +1,88 @@ +//! The [`L2Finalizer`]. + +use kona_engine::{EngineClient, EngineTask, FinalizeTask}; +use kona_protocol::{BlockInfo, OpAttributesWithParent}; +use std::collections::BTreeMap; +use tokio::sync::watch; + +use crate::actors::engine::actor::EngineActorState; + +/// An internal type alias for L1 block numbers. +type L1BlockNumber = u64; + +/// An internal type alias for L2 block numbers. +type L2BlockNumber = u64; + +/// The [`L2Finalizer`] is responsible for finalizing L2 blocks derived from finalized L1 blocks. +/// It maintains a queue of derived L2 blocks that are awaiting finalization, and finalizes them +/// as new finalized L1 blocks are received. +#[derive(Debug)] +pub struct L2Finalizer { + /// A channel that receives new finalized L1 blocks intermittently. + finalized_l1_block_rx: watch::Receiver>, + /// A map of `L1 block number -> highest derived L2 block number` within the L1 epoch, used to + /// track derived [`OpAttributesWithParent`] awaiting finalization. When a new finalized L1 + /// block is received, the highest L2 block whose inputs are contained within the finalized + /// L1 chain is finalized. + awaiting_finalization: BTreeMap, +} + +impl L2Finalizer { + /// Creates a new [`L2Finalizer`] with the given channel receiver for finalized L1 blocks. + pub const fn new(finalized_l1_block_rx: watch::Receiver>) -> Self { + Self { finalized_l1_block_rx, awaiting_finalization: BTreeMap::new() } + } + + /// Enqueues a derived [`OpAttributesWithParent`] for finalization. When a new finalized L1 + /// block is observed that is `>=` the height of [`OpAttributesWithParent::derived_from`], the + /// L2 block associated with the payload attributes will be finalized. + pub fn enqueue_for_finalization(&mut self, attributes: &OpAttributesWithParent) { + self.awaiting_finalization + .entry( + attributes.derived_from.map(|b| b.number).expect( + "Fatal: Cannot enqueue attributes for finalization that weren't derived", + ), + ) + .and_modify(|n| *n = (*n).max(attributes.block_number())) + .or_insert(attributes.block_number()); + } + + /// Clears the finalization queue. + pub fn clear(&mut self) { + self.awaiting_finalization.clear(); + } + + /// Receives a new finalized L1 block from the channel. + pub async fn new_finalized_block(&mut self) -> Result<(), watch::error::RecvError> { + self.finalized_l1_block_rx.changed().await + } + + /// Attempts to finalize any L2 blocks that the finalizer knows about and are contained within + /// the new finalized L1 chain. + pub(super) async fn try_finalize_next( + &mut self, + engine_state: &mut EngineActorState, + ) { + // If there is no finalized L1 block available in the watch channel, do nothing. + let Some(new_finalized_l1) = *self.finalized_l1_block_rx.borrow() else { + return; + }; + + // Find the highest safe L2 block that is contained within the finalized chain, + // that the finalizer is aware of. + let highest_safe = self.awaiting_finalization.range(..=new_finalized_l1.number).next_back(); + + // If the highest safe block is found, enqueue a finalization task and drain the + // queue of all L1 blocks not contained in the finalized L1 chain. + if let Some((_, highest_safe_number)) = highest_safe { + let task = EngineTask::Finalize(Box::new(FinalizeTask::new( + engine_state.client.clone(), + engine_state.rollup.clone(), + *highest_safe_number, + ))); + engine_state.engine.enqueue(task); + + self.awaiting_finalization.retain(|&number, _| number > new_finalized_l1.number); + } + } +} diff --git a/kona/crates/node/service/src/actors/engine/mod.rs b/kona/crates/node/service/src/actors/engine/mod.rs new file mode 100644 index 0000000000000..f959e38fa84f0 --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/mod.rs @@ -0,0 +1,24 @@ +//! The [`EngineActor`] and its components. + +mod actor; +pub use actor::{ + BuildRequest, EngineActor, EngineConfig, EngineContext, EngineInboundData, ResetRequest, + SealRequest, +}; + +mod error; +pub use error::EngineError; + +mod api; +pub use api::{ + BlockBuildingClient, BlockEngineError, BlockEngineResult, QueuedBlockBuildingClient, +}; + +mod finalizer; + +pub use finalizer::L2Finalizer; + +mod rollup_boost; + +#[cfg(test)] +pub use api::MockBlockBuildingClient; diff --git a/kona/crates/node/service/src/actors/engine/rollup_boost.rs b/kona/crates/node/service/src/actors/engine/rollup_boost.rs new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/kona/crates/node/service/src/actors/engine/rollup_boost.rs @@ -0,0 +1 @@ + diff --git a/kona/crates/node/service/src/actors/l1_watcher/actor.rs b/kona/crates/node/service/src/actors/l1_watcher/actor.rs new file mode 100644 index 0000000000000..b9c072c60c560 --- /dev/null +++ b/kona/crates/node/service/src/actors/l1_watcher/actor.rs @@ -0,0 +1,212 @@ +//! [`NodeActor`] implementation for an L1 chain watcher that polls for L1 block updates over HTTP +//! RPC. + +use crate::{ + NodeActor, + actors::{CancellableContext, l1_watcher::error::L1WatcherActorError}, +}; +use alloy_eips::BlockId; +use alloy_primitives::Address; +use alloy_provider::Provider; +use async_trait::async_trait; +use futures::{Stream, StreamExt}; +use kona_genesis::{RollupConfig, SystemConfigLog, SystemConfigUpdate, UnsafeBlockSignerUpdate}; +use kona_protocol::BlockInfo; +use kona_rpc::{L1State, L1WatcherQueries}; +use std::sync::Arc; +use tokio::{ + select, + sync::{ + mpsc::{self}, + watch, + }, +}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; + +/// An L1 chain watcher that checks for L1 block updates over RPC. +#[derive(Debug)] +pub struct L1WatcherActor +where + BS: Stream + Unpin + Send, + L1P: Provider, +{ + /// The [`RollupConfig`] to tell if ecotone is active. + /// This is used to determine if the L1 watcher should check for unsafe block signer updates. + rollup_config: Arc, + /// The L1 provider. + l1_provider: L1P, + /// The inbound queries to the L1 watcher. + inbound_queries: mpsc::Receiver, + /// The latest L1 head block. + latest_head: watch::Sender>, + /// The latest L1 finalized block. + latest_finalized: watch::Sender>, + /// The block signer sender. + block_signer_sender: mpsc::Sender

, + /// The cancellation token, shared between all tasks. + cancellation: CancellationToken, + /// A stream over the latest head. + head_stream: BS, + /// A stream over the finalized block accepted as canonical. + finalized_stream: BS, +} +impl L1WatcherActor +where + BS: Stream + Unpin + Send, + L1P: Provider, +{ + /// Instantiate a new [`L1WatcherActor`]. + #[allow(clippy::too_many_arguments)] + pub const fn new( + rollup_config: Arc, + l1_provider: L1P, + l1_query_rx: mpsc::Receiver, + l1_head_updates_tx: watch::Sender>, + finalized_l1_block_tx: watch::Sender>, + signer: mpsc::Sender
, + cancellation: CancellationToken, + head_stream: BS, + finalized_stream: BS, + ) -> Self { + Self { + rollup_config, + l1_provider, + inbound_queries: l1_query_rx, + latest_head: l1_head_updates_tx, + latest_finalized: finalized_l1_block_tx, + block_signer_sender: signer, + cancellation, + head_stream, + finalized_stream, + } + } +} + +#[async_trait] +impl NodeActor for L1WatcherActor +where + BS: Stream + Unpin + Send + 'static, + L1P: Provider + 'static, +{ + type Error = L1WatcherActorError; + type StartData = (); + + /// Start the main processing loop. + async fn start(mut self, _: Self::StartData) -> Result<(), Self::Error> { + let cancel = self.cancellation.clone(); + let latest_head = self.latest_head.subscribe(); + + loop { + select! { + _ = cancel.cancelled() => { + // Exit the task on cancellation. + info!( + target: "l1_watcher", + "Received shutdown signal. Exiting L1 watcher task." + ); + + return Ok(()); + }, + new_head = self.head_stream.next() => match new_head { + None => { + return Err(L1WatcherActorError::StreamEnded); + } + Some(head_block_info) => { + // Send the head update event to all consumers. + self.latest_head.send_replace(Some(head_block_info)); + + // For each log, attempt to construct a [`SystemConfigLog`]. + // Build the [`SystemConfigUpdate`] from the log. + // If the update is an Unsafe block signer update, send the address + // to the block signer sender. + let filter_address = self.rollup_config.l1_system_config_address; + let logs = self.l1_provider .get_logs(&alloy_rpc_types_eth::Filter::new().address(filter_address).select(head_block_info.hash)).await?; + let ecotone_active = self.rollup_config.is_ecotone_active(head_block_info.timestamp); + for log in logs { + let sys_cfg_log = SystemConfigLog::new(log.into(), ecotone_active); + if let Ok(SystemConfigUpdate::UnsafeBlockSigner(UnsafeBlockSignerUpdate { unsafe_block_signer })) = sys_cfg_log.build() { + info!( + target: "l1_watcher", + "Unsafe block signer update: {unsafe_block_signer}" + ); + if let Err(e) = self.block_signer_sender.send(unsafe_block_signer).await { + error!( + target: "l1_watcher", + "Error sending unsafe block signer update: {e}" + ); + } + } + } + }, + }, + new_finalized = self.finalized_stream.next() => match new_finalized { + None => { + return Err(L1WatcherActorError::StreamEnded); + } + Some(finalized_block_info) => { + self.latest_finalized.send_replace(Some(finalized_block_info)); + } + }, + inbound_query = self.inbound_queries.recv() => match inbound_query { + Some(query) => { + match query { + L1WatcherQueries::Config(sender) => { + if let Err(e) = sender.send((*self.rollup_config).clone()) { + warn!(target: "l1_watcher", error = ?e, "Failed to send L1 config to the query sender"); + } + } + L1WatcherQueries::L1State(sender) => { + let current_l1 = *latest_head.borrow(); + + let head_l1 = match self.l1_provider.get_block(BlockId::latest()).await { + Ok(block) => block, + Err(e) => { + warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest head block"); + None + }}.map(|block| block.into_consensus().into()); + + let finalized_l1 = match self.l1_provider.get_block(BlockId::finalized()).await { + Ok(block) => block, + Err(e) => { + warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest finalized block"); + None + }}.map(|block| block.into_consensus().into()); + + let safe_l1 = match self.l1_provider.get_block(BlockId::safe()).await { + Ok(block) => block, + Err(e) => { + warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest safe block"); + None + }}.map(|block| block.into_consensus().into()); + + if let Err(e) = sender.send(L1State { + current_l1, + current_l1_finalized: finalized_l1, + head_l1, + safe_l1, + finalized_l1, + }) { + warn!(target: "l1_watcher", error = ?e, "Failed to send L1 state to the query sender"); + } + } + } + }, + None => { + error!(target: "l1_watcher", "L1 watcher query channel closed unexpectedly, exiting query processor task."); + return Err(L1WatcherActorError::StreamEnded) + } + } + } + } + } +} + +impl CancellableContext for L1WatcherActor +where + BS: Stream + Unpin + Send + 'static, + L1P: Provider, +{ + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} diff --git a/kona/crates/node/service/src/actors/l1_watcher/blockstream.rs b/kona/crates/node/service/src/actors/l1_watcher/blockstream.rs new file mode 100644 index 0000000000000..7ecc01bc62305 --- /dev/null +++ b/kona/crates/node/service/src/actors/l1_watcher/blockstream.rs @@ -0,0 +1,68 @@ +use std::time::Duration; + +use alloy_eips::BlockNumberOrTag; +use alloy_provider::Provider; +use alloy_rpc_client::PollerBuilder; +use alloy_rpc_types_eth::Block; +use async_stream::stream; +use futures::{Stream, StreamExt}; +use kona_protocol::BlockInfo; + +/// A wrapper around a [`PollerBuilder`] that observes [`BlockInfo`] updates on a [`Provider`]. +/// +/// Note that this stream is not guaranteed to be contiguous. It may miss certain blocks, and +/// yielded items should only be considered to be the latest block matching the given +/// [`BlockNumberOrTag`]. +#[derive(Debug, Clone)] +pub struct BlockStream +where + L1P: Provider, +{ + /// The inner [`Provider`]. + l1_provider: L1P, + /// The block tag to poll for. + tag: BlockNumberOrTag, + /// The poll interval (in seconds). + poll_interval: Duration, +} + +impl BlockStream { + /// Creates a new [`Stream`] instance. + /// + /// # Returns + /// Returns error if the passed [`BlockNumberOrTag`] is of the [`BlockNumberOrTag::Number`] + /// variant. + pub fn new_as_stream( + l1_provider: L1P, + tag: BlockNumberOrTag, + poll_interval: Duration, + ) -> Result + Unpin + Send, String> { + if matches!(tag, BlockNumberOrTag::Number(_)) { + error!("Invalid BlockNumberOrTag variant - Must be a tag"); + } + Ok(Self { l1_provider, tag, poll_interval }.into_stream()) + } + + /// Creates a [`Stream`] of [`BlockInfo`]. + pub fn into_stream(self) -> impl Stream + Unpin + Send { + let mut poll_stream = PollerBuilder::<(BlockNumberOrTag, bool), Block>::new( + self.l1_provider.weak_client(), + "eth_getBlockByNumber", + (self.tag, false), + ) + .with_poll_interval(self.poll_interval) + .into_stream(); + + Box::pin(stream! { + let mut last_block = None; + while let Some(next) = poll_stream.next().await { + let info: BlockInfo = next.into_consensus().into(); + + if last_block.map(|b| b != info).unwrap_or(true) { + last_block = Some(info); + yield info; + } + } + }) + } +} diff --git a/kona/crates/node/service/src/actors/l1_watcher/error.rs b/kona/crates/node/service/src/actors/l1_watcher/error.rs new file mode 100644 index 0000000000000..9add016e2e552 --- /dev/null +++ b/kona/crates/node/service/src/actors/l1_watcher/error.rs @@ -0,0 +1,22 @@ +use std::sync::mpsc::SendError; + +use alloy_eips::BlockId; +use alloy_transport::TransportError; +use thiserror::Error; + +/// The error type for the `L1WatcherActor`. +#[derive(Error, Debug)] +pub enum L1WatcherActorError { + /// Error sending the head update event. + #[error("Error sending the head update event: {0}")] + SendError(#[from] SendError), + /// Error in the transport layer. + #[error("Transport error: {0}")] + Transport(#[from] TransportError), + /// The L1 block was not found. + #[error("L1 block not found: {0}")] + L1BlockNotFound(BlockId), + /// Stream ended unexpectedly. + #[error("Stream ended unexpectedly")] + StreamEnded, +} diff --git a/kona/crates/node/service/src/actors/l1_watcher/mod.rs b/kona/crates/node/service/src/actors/l1_watcher/mod.rs new file mode 100644 index 0000000000000..0d2b76fb82842 --- /dev/null +++ b/kona/crates/node/service/src/actors/l1_watcher/mod.rs @@ -0,0 +1,8 @@ +mod actor; +pub use actor::L1WatcherActor; + +mod blockstream; +pub use blockstream::BlockStream; + +mod error; +pub use error::L1WatcherActorError; diff --git a/kona/crates/node/service/src/actors/mod.rs b/kona/crates/node/service/src/actors/mod.rs new file mode 100644 index 0000000000000..fd5083d4a5ec9 --- /dev/null +++ b/kona/crates/node/service/src/actors/mod.rs @@ -0,0 +1,46 @@ +//! [NodeActor] services for the node. +//! +//! [NodeActor]: super::NodeActor + +mod traits; +pub use traits::{CancellableContext, NodeActor}; + +mod engine; +pub use engine::{ + BlockBuildingClient, BlockEngineError, BlockEngineResult, BuildRequest, EngineActor, + EngineConfig, EngineContext, EngineError, EngineInboundData, L2Finalizer, + QueuedBlockBuildingClient, ResetRequest, SealRequest, +}; + +mod rpc; +pub use rpc::{RpcActor, RpcActorError, RpcContext}; + +mod derivation; +pub use derivation::{ + DerivationActor, DerivationBuilder, DerivationContext, DerivationError, + DerivationInboundChannels, DerivationState, InboundDerivationMessage, PipelineBuilder, +}; + +mod l1_watcher; +pub use l1_watcher::{BlockStream, L1WatcherActor, L1WatcherActorError}; + +mod network; +pub use network::{ + NetworkActor, NetworkActorError, NetworkBuilder, NetworkBuilderError, NetworkConfig, + NetworkContext, NetworkDriver, NetworkDriverError, NetworkHandler, NetworkInboundData, + QueuedUnsafePayloadGossipClient, UnsafePayloadGossipClient, UnsafePayloadGossipClientError, +}; + +mod sequencer; +pub use sequencer::{ + Conductor, ConductorClient, ConductorError, DelayedL1OriginSelectorProvider, L1OriginSelector, + L1OriginSelectorError, L1OriginSelectorProvider, OriginSelector, QueuedSequencerAdminAPIClient, + SequencerActor, SequencerActorError, SequencerAdminQuery, SequencerConfig, +}; + +#[cfg(test)] +pub use engine::MockBlockBuildingClient; +#[cfg(test)] +pub use network::MockUnsafePayloadGossipClient; +#[cfg(test)] +pub use sequencer::{MockConductor, MockOriginSelector}; diff --git a/kona/crates/node/service/src/actors/network/README.md b/kona/crates/node/service/src/actors/network/README.md new file mode 100644 index 0000000000000..f22a99f80fb52 --- /dev/null +++ b/kona/crates/node/service/src/actors/network/README.md @@ -0,0 +1,54 @@ +# Network actor + +The network actor is responsible for handling interactions with the p2p layer of the kona-node, specifically the libp2p gossip driver and the discv5 handler. + +### Example + +> **Warning** +> +> Notice, the socket address uses `0.0.0.0`. +> If you are experiencing issues connecting to peers for discovery, +> check to make sure you are not using the loopback address, +> `127.0.0.1` aka "localhost", which can prevent outward facing connections. + +```rust,no_run +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use alloy_primitives::address; +use tokio_util::sync::CancellationToken; +use kona_genesis::RollupConfig; +use kona_p2p::{LocalNode, Config}; +use kona_node_service::{NetworkActor}; +use libp2p::Multiaddr; +use discv5::enr::CombinedKey; + +#[tokio::main] +async fn main() { + // Construct the Network + let signer = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + let gossip = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099); + let mut gossip_addr = Multiaddr::from(gossip.ip()); + gossip_addr.push(libp2p::multiaddr::Protocol::Tcp(gossip.port())); + let advertise_ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED); + + let CombinedKey::Secp256k1(k256_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + let disc = LocalNode::new(k256_key, advertise_ip, 9097, 9098); + + // The unsafe blocks are sent by the network actor to `blocks_rx`. This channel receiver can be + // used by external actors/modules to handle incoming unsafe blocks. + let (blocks, blocks_rx) = tokio::sync::mpsc::channel(1024); + + let (inbound_data, network) = NetworkActor::new(NetworkActor::builder(Config::new( + RollupConfig::default(), + disc, + gossip_addr, + signer + ))); + + // This will start the p2p stack of the kona-node (ie the libp2p gossip and discovery layers) + network.start(NetworkContext { blocks, cancellation: CancellationToken::new() }).await?; +} +``` + +[!WARNING]: ###example \ No newline at end of file diff --git a/kona/crates/node/service/src/actors/network/actor.rs b/kona/crates/node/service/src/actors/network/actor.rs new file mode 100644 index 0000000000000..b3f1b8ca98a48 --- /dev/null +++ b/kona/crates/node/service/src/actors/network/actor.rs @@ -0,0 +1,332 @@ +use alloy_primitives::Address; +use async_trait::async_trait; +use kona_gossip::P2pRpcRequest; +use kona_rpc::NetworkAdminQuery; +use kona_sources::BlockSignerError; +use libp2p::TransportError; +use op_alloy_rpc_types_engine::{OpExecutionPayloadEnvelope, OpNetworkPayloadEnvelope}; +use thiserror::Error; +use tokio::{self, select, sync::mpsc}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; + +use crate::{ + CancellableContext, NodeActor, + actors::network::{ + builder::NetworkBuilder, driver::NetworkDriverError, error::NetworkBuilderError, + }, +}; + +/// The network actor handles two core networking components of the rollup node: +/// - *discovery*: Peer discovery over UDP using discv5. +/// - *gossip*: Block gossip over TCP using libp2p. +/// +/// The network actor itself is a light wrapper around the [`NetworkBuilder`]. +/// +/// ## Example +/// +/// ```rust,ignore +/// use kona_gossip::NetworkDriver; +/// use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +/// +/// let chain_id = 10; +/// let signer = Address::random(); +/// let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099); +/// +/// // Construct the `Network` using the builder. +/// // let mut driver = Network::builder() +/// // .with_unsafe_block_signer(signer) +/// // .with_chain_id(chain_id) +/// // .with_gossip_addr(socket) +/// // .build() +/// // .unwrap(); +/// +/// // Construct the `NetworkActor` with the [`Network`]. +/// // let actor = NetworkActor::new(driver); +/// ``` +#[derive(Debug)] +pub struct NetworkActor { + /// Network driver + pub(super) builder: NetworkBuilder, + /// A channel to receive the unsafe block signer address. + pub(super) signer: mpsc::Receiver
, + /// Handler for p2p RPC Requests. + pub(super) p2p_rpc: mpsc::Receiver, + /// A channel to receive admin rpc requests. + pub(super) admin_rpc: mpsc::Receiver, + /// A channel to receive unsafe blocks and send them through the gossip layer. + pub(super) publish_rx: mpsc::Receiver, +} + +/// The inbound data for the network actor. +#[derive(Debug)] +pub struct NetworkInboundData { + /// A channel to send the unsafe block signer address to the network actor. + pub signer: mpsc::Sender
, + /// Handler for p2p RPC Requests sent to the network actor. + pub p2p_rpc: mpsc::Sender, + /// Handler for admin RPC Requests. + pub admin_rpc: mpsc::Sender, + /// A channel to send unsafe blocks to the network actor. + /// This channel should only be used by the sequencer actor/admin RPC api to forward their + /// newly produced unsafe blocks to the network actor. + pub gossip_payload_tx: mpsc::Sender, +} + +impl NetworkActor { + /// Constructs a new [`NetworkActor`] given the [`NetworkBuilder`] + pub fn new(driver: NetworkBuilder) -> (NetworkInboundData, Self) { + let (signer_tx, signer_rx) = mpsc::channel(16); + let (rpc_tx, rpc_rx) = mpsc::channel(1024); + let (admin_rpc_tx, admin_rpc_rx) = mpsc::channel(1024); + let (publish_tx, publish_rx) = tokio::sync::mpsc::channel(256); + let actor = Self { + builder: driver, + signer: signer_rx, + p2p_rpc: rpc_rx, + admin_rpc: admin_rpc_rx, + publish_rx, + }; + let outbound_data = NetworkInboundData { + signer: signer_tx, + p2p_rpc: rpc_tx, + admin_rpc: admin_rpc_tx, + gossip_payload_tx: publish_tx, + }; + (outbound_data, actor) + } +} + +/// The communication context used by the network actor. +#[derive(Debug)] +pub struct NetworkContext { + /// The channel used by the sequencer actor for sending unsafe blocks to the network. + pub blocks: mpsc::Sender, + /// Cancels the network actor. + pub cancellation: CancellationToken, +} + +impl CancellableContext for NetworkContext { + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} + +/// An error from the network actor. +#[derive(Debug, Error)] +pub enum NetworkActorError { + /// Network builder error. + #[error(transparent)] + NetworkBuilder(#[from] NetworkBuilderError), + /// Network driver error. + #[error(transparent)] + NetworkDriver(#[from] NetworkDriverError), + /// Driver startup failed. + #[error(transparent)] + DriverStartup(#[from] TransportError), + /// The network driver was missing its unsafe block receiver. + #[error("Missing unsafe block receiver in network driver")] + MissingUnsafeBlockReceiver, + /// The network driver was missing its unsafe block signer sender. + #[error("Missing unsafe block signer in network driver")] + MissingUnsafeBlockSigner, + /// Channel closed unexpectedly. + #[error("Channel closed unexpectedly")] + ChannelClosed, + /// Failed to sign the payload. + #[error("Failed to sign the payload: {0}")] + FailedToSignPayload(#[from] BlockSignerError), +} + +#[async_trait] +impl NodeActor for NetworkActor { + type Error = NetworkActorError; + type StartData = NetworkContext; + + async fn start( + mut self, + NetworkContext { blocks, cancellation }: Self::StartData, + ) -> Result<(), Self::Error> { + let mut handler = self.builder.build()?.start().await?; + + // New unsafe block channel. + let (unsafe_block_tx, mut unsafe_block_rx) = tokio::sync::mpsc::unbounded_channel(); + + loop { + select! { + _ = cancellation.cancelled() => { + info!( + target: "network", + "Received shutdown signal. Exiting network task." + ); + return Ok(()); + } + block = unsafe_block_rx.recv() => { + let Some(block) = block else { + error!(target: "node::p2p", "The unsafe block receiver channel has closed"); + return Err(NetworkActorError::ChannelClosed); + }; + + if blocks.send(block).await.is_err() { + warn!(target: "network", "Failed to forward unsafe block"); + return Err(NetworkActorError::ChannelClosed); + } + } + signer = self.signer.recv() => { + let Some(signer) = signer else { + warn!( + target: "network", + "Found no unsafe block signer on receive" + ); + return Err(NetworkActorError::ChannelClosed); + }; + if handler.unsafe_block_signer_sender.send(signer).is_err() { + warn!( + target: "network", + "Failed to send unsafe block signer to network handler", + ); + } + } + Some(block) = self.publish_rx.recv(), if !self.publish_rx.is_closed() => { + let timestamp = block.execution_payload.timestamp(); + let selector = |handler: &kona_gossip::BlockHandler| { + handler.topic(timestamp) + }; + let Some(signer) = handler.signer.as_ref() else { + warn!(target: "net", "No local signer available to sign the payload"); + continue; + }; + + let chain_id = handler.discovery.chain_id; + + let sender_address = *handler.unsafe_block_signer_sender.borrow(); + + let payload_hash = block.payload_hash(); + let signature = signer.sign_block(payload_hash, chain_id, sender_address).await?; + + let payload = OpNetworkPayloadEnvelope { + payload: block.execution_payload, + parent_beacon_block_root: block.parent_beacon_block_root, + signature, + payload_hash, + }; + + match handler.gossip.publish(selector, Some(payload)) { + Ok(id) => info!("Published unsafe payload | {:?}", id), + Err(e) => warn!("Failed to publish unsafe payload: {:?}", e), + } + } + event = handler.gossip.next() => { + let Some(event) = event else { + error!(target: "node::p2p", "The gossip swarm stream has ended"); + return Err(NetworkActorError::ChannelClosed); + }; + + if let Some(payload) = handler.gossip.handle_event(event) { + if unsafe_block_tx.send(payload.into()).is_err() { + warn!(target: "node::p2p", "Failed to send unsafe block to network handler"); + } + } + }, + enr = handler.enr_receiver.recv() => { + let Some(enr) = enr else { + error!(target: "node::p2p", "The enr receiver channel has closed"); + return Err(NetworkActorError::ChannelClosed); + }; + handler.gossip.dial(enr); + }, + _ = handler.peer_score_inspector.tick(), if handler.gossip.peer_monitoring.as_ref().is_some() => { + handler.handle_peer_monitoring().await; + }, + Some(NetworkAdminQuery::PostUnsafePayload { payload }) = self.admin_rpc.recv(), if !self.admin_rpc.is_closed() => { + debug!(target: "node::p2p", "Broadcasting unsafe payload from admin api"); + if unsafe_block_tx.send(payload).is_err() { + warn!(target: "node::p2p", "Failed to send unsafe block to network handler"); + } + }, + Some(req) = self.p2p_rpc.recv(), if !self.p2p_rpc.is_closed() => { + req.handle(&mut handler.gossip, &handler.discovery); + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV3}; + use alloy_signer::SignerSync; + use alloy_signer_local::PrivateKeySigner; + use arbitrary::Arbitrary; + use op_alloy_rpc_types_engine::OpExecutionPayload; + use rand::Rng; + + #[test] + fn test_payload_signature_roundtrip_v1() { + let mut bytes = [0u8; 4096]; + rand::rng().fill(bytes.as_mut_slice()); + + let pubkey = PrivateKeySigner::random(); + let expected_address = pubkey.address(); + const CHAIN_ID: u64 = 1337; + + let block = OpExecutionPayloadEnvelope { + execution_payload: OpExecutionPayload::V1( + ExecutionPayloadV1::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(), + ), + parent_beacon_block_root: None, + }; + + let payload_hash = block.payload_hash(); + let signature = pubkey.sign_hash_sync(&payload_hash.signature_message(CHAIN_ID)).unwrap(); + let payload = OpNetworkPayloadEnvelope { + payload: block.execution_payload, + parent_beacon_block_root: block.parent_beacon_block_root, + signature, + payload_hash, + }; + let encoded_payload = payload.encode_v1().unwrap(); + + let decoded_payload = OpNetworkPayloadEnvelope::decode_v1(&encoded_payload).unwrap(); + + let msg = decoded_payload.payload_hash.signature_message(CHAIN_ID); + let msg_signer = decoded_payload.signature.recover_address_from_prehash(&msg).unwrap(); + + assert_eq!(expected_address, msg_signer); + } + + #[test] + fn test_payload_signature_roundtrip_v3() { + let mut bytes = [0u8; 4096]; + rand::rng().fill(bytes.as_mut_slice()); + + let pubkey = PrivateKeySigner::random(); + let expected_address = pubkey.address(); + const CHAIN_ID: u64 = 1337; + + let block = OpExecutionPayloadEnvelope { + execution_payload: OpExecutionPayload::V3( + ExecutionPayloadV3::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(), + ), + parent_beacon_block_root: Some(B256::random()), + }; + + let payload_hash = block.payload_hash(); + let signature = pubkey.sign_hash_sync(&payload_hash.signature_message(CHAIN_ID)).unwrap(); + let payload = OpNetworkPayloadEnvelope { + payload: block.execution_payload, + parent_beacon_block_root: block.parent_beacon_block_root, + signature, + payload_hash, + }; + let encoded_payload = payload.encode_v3().unwrap(); + + let decoded_payload = OpNetworkPayloadEnvelope::decode_v3(&encoded_payload).unwrap(); + + let msg = decoded_payload.payload_hash.signature_message(CHAIN_ID); + let msg_signer = decoded_payload.signature.recover_address_from_prehash(&msg).unwrap(); + + assert_eq!(expected_address, msg_signer); + } +} diff --git a/kona/crates/node/service/src/actors/network/builder.rs b/kona/crates/node/service/src/actors/network/builder.rs new file mode 100644 index 0000000000000..1c6a6eb3161b4 --- /dev/null +++ b/kona/crates/node/service/src/actors/network/builder.rs @@ -0,0 +1,292 @@ +//! Network Builder Module. + +use alloy_primitives::Address; +use discv5::Config as Discv5Config; +use kona_disc::{Discv5Builder, LocalNode}; +use kona_genesis::RollupConfig; +use kona_gossip::{GaterConfig, GossipDriverBuilder}; +use kona_peers::{BootNodes, BootStoreFile, PeerMonitoring, PeerScoreLevel}; +use kona_sources::BlockSigner; +use libp2p::{Multiaddr, identity::Keypair}; +use std::time::Duration; + +use crate::{ + NetworkBuilderError, + actors::network::{NetworkConfig, NetworkDriver}, +}; + +/// Constructs a [`NetworkDriver`] for the OP Stack Consensus Layer. +#[derive(Debug)] +pub struct NetworkBuilder { + /// The discovery driver. + pub(super) discovery: Discv5Builder, + /// The gossip driver. + pub(super) gossip: GossipDriverBuilder, + /// A signer for payloads. + pub(super) signer: Option, + /// Whether to update the ENR socket after the libp2p Swarm is started. + /// This is set to true by default. + /// This may be set to false if the node is configured to use a static advertised address (when + /// used with a nat for example). + pub(super) enr_update: bool, +} + +impl From for NetworkBuilder { + fn from(config: NetworkConfig) -> Self { + Self::new( + config.rollup_config, + config.unsafe_block_signer, + config.gossip_address, + config.keypair, + config.discovery_address, + config.discovery_config, + config.gossip_signer, + ) + .with_enr_update(config.enr_update) + .with_discovery_randomize(config.discovery_randomize) + .with_bootstore(config.bootstore) + .with_bootnodes(config.bootnodes) + .with_discovery_interval(config.discovery_interval) + .with_gossip_config(config.gossip_config) + .with_peer_scoring(config.scoring) + .with_peer_monitoring(config.monitor_peers) + .with_topic_scoring(config.topic_scoring) + .with_gater_config(config.gater_config) + } +} + +impl NetworkBuilder { + /// Creates a new [`NetworkBuilder`]. + pub fn new( + rollup_config: RollupConfig, + unsafe_block_signer: Address, + gossip_addr: Multiaddr, + keypair: Keypair, + discovery_address: LocalNode, + discovery_config: discv5::Config, + signer: Option, + ) -> Self { + Self { + discovery: Discv5Builder::new( + discovery_address, + rollup_config.l2_chain_id.id(), + discovery_config, + ), + gossip: GossipDriverBuilder::new( + rollup_config, + unsafe_block_signer, + gossip_addr, + keypair, + ), + signer, + enr_update: true, + } + } + + /// Sets the ENR update flag for the [`NetworkBuilder`]. + pub fn with_enr_update(self, enr_update: bool) -> Self { + Self { enr_update, ..self } + } + + /// Sets the configuration for the connection gater. + pub fn with_gater_config(self, config: GaterConfig) -> Self { + Self { gossip: self.gossip.with_gater_config(config), ..self } + } + + /// Sets the signer for the [`NetworkBuilder`]. + pub fn with_signer(self, signer: Option) -> Self { + Self { signer, ..self } + } + + /// Sets the bootstore path for the [`Discv5Builder`]. + pub fn with_bootstore(self, bootstore: Option) -> Self { + Self { discovery: self.discovery.with_bootstore_file(bootstore), ..self } + } + + /// Sets the interval at which to randomize discovery peers. + pub fn with_discovery_randomize(self, randomize: Option) -> Self { + Self { discovery: self.discovery.with_discovery_randomize(randomize), ..self } + } + + /// Sets the initial bootnodes to add to the bootstore. + pub fn with_bootnodes(self, bootnodes: BootNodes) -> Self { + Self { discovery: self.discovery.with_bootnodes(bootnodes), ..self } + } + + /// Sets the peer scoring based on the given [`PeerScoreLevel`]. + pub fn with_peer_scoring(self, level: PeerScoreLevel) -> Self { + Self { gossip: self.gossip.with_peer_scoring(level), ..self } + } + + /// Sets topic scoring for the [`GossipDriverBuilder`]. + pub fn with_topic_scoring(self, topic_scoring: bool) -> Self { + Self { gossip: self.gossip.with_topic_scoring(topic_scoring), ..self } + } + + /// Sets the peer monitoring for the [`GossipDriverBuilder`]. + pub fn with_peer_monitoring(self, peer_monitoring: Option) -> Self { + Self { gossip: self.gossip.with_peer_monitoring(peer_monitoring), ..self } + } + + /// Sets the discovery interval for the [`Discv5Builder`]. + pub fn with_discovery_interval(self, interval: tokio::time::Duration) -> Self { + Self { discovery: self.discovery.with_interval(interval), ..self } + } + + /// Sets the address for the [`Discv5Builder`]. + pub fn with_discovery_address(self, address: LocalNode) -> Self { + Self { discovery: self.discovery.with_local_node(address), ..self } + } + + /// Sets the gossipsub config for the [`GossipDriverBuilder`]. + pub fn with_gossip_config(self, config: libp2p::gossipsub::Config) -> Self { + Self { gossip: self.gossip.with_config(config), ..self } + } + + /// Sets the [`Discv5Config`] for the [`Discv5Builder`]. + pub fn with_discovery_config(self, config: Discv5Config) -> Self { + Self { discovery: self.discovery.with_discovery_config(config), ..self } + } + + /// Sets the gossip address for the [`GossipDriverBuilder`]. + pub fn with_gossip_address(self, addr: Multiaddr) -> Self { + Self { gossip: self.gossip.with_address(addr), ..self } + } + + /// Sets the timeout for the [`GossipDriverBuilder`]. + pub fn with_timeout(self, timeout: Duration) -> Self { + Self { gossip: self.gossip.with_timeout(timeout), ..self } + } + + /// Builds the [`NetworkDriver`]. + pub fn build(self) -> Result { + let (gossip, unsafe_block_signer_sender) = self.gossip.build()?; + let discovery = self.discovery.build()?; + + Ok(NetworkDriver { + gossip, + discovery, + unsafe_block_signer_sender, + signer: self.signer, + enr_update: self.enr_update, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_chains::Chain; + use discv5::{ConfigBuilder, ListenConfig, enr::CombinedKey}; + use libp2p::gossipsub::IdentTopic; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + + #[derive(Debug)] + struct NetworkBuilderParams { + rollup_config: RollupConfig, + signer: Address, + } + + impl Default for NetworkBuilderParams { + fn default() -> Self { + Self { rollup_config: RollupConfig::default(), signer: Address::random() } + } + } + + fn network_builder(params: NetworkBuilderParams) -> NetworkBuilder { + let keypair = Keypair::generate_secp256k1(); + let signer = params.signer; + let gossip = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099); + let mut gossip_addr = Multiaddr::from(gossip.ip()); + gossip_addr.push(libp2p::multiaddr::Protocol::Tcp(gossip.port())); + + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let discovery_address = + LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9098, 9098); + + let discovery_config = + ConfigBuilder::new(ListenConfig::from_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9098)) + .build(); + + NetworkBuilder::new( + params.rollup_config, + signer, + gossip_addr, + keypair, + discovery_address, + discovery_config, + None, + ) + } + + #[test] + fn test_build_simple_succeeds() { + let signer = Address::random(); + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + let disc_listen = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9097); + let disc_enr = LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9098, 9098); + let gossip = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099); + let mut gossip_addr = Multiaddr::from(gossip.ip()); + gossip_addr.push(libp2p::multiaddr::Protocol::Tcp(gossip.port())); + + let driver = network_builder(NetworkBuilderParams { + rollup_config: RollupConfig { + l2_chain_id: Chain::optimism_mainnet(), + ..Default::default() + }, + signer, + }) + .with_gossip_address(gossip_addr.clone()) + .with_discovery_address(disc_enr) + .with_discovery_config(ConfigBuilder::new(disc_listen.into()).build()) + .build() + .unwrap(); + + // Driver Assertions + let id = 10; + assert_eq!(driver.gossip.addr, gossip_addr); + assert_eq!(driver.discovery.chain_id, id); + assert_eq!(driver.discovery.disc.local_enr().tcp4().unwrap(), 9098); + + // Block Handler Assertions + assert_eq!(driver.gossip.handler.rollup_config.l2_chain_id, id); + let v1 = IdentTopic::new(format!("/optimism/{id}/0/blocks")); + println!("{:?}", driver.gossip.handler.blocks_v1_topic); + assert_eq!(driver.gossip.handler.blocks_v1_topic.hash(), v1.hash()); + let v2 = IdentTopic::new(format!("/optimism/{id}/1/blocks")); + assert_eq!(driver.gossip.handler.blocks_v2_topic.hash(), v2.hash()); + let v3 = IdentTopic::new(format!("/optimism/{id}/2/blocks")); + assert_eq!(driver.gossip.handler.blocks_v3_topic.hash(), v3.hash()); + let v4 = IdentTopic::new(format!("/optimism/{id}/3/blocks")); + assert_eq!(driver.gossip.handler.blocks_v4_topic.hash(), v4.hash()); + } + + #[test] + fn test_build_network_custom_configs() { + let gossip = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9099); + let mut gossip_addr = Multiaddr::from(gossip.ip()); + gossip_addr.push(libp2p::multiaddr::Protocol::Tcp(gossip.port())); + + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let disc = LocalNode::new(secret_key, IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9097, 9097); + let discovery_config = + ConfigBuilder::new(ListenConfig::from_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9098)) + .build(); + let driver = network_builder(Default::default()) + .with_gossip_address(gossip_addr) + .with_discovery_address(disc) + .with_discovery_config(discovery_config) + .build() + .unwrap(); + + assert_eq!(driver.discovery.disc.local_enr().tcp4().unwrap(), 9097); + } +} diff --git a/kona/crates/node/service/src/actors/network/config.rs b/kona/crates/node/service/src/actors/network/config.rs new file mode 100644 index 0000000000000..2aae0e1eb5f0c --- /dev/null +++ b/kona/crates/node/service/src/actors/network/config.rs @@ -0,0 +1,100 @@ +//! Configuration for the `Network`. + +use alloy_primitives::Address; +use kona_disc::LocalNode; +use kona_genesis::RollupConfig; +use kona_gossip::GaterConfig; +use kona_peers::{BootNodes, BootStoreFile, PeerMonitoring, PeerScoreLevel}; +use kona_sources::BlockSigner; +use libp2p::{Multiaddr, identity::Keypair}; +use tokio::time::Duration; + +/// Configuration for kona's P2P stack. +#[derive(Debug, Clone)] +pub struct NetworkConfig { + /// Discovery Config. + pub discovery_config: discv5::Config, + /// The local node's advertised address to external peers. + /// Note: This may be different from the node's discovery listen address. + pub discovery_address: LocalNode, + /// The interval to find peers. + pub discovery_interval: Duration, + /// The interval to remove peers from the discovery service. + pub discovery_randomize: Option, + /// Whether to update the ENR socket when the gossip listen address changes. + pub enr_update: bool, + /// The gossip address. + pub gossip_address: libp2p::Multiaddr, + /// The unsafe block signer. + pub unsafe_block_signer: Address, + /// The keypair. + pub keypair: Keypair, + /// The gossip config. + pub gossip_config: libp2p::gossipsub::Config, + /// The peer score level. + pub scoring: PeerScoreLevel, + /// Whether to enable topic scoring. + pub topic_scoring: bool, + /// Peer score monitoring config. + pub monitor_peers: Option, + /// An optional path to the bootstore. + pub bootstore: Option, + /// The configuration for the connection gater. + pub gater_config: GaterConfig, + /// An optional list of bootnode ENRs to start the node with. + pub bootnodes: BootNodes, + /// The [`RollupConfig`]. + pub rollup_config: RollupConfig, + /// A signer for gossip payloads. + pub gossip_signer: Option, +} + +impl NetworkConfig { + const DEFAULT_DISCOVERY_INTERVAL: Duration = Duration::from_secs(5); + const DEFAULT_DISCOVERY_RANDOMIZE: Option = None; + + /// Returns the [`discv5::Config`] from the CLI arguments. + pub fn discv5_config(listen_config: discv5::ListenConfig, static_ip: bool) -> discv5::Config { + // We can use a default listen config here since it + // will be overridden by the discovery service builder. + let mut builder = discv5::ConfigBuilder::new(listen_config); + + if static_ip { + builder.disable_enr_update(); + + // If we have a static IP, we don't want to use any kind of NAT discovery mechanism. + builder.auto_nat_listen_duration(None); + } + + builder.build() + } + + /// Creates a new [`NetworkConfig`] with the given [`RollupConfig`] with the minimum required + /// fields. Generates a random keypair for the node. + pub fn new( + rollup_config: RollupConfig, + discovery_listen: LocalNode, + gossip_address: Multiaddr, + unsafe_block_signer: Address, + ) -> Self { + Self { + rollup_config, + discovery_config: discv5::ConfigBuilder::new((&discovery_listen).into()).build(), + discovery_address: discovery_listen, + discovery_interval: Self::DEFAULT_DISCOVERY_INTERVAL, + discovery_randomize: Self::DEFAULT_DISCOVERY_RANDOMIZE, + gossip_address, + unsafe_block_signer, + enr_update: true, + keypair: Keypair::generate_secp256k1(), + bootnodes: Default::default(), + bootstore: Default::default(), + gater_config: Default::default(), + gossip_config: Default::default(), + scoring: Default::default(), + topic_scoring: Default::default(), + monitor_peers: Default::default(), + gossip_signer: Default::default(), + } + } +} diff --git a/kona/crates/node/service/src/actors/network/driver.rs b/kona/crates/node/service/src/actors/network/driver.rs new file mode 100644 index 0000000000000..e2496c24c2f4e --- /dev/null +++ b/kona/crates/node/service/src/actors/network/driver.rs @@ -0,0 +1,97 @@ +use std::net::{IpAddr, SocketAddr}; + +use alloy_primitives::Address; +use discv5::multiaddr::Protocol; +use futures::future::OptionFuture; +use kona_disc::Discv5Driver; +use kona_gossip::{ConnectionGater, GossipDriver, PEER_SCORE_INSPECT_FREQUENCY}; +use kona_sources::{BlockSigner, BlockSignerStartError}; +use libp2p::{Multiaddr, TransportError}; +use tokio::sync::watch; + +use crate::actors::network::handler::NetworkHandler; + +/// A network driver. This is the driver that is used to start the network. +#[derive(Debug)] +pub struct NetworkDriver { + /// The gossip driver. + pub gossip: GossipDriver, + /// The discovery driver. + pub discovery: Discv5Driver, + /// Whether to update the ENR socket after the libp2p Swarm is started. + /// This is set to true by default. + /// This may be set to false if the node is configured to use a static advertised address (when + /// used with a nat for example). + pub enr_update: bool, + /// The unsafe block signer sender. + pub unsafe_block_signer_sender: watch::Sender
, + /// A block signer. This is optional and should be set if the node is configured to sign blocks + pub signer: Option, +} + +/// An error from the [`NetworkDriver`]. +#[derive(Debug, thiserror::Error)] +pub enum NetworkDriverError { + /// An error occurred starting the libp2p Swarm. + #[error("error starting libp2p Swarm")] + GossipStartError(#[from] TransportError), + /// An error occurred starting the block signer client. + #[error("error starting block signer client: {0}")] + BlockSignerStartError(#[from] BlockSignerStartError), + /// An error occurred parsing the gossip listen address. + #[error("error parsing gossip listen address: {0}")] + InvalidGossipListenAddr(Multiaddr), +} + +impl NetworkDriver { + /// Starts the network. + pub async fn start(mut self) -> Result { + // Start the libp2p Swarm + let gossip_listen_addr = self.gossip.start().await?; + + if self.enr_update { + // Update the local ENR socket to the gossip listen address. + // Parse the multiaddr to a socket address. + let ip_address = gossip_listen_addr + .iter() + .find_map(|p| match p { + Protocol::Ip4(ip) => Some(IpAddr::V4(ip)), + Protocol::Ip6(ip) => Some(IpAddr::V6(ip)), + _ => None, + }) + .ok_or_else(|| { + NetworkDriverError::InvalidGossipListenAddr(gossip_listen_addr.clone()) + })?; + let port = gossip_listen_addr + .iter() + .find_map(|p| match p { + Protocol::Tcp(port) => Some(port), + _ => None, + }) + .ok_or_else(|| { + NetworkDriverError::InvalidGossipListenAddr(gossip_listen_addr.clone()) + })?; + + self.discovery.disc.update_local_enr_socket(SocketAddr::new(ip_address, port), true); + } + + // Start the discovery service. + let (handler, enr_receiver) = self.discovery.start(); + + // We are checking the peer scores every [`PEER_SCORE_INSPECT_FREQUENCY`] seconds. + let peer_score_inspector = tokio::time::interval(*PEER_SCORE_INSPECT_FREQUENCY); + + // Start the block signer if it is configured. + let signer = + OptionFuture::from(self.signer.map(async |s| s.start().await)).await.transpose()?; + + Ok(NetworkHandler { + gossip: self.gossip, + discovery: handler, + enr_receiver, + unsafe_block_signer_sender: self.unsafe_block_signer_sender, + peer_score_inspector, + signer, + }) + } +} diff --git a/kona/crates/node/service/src/actors/network/error.rs b/kona/crates/node/service/src/actors/network/error.rs new file mode 100644 index 0000000000000..a6c7cec7d5061 --- /dev/null +++ b/kona/crates/node/service/src/actors/network/error.rs @@ -0,0 +1,15 @@ +//! Contains the error type for the network driver builder. + +use kona_disc::Discv5BuilderError; +use kona_gossip::GossipDriverBuilderError; + +/// An error from the [`crate::NetworkBuilder`]. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum NetworkBuilderError { + /// An error from building the gossip driver. + #[error(transparent)] + GossipDriverBuilder(#[from] GossipDriverBuilderError), + /// An error from building the discv5 driver. + #[error(transparent)] + DiscoveryDriverBuilder(#[from] Discv5BuilderError), +} diff --git a/kona/crates/node/service/src/actors/network/gossip.rs b/kona/crates/node/service/src/actors/network/gossip.rs new file mode 100644 index 0000000000000..9f8eea74a05f3 --- /dev/null +++ b/kona/crates/node/service/src/actors/network/gossip.rs @@ -0,0 +1,50 @@ +use async_trait::async_trait; +use derive_more::Constructor; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use std::fmt::Debug; +use thiserror::Error; +use tokio::sync::mpsc; + +/// Client used to schedule unsafe [`OpExecutionPayloadEnvelope`] to be gossiped. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait UnsafePayloadGossipClient: Send + Sync + Debug { + /// This is a fire-and-forget function that schedules the provided + /// [`OpExecutionPayloadEnvelope`] to be gossiped. The implementation should return as + /// quickly as possible and offers no guarantees that the payload actually was gossiped + /// successfully. + async fn schedule_execution_payload_gossip( + &self, + payload: OpExecutionPayloadEnvelope, + ) -> Result<(), UnsafePayloadGossipClientError>; +} + +/// Errors that can occur when using the [`UnsafePayloadGossipClient`]. +#[derive(Debug, Error)] +pub enum UnsafePayloadGossipClientError { + /// Error sending request. + #[error("Error sending request: {0}")] + RequestError(String), +} + +/// Queued implementation of [`UnsafePayloadGossipClient`] that handles requests by sending them +/// to a handler via the contained sender. +#[derive(Debug, Clone, Constructor)] +pub struct QueuedUnsafePayloadGossipClient { + /// Queue used to relay unsafe payloads to gossip. + request_tx: mpsc::Sender, +} + +#[async_trait] +impl UnsafePayloadGossipClient for QueuedUnsafePayloadGossipClient { + async fn schedule_execution_payload_gossip( + &self, + payload: OpExecutionPayloadEnvelope, + ) -> Result<(), UnsafePayloadGossipClientError> { + self.request_tx + .send(payload.clone()) + .await + .map_err(|_| UnsafePayloadGossipClientError::RequestError("request channel closed".to_string())) + .inspect_err(|err| error!(target: "gossip_client", ?payload, ?err, "failed to request to gossip payload.")) + } +} diff --git a/kona/crates/node/service/src/actors/network/handler.rs b/kona/crates/node/service/src/actors/network/handler.rs new file mode 100644 index 0000000000000..2da715475758d --- /dev/null +++ b/kona/crates/node/service/src/actors/network/handler.rs @@ -0,0 +1,105 @@ +use std::collections::HashSet; + +use alloy_primitives::Address; +use discv5::Enr; +use kona_disc::{Discv5Handler, HandlerRequest}; +use kona_gossip::{ConnectionGater, GossipDriver}; +use kona_sources::BlockSignerHandler; +use tokio::sync::{mpsc, watch}; + +/// A network handler used to communicate with the network once it is started. +#[derive(Debug)] +pub struct NetworkHandler { + /// The gossip driver. + pub gossip: GossipDriver, + /// The discovery handler. + pub discovery: Discv5Handler, + /// The receiver for the ENRs. + pub enr_receiver: mpsc::Receiver, + /// The sender for the unsafe block signer. + pub unsafe_block_signer_sender: watch::Sender
, + /// The peer score inspector. Is used to ban peers that are below a given threshold. + pub peer_score_inspector: tokio::time::Interval, + /// A handler for the block signer. + pub signer: Option, +} + +impl NetworkHandler { + pub(super) async fn handle_peer_monitoring(&mut self) { + // Inspect peer scores and ban peers that are below the threshold. + let Some(ban_peers) = self.gossip.peer_monitoring.as_ref() else { + return; + }; + + // We iterate over all connected peers and check their scores. + // We collect a list of peers to remove + let peers_to_remove = self + .gossip + .swarm + .connected_peers() + .filter_map(|peer_id| { + // If the score is not available, we use a default value of 0. + let score = + self.gossip.swarm.behaviour().gossipsub.peer_score(peer_id).unwrap_or_default(); + + // Record the peer score in the metrics. + kona_macros::record!( + histogram, + kona_gossip::Metrics::PEER_SCORES, + "peer", + peer_id.to_string(), + score + ); + + if score < ban_peers.ban_threshold { + return Some(*peer_id); + } + + None + }) + .collect::>(); + + // We remove the addresses from the gossip layer. + let addrs_to_ban = peers_to_remove.into_iter().filter_map(|peer_to_remove| { + // In that case, we ban the peer. This means... + // 1. We remove the peer from the network gossip. + // 2. We ban the peer from the discv5 service. + if self.gossip.swarm.disconnect_peer_id(peer_to_remove).is_err() { + warn!(peer = ?peer_to_remove, "Trying to disconnect a non-existing peer from the gossip driver."); + } + + // Record the duration of the peer connection. + if let Some(start_time) = self.gossip.peer_connection_start.remove(&peer_to_remove) { + let peer_duration = start_time.elapsed(); + kona_macros::record!( + histogram, + kona_gossip::Metrics::GOSSIP_PEER_CONNECTION_DURATION_SECONDS, + peer_duration.as_secs_f64() + ); + } + + if let Some(info) = self.gossip.peerstore.remove(&peer_to_remove){ + use kona_gossip::ConnectionGate; + self.gossip.connection_gate.remove_dial(&peer_to_remove); + let score = self.gossip.swarm.behaviour().gossipsub.peer_score(&peer_to_remove).unwrap_or_default(); + kona_macros::inc!(gauge, kona_gossip::Metrics::BANNED_PEERS, "peer_id" => peer_to_remove.to_string(), "score" => score.to_string()); + return Some(info.listen_addrs); + } + + None + }).collect::>().into_iter().flatten().collect::>(); + + // We send a request to the discovery handler to ban the set of addresses. + if let Err(send_err) = self + .discovery + .sender + .send(HandlerRequest::BanAddrs { + addrs_to_ban: addrs_to_ban.into(), + ban_duration: ban_peers.ban_duration, + }) + .await + { + warn!(err = ?send_err, "Impossible to send a request to the discovery handler. The channel connection is dropped."); + } + } +} diff --git a/kona/crates/node/service/src/actors/network/mod.rs b/kona/crates/node/service/src/actors/network/mod.rs new file mode 100644 index 0000000000000..50417495df0dd --- /dev/null +++ b/kona/crates/node/service/src/actors/network/mod.rs @@ -0,0 +1,27 @@ +//! Network Actor + +mod actor; +pub use actor::{NetworkActor, NetworkActorError, NetworkContext, NetworkInboundData}; + +mod builder; +pub use builder::NetworkBuilder; + +mod driver; +pub use driver::{NetworkDriver, NetworkDriverError}; + +mod error; +pub use error::NetworkBuilderError; + +mod handler; +pub use handler::NetworkHandler; + +mod config; +mod gossip; +pub use gossip::{ + QueuedUnsafePayloadGossipClient, UnsafePayloadGossipClient, UnsafePayloadGossipClientError, +}; + +pub use config::NetworkConfig; + +#[cfg(test)] +pub use gossip::MockUnsafePayloadGossipClient; diff --git a/kona/crates/node/service/src/actors/rpc.rs b/kona/crates/node/service/src/actors/rpc.rs new file mode 100644 index 0000000000000..9b8d02cd5b6ef --- /dev/null +++ b/kona/crates/node/service/src/actors/rpc.rs @@ -0,0 +1,230 @@ +//! RPC Server Actor + +use crate::{NodeActor, actors::CancellableContext}; +use async_trait::async_trait; +use kona_gossip::P2pRpcRequest; +use kona_rpc::{ + AdminApiServer, AdminRpc, DevEngineApiServer, DevEngineRpc, HealthzApiServer, HealthzRpc, + NetworkAdminQuery, OpP2PApiServer, RollupBoostAdminQuery, RollupBoostHealthQuery, + RollupBoostHealthzApiServer, RollupNodeApiServer, SequencerAdminAPIClient, WsRPC, WsServer, +}; +use std::time::Duration; + +use jsonrpsee::{ + RpcModule, + core::RegisterMethodError, + server::{Server, ServerHandle, middleware::http::ProxyGetRequestLayer}, +}; +use kona_engine::EngineQueries; +use kona_rpc::{L1WatcherQueries, P2pRpc, RollupRpc, RpcBuilder}; +use tokio::sync::mpsc; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; + +/// An error returned by the [`RpcActor`]. +#[derive(Debug, thiserror::Error)] +pub enum RpcActorError { + /// Failed to register the healthz endpoint. + #[error("Failed to register the healthz endpoint")] + RegisterHealthz(#[from] RegisterMethodError), + /// Failed to launch the RPC server. + #[error(transparent)] + LaunchFailed(#[from] std::io::Error), + /// The [`RpcActor`]'s RPC server stopped unexpectedly. + #[error("RPC server stopped unexpectedly")] + ServerStopped, + /// Failed to stop the RPC server. + #[error("Failed to stop the RPC server")] + StopFailed, +} + +/// An actor that handles the RPC server for the rollup node. +#[derive(Debug)] +pub struct RpcActor { + /// A launcher for the rpc. + config: RpcBuilder, + + phantom: std::marker::PhantomData, +} + +impl RpcActor { + /// Constructs a new [`RpcActor`] given the [`RpcBuilder`]. + pub const fn new(config: RpcBuilder) -> Self { + Self { config, phantom: std::marker::PhantomData } + } +} + +/// The communication context used by the RPC actor. +#[derive(Debug)] +pub struct RpcContext { + /// The network p2p rpc sender. + pub p2p_network: mpsc::Sender, + /// The network admin rpc sender. + pub network_admin: mpsc::Sender, + /// The sequencer admin rpc sender. + pub sequencer_admin: Option, + /// The l1 watcher queries sender. + pub l1_watcher_queries: mpsc::Sender, + /// The engine query sender. + pub engine_query: mpsc::Sender, + /// The cancellation token, shared between all tasks. + pub cancellation: CancellationToken, + /// The rollup boost admin rpc sender. + pub rollup_boost_admin: mpsc::Sender, + /// The rollup boost health rpc sender. + pub rollup_boost_health: mpsc::Sender, +} + +impl CancellableContext for RpcContext { + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} + +/// Launches the jsonrpsee [`Server`]. +/// +/// If the RPC server is disabled, this will return `Ok(None)`. +/// +/// ## Errors +/// +/// - [`std::io::Error`] if the server fails to start. +async fn launch( + config: &RpcBuilder, + module: RpcModule<()>, +) -> Result { + let middleware = tower::ServiceBuilder::new() + .layer( + ProxyGetRequestLayer::new([ + ("/healthz", "healthz"), + ("/kona-rollup-boost/healthz", "kona-rollup-boost_healthz"), + ]) + .expect("Critical: Failed to build GET method proxy"), + ) + .timeout(Duration::from_secs(2)); + let server = Server::builder().set_http_middleware(middleware).build(config.socket).await?; + + if let Ok(addr) = server.local_addr() { + info!(target: "rpc", addr = ?addr, "RPC server bound to address"); + } else { + error!(target: "rpc", "Failed to get local address for RPC server"); + } + + Ok(server.start(module)) +} + +#[async_trait] +impl NodeActor for RpcActor { + type Error = RpcActorError; + type StartData = RpcContext; + + async fn start( + mut self, + RpcContext { + cancellation, + p2p_network, + l1_watcher_queries, + engine_query, + network_admin, + sequencer_admin, + rollup_boost_admin, + rollup_boost_health, + }: Self::StartData, + ) -> Result<(), Self::Error> { + let mut modules = RpcModule::new(()); + + let healthz_rpc = HealthzRpc::new(rollup_boost_health); + modules.merge(HealthzApiServer::into_rpc(healthz_rpc.clone()))?; + modules.merge(RollupBoostHealthzApiServer::into_rpc(healthz_rpc))?; + + // Build the p2p rpc module. + modules.merge(P2pRpc::new(p2p_network).into_rpc())?; + + // Build the admin rpc module. + modules.merge( + AdminRpc::new(sequencer_admin, network_admin, Some(rollup_boost_admin)).into_rpc(), + )?; + + // Create context for communication between actors. + let rollup_rpc = RollupRpc::new(engine_query.clone(), l1_watcher_queries); + modules.merge(rollup_rpc.into_rpc())?; + + // Add development RPC module for engine state introspection if enabled + if self.config.dev_enabled() { + let dev_rpc = DevEngineRpc::new(engine_query.clone()); + modules.merge(dev_rpc.into_rpc())?; + } + + if self.config.ws_enabled() { + modules.merge(WsRPC::new(engine_query).into_rpc())?; + } + + let restarts = self.config.restart_count(); + + let mut handle = launch(&self.config, modules.clone()).await?; + + for _ in 0..=restarts { + tokio::select! { + _ = handle.clone().stopped() => { + match launch(&self.config, modules.clone()).await { + Ok(h) => handle = h, + Err(err) => { + error!(target: "rpc", ?err, "Failed to launch rpc server"); + cancellation.cancel(); + return Err(RpcActorError::ServerStopped); + } + } + } + _ = cancellation.cancelled() => { + // The cancellation token has been triggered, so we should stop the server. + handle.stop().map_err(|_| RpcActorError::StopFailed)?; + // Since the RPC Server didn't originate the error, we should return Ok. + return Ok(()); + } + } + } + + // Stop the node if there has already been 3 rpc restarts. + cancellation.cancel(); + return Err(RpcActorError::ServerStopped); + } +} + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use super::*; + + #[tokio::test] + async fn test_launch_no_modules() { + let launcher = RpcBuilder { + socket: SocketAddr::from(([127, 0, 0, 1], 8080)), + no_restart: false, + enable_admin: false, + admin_persistence: None, + ws_enabled: false, + dev_enabled: false, + }; + let result = launch(&launcher, RpcModule::new(())).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_launch_with_modules() { + let launcher = RpcBuilder { + socket: SocketAddr::from(([127, 0, 0, 1], 8081)), + no_restart: false, + enable_admin: false, + admin_persistence: None, + ws_enabled: false, + dev_enabled: false, + }; + let mut modules = RpcModule::new(()); + + modules.merge(RpcModule::new(())).expect("module merge"); + modules.merge(RpcModule::new(())).expect("module merge"); + modules.merge(RpcModule::new(())).expect("module merge"); + + let result = launch(&launcher, modules).await; + assert!(result.is_ok()); + } +} diff --git a/kona/crates/node/service/src/actors/sequencer/actor.rs b/kona/crates/node/service/src/actors/sequencer/actor.rs new file mode 100644 index 0000000000000..146f7204c6650 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/actor.rs @@ -0,0 +1,521 @@ +//! The [`SequencerActor`]. + +use crate::{ + CancellableContext, NodeActor, UnsafePayloadGossipClient, + actors::{ + BlockBuildingClient, + engine::BlockEngineError, + sequencer::{ + admin_api_client::SequencerAdminQuery, + conductor::Conductor, + error::SequencerActorError, + metrics::{ + update_attributes_build_duration_metrics, update_block_build_duration_metrics, + update_conductor_commitment_duration_metrics, update_seal_duration_metrics, + }, + origin_selector::OriginSelector, + }, + }, +}; +use alloy_rpc_types_engine::PayloadId; +use async_trait::async_trait; +use kona_derive::{AttributesBuilder, PipelineErrorKind}; +use kona_engine::{InsertTaskError, SealTaskError, SynchronizeTaskError}; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use std::{ + sync::Arc, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; +use tokio::{select, sync::mpsc}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; + +/// The handle to a block that has been started but not sealed. +#[derive(Debug)] +struct UnsealedPayloadHandle { + /// The [`PayloadId`] of the unsealed payload. + pub payload_id: PayloadId, + /// The [`OpAttributesWithParent`] used to start block building. + pub attributes_with_parent: OpAttributesWithParent, +} + +/// The return payload of the `seal_last_and_start_next` function. This allows the sequencer +/// to make an informed decision about when to seal and build the next block. +#[derive(Debug)] +struct SealLastStartNextResult { + /// The [`UnsealedPayloadHandle`] that was built. + pub unsealed_payload_handle: Option, + /// How long it took to execute the seal operation. + pub seal_duration: Duration, +} + +/// The [`SequencerActor`] is responsible for building L2 blocks on top of the current unsafe head +/// and scheduling them to be signed and gossipped by the P2P layer, extending the L2 chain with new +/// blocks. +#[derive(Debug)] +pub struct SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> where + AttributesBuilder_: AttributesBuilder, + BlockBuildingClient_: BlockBuildingClient, + Conductor_: Conductor, + OriginSelector_: OriginSelector, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient, +{ + /// Receiver for admin API requests. + pub admin_api_rx: mpsc::Receiver, + /// The attributes builder used for block building. + pub attributes_builder: AttributesBuilder_, + /// The struct used to build blocks. + pub block_building_client: BlockBuildingClient_, + /// The cancellation token, shared between all tasks. + pub cancellation_token: CancellationToken, + /// The optional conductor RPC client. + pub conductor: Option, + /// Whether the sequencer is active. + pub is_active: bool, + /// Whether the sequencer is in recovery mode. + pub in_recovery_mode: bool, + /// The struct used to determine the next L1 origin. + pub origin_selector: OriginSelector_, + /// The rollup configuration. + pub rollup_config: Arc, + /// A client to asynchronously sign and gossip built payloads to the network actor. + pub unsafe_payload_gossip_client: UnsafePayloadGossipClient_, +} + +impl< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> + SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, + > +where + AttributesBuilder_: AttributesBuilder, + BlockBuildingClient_: BlockBuildingClient, + Conductor_: Conductor, + OriginSelector_: OriginSelector, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient, +{ + /// Seals and commits the last pending block, if one exists and starts the build job for the + /// next L2 block, on top of the current unsafe head. + /// + /// If a new block was started, it will return the associated [`UnsealedPayloadHandle`] so + /// that it may be sealed and committed in a future call to this function. + async fn seal_last_and_start_next( + &mut self, + payload_to_seal: Option<&UnsealedPayloadHandle>, + ) -> Result { + let seal_duration = match payload_to_seal { + Some(to_seal) => { + let seal_start = Instant::now(); + self.seal_and_commit_payload_if_applicable(to_seal).await?; + seal_start.elapsed() + } + None => Duration::default(), + }; + + let unsealed_payload_handle = self.build_unsealed_payload().await?; + + Ok(SealLastStartNextResult { unsealed_payload_handle, seal_duration }) + } + + /// Sends a seal request to seal the provided [`UnsealedPayloadHandle`], committing and + /// gossiping the resulting block, if one is built. + async fn seal_and_commit_payload_if_applicable( + &mut self, + unsealed_payload_handle: &UnsealedPayloadHandle, + ) -> Result<(), SequencerActorError> { + let seal_request_start = Instant::now(); + + // Send the seal request to the engine to seal the unsealed block. + let payload = self + .block_building_client + .seal_and_canonicalize_block( + unsealed_payload_handle.payload_id, + unsealed_payload_handle.attributes_with_parent.clone(), + ) + .await?; + + update_seal_duration_metrics(seal_request_start.elapsed()); + + // If the conductor is available, commit the payload to it. + if let Some(conductor) = &self.conductor { + let _conductor_commitment_start = Instant::now(); + if let Err(err) = conductor.commit_unsafe_payload(&payload).await { + error!(target: "sequencer", ?err, "Failed to commit unsafe payload to conductor"); + } + + update_conductor_commitment_duration_metrics(_conductor_commitment_start.elapsed()); + } + + self.unsafe_payload_gossip_client + .schedule_execution_payload_gossip(payload) + .await + .map_err(Into::into) + } + + /// Starts building an L2 block by creating and populating payload attributes referencing the + /// correct L1 origin block and sending them to the block engine. + async fn build_unsealed_payload( + &mut self, + ) -> Result, SequencerActorError> { + let unsafe_head = self.block_building_client.get_unsafe_head().await?; + + let Some(l1_origin) = self.get_next_payload_l1_origin(unsafe_head).await? else { + // Temporary error - retry on next tick. + return Ok(None); + }; + + info!( + target: "sequencer", + parent_num = unsafe_head.block_info.number, + l1_origin_num = l1_origin.number, + "Started sequencing new block" + ); + + // Build the payload attributes for the next block. + let attributes_build_start = Instant::now(); + + let Some(attributes_with_parent) = self.build_attributes(unsafe_head, l1_origin).await? + else { + // Temporary error or reset - retry on next tick. + return Ok(None); + }; + + update_attributes_build_duration_metrics(attributes_build_start.elapsed()); + + // Send the built attributes to the engine to be built. + let build_request_start = Instant::now(); + + let payload_id = + self.block_building_client.start_build_block(attributes_with_parent.clone()).await?; + + update_block_build_duration_metrics(build_request_start.elapsed()); + + Ok(Some(UnsealedPayloadHandle { payload_id, attributes_with_parent })) + } + + /// Determines and validates the L1 origin block for the provided L2 unsafe head. + /// Returns `Ok(None)` for temporary errors that should be retried. + async fn get_next_payload_l1_origin( + &mut self, + unsafe_head: L2BlockInfo, + ) -> Result, SequencerActorError> { + let l1_origin = match self + .origin_selector + .next_l1_origin(unsafe_head, self.in_recovery_mode) + .await + { + Ok(l1_origin) => l1_origin, + Err(err) => { + warn!( + target: "sequencer", + ?err, + "Temporary error occurred while selecting next L1 origin. Re-attempting on next tick." + ); + return Ok(None); + } + }; + + if unsafe_head.l1_origin.hash != l1_origin.parent_hash && + unsafe_head.l1_origin.hash != l1_origin.hash + { + warn!( + target: "sequencer", + l1_origin = ?l1_origin, + unsafe_head_hash = %unsafe_head.l1_origin.hash, + unsafe_head_l1_origin = ?unsafe_head.l1_origin, + "Cannot build new L2 block on inconsistent L1 origin, resetting engine" + ); + self.block_building_client.reset_engine_forkchoice().await?; + return Ok(None); + } + Ok(Some(l1_origin)) + } + + /// Builds the OpAttributesWithParent for the next block to build. If None is returned, it + /// indicates that no attributes could be built at this time but future attempts may be made. + async fn build_attributes( + &mut self, + unsafe_head: L2BlockInfo, + l1_origin: BlockInfo, + ) -> Result, SequencerActorError> { + let mut attributes = match self + .attributes_builder + .prepare_payload_attributes(unsafe_head, l1_origin.id()) + .await + { + Ok(attrs) => attrs, + Err(PipelineErrorKind::Temporary(_)) => { + // Temporary error - retry on next tick. + return Ok(None); + } + Err(PipelineErrorKind::Reset(_)) => { + if let Err(err) = self.block_building_client.reset_engine_forkchoice().await { + error!(target: "sequencer", ?err, "Failed to reset engine"); + return Err(SequencerActorError::ChannelClosed); + } + + warn!( + target: "sequencer", + "Resetting engine due to pipeline error while preparing payload attributes" + ); + return Ok(None); + } + Err(err @ PipelineErrorKind::Critical(_)) => { + error!(target: "sequencer", ?err, "Failed to prepare payload attributes"); + return Err(err.into()); + } + }; + + attributes.no_tx_pool = Some(!self.should_use_tx_pool(l1_origin, &attributes)); + + let attrs_with_parent = OpAttributesWithParent::new(attributes, unsafe_head, None, false); + Ok(Some(attrs_with_parent)) + } + + /// Determines, for the provided L1 origin block and payload attributes being constructed, if + /// transaction pool transactions should be enabled. + fn should_use_tx_pool(&self, l1_origin: BlockInfo, attributes: &OpPayloadAttributes) -> bool { + if self.in_recovery_mode { + warn!(target: "sequencer", "Sequencer is in recovery mode, producing empty block"); + return false; + } + + // If the next L2 block is beyond the sequencer drift threshold, we must produce an empty + // block. + if attributes.payload_attributes.timestamp > + l1_origin.timestamp + self.rollup_config.max_sequencer_drift(l1_origin.timestamp) + { + return false; + } + + // Do not include transactions in the first Ecotone block. + if self.rollup_config.is_first_ecotone_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing ecotone upgrade block"); + return false; + } + + // Do not include transactions in the first Fjord block. + if self.rollup_config.is_first_fjord_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing fjord upgrade block"); + return false; + } + + // Do not include transactions in the first Granite block. + if self.rollup_config.is_first_granite_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing granite upgrade block"); + return false; + } + + // Do not include transactions in the first Holocene block. + if self.rollup_config.is_first_holocene_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing holocene upgrade block"); + return false; + } + + // Do not include transactions in the first Isthmus block. + if self.rollup_config.is_first_isthmus_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing isthmus upgrade block"); + return false; + } + + // Do not include transactions in the first Jovian block. + // See: `` + if self.rollup_config.is_first_jovian_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing jovian upgrade block"); + return false; + } + + // Do not include transactions in the first Interop block. + if self.rollup_config.is_first_interop_block(attributes.payload_attributes.timestamp) { + info!(target: "sequencer", "Sequencing interop upgrade block"); + return false; + } + + // Transaction pool transactions are enabled if none of the reasons to disable are satisfied + // above. + true + } + + /// Schedules the initial engine reset request and waits for the unsafe head to be updated. + async fn schedule_initial_reset(&mut self) -> Result<(), SequencerActorError> { + // Reset the engine, in order to initialize the engine state. + // NB: this call waits for confirmation that the reset succeeded and we can proceed with + // post-reset logic. + self.block_building_client.reset_engine_forkchoice().await.map_err(|err| { + error!(target: "sequencer", ?err, "Failed to send reset request to engine"); + err.into() + }) + } +} + +#[async_trait] +impl< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> NodeActor + for SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, + > +where + AttributesBuilder_: AttributesBuilder + Sync + 'static, + BlockBuildingClient_: BlockBuildingClient + Sync + 'static, + Conductor_: Conductor + Sync + 'static, + OriginSelector_: OriginSelector + Sync + 'static, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient + Sync + 'static, +{ + type Error = SequencerActorError; + type StartData = (); + + async fn start(mut self, _: Self::StartData) -> Result<(), Self::Error> { + let mut build_ticker = + tokio::time::interval(Duration::from_secs(self.rollup_config.block_time)); + + self.update_metrics(); + + // Reset the engine state prior to beginning block building. + self.schedule_initial_reset().await?; + + let mut next_payload_to_seal: Option = None; + let mut last_seal_duration = Duration::from_secs(0); + loop { + select! { + // We are using a biased select here to ensure that the admin queries are given priority over the block building task. + // This is important to limit the occurrence of race conditions where a stopped query is received when a sequencer is building a new block. + biased; + _ = self.cancellation_token.cancelled() => { + info!( + target: "sequencer", + "Received shutdown signal. Exiting sequencer task." + ); + return Ok(()); + } + Some(query) = self.admin_api_rx.recv() => { + let active_before = self.is_active; + + self.handle_admin_query(query).await; + + // immediately attempt to build a block if the sequencer was just started + if !active_before && self.is_active { + build_ticker.reset_immediately(); + } + } + // The sequencer must be active to build new blocks. + _ = build_ticker.tick(), if self.is_active => { + + match self.seal_last_and_start_next(next_payload_to_seal.as_ref()).await { + Ok(res) => { + next_payload_to_seal = res.unsealed_payload_handle; + last_seal_duration = res.seal_duration; + }, + Err(SequencerActorError::BlockEngine(BlockEngineError::SealError(err))) => { + if is_seal_task_err_fatal(&err) { + error!(target: "sequencer", err=?err, "Critical seal task error occurred"); + self.cancellation_token.cancel(); + return Err(SequencerActorError::BlockEngine(BlockEngineError::SealError(err))); + } else { + next_payload_to_seal = None; + } + }, + Err(other_err) => { + error!(target: "sequencer", err = ?other_err, "Unexpected error building or sealing payload"); + self.cancellation_token.cancel(); + return Err(other_err); + } + } + + if let Some(ref payload) = next_payload_to_seal { + let next_block_seconds = payload.attributes_with_parent.parent().block_info.timestamp.saturating_add(self.rollup_config.block_time); + // next block time is last + block_time - time it takes to seal. + let next_block_time = UNIX_EPOCH + Duration::from_secs(next_block_seconds) - last_seal_duration; + match next_block_time.duration_since(SystemTime::now()) { + Ok(duration) => build_ticker.reset_after(duration), + Err(_) => build_ticker.reset_immediately(), + }; + } else { + build_ticker.reset_immediately(); + } + } + } + } + } +} + +impl< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> CancellableContext + for SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, + > +where + AttributesBuilder_: AttributesBuilder, + BlockBuildingClient_: BlockBuildingClient, + Conductor_: Conductor, + OriginSelector_: OriginSelector, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient, +{ + fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.cancellation_token.cancelled() + } +} + +// Determines whether the provided [`SealTaskError`] is fatal for the sequencer. +// +// NB: We could use `err.severity()`, but that gives EngineActor control over this classification. +// `SequencerActor` may have different interpretations of severity, and it is not clear when making +// a change in that area of the codebase that it will affect this area. When a new task error is +// added, this approach guarantees compilation will fail until it is handled here. +fn is_seal_task_err_fatal(err: &SealTaskError) -> bool { + match err { + SealTaskError::PayloadInsertionFailed(insert_err) => match &**insert_err { + InsertTaskError::ForkchoiceUpdateFailed(synchronize_error) => match synchronize_error { + SynchronizeTaskError::FinalizedAheadOfUnsafe(_, _) => true, + SynchronizeTaskError::ForkchoiceUpdateFailed(_) => false, + SynchronizeTaskError::InvalidForkchoiceState => false, + SynchronizeTaskError::UnexpectedPayloadStatus(_) => false, + }, + InsertTaskError::FromBlockError(_) => true, + InsertTaskError::InsertFailed(_) => false, + InsertTaskError::UnexpectedPayloadStatus(_) => false, + InsertTaskError::L2BlockInfoConstruction(_) => true, + }, + SealTaskError::GetPayloadFailed(_) => false, + SealTaskError::DepositOnlyPayloadFailed => true, + SealTaskError::DepositOnlyPayloadReattemptFailed => true, + SealTaskError::HoloceneInvalidFlush => false, + SealTaskError::FromBlock(_) => true, + SealTaskError::MpscSend(_) => true, + SealTaskError::ClockWentBackwards => true, + SealTaskError::UnsafeHeadChangedSinceBuild => false, + } +} diff --git a/kona/crates/node/service/src/actors/sequencer/admin_api_client.rs b/kona/crates/node/service/src/actors/sequencer/admin_api_client.rs new file mode 100644 index 0000000000000..d1e8f63aac1e9 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/admin_api_client.rs @@ -0,0 +1,128 @@ +//! The RPC server for the sequencer actor. +//! Mostly handles queries from the admin rpc. + +use alloy_primitives::B256; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_rpc::{SequencerAdminAPIClient, SequencerAdminAPIError}; +use tokio::sync::{mpsc, oneshot}; + +/// Queued implementation of [`SequencerAdminAPIClient`] that handles requests by sending them to +/// a handler via the contained sender. +#[derive(Debug, Clone, Constructor)] +pub struct QueuedSequencerAdminAPIClient { + /// Queue used to relay admin queries + request_tx: mpsc::Sender, +} + +/// The query types to the sequencer actor for the admin api. +#[derive(Debug)] +pub enum SequencerAdminQuery { + /// A query to check if the sequencer is active. + SequencerActive(oneshot::Sender>), + /// A query to start the sequencer. + StartSequencer(oneshot::Sender>), + /// A query to stop the sequencer. + StopSequencer(oneshot::Sender>), + /// A query to check if the conductor is enabled. + ConductorEnabled(oneshot::Sender>), + /// A query to check if the sequencer is in recovery mode. + RecoveryMode(oneshot::Sender>), + /// A query to set the recovery mode. + SetRecoveryMode(bool, oneshot::Sender>), + /// A query to override the leader. + OverrideLeader(oneshot::Sender>), + /// A query to reset the derivation pipeline. + ResetDerivationPipeline(oneshot::Sender>), +} + +#[async_trait] +impl SequencerAdminAPIClient for QueuedSequencerAdminAPIClient { + async fn is_sequencer_active(&self) -> Result { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::SequencerActive(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn is_conductor_enabled(&self) -> Result { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::ConductorEnabled(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn is_recovery_mode(&self) -> Result { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::RecoveryMode(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn start_sequencer(&self) -> Result<(), SequencerAdminAPIError> { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::StartSequencer(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn stop_sequencer(&self) -> Result { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::StopSequencer(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn set_recovery_mode(&self, mode: bool) -> Result<(), SequencerAdminAPIError> { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::SetRecoveryMode(mode, tx)).await.map_err( + |_| SequencerAdminAPIError::RequestError("request channel closed".to_string()), + )?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn override_leader(&self) -> Result<(), SequencerAdminAPIError> { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::OverrideLeader(tx)).await.map_err(|_| { + SequencerAdminAPIError::RequestError("request channel closed".to_string()) + })?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } + + async fn reset_derivation_pipeline(&self) -> Result<(), SequencerAdminAPIError> { + let (tx, rx) = oneshot::channel(); + + self.request_tx.send(SequencerAdminQuery::ResetDerivationPipeline(tx)).await.map_err( + |_| SequencerAdminAPIError::RequestError("request channel closed".to_string()), + )?; + rx.await.map_err(|_| { + SequencerAdminAPIError::ResponseError("response channel closed".to_string()) + })? + } +} diff --git a/kona/crates/node/service/src/actors/sequencer/admin_api_impl.rs b/kona/crates/node/service/src/actors/sequencer/admin_api_impl.rs new file mode 100644 index 0000000000000..4b1a4dac5ea87 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/admin_api_impl.rs @@ -0,0 +1,163 @@ +use super::SequencerActor; +use crate::{ + BlockBuildingClient, Conductor, OriginSelector, SequencerAdminQuery, UnsafePayloadGossipClient, +}; +use alloy_primitives::B256; +use kona_derive::AttributesBuilder; +use kona_rpc::{SequencerAdminAPIError, StopSequencerError}; + +/// Handler for the Sequencer Admin API. +impl< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> + SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, + > +where + AttributesBuilder_: AttributesBuilder, + BlockBuildingClient_: BlockBuildingClient, + Conductor_: Conductor, + OriginSelector_: OriginSelector, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient, +{ + /// Handles the provided [`SequencerAdminQuery`], sending the response via the provided sender. + /// This function is used to decouple admin API logic from the response mechanism (channels). + pub(super) async fn handle_admin_query(&mut self, query: SequencerAdminQuery) { + match query { + SequencerAdminQuery::SequencerActive(tx) => { + if tx.send(self.is_sequencer_active().await).is_err() { + warn!(target: "sequencer", "Failed to send response for is_sequencer_active query"); + } + } + SequencerAdminQuery::StartSequencer(tx) => { + if tx.send(self.start_sequencer().await).is_err() { + warn!(target: "sequencer", "Failed to send response for start_sequencer query"); + } + } + SequencerAdminQuery::StopSequencer(tx) => { + if tx.send(self.stop_sequencer().await).is_err() { + warn!(target: "sequencer", "Failed to send response for stop_sequencer query"); + } + } + SequencerAdminQuery::ConductorEnabled(tx) => { + if tx.send(self.is_conductor_enabled().await).is_err() { + warn!(target: "sequencer", "Failed to send response for is_conductor_enabled query"); + } + } + SequencerAdminQuery::RecoveryMode(tx) => { + if tx.send(self.in_recovery_mode().await).is_err() { + warn!(target: "sequencer", "Failed to send response for in_recovery_mode query"); + } + } + SequencerAdminQuery::SetRecoveryMode(is_active, tx) => { + if tx.send(self.set_recovery_mode(is_active).await).is_err() { + warn!(target: "sequencer", is_active = is_active, "Failed to send response for set_recovery_mode query"); + } + } + SequencerAdminQuery::OverrideLeader(tx) => { + if tx.send(self.override_leader().await).is_err() { + warn!(target: "sequencer", "Failed to send response for override_leader query"); + } + } + SequencerAdminQuery::ResetDerivationPipeline(tx) => { + if tx.send(self.reset_derivation_pipeline().await).is_err() { + warn!(target: "sequencer", "Failed to send response for reset_derivation_pipeline query"); + } + } + } + } + + /// Returns whether the sequencer is active. + pub(super) async fn is_sequencer_active(&self) -> Result { + Ok(self.is_active) + } + + /// Returns whether the conductor is enabled. + pub(super) async fn is_conductor_enabled(&self) -> Result { + Ok(self.conductor.is_some()) + } + + /// Returns whether the node is in recovery mode. + pub(super) async fn in_recovery_mode(&self) -> Result { + Ok(self.in_recovery_mode) + } + + /// Starts the sequencer in an idempotent fashion. + pub(super) async fn start_sequencer(&mut self) -> Result<(), SequencerAdminAPIError> { + if self.is_active { + info!(target: "sequencer", "received request to start sequencer, but it is already started"); + return Ok(()); + } + + info!(target: "sequencer", "Starting sequencer"); + self.is_active = true; + + self.update_metrics(); + + Ok(()) + } + + /// Stops the sequencer in an idempotent fashion. + pub(super) async fn stop_sequencer(&mut self) -> Result { + info!(target: "sequencer", "Stopping sequencer"); + self.is_active = false; + + self.update_metrics(); + + self.block_building_client.get_unsafe_head().await + .map(|h| h.hash()) + .map_err(|e| { + error!(target: "sequencer", err=?e, "Error fetching unsafe head after stopping sequencer, which should never happen."); + SequencerAdminAPIError::StopError(StopSequencerError::ErrorAfterSequencerWasStopped("current unsafe hash is unavailable.".to_string())) + }) + } + + /// Sets the recovery mode of the sequencer in an idempotent fashion. + pub(super) async fn set_recovery_mode( + &mut self, + is_active: bool, + ) -> Result<(), SequencerAdminAPIError> { + self.in_recovery_mode = is_active; + info!(target: "sequencer", is_active, "Updated recovery mode"); + + self.update_metrics(); + + Ok(()) + } + + /// Overrides the leader, if the conductor is enabled. + /// If not, an error will be returned. + pub(super) async fn override_leader(&mut self) -> Result<(), SequencerAdminAPIError> { + let Some(conductor) = self.conductor.as_mut() else { + return Err(SequencerAdminAPIError::LeaderOverrideError( + "No conductor configured".to_string(), + )); + }; + + if let Err(e) = conductor.override_leader().await { + error!(target: "sequencer::rpc", "Failed to override leader: {}", e); + return Err(SequencerAdminAPIError::LeaderOverrideError(e.to_string())); + } + info!(target: "sequencer", "Overrode leader via the conductor service"); + + self.update_metrics(); + + Ok(()) + } + + pub(super) async fn reset_derivation_pipeline(&mut self) -> Result<(), SequencerAdminAPIError> { + info!(target: "sequencer", "Resetting derivation pipeline"); + self.block_building_client.reset_engine_forkchoice().await.map_err(|e| { + error!(target: "sequencer", err=?e, "Failed to reset engine forkchoice"); + SequencerAdminAPIError::RequestError(format!("Failed to reset engine: {e}")) + }) + } +} diff --git a/kona/crates/node/service/src/actors/sequencer/admin_api_impl_test.rs b/kona/crates/node/service/src/actors/sequencer/admin_api_impl_test.rs new file mode 100644 index 0000000000000..6a9e3c88acb32 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/admin_api_impl_test.rs @@ -0,0 +1,439 @@ +use crate::{ + BlockEngineError, ConductorError, SequencerActor, SequencerAdminQuery, + actors::{ + MockBlockBuildingClient, MockConductor, MockOriginSelector, MockUnsafePayloadGossipClient, + }, +}; +use alloy_primitives::B256; +use alloy_transport::RpcError; +use kona_derive::test_utils::TestAttributesBuilder; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo}; +use kona_rpc::{SequencerAdminAPIError, StopSequencerError}; +use rstest::rstest; +use std::{sync::Arc, vec}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; + +// Returns a test SequencerActorBuilder with mocks that can be used or overridden. +fn test_actor() -> SequencerActor< + TestAttributesBuilder, + MockBlockBuildingClient, + MockConductor, + MockOriginSelector, + MockUnsafePayloadGossipClient, +> { + let (_admin_api_tx, admin_api_rx) = mpsc::channel(20); + SequencerActor { + admin_api_rx, + attributes_builder: TestAttributesBuilder { attributes: vec![] }, + block_building_client: MockBlockBuildingClient::new(), + cancellation_token: CancellationToken::new(), + conductor: None, + is_active: true, + in_recovery_mode: false, + origin_selector: MockOriginSelector::new(), + rollup_config: Arc::new(RollupConfig::default()), + unsafe_payload_gossip_client: MockUnsafePayloadGossipClient::new(), + } +} + +#[rstest] +#[tokio::test] +async fn test_is_sequencer_active( + #[values(true, false)] active: bool, + #[values(true, false)] via_channel: bool, +) { + let mut actor = test_actor(); + actor.is_active = active; + + let result = async { + match via_channel { + false => actor.is_sequencer_active().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::SequencerActive(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + assert!(result.is_ok()); + assert_eq!(active, result.unwrap()); +} + +#[rstest] +#[tokio::test] +async fn test_is_conductor_enabled( + #[values(true, false)] conductor_exists: bool, + #[values(true, false)] via_channel: bool, +) { + let mut actor = test_actor(); + if conductor_exists { + actor.conductor = Some(MockConductor::new()) + }; + + let result = async { + match via_channel { + false => actor.is_conductor_enabled().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::ConductorEnabled(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + assert!(result.is_ok()); + assert_eq!(conductor_exists, result.unwrap()); +} + +#[rstest] +#[tokio::test] +async fn test_in_recovery_mode( + #[values(true, false)] recovery_mode: bool, + #[values(true, false)] via_channel: bool, +) { + let mut actor = test_actor(); + actor.in_recovery_mode = recovery_mode; + + let result = async { + match via_channel { + false => actor.in_recovery_mode().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::RecoveryMode(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + assert!(result.is_ok()); + assert_eq!(recovery_mode, result.unwrap()); +} + +#[rstest] +#[tokio::test] +async fn test_start_sequencer( + #[values(true, false)] already_started: bool, + #[values(true, false)] via_channel: bool, +) { + let mut actor = test_actor(); + actor.is_active = already_started; + + // verify starting state + let result = actor.is_sequencer_active().await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), already_started); + + // start the sequencer + let result = async { + match via_channel { + false => actor.start_sequencer().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::StartSequencer(tx)).await; + rx.await.unwrap() + } + } + } + .await; + assert!(result.is_ok()); + + // verify it is started + let result = actor.is_sequencer_active().await; + assert!(result.is_ok()); + assert!(result.unwrap()); +} + +#[rstest] +#[tokio::test] +async fn test_stop_sequencer_success( + #[values(true, false)] already_stopped: bool, + #[values(true, false)] via_channel: bool, +) { + let unsafe_head = L2BlockInfo { + block_info: BlockInfo { hash: B256::from([1u8; 32]), ..Default::default() }, + ..Default::default() + }; + let expected_hash = unsafe_head.hash(); + + let mut client = MockBlockBuildingClient::new(); + client.expect_get_unsafe_head().times(1).return_once(move || Ok(unsafe_head)); + + let mut actor = test_actor(); + actor.block_building_client = client; + actor.is_active = !already_stopped; + + // verify starting state + let result = actor.is_sequencer_active().await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), !already_stopped); + + // stop the sequencer + let result = async { + match via_channel { + false => actor.stop_sequencer().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::StopSequencer(tx)).await; + rx.await.unwrap() + } + } + } + .await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_hash); + + // verify ending state + let result = actor.is_sequencer_active().await; + assert!(result.is_ok()); + assert!(!result.unwrap()); +} + +#[rstest] +#[tokio::test] +async fn test_stop_sequencer_error_fetching_unsafe_head(#[values(true, false)] via_channel: bool) { + let mut client = MockBlockBuildingClient::new(); + client + .expect_get_unsafe_head() + .times(1) + .return_once(|| Err(BlockEngineError::RequestError("whoops!".to_string()))); + + let mut actor = test_actor(); + actor.block_building_client = client; + + let result = async { + match via_channel { + false => actor.stop_sequencer().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::StopSequencer(tx)).await; + rx.await.unwrap() + } + } + } + .await; + assert!(result.is_err()); + + assert!(matches!( + result.unwrap_err(), + SequencerAdminAPIError::StopError(StopSequencerError::ErrorAfterSequencerWasStopped(_)) + )); + assert!(!actor.is_active); +} + +#[rstest] +#[tokio::test] +async fn test_set_recovery_mode( + #[values(true, false)] starting_mode: bool, + #[values(true, false)] mode_to_set: bool, + #[values(true, false)] via_channel: bool, +) { + let mut actor = test_actor(); + actor.in_recovery_mode = starting_mode; + + // verify starting state + let result = actor.in_recovery_mode().await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), starting_mode); + + // set recovery mode + let result = async { + match via_channel { + false => actor.set_recovery_mode(mode_to_set).await, + true => { + let (tx, rx) = oneshot::channel(); + actor + .handle_admin_query(SequencerAdminQuery::SetRecoveryMode(mode_to_set, tx)) + .await; + rx.await.unwrap() + } + } + } + .await; + assert!(result.is_ok()); + + // verify it is set + let result = actor.in_recovery_mode().await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), mode_to_set); +} + +#[rstest] +#[tokio::test] +async fn test_override_leader( + #[values(true, false)] conductor_configured: bool, + #[values(true, false)] conductor_error: bool, + #[values(true, false)] via_channel: bool, +) { + // mock error string returned by conductor, if configured (to differentiate between error + // returned if not configured) + let conductor_error_string = "test: error within conductor"; + + let mut actor = { + // wire up conductor absence/presence and response error/success + if !conductor_configured { + test_actor() + } else if conductor_error { + let mut conductor = MockConductor::new(); + conductor.expect_override_leader().times(1).return_once(move || { + Err(ConductorError::Rpc(RpcError::local_usage_str(conductor_error_string))) + }); + let mut actor = test_actor(); + actor.conductor = Some(conductor); + actor + } else { + let mut conductor = MockConductor::new(); + conductor.expect_override_leader().times(1).return_once(|| Ok(())); + let mut actor = test_actor(); + actor.conductor = Some(conductor); + actor + } + }; + + // call to override leader + let result = async { + match via_channel { + false => actor.override_leader().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::OverrideLeader(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + // verify result + if !conductor_configured || conductor_error { + assert!(result.is_err()); + assert_eq!( + conductor_configured, + result.err().unwrap().to_string().contains(conductor_error_string) + ); + } else { + assert!(result.is_ok()) + } +} + +#[rstest] +#[tokio::test] +async fn test_reset_derivation_pipeline_success(#[values(true, false)] via_channel: bool) { + let mut client = MockBlockBuildingClient::new(); + client.expect_reset_engine_forkchoice().times(1).return_once(|| Ok(())); + + let mut actor = test_actor(); + actor.block_building_client = client; + + let result = async { + match via_channel { + false => actor.reset_derivation_pipeline().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::ResetDerivationPipeline(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + assert!(result.is_ok()); +} + +#[rstest] +#[tokio::test] +async fn test_reset_derivation_pipeline_error(#[values(true, false)] via_channel: bool) { + let mut client = MockBlockBuildingClient::new(); + client + .expect_reset_engine_forkchoice() + .times(1) + .return_once(|| Err(BlockEngineError::RequestError("reset failed".to_string()))); + + let mut actor = test_actor(); + actor.block_building_client = client; + + let result = async { + match via_channel { + false => actor.reset_derivation_pipeline().await, + true => { + let (tx, rx) = oneshot::channel(); + actor.handle_admin_query(SequencerAdminQuery::ResetDerivationPipeline(tx)).await; + rx.await.unwrap() + } + } + } + .await; + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Failed to reset engine")); +} + +#[rstest] +#[tokio::test] +async fn test_handle_admin_query_resilient_to_dropped_receiver() { + let mut conductor = MockConductor::new(); + conductor.expect_override_leader().times(1).returning(|| Ok(())); + + let unsafe_head = L2BlockInfo { + block_info: BlockInfo { hash: B256::from([1u8; 32]), ..Default::default() }, + ..Default::default() + }; + let mut client = MockBlockBuildingClient::new(); + client.expect_get_unsafe_head().times(1).returning(move || Ok(unsafe_head)); + client.expect_reset_engine_forkchoice().times(1).returning(|| Ok(())); + + let mut actor = test_actor(); + actor.conductor = Some(conductor); + actor.block_building_client = client; + + let mut queries: Vec = Vec::new(); + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::SequencerActive(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::StartSequencer(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::StopSequencer(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::ConductorEnabled(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::RecoveryMode(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::SetRecoveryMode(true, tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::OverrideLeader(tx)); + } + { + // immediately drop receiver + let (tx, _rx) = oneshot::channel(); + queries.push(SequencerAdminQuery::ResetDerivationPipeline(tx)); + } + + // None of these should fail even if the receiver is dropped + for query in queries { + actor.handle_admin_query(query).await; + } +} diff --git a/kona/crates/node/service/src/actors/sequencer/conductor.rs b/kona/crates/node/service/src/actors/sequencer/conductor.rs new file mode 100644 index 0000000000000..e62c8ba22d8eb --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/conductor.rs @@ -0,0 +1,72 @@ +use alloy_rpc_client::ReqwestClient; +use alloy_transport::{RpcError, TransportErrorKind}; +use async_trait::async_trait; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use std::fmt::Debug; +use url::Url; + +/// Trait for interacting with the conductor service. +/// +/// The conductor service is responsible for coordinating sequencer behavior +/// in a high-availability setup with leader election. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait Conductor: Debug + Send + Sync { + /// Commit an unsafe payload to the conductor. + async fn commit_unsafe_payload( + &self, + payload: &OpExecutionPayloadEnvelope, + ) -> Result<(), ConductorError>; + + /// Override the leader of the conductor. + async fn override_leader(&self) -> Result<(), ConductorError>; +} + +/// A client for communicating with the conductor service via RPC +#[derive(Debug, Clone)] +pub struct ConductorClient { + /// The inner RPC provider + rpc: ReqwestClient, +} + +#[async_trait] +impl Conductor for ConductorClient { + /// Commit an unsafe payload to the conductor. + async fn commit_unsafe_payload( + &self, + payload: &OpExecutionPayloadEnvelope, + ) -> Result<(), ConductorError> { + self.rpc.request("conductor_commitUnsafePayload", [payload]).await.map_err(Into::into) + } + + /// Override the leader of the conductor. + async fn override_leader(&self) -> Result<(), ConductorError> { + self.rpc.request("conductor_overrideLeader", ()).await.map_err(Into::into) + } +} + +impl ConductorClient { + /// Creates a new conductor client using HTTP transport + pub fn new_http(url: Url) -> Self { + let rpc = ReqwestClient::new_http(url); + Self { rpc } + } + + /// Check if the node is a leader of the conductor. + pub async fn leader(&self) -> Result { + self.rpc.request("conductor_leader", ()).await.map_err(Into::into) + } + + /// Check if the conductor is active. + pub async fn conductor_active(&self) -> Result { + self.rpc.request("conductor_active", ()).await.map_err(Into::into) + } +} + +/// Error type for conductor operations +#[derive(Debug, thiserror::Error)] +pub enum ConductorError { + /// An error occurred while making an RPC call to the conductor. + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), +} diff --git a/kona/crates/node/service/src/actors/sequencer/config.rs b/kona/crates/node/service/src/actors/sequencer/config.rs new file mode 100644 index 0000000000000..ae171ee13965a --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/config.rs @@ -0,0 +1,20 @@ +//! Configuration for the [`SequencerActor`]. +//! +//! [`SequencerActor`]: super::SequencerActor + +use url::Url; + +/// Configuration for the [`SequencerActor`]. +/// +/// [`SequencerActor`]: super::SequencerActor +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct SequencerConfig { + /// Whether or not the sequencer is enabled at startup. + pub sequencer_stopped: bool, + /// Whether or not the sequencer is in recovery mode. + pub sequencer_recovery_mode: bool, + /// The [`Url`] for the conductor RPC endpoint. If [`Some`], enables the conductor service. + pub conductor_rpc_url: Option, + /// The confirmation delay for the sequencer. + pub l1_conf_delay: u64, +} diff --git a/kona/crates/node/service/src/actors/sequencer/error.rs b/kona/crates/node/service/src/actors/sequencer/error.rs new file mode 100644 index 0000000000000..70f1cbf5c4ed9 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/error.rs @@ -0,0 +1,28 @@ +use crate::{ + L1OriginSelectorError, UnsafePayloadGossipClientError, actors::engine::BlockEngineError, +}; +use kona_derive::PipelineErrorKind; +use kona_engine::BuildTaskError; + +/// An error produced by the [`crate::SequencerActor`]. +#[derive(Debug, thiserror::Error)] +pub enum SequencerActorError { + /// An error occurred while building payload attributes. + #[error(transparent)] + AttributesBuilder(#[from] PipelineErrorKind), + /// A channel was unexpectedly closed. + #[error("Channel closed unexpectedly")] + ChannelClosed, + /// An error occurred while selecting the next L1 origin. + #[error(transparent)] + L1OriginSelector(#[from] L1OriginSelectorError), + /// An error occurred while attempting to seal a payload. + #[error(transparent)] + BlockEngine(#[from] BlockEngineError), + /// An error occurred while attempting to build a payload. + #[error(transparent)] + BuildError(#[from] BuildTaskError), + /// An error occurred while attempting to schedule unsafe payload gossip. + #[error("An error occurred while attempting to schedule unsafe payload gossip: {0}")] + PayloadGossip(#[from] UnsafePayloadGossipClientError), +} diff --git a/kona/crates/node/service/src/actors/sequencer/metrics.rs b/kona/crates/node/service/src/actors/sequencer/metrics.rs new file mode 100644 index 0000000000000..5f242e128a048 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/metrics.rs @@ -0,0 +1,70 @@ +use std::time::Duration; + +use crate::{ + BlockBuildingClient, Conductor, OriginSelector, SequencerActor, UnsafePayloadGossipClient, +}; +use kona_derive::AttributesBuilder; + +/// SequencerActor metrics-related method implementations. +impl< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, +> + SequencerActor< + AttributesBuilder_, + BlockBuildingClient_, + Conductor_, + OriginSelector_, + UnsafePayloadGossipClient_, + > +where + AttributesBuilder_: AttributesBuilder, + BlockBuildingClient_: BlockBuildingClient, + Conductor_: Conductor, + OriginSelector_: OriginSelector, + UnsafePayloadGossipClient_: UnsafePayloadGossipClient, +{ + /// Updates the metrics for the sequencer actor. + pub(super) fn update_metrics(&self) { + // no-op if disabled. + #[cfg(feature = "metrics")] + { + let state_flags: [(&str, String); 2] = [ + ("active", self.is_active.to_string()), + ("recovery", self.in_recovery_mode.to_string()), + ]; + + let gauge = metrics::gauge!(crate::Metrics::SEQUENCER_STATE, &state_flags); + gauge.set(1); + } + } +} + +#[inline] +pub(super) fn update_attributes_build_duration_metrics(duration: Duration) { + // Log the attributes build duration, if metrics are enabled. + kona_macros::set!(gauge, crate::Metrics::SEQUENCER_ATTRIBUTES_BUILDER_DURATION, duration); +} + +#[inline] +pub(super) fn update_conductor_commitment_duration_metrics(duration: Duration) { + kona_macros::set!(gauge, crate::Metrics::SEQUENCER_CONDUCTOR_COMMITMENT_DURATION, duration); +} + +#[inline] +pub(super) fn update_block_build_duration_metrics(duration: Duration) { + kona_macros::set!( + gauge, + crate::Metrics::SEQUENCER_BLOCK_BUILDING_START_TASK_DURATION, + duration + ); +} + +#[inline] +pub(super) fn update_seal_duration_metrics(duration: Duration) { + // Log the block building seal task duration, if metrics are enabled. + kona_macros::set!(gauge, crate::Metrics::SEQUENCER_BLOCK_BUILDING_SEAL_TASK_DURATION, duration); +} diff --git a/kona/crates/node/service/src/actors/sequencer/mod.rs b/kona/crates/node/service/src/actors/sequencer/mod.rs new file mode 100644 index 0000000000000..4970ceae20749 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/mod.rs @@ -0,0 +1,36 @@ +//! The `SequencerActor` and its components. + +mod config; +pub use config::SequencerConfig; + +mod origin_selector; +pub use origin_selector::{ + DelayedL1OriginSelectorProvider, L1OriginSelector, L1OriginSelectorError, + L1OriginSelectorProvider, OriginSelector, +}; + +mod actor; +pub use actor::SequencerActor; + +mod admin_api_client; +pub use admin_api_client::{QueuedSequencerAdminAPIClient, SequencerAdminQuery}; + +mod admin_api_impl; + +mod metrics; + +mod error; +pub use error::SequencerActorError; + +mod conductor; + +pub use conductor::{Conductor, ConductorClient, ConductorError}; + +#[cfg(test)] +pub use conductor::MockConductor; + +#[cfg(test)] +pub use origin_selector::MockOriginSelector; + +#[cfg(test)] +mod admin_api_impl_test; diff --git a/kona/crates/node/service/src/actors/sequencer/origin_selector.rs b/kona/crates/node/service/src/actors/sequencer/origin_selector.rs new file mode 100644 index 0000000000000..e2e70a1f3c7b3 --- /dev/null +++ b/kona/crates/node/service/src/actors/sequencer/origin_selector.rs @@ -0,0 +1,497 @@ +//! The [`L1OriginSelector`]. + +use alloy_primitives::B256; +use alloy_provider::{Provider, RootProvider}; +use alloy_transport::{RpcError, TransportErrorKind}; +use async_trait::async_trait; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo}; +use std::{fmt::Debug, sync::Arc}; +use tokio::sync::watch; + +/// Trait for selecting the next L1 origin block for sequencing. +/// +/// This trait is used by the sequencer to determine which L1 block should be used +/// as the origin for the next L2 block being built. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait OriginSelector: Debug + Send + Sync { + /// Selects the next L1 origin block for sequencing. + /// + /// # Arguments + /// * `unsafe_head` - The current unsafe head of the L2 chain + /// * `is_recovery_mode` - Whether the sequencer is in recovery mode + /// + /// # Returns + /// The selected L1 origin block information, or an error if selection failed. + async fn next_l1_origin( + &mut self, + unsafe_head: L2BlockInfo, + is_recovery_mode: bool, + ) -> Result; +} + +/// The [`L1OriginSelector`] is responsible for selecting the L1 origin block based on the +/// current L2 unsafe head's sequence epoch. +#[derive(Debug)] +pub struct L1OriginSelector { + /// The [`RollupConfig`]. + cfg: Arc, + /// The [`L1OriginSelectorProvider`]. + l1: P, + /// The current L1 origin. + current: Option, + /// The next L1 origin. + next: Option, +} + +#[async_trait] +impl OriginSelector for L1OriginSelector

{ + /// Determines what the next L1 origin block should be, based off of the [`L2BlockInfo`] unsafe + /// head. + /// + /// The L1 origin is selected based off of the sequencing epoch, determined by the next L2 + /// block's timestamp in relation to the current L1 origin's timestamp. If the next L2 + /// block's timestamp is greater than the L2 unsafe head's L1 origin timestamp, the L1 + /// origin is the block following the current L1 origin. + async fn next_l1_origin( + &mut self, + unsafe_head: L2BlockInfo, + is_recovery_mode: bool, + ) -> Result { + self.select_origins(&unsafe_head, is_recovery_mode).await?; + + // Start building on the next L1 origin block if the next L2 block's timestamp is + // greater than or equal to the next L1 origin's timestamp. + if let Some(next) = self.next { + if unsafe_head.block_info.timestamp + self.cfg.block_time >= next.timestamp { + return Ok(next); + } + } + + let Some(current) = self.current else { + unreachable!("Current L1 origin should always be set by `select_origins`"); + }; + + let max_seq_drift = self.cfg.max_sequencer_drift(current.timestamp); + let past_seq_drift = unsafe_head.block_info.timestamp + self.cfg.block_time - + current.timestamp > + max_seq_drift; + + // If the sequencer drift has not been exceeded, return the current L1 origin. + if !past_seq_drift { + return Ok(current); + } + + warn!( + target: "l1_origin_selector", + current_origin_time = current.timestamp, + unsafe_head_time = unsafe_head.block_info.timestamp, + max_seq_drift, + "Next L2 block time is past the sequencer drift" + ); + + if self + .next + .map(|n| unsafe_head.block_info.timestamp + self.cfg.block_time < n.timestamp) + .unwrap_or(false) + { + // If the next L1 origin is ahead of the next L2 block's timestamp, return the current + // origin. + return Ok(current); + } + + self.next.ok_or(L1OriginSelectorError::NotEnoughData(current)) + } +} + +impl L1OriginSelector

{ + /// Creates a new [`L1OriginSelector`]. + pub const fn new(cfg: Arc, l1: P) -> Self { + Self { cfg, l1, current: None, next: None } + } + + /// Returns the current L1 origin. + pub const fn current(&self) -> Option<&BlockInfo> { + self.current.as_ref() + } + + /// Returns the next L1 origin. + pub const fn next(&self) -> Option<&BlockInfo> { + self.next.as_ref() + } + + /// Selects the current and next L1 origin blocks based on the unsafe head. + async fn select_origins( + &mut self, + unsafe_head: &L2BlockInfo, + in_recovery_mode: bool, + ) -> Result<(), L1OriginSelectorError> { + if in_recovery_mode { + self.current = self.l1.get_block_by_hash(unsafe_head.l1_origin.hash).await?; + self.next = self.l1.get_block_by_number(unsafe_head.l1_origin.number + 1).await?; + return Ok(()); + } + + if self.current.map(|c| c.hash == unsafe_head.l1_origin.hash).unwrap_or(false) { + // Do nothing; The next L2 block exists in the same epoch as the current L1 origin. + } else if self.next.map(|n| n.hash == unsafe_head.l1_origin.hash).unwrap_or(false) { + // Advance the origin. + self.current = self.next.take(); + self.next = None; + } else { + // Find the current origin block, as it is missing. + let current = self.l1.get_block_by_hash(unsafe_head.l1_origin.hash).await?; + + self.current = current; + self.next = None; + } + + self.try_fetch_next_origin().await + } + + /// Attempts to fetch the next L1 origin block. + async fn try_fetch_next_origin(&mut self) -> Result<(), L1OriginSelectorError> { + // If there is no next L1 origin set, attempt to find it. If it's not yet available, leave + // it unset. + if let Some(current) = self.current.as_ref() { + // If the next L1 origin is already set, do nothing. + if self.next.is_some() { + return Ok(()); + } + + // If the next L1 origin is a logical extension of the current L1 chain, set it. + // + // Ignore the eventuality that the block is not found, as the next L1 origin fetch is + // performed on a best-effort basis. + let next = self.l1.get_block_by_number(current.number + 1).await?; + if next.map(|n| n.parent_hash == current.hash).unwrap_or(false) { + self.next = next; + } + } + + Ok(()) + } +} + +/// An error produced by the [`L1OriginSelector`]. +#[derive(Debug, thiserror::Error)] +pub enum L1OriginSelectorError { + /// An error produced by the [`RootProvider`]. + #[error(transparent)] + Provider(#[from] RpcError), + /// The L1 provider does not have enough data to select the next L1 origin block. + #[error( + "Waiting for more L1 data to be available to select the next L1 origin block. Current L1 origin: {0:?}" + )] + NotEnoughData(BlockInfo), +} + +/// L1 [`BlockInfo`] provider interface for the [`L1OriginSelector`]. +#[async_trait] +pub trait L1OriginSelectorProvider: Debug + Sync { + /// Returns a [`BlockInfo`] by its hash. + async fn get_block_by_hash( + &self, + hash: B256, + ) -> Result, L1OriginSelectorError>; + + /// Returns a [`BlockInfo`] by its number. + async fn get_block_by_number( + &self, + number: u64, + ) -> Result, L1OriginSelectorError>; +} + +/// A wrapper around the [`RootProvider`] that delays the view of the L1 chain by a configurable +/// amount of blocks. +#[derive(Debug)] +pub struct DelayedL1OriginSelectorProvider { + /// The inner [`RootProvider`]. + inner: RootProvider, + /// The L1 head watch channel. + l1_head: watch::Receiver>, + /// The confirmation depth to delay the view of the L1 chain. + confirmation_depth: u64, +} + +impl DelayedL1OriginSelectorProvider { + /// Creates a new [`DelayedL1OriginSelectorProvider`]. + pub const fn new( + inner: RootProvider, + l1_head: watch::Receiver>, + confirmation_depth: u64, + ) -> Self { + Self { inner, l1_head, confirmation_depth } + } +} + +#[async_trait] +impl L1OriginSelectorProvider for DelayedL1OriginSelectorProvider { + async fn get_block_by_hash( + &self, + hash: B256, + ) -> Result, L1OriginSelectorError> { + // By-hash lookups are not delayed, as they're direct indexes. + Ok(Provider::get_block_by_hash(&self.inner, hash).await?.map(Into::into)) + } + + async fn get_block_by_number( + &self, + number: u64, + ) -> Result, L1OriginSelectorError> { + let Some(l1_head) = *self.l1_head.borrow() else { + // If the L1 head is not available, do not enforce a confirmation delay. + return Ok(Provider::get_block_by_number(&self.inner, number.into()) + .await? + .map(Into::into)); + }; + + if number == 0 || + self.confirmation_depth == 0 || + number + self.confirmation_depth <= l1_head.number + { + Ok(Provider::get_block_by_number(&self.inner, number.into()).await?.map(Into::into)) + } else { + Ok(None) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_eips::NumHash; + use rstest::rstest; + use std::collections::HashSet; + + /// A mock [`OriginSelectorProvider`] with a local set of [`BlockInfo`]s available. + #[derive(Default, Debug, Clone)] + struct MockOriginSelectorProvider { + blocks: HashSet, + } + + impl MockOriginSelectorProvider { + /// Creates a new [`MockOriginSelectorProvider`]. + pub(crate) fn with_block(&mut self, block: BlockInfo) { + self.blocks.insert(block); + } + } + + #[async_trait] + impl L1OriginSelectorProvider for MockOriginSelectorProvider { + async fn get_block_by_hash( + &self, + hash: B256, + ) -> Result, L1OriginSelectorError> { + Ok(self.blocks.iter().find(|b| b.hash == hash).copied()) + } + + async fn get_block_by_number( + &self, + number: u64, + ) -> Result, L1OriginSelectorError> { + Ok(self.blocks.iter().find(|b| b.number == number).copied()) + } + } + + #[tokio::test] + #[rstest] + #[case::single_epoch(1)] + #[case::many_epochs(12)] + async fn test_next_l1_origin_several_epochs(#[case] num_epochs: usize) { + // Assume an L1 slot time of 12 seconds. + const L1_SLOT_TIME: u64 = 12; + // Assume an L2 block time of 2 seconds. + const L2_BLOCK_TIME: u64 = 2; + + // Initialize the rollup configuration with a block time of 2 seconds and a sequencer drift + // of 600 seconds. + let cfg = Arc::new(RollupConfig { + block_time: L2_BLOCK_TIME, + max_sequencer_drift: 600, + ..Default::default() + }); + + // Initialize the provider with mock L1 blocks, equal to the number of epochs + 1 + // (such that the next logical origin is always available.) + let mut provider = MockOriginSelectorProvider::default(); + for i in 0..num_epochs + 1 { + provider.with_block(BlockInfo { + parent_hash: B256::with_last_byte(i.saturating_sub(1) as u8), + hash: B256::with_last_byte(i as u8), + number: i as u64, + timestamp: i as u64 * L1_SLOT_TIME, + }); + } + + let mut selector = L1OriginSelector::new(cfg.clone(), provider); + + // Ensure all L1 origin blocks are produced correctly for each L2 block within all available + // epochs. + for i in 0..(num_epochs as u64 * (L1_SLOT_TIME / cfg.block_time)) { + let current_epoch = (i * cfg.block_time) / L1_SLOT_TIME; + let unsafe_head = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: i, + timestamp: i * cfg.block_time, + ..Default::default() + }, + l1_origin: NumHash { + number: current_epoch, + hash: B256::with_last_byte(current_epoch as u8), + }, + seq_num: 0, + }; + let next = selector.next_l1_origin(unsafe_head, false).await.unwrap(); + + // The expected L1 origin block is the one corresponding to the epoch of the current L2 + // block. + let expected_epoch = ((i + 1) * cfg.block_time) / L1_SLOT_TIME; + assert_eq!(next.hash, B256::with_last_byte(expected_epoch as u8)); + assert_eq!(next.number, expected_epoch); + } + } + + #[tokio::test] + #[rstest] + #[case::not_available(false)] + #[case::is_available(true)] + async fn test_next_l1_origin_next_maybe_available(#[case] next_l1_origin_available: bool) { + // Assume an L2 block time of 2 seconds. + const L2_BLOCK_TIME: u64 = 2; + + // Initialize the rollup configuration with a block time of 2 seconds and a sequencer drift + // of 600 seconds. + let cfg = Arc::new(RollupConfig { + block_time: L2_BLOCK_TIME, + max_sequencer_drift: 600, + ..Default::default() + }); + + // Initialize the provider with a single L1 block. + let mut provider = MockOriginSelectorProvider::default(); + provider.with_block(BlockInfo { + parent_hash: B256::ZERO, + hash: B256::ZERO, + number: 0, + timestamp: 0, + }); + + if next_l1_origin_available { + // If the next L1 origin is available, add it to the provider. + provider.with_block(BlockInfo { + parent_hash: B256::ZERO, + hash: B256::with_last_byte(1), + number: 1, + timestamp: cfg.block_time, + }); + } + + let mut selector = L1OriginSelector::new(cfg.clone(), provider); + + let current_epoch = 0; + let unsafe_head = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: 5, + timestamp: 5 * cfg.block_time, + ..Default::default() + }, + l1_origin: NumHash { + number: current_epoch, + hash: B256::with_last_byte(current_epoch as u8), + }, + seq_num: 0, + }; + let next = selector.next_l1_origin(unsafe_head, false).await.unwrap(); + + // The expected L1 origin block is the one corresponding to the epoch of the current L2 + // block. Assuming the next L1 origin block is not available from the eyes of the + // provider (_and_ it is not past the sequencer drift), the current L1 origin block + // will be re-used. + let expected_epoch = + if next_l1_origin_available { current_epoch + 1 } else { current_epoch }; + assert_eq!(next.hash, B256::with_last_byte(expected_epoch as u8)); + assert_eq!(next.number, expected_epoch); + } + + #[tokio::test] + #[rstest] + #[case::next_not_available(false, false)] + #[case::next_available_but_behind(true, false)] + #[case::next_available_and_ahead(true, true)] + async fn test_next_l1_origin_next_past_seq_drift( + #[case] next_available: bool, + #[case] next_ahead_of_unsafe: bool, + ) { + // Assume an L2 block time of 2 seconds. + const L2_BLOCK_TIME: u64 = 2; + + // Initialize the rollup configuration with a block time of 2 seconds and a sequencer drift + // of 600 seconds. + let cfg = Arc::new(RollupConfig { + block_time: L2_BLOCK_TIME, + max_sequencer_drift: 600, + ..Default::default() + }); + + // Initialize the provider with a single L1 block. + let mut provider = MockOriginSelectorProvider::default(); + provider.with_block(BlockInfo { + parent_hash: B256::ZERO, + hash: B256::ZERO, + number: 0, + timestamp: 0, + }); + + if next_available { + // If the next L1 origin is to be available, add it to the provider. + provider.with_block(BlockInfo { + parent_hash: B256::ZERO, + hash: B256::with_last_byte(1), + number: 1, + timestamp: if next_ahead_of_unsafe { + cfg.max_sequencer_drift + cfg.block_time * 2 + } else { + cfg.block_time + }, + }); + } + + let mut selector = L1OriginSelector::new(cfg.clone(), provider); + + let current_epoch = 0; + let unsafe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: cfg.max_sequencer_drift, ..Default::default() }, + l1_origin: NumHash { + number: current_epoch, + hash: B256::with_last_byte(current_epoch as u8), + }, + seq_num: 0, + }; + + if next_available { + if next_ahead_of_unsafe { + // If the next L1 origin is available and ahead of the unsafe head, the L1 origin + // should not change. + let next = selector.next_l1_origin(unsafe_head, false).await.unwrap(); + assert_eq!(next.hash, B256::ZERO); + assert_eq!(next.number, 0); + } else { + // If the next L1 origin is available and behind the unsafe head, the L1 origin + // should advance. + let next = selector.next_l1_origin(unsafe_head, false).await.unwrap(); + assert_eq!(next.hash, B256::with_last_byte(1)); + assert_eq!(next.number, 1); + } + } else { + // If we're past the sequencer drift, and the next L1 block is not available, a + // `NotEnoughData` error should be returned signifying that we cannot + // proceed with the next L1 origin until the block is present. + let next_err = selector.next_l1_origin(unsafe_head, false).await.unwrap_err(); + assert!(matches!(next_err, L1OriginSelectorError::NotEnoughData(_))); + } + } +} diff --git a/kona/crates/node/service/src/actors/traits.rs b/kona/crates/node/service/src/actors/traits.rs new file mode 100644 index 0000000000000..38fdf47d12630 --- /dev/null +++ b/kona/crates/node/service/src/actors/traits.rs @@ -0,0 +1,28 @@ +//! [NodeActor] trait. + +use async_trait::async_trait; +use tokio_util::sync::WaitForCancellationFuture; + +/// The communication context used by the actor. +pub trait CancellableContext: Send { + /// Returns a future that resolves when the actor is cancelled. + fn cancelled(&self) -> WaitForCancellationFuture<'_>; +} + +/// The [NodeActor] is an actor-like service for the node. +/// +/// Actors may: +/// - Handle incoming messages. +/// - Perform background tasks. +/// - Emit new events for other actors to process. +#[async_trait] +pub trait NodeActor: Send + 'static { + /// The error type for the actor. + type Error: std::fmt::Debug; + /// The type necessary to pass to the start function. + /// This is the result of + type StartData: Sized; + + /// Starts the actor. + async fn start(self, start_context: Self::StartData) -> Result<(), Self::Error>; +} diff --git a/kona/crates/node/service/src/lib.rs b/kona/crates/node/service/src/lib.rs new file mode 100644 index 0000000000000..35921d02914d6 --- /dev/null +++ b/kona/crates/node/service/src/lib.rs @@ -0,0 +1,40 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[macro_use] +extern crate tracing; + +mod service; +pub use service::{ + InteropMode, L1Config, L1ConfigBuilder, NodeMode, RollupNode, RollupNodeBuilder, +}; + +mod actors; +pub use actors::{ + BlockBuildingClient, BlockEngineError, BlockEngineResult, BlockStream, BuildRequest, + CancellableContext, Conductor, ConductorClient, ConductorError, + DelayedL1OriginSelectorProvider, DerivationActor, DerivationBuilder, DerivationContext, + DerivationError, DerivationInboundChannels, DerivationState, EngineActor, EngineConfig, + EngineContext, EngineError, EngineInboundData, InboundDerivationMessage, L1OriginSelector, + L1OriginSelectorError, L1OriginSelectorProvider, L1WatcherActor, L1WatcherActorError, + L2Finalizer, NetworkActor, NetworkActorError, NetworkBuilder, NetworkBuilderError, + NetworkConfig, NetworkContext, NetworkDriver, NetworkDriverError, NetworkHandler, + NetworkInboundData, NodeActor, OriginSelector, PipelineBuilder, QueuedBlockBuildingClient, + QueuedSequencerAdminAPIClient, QueuedUnsafePayloadGossipClient, ResetRequest, RpcActor, + RpcActorError, RpcContext, SealRequest, SequencerActor, SequencerActorError, + SequencerAdminQuery, SequencerConfig, UnsafePayloadGossipClient, + UnsafePayloadGossipClientError, +}; + +mod metrics; +pub use metrics::Metrics; + +#[cfg(test)] +pub use actors::{ + MockBlockBuildingClient, MockConductor, MockOriginSelector, MockUnsafePayloadGossipClient, +}; diff --git a/kona/crates/node/service/src/metrics/mod.rs b/kona/crates/node/service/src/metrics/mod.rs new file mode 100644 index 0000000000000..02b84b3b0781f --- /dev/null +++ b/kona/crates/node/service/src/metrics/mod.rs @@ -0,0 +1,100 @@ +//! Metrics for the node service + +/// Container for metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the counter that tracks the number of times the L1 has reorganized. + pub const L1_REORG_COUNT: &str = "kona_node_l1_reorg_count"; + + /// Identifier for the counter that tracks the L1 origin of the derivation pipeline. + pub const DERIVATION_L1_ORIGIN: &str = "kona_node_derivation_l1_origin"; + + /// Identifier for the counter of critical derivation errors (strictly for alerting.) + pub const DERIVATION_CRITICAL_ERROR: &str = "kona_node_derivation_critical_errors"; + + /// Identifier for the counter that tracks sequencer state flags. + pub const SEQUENCER_STATE: &str = "kona_node_sequencer_state"; + + /// Gauge for the sequencer's attributes builder duration. + pub const SEQUENCER_ATTRIBUTES_BUILDER_DURATION: &str = + "kona_node_sequencer_attributes_build_duration"; + + /// Gauge for the sequencer's block building start task duration. + pub const SEQUENCER_BLOCK_BUILDING_START_TASK_DURATION: &str = + "kona_node_sequencer_block_building_start_task_duration"; + + /// Gauge for the sequencer's block building seal task duration. + pub const SEQUENCER_BLOCK_BUILDING_SEAL_TASK_DURATION: &str = + "kona_node_sequencer_block_building_seal_task_duration"; + + /// Gauge for the sequencer's conductor commitment duration. + pub const SEQUENCER_CONDUCTOR_COMMITMENT_DURATION: &str = + "kona_node_sequencer_conductor_commitment_duration"; + + /// Initializes metrics for the node service. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in [`kona-node-service`][crate]. + #[cfg(feature = "metrics")] + pub fn describe() { + // L1 reorg count + metrics::describe_counter!(Self::L1_REORG_COUNT, metrics::Unit::Count, "L1 reorg count"); + + // Derivation L1 origin + metrics::describe_counter!(Self::DERIVATION_L1_ORIGIN, "Derivation pipeline L1 origin"); + + // Derivation critical error + metrics::describe_counter!( + Self::DERIVATION_CRITICAL_ERROR, + "Critical errors in the derivation pipeline" + ); + + // Sequencer state + metrics::describe_counter!(Self::SEQUENCER_STATE, "Tracks sequencer state flags"); + + // Sequencer attributes builder duration + metrics::describe_gauge!( + Self::SEQUENCER_ATTRIBUTES_BUILDER_DURATION, + "Duration of the sequencer attributes builder" + ); + + // Sequencer block building job duration + metrics::describe_gauge!( + Self::SEQUENCER_BLOCK_BUILDING_START_TASK_DURATION, + "Duration of the sequencer block building start task" + ); + + // Sequencer block building job duration + metrics::describe_gauge!( + Self::SEQUENCER_BLOCK_BUILDING_SEAL_TASK_DURATION, + "Duration of the sequencer block building seal task" + ); + + // Sequencer conductor commitment duration + metrics::describe_gauge!( + Self::SEQUENCER_CONDUCTOR_COMMITMENT_DURATION, + "Duration of the sequencer conductor commitment" + ); + } + + /// Initializes metrics to `0` so they can be queried immediately by consumers of prometheus + /// metrics. + #[cfg(feature = "metrics")] + pub fn zero() { + // L1 reorg reset count + kona_macros::set!(counter, Self::L1_REORG_COUNT, 0); + + // Derivation critical error + kona_macros::set!(counter, Self::DERIVATION_CRITICAL_ERROR, 0); + } +} diff --git a/kona/crates/node/service/src/service/builder.rs b/kona/crates/node/service/src/service/builder.rs new file mode 100644 index 0000000000000..c4412234149a5 --- /dev/null +++ b/kona/crates/node/service/src/service/builder.rs @@ -0,0 +1,150 @@ +//! Contains the builder for the [`RollupNode`]. + +use crate::{ + EngineConfig, InteropMode, NetworkConfig, RollupNode, SequencerConfig, service::node::L1Config, +}; +use alloy_primitives::Bytes; +use alloy_provider::RootProvider; +use alloy_rpc_client::RpcClient; +use alloy_transport_http::{ + AuthLayer, Http, HyperClient, + hyper_util::{client::legacy::Client, rt::TokioExecutor}, +}; +use http_body_util::Full; +use op_alloy_network::Optimism; +use std::sync::Arc; +use tower::ServiceBuilder; +use url::Url; + +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_providers_alloy::OnlineBeaconClient; +use kona_rpc::RpcBuilder; + +/// The [`L1ConfigBuilder`] is used to construct a [`L1Config`]. +#[derive(Debug)] +pub struct L1ConfigBuilder { + /// The L1 chain configuration. + pub chain_config: L1ChainConfig, + /// Whether to trust the L1 RPC. + pub trust_rpc: bool, + /// The L1 beacon API. + pub beacon: Url, + /// The L1 RPC URL. + pub rpc_url: Url, + /// The duration in seconds of an L1 slot. This can be used to hardcode a fixed slot + /// duration if the l1-beacon's slot configuration is not available. + pub slot_duration_override: Option, +} + +/// The [`RollupNodeBuilder`] is used to construct a [`RollupNode`] service. +#[derive(Debug)] +pub struct RollupNodeBuilder { + /// The rollup configuration. + pub config: RollupConfig, + /// The L1 chain configuration. + pub l1_config_builder: L1ConfigBuilder, + /// Whether to trust the L2 RPC. + pub l2_trust_rpc: bool, + /// Engine builder configuration. + pub engine_config: EngineConfig, + /// The [`NetworkConfig`]. + pub p2p_config: NetworkConfig, + /// An RPC Configuration. + pub rpc_config: Option, + /// The [`SequencerConfig`]. + pub sequencer_config: Option, + /// Whether to run the node in interop mode. + pub interop_mode: InteropMode, +} + +impl RollupNodeBuilder { + /// Creates a new [`RollupNodeBuilder`] with the given [`RollupConfig`]. + pub fn new( + config: RollupConfig, + l1_config_builder: L1ConfigBuilder, + l2_trust_rpc: bool, + engine_config: EngineConfig, + p2p_config: NetworkConfig, + rpc_config: Option, + ) -> Self { + Self { + config, + l1_config_builder, + l2_trust_rpc, + engine_config, + p2p_config, + rpc_config, + interop_mode: InteropMode::default(), + sequencer_config: None, + } + } + + /// Sets the [`EngineConfig`] on the [`RollupNodeBuilder`]. + pub fn with_engine_config(self, engine_config: EngineConfig) -> Self { + Self { engine_config, ..self } + } + + /// Sets the [`RpcBuilder`] on the [`RollupNodeBuilder`]. + pub fn with_rpc_config(self, rpc_config: Option) -> Self { + Self { rpc_config, ..self } + } + + /// Appends the [`SequencerConfig`] to the builder. + pub fn with_sequencer_config(self, sequencer_config: SequencerConfig) -> Self { + Self { sequencer_config: Some(sequencer_config), ..self } + } + + /// Assembles the [`RollupNode`] service. + /// + /// ## Panics + /// + /// Panics if: + /// - The L1 provider RPC URL is not set. + /// - The L1 beacon API URL is not set. + /// - The L2 provider RPC URL is not set. + /// - The L2 engine URL is not set. + /// - The jwt secret is not set. + /// - The P2P config is not set. + /// - The rollup boost args are not set. + pub fn build(self) -> RollupNode { + let mut l1_beacon = OnlineBeaconClient::new_http(self.l1_config_builder.beacon.to_string()); + if let Some(l1_slot_duration) = self.l1_config_builder.slot_duration_override { + l1_beacon = l1_beacon.with_l1_slot_duration_override(l1_slot_duration); + } + + let l1_config = L1Config { + chain_config: Arc::new(self.l1_config_builder.chain_config), + trust_rpc: self.l1_config_builder.trust_rpc, + beacon_client: l1_beacon, + engine_provider: RootProvider::new_http(self.l1_config_builder.rpc_url.clone()), + }; + + let jwt_secret = self.engine_config.l2_jwt_secret; + let hyper_client = Client::builder(TokioExecutor::new()).build_http::>(); + + let auth_layer = AuthLayer::new(jwt_secret); + let service = ServiceBuilder::new().layer(auth_layer).service(hyper_client); + + let layer_transport = HyperClient::with_service(service); + let http_hyper = Http::with_client(layer_transport, self.engine_config.l2_url.clone()); + let rpc_client = RpcClient::new(http_hyper, false); + let l2_provider = RootProvider::::new(rpc_client); + + let rollup_config = Arc::new(self.config); + + let p2p_config = self.p2p_config; + let sequencer_config = self.sequencer_config.unwrap_or_default(); + + RollupNode { + config: rollup_config, + l1_config, + interop_mode: self.interop_mode, + l2_provider, + l2_trust_rpc: self.l2_trust_rpc, + engine_config: self.engine_config, + rpc_builder: self.rpc_config, + p2p_config, + sequencer_config, + } + } +} diff --git a/kona/crates/node/service/src/service/mod.rs b/kona/crates/node/service/src/service/mod.rs new file mode 100644 index 0000000000000..101fce8d62f99 --- /dev/null +++ b/kona/crates/node/service/src/service/mod.rs @@ -0,0 +1,16 @@ +//! Core [`RollupNode`] service, composing the available [`NodeActor`]s into various modes of +//! operation. +//! +//! [`NodeActor`]: crate::NodeActor + +mod builder; +pub use builder::{L1ConfigBuilder, RollupNodeBuilder}; + +mod mode; +pub use mode::{InteropMode, NodeMode}; + +mod node; +pub use node::{L1Config, RollupNode}; + +pub(crate) mod util; +pub(crate) use util::spawn_and_wait; diff --git a/kona/crates/node/service/src/service/mode.rs b/kona/crates/node/service/src/service/mode.rs new file mode 100644 index 0000000000000..4dc02222475c4 --- /dev/null +++ b/kona/crates/node/service/src/service/mode.rs @@ -0,0 +1,49 @@ +//! Contains enums that configure the mode for the node to operate in. + +/// The [`NodeMode`] enum represents the modes of operation for the [`RollupNode`]. +/// +/// [`RollupNode`]: crate::RollupNode +#[derive( + Debug, + Default, + Clone, + Copy, + PartialEq, + Eq, + derive_more::Display, + derive_more::FromStr, + strum::EnumIter, +)] +pub enum NodeMode { + /// Validator mode. + #[display("Validator")] + #[default] + Validator, + /// Sequencer mode. + #[display("Sequencer")] + Sequencer, +} + +impl NodeMode { + /// Returns `true` if [`Self`] is [`Self::Validator`]. + pub const fn is_validator(&self) -> bool { + matches!(self, Self::Validator) + } + + /// Returns `true` if [`Self`] is [`Self::Sequencer`]. + pub const fn is_sequencer(&self) -> bool { + matches!(self, Self::Sequencer) + } +} + +/// The [`InteropMode`] enum represents how the node works with interop. +#[derive(Debug, derive_more::Display, Default, Clone, Copy, PartialEq, Eq)] +pub enum InteropMode { + /// The node is in polled mode. + #[display("Polled")] + #[default] + Polled, + /// The node is in indexed mode. + #[display("Indexed")] + Indexed, +} diff --git a/kona/crates/node/service/src/service/node.rs b/kona/crates/node/service/src/service/node.rs new file mode 100644 index 0000000000000..c32a0c172eea4 --- /dev/null +++ b/kona/crates/node/service/src/service/node.rs @@ -0,0 +1,316 @@ +//! Contains the [`RollupNode`] implementation. +use crate::{ + ConductorClient, DelayedL1OriginSelectorProvider, DerivationActor, DerivationBuilder, + DerivationContext, EngineActor, EngineConfig, EngineContext, InteropMode, L1OriginSelector, + L1WatcherActor, NetworkActor, NetworkBuilder, NetworkConfig, NetworkContext, NodeActor, + NodeMode, QueuedBlockBuildingClient, QueuedSequencerAdminAPIClient, RpcActor, RpcContext, + SequencerActor, SequencerConfig, + actors::{ + BlockStream, DerivationInboundChannels, EngineInboundData, NetworkInboundData, + QueuedUnsafePayloadGossipClient, + }, +}; +use alloy_eips::BlockNumberOrTag; +use alloy_provider::RootProvider; +use kona_derive::StatefulAttributesBuilder; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_providers_alloy::{AlloyChainProvider, AlloyL2ChainProvider, OnlineBeaconClient}; +use kona_rpc::RpcBuilder; +use op_alloy_network::Optimism; +use std::{ops::Not as _, sync::Arc, time::Duration}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +const DERIVATION_PROVIDER_CACHE_SIZE: usize = 1024; +const HEAD_STREAM_POLL_INTERVAL: u64 = 4; +const FINALIZED_STREAM_POLL_INTERVAL: u64 = 60; + +/// The configuration for the L1 chain. +#[derive(Debug, Clone)] +pub struct L1Config { + /// The L1 chain configuration. + pub chain_config: Arc, + /// Whether to trust the L1 RPC. + pub trust_rpc: bool, + /// The L1 beacon client. + pub beacon_client: OnlineBeaconClient, + /// The L1 engine provider. + pub engine_provider: RootProvider, +} + +/// The standard implementation of the [RollupNode] service, using the governance approved OP Stack +/// configuration of components. +#[derive(Debug)] +pub struct RollupNode { + /// The rollup configuration. + pub(crate) config: Arc, + /// The L1 configuration. + pub(crate) l1_config: L1Config, + /// The interop mode for the node. + pub(crate) interop_mode: InteropMode, + /// The L2 EL provider. + pub(crate) l2_provider: RootProvider, + /// Whether to trust the L2 RPC. + pub(crate) l2_trust_rpc: bool, + /// The [`EngineConfig`] for the node. + pub(crate) engine_config: EngineConfig, + /// The [`RpcBuilder`] for the node. + pub(crate) rpc_builder: Option, + /// The P2P [`NetworkConfig`] for the node. + pub(crate) p2p_config: NetworkConfig, + /// The [`SequencerConfig`] for the node. + pub(crate) sequencer_config: SequencerConfig, +} + +impl RollupNode { + /// The mode of operation for the node. + const fn mode(&self) -> NodeMode { + self.engine_config.mode + } + + /// Returns a derivation builder for the node. + fn derivation_builder(&self) -> DerivationBuilder { + DerivationBuilder { + l1_provider: self.l1_config.engine_provider.clone(), + l1_trust_rpc: self.l1_config.trust_rpc, + l1_beacon: self.l1_config.beacon_client.clone(), + l2_provider: self.l2_provider.clone(), + l2_trust_rpc: self.l2_trust_rpc, + rollup_config: self.config.clone(), + l1_config: self.l1_config.chain_config.clone(), + interop_mode: self.interop_mode, + } + } + + /// Creates a network builder for the node. + fn network_builder(&self) -> NetworkBuilder { + NetworkBuilder::from(self.p2p_config.clone()) + } + + /// Returns an engine builder for the node. + fn engine_config(&self) -> EngineConfig { + self.engine_config.clone() + } + + /// Returns an rpc builder for the node. + fn rpc_builder(&self) -> Option { + self.rpc_builder.clone() + } + + /// Returns the sequencer builder for the node. + fn create_attributes_builder( + &self, + ) -> StatefulAttributesBuilder { + let l1_derivation_provider = AlloyChainProvider::new_with_trust( + self.l1_config.engine_provider.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l1_config.trust_rpc, + ); + let l2_derivation_provider = AlloyL2ChainProvider::new_with_trust( + self.l2_provider.clone(), + self.config.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l2_trust_rpc, + ); + + StatefulAttributesBuilder::new( + self.config.clone(), + self.l1_config.chain_config.clone(), + l2_derivation_provider, + l1_derivation_provider, + ) + } + + /// Starts the rollup node service. + /// + /// The rollup node, in validator mode, listens to two sources of information to sync the L2 + /// chain: + /// + /// 1. The data availability layer, with a watcher that listens for new updates. L2 inputs (L2 + /// transaction batches + deposits) are then derived from the DA layer. + /// 2. The L2 sequencer, which produces unsafe L2 blocks and sends them to the network over p2p + /// gossip. + /// + /// From these two sources, the node imports `unsafe` blocks from the L2 sequencer, `safe` + /// blocks from the L2 derivation pipeline into the L2 execution layer via the Engine API, + /// and finalizes `safe` blocks that it has derived when L1 finalized block updates are + /// received. + /// + /// In sequencer mode, the node is responsible for producing unsafe L2 blocks and sending them + /// to the network over p2p gossip. The node also listens for L1 finalized block updates and + /// finalizes `safe` blocks that it has derived when L1 finalized block updates are + /// received. + pub async fn start(&self) -> Result<(), String> { + // Create a global cancellation token for graceful shutdown of tasks. + let cancellation = CancellationToken::new(); + + // Create the derivation actor. + let ( + DerivationInboundChannels { + derivation_signal_tx, + l1_head_updates_tx, + engine_l2_safe_head_tx, + el_sync_complete_tx, + }, + derivation, + ) = DerivationActor::new(self.derivation_builder()); + + // Create the engine actor. + let ( + EngineInboundData { + attributes_tx, + build_request_tx, + finalized_l1_block_tx, + inbound_queries_tx: engine_rpc, + reset_request_tx, + rollup_boost_admin_query_tx: rollup_boost_admin_rpc, + rollup_boost_health_query_tx: rollup_boost_health_rpc, + seal_request_tx, + unsafe_block_tx, + unsafe_head_rx, + }, + engine, + ) = EngineActor::new(self.engine_config()); + + // Create the p2p actor. + let ( + NetworkInboundData { + signer, + p2p_rpc: network_rpc, + gossip_payload_tx, + admin_rpc: net_admin_rpc, + }, + network, + ) = NetworkActor::new(self.network_builder()); + + // Create the RPC server actor. + let rpc = self.rpc_builder().map(RpcActor::new); + + let delayed_l1_provider = DelayedL1OriginSelectorProvider::new( + self.l1_config.engine_provider.clone(), + l1_head_updates_tx.subscribe(), + self.sequencer_config.l1_conf_delay, + ); + + let delayed_origin_selector = + L1OriginSelector::new(self.config.clone(), delayed_l1_provider); + + // Conditionally add conductor if configured + let conductor = + self.sequencer_config.conductor_rpc_url.clone().map(ConductorClient::new_http); + + // Create the L1 Watcher actor + + // A channel to send queries about the state of L1. + let (l1_query_tx, l1_query_rx) = mpsc::channel(1024); + + let head_stream = BlockStream::new_as_stream( + self.l1_config.engine_provider.clone(), + BlockNumberOrTag::Latest, + Duration::from_secs(HEAD_STREAM_POLL_INTERVAL), + )?; + let finalized_stream = BlockStream::new_as_stream( + self.l1_config.engine_provider.clone(), + BlockNumberOrTag::Finalized, + Duration::from_secs(FINALIZED_STREAM_POLL_INTERVAL), + )?; + + // Create the [`L1WatcherActor`]. Previously known as the DA watcher actor. + let l1_watcher = L1WatcherActor::new( + self.config.clone(), + self.l1_config.engine_provider.clone(), + l1_query_rx, + l1_head_updates_tx.clone(), + finalized_l1_block_tx.clone(), + signer, + cancellation.clone(), + head_stream, + finalized_stream, + ); + + // Create the sequencer if needed + let (sequencer_actor, sequencer_admin_api_tx) = if self.mode().is_sequencer() { + let block_building_client = QueuedBlockBuildingClient { + build_request_tx: build_request_tx.ok_or( + "build_request_tx is None in sequencer mode. This should never happen." + .to_string(), + )?, + reset_request_tx: reset_request_tx.clone(), + seal_request_tx: seal_request_tx.ok_or( + "seal_request_tx is None in sequencer mode. This should never happen." + .to_string(), + )?, + unsafe_head_rx: unsafe_head_rx.ok_or( + "unsafe_head_rx is None in sequencer mode. This should never happen." + .to_string(), + )?, + }; + + // Create the admin API channel + let (sequencer_admin_api_tx, sequencer_admin_api_rx) = mpsc::channel(1024); + let queued_gossip_client = + QueuedUnsafePayloadGossipClient::new(gossip_payload_tx.clone()); + + ( + Some(SequencerActor { + admin_api_rx: sequencer_admin_api_rx, + attributes_builder: self.create_attributes_builder(), + block_building_client, + cancellation_token: cancellation.clone(), + conductor, + is_active: self.sequencer_config.sequencer_stopped.not(), + in_recovery_mode: self.sequencer_config.sequencer_recovery_mode, + origin_selector: delayed_origin_selector, + rollup_config: self.config.clone(), + unsafe_payload_gossip_client: queued_gossip_client, + }), + Some(QueuedSequencerAdminAPIClient::new(sequencer_admin_api_tx)), + ) + } else { + (None, None) + }; + + crate::service::spawn_and_wait!( + cancellation, + actors = [ + rpc.map(|r| ( + r, + RpcContext { + cancellation: cancellation.clone(), + p2p_network: network_rpc, + network_admin: net_admin_rpc, + sequencer_admin: sequencer_admin_api_tx, + l1_watcher_queries: l1_query_tx, + engine_query: engine_rpc, + rollup_boost_admin: rollup_boost_admin_rpc, + rollup_boost_health: rollup_boost_health_rpc, + } + )), + sequencer_actor.map(|s| (s, ())), + Some(( + network, + NetworkContext { blocks: unsafe_block_tx, cancellation: cancellation.clone() } + )), + Some((l1_watcher, ())), + Some(( + derivation, + DerivationContext { + reset_request_tx: reset_request_tx.clone(), + derived_attributes_tx: attributes_tx, + cancellation: cancellation.clone(), + } + )), + Some(( + engine, + EngineContext { + engine_l2_safe_head_tx, + sync_complete_tx: el_sync_complete_tx, + derivation_signal_tx, + cancellation: cancellation.clone(), + } + )), + ] + ); + Ok(()) + } +} diff --git a/kona/crates/node/service/src/service/util.rs b/kona/crates/node/service/src/service/util.rs new file mode 100644 index 0000000000000..4b8f278c7d9e0 --- /dev/null +++ b/kona/crates/node/service/src/service/util.rs @@ -0,0 +1,59 @@ +//! Utilities for the rollup node service, internal to the crate. + +/// Spawns a set of parallel actors in a [JoinSet], and cancels all actors if any of them fail. The +/// type of the error in the [NodeActor]s is erased to avoid having to specify a common error type +/// between actors. +/// +/// Actors are passed in as optional arguments, in case a given actor is not needed. +/// +/// [JoinSet]: tokio::task::JoinSet +/// [NodeActor]: crate::NodeActor +macro_rules! spawn_and_wait { + ($cancellation:expr, actors = [$($actor:expr$(,)?)*]) => { + let mut task_handles = tokio::task::JoinSet::new(); + + // Check if the actor is present, and spawn it if it is. + $( + if let Some((actor, context)) = $actor { + let cancellation = $cancellation.clone(); + task_handles.spawn(async move { + // This guard ensures that the cancellation token is cancelled when the actor is + // dropped. This ensures that the actor is properly shut down. + // Note the underscore prefix: this is to signal that we don't use the guard anywhere, but + // *the compiler shouldn't optimize it away*. + // Note that using a simple `_` would not work here because it gets optimized away in + // release mode. + let _guard = cancellation.drop_guard(); + + if let Err(e) = actor.start(context).await { + return Err(format!("{e:?}")); + } + Ok(()) + }); + } + )* + + while let Some(result) = task_handles.join_next().await { + match result { + Ok(Ok(())) => { /* Actor completed successfully */ } + Ok(Err(e)) => { + tracing::error!(target: "rollup_node", "Critical error in sub-routine: {e}"); + // Cancel all tasks and gracefully shutdown. + $cancellation.cancel(); + return Err(e); + } + Err(e) => { + let error_msg = format!("Task join error: {e}"); + // Log the error and cancel all tasks. + tracing::error!(target: "rollup_node", "Task join error: {e}"); + // Cancel all tasks and gracefully shutdown. + $cancellation.cancel(); + return Err(error_msg); + } + } + } + }; +} + +// Export the `spawn_and_wait` macro for use in other modules. +pub(crate) use spawn_and_wait; diff --git a/kona/crates/node/service/tests/actors/generator/block_builder.rs b/kona/crates/node/service/tests/actors/generator/block_builder.rs new file mode 100644 index 0000000000000..c3390bacf5366 --- /dev/null +++ b/kona/crates/node/service/tests/actors/generator/block_builder.rs @@ -0,0 +1,135 @@ +use std::time::SystemTime; + +use alloy_consensus::{Block, EMPTY_OMMER_ROOT_HASH}; +use alloy_eips::Encodable2718; +use alloy_primitives::Bytes; +use arbitrary::{Arbitrary, Unstructured}; +use libp2p::bytes::BufMut; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadEnvelope}; + +use crate::actors::generator::seed::SeedGenerator; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum PayloadVersion { + V1, + #[allow(dead_code)] + V2, + #[allow(dead_code)] + V3, + #[allow(dead_code)] + V4, +} + +impl SeedGenerator { + /// Generate a random op execution payload. + pub(crate) fn random_valid_payload( + &mut self, + version: PayloadVersion, + ) -> anyhow::Result { + let block: Block = match version { + PayloadVersion::V1 => self.v1_valid_block(), + PayloadVersion::V2 => self.v2_valid_block(), + PayloadVersion::V3 => self.v3_valid_block(), + PayloadVersion::V4 => self.v4_valid_block(), + }; + + let (payload, _) = OpExecutionPayload::from_block_slow(&block); + + let parent_beacon_block_root = block.header.parent_beacon_block_root; + + let envelope = + OpExecutionPayloadEnvelope { parent_beacon_block_root, execution_payload: payload }; + + Ok(envelope) + } + + fn valid_block(&mut self) -> Block { + // Simulate some random data + let data = self.random_bytes(1024 * 1024); + + // Create unstructured data with the random bytes + let u = Unstructured::new(&data); + + // Generate a random instance of MyStruct + let mut block: Block = Block::arbitrary_take_rest(u).unwrap(); + + let transactions: Vec = + block.body.transactions().map(|tx| tx.encoded_2718().into()).collect(); + + let transactions_root = + alloy_consensus::proofs::ordered_trie_root_with_encoder(&transactions, |item, buf| { + buf.put_slice(item) + }); + + block.header.transactions_root = transactions_root; + + // We always need to set the base fee per gas to a positive value to ensure the block is + // valid. + block.header.base_fee_per_gas = + Some(block.header.base_fee_per_gas.unwrap_or_default().saturating_add(1)); + + let current_timestamp = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + block.header.timestamp = current_timestamp; + + block + } + + /// Make the block v1 compatible + fn v1_valid_block(&mut self) -> Block { + let mut block = self.valid_block(); + block.header.withdrawals_root = None; + block.header.blob_gas_used = None; + block.header.excess_blob_gas = None; + block.header.parent_beacon_block_root = None; + block.header.requests_hash = None; + block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH; + block.header.difficulty = Default::default(); + block.header.nonce = Default::default(); + + block + } + + /// Make the block v2 compatible + pub(crate) fn v2_valid_block(&mut self) -> Block { + let mut block = self.v1_valid_block(); + + block.body.withdrawals = Some(vec![].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + + block.header.withdrawals_root = Some(withdrawals_root); + + block + } + + /// Make the block v3 compatible + pub(crate) fn v3_valid_block(&mut self) -> Block { + let mut block = self.valid_block(); + + block.body.withdrawals = Some(vec![].into()); + let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root( + &block.body.withdrawals.clone().unwrap_or_default(), + ); + block.header.withdrawals_root = Some(withdrawals_root); + + block.header.blob_gas_used = Some(0); + block.header.excess_blob_gas = Some(0); + block.header.parent_beacon_block_root = + Some(block.header.parent_beacon_block_root.unwrap_or_default()); + + block.header.requests_hash = None; + block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH; + block.header.difficulty = Default::default(); + block.header.nonce = Default::default(); + + block + } + + /// Make the block v4 compatible + pub(crate) fn v4_valid_block(&mut self) -> Block { + self.v3_valid_block() + } +} diff --git a/kona/crates/node/service/tests/actors/generator/mod.rs b/kona/crates/node/service/tests/actors/generator/mod.rs new file mode 100644 index 0000000000000..01a307e0014b8 --- /dev/null +++ b/kona/crates/node/service/tests/actors/generator/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod block_builder; +pub(crate) mod seed; diff --git a/kona/crates/node/service/tests/actors/generator/seed.rs b/kona/crates/node/service/tests/actors/generator/seed.rs new file mode 100644 index 0000000000000..696fe6a0f3305 --- /dev/null +++ b/kona/crates/node/service/tests/actors/generator/seed.rs @@ -0,0 +1,35 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +use rand::SeedableRng; + +pub(crate) static SEED_GENERATOR_BUILDER: SeedGeneratorBuilder = SeedGeneratorBuilder::new(); + +pub(crate) struct SeedGeneratorBuilder(AtomicU64); + +impl SeedGeneratorBuilder { + pub(crate) const fn new() -> Self { + Self(AtomicU64::new(0)) + } + + fn next(&self) -> u64 { + self.0.fetch_add(1, Ordering::SeqCst) + } + + pub(crate) fn next_generator(&self) -> SeedGenerator { + SeedGenerator(rand::rngs::StdRng::seed_from_u64(self.next())) + } +} + +pub(crate) struct SeedGenerator(pub(super) rand::rngs::StdRng); + +impl SeedGenerator { + pub(crate) const fn as_rng(&mut self) -> &mut rand::rngs::StdRng { + &mut self.0 + } + + pub(crate) fn random_bytes(&mut self, len: usize) -> Vec { + let mut data = vec![0; len]; + rand::Rng::fill(self.as_rng(), &mut data[..]); + data + } +} diff --git a/kona/crates/node/service/tests/actors/mod.rs b/kona/crates/node/service/tests/actors/mod.rs new file mode 100644 index 0000000000000..c4f4caaa32f30 --- /dev/null +++ b/kona/crates/node/service/tests/actors/mod.rs @@ -0,0 +1,4 @@ +//! Integration tests for the node actors. + +pub(crate) mod generator; +mod network; diff --git a/kona/crates/node/service/tests/actors/network/mocks/builder.rs b/kona/crates/node/service/tests/actors/network/mocks/builder.rs new file mode 100644 index 0000000000000..e108021f81905 --- /dev/null +++ b/kona/crates/node/service/tests/actors/network/mocks/builder.rs @@ -0,0 +1,107 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use alloy_chains::Chain; +use alloy_signer::k256; +use discv5::{ConfigBuilder, Enr, ListenConfig}; + +use alloy_primitives::Address; +use kona_disc::LocalNode; +use kona_genesis::RollupConfig; +use kona_node_service::{NetworkActor, NetworkBuilder, NetworkContext, NodeActor}; +use kona_peers::BootNode; +use kona_sources::BlockSigner; +use libp2p::{Multiaddr, identity::Keypair, multiaddr::Protocol}; +use rand::RngCore; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +use crate::actors::network::TestNetwork; + +pub(crate) struct TestNetworkBuilder { + chain_id: u64, + unsafe_block_signer: Address, + custom_keypair: Option, +} + +impl TestNetworkBuilder { + fn rollup_config(&self) -> RollupConfig { + RollupConfig { l2_chain_id: Chain::from_id(self.chain_id), ..Default::default() } + } + + pub(crate) fn new() -> Self { + let chain_id = rand::rng().next_u64(); + + Self { chain_id, unsafe_block_signer: Address::ZERO, custom_keypair: None } + } + + /// Sets a sequencer keypair for the network. + /// The next network built will be the sequencer's network. This will set the unsafe block + /// signer to the sequencer's address and the custom keypair to the sequencer's keypair. + /// This amounts to calling [`Self::with_unsafe_block_signer`] and [`Self::with_custom_keypair`] + /// sequentially. + pub(crate) fn set_sequencer(mut self) -> Self { + let sequencer_keypair = Keypair::generate_secp256k1(); + let secp256k1_key = sequencer_keypair.clone().try_into_secp256k1() + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to secp256k1. This is a bug since we only support secp256k1 keys: {e}")).unwrap() + .secret().to_bytes(); + let local_node_key = k256::ecdsa::SigningKey::from_bytes(&secp256k1_key.into()) + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to k256 signing key. This is a bug since we only support secp256k1 keys: {e}")).unwrap(); + + self.custom_keypair = Some(sequencer_keypair); + self.unsafe_block_signer = Address::from_private_key(&local_node_key); + + self + } + + /// Minimal network configuration. + /// Only allows loopback addresses in the discovery table. + pub(crate) fn build(&mut self, bootnodes: Vec) -> TestNetwork { + let keypair = self.custom_keypair.take().unwrap_or(Keypair::generate_secp256k1()); + + let secp256k1_key = keypair.clone().try_into_secp256k1() + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to secp256k1. This is a bug since we only support secp256k1 keys: {e}")).unwrap() + .secret().to_bytes(); + let local_node_key = k256::ecdsa::SigningKey::from_bytes(&secp256k1_key.into()) + .map_err(|e| anyhow::anyhow!("Impossible to convert keypair to k256 signing key. This is a bug since we only support secp256k1 keys: {e}")).unwrap(); + + let node_addr = IpAddr::V4(Ipv4Addr::UNSPECIFIED); + + let discovery_config = ConfigBuilder::new(ListenConfig::from_ip(node_addr, 0)) + // Only allow loopback addresses. + .table_filter(|enr| { + let Some(ip) = enr.ip4() else { + return false; + }; + + ip.is_loopback() + }) + .build(); + + let mut gossip_multiaddr = Multiaddr::from(node_addr); + gossip_multiaddr.push(Protocol::Tcp(0)); + + // Create a new network actor. No external connections + let builder = NetworkBuilder::new( + // Create a new rollup config. We don't need to specify any of the fields. + self.rollup_config(), + self.unsafe_block_signer, + gossip_multiaddr, + keypair, + LocalNode::new(local_node_key.clone(), node_addr, 0, 0), + discovery_config, + Some(BlockSigner::Local(local_node_key.into())), + ) + .with_bootnodes(bootnodes.into_iter().map(Into::into).collect::>().into()); + + let (inbound_data, actor) = NetworkActor::new(builder); + + let (blocks_tx, blocks_rx) = mpsc::channel(1024); + let cancellation = CancellationToken::new(); + + let context = NetworkContext { blocks: blocks_tx, cancellation }; + + let handle = tokio::spawn(async move { actor.start(context).await }); + + TestNetwork { inbound_data, blocks_rx, handle } + } +} diff --git a/kona/crates/node/service/tests/actors/network/mocks/mod.rs b/kona/crates/node/service/tests/actors/network/mocks/mod.rs new file mode 100644 index 0000000000000..9394205f9ea41 --- /dev/null +++ b/kona/crates/node/service/tests/actors/network/mocks/mod.rs @@ -0,0 +1,98 @@ +//! Tests interactions with sequencer actor's inputs channels. + +use std::{str::FromStr, time::Duration}; + +use backon::{ExponentialBuilder, Retryable}; +use discv5::Enr; +use kona_gossip::{P2pRpcRequest, PeerDump, PeerInfo}; +use kona_node_service::{NetworkActorError, NetworkInboundData}; +use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; +use tokio::{ + sync::{mpsc, oneshot}, + task::JoinHandle, +}; + +pub(crate) mod builder; + +pub(crate) struct TestNetwork { + pub(super) inbound_data: NetworkInboundData, + pub(super) blocks_rx: mpsc::Receiver, + #[allow(dead_code)] + handle: JoinHandle>, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum TestNetworkError { + #[error("P2p receiver closed")] + P2pReceiverClosed, + #[error("P2p receiver closed before sending response: {0}")] + OneshotError(#[from] oneshot::error::RecvError), + #[error("Peer info missing ENR")] + PeerInfoMissingEnr, + #[error("Invalid ENR: {0}")] + InvalidEnr(String), + #[error("Peer not connected")] + PeerNotConnected, +} + +impl TestNetwork { + pub(super) async fn peer_info(&self) -> Result { + // Try to get the peer info. Send a peer info request to the network actor. + let (peer_info_tx, peer_info_rx) = oneshot::channel(); + let peer_info_request = P2pRpcRequest::PeerInfo(peer_info_tx); + self.inbound_data + .p2p_rpc + .send(peer_info_request) + .await + .map_err(|_| TestNetworkError::P2pReceiverClosed)?; + + let info = peer_info_rx.await?; + + Ok(info) + } + + pub(super) async fn peers(&self) -> Result { + let (peers_tx, peers_rx) = oneshot::channel(); + let peers_request = P2pRpcRequest::Peers { out: peers_tx, connected: true }; + self.inbound_data + .p2p_rpc + .send(peers_request) + .await + .map_err(|_| TestNetworkError::P2pReceiverClosed)?; + let peers = peers_rx.await?; + Ok(peers) + } + + pub(super) async fn is_connected_to(&self, other: &Self) -> Result<(), TestNetworkError> { + let other_peer_id = other.peer_id().await?; + let peers = self.peers().await?; + if !peers.peers.contains_key(&other_peer_id) { + return Err(TestNetworkError::PeerNotConnected); + } + Ok(()) + } + + /// Like `is_connected_to`, but retries a couple of times until the connection is established. + pub(super) async fn is_connected_to_with_retries( + &self, + other: &Self, + ) -> Result<(), TestNetworkError> { + (async || self.is_connected_to(other).await) + .retry(ExponentialBuilder::default().with_total_delay(Some(Duration::from_secs(360)))) + // When to retry + .when(|e| matches!(e, TestNetworkError::PeerNotConnected)) + .notify(|e, duration| tracing::info!(target: "network", "Retrying connection. Error: {e:?}, duration: {duration:?}")) + .await + } + + pub(super) async fn peer_enr(&self) -> Result { + let enr = self.peer_info().await?.enr.ok_or(TestNetworkError::PeerInfoMissingEnr)?; + // Parse the ENR + let enr = Enr::from_str(&enr).map_err(TestNetworkError::InvalidEnr)?; + Ok(enr) + } + + pub(super) async fn peer_id(&self) -> Result { + Ok(self.peer_info().await?.peer_id) + } +} diff --git a/kona/crates/node/service/tests/actors/network/mod.rs b/kona/crates/node/service/tests/actors/network/mod.rs new file mode 100644 index 0000000000000..c1916624495fd --- /dev/null +++ b/kona/crates/node/service/tests/actors/network/mod.rs @@ -0,0 +1,9 @@ +//! Integration tests for the network actor. + +use crate::actors::network::mocks::TestNetwork; + +pub(super) mod mocks; + +mod p2p; + +mod sequencer; diff --git a/kona/crates/node/service/tests/actors/network/p2p.rs b/kona/crates/node/service/tests/actors/network/p2p.rs new file mode 100644 index 0000000000000..75e9a94481870 --- /dev/null +++ b/kona/crates/node/service/tests/actors/network/p2p.rs @@ -0,0 +1,44 @@ +use crate::actors::network::mocks::builder::TestNetworkBuilder; + +#[tokio::test(flavor = "multi_thread")] +async fn test_p2p_network_conn() -> anyhow::Result<()> { + let mut builder = TestNetworkBuilder::new(); + let network_1 = builder.build(vec![]); + let enr_1 = network_1.peer_enr().await?; + + let network_2 = builder.build(vec![enr_1]); + + network_2.is_connected_to_with_retries(&network_1).await?; + + network_1.is_connected_to_with_retries(&network_2).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_large_network_conn() -> anyhow::Result<()> { + const NETWORKS: usize = 10; + + let mut builder = TestNetworkBuilder::new(); + + let (mut networks, mut bootnodes) = (vec![], vec![]); + + for _ in 0..NETWORKS { + let network = builder.build(bootnodes.clone()); + let enr = network.peer_enr().await?; + networks.push(network); + bootnodes.push(enr); + } + + for network in networks.iter() { + for other_network in networks.iter() { + if network.peer_id().await? == other_network.peer_id().await? { + continue; + } + + network.is_connected_to_with_retries(other_network).await?; + } + } + + Ok(()) +} diff --git a/kona/crates/node/service/tests/actors/network/sequencer.rs b/kona/crates/node/service/tests/actors/network/sequencer.rs new file mode 100644 index 0000000000000..b864c9b5a56d3 --- /dev/null +++ b/kona/crates/node/service/tests/actors/network/sequencer.rs @@ -0,0 +1,78 @@ +use crate::actors::{ + generator::{block_builder::PayloadVersion, seed::SEED_GENERATOR_BUILDER}, + network::mocks::builder::TestNetworkBuilder, +}; + +/// Test that we can properly gossip blocks to the sequencer. +#[tokio::test(flavor = "multi_thread")] +async fn test_sequencer_network_conn() -> anyhow::Result<()> { + let mut builder = TestNetworkBuilder::new().set_sequencer(); + + let sequencer_network = builder.build(vec![]); + let enr_1 = sequencer_network.peer_enr().await?; + + let mut validator_network = builder.build(vec![enr_1]); + + sequencer_network.is_connected_to_with_retries(&validator_network).await?; + + validator_network.is_connected_to_with_retries(&sequencer_network).await?; + + let mut seed_generator = SEED_GENERATOR_BUILDER.next_generator(); + + let envelope = seed_generator.random_valid_payload(PayloadVersion::V1)?; + + sequencer_network.inbound_data.gossip_payload_tx.send(envelope.clone()).await?; + + let block = + validator_network.blocks_rx.recv().await.ok_or(anyhow::anyhow!("No block received"))?; + + assert_eq!(block.parent_beacon_block_root, envelope.parent_beacon_block_root); + assert_eq!(block.execution_payload, envelope.execution_payload); + + Ok(()) +} + +/// Test that the network can properly propagate blocks to all connected peers. +/// +/// We are setting up a linear network topology, and we check that the block propagates to every +/// block of the network. +#[tokio::test(flavor = "multi_thread")] +async fn test_sequencer_network_propagation() -> anyhow::Result<()> { + const NETWORKS: usize = 10; + + let mut builder = TestNetworkBuilder::new().set_sequencer(); + + let sequencer_network = builder.build(vec![]); + let mut previous_enrs = vec![sequencer_network.peer_enr().await?]; + + let mut validator_networks = Vec::new(); + + for _ in 0..NETWORKS { + let network = builder.build(previous_enrs.clone()); + + previous_enrs.push(network.peer_enr().await?); + validator_networks.push(network); + } + + // Check that all networks are connected to the sequencer. + for network in validator_networks.iter() { + network.is_connected_to_with_retries(&sequencer_network).await?; + } + + // Send a block to the sequencer. + let mut seed_generator = SEED_GENERATOR_BUILDER.next_generator(); + + let envelope = seed_generator.random_valid_payload(PayloadVersion::V1)?; + + sequencer_network.inbound_data.gossip_payload_tx.send(envelope.clone()).await?; + + // Check that the block propagates to all networks. + for network in validator_networks.iter_mut() { + let block = network.blocks_rx.recv().await.ok_or(anyhow::anyhow!("No block received"))?; + + assert_eq!(block.parent_beacon_block_root, envelope.parent_beacon_block_root); + assert_eq!(block.execution_payload, envelope.execution_payload); + } + + Ok(()) +} diff --git a/kona/crates/node/service/tests/integration.rs b/kona/crates/node/service/tests/integration.rs new file mode 100644 index 0000000000000..c07fb7271f30f --- /dev/null +++ b/kona/crates/node/service/tests/integration.rs @@ -0,0 +1,4 @@ +//! Integration tests for the node service crate. + +/// Tests for the node actors. +mod actors; diff --git a/kona/crates/node/service/tests/rollup_boost_missing_jwt.rs b/kona/crates/node/service/tests/rollup_boost_missing_jwt.rs new file mode 100644 index 0000000000000..0cc3288b0c0b2 --- /dev/null +++ b/kona/crates/node/service/tests/rollup_boost_missing_jwt.rs @@ -0,0 +1,65 @@ +//! Reproduces panic: "failed to create rollup boost server: Missing Client JWT secret" when +//! constructing rollup-boost without an L2 client JWT provided. + +#[cfg(test)] +mod tests { + use http::Uri; + use rollup_boost::{ + ExecutionMode, FlashblocksArgs, FlashblocksWebsocketConfig, Probes, RollupBoostLibArgs, + RollupBoostServer, + }; + use std::sync::Arc; + + #[test] + fn repro_missing_client_jwt_secret() { + // Build args with execution enabled and flashblocks enabled but NO L2 JWT provided. + // This mirrors the failing acceptance configuration when no client JWT is wired through. + let args = RollupBoostLibArgs { + builder: rollup_boost::BuilderArgs { + // Any URI; builder may be disabled at runtime, but server creation still validates + // args. + builder_url: "http://127.0.0.1:8551".parse::().unwrap(), + builder_jwt_token: None, + builder_jwt_path: None, // intentionally missing + builder_timeout: 1000, + }, + l2_client: rollup_boost::L2ClientArgs { + l2_url: "http://127.0.0.1:8551".parse::().unwrap(), + l2_jwt_token: None, + l2_jwt_path: None, /* intentionally missing -> triggers the panic/error in server + * ctor */ + l2_timeout: 1000, + }, + // Default is ExecutionMode::Enabled in the crate; rely on that or set explicitly if + // needed. + flashblocks: FlashblocksArgs { + flashblocks: true, + flashblocks_builder_url: "ws://127.0.0.1:1111".parse().unwrap(), + flashblocks_host: "127.0.0.1".to_string(), + flashblocks_port: 1112, + flashblocks_ws_config: FlashblocksWebsocketConfig { + flashblock_builder_ws_initial_reconnect_ms: 5000, + flashblock_builder_ws_max_reconnect_ms: 5000, + flashblock_builder_ws_ping_interval_ms: 500, + flashblock_builder_ws_pong_timeout_ms: 1500, + }, + }, + block_selection_policy: None, + execution_mode: ExecutionMode::Enabled, + external_state_root: false, + ignore_unhealthy_builders: false, + health_check_interval: 10, + max_unsafe_interval: 10, + }; + + let probes = Arc::new(Probes::default()); + let err = + RollupBoostServer::::new_from_args(args, probes) + .expect_err("expected missing JWT to error"); + let msg = format!("{err}"); + assert!( + msg.to_lowercase().contains("missing client jwt secret"), + "unexpected error: {msg}" + ); + } +} diff --git a/kona/crates/node/sources/Cargo.toml b/kona/crates/node/sources/Cargo.toml new file mode 100644 index 0000000000000..5bf381e40ff8d --- /dev/null +++ b/kona/crates/node/sources/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "kona-sources" +version = "0.1.2" +description = "Data source types and utilities for the kona-node" + +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +authors.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace + +# Alloy +alloy-transport.workspace = true +alloy-primitives.workspace = true +alloy-rpc-client.workspace = true +alloy-transport-http = { workspace = true, features = ["reqwest", "reqwest-rustls-tls", "hyper", "hyper-tls"] } + +alloy-signer.workspace = true +alloy-signer-local.workspace = true + +# OP Alloy +op-alloy-rpc-types-engine.workspace = true + +# Misc +tracing.workspace = true +thiserror.workspace = true +derive_more.workspace = true + +# HTTP client and TLS for remote signer +reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } +url.workspace = true +serde.workspace = true +serde_json.workspace = true +rustls.workspace = true +tokio = { workspace = true, features = ["full"] } +notify.workspace = true + +[features] +default = [] + +[dev-dependencies] +tokio.workspace = true +serde_json.workspace = true diff --git a/kona/crates/node/sources/README.md b/kona/crates/node/sources/README.md new file mode 100644 index 0000000000000..34be87f8fa864 --- /dev/null +++ b/kona/crates/node/sources/README.md @@ -0,0 +1,8 @@ +## `kona-sources` + +CI +kona-sources crate +MIT License +Docs + +Data source types and utilities for the kona-node. diff --git a/kona/crates/node/sources/src/lib.rs b/kona/crates/node/sources/src/lib.rs new file mode 100644 index 0000000000000..4dac96280152b --- /dev/null +++ b/kona/crates/node/sources/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod signer; +pub use signer::{ + BlockSigner, BlockSignerError, BlockSignerHandler, BlockSignerStartError, CertificateError, + ClientCert, RemoteSigner, RemoteSignerError, RemoteSignerHandler, RemoteSignerStartError, +}; diff --git a/kona/crates/node/sources/src/signer/mod.rs b/kona/crates/node/sources/src/signer/mod.rs new file mode 100644 index 0000000000000..7e7b6764847d8 --- /dev/null +++ b/kona/crates/node/sources/src/signer/mod.rs @@ -0,0 +1,89 @@ +//! Signer utilities for the Kona node. +//! +//! We currently support two types of block signers: +//! +//! 1. A local block signer that is used to sign blocks with a locally available private key. +//! 2. A remote block signer that is used to sign blocks with a remote private key. + +use alloy_primitives::{Address, ChainId}; +use alloy_signer::{Signature, SignerSync}; +use derive_more::From; +use op_alloy_rpc_types_engine::PayloadHash; +use std::fmt::Debug; + +mod remote; +pub use remote::{ + CertificateError, ClientCert, RemoteSigner, RemoteSignerError, RemoteSignerHandler, + RemoteSignerStartError, +}; + +/// A builder for a block signer. +#[derive(Debug, Clone, From)] +pub enum BlockSigner { + /// A local block signer that is used to sign blocks with a locally available private key. + Local(#[from] alloy_signer_local::PrivateKeySigner), + /// A remote block signer that is used to sign blocks with a remote private key. + Remote(#[from] RemoteSigner), +} + +/// A handler for a block signer. +#[derive(Debug)] +pub enum BlockSignerHandler { + /// A local block signer that is used to sign blocks with a locally available private key. + Local(alloy_signer_local::PrivateKeySigner), + /// A remote block signer that is used to sign blocks with a remote private key. + Remote(RemoteSignerHandler), +} + +/// Errors that can occur when starting a block signer. +#[derive(Debug, thiserror::Error)] +pub enum BlockSignerStartError { + /// An error that can occur when signing a block with a local signer. + #[error(transparent)] + Local(#[from] alloy_signer::Error), + /// An error that can occur when signing a block with a remote signer. + #[error(transparent)] + Remote(#[from] RemoteSignerStartError), +} + +/// Errors that can occur when signing a block. +#[derive(Debug, thiserror::Error)] +pub enum BlockSignerError { + /// An error that can occur when signing a block with a local signer. + #[error(transparent)] + Local(#[from] alloy_signer::Error), + /// An error that can occur when signing a block with a remote signer. + #[error(transparent)] + Remote(#[from] RemoteSignerError), +} + +impl BlockSigner { + /// Starts a block signer. + pub async fn start(self) -> Result { + match self { + Self::Local(signer) => Ok(BlockSignerHandler::Local(signer)), + Self::Remote(signer) => Ok(BlockSignerHandler::Remote(signer.start().await?)), + } + } +} + +impl BlockSignerHandler { + /// Signs a payload with the signer. + pub async fn sign_block( + &self, + payload_hash: PayloadHash, + chain_id: ChainId, + sender_address: Address, + ) -> Result { + let signature = match self { + Self::Local(signer) => { + signer.sign_hash_sync(&payload_hash.signature_message(chain_id))? + } + Self::Remote(signer) => { + signer.sign_block_v1(payload_hash, chain_id, sender_address).await? + } + }; + + Ok(signature) + } +} diff --git a/kona/crates/node/sources/src/signer/remote/cert.rs b/kona/crates/node/sources/src/signer/remote/cert.rs new file mode 100644 index 0000000000000..06ee259e7912b --- /dev/null +++ b/kona/crates/node/sources/src/signer/remote/cert.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use alloy_rpc_client::{ClientBuilder, RpcClient}; +use alloy_transport_http::Http; +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use rustls::{ + ClientConfig, RootCertStore, + pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, +}; +use thiserror::Error; +use tokio::sync::RwLock; + +use crate::signer::remote::client::RemoteSigner; + +/// Client certificate and key pair for mTLS authentication (PEM format) +#[derive(Debug, Clone)] +pub struct ClientCert { + /// Path to the client certificate in PEM format + pub cert: std::path::PathBuf, + /// Path to the client private key in PEM format + pub key: std::path::PathBuf, +} + +/// PEM parsing error type alias +type PemError = rustls::pki_types::pem::Error; + +/// Errors that can occur when handling certificates +#[derive(Debug, Error)] +pub enum CertificateError { + /// Invalid CA certificate path + #[error("Invalid CA certificate path: {0}")] + InvalidCACertificatePath(PemError), + /// Invalid certificate error + #[error("Invalid CA certificate: {0}")] + InvalidCACertificate(PemError), + /// Failed to add CA certificate + #[error("Failed to add CA certificate: {0}")] + AddCACertificate(rustls::Error), + /// Failed to configure client auth + #[error("Failed to configure client auth: {0}")] + ConfigureClientAuth(rustls::Error), + /// Invalid client certificate path + #[error("Invalid client certificate path: {0}")] + InvalidClientCertificatePath(PemError), + /// Invalid client certificate + #[error("Invalid client certificate: {0}")] + InvalidClientCertificate(PemError), + /// Invalid private key + #[error("Invalid private key: {0}")] + InvalidPrivateKey(PemError), +} + +impl RemoteSigner { + /// Builds TLS configuration with certificate handling for the remote signer + pub(super) fn build_tls_config(&self) -> Result { + let mut root_store = RootCertStore::empty(); + + // Add custom CA certificate if provided + if let Some(ca_cert_path) = &self.ca_cert { + let ca_certs: Vec> = + CertificateDer::pem_file_iter(ca_cert_path) + .map_err(CertificateError::InvalidCACertificatePath)? + .collect::, _>>() + .map_err(CertificateError::InvalidCACertificate)?; + + for cert in ca_certs { + root_store.add(cert).map_err(CertificateError::AddCACertificate)?; + } + } + + let tls_config = ClientConfig::builder().with_root_certificates(root_store); + + // Configure client certificates for mTLS if provided + match &self.client_cert { + None => Ok(tls_config.with_no_client_auth()), + Some(ClientCert { cert, key }) => { + let certs: Vec> = CertificateDer::pem_file_iter(cert) + .map_err(CertificateError::InvalidClientCertificatePath)? + .collect::, _>>() + .map_err(CertificateError::InvalidClientCertificate)?; + + let private_key = PrivateKeyDer::from_pem_file(key) + .map_err(CertificateError::InvalidPrivateKey)?; + + Ok(tls_config + .with_client_auth_cert(certs, private_key) + .map_err(CertificateError::ConfigureClientAuth)?) + } + } + } + + /// Starts a certificate watcher that monitors client certificate files and reloads the client + /// automatically when they are updated. + /// + /// Returns `Ok(None)` if no client certificates are configured. + pub(super) async fn start_certificate_watcher( + &self, + client: Arc>, + ) -> Result, notify::Error> { + let Some(ref client_cert) = self.client_cert.clone() else { + return Ok(None); + }; + + // Clone the builder to avoid borrowing issues + let builder = self.clone(); + let mut watcher = notify::recommended_watcher(move |res| { + builder.handle_watcher_event(client.clone(), res) + })?; + + tracing::info!(target: "signer", "Starting certificate watcher for automatic TLS reload"); + + watcher.watch(&client_cert.cert, RecursiveMode::NonRecursive)?; + watcher.watch(&client_cert.key, RecursiveMode::NonRecursive)?; + + Ok(Some(watcher)) + } + + /// Handles certificate watcher events + /// + /// This function is called by the certificate watcher when a certificate file is modified. + /// It reloads the TLS configuration and updates the client. + fn handle_watcher_event( + &self, + client: Arc>, + res: Result, + ) { + match res { + Ok(Event { kind: EventKind::Modify(_), .. }) => { + tracing::debug!( + target: "signer:certificate-watcher", + "Certificate file changed, reloading TLS configuration" + ); + + match self.build_http_client() { + Ok(new_client) => { + let transport = Http::with_client(new_client, self.endpoint.clone()); + let new_client = ClientBuilder::default().transport(transport, false); + + // Update the client with the new TLS configuration. We're using a blocking + // write here because the handler is synchronous. + let mut client_guard = client.blocking_write(); + *client_guard = new_client; + tracing::info!(target: "signer:certificate-watcher", "TLS configuration reloaded successfully"); + } + Err(e) => { + tracing::error!(target: "signer:certificate-watcher", error = %e, "Failed to reload TLS configuration"); + } + } + } + Ok(event) => { + tracing::trace!(target: "signer:certificate-watcher", event = ?event, "Ignoring non-modify event."); + } + Err(e) => { + tracing::error!(target: "signer:certificate-watcher", error = %e, "Failed to receive event from watcher channel."); + } + } + } +} diff --git a/kona/crates/node/sources/src/signer/remote/client.rs b/kona/crates/node/sources/src/signer/remote/client.rs new file mode 100644 index 0000000000000..0bc8db6676bd7 --- /dev/null +++ b/kona/crates/node/sources/src/signer/remote/client.rs @@ -0,0 +1,117 @@ +use alloy_primitives::Address; +use alloy_rpc_client::ClientBuilder; +use alloy_transport_http::Http; +use reqwest::header::HeaderMap; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::RwLock; +use url::Url; + +use crate::{ + RemoteSignerHandler, + signer::remote::cert::{CertificateError, ClientCert}, +}; + +/// Configuration for the remote signer client +/// +/// This configuration supports various TLS/certificate scenarios: +/// +/// 1. **Basic HTTPS**: Only `endpoint` and `address` are required. +/// 2. **Custom CA**: Provide `ca_cert` to verify servers with custom/self-signed certificates. +/// 3. **Mutual TLS (mTLS)**: Provide both `client_cert` and `client_key` for client authentication. +/// 4. **Full mTLS with custom CA**: Combine all certificate options for maximum security. +/// +/// Certificate formats supported: +/// - PEM format for all certificates and keys +/// - Certificates should be provided as file paths. +/// +/// By default, the process will watch for changes in the client certificate files and reload the +/// client automatically. +#[derive(Debug, Clone)] +pub struct RemoteSigner { + /// The URL of the remote signer endpoint + pub endpoint: Url, + /// The address of the signer. + pub address: Address, + /// Optional client certificate for mTLS (PEM format) + pub client_cert: Option, + /// Optional CA certificate for server verification (PEM format) + pub ca_cert: Option, + /// Headers to pass to the remote signer. + pub headers: HeaderMap, +} + +/// Errors that can occur when starting a remote signer. +#[derive(Debug, Error)] +pub enum RemoteSignerStartError { + /// Failed to ping signer + #[error("Failed to ping signer: {0}")] + Ping(alloy_transport::TransportError), + /// HTTP client build error + #[error("HTTP client build error: {0}")] + HTTPClientBuild(#[from] reqwest::Error), + /// Invalid certificate error + #[error("Invalid certificate: {0}")] + Certificate(#[from] CertificateError), + /// Certificate watcher error + #[error("Certificate watcher error: {0}")] + CertificateWatcher(#[from] notify::Error), +} + +impl RemoteSigner { + /// Creates a new remote signer with the given configuration + /// + /// If client certificates are configured, this will automatically start a certificate watcher + /// that monitors the certificate files for changes. When certificates are updated (e.g., by + /// cert-manager in Kubernetes), the TLS client will be automatically reloaded with the new + /// certificates without requiring a restart. + /// + /// # Certificate Watching + /// + /// The certificate watcher monitors: + /// - Client certificate file (if mTLS is configured) + /// - Client private key file (if mTLS is configured) + /// - CA certificate file (if custom CA is configured) + /// + /// When any of these files are modified, the watcher will: + /// 1. Log the certificate change event + /// 2. Reload the certificate files from disk + /// 3. Rebuild the HTTP client with the new TLS configuration + /// 4. Replace the existing client atomically + /// + /// This enables zero-downtime certificate rotation in production environments. + pub async fn start(self) -> Result { + let http_client = self.build_http_client()?; + let transport = Http::with_client(http_client, self.endpoint.clone()); + let client = ClientBuilder::default().transport(transport, true); + + // Try to ping the signer to check if it's reachable + let version: String = + client.request("health_status", ()).await.map_err(RemoteSignerStartError::Ping)?; + + tracing::info!(target: "signer", version, "Connected to op-signer server"); + + let client = Arc::new(RwLock::new(client)); + + // Start certificate watcher if client certificates are configured + let watcher_handle = self.start_certificate_watcher(client.clone()).await?; + + Ok(RemoteSignerHandler { client, watcher_handle, address: self.address }) + } + + /// Builds an HTTP client with certificate handling for the remote signer + pub(super) fn build_http_client(&self) -> Result { + let mut client_builder = reqwest::Client::builder(); + + // Configure TLS if certificates are provided + if self.client_cert.is_some() || self.ca_cert.is_some() { + let tls_config = self.build_tls_config()?; + client_builder = client_builder.use_preconfigured_tls(tls_config); + } + + // Set headers + client_builder = client_builder.default_headers(self.headers.clone()); + + client_builder.build().map_err(RemoteSignerStartError::HTTPClientBuild) + } +} diff --git a/kona/crates/node/sources/src/signer/remote/handler.rs b/kona/crates/node/sources/src/signer/remote/handler.rs new file mode 100644 index 0000000000000..6f72391562e67 --- /dev/null +++ b/kona/crates/node/sources/src/signer/remote/handler.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use alloy_primitives::{Address, B256, ChainId, SignatureError}; +use alloy_rpc_client::RpcClient; +use alloy_signer::Signature; +use notify::RecommendedWatcher; +use op_alloy_rpc_types_engine::PayloadHash; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use tokio::sync::RwLock; + +/// Request parameters for signing a block payload +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct BlockPayloadArgs { + domain: B256, + chain_id: u64, + payload_hash: B256, + sender_address: Address, +} + +/// Response from the remote signer +#[derive(Debug, Deserialize)] +struct SignResponse { + signature: String, +} + +/// Remote signer that communicates with an external signing service via JSON-RPC +#[derive(Debug)] +pub struct RemoteSignerHandler { + /// The JSON-RPC client. + pub(super) client: Arc>, + /// The address of the signer. + pub(super) address: Address, + /// The watcher handle for certificate watching. + pub(super) watcher_handle: Option, +} + +/// Errors that can occur when using the remote signer +#[derive(Debug, Error)] +pub enum RemoteSignerError { + /// JSON-RPC transport error + #[error("JSON-RPC transport error: {0}")] + SigningRPCError(#[from] alloy_transport::TransportError), + /// JSON serialization error + #[error("JSON serialization error: {0}")] + JsonError(#[from] serde_json::Error), + /// Failed to ping signer + #[error("Failed to ping signer: {0}")] + PingError(alloy_transport::TransportError), + /// Invalid signature hex encoding + #[error("Invalid signature hex encoding: {0}")] + InvalidSignatureHex(alloy_primitives::hex::FromHexError), + /// Invalid signature length + #[error("Invalid signature length, expected 65 bytes, got {0}")] + InvalidSignatureLength(usize), + /// Signature error + #[error("Signature error: {0}")] + SignatureError(#[from] SignatureError), + /// Invalid address + #[error( + "Unsafe block signer address does not match remote signer address: {unsafe_block_signer} != {remote_signer}" + )] + InvalidAddress { + /// The unsafe block signer address. + unsafe_block_signer: Address, + /// The remote signer address. + remote_signer: Address, + }, +} + +impl RemoteSignerHandler { + /// Returns true if certificate watching is enabled + pub const fn is_certificate_watching_enabled(&self) -> bool { + self.watcher_handle.is_some() + } + + /// Signs a block payload hash using the remote signer via JSON-RPC + pub async fn sign_block_v1( + &self, + payload_hash: PayloadHash, + chain_id: ChainId, + sender_address: Address, + ) -> Result { + if sender_address != self.address { + return Err(RemoteSignerError::InvalidAddress { + unsafe_block_signer: sender_address, + remote_signer: self.address, + }); + } + + let params = BlockPayloadArgs { + // For v1 payloads, the domain is always zero + domain: B256::ZERO, + chain_id, + payload_hash: payload_hash.0, + sender_address, + }; + + // Make JSON-RPC call to the custom method + let response: SignResponse = { + self.client + .read() + .await + .request("opsigner_signBlockPayload", ¶ms) + .await + .map_err(RemoteSignerError::SigningRPCError)? + }; + + // Parse the hex signature + let signature_bytes = + alloy_primitives::hex::decode(response.signature.trim_start_matches("0x")) + .map_err(RemoteSignerError::InvalidSignatureHex)?; + + if signature_bytes.len() != 65 { + return Err(RemoteSignerError::InvalidSignatureLength(signature_bytes.len())); + } + + let signature = Signature::from_raw(signature_bytes.as_slice()) + .map_err(RemoteSignerError::SignatureError)?; + + Ok(signature) + } +} diff --git a/kona/crates/node/sources/src/signer/remote/mod.rs b/kona/crates/node/sources/src/signer/remote/mod.rs new file mode 100644 index 0000000000000..e9614ca894d45 --- /dev/null +++ b/kona/crates/node/sources/src/signer/remote/mod.rs @@ -0,0 +1,7 @@ +mod cert; +pub use cert::{CertificateError, ClientCert}; +mod client; +pub use client::{RemoteSigner, RemoteSignerStartError}; + +mod handler; +pub use handler::{RemoteSignerError, RemoteSignerHandler}; diff --git a/kona/crates/proof/driver/CHANGELOG.md b/kona/crates/proof/driver/CHANGELOG.md new file mode 100644 index 0000000000000..36dc511b5525a --- /dev/null +++ b/kona/crates/proof/driver/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.3](https://github.com/op-rs/kona/compare/kona-driver-v0.2.2...kona-driver-v0.2.3) - 2025-01-16 + +### Added + +- *(client)* Interop binary (#903) + +### Other + +- Update Maili Deps (#908) + +## [0.2.2](https://github.com/op-rs/kona/compare/kona-driver-v0.2.1...kona-driver-v0.2.2) - 2025-01-13 + +### Other + +- Move temporary error logs to lower level than WARN ([#897](https://github.com/op-rs/kona/pull/897)) +- *(deps)* Replace dep `alloy-rs/op-alloy-protocol`->`op-rs/maili-protocol` (#890) + +## [0.2.1](https://github.com/op-rs/kona/compare/kona-driver-v0.2.0...kona-driver-v0.2.1) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +## [0.2.0](https://github.com/op-rs/kona/compare/kona-driver-v0.1.0...kona-driver-v0.2.0) - 2024-12-03 + +### Added + +- *(driver)* wait for engine ([#851](https://github.com/op-rs/kona/pull/851)) +- *(driver)* refines the executor interface for the driver ([#850](https://github.com/op-rs/kona/pull/850)) + +### Fixed + +- bump ([#865](https://github.com/op-rs/kona/pull/865)) + +### Other + +- *(driver)* advance with optional target ([#848](https://github.com/op-rs/kona/pull/848)) +- *(driver)* visibility ([#834](https://github.com/op-rs/kona/pull/834)) + +## [0.0.1](https://github.com/op-rs/kona/compare/kona-driver-v0.0.0...kona-driver-v0.0.1) - 2024-11-20 + +### Added + +- *(driver,client)* Pipeline Cursor Refactor ([#798](https://github.com/op-rs/kona/pull/798)) +- *(driver)* Abstract, Default Pipeline ([#796](https://github.com/op-rs/kona/pull/796)) + +### Fixed + +- imports ([#829](https://github.com/op-rs/kona/pull/829)) +- *(client)* SyncStart Refactor ([#797](https://github.com/op-rs/kona/pull/797)) + +### Other + +- *(driver)* use tracing macros ([#823](https://github.com/op-rs/kona/pull/823)) +- *(driver)* use tracing macros ([#822](https://github.com/op-rs/kona/pull/822)) +- *(workspace)* Migrate back to `thiserror` v2 ([#811](https://github.com/op-rs/kona/pull/811)) diff --git a/kona/crates/proof/driver/Cargo.toml b/kona/crates/proof/driver/Cargo.toml new file mode 100644 index 0000000000000..299b8da8b9915 --- /dev/null +++ b/kona/crates/proof/driver/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "kona-driver" +description = "A no_std derivation pipeline driver" +version = "0.4.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-derive.workspace = true +kona-executor.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-consensus.workspace = true +alloy-primitives.workspace = true +alloy-evm = { workspace = true, features = ["op"] } + +# OP Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine.workspace = true + +# Misc +async-trait.workspace = true +spin.workspace = true +thiserror .workspace = true +tracing.workspace = true diff --git a/kona/crates/proof/driver/README.md b/kona/crates/proof/driver/README.md new file mode 100644 index 0000000000000..f7e4d44c080fb --- /dev/null +++ b/kona/crates/proof/driver/README.md @@ -0,0 +1,3 @@ +# `kona-driver` + +A `no_std` derivation pipeline driver. diff --git a/kona/crates/proof/driver/src/core.rs b/kona/crates/proof/driver/src/core.rs new file mode 100644 index 0000000000000..b5b315205e2e4 --- /dev/null +++ b/kona/crates/proof/driver/src/core.rs @@ -0,0 +1,333 @@ +//! The driver of the kona derivation pipeline. + +use crate::{DriverError, DriverPipeline, DriverResult, Executor, PipelineCursor, TipCursor}; +use alloc::{sync::Arc, vec::Vec}; +use alloy_consensus::BlockBody; +use alloy_primitives::{B256, Bytes}; +use alloy_rlp::Decodable; +use core::fmt::Debug; +use kona_derive::{Pipeline, PipelineError, PipelineErrorKind, Signal, SignalReceiver}; +use kona_executor::BlockBuildingOutcome; +use kona_genesis::RollupConfig; +use kona_protocol::L2BlockInfo; +use op_alloy_consensus::{OpBlock, OpTxEnvelope, OpTxType}; +use spin::RwLock; + +/// The Rollup Driver entrypoint. +/// +/// The [`Driver`] is the main coordination component for the rollup derivation and execution +/// process. It manages the interaction between the derivation pipeline and block executor +/// to produce verified L2 blocks from L1 data. +/// +/// ## Architecture +/// The driver operates with three main components: +/// - **Pipeline**: Derives L2 block attributes from L1 data +/// - **Executor**: Builds and executes L2 blocks from attributes +/// - **Cursor**: Tracks the current state of derivation progress +/// +/// ## Usage Pattern +/// ```text +/// 1. Initialize driver with cursor, executor, and pipeline +/// 2. Call wait_for_executor() to ensure readiness +/// 3. Call advance_to_target() to derive blocks up to target +/// 4. Driver coordinates pipeline stepping and block execution +/// 5. Updates cursor with progress and maintains safe head artifacts +/// ``` +/// +/// ## Error Handling +/// The driver handles various error scenarios: +/// - Pipeline derivation failures (temporary, reset, critical) +/// - Block execution failures (with Holocene deposit-only retry) +/// - L1 data exhaustion (graceful halt) +/// - Interop mode considerations +#[derive(Debug)] +pub struct Driver +where + E: Executor + Send + Sync + Debug, + DP: DriverPipeline

+ Send + Sync + Debug, + P: Pipeline + SignalReceiver + Send + Sync + Debug, +{ + /// Marker for the pipeline type parameter. + /// + /// This phantom data ensures type safety while allowing the driver + /// to work with different pipeline implementations. + _marker: core::marker::PhantomData

, + /// Cursor tracking the current L2 derivation state and safe head. + /// + /// The cursor maintains the current position in the derivation process, + /// including the L2 safe head, output root, and L1 origin. It's wrapped + /// in an `Arc>` for thread-safe shared access. + pub cursor: Arc>, + /// The block executor responsible for building and executing L2 blocks. + /// + /// The executor takes payload attributes from the pipeline and produces + /// complete blocks with execution results and state changes. + pub executor: E, + /// The derivation pipeline that produces block attributes from L1 data. + /// + /// The pipeline abstracts the complex derivation logic and provides + /// a high-level interface for producing sequential block attributes. + pub pipeline: DP, + /// Cached execution artifacts and transactions from the most recent safe head. + /// + /// This cache contains the [`BlockBuildingOutcome`] and raw transaction data + /// from the last successfully executed block. It's used for efficiency and + /// debugging purposes. `None` when no block has been executed yet. + pub safe_head_artifacts: Option<(BlockBuildingOutcome, Vec)>, +} + +impl Driver +where + E: Executor + Send + Sync + Debug, + DP: DriverPipeline

+ Send + Sync + Debug, + P: Pipeline + SignalReceiver + Send + Sync + Debug, +{ + /// Creates a new [`Driver`] instance. + /// + /// Initializes the driver with the provided cursor, executor, and pipeline components. + /// The driver starts with no cached safe head artifacts. + /// + /// # Arguments + /// * `cursor` - Shared cursor for tracking derivation state + /// * `executor` - Block executor for building and executing L2 blocks + /// * `pipeline` - Derivation pipeline for producing block attributes + /// + /// # Returns + /// A new [`Driver`] instance ready for operation after calling [`Self::wait_for_executor`]. + /// + /// # Usage + /// ```rust,ignore + /// let driver = Driver::new(cursor, executor, pipeline); + /// driver.wait_for_executor().await; + /// let result = driver.advance_to_target(&config, Some(target_block)).await; + /// ``` + pub const fn new(cursor: Arc>, executor: E, pipeline: DP) -> Self { + Self { + _marker: core::marker::PhantomData, + cursor, + executor, + pipeline, + safe_head_artifacts: None, + } + } + + /// Waits until the executor is ready for block processing. + /// + /// This method blocks until the underlying executor has completed any necessary + /// initialization or synchronization required before it can begin processing + /// payload attributes and executing blocks. + /// + /// # Usage + /// Must be called after creating the driver and before calling [`Self::advance_to_target`]. + /// This ensures the executor is in a valid state for block execution. + /// + /// # Example + /// ```rust,ignore + /// let mut driver = Driver::new(cursor, executor, pipeline); + /// driver.wait_for_executor().await; // Required before derivation + /// ``` + pub async fn wait_for_executor(&mut self) { + self.executor.wait_until_ready().await; + } + + /// Advances the derivation pipeline to the target block number. + /// + /// This is the main driver method that coordinates the derivation pipeline and block + /// executor to produce L2 blocks up to the specified target. It handles the complete + /// lifecycle of block derivation including pipeline stepping, block execution, error + /// recovery, and state updates. + /// + /// # Arguments + /// * `cfg` - The rollup configuration containing chain parameters and activation heights + /// * `target` - Optional target block number. If `None`, derives indefinitely until data source + /// is exhausted or an error occurs + /// + /// # Returns + /// * `Ok((l2_safe_head, output_root))` - Tuple containing the final [`L2BlockInfo`] and output + /// root hash when target is reached or derivation completes + /// * `Err(DriverError)` - Various error conditions that prevent further derivation + /// + /// # Errors + /// This method can fail with several error types: + /// + /// ## Pipeline Errors + /// - **EndOfSource (Critical)**: L1 data source exhausted + /// - In interop mode: Returns error immediately for caller handling + /// - In normal mode: Adjusts target to current safe head and halts gracefully + /// - **Temporary**: Insufficient data, automatically retried + /// - **Reset**: Reorg detected, pipeline reset and derivation continues + /// - **Other Critical**: Fatal pipeline errors that stop derivation + /// + /// ## Execution Errors + /// - **Pre-Holocene**: Block execution failures cause block to be discarded + /// - **Holocene+**: Failed blocks are retried as deposit-only blocks + /// - Strips non-deposit transactions and flushes invalidated channel + /// - If deposit-only block also fails, returns critical error + /// + /// ## Other Errors + /// - **MissingOrigin**: Pipeline origin not available when expected + /// - **BlockConversion**: Failed to convert block format + /// - **RLP**: Failed to decode transaction data + /// + /// # Behavior Details + /// + /// ## Main Loop + /// The method operates in a continuous loop: + /// 1. Check if target block number reached (if specified) + /// 2. Produce payload attributes from pipeline + /// 3. Execute payload with executor + /// 4. Handle execution failures with retry logic + /// 5. Construct complete block and update cursor + /// 6. Cache artifacts and continue + /// + /// ## Target Handling + /// - If `target` is `Some(n)`: Stops when safe head reaches block `n` + /// - If `target` is `None`: Continues until data exhausted or critical error + /// - Target can be dynamically adjusted if data source is exhausted + /// + /// ## State Updates + /// Each successful block updates: + /// - Pipeline cursor with new L1 origin and L2 safe head + /// - Executor safe head for next block building + /// - Cached artifacts for the most recent block + /// - Output root computation for verification + /// + /// # Usage Pattern + /// ```rust,ignore + /// // Derive to specific block + /// let (safe_head, output_root) = driver + /// .advance_to_target(&rollup_config, Some(100)) + /// .await?; + /// + /// // Derive until data exhausted + /// let (final_head, output_root) = driver + /// .advance_to_target(&rollup_config, None) + /// .await?; + /// ``` + /// + /// # Panics + /// This method does not explicitly panic, but may propagate panics from: + /// - RwLock poisoning (if another thread panicked while holding the cursor lock) + /// - Executor or pipeline implementation panics + /// - Arithmetic overflow in block number operations (highly unlikely) + pub async fn advance_to_target( + &mut self, + cfg: &RollupConfig, + mut target: Option, + ) -> DriverResult<(L2BlockInfo, B256), E::Error> { + loop { + // Check if we have reached the target block number. + let pipeline_cursor = self.cursor.read(); + let tip_cursor = pipeline_cursor.tip(); + if let Some(tb) = target { + if tip_cursor.l2_safe_head.block_info.number >= tb { + info!(target: "client", "Derivation complete, reached L2 safe head."); + return Ok((tip_cursor.l2_safe_head, tip_cursor.l2_safe_head_output_root)); + } + } + + let mut attributes = match self.pipeline.produce_payload(tip_cursor.l2_safe_head).await + { + Ok(attrs) => attrs.take_inner(), + Err(PipelineErrorKind::Critical(PipelineError::EndOfSource)) => { + warn!(target: "client", "Exhausted data source; Halting derivation and using current safe head."); + + // Adjust the target block number to the current safe head, as no more blocks + // can be produced. + if target.is_some() { + target = Some(tip_cursor.l2_safe_head.block_info.number); + }; + + // If we are in interop mode, this error must be handled by the caller. + // Otherwise, we continue the loop to halt derivation on the next iteration. + if cfg.is_interop_active(self.cursor.read().l2_safe_head().block_info.number) { + return Err(PipelineError::EndOfSource.crit().into()); + } else { + continue; + } + } + Err(e) => { + error!(target: "client", "Failed to produce payload: {:?}", e); + return Err(DriverError::Pipeline(e)); + } + }; + + self.executor.update_safe_head(tip_cursor.l2_safe_head_header.clone()); + let outcome = match self.executor.execute_payload(attributes.clone()).await { + Ok(outcome) => outcome, + Err(e) => { + error!(target: "client", "Failed to execute L2 block: {}", e); + + if cfg.is_holocene_active(attributes.payload_attributes.timestamp) { + // Retry with a deposit-only block. + warn!(target: "client", "Flushing current channel and retrying deposit only block"); + + // Flush the current batch and channel - if a block was replaced with a + // deposit-only block due to execution failure, the + // batch and channel it is contained in is forwards + // invalidated. + self.pipeline.signal(Signal::FlushChannel).await?; + + // Strip out all transactions that are not deposits. + attributes.transactions = attributes.transactions.map(|txs| { + txs.into_iter() + .filter(|tx| !tx.is_empty() && tx[0] == OpTxType::Deposit as u8) + .collect::>() + }); + + // Retry the execution. + self.executor.update_safe_head(tip_cursor.l2_safe_head_header.clone()); + match self.executor.execute_payload(attributes.clone()).await { + Ok(header) => header, + Err(e) => { + error!( + target: "client", + "Critical - Failed to execute deposit-only block: {e}", + ); + return Err(DriverError::Executor(e)); + } + } + } else { + // Pre-Holocene, discard the block if execution fails. + continue; + } + } + }; + + // Construct the block. + let block = OpBlock { + header: outcome.header.inner().clone(), + body: BlockBody { + transactions: attributes + .transactions + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|tx| OpTxEnvelope::decode(&mut tx.as_ref()).map_err(DriverError::Rlp)) + .collect::, E::Error>>()?, + ommers: Vec::new(), + withdrawals: None, + }, + }; + + // Get the pipeline origin and update the tip cursor. + let origin = self.pipeline.origin().ok_or(PipelineError::MissingOrigin.crit())?; + let l2_info = L2BlockInfo::from_block_and_genesis( + &block, + &self.pipeline.rollup_config().genesis, + )?; + let tip_cursor = TipCursor::new( + l2_info, + outcome.header.clone(), + self.executor.compute_output_root().map_err(DriverError::Executor)?, + ); + + // Advance the derivation pipeline cursor + drop(pipeline_cursor); + self.cursor.write().advance(origin, tip_cursor); + + // Update the latest safe head artifacts. + self.safe_head_artifacts = Some((outcome, attributes.transactions.unwrap_or_default())); + } + } +} diff --git a/kona/crates/proof/driver/src/cursor.rs b/kona/crates/proof/driver/src/cursor.rs new file mode 100644 index 0000000000000..31406d6ac2005 --- /dev/null +++ b/kona/crates/proof/driver/src/cursor.rs @@ -0,0 +1,229 @@ +//! Contains the cursor for the derivation pipeline. +//! +//! This module provides the [`PipelineCursor`] which tracks the state of the derivation +//! pipeline including L1 origins, L2 safe heads, and caching for efficient reorg handling. + +use alloc::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::{B256, map::HashMap}; +use kona_protocol::{BlockInfo, L2BlockInfo}; + +use crate::TipCursor; + +/// A cursor that tracks the derivation pipeline state and progress. +/// +/// The [`PipelineCursor`] maintains a cache of recent L1 origins and their corresponding +/// L2 tips to efficiently handle reorgs and provide quick access to recent derivation +/// state. It implements a capacity-bounded LRU cache to prevent unbounded memory growth. +/// +/// # Cache Management +/// The cursor maintains a cache of recent L1/L2 mappings with a capacity based on the +/// channel timeout. This ensures that reorgs within the channel timeout can be handled +/// efficiently without re-deriving from genesis. +/// +/// # Reorg Handling +/// When L1 reorgs occur, the cursor can reset to a previous safe state within the +/// channel timeout window, allowing derivation to continue from a known good state +/// rather than starting over. +/// +/// # Memory Bounds +/// The cache size is bounded by `channel_timeout + 5` to ensure reasonable memory +/// usage while providing sufficient history for reorg recovery. +#[derive(Debug, Clone)] +pub struct PipelineCursor { + /// The maximum number of cached L1/L2 mappings before evicting old entries. + /// + /// This capacity is calculated as `channel_timeout + 5` to ensure sufficient + /// history for reorg handling while preventing unbounded memory growth. + pub capacity: usize, + /// The channel timeout in blocks used for reorg recovery calculations. + /// + /// This value determines how far back the cursor can reset during reorgs + /// and influences the cache capacity to ensure adequate history retention. + pub channel_timeout: u64, + /// The current L1 origin block that the pipeline is processing. + /// + /// This represents the most recent L1 block from which L2 blocks are being + /// derived. It advances as the pipeline processes new L1 data. + pub origin: BlockInfo, + /// Ordered list of L1 origin block numbers for cache eviction policy. + /// + /// This deque maintains insertion order to implement LRU eviction when the + /// cache reaches capacity. The front contains the oldest entries. + pub origins: VecDeque, + /// Mapping from L1 block numbers to their corresponding [`BlockInfo`]. + /// + /// This cache stores L1 block information for quick lookup during reorg + /// recovery without needing to re-fetch from the data source. + pub origin_infos: HashMap, + /// Mapping from L1 origin block numbers to their corresponding L2 tips. + /// + /// This is the main cache storing the relationship between L1 origins and + /// the L2 safe head state derived from them. Used for efficient reorg recovery. + pub tips: BTreeMap, +} + +impl PipelineCursor { + /// Creates a new pipeline cursor with the specified channel timeout and initial origin. + /// + /// The cursor is initialized with a cache capacity of `channel_timeout + 5` to ensure + /// sufficient history for reorg handling. The initial L1 origin is added to the cache. + /// + /// # Arguments + /// * `channel_timeout` - The channel timeout in blocks for reorg recovery + /// * `origin` - The initial L1 origin block to start derivation from + /// + /// # Returns + /// A new [`PipelineCursor`] initialized with the given parameters + /// + /// # Cache Capacity + /// The capacity is set to `channel_timeout + 5` blocks to provide adequate + /// history for reorg recovery while maintaining reasonable memory usage. + /// This ensures the cursor can reset to any point within the channel timeout window. + pub fn new(channel_timeout: u64, origin: BlockInfo) -> Self { + // NOTE: capacity must be greater than the `channel_timeout` to allow + // for derivation to proceed through a deep reorg. + // Ref: + let capacity = channel_timeout as usize + 5; + + let mut origins = VecDeque::with_capacity(capacity); + origins.push_back(origin.number); + let mut origin_infos = HashMap::default(); + origin_infos.insert(origin.number, origin); + Self { capacity, channel_timeout, origin, origins, origin_infos, tips: Default::default() } + } + + /// Returns the current L1 origin block being processed by the pipeline. + /// + /// This is the most recent L1 block from which the pipeline is deriving L2 blocks. + /// The origin advances as new L1 data becomes available and is processed. + pub const fn origin(&self) -> BlockInfo { + self.origin + } + + /// Returns the current L2 safe head block information. + /// + /// The L2 safe head represents the most recent L2 block that has been successfully + /// derived and is considered safe. This is the tip of the verified L2 chain. + pub fn l2_safe_head(&self) -> &L2BlockInfo { + &self.tip().l2_safe_head + } + + /// Returns the sealed header of the current L2 safe head. + /// + /// The sealed header contains the complete block header with computed hash, + /// providing access to all block metadata including parent hash, timestamp, + /// gas limits, and other consensus-critical information. + pub fn l2_safe_head_header(&self) -> &Sealed

{ + &self.tip().l2_safe_head_header + } + + /// Returns the output root of the current L2 safe head. + /// + /// The output root is a commitment to the L2 state after executing the safe head + /// block. It's used for fraud proof verification and state root challenges. + pub fn l2_safe_head_output_root(&self) -> &B256 { + &self.tip().l2_safe_head_output_root + } + + /// Returns the current L2 tip cursor containing safe head information. + /// + /// The tip cursor encapsulates the L2 safe head block info, header, and output root + /// for the most recently processed L1 origin. + /// + /// # Panics + /// This method panics if called before the cursor is properly initialized with at + /// least one L1/L2 mapping. This should never happen in normal operation as the + /// cursor is initialized with an origin in [`Self::new`]. + pub fn tip(&self) -> &TipCursor { + if let Some((_, l2_tip)) = self.tips.last_key_value() { + l2_tip + } else { + unreachable!("cursor must be initialized with one block before advancing") + } + } + + /// Advances the cursor to a new L1 origin and corresponding L2 tip. + /// + /// This method updates the cursor state with a new L1/L2 mapping, representing + /// progress in the derivation pipeline. If the cache is at capacity, the oldest + /// entry is evicted using LRU policy. + /// + /// # Arguments + /// * `origin` - The new L1 origin block that has been processed + /// * `l2_tip_block` - The L2 tip cursor resulting from processing this L1 origin + /// + /// # Cache Management + /// - If cache is full, evicts the oldest L1/L2 mapping + /// - Updates the current origin to the new L1 block + /// - Adds the new mapping to the cache for future reorg recovery + /// + /// # Usage + /// Called by the driver after successfully deriving an L2 block from L1 data + /// to advance the derivation state and maintain the cursor cache. + pub fn advance(&mut self, origin: BlockInfo, l2_tip_block: TipCursor) { + if self.tips.len() >= self.capacity { + let key = self.origins.pop_front().unwrap(); + self.tips.remove(&key); + } + + self.origin = origin; + self.origins.push_back(origin.number); + self.origin_infos.insert(origin.number, origin); + self.tips.insert(origin.number, l2_tip_block); + } + + /// Resets the cursor state due to an L1 reorganization. + /// + /// When the L1 chain undergoes a reorg, the cursor needs to reset to a safe state + /// that accounts for the channel timeout. This ensures that any L2 blocks that + /// might have started derivation from invalidated L1 blocks are properly handled. + /// + /// # Arguments + /// * `fork_block` - The L1 block number where the reorg was detected + /// + /// # Returns + /// A tuple containing: + /// * [`TipCursor`] - The L2 safe state to reset to + /// * [`BlockInfo`] - The L1 origin block info corresponding to the reset state + /// + /// # Reset Logic + /// The reset target is calculated as `fork_block - channel_timeout` because: + /// 1. L2 blocks can derive from L1 data spanning the channel timeout window + /// 2. Any L2 block that started derivation within this window might be affected + /// 3. Resetting before this window ensures a clean slate for re-derivation + /// + /// # Cache Lookup Strategy + /// 1. **Exact match**: If `channel_start` block is cached, use it directly + /// 2. **Fallback**: Find the most recent cached block before `channel_start` + /// + /// # Panics + /// This method panics if no suitable reset target is found in the cache, + /// which should never happen if the cache capacity is properly sized relative + /// to the channel timeout. + /// + /// # Usage + /// Called automatically when the pipeline detects an L1 reorg to ensure + /// derivation continues from a safe, unaffected state. + pub fn reset(&mut self, fork_block: u64) -> (TipCursor, BlockInfo) { + let channel_start = fork_block - self.channel_timeout; + + match self.tips.get(&channel_start) { + Some(l2_safe_tip) => { + // The channel start block is in the cache, we can use it to reset the cursor. + (l2_safe_tip.clone(), self.origin_infos[&channel_start]) + } + None => { + // If the channel start block is not in the cache, we reset the cursor + // to the closest known L1 block for which we have a corresponding L2 block. + let (last_l1_known_tip, l2_known_tip) = self + .tips + .range(..=channel_start) + .next_back() + .expect("walked back to genesis without finding anchor origin block"); + + (l2_known_tip.clone(), self.origin_infos[last_l1_known_tip]) + } + } + } +} diff --git a/kona/crates/proof/driver/src/errors.rs b/kona/crates/proof/driver/src/errors.rs new file mode 100644 index 0000000000000..f517a6936363d --- /dev/null +++ b/kona/crates/proof/driver/src/errors.rs @@ -0,0 +1,28 @@ +//! Contains driver-related error types. + +use kona_derive::PipelineErrorKind; +use kona_protocol::FromBlockError; +use thiserror::Error; + +/// A [`Result`] type for the [`DriverError`]. +pub type DriverResult = Result>; + +/// Driver error. +#[derive(Error, Debug)] +pub enum DriverError +where + E: core::error::Error, +{ + /// Pipeline error. + #[error("Pipeline error: {0}")] + Pipeline(#[from] PipelineErrorKind), + /// An error returned by the executor. + #[error("Executor error: {0}")] + Executor(E), + /// An error returned by the conversion from a block to an [`kona_protocol::L2BlockInfo`]. + #[error("From block error: {0}")] + FromBlock(#[from] FromBlockError), + /// Error decoding or encoding RLP. + #[error("RLP error: {0}")] + Rlp(alloy_rlp::Error), +} diff --git a/kona/crates/proof/driver/src/executor.rs b/kona/crates/proof/driver/src/executor.rs new file mode 100644 index 0000000000000..ee013380c6fc8 --- /dev/null +++ b/kona/crates/proof/driver/src/executor.rs @@ -0,0 +1,105 @@ +//! An abstraction for the driver's block executor. +//! +//! This module provides the [`Executor`] trait which abstracts block execution for the driver. +//! The executor is responsible for building and executing blocks from payload attributes, +//! maintaining safe head state, and computing output roots for the execution results. + +use alloc::boxed::Box; +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::B256; +use async_trait::async_trait; +use core::error::Error; +use kona_executor::BlockBuildingOutcome; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// Executor trait for block execution in the driver pipeline. +/// +/// This trait abstracts the block execution functionality needed by the driver. +/// Implementations are responsible for: +/// - Building blocks from payload attributes +/// - Maintaining execution state and safe head tracking +/// - Computing output roots after block execution +/// - Handling execution errors and recovery scenarios +/// +/// The executor operates in a sequential manner where blocks must be executed +/// in order to maintain proper state transitions. +#[async_trait] +pub trait Executor { + /// The error type for the Executor. + /// + /// Should implement [`Error`] and provide detailed information about + /// execution failures, including transaction-level errors and state issues. + type Error: Error; + + /// Waits for the executor to be ready for block execution. + /// + /// This method blocks until the executor has completed any necessary + /// initialization or synchronization required before it can begin + /// processing blocks. + /// + /// # Usage + /// This should be called before attempting to execute any payloads + /// to ensure the executor is in a valid state. + async fn wait_until_ready(&mut self); + + /// Updates the safe head to the specified header. + /// + /// Sets the executor's internal safe head state to the provided sealed header. + /// This is used to establish the starting point for subsequent block execution. + /// + /// # Arguments + /// * `header` - The sealed header to set as the new safe head + /// + /// # Usage + /// This must be called before executing a payload to ensure the executor + /// builds on the correct parent block. + fn update_safe_head(&mut self, header: Sealed
); + + /// Execute the given payload attributes to build and execute a block. + /// + /// Takes the provided payload attributes and builds a complete block, + /// executing all transactions and computing the resulting state changes. + /// + /// # Arguments + /// * `attributes` - The payload attributes containing transactions and metadata + /// + /// # Returns + /// * `Ok(BlockBuildingOutcome)` - Successful execution result with the built block + /// * `Err(Self::Error)` - Execution failure with detailed error information + /// + /// # Errors + /// This method can fail due to: + /// - Invalid transactions in the payload + /// - Execution errors (e.g., out of gas, revert) + /// - State inconsistencies + /// - Block validation failures + /// + /// # Usage + /// Must be called after setting the safe head with [`Self::update_safe_head`]. + /// The execution builds on the current safe head state. + async fn execute_payload( + &mut self, + attributes: OpPayloadAttributes, + ) -> Result; + + /// Computes the output root for the most recently executed block. + /// + /// Calculates the Merkle root of the execution outputs which is used + /// for verification and state commitment purposes. + /// + /// # Returns + /// * `Ok(B256)` - The computed output root hash + /// * `Err(Self::Error)` - Computation failure + /// + /// # Errors + /// This method can fail if: + /// - No block has been executed yet + /// - State is inconsistent or corrupted + /// - Output computation encounters an internal error + /// + /// # Usage + /// Expected to be called immediately after successful payload execution + /// via [`Self::execute_payload`]. The computed root corresponds to the state + /// after the most recent block execution. + fn compute_output_root(&mut self) -> Result; +} diff --git a/kona/crates/proof/driver/src/lib.rs b/kona/crates/proof/driver/src/lib.rs new file mode 100644 index 0000000000000..f83e3410a528e --- /dev/null +++ b/kona/crates/proof/driver/src/lib.rs @@ -0,0 +1,31 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), no_std)] + +extern crate alloc; + +#[macro_use] +extern crate tracing; + +mod errors; +pub use errors::{DriverError, DriverResult}; + +mod pipeline; +pub use pipeline::DriverPipeline; + +mod executor; +pub use executor::Executor; + +mod core; +pub use core::Driver; + +mod cursor; +pub use cursor::PipelineCursor; + +mod tip; +pub use tip::TipCursor; diff --git a/kona/crates/proof/driver/src/pipeline.rs b/kona/crates/proof/driver/src/pipeline.rs new file mode 100644 index 0000000000000..a13b15eac0af0 --- /dev/null +++ b/kona/crates/proof/driver/src/pipeline.rs @@ -0,0 +1,167 @@ +//! Abstracts the derivation pipeline from the driver. +//! +//! This module provides the [`DriverPipeline`] trait which serves as a high-level +//! abstraction for the driver's derivation pipeline. The pipeline is responsible +//! for deriving L2 blocks from L1 data and producing payload attributes for execution. + +use alloc::boxed::Box; +use async_trait::async_trait; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; + +use kona_derive::{ + ActivationSignal, Pipeline, PipelineError, PipelineErrorKind, ResetError, ResetSignal, + SignalReceiver, StepResult, +}; + +/// High-level abstraction for the driver's derivation pipeline. +/// +/// The [`DriverPipeline`] trait extends the base [`Pipeline`] functionality with +/// driver-specific operations needed for block production. It handles the complex +/// logic of stepping through derivation stages, managing resets and reorgs, and +/// producing payload attributes for block building. +/// +/// ## Key Responsibilities +/// - Stepping through derivation pipeline stages +/// - Handling L1 origin advancement +/// - Managing pipeline resets due to reorgs or activation signals +/// - Producing payload attributes for disputed blocks +/// - Caching and cache invalidation +/// +/// ## Error Handling +/// The pipeline can encounter several types of errors: +/// - **Temporary**: Retryable errors (e.g., missing data) +/// - **Reset**: Errors requiring pipeline reset (e.g., reorgs, activations) +/// - **Critical**: Fatal errors that stop derivation +#[async_trait] +pub trait DriverPipeline

: Pipeline + SignalReceiver +where + P: Pipeline + SignalReceiver, +{ + /// Flushes any cached data due to a reorganization. + /// + /// This method clears internal caches that may contain stale data + /// when a reorganization is detected on the L1 chain. It ensures + /// that subsequent derivation operations work with fresh data. + /// + /// # Usage + /// Called automatically when a reorg is detected during pipeline + /// stepping, but can also be called manually if needed. + fn flush(&mut self); + + /// Produces payload attributes for the next block after the given L2 safe head. + /// + /// This method advances the derivation pipeline to produce the next set of + /// [`OpAttributesWithParent`] that can be used for block building. It handles + /// the complex stepping logic including error recovery, resets, and reorgs. + /// + /// # Arguments + /// * `l2_safe_head` - The current L2 safe head block info to build upon + /// + /// # Returns + /// * `Ok(OpAttributesWithParent)` - Successfully produced payload attributes + /// * `Err(PipelineErrorKind)` - Pipeline error preventing payload production + /// + /// # Errors + /// This method can fail with various error types: + /// - **Temporary errors**: Insufficient data, retries automatically + /// - **Reset errors**: Reorg detected or activation needed, triggers pipeline reset + /// - **Critical errors**: Fatal issues that require external intervention + /// + /// # Behavior + /// The method operates in a loop, continuously stepping the pipeline until: + /// 1. Payload attributes are successfully produced + /// 2. A critical error occurs + /// 3. The pipeline signals completion + /// + /// ## Reset Handling + /// When reset errors occur: + /// - **Reorg detected**: Flushes cache and resets to safe head + /// - **Holocene activation**: Sends activation signal + /// - **Other resets**: Standard reset to safe head with system config + /// + /// ## Step Results + /// The pipeline can return different step results: + /// - **PreparedAttributes**: Attributes ready for the next block + /// - **AdvancedOrigin**: L1 origin moved forward + /// - **OriginAdvanceErr/StepFailed**: Various error conditions + async fn produce_payload( + &mut self, + l2_safe_head: L2BlockInfo, + ) -> Result { + // As we start the safe head at the disputed block's parent, we step the pipeline until the + // first attributes are produced. All batches at and before the safe head will be + // dropped, so the first payload will always be the disputed one. + loop { + match self.step(l2_safe_head).await { + StepResult::PreparedAttributes => { + info!(target: "client_derivation_driver", "Stepped derivation pipeline") + } + StepResult::AdvancedOrigin => { + info!( + target: "client_derivation_driver", + l1_block_number = self.origin().map(|o| o.number).ok_or(PipelineError::MissingOrigin.crit())?, + "Advanced origin" + ) + } + StepResult::OriginAdvanceErr(e) | StepResult::StepFailed(e) => { + // Break the loop unless the error signifies that there is not enough data to + // complete the current step. In this case, we retry the step to see if other + // stages can make progress. + match e { + PipelineErrorKind::Temporary(_) => { + trace!(target: "client_derivation_driver", "Failed to step derivation pipeline temporarily: {:?}", e); + continue; + } + PipelineErrorKind::Reset(e) => { + warn!(target: "client_derivation_driver", "Failed to step derivation pipeline due to reset: {:?}", e); + let system_config = self + .system_config_by_number(l2_safe_head.block_info.number) + .await?; + + if matches!(e, ResetError::HoloceneActivation) { + let l1_origin = + self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + self.signal( + ActivationSignal { + l2_safe_head, + l1_origin, + system_config: Some(system_config), + } + .signal(), + ) + .await?; + } else { + // Flushes cache if a reorg is detected. + if matches!(e, ResetError::ReorgDetected(_, _)) { + self.flush(); + } + + // Reset the pipeline to the initial L2 safe head and L1 origin, + // and try again. + let l1_origin = + self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + self.signal( + ResetSignal { + l2_safe_head, + l1_origin, + system_config: Some(system_config), + } + .signal(), + ) + .await?; + } + } + PipelineErrorKind::Critical(_) => { + warn!(target: "client_derivation_driver", "Failed to step derivation pipeline: {:?}", e); + return Err(e); + } + } + } + } + + if let Some(attrs) = self.next() { + return Ok(attrs); + } + } + } +} diff --git a/kona/crates/proof/driver/src/tip.rs b/kona/crates/proof/driver/src/tip.rs new file mode 100644 index 0000000000000..337707fc415a9 --- /dev/null +++ b/kona/crates/proof/driver/src/tip.rs @@ -0,0 +1,99 @@ +//! Contains the tip cursor for the derivation driver. +//! +//! This module provides the [`TipCursor`] which encapsulates the L2 safe head state +//! including block information, header, and output root for a specific derivation tip. + +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::B256; +use kona_protocol::L2BlockInfo; + +/// A cursor that encapsulates the L2 safe head state at a specific derivation tip. +/// +/// The [`TipCursor`] represents a snapshot of the L2 chain state at a particular point +/// in the derivation process. It contains all the essential information needed to +/// represent an L2 safe head including the block metadata, sealed header, and output root. +/// +/// # Components +/// - **Block Info**: L2 block metadata and consensus information +/// - **Sealed Header**: Complete block header with computed hash +/// - **Output Root**: State commitment for fraud proof verification +/// +/// # Usage +/// The tip cursor is used by the pipeline cursor to cache L2 safe head states +/// corresponding to different L1 origins, enabling efficient reorg recovery and +/// state management during derivation. +/// +/// # Immutability +/// Once created, a tip cursor represents an immutable snapshot of the L2 state. +/// New tip cursors are created as derivation progresses rather than mutating +/// existing ones. +#[derive(Debug, Clone)] +pub struct TipCursor { + /// The L2 block information for the safe head. + /// + /// Contains all the L2-specific metadata including block number, timestamp, + /// L1 origin information, and other consensus-critical data derived from + /// the L1 chain. + pub l2_safe_head: L2BlockInfo, + /// The sealed header of the L2 safe head block. + /// + /// The sealed header includes the complete block header with the computed + /// block hash, providing access to parent hash, state root, transaction root, + /// and other header fields needed for block validation. + pub l2_safe_head_header: Sealed

, + /// The output root computed for the L2 safe head state. + /// + /// The output root is a cryptographic commitment to the L2 state after + /// executing this block. It's used for fraud proof verification and + /// enables efficient state challenges without requiring full state data. + pub l2_safe_head_output_root: B256, +} + +impl TipCursor { + /// Creates a new tip cursor with the specified L2 safe head components. + /// + /// # Arguments + /// * `l2_safe_head` - The L2 block information for the safe head + /// * `l2_safe_head_header` - The sealed header of the L2 safe head block + /// * `l2_safe_head_output_root` - The computed output root for this state + /// + /// # Returns + /// A new [`TipCursor`] encapsulating the provided L2 safe head state + /// + /// # Usage + /// Called when the driver completes derivation of a new L2 block to create + /// a snapshot of the resulting safe head state for caching and reorg recovery. + pub const fn new( + l2_safe_head: L2BlockInfo, + l2_safe_head_header: Sealed
, + l2_safe_head_output_root: B256, + ) -> Self { + Self { l2_safe_head, l2_safe_head_header, l2_safe_head_output_root } + } + + /// Returns a reference to the L2 safe head block information. + /// + /// Provides access to the L2 block metadata including block number, timestamp, + /// L1 origin information, and other consensus-critical data. + pub const fn l2_safe_head(&self) -> &L2BlockInfo { + &self.l2_safe_head + } + + /// Returns a reference to the sealed header of the L2 safe head. + /// + /// The sealed header contains the complete block header with computed hash, + /// providing access to parent hash, state root, transaction root, and other + /// header fields needed for block validation and chain verification. + pub const fn l2_safe_head_header(&self) -> &Sealed
{ + &self.l2_safe_head_header + } + + /// Returns a reference to the output root of the L2 safe head. + /// + /// The output root is a cryptographic commitment to the L2 state after + /// executing this block. It enables fraud proof verification and efficient + /// state challenges without requiring access to the full state data. + pub const fn l2_safe_head_output_root(&self) -> &B256 { + &self.l2_safe_head_output_root + } +} diff --git a/kona/crates/proof/executor/CHANGELOG.md b/kona/crates/proof/executor/CHANGELOG.md new file mode 100644 index 0000000000000..9a3e3f788bc11 --- /dev/null +++ b/kona/crates/proof/executor/CHANGELOG.md @@ -0,0 +1,137 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.3](https://github.com/op-rs/kona/compare/kona-executor-v0.2.2...kona-executor-v0.2.3) - 2025-01-16 + +### Other + +- Update Maili Deps (#908) + +## [0.2.2](https://github.com/op-rs/kona/compare/kona-executor-v0.2.1...kona-executor-v0.2.2) - 2025-01-13 + +### Other + +- update Cargo.toml dependencies + +## [0.2.1](https://github.com/op-rs/kona/compare/kona-executor-v0.2.0...kona-executor-v0.2.1) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +### Other + +- Bump Dependencies (#880) +- remove redundant words in comment (#882) +- Isthmus Withdrawals Root (#881) + +## [0.2.0](https://github.com/op-rs/kona/compare/kona-executor-v0.1.0...kona-executor-v0.2.0) - 2024-12-03 + +### Added + +- *(driver)* refines the executor interface for the driver ([#850](https://github.com/op-rs/kona/pull/850)) + +### Fixed + +- bump ([#855](https://github.com/op-rs/kona/pull/855)) +- use non problematic hashmap fns ([#853](https://github.com/op-rs/kona/pull/853)) + +### Other + +- update deps and clean up misc features ([#864](https://github.com/op-rs/kona/pull/864)) + +## [0.0.6](https://github.com/op-rs/kona/compare/kona-executor-v0.0.5...kona-executor-v0.0.6) - 2024-11-20 + +### Added + +- *(mpt)* Extend `TrieProvider` in `kona-executor` ([#813](https://github.com/op-rs/kona/pull/813)) + +### Other + +- *(driver)* use tracing macros ([#823](https://github.com/op-rs/kona/pull/823)) +- *(workspace)* Migrate back to `thiserror` v2 ([#811](https://github.com/op-rs/kona/pull/811)) + +## [0.0.5](https://github.com/op-rs/kona/compare/kona-executor-v0.0.4...kona-executor-v0.0.5) - 2024-11-06 + +### Added + +- *(TrieProvider)* Abstract TrieNode retrieval ([#787](https://github.com/op-rs/kona/pull/787)) + +### Other + +- *(executor)* rm upstream util ([#755](https://github.com/op-rs/kona/pull/755)) + +## [0.0.4](https://github.com/op-rs/kona/compare/kona-executor-v0.0.3...kona-executor-v0.0.4) - 2024-10-29 + +### Other + +- update Cargo.toml dependencies + +## [0.0.3](https://github.com/op-rs/kona/compare/kona-executor-v0.0.2...kona-executor-v0.0.3) - 2024-10-25 + +### Added + +- remove thiserror ([#735](https://github.com/op-rs/kona/pull/735)) +- *(executor)* Clean ups ([#719](https://github.com/op-rs/kona/pull/719)) +- *(executor)* EIP-1559 configurability spec updates ([#716](https://github.com/op-rs/kona/pull/716)) +- *(executor)* Update EIP-1559 configurability ([#648](https://github.com/op-rs/kona/pull/648)) +- *(executor)* Use EIP-1559 parameters from payload attributes ([#616](https://github.com/op-rs/kona/pull/616)) +- *(derive)* bump op-alloy dep ([#605](https://github.com/op-rs/kona/pull/605)) +- kona-providers ([#596](https://github.com/op-rs/kona/pull/596)) +- *(executor)* Migrate to `thiserror` ([#544](https://github.com/op-rs/kona/pull/544)) +- *(mpt)* Migrate to `thiserror` ([#541](https://github.com/op-rs/kona/pull/541)) +- *(primitives)* Remove Attributes ([#529](https://github.com/op-rs/kona/pull/529)) +- large dependency update ([#528](https://github.com/op-rs/kona/pull/528)) + +### Fixed + +- *(executor)* Holocene EIP-1559 params in Header ([#622](https://github.com/op-rs/kona/pull/622)) +- *(workspace)* hoist and fix lints ([#577](https://github.com/op-rs/kona/pull/577)) + +### Other + +- re-org imports ([#711](https://github.com/op-rs/kona/pull/711)) +- *(workspace)* Removes Primitives ([#638](https://github.com/op-rs/kona/pull/638)) +- *(executor)* move todo to issue: ([#680](https://github.com/op-rs/kona/pull/680)) +- *(executor)* Cover Builder ([#676](https://github.com/op-rs/kona/pull/676)) +- *(executor)* Use Upstreamed op-alloy Methods ([#651](https://github.com/op-rs/kona/pull/651)) +- *(executor)* Test Coverage over Executor Utilities ([#650](https://github.com/op-rs/kona/pull/650)) +- doc logos ([#609](https://github.com/op-rs/kona/pull/609)) +- *(workspace)* Allow stdlib in `cfg(test)` ([#548](https://github.com/op-rs/kona/pull/548)) +- Bumps Dependency Versions ([#520](https://github.com/op-rs/kona/pull/520)) +- *(primitives)* rm RawTransaction ([#505](https://github.com/op-rs/kona/pull/505)) + +## [0.0.2](https://github.com/op-rs/kona/compare/kona-executor-v0.0.1...kona-executor-v0.0.2) - 2024-09-04 + +### Added +- *(executor)* Expose full revm Handler ([#475](https://github.com/op-rs/kona/pull/475)) +- *(workspace)* Workspace Re-exports ([#468](https://github.com/op-rs/kona/pull/468)) +- *(executor)* `StatelessL2BlockExecutor` benchmarks ([#350](https://github.com/op-rs/kona/pull/350)) +- *(executor)* Generic precompile overrides ([#340](https://github.com/op-rs/kona/pull/340)) +- *(executor)* Builder pattern for `StatelessL2BlockExecutor` ([#339](https://github.com/op-rs/kona/pull/339)) + +### Fixed +- *(workspace)* Use published `revm` version ([#459](https://github.com/op-rs/kona/pull/459)) +- downgrade for release plz ([#458](https://github.com/op-rs/kona/pull/458)) +- *(workspace)* Add Unused Dependency Lint ([#453](https://github.com/op-rs/kona/pull/453)) +- Don't hold onto intermediate execution cache across block boundaries ([#396](https://github.com/op-rs/kona/pull/396)) + +### Other +- *(workspace)* Alloy Version Bumps ([#467](https://github.com/op-rs/kona/pull/467)) +- *(workspace)* Update for `op-rs` org transfer ([#474](https://github.com/op-rs/kona/pull/474)) +- *(workspace)* Hoist Dependencies ([#466](https://github.com/op-rs/kona/pull/466)) +- refactor types out of kona-derive ([#454](https://github.com/op-rs/kona/pull/454)) +- *(deps)* Bump revm version to v13 ([#422](https://github.com/op-rs/kona/pull/422)) + +## [0.0.1](https://github.com/op-rs/kona/releases/tag/kona-executor-v0.0.1) - 2024-06-22 + +### Other +- *(workspace)* Prep release ([#301](https://github.com/op-rs/kona/pull/301)) +- version dependencies ([#296](https://github.com/op-rs/kona/pull/296)) +- *(deps)* fast forward op alloy dep ([#267](https://github.com/op-rs/kona/pull/267)) +- *(workspace)* `kona-executor` ([#259](https://github.com/op-rs/kona/pull/259)) diff --git a/kona/crates/proof/executor/Cargo.toml b/kona/crates/proof/executor/Cargo.toml new file mode 100644 index 0000000000000..a2fa25c6e3c8f --- /dev/null +++ b/kona/crates/proof/executor/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "kona-executor" +description = "A no_std stateless block builder for the OP Stack" +version = "0.4.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-mpt.workspace = true +kona-genesis = { workspace = true, features = ["revm"] } +kona-protocol.workspace = true + +# Alloy +alloy-consensus = { workspace = true, features = ["k256"] } +alloy-primitives = { workspace = true, features = ["rlp"] } +alloy-eips.workspace = true +alloy-rlp.workspace = true +alloy-trie.workspace = true + +# Op Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine = { workspace = true, features = ["serde", "k256"] } +alloy-op-hardforks.workspace = true + +# revm +op-revm.workspace = true +revm.workspace = true + +# alloy-evm +alloy-op-evm.workspace = true +alloy-evm = { workspace = true, features = ["op"] } + +# General +thiserror.workspace = true +tracing.workspace = true + +# `test-utils` feature +rand = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"], optional = true } +tokio = { workspace = true, features = ["full"], optional = true } +rstest = { workspace = true, optional = true } +kona-registry = { workspace = true, optional = true } +rocksdb = { workspace = true, features = ["snappy"], optional = true } +tempfile = { workspace = true, optional = true } +alloy-rpc-types-engine = { workspace = true, optional = true } +alloy-provider = { workspace = true, features = ["reqwest"], optional = true } +alloy-rpc-client = { workspace = true, optional = true } +alloy-transport = { workspace = true, optional = true } +alloy-transport-http = { workspace = true, optional = true } + +[features] +test-utils = [ + "dep:alloy-provider", + "dep:alloy-rpc-client", + "dep:alloy-rpc-types-engine", + "dep:alloy-transport", + "dep:alloy-transport-http", + "dep:kona-registry", + "dep:rand", + "dep:rocksdb", + "dep:rstest", + "dep:serde", + "dep:serde_json", + "dep:tempfile", + "dep:tokio", + "kona-protocol/test-utils", +] diff --git a/kona/crates/proof/executor/README.md b/kona/crates/proof/executor/README.md new file mode 100644 index 0000000000000..0f3fbbc128a1a --- /dev/null +++ b/kona/crates/proof/executor/README.md @@ -0,0 +1,8 @@ +# `kona-executor` + +CI +Kona Stateless Executor +License +Codecov + +A `no_std` implementation of a stateless block executor for the OP stack, backed by [`kona-mpt`](../mpt)'s `TrieDB`. diff --git a/kona/crates/proof/executor/src/builder/assemble.rs b/kona/crates/proof/executor/src/builder/assemble.rs new file mode 100644 index 0000000000000..597fe5e96afeb --- /dev/null +++ b/kona/crates/proof/executor/src/builder/assemble.rs @@ -0,0 +1,198 @@ +//! [Header] assembly logic for the [StatelessL2Builder]. + +use super::StatelessL2Builder; +use crate::{ + ExecutorError, ExecutorResult, TrieDBError, TrieDBProvider, + util::{encode_holocene_eip_1559_params, encode_jovian_eip_1559_params}, +}; +use alloc::vec::Vec; +use alloy_consensus::{EMPTY_OMMER_ROOT_HASH, Header, Sealed}; +use alloy_eips::{Encodable2718, eip7685::EMPTY_REQUESTS_HASH}; +use alloy_evm::{EvmFactory, block::BlockExecutionResult}; +use alloy_primitives::{B256, Sealable, U256, logs_bloom}; +use alloy_trie::EMPTY_ROOT_HASH; +use kona_genesis::RollupConfig; +use kona_mpt::{TrieHinter, ordered_trie_with_encoder}; +use kona_protocol::{OutputRoot, Predeploys}; +use op_alloy_consensus::OpReceiptEnvelope; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use revm::{context::BlockEnv, database::BundleState}; + +impl StatelessL2Builder<'_, P, H, Evm> +where + P: TrieDBProvider, + H: TrieHinter, + Evm: EvmFactory, +{ + /// Seals the block executed from the given [OpPayloadAttributes] and [BlockEnv], returning the + /// computed [Header]. + pub(crate) fn seal_block( + &mut self, + attrs: &OpPayloadAttributes, + parent_hash: B256, + block_env: &BlockEnv, + ex_result: &BlockExecutionResult, + bundle: BundleState, + ) -> ExecutorResult> { + let timestamp = block_env.timestamp.saturating_to::(); + + // Compute the roots for the block header. + let state_root = self.trie_db.state_root(&bundle)?; + let transactions_root = ordered_trie_with_encoder( + // SAFETY: The OP Stack protocol will never generate a payload attributes with an empty + // transactions field. Panicking here is the desired behavior, as it indicates a severe + // protocol violation. + attrs.transactions.as_ref().expect("Transactions must be non-empty"), + |tx, buf| buf.put_slice(tx.as_ref()), + ) + .root(); + let receipts_root = compute_receipts_root(&ex_result.receipts, self.config, timestamp); + let withdrawals_root = if self.config.is_isthmus_active(timestamp) { + Some(self.message_passer_account(block_env.number.saturating_to::())?) + } else if self.config.is_canyon_active(timestamp) { + Some(EMPTY_ROOT_HASH) + } else { + None + }; + + // Compute the logs bloom from the receipts generated during block execution. + let logs_bloom = logs_bloom(ex_result.receipts.iter().flat_map(|r| r.logs())); + + // Compute Cancun fields, if active. + let (blob_gas_used, excess_blob_gas) = if self.config.is_jovian_active(timestamp) { + (Some(ex_result.blob_gas_used), Some(0)) + } else if self.config.is_ecotone_active(timestamp) { + (Some(0), Some(0)) + } else { + Default::default() + }; + + // At holocene activation, the base fee parameters from the payload are placed + // into the Header's `extra_data` field. + // + // If the payload's `eip_1559_params` are equal to `0`, then the header's `extraData` + // field is set to the encoded canyon base fee parameters. + let encoded_base_fee_params = match self.config { + config if config.is_jovian_active(timestamp) => { + let extra_data = encode_jovian_eip_1559_params(self.config, attrs)?; + Ok(extra_data) + } + config if config.is_holocene_active(timestamp) => { + encode_holocene_eip_1559_params(self.config, attrs) + } + _ => Ok(Default::default()), + }?; + + // The requests hash on the OP Stack, if Isthmus is active, is always the empty SHA256 hash. + let requests_hash = self.config.is_isthmus_active(timestamp).then_some(EMPTY_REQUESTS_HASH); + + // Construct the new header. + let header = Header { + parent_hash, + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: attrs.payload_attributes.suggested_fee_recipient, + state_root, + transactions_root, + receipts_root, + withdrawals_root, + requests_hash, + logs_bloom, + difficulty: U256::ZERO, + number: block_env.number.saturating_to::(), + gas_limit: attrs.gas_limit.ok_or(ExecutorError::MissingGasLimit)?, + gas_used: ex_result.gas_used, + timestamp, + mix_hash: attrs.payload_attributes.prev_randao, + nonce: Default::default(), + base_fee_per_gas: Some(block_env.basefee), + blob_gas_used, + excess_blob_gas: excess_blob_gas.and_then(|x| x.try_into().ok()), + parent_beacon_block_root: attrs.payload_attributes.parent_beacon_block_root, + extra_data: encoded_base_fee_params, + } + .seal_slow(); + + Ok(header) + } + + /// Computes the current output root of the latest executed block, based on the parent header + /// and the underlying state trie. + /// + /// **CONSTRUCTION:** + /// ```text + /// output_root = keccak256(version_byte .. payload) + /// payload = state_root .. withdrawal_storage_root .. latest_block_hash + /// ``` + pub fn compute_output_root(&mut self) -> ExecutorResult { + let parent_number = self.trie_db.parent_block_header().number; + + info!( + target: "block_builder", + parent_state_root = ?self.trie_db.parent_block_header().state_root, + parent_block_number = parent_number, + "Computing output root", + ); + + let storage_root = self.message_passer_account(parent_number)?; + let parent_header = self.trie_db.parent_block_header(); + + // Construct the raw output and hash it. + let output_root_hash = + OutputRoot::from_parts(parent_header.state_root, storage_root, parent_header.seal()) + .hash(); + + info!( + target: "block_builder", + parent_block_number = parent_number, + output_root = ?output_root_hash, + "Computed output root", + ); + + // Hash the output and return + Ok(output_root_hash) + } + + /// Fetches the L2 to L1 message passer account from the cache or underlying trie. + fn message_passer_account(&mut self, block_number: u64) -> Result { + match self.trie_db.storage_roots().get(&Predeploys::L2_TO_L1_MESSAGE_PASSER) { + Some(storage_root) => Ok(storage_root.blind()), + None => Ok(self + .trie_db + .get_trie_account(&Predeploys::L2_TO_L1_MESSAGE_PASSER, block_number)? + .ok_or(TrieDBError::MissingAccountInfo)? + .storage_root), + } + } +} + +/// Computes the receipts root from the given set of receipts. +pub fn compute_receipts_root( + receipts: &[OpReceiptEnvelope], + config: &RollupConfig, + timestamp: u64, +) -> B256 { + // There is a minor bug in op-geth and op-erigon where in the Regolith hardfork, + // the receipt root calculation does not include the deposit nonce in the + // receipt encoding. In the Regolith hardfork, we must strip the deposit nonce + // from the receipt encoding to match the receipt root calculation. + if config.is_regolith_active(timestamp) && !config.is_canyon_active(timestamp) { + let receipts = receipts + .iter() + .cloned() + .map(|receipt| match receipt { + OpReceiptEnvelope::Deposit(mut deposit_receipt) => { + deposit_receipt.receipt.deposit_nonce = None; + OpReceiptEnvelope::Deposit(deposit_receipt) + } + _ => receipt, + }) + .collect::>(); + + ordered_trie_with_encoder(receipts.as_ref(), |receipt, mut buf| { + receipt.encode_2718(&mut buf) + }) + .root() + } else { + ordered_trie_with_encoder(receipts, |receipt, mut buf| receipt.encode_2718(&mut buf)).root() + } +} diff --git a/kona/crates/proof/executor/src/builder/core.rs b/kona/crates/proof/executor/src/builder/core.rs new file mode 100644 index 0000000000000..407c82a7c549d --- /dev/null +++ b/kona/crates/proof/executor/src/builder/core.rs @@ -0,0 +1,333 @@ +//! Stateless OP Stack L2 block builder implementation. +//! +//! The [StatelessL2Builder] provides a complete block building and execution engine +//! for OP Stack L2 chains that operates in a stateless manner, pulling required state +//! data from a [TrieDB] during execution rather than maintaining full state. + +use crate::{ExecutorError, ExecutorResult, TrieDB, TrieDBError, TrieDBProvider}; +use alloc::{string::ToString, vec::Vec}; +use alloy_consensus::{Header, Sealed, crypto::RecoveryError}; +use alloy_evm::{ + EvmFactory, FromRecoveredTx, FromTxWithEncoded, + block::{BlockExecutionResult, BlockExecutor, BlockExecutorFactory}, +}; +use alloy_op_evm::{ + OpBlockExecutionCtx, OpBlockExecutorFactory, + block::{OpAlloyReceiptBuilder, OpTxEnv}, +}; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_mpt::TrieHinter; +use op_alloy_consensus::{OpReceiptEnvelope, OpTxEnvelope}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use op_revm::OpSpecId; +use revm::{ + context::BlockEnv, + database::{State, states::bundle_state::BundleRetention}, +}; + +/// Stateless OP Stack L2 block builder that derives state from trie proofs during execution. +/// +/// The [`StatelessL2Builder`] is a specialized block execution engine designed for fault proof +/// systems and stateless verification. Instead of maintaining full L2 state, it dynamically +/// retrieves required state data from a [`TrieDB`] backed by Merkle proofs and witnesses. +/// +/// # Architecture +/// +/// The builder operates in a stateless manner by: +/// 1. **Trie Database**: Uses [`TrieDB`] to access state via Merkle proofs +/// 2. **EVM Factory**: Creates execution environments with proof-backed state +/// 3. **Block Executor**: Executes transactions using witness-provided state +/// 4. **Receipt Generation**: Produces execution receipts and state commitments +/// +/// # Stateless Execution Model +/// +/// Traditional execution engines maintain full state databases, but the stateless model: +/// - Receives state witnesses containing only required data +/// - Verifies state access against Merkle proofs +/// - Executes transactions without persistent state storage +/// - Produces verifiable execution results and state commitments +/// +/// # Use Cases +/// +/// ## Fault Proof Systems +/// - Enables dispute resolution without full state replication +/// - Provides verifiable execution results for challenge games +/// - Supports optimistic rollup fraud proof generation +/// +/// ## Stateless Verification +/// - Allows third parties to verify L2 blocks without full state +/// - Enables light clients to validate L2 execution +/// - Supports decentralized verification networks +/// +/// # Performance Characteristics +/// +/// - **Memory**: Lower memory usage than stateful execution (no full state) +/// - **I/O**: Higher I/O for proof verification and witness access +/// - **CPU**: Additional overhead for cryptographic proof verification +/// - **Determinism**: Guaranteed deterministic execution results +/// +/// # Type Parameters +/// +/// * `P` - Trie database provider implementing [`TrieDBProvider`] +/// * `H` - Trie hinter implementing [`TrieHinter`] for state access optimization +/// * `Evm` - EVM factory implementing [`EvmFactory`] for execution environment creation +#[derive(Debug)] +pub struct StatelessL2Builder<'a, P, H, Evm> +where + P: TrieDBProvider, + H: TrieHinter, + Evm: EvmFactory, +{ + /// The rollup configuration containing chain parameters and activation heights. + /// + /// Provides access to network-specific parameters including gas limits, + /// hard fork activation heights, and system addresses needed for proper + /// L2 block execution and validation. + pub(crate) config: &'a RollupConfig, + /// The trie database providing stateless access to L2 state via Merkle proofs. + /// + /// The [`TrieDB`] serves as the primary interface for state access during + /// execution, resolving account and storage queries using witness data + /// and cryptographic proofs rather than a traditional state database. + pub(crate) trie_db: TrieDB, + /// The block executor factory for creating OP Stack execution environments. + /// + /// This factory creates specialized OP Stack execution environments that + /// understand OP-specific transaction types, system calls, and state + /// management required for proper L2 block execution. + pub(crate) factory: OpBlockExecutorFactory, +} + +impl<'a, P, H, Evm> StatelessL2Builder<'a, P, H, Evm> +where + P: TrieDBProvider + Debug, + H: TrieHinter + Debug, + Evm: EvmFactory + 'static, + ::Tx: + FromTxWithEncoded + FromRecoveredTx + OpTxEnv, +{ + /// Creates a new stateless L2 block builder instance. + /// + /// Initializes the builder with the necessary components for stateless block execution + /// including the trie database, execution factory, and rollup configuration. + /// + /// # Arguments + /// * `config` - Rollup configuration with chain parameters and activation heights + /// * `evm_factory` - EVM factory for creating execution environments + /// * `provider` - Trie database provider for state access + /// * `hinter` - Trie hinter for optimizing state access patterns + /// * `parent_header` - Sealed header of the parent block to build upon + /// + /// # Returns + /// A new [`StatelessL2Builder`] ready for block building operations + /// + /// # Usage + /// ```rust,ignore + /// let builder = StatelessL2Builder::new( + /// &rollup_config, + /// evm_factory, + /// trie_provider, + /// trie_hinter, + /// parent_header, + /// ); + /// ``` + pub fn new( + config: &'a RollupConfig, + evm_factory: Evm, + provider: P, + hinter: H, + parent_header: Sealed
, + ) -> Self { + let trie_db = TrieDB::new(parent_header, provider, hinter); + let factory = OpBlockExecutorFactory::new( + OpAlloyReceiptBuilder::default(), + config.clone(), + evm_factory, + ); + Self { config, trie_db, factory } + } + + /// Builds and executes a new L2 block using the provided payload attributes. + /// + /// This method performs the complete block building and execution process in a stateless + /// manner, dynamically retrieving required state data via the trie database and producing + /// a fully executed block with receipts and state commitments. + /// + /// # Arguments + /// * `attrs` - Payload attributes containing transactions and block metadata + /// + /// # Returns + /// * `Ok(BlockBuildingOutcome)` - Successfully built and executed block with receipts + /// * `Err(ExecutorError)` - Block building or execution failure + /// + /// # Errors + /// This method can fail due to various conditions: + /// + /// ## Input Validation Errors + /// - [`ExecutorError::MissingGasLimit`]: Gas limit not provided in attributes + /// - [`ExecutorError::MissingTransactions`]: Transaction list not provided + /// - [`ExecutorError::MissingEIP1559Params`]: Required fee parameters missing (post-Holocene) + /// - [`ExecutorError::MissingParentBeaconBlockRoot`]: Beacon root missing (post-Dencun) + /// + /// ## Execution Errors + /// - [`ExecutorError::BlockGasLimitExceeded`]: Cumulative gas exceeds block limit + /// - [`ExecutorError::UnsupportedTransactionType`]: Unknown transaction type encountered + /// - [`ExecutorError::ExecutionError`]: EVM-level execution failures + /// + /// ## State Access Errors + /// - [`ExecutorError::TrieDBError`]: State tree access or proof verification failures + /// - Missing account data in witness + /// - Invalid Merkle proofs + /// + /// ## Data Integrity Errors + /// - [`ExecutorError::Recovery`]: Transaction signature recovery failures + /// - [`ExecutorError::RLPError`]: Data encoding/decoding errors + /// + /// # Block Building Process + /// + /// The block building process follows these steps: + /// + /// 1. **Environment Setup**: Configure EVM environment with proper gas settings + /// 2. **Witness Hinting**: Send payload witness hints to optimize state access + /// 3. **Transaction Execution**: Execute each transaction in order with state updates + /// 4. **Receipt Generation**: Generate execution receipts for all transactions + /// 5. **State Commitment**: Compute final state roots and output commitments + /// 6. **Block Assembly**: Assemble complete block with header and execution results + /// + /// # Stateless Execution Details + /// + /// Unlike traditional execution engines, this builder: + /// - Resolves state access via Merkle proofs instead of database lookups + /// - Validates all state access against cryptographic witnesses + /// - Produces deterministic results independent of execution environment + /// - Enables verification without full state replication + /// + /// # Performance Considerations + /// + /// - State access latency depends on proof verification overhead + /// - Memory usage scales with witness size rather than full state + /// - CPU overhead from cryptographic proof verification + /// - I/O patterns optimized through trie hinter guidance + pub fn build_block( + &mut self, + attrs: OpPayloadAttributes, + ) -> ExecutorResult { + // Step 1. Set up the execution environment. + let (base_fee_params, min_base_fee) = Self::active_base_fee_params( + self.config, + self.trie_db.parent_block_header(), + attrs.payload_attributes.timestamp, + )?; + let evm_env = self.evm_env( + self.config.spec_id(attrs.payload_attributes.timestamp), + self.trie_db.parent_block_header(), + &attrs, + &base_fee_params, + min_base_fee, + )?; + let block_env = evm_env.block_env().clone(); + let parent_hash = self.trie_db.parent_block_header().seal(); + + // Attempt to send a payload witness hint to the host. This hint instructs the host to + // populate its preimage store with the preimages required to statelessly execute + // this payload. This feature is experimental, so if the hint fails, we continue + // without it and fall back on on-demand preimage fetching for execution. + self.trie_db + .hinter + .hint_execution_witness(parent_hash, &attrs) + .map_err(|e| TrieDBError::Provider(e.to_string()))?; + + info!( + target: "block_builder", + block_number = %block_env.number, + block_timestamp = %block_env.timestamp, + block_gas_limit = block_env.gas_limit, + transactions = attrs.transactions.as_ref().map_or(0, |txs| txs.len()), + "Beginning block building." + ); + + // Step 2. Create the executor, using the trie database. + let mut state = State::builder() + .with_database(&mut self.trie_db) + .with_bundle_update() + .without_state_clear() + .build(); + let evm = self.factory.evm_factory().create_evm(&mut state, evm_env); + let ctx = OpBlockExecutionCtx { + parent_hash, + parent_beacon_block_root: attrs.payload_attributes.parent_beacon_block_root, + // This field is unused for individual block building jobs. + extra_data: Default::default(), + }; + let executor = self.factory.create_executor(evm, ctx); + + // Step 3. Execute the block containing the transactions within the payload attributes. + let transactions = attrs + .recovered_transactions_with_encoded() + .collect::, RecoveryError>>() + .map_err(ExecutorError::Recovery)?; + let ex_result = executor.execute_block(transactions.iter())?; + + info!( + target: "block_builder", + gas_used = ex_result.gas_used, + gas_limit = block_env.gas_limit, + "Finished block building. Beginning sealing job." + ); + + // Step 4. Merge state transitions and seal the block. + state.merge_transitions(BundleRetention::Reverts); + let bundle = state.take_bundle(); + let header = self.seal_block(&attrs, parent_hash, &block_env, &ex_result, bundle)?; + + info!( + target: "block_builder", + number = header.number, + hash = ?header.seal(), + state_root = ?header.state_root, + transactions_root = ?header.transactions_root, + receipts_root = ?header.receipts_root, + "Sealed new block", + ); + + // Update the parent block hash in the state database, preparing for the next block. + self.trie_db.set_parent_block_header(header.clone()); + Ok((header, ex_result).into()) + } +} + +/// The outcome of a block building operation, returning the sealed block [`Header`] and the +/// [`BlockExecutionResult`]. +#[derive(Debug, Clone)] +pub struct BlockBuildingOutcome { + /// The block header. + pub header: Sealed
, + /// The block execution result. + pub execution_result: BlockExecutionResult, +} + +impl From<(Sealed
, BlockExecutionResult)> for BlockBuildingOutcome { + fn from( + (header, execution_result): (Sealed
, BlockExecutionResult), + ) -> Self { + Self { header, execution_result } + } +} + +#[cfg(test)] +mod test { + use crate::test_utils::run_test_fixture; + use rstest::rstest; + use std::path::PathBuf; + + #[rstest] + #[tokio::test] + async fn test_statelessly_execute_block( + #[base_dir = "./testdata"] + #[files("*.tar.gz")] + path: PathBuf, + ) { + run_test_fixture(path).await; + } +} diff --git a/kona/crates/proof/executor/src/builder/env.rs b/kona/crates/proof/executor/src/builder/env.rs new file mode 100644 index 0000000000000..a4a8f39938991 --- /dev/null +++ b/kona/crates/proof/executor/src/builder/env.rs @@ -0,0 +1,167 @@ +//! Environment utility functions for [StatelessL2Builder]. + +use super::StatelessL2Builder; +use crate::{ + ExecutorError, ExecutorResult, TrieDBProvider, + util::{ + decode_holocene_eip_1559_params_block_header, decode_jovian_eip_1559_params_block_header, + }, +}; +use alloy_consensus::{BlockHeader, Header}; +use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams}; +use alloy_evm::{EvmEnv, EvmFactory}; +use alloy_primitives::U256; +use kona_genesis::RollupConfig; +use kona_mpt::TrieHinter; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use op_revm::OpSpecId; +use revm::{ + context::{BlockEnv, CfgEnv}, + context_interface::block::BlobExcessGasAndPrice, + primitives::eip4844::{ + BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE, + }, +}; + +impl StatelessL2Builder<'_, P, H, Evm> +where + P: TrieDBProvider, + H: TrieHinter, + Evm: EvmFactory, +{ + /// Returns the active [`EvmEnv`] for the executor. + pub(crate) fn evm_env( + &self, + spec_id: OpSpecId, + parent_header: &Header, + payload_attrs: &OpPayloadAttributes, + base_fee_params: &BaseFeeParams, + min_base_fee: u64, + ) -> ExecutorResult> { + let block_env = self.prepare_block_env( + spec_id, + parent_header, + payload_attrs, + base_fee_params, + min_base_fee, + )?; + let cfg_env = self.evm_cfg_env(payload_attrs.payload_attributes.timestamp); + Ok(EvmEnv::new(cfg_env, block_env)) + } + + /// Returns the active [CfgEnv] for the executor. + pub(crate) fn evm_cfg_env(&self, timestamp: u64) -> CfgEnv { + CfgEnv::new() + .with_chain_id(self.config.l2_chain_id.id()) + .with_spec(self.config.spec_id(timestamp)) + } + + fn next_block_base_fee( + &self, + params: BaseFeeParams, + parent: &Header, + min_base_fee: u64, + ) -> Option { + if !self.config.is_jovian_active(parent.timestamp()) { + return parent.next_block_base_fee(params); + } + + // Starting from Jovian, we use the maximum of the gas used and the blob gas used to + // calculate the next base fee. + let gas_used = if parent.blob_gas_used().unwrap_or_default() > parent.gas_used() { + parent.blob_gas_used().unwrap_or_default() + } else { + parent.gas_used() + }; + + let mut next_block_base_fee = calc_next_block_base_fee( + gas_used, + parent.gas_limit(), + parent.base_fee_per_gas().unwrap_or_default(), + params, + ); + + // If the next block base fee is less than the min base fee, set it to the min base fee. + // # Note + // Before Jovian activation, the min-base-fee is 0 so this is a no-op. + if next_block_base_fee < min_base_fee { + next_block_base_fee = min_base_fee; + } + + Some(next_block_base_fee) + } + + /// Prepares a [BlockEnv] with the given [OpPayloadAttributes]. + pub(crate) fn prepare_block_env( + &self, + spec_id: OpSpecId, + parent_header: &Header, + payload_attrs: &OpPayloadAttributes, + base_fee_params: &BaseFeeParams, + min_base_fee: u64, + ) -> ExecutorResult { + let (params, fraction) = if spec_id.is_enabled_in(OpSpecId::ISTHMUS) { + (Some(BlobParams::prague()), BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE) + } else if spec_id.is_enabled_in(OpSpecId::ECOTONE) { + (Some(BlobParams::cancun()), BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN) + } else { + (None, 0) + }; + + let blob_excess_gas_and_price = parent_header + .maybe_next_block_excess_blob_gas(params) + .or_else(|| spec_id.is_enabled_in(OpSpecId::ECOTONE).then_some(0)) + .map(|excess| BlobExcessGasAndPrice::new(excess, fraction)); + + let next_block_base_fee = self + .next_block_base_fee(*base_fee_params, parent_header, min_base_fee) + .unwrap_or_default(); + + Ok(BlockEnv { + number: U256::from(parent_header.number + 1), + beneficiary: payload_attrs.payload_attributes.suggested_fee_recipient, + timestamp: U256::from(payload_attrs.payload_attributes.timestamp), + gas_limit: payload_attrs.gas_limit.ok_or(ExecutorError::MissingGasLimit)?, + basefee: next_block_base_fee, + prevrandao: Some(payload_attrs.payload_attributes.prev_randao), + blob_excess_gas_and_price, + ..Default::default() + }) + } + + /// Returns the active base fee parameters for the parent header. + /// Returns the min-base-fee as the second element of the tuple. + /// + /// ## Note + /// Before Jovian activation, the min-base-fee is 0. + pub(crate) fn active_base_fee_params( + config: &RollupConfig, + parent_header: &Header, + payload_timestamp: u64, + ) -> ExecutorResult<(BaseFeeParams, u64)> { + match config { + // After Holocene activation, the base fee parameters are stored in the + // `extraData` field of the parent header. If Holocene wasn't active in the + // parent block, the default base fee parameters are used. + _ if config.is_jovian_active(parent_header.timestamp) => { + decode_jovian_eip_1559_params_block_header(parent_header) + } + _ if config.is_holocene_active(parent_header.timestamp) => { + decode_holocene_eip_1559_params_block_header(parent_header) + .map(|base_fee_params| (base_fee_params, 0)) + } + // If the next payload attribute timestamp is past canyon activation, + // use the canyon base fee params from the rollup config. + _ if config.is_canyon_active(payload_timestamp) => { + // If the payload attribute timestamp is past canyon activation, + // use the canyon base fee params from the rollup config. + Ok((config.chain_op_config.as_canyon_base_fee_params(), 0)) + } + _ => { + // If the next payload attribute timestamp is prior to canyon activation, + // use the default base fee params from the rollup config. + Ok((config.chain_op_config.as_base_fee_params(), 0)) + } + } + } +} diff --git a/kona/crates/proof/executor/src/builder/mod.rs b/kona/crates/proof/executor/src/builder/mod.rs new file mode 100644 index 0000000000000..53f140db85eb7 --- /dev/null +++ b/kona/crates/proof/executor/src/builder/mod.rs @@ -0,0 +1,9 @@ +//! Stateless OP Stack block builder implementation. + +mod core; +pub use core::{BlockBuildingOutcome, StatelessL2Builder}; + +mod assemble; +pub use assemble::compute_receipts_root; + +mod env; diff --git a/kona/crates/proof/executor/src/db/mod.rs b/kona/crates/proof/executor/src/db/mod.rs new file mode 100644 index 0000000000000..5141b8aa1d2cc --- /dev/null +++ b/kona/crates/proof/executor/src/db/mod.rs @@ -0,0 +1,477 @@ +//! This module contains an implementation of an in-memory Trie DB for [`revm`], that allows for +//! incremental updates through fetching node preimages on the fly during execution. + +use crate::errors::{TrieDBError, TrieDBResult}; +use alloc::{string::ToString, vec::Vec}; +use alloy_consensus::{EMPTY_ROOT_HASH, Header, Sealed}; +use alloy_primitives::{Address, B256, U256, keccak256}; +use alloy_rlp::{Decodable, Encodable}; +use alloy_trie::TrieAccount; +use kona_mpt::{Nibbles, TrieHinter, TrieNode, TrieNodeError}; +use revm::{ + Database, + database::{BundleState, states::StorageSlot}, + primitives::{BLOCK_HASH_HISTORY, HashMap}, + state::{AccountInfo, Bytecode}, +}; + +mod traits; +pub use traits::{NoopTrieDBProvider, TrieDBProvider}; + +/// A Trie DB that caches open state in-memory. +/// +/// When accounts that don't already exist within the cached [`TrieNode`] are queried, the database +/// fetches the preimages of the trie nodes on the path to the account using the `PreimageFetcher` +/// (`F` generic). This allows for data to be fetched in a verifiable manner given an initial +/// trusted state root as it is needed during execution. +/// +/// The [`TrieDB`] is intended to be wrapped by a [`State`], which is then used by [`revm`] to +/// capture state transitions during block execution. +/// +/// **Behavior**: +/// - When an account is queried and the trie path has not already been opened by [Self::basic], we +/// fall through to the `PreimageFetcher` to fetch the preimages of the trie nodes on the path to +/// the account. After it has been fetched, the path will be cached until the next call to +/// [Self::state_root]. +/// - When querying for the code hash of an account, the [`TrieDBProvider`] is consulted to fetch +/// the code hash of the account. +/// - When a [`BundleState`] changeset is committed to the parent [`State`] database, the changes +/// are first applied to the [`State`]'s cache, then the trie hash is recomputed with +/// [Self::state_root]. +/// - When the block hash of a block number is needed via [Self::block_hash], the +/// `HeaderByHashFetcher` is consulted to walk back to the desired block number by revealing the +/// parent hash of block headers until the desired block number is reached, up to a maximum of +/// [BLOCK_HASH_HISTORY] blocks back relative to the current parent block hash. +/// +/// **Example Construction**: +/// ```rust +/// use alloy_consensus::{Header, Sealable}; +/// use alloy_evm::{EvmEnv, EvmFactory, block::BlockExecutorFactory}; +/// use alloy_op_evm::{ +/// OpBlockExecutionCtx, OpBlockExecutorFactory, OpEvmFactory, block::OpAlloyReceiptBuilder, +/// }; +/// use alloy_op_hardforks::OpChainHardforks; +/// use alloy_primitives::{B256, Bytes}; +/// use kona_executor::{NoopTrieDBProvider, TrieDB}; +/// use kona_mpt::NoopTrieHinter; +/// use revm::database::{State, states::bundle_state::BundleRetention}; +/// +/// let mock_parent_block_header = Header::default(); +/// let trie_db = +/// TrieDB::new(mock_parent_block_header.seal_slow(), NoopTrieDBProvider, NoopTrieHinter); +/// let executor_factory = OpBlockExecutorFactory::new( +/// OpAlloyReceiptBuilder::default(), +/// OpChainHardforks::op_mainnet(), +/// OpEvmFactory::default(), +/// ); +/// let mut state = State::builder().with_database(trie_db).with_bundle_update().build(); +/// let evm = executor_factory.evm_factory().create_evm(&mut state, EvmEnv::default()); +/// let executor = executor_factory.create_executor(evm, OpBlockExecutionCtx::default()); +/// +/// // Execute your block's transactions... +/// drop(executor); +/// +/// state.merge_transitions(BundleRetention::Reverts); +/// let bundle = state.take_bundle(); +/// let state_root = state.database.state_root(&bundle).expect("Failed to compute state root"); +/// ``` +/// +/// [`State`]: revm::database::State +#[derive(Debug, Clone)] +pub struct TrieDB +where + F: TrieDBProvider, + H: TrieHinter, +{ + /// The [`TrieNode`] representation of the root node. + root_node: TrieNode, + /// Storage roots of accounts within the trie. + storage_roots: HashMap, + /// The parent block hash of the current block. + parent_block_header: Sealed
, + /// The [`TrieDBProvider`] + pub fetcher: F, + /// The [`TrieHinter`] + pub hinter: H, +} + +impl TrieDB +where + F: TrieDBProvider, + H: TrieHinter, +{ + /// Creates a new [TrieDB] with the given root node. + pub fn new(parent_block_header: Sealed
, fetcher: F, hinter: H) -> Self { + Self { + root_node: TrieNode::new_blinded(parent_block_header.state_root), + storage_roots: Default::default(), + parent_block_header, + fetcher, + hinter, + } + } + + /// Consumes `Self` and takes the current state root of the trie DB. + pub fn take_root_node(self) -> TrieNode { + self.root_node + } + + /// Returns a shared reference to the root [TrieNode] of the trie DB. + pub const fn root(&self) -> &TrieNode { + &self.root_node + } + + /// Returns the mapping of [Address]es to storage roots. + pub const fn storage_roots(&self) -> &HashMap { + &self.storage_roots + } + + /// Returns a reference to the current parent block header of the trie DB. + pub const fn parent_block_header(&self) -> &Sealed
{ + &self.parent_block_header + } + + /// Sets the parent block header of the trie DB. Should be called after a block has been + /// executed and the Header has been created. + /// + /// ## Takes + /// - `parent_block_header`: The parent block header of the current block. + pub fn set_parent_block_header(&mut self, parent_block_header: Sealed
) { + self.parent_block_header = parent_block_header; + } + + /// Applies a [BundleState] changeset to the [TrieNode] and recomputes the state root hash. + /// + /// ## Takes + /// - `bundle`: The [BundleState] changeset to apply to the trie DB. + /// + /// ## Returns + /// - `Ok(B256)`: The new state root hash of the trie DB. + /// - `Err(_)`: If the state root hash could not be computed. + pub fn state_root(&mut self, bundle: &BundleState) -> TrieDBResult { + debug!(target: "client_executor", "Recomputing state root"); + + // Update the accounts in the trie with the changeset. + self.update_accounts(bundle)?; + + // Recompute the root hash of the trie. + let root = self.root_node.blind(); + + debug!( + target: "client_executor", + "Recomputed state root: {root}", + ); + + // Extract the new state root from the root node. + Ok(root) + } + + /// Fetches the [TrieAccount] of an account from the trie DB. + /// + /// ## Takes + /// - `address`: The address of the account. + /// + /// ## Returns + /// - `Ok(Some(TrieAccount))`: The [TrieAccount] of the account. + /// - `Ok(None)`: If the account does not exist in the trie. + /// - `Err(_)`: If the account could not be fetched. + pub fn get_trie_account( + &mut self, + address: &Address, + block_number: u64, + ) -> TrieDBResult> { + // Send a hint to the host to fetch the account proof. + self.hinter + .hint_account_proof(*address, block_number) + .map_err(|e| TrieDBError::Provider(e.to_string()))?; + + // Fetch the account from the trie. + let hashed_address_nibbles = Nibbles::unpack(keccak256(address.as_slice())); + let Some(trie_account_rlp) = self.root_node.open(&hashed_address_nibbles, &self.fetcher)? + else { + return Ok(None); + }; + + // Decode the trie account from the RLP bytes. + TrieAccount::decode(&mut trie_account_rlp.as_ref()) + .map_err(TrieNodeError::RLPError) + .map_err(Into::into) + .map(Some) + } + + /// Modifies the accounts in the storage trie with the given [BundleState] changeset. + /// + /// ## Takes + /// - `bundle`: The [BundleState] changeset to apply to the trie DB. + /// + /// ## Returns + /// - `Ok(())` if the accounts were successfully updated. + /// - `Err(_)` if the accounts could not be updated. + fn update_accounts(&mut self, bundle: &BundleState) -> TrieDBResult<()> { + // Sort the storage keys prior to applying the changeset, to ensure that the order of + // application is deterministic between runs. + let mut sorted_state = + bundle.state().iter().map(|(k, v)| (k, keccak256(*k), v)).collect::>(); + sorted_state.sort_by_key(|(_, hashed_addr, _)| *hashed_addr); + + for (address, hashed_address, bundle_account) in sorted_state { + if bundle_account.status.is_not_modified() { + continue; + } + + // Compute the path to the account in the trie. + let account_path = Nibbles::unpack(hashed_address.as_slice()); + + // If the account was destroyed, delete it from the trie. + if bundle_account.was_destroyed() { + self.root_node.delete(&account_path, &self.fetcher, &self.hinter)?; + self.storage_roots.remove(address); + continue; + } + + let account_info = + bundle_account.account_info().ok_or(TrieDBError::MissingAccountInfo)?; + + let mut trie_account = TrieAccount { + balance: account_info.balance, + nonce: account_info.nonce, + code_hash: account_info.code_hash, + ..Default::default() + }; + + // Update the account's storage root + let acc_storage_root = self + .storage_roots + .entry(*address) + .or_insert_with(|| TrieNode::new_blinded(EMPTY_ROOT_HASH)); + + // Sort the hashed storage keys prior to applying the changeset, to ensure that the + // order of application is deterministic between runs. + let mut sorted_storage = bundle_account + .storage + .iter() + .map(|(k, v)| (keccak256(k.to_be_bytes::<32>()), v)) + .collect::>(); + sorted_storage.sort_by_key(|(slot, _)| *slot); + + sorted_storage.into_iter().try_for_each(|(hashed_key, value)| { + Self::change_storage( + acc_storage_root, + hashed_key, + value, + &self.fetcher, + &self.hinter, + ) + })?; + + // Recompute the account storage root. + let root = acc_storage_root.blind(); + trie_account.storage_root = root; + + // RLP encode the trie account for insertion. + let mut account_buf = Vec::with_capacity(trie_account.length()); + trie_account.encode(&mut account_buf); + + // Insert or update the account in the trie. + self.root_node.insert(&account_path, account_buf.into(), &self.fetcher)?; + } + + Ok(()) + } + + /// Modifies a storage slot of an account in the Merkle Patricia Trie. + /// + /// ## Takes + /// - `storage_root`: The storage root of the account. + /// - `hashed_key`: The hashed storage slot key. + /// - `value`: The new value of the storage slot. + /// - `fetcher`: The trie node fetcher. + /// - `hinter`: The trie hinter. + /// + /// ## Returns + /// - `Ok(())` if the storage slot was successfully modified. + /// - `Err(_)` if the storage slot could not be modified. + fn change_storage( + storage_root: &mut TrieNode, + hashed_key: B256, + value: &StorageSlot, + fetcher: &F, + hinter: &H, + ) -> TrieDBResult<()> { + if !value.is_changed() { + return Ok(()); + } + + // RLP encode the storage slot value. + let mut rlp_buf = Vec::with_capacity(value.present_value.length()); + value.present_value.encode(&mut rlp_buf); + + // Insert or update the storage slot in the trie. + let hashed_slot_key = Nibbles::unpack(hashed_key.as_slice()); + if value.present_value.is_zero() { + // If the storage slot is being set to zero, prune it from the trie. + storage_root.delete(&hashed_slot_key, fetcher, hinter)?; + } else { + // Otherwise, update the storage slot. + storage_root.insert(&hashed_slot_key, rlp_buf.into(), fetcher)?; + } + + Ok(()) + } +} + +impl Database for TrieDB +where + F: TrieDBProvider, + H: TrieHinter, +{ + type Error = TrieDBError; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + // Fetch the account from the trie. + let Some(trie_account) = + self.get_trie_account(&address, self.parent_block_header.number)? + else { + // If the account does not exist in the trie, return `Ok(None)`. + return Ok(None); + }; + + // Insert the account's storage root into the cache. + self.storage_roots.insert(address, TrieNode::new_blinded(trie_account.storage_root)); + + // Return a partial DB account. The storage and code are not loaded out-right, and are + // loaded optimistically in the `Database` + `DatabaseRef` trait implementations. + Ok(Some(AccountInfo { + balance: trie_account.balance, + nonce: trie_account.nonce, + code_hash: trie_account.code_hash, + code: None, + })) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + self.fetcher + .bytecode_by_hash(code_hash) + .map(Bytecode::new_raw) + .map_err(|e| TrieDBError::Provider(e.to_string())) + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + // Send a hint to the host to fetch the storage proof. + self.hinter + .hint_storage_proof(address, index, self.parent_block_header.number) + .map_err(|e| TrieDBError::Provider(e.to_string()))?; + + // Fetch the account's storage root from the cache. If storage is being accessed, the + // account should have been loaded into the cache by the `basic` method. If the account was + // non-existing, the storage root will not be present. + match self.storage_roots.get_mut(&address) { + None => { + // If the storage root for the account does not exist, return zero. + Ok(U256::ZERO) + } + Some(storage_root) => { + // Fetch the storage slot from the trie. + let hashed_slot_key = keccak256(index.to_be_bytes::<32>().as_slice()); + match storage_root.open(&Nibbles::unpack(hashed_slot_key), &self.fetcher)? { + Some(slot_value) => { + // Decode the storage slot value. + let int_slot = U256::decode(&mut slot_value.as_ref()) + .map_err(TrieNodeError::RLPError)?; + Ok(int_slot) + } + None => { + // If the storage slot does not exist, return zero. + Ok(U256::ZERO) + } + } + } + } + } + + fn block_hash(&mut self, block_number: u64) -> Result { + // Copy the current header + let mut header = self.parent_block_header.inner().clone(); + + // Check if the block number is in range. If not, we can fail early. + if block_number > header.number || + header.number.saturating_sub(block_number) > BLOCK_HASH_HISTORY + { + return Ok(B256::default()); + } + + // Walk back the block headers to the desired block number. + while header.number > block_number { + header = self + .fetcher + .header_by_hash(header.parent_hash) + .map_err(|e| TrieDBError::Provider(e.to_string()))?; + } + + Ok(header.hash_slow()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Sealable; + use alloy_primitives::b256; + use kona_mpt::NoopTrieHinter; + + fn new_test_db() -> TrieDB { + TrieDB::new(Header::default().seal_slow(), NoopTrieDBProvider, NoopTrieHinter) + } + + #[test] + fn test_trie_db_take_root_node() { + let db = new_test_db(); + let root_node = db.take_root_node(); + assert_eq!(root_node.blind(), EMPTY_ROOT_HASH); + } + + #[test] + fn test_trie_db_root_node_ref() { + let db = new_test_db(); + let root_node = db.root(); + assert_eq!(root_node.blind(), EMPTY_ROOT_HASH); + } + + #[test] + fn test_trie_db_storage_roots() { + let db = new_test_db(); + let storage_roots = db.storage_roots(); + assert!(storage_roots.is_empty()); + } + + #[test] + fn test_block_hash_above_range() { + let mut db = new_test_db(); + db.parent_block_header = Header { number: 10, ..Default::default() }.seal_slow(); + let block_number = 11; + let block_hash = db.block_hash(block_number).unwrap(); + assert_eq!(block_hash, B256::default()); + } + + #[test] + fn test_block_hash_below_range() { + let mut db = new_test_db(); + db.parent_block_header = + Header { number: BLOCK_HASH_HISTORY + 10, ..Default::default() }.seal_slow(); + let block_number = 0; + let block_hash = db.block_hash(block_number).unwrap(); + assert_eq!(block_hash, B256::default()); + } + + #[test] + fn test_block_hash_provider_missing_hash() { + let mut db = new_test_db(); + db.parent_block_header = Header { number: 10, ..Default::default() }.seal_slow(); + let block_number = 5; + let block_hash = db.block_hash(block_number).unwrap(); + assert_eq!( + block_hash, + b256!("78dec18c6d7da925bbe773c315653cdc70f6444ed6c1de9ac30bdb36cff74c3b") + ); + } +} diff --git a/kona/crates/proof/executor/src/db/traits.rs b/kona/crates/proof/executor/src/db/traits.rs new file mode 100644 index 0000000000000..99368547c8ee7 --- /dev/null +++ b/kona/crates/proof/executor/src/db/traits.rs @@ -0,0 +1,57 @@ +//! Contains the [TrieDBProvider] trait for fetching EVM bytecode hash preimages as well as [Header] +//! preimages. + +use alloc::string::String; +use alloy_consensus::Header; +use alloy_primitives::{B256, Bytes}; +use kona_mpt::{TrieNode, TrieProvider}; + +/// The [TrieDBProvider] trait defines the synchronous interface for fetching EVM bytecode hash +/// preimages as well as [Header] preimages. +pub trait TrieDBProvider: TrieProvider { + /// Fetches the preimage of the bytecode hash provided. + /// + /// ## Takes + /// - `hash`: The hash of the bytecode. + /// + /// ## Returns + /// - Ok(Bytes): The bytecode of the contract. + /// - Err(Self::Error): If the bytecode hash could not be fetched. + /// + /// [TrieDB]: crate::TrieDB + fn bytecode_by_hash(&self, code_hash: B256) -> Result; + + /// Fetches the preimage of [Header] hash provided. + /// + /// ## Takes + /// - `hash`: The hash of the RLP-encoded [Header]. + /// + /// ## Returns + /// - Ok(Bytes): The [Header]. + /// - Err(Self::Error): If the [Header] could not be fetched. + /// + /// [TrieDB]: crate::TrieDB + fn header_by_hash(&self, hash: B256) -> Result; +} + +/// The default, no-op implementation of the [TrieDBProvider] trait, used for testing. +#[derive(Debug, Clone, Copy)] +pub struct NoopTrieDBProvider; + +impl TrieProvider for NoopTrieDBProvider { + type Error = String; + + fn trie_node_by_hash(&self, _key: B256) -> Result { + Ok(TrieNode::Empty) + } +} + +impl TrieDBProvider for NoopTrieDBProvider { + fn bytecode_by_hash(&self, _code_hash: B256) -> Result { + Ok(Bytes::default()) + } + + fn header_by_hash(&self, _hash: B256) -> Result { + Ok(Header::default()) + } +} diff --git a/kona/crates/proof/executor/src/errors.rs b/kona/crates/proof/executor/src/errors.rs new file mode 100644 index 0000000000000..e1482cb87114b --- /dev/null +++ b/kona/crates/proof/executor/src/errors.rs @@ -0,0 +1,314 @@ +//! Error types for the `kona-executor` crate. +//! +//! This module provides comprehensive error handling for the stateless L2 block +//! execution engine, covering validation errors, execution failures, and +//! database operation errors. + +use alloc::string::String; +use alloy_evm::block::BlockExecutionError; +use kona_mpt::TrieNodeError; +use op_alloy_consensus::EIP1559ParamError; +use revm::context::DBErrorMarker; +use thiserror::Error; + +/// Errors that can occur when validating EIP-1559 parameters from block header extra data. +/// +/// This error type is used for validation errors during decoding of EIP-1559 parameters, +/// providing more specific error variants than the upstream [`EIP1559ParamError`] which +/// is primarily designed for encoding errors. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum Eip1559ValidationError { + /// The EIP-1559 denominator cannot be zero as it would cause division by zero. + /// + /// This error occurs when the decoded denominator value from the block header's + /// extra data field is zero, which violates protocol specifications. + #[error("EIP-1559 denominator cannot be zero")] + ZeroDenominator, + /// Error from EIP-1559 parameter decoding. + /// + /// This variant wraps errors from the underlying EIP-1559 parameter decoding, + /// such as invalid version bytes, incorrect data lengths, or missing parameters. + #[error(transparent)] + Decode(#[from] EIP1559ParamError), +} + +/// Error type for the [`StatelessL2Builder`] block execution engine. +/// +/// [`ExecutorError`] represents various failure modes that can occur during +/// stateless L2 block building and execution. These errors provide detailed +/// context for debugging execution failures and enable appropriate error +/// handling strategies. +/// +/// # Error Categories +/// +/// ## Input Validation Errors +/// - Missing required fields in payload attributes +/// - Invalid block header parameters +/// - Unsupported transaction types +/// +/// ## Execution Errors +/// - Block gas limit violations +/// - Transaction execution failures +/// - EVM-level execution errors +/// +/// ## Data Integrity Errors +/// - Trie database operation failures +/// - RLP encoding/decoding errors +/// - Cryptographic signature recovery failures +/// +/// [`StatelessL2Builder`]: crate::StatelessL2Builder +#[derive(Error, Debug)] +pub enum ExecutorError { + /// Gas limit not provided in the payload attributes. + /// + /// This error occurs when the payload attributes are missing the required + /// gas limit field, which is necessary for block execution validation. + /// The gas limit defines the maximum amount of gas that can be consumed + /// by all transactions in the block. + /// + /// # Common Causes + /// - Malformed payload attributes from the driver + /// - Missing fields in payload construction + /// - Protocol version mismatches + #[error("Gas limit not provided in payload attributes")] + MissingGasLimit, + /// Transactions list not provided in the payload attributes. + /// + /// This error occurs when the payload attributes don't include the + /// required transactions list. Even empty blocks require an empty + /// transactions array to be explicitly provided. + /// + /// # Common Causes + /// - Incomplete payload attribute construction + /// - Serialization/deserialization errors + /// - Protocol specification violations + #[error("Transactions not provided in payload attributes")] + MissingTransactions, + /// EIP-1559 fee parameters missing in execution payload after Holocene activation. + /// + /// Post-Holocene blocks require EIP-1559 base fee and blob base fee parameters + /// to be present in the execution payload for proper fee market operation. + /// This error indicates these required parameters are missing. + /// + /// # Common Causes + /// - Holocene upgrade not properly implemented in payload construction + /// - Missing fee parameter calculation + /// - Incorrect hard fork activation detection + #[error("Missing EIP-1559 parameters in execution payload post-Holocene")] + MissingEIP1559Params, + /// Parent beacon block root not provided in the payload attributes. + /// + /// This error occurs when the payload attributes are missing the parent + /// beacon block root, which is required for post-Dencun blocks to enable + /// proper beacon chain integration and blob transaction validation. + /// + /// # Common Causes + /// - Missing beacon chain data in payload construction + /// - Incorrect Dencun upgrade implementation + /// - Beacon client communication failures + #[error("Parent beacon block root not provided in payload attributes")] + MissingParentBeaconBlockRoot, + /// Invalid `extraData` field in the block header. + /// + /// This error occurs when the block header's `extraData` field contains + /// invalid data that violates protocol specifications. The `extraData` + /// field has specific format requirements depending on the network. + /// + /// # Common Causes + /// - Malformed extraData during header construction + /// - Incorrect protocol version handling + /// - Data corruption during header assembly + /// - Zero denominator in EIP-1559 parameters + #[error("Invalid `extraData` field in the block header: {0}")] + InvalidExtraData(#[from] Eip1559ValidationError), + /// Block gas limit exceeded during execution. + /// + /// This error occurs when the cumulative gas consumption of all transactions + /// in the block exceeds the block's gas limit. This violates consensus rules + /// and indicates either invalid transaction inclusion or incorrect gas accounting. + /// + /// # Common Causes + /// - Transaction gas estimation errors + /// - Incorrect gas limit calculation + /// - Invalid transaction ordering + /// - Gas accounting bugs in execution + #[error("Block gas limit exceeded")] + BlockGasLimitExceeded, + /// Unsupported transaction type encountered during execution. + /// + /// This error occurs when the executor encounters a transaction type that + /// it doesn't know how to process. This may indicate a protocol upgrade + /// that hasn't been implemented or invalid transaction data. + /// + /// # Arguments + /// * `0` - The unsupported transaction type identifier + /// + /// # Common Causes + /// - New transaction types not yet supported + /// - Corrupted transaction data + /// - Protocol version mismatches + #[error("Unsupported transaction type: {0}")] + UnsupportedTransactionType(u8), + /// Trie database operation failed. + /// + /// This error wraps [`TrieDBError`] variants that occur during state + /// tree operations, including node lookups, proof verification, and + /// state root computation. + #[error("Trie error: {0}")] + TrieDBError(#[from] TrieDBError), + /// Block execution failed at the EVM level. + /// + /// This error wraps [`BlockExecutionError`] variants that occur during + /// the actual execution of transactions within the block, including + /// transaction failures, state conflicts, and EVM-level errors. + #[error("Execution error: {0}")] + ExecutionError(#[from] BlockExecutionError), + /// Cryptographic signature recovery failed. + /// + /// This error occurs when attempting to recover the sender address from + /// a transaction signature fails due to invalid signatures, malformed + /// signature data, or cryptographic errors. + /// + /// # Common Causes + /// - Invalid transaction signatures + /// - Corrupted signature data + /// - Unsupported signature algorithms + /// - Chain ID mismatches + #[error("sender recovery error: {0}")] + Recovery(#[from] alloy_consensus::crypto::RecoveryError), + /// RLP encoding or decoding error. + /// + /// This error occurs when RLP (Recursive Length Prefix) serialization + /// or deserialization fails due to malformed data, invalid length + /// prefixes, or unsupported data structures. + /// + /// # Common Causes + /// - Corrupted transaction or block data + /// - Invalid RLP formatting + /// - Unsupported data types in RLP stream + #[error("RLP error: {0}")] + RLPError(alloy_eips::eip2718::Eip2718Error), + /// Executor instance not available when required. + /// + /// This error occurs when attempting to perform an operation that requires + /// an active executor instance, but none is available. This typically + /// indicates a lifecycle or initialization issue. + /// + /// # Common Causes + /// - Executor not properly initialized + /// - Executor already consumed or dropped + /// - Incorrect executor lifecycle management + #[error("Missing the executor")] + MissingExecutor, +} + +/// Result type alias for operations that may fail with [`ExecutorError`]. +/// +/// This type alias provides a convenient way to handle executor operations +/// that can fail, encapsulating the success value type `T` and the standard +/// [`ExecutorError`] for failures. +/// +/// # Usage +/// ```rust,ignore +/// fn build_block(attrs: PayloadAttributes) -> ExecutorResult { +/// // ... block building logic +/// } +/// ``` +pub type ExecutorResult = Result; + +/// Result type alias for trie database operations that may fail with [`TrieDBError`]. +/// +/// This type alias provides a convenient way to handle trie database operations +/// that can fail, specifically for operations involving state tree access, +/// proof verification, and account data retrieval. +/// +/// # Usage +/// ```rust,ignore +/// fn get_account_info(address: Address) -> TrieDBResult { +/// // ... trie database lookup logic +/// } +/// ``` +pub type TrieDBResult = Result; + +/// Error type for [`TrieDB`] database operations. +/// +/// [`TrieDBError`] represents failures that can occur during trie database +/// operations, including state tree traversal, proof verification, and +/// account data retrieval. These errors are critical for stateless execution +/// as they indicate issues with the underlying state data. +/// +/// # Error Categories +/// +/// ## State Tree Errors +/// - Blinded trie node issues +/// - Missing account information +/// - Trie node corruption or invalid proofs +/// +/// ## Provider Errors +/// - Communication failures with trie data providers +/// - Missing or corrupted state data +/// - Network or I/O related issues +/// +/// [`TrieDB`]: crate::TrieDB +#[derive(Error, Debug, PartialEq, Eq)] +pub enum TrieDBError { + /// Trie root node has not been properly blinded for stateless execution. + /// + /// This error occurs when attempting to perform stateless execution on a trie + /// where the root node hasn't been blinded (converted to a form suitable for + /// stateless proofs). Blinding is required to enable stateless verification. + /// + /// # Common Causes + /// - Incorrect trie preparation for stateless execution + /// - Missing proof data or invalid proof format + /// - State tree not properly configured for witness-based execution + #[error("Trie root node has not been blinded")] + RootNotBlinded, + /// Account information missing for a bundle account during state access. + /// + /// This error occurs when the trie database cannot find required account + /// information during execution. In stateless execution, all required + /// account data must be provided via witnesses or proofs. + /// + /// # Common Causes + /// - Incomplete witness data for accessed accounts + /// - Missing account proofs in the witness + /// - Account state changes not properly tracked + /// - Proof verification failures + #[error("Missing account info for bundle account.")] + MissingAccountInfo, + /// Trie node operation failed due to invalid node data or proof. + /// + /// This error wraps [`TrieNodeError`] variants that occur during individual + /// trie node operations, including hash verification, node parsing, and + /// proof validation. + /// + /// # Common Causes + /// - Corrupted trie node data + /// - Invalid Merkle proofs + /// - Hash mismatches in trie verification + /// - Malformed node structure + #[error("Trie node error: {0}")] + TrieNode(#[from] TrieNodeError), + /// Trie data provider communication or operation failed. + /// + /// This error occurs when the underlying trie data provider fails to + /// retrieve or validate trie data. It includes network failures, data + /// corruption, and provider-specific errors. + /// + /// # Common Causes + /// - Network communication failures + /// - Provider service unavailable + /// - Data corruption in provider storage + /// - Timeout during data retrieval + #[error("Trie provider error: {0}")] + Provider(String), +} + +impl DBErrorMarker for TrieDBError {} + +impl From for ExecutorError { + fn from(err: EIP1559ParamError) -> Self { + Self::InvalidExtraData(Eip1559ValidationError::Decode(err)) + } +} diff --git a/kona/crates/proof/executor/src/lib.rs b/kona/crates/proof/executor/src/lib.rs new file mode 100644 index 0000000000000..d92219745930d --- /dev/null +++ b/kona/crates/proof/executor/src/lib.rs @@ -0,0 +1,29 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(any(test, feature = "test-utils")), no_std)] + +extern crate alloc; + +#[macro_use] +extern crate tracing; + +mod db; +pub use db::{NoopTrieDBProvider, TrieDB, TrieDBProvider}; + +mod builder; +pub use builder::{BlockBuildingOutcome, StatelessL2Builder, compute_receipts_root}; + +mod errors; +pub use errors::{ + Eip1559ValidationError, ExecutorError, ExecutorResult, TrieDBError, TrieDBResult, +}; + +pub(crate) mod util; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/kona/crates/proof/executor/src/test_utils.rs b/kona/crates/proof/executor/src/test_utils.rs new file mode 100644 index 0000000000000..0669392ab5a0e --- /dev/null +++ b/kona/crates/proof/executor/src/test_utils.rs @@ -0,0 +1,372 @@ +//! Test utilities for the executor. + +use crate::{StatelessL2Builder, TrieDBProvider}; +use alloy_consensus::Header; +use alloy_op_evm::OpEvmFactory; +use alloy_primitives::{B256, Bytes, Sealable}; +use alloy_provider::{Provider, RootProvider, network::primitives::BlockTransactions}; +use alloy_rlp::Decodable; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_engine::PayloadAttributes; +use alloy_transport_http::{Client, Http}; +use kona_genesis::RollupConfig; +use kona_mpt::{NoopTrieHinter, TrieNode, TrieProvider}; +use kona_registry::ROLLUP_CONFIGS; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use rocksdb::{DB, Options}; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, sync::Arc}; +use tokio::{fs, runtime::Handle, sync::Mutex}; + +/// Executes a [ExecutorTestFixture] stored at the passed `fixture_path` and asserts that the +/// produced block hash matches the expected block hash. +pub async fn run_test_fixture(fixture_path: PathBuf) { + // First, untar the fixture. + let fixture_dir = tempfile::tempdir().expect("Failed to create temporary directory"); + tokio::process::Command::new("tar") + .arg("-xvf") + .arg(fixture_path.as_path()) + .arg("-C") + .arg(fixture_dir.path()) + .arg("--strip-components=1") + .output() + .await + .expect("Failed to untar fixture"); + + let mut options = Options::default(); + options.set_compression_type(rocksdb::DBCompressionType::Snappy); + options.create_if_missing(true); + let kv_store = DB::open(&options, fixture_dir.path().join("kv")) + .unwrap_or_else(|e| panic!("Failed to open database at {fixture_dir:?}: {e}")); + let provider = DiskTrieNodeProvider::new(kv_store); + let fixture: ExecutorTestFixture = + serde_json::from_slice(&fs::read(fixture_dir.path().join("fixture.json")).await.unwrap()) + .expect("Failed to deserialize fixture"); + + let mut executor = StatelessL2Builder::new( + &fixture.rollup_config, + OpEvmFactory::default(), + provider, + NoopTrieHinter, + fixture.parent_header.seal_slow(), + ); + + let outcome = executor.build_block(fixture.executing_payload).unwrap(); + + assert_eq!( + outcome.header.hash(), + fixture.expected_block_hash, + "Produced header does not match the expected header" + ); +} + +/// The test fixture format for the [`StatelessL2Builder`]. +#[derive(Debug, Serialize, Deserialize)] +pub struct ExecutorTestFixture { + /// The rollup configuration for the executing chain. + pub rollup_config: RollupConfig, + /// The parent block header. + pub parent_header: Header, + /// The executing payload attributes. + pub executing_payload: OpPayloadAttributes, + /// The expected block hash + pub expected_block_hash: B256, +} + +/// A test fixture creator for the [`StatelessL2Builder`]. +#[derive(Debug)] +pub struct ExecutorTestFixtureCreator { + /// The RPC provider for the L2 execution layer. + pub provider: RootProvider, + /// The block number to create the test fixture for. + pub block_number: u64, + /// The key value store for the test fixture. + pub kv_store: Arc>, + /// The data directory for the test fixture. + pub data_dir: PathBuf, +} + +impl ExecutorTestFixtureCreator { + /// Creates a new [`ExecutorTestFixtureCreator`] with the given parameters. + pub fn new(provider_url: &str, block_number: u64, base_fixture_directory: PathBuf) -> Self { + let base = base_fixture_directory.join(format!("block-{block_number}")); + + let url = provider_url.parse().expect("Invalid provider URL"); + let http = Http::::new(url); + let provider = RootProvider::new(RpcClient::new(http, false)); + + let mut options = Options::default(); + options.set_compression_type(rocksdb::DBCompressionType::Snappy); + options.create_if_missing(true); + let db = DB::open(&options, base.join("kv").as_path()) + .unwrap_or_else(|e| panic!("Failed to open database at {base:?}: {e}")); + + Self { provider, block_number, kv_store: Arc::new(Mutex::new(db)), data_dir: base } + } +} + +impl ExecutorTestFixtureCreator { + /// Create a static test fixture with the configuration provided. + pub async fn create_static_fixture(self) { + let chain_id = self.provider.get_chain_id().await.expect("Failed to get chain ID"); + let rollup_config = ROLLUP_CONFIGS.get(&chain_id).expect("Rollup config not found"); + + let executing_block = self + .provider + .get_block_by_number(self.block_number.into()) + .await + .expect("Failed to get parent block") + .expect("Block not found"); + let parent_block = self + .provider + .get_block_by_number((self.block_number - 1).into()) + .await + .expect("Failed to get parent block") + .expect("Block not found"); + + let executing_header = executing_block.header; + let parent_header = parent_block.header.inner.seal_slow(); + + let encoded_executing_transactions = match executing_block.transactions { + BlockTransactions::Hashes(transactions) => { + let mut encoded_transactions = Vec::with_capacity(transactions.len()); + for tx_hash in transactions { + let tx = self + .provider + .client() + .request::<&[B256; 1], Bytes>("debug_getRawTransaction", &[tx_hash]) + .await + .expect("Block not found"); + encoded_transactions.push(tx); + } + encoded_transactions + } + _ => panic!("Only BlockTransactions::Hashes are supported."), + }; + + let payload_attrs = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: executing_header.timestamp, + parent_beacon_block_root: executing_header.parent_beacon_block_root, + prev_randao: executing_header.mix_hash, + withdrawals: Default::default(), + suggested_fee_recipient: executing_header.beneficiary, + }, + gas_limit: Some(executing_header.gas_limit), + transactions: Some(encoded_executing_transactions), + no_tx_pool: None, + eip_1559_params: rollup_config.is_holocene_active(executing_header.timestamp).then( + || { + executing_header.extra_data[1..9] + .try_into() + .expect("Invalid header format for Holocene") + }, + ), + min_base_fee: rollup_config.is_jovian_active(executing_header.timestamp).then(|| { + // The min base fee is the bytes 9-17 of the extra data. + executing_header.extra_data[9..17] + .try_into() + .map(u64::from_be_bytes) + .expect("Invalid header format for Jovian") + }), + }; + + let fixture_path = self.data_dir.join("fixture.json"); + let fixture = ExecutorTestFixture { + rollup_config: rollup_config.clone(), + parent_header: parent_header.inner().clone(), + executing_payload: payload_attrs.clone(), + expected_block_hash: executing_header.hash_slow(), + }; + + let mut executor = StatelessL2Builder::new( + rollup_config, + OpEvmFactory::default(), + self, + NoopTrieHinter, + parent_header, + ); + let outcome = executor.build_block(payload_attrs).expect("Failed to execute block"); + + assert_eq!( + outcome.header.inner(), + &executing_header.inner, + "Produced header does not match the expected header" + ); + fs::write(fixture_path.as_path(), serde_json::to_vec(&fixture).unwrap()).await.unwrap(); + + // Tar the fixture. + let data_dir = fixture_path.parent().unwrap(); + tokio::process::Command::new("tar") + .arg("-czf") + .arg(data_dir.with_extension("tar.gz").file_name().unwrap()) + .arg(data_dir.file_name().unwrap()) + .current_dir(data_dir.parent().unwrap()) + .output() + .await + .expect("Failed to tar fixture"); + + // Remove the leftover directory. + fs::remove_dir_all(data_dir).await.expect("Failed to remove temporary directory"); + } +} + +impl TrieProvider for ExecutorTestFixtureCreator { + type Error = TestTrieNodeProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + // Fetch the preimage from the L2 chain provider. + let preimage: Bytes = tokio::task::block_in_place(move || { + Handle::current().block_on(async { + let preimage: Bytes = self + .provider + .client() + .request("debug_dbGet", &[key]) + .await + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)?; + + self.kv_store + .lock() + .await + .put(key, preimage.clone()) + .map_err(|_| TestTrieNodeProviderError::KVStore)?; + + Ok(preimage) + }) + })?; + + // Decode the preimage into a trie node. + TrieNode::decode(&mut preimage.as_ref()).map_err(TestTrieNodeProviderError::Rlp) + } +} + +impl TrieDBProvider for ExecutorTestFixtureCreator { + fn bytecode_by_hash(&self, hash: B256) -> Result { + // geth hashdb scheme code hash key prefix + const CODE_PREFIX: u8 = b'c'; + + // Fetch the preimage from the L2 chain provider. + let preimage: Bytes = tokio::task::block_in_place(move || { + Handle::current().block_on(async { + // Attempt to fetch the code from the L2 chain provider. + let code_hash = [&[CODE_PREFIX], hash.as_slice()].concat(); + let code = self + .provider + .client() + .request::<&[Bytes; 1], Bytes>("debug_dbGet", &[code_hash.into()]) + .await; + + // Check if the first attempt to fetch the code failed. If it did, try fetching the + // code hash preimage without the geth hashdb scheme prefix. + let code = match code { + Ok(code) => code, + Err(_) => self + .provider + .client() + .request::<&[B256; 1], Bytes>("debug_dbGet", &[hash]) + .await + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)?, + }; + + self.kv_store + .lock() + .await + .put(hash, code.clone()) + .map_err(|_| TestTrieNodeProviderError::KVStore)?; + + Ok(code) + }) + })?; + + Ok(preimage) + } + + fn header_by_hash(&self, hash: B256) -> Result { + let encoded_header: Bytes = tokio::task::block_in_place(move || { + Handle::current().block_on(async { + let preimage: Bytes = self + .provider + .client() + .request("debug_getRawHeader", &[hash]) + .await + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)?; + + self.kv_store + .lock() + .await + .put(hash, preimage.clone()) + .map_err(|_| TestTrieNodeProviderError::KVStore)?; + + Ok(preimage) + }) + })?; + + // Decode the Header. + Header::decode(&mut encoded_header.as_ref()).map_err(TestTrieNodeProviderError::Rlp) + } +} + +/// A simple [`TrieDBProvider`] that reads data from a disk-based key-value store. +#[derive(Debug)] +pub struct DiskTrieNodeProvider { + kv_store: DB, +} + +impl DiskTrieNodeProvider { + /// Creates a new [`DiskTrieNodeProvider`] with the given [`rocksdb`] K/V store. + pub const fn new(kv_store: DB) -> Self { + Self { kv_store } + } +} + +impl TrieProvider for DiskTrieNodeProvider { + type Error = TestTrieNodeProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + TrieNode::decode( + &mut self + .kv_store + .get(key) + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)? + .ok_or(TestTrieNodeProviderError::PreimageNotFound)? + .as_slice(), + ) + .map_err(TestTrieNodeProviderError::Rlp) + } +} + +impl TrieDBProvider for DiskTrieNodeProvider { + fn bytecode_by_hash(&self, code_hash: B256) -> Result { + self.kv_store + .get(code_hash) + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)? + .map(Bytes::from) + .ok_or(TestTrieNodeProviderError::PreimageNotFound) + } + + fn header_by_hash(&self, hash: B256) -> Result { + Header::decode( + &mut self + .kv_store + .get(hash) + .map_err(|_| TestTrieNodeProviderError::PreimageNotFound)? + .ok_or(TestTrieNodeProviderError::PreimageNotFound)? + .as_slice(), + ) + .map_err(TestTrieNodeProviderError::Rlp) + } +} + +/// An error type for the [`DiskTrieNodeProvider`] and [`ExecutorTestFixtureCreator`]. +#[derive(Debug, thiserror::Error)] +pub enum TestTrieNodeProviderError { + /// The preimage was not found in the key-value store. + #[error("Preimage not found")] + PreimageNotFound, + /// Failed to decode the RLP-encoded data. + #[error("Failed to decode RLP: {0}")] + Rlp(alloy_rlp::Error), + /// Failed to write back to the key-value store. + #[error("Failed to write back to key value store")] + KVStore, +} diff --git a/kona/crates/proof/executor/src/util.rs b/kona/crates/proof/executor/src/util.rs new file mode 100644 index 0000000000000..289dd26254153 --- /dev/null +++ b/kona/crates/proof/executor/src/util.rs @@ -0,0 +1,223 @@ +//! Contains utilities for the L2 executor. + +use crate::{Eip1559ValidationError, ExecutorError, ExecutorResult}; +use alloy_consensus::{BlockHeader, Header}; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_primitives::Bytes; +use kona_genesis::RollupConfig; +use op_alloy_consensus::{ + EIP1559ParamError, decode_holocene_extra_data, decode_jovian_extra_data, + encode_holocene_extra_data, encode_jovian_extra_data, +}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// Parse Holocene [Header] extra data from the block header. +/// +/// ## Takes +/// - `extra_data`: The extra data field of the [Header]. +/// +/// ## Returns +/// - `Ok(BaseFeeParams)`: The EIP-1559 parameters. +/// - `Err(ExecutorError::InvalidExtraData)`: If the extra data is invalid. +pub(crate) fn decode_holocene_eip_1559_params_block_header( + header: &Header, +) -> ExecutorResult { + let (elasticity, denominator) = decode_holocene_extra_data(header.extra_data())?; + + // Check for potential division by zero. + // In the block header, the denominator is always non-zero. + // + if denominator == 0 { + return Err(ExecutorError::InvalidExtraData(Eip1559ValidationError::ZeroDenominator)); + } + + Ok(BaseFeeParams { + elasticity_multiplier: elasticity.into(), + max_change_denominator: denominator.into(), + }) +} + +pub(crate) fn decode_jovian_eip_1559_params_block_header( + header: &Header, +) -> ExecutorResult<(BaseFeeParams, u64)> { + let (elasticity, denominator, min_base_fee) = decode_jovian_extra_data(header.extra_data())?; + + // Check for potential division by zero. + // In the block header, the denominator is always non-zero. + // + if denominator == 0 { + return Err(ExecutorError::InvalidExtraData(Eip1559ValidationError::ZeroDenominator)); + } + + Ok(( + BaseFeeParams { + elasticity_multiplier: elasticity.into(), + max_change_denominator: denominator.into(), + }, + min_base_fee, + )) +} + +/// Encode Holocene [Header] extra data. +/// +/// ## Takes +/// - `config`: The [RollupConfig] for the chain. +/// - `attributes`: The [OpPayloadAttributes] for the block. +/// +/// ## Returns +/// - `Ok(data)`: The encoded extra data. +/// - `Err(ExecutorError::MissingEIP1559Params)`: If the EIP-1559 parameters are missing. +pub(crate) fn encode_holocene_eip_1559_params( + config: &RollupConfig, + attributes: &OpPayloadAttributes, +) -> ExecutorResult { + Ok(encode_holocene_extra_data( + attributes.eip_1559_params.ok_or(ExecutorError::MissingEIP1559Params)?, + config.chain_op_config.as_base_fee_params(), + )?) +} + +/// Encode Jovian [Header] extra data. +/// +/// ## Takes +/// - `config`: The [RollupConfig] for the chain. +/// - `attributes`: The [OpPayloadAttributes] for the block. +/// +/// ## Returns +/// - `Ok(data)`: The encoded extra data. +/// - `Err(ExecutorError::MissingEIP1559Params)`: If the EIP-1559 parameters are missing. +pub(crate) fn encode_jovian_eip_1559_params( + config: &RollupConfig, + attributes: &OpPayloadAttributes, +) -> ExecutorResult { + Ok(encode_jovian_extra_data( + attributes.eip_1559_params.ok_or(ExecutorError::MissingEIP1559Params)?, + config.chain_op_config.as_base_fee_params(), + attributes.min_base_fee.ok_or(ExecutorError::InvalidExtraData( + Eip1559ValidationError::Decode(EIP1559ParamError::MinBaseFeeNotSet), + ))?, + )?) +} + +#[cfg(test)] +mod test { + use super::decode_holocene_eip_1559_params_block_header; + use crate::util::{ + decode_jovian_eip_1559_params_block_header, encode_holocene_eip_1559_params, + }; + use alloy_consensus::Header; + use alloy_primitives::{B64, b64, hex}; + use alloy_rpc_types_engine::PayloadAttributes; + use kona_genesis::{BaseFeeConfig, RollupConfig}; + use op_alloy_rpc_types_engine::OpPayloadAttributes; + + fn mock_payload(eip_1559_params: Option) -> OpPayloadAttributes { + OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 0, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + withdrawals: Default::default(), + parent_beacon_block_root: Default::default(), + }, + transactions: None, + no_tx_pool: None, + gas_limit: None, + eip_1559_params, + min_base_fee: None, + } + } + + #[test] + fn test_decode_holocene_eip_1559_params() { + let params = hex!("00BEEFBABE0BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + let params = decode_holocene_eip_1559_params_block_header(&mock_header).unwrap(); + + assert_eq!(params.elasticity_multiplier, 0x0BAD_C0DE); + assert_eq!(params.max_change_denominator, 0xBEEF_BABE); + } + + #[test] + fn test_decode_jovian_eip_1559_params() { + let params = hex!("01BEEFBABE0BADC0DE00000000DEADBEEF"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + let (params, base_fee) = decode_jovian_eip_1559_params_block_header(&mock_header).unwrap(); + + assert_eq!(params.elasticity_multiplier, 0x0BAD_C0DE); + assert_eq!(params.max_change_denominator, 0xBEEF_BABE); + assert_eq!(base_fee, 0xDEAD_BEEF); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_version() { + let params = hex!("01BEEFBABE0BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params_block_header(&mock_header).is_err()); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_denominator() { + let params = hex!("00000000000BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params_block_header(&mock_header).is_err()); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_length() { + let params = hex!("00"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params_block_header(&mock_header).is_err()); + } + + #[test] + fn test_encode_holocene_eip_1559_params_missing() { + let cfg = RollupConfig { + chain_op_config: BaseFeeConfig { + eip1559_denominator: 32, + eip1559_elasticity: 64, + eip1559_denominator_canyon: 32, + }, + ..Default::default() + }; + let attrs = mock_payload(None); + + assert!(encode_holocene_eip_1559_params(&cfg, &attrs).is_err()); + } + + #[test] + fn test_encode_holocene_eip_1559_params_default() { + let cfg = RollupConfig { + chain_op_config: BaseFeeConfig { + eip1559_denominator: 32, + eip1559_elasticity: 64, + eip1559_denominator_canyon: 32, + }, + ..Default::default() + }; + let attrs = mock_payload(Some(B64::ZERO)); + + assert_eq!( + encode_holocene_eip_1559_params(&cfg, &attrs).unwrap(), + hex!("000000002000000040").to_vec() + ); + } + + #[test] + fn test_encode_holocene_eip_1559_params() { + let cfg = RollupConfig { + chain_op_config: BaseFeeConfig { + eip1559_denominator: 32, + eip1559_elasticity: 64, + eip1559_denominator_canyon: 32, + }, + ..Default::default() + }; + let attrs = mock_payload(Some(b64!("0000004000000060"))); + + assert_eq!( + encode_holocene_eip_1559_params(&cfg, &attrs).unwrap(), + hex!("000000004000000060").to_vec() + ); + } +} diff --git a/kona/crates/proof/executor/testdata/block-26207960.tar.gz b/kona/crates/proof/executor/testdata/block-26207960.tar.gz new file mode 100644 index 0000000000000..cf3ee2cb5426e Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26207960.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26207961.tar.gz b/kona/crates/proof/executor/testdata/block-26207961.tar.gz new file mode 100644 index 0000000000000..d46eca8d78a21 Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26207961.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26207962.tar.gz b/kona/crates/proof/executor/testdata/block-26207962.tar.gz new file mode 100644 index 0000000000000..3cd4d1349990f Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26207962.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26207963.tar.gz b/kona/crates/proof/executor/testdata/block-26207963.tar.gz new file mode 100644 index 0000000000000..eceb474d20eb3 Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26207963.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26208384.tar.gz b/kona/crates/proof/executor/testdata/block-26208384.tar.gz new file mode 100644 index 0000000000000..6451f8b0b19d3 Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26208384.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26208858.tar.gz b/kona/crates/proof/executor/testdata/block-26208858.tar.gz new file mode 100644 index 0000000000000..2d83e51a2a06d Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26208858.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26208927.tar.gz b/kona/crates/proof/executor/testdata/block-26208927.tar.gz new file mode 100644 index 0000000000000..d0ecbbf302248 Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26208927.tar.gz differ diff --git a/kona/crates/proof/executor/testdata/block-26211680.tar.gz b/kona/crates/proof/executor/testdata/block-26211680.tar.gz new file mode 100644 index 0000000000000..80fa22dfa4475 Binary files /dev/null and b/kona/crates/proof/executor/testdata/block-26211680.tar.gz differ diff --git a/kona/crates/proof/mpt/CHANGELOG.md b/kona/crates/proof/mpt/CHANGELOG.md new file mode 100644 index 0000000000000..fbebf6e104868 --- /dev/null +++ b/kona/crates/proof/mpt/CHANGELOG.md @@ -0,0 +1,126 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.2](https://github.com/op-rs/kona/compare/kona-mpt-v0.1.1...kona-mpt-v0.1.2) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +### Other + +- Bump Dependencies (#880) + +## [0.1.1](https://github.com/op-rs/kona/compare/kona-mpt-v0.1.0...kona-mpt-v0.1.1) - 2024-12-03 + +### Other + +- update deps and clean up misc features ([#864](https://github.com/op-rs/kona/pull/864)) + +## [0.0.7](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.6...kona-mpt-v0.0.7) - 2024-11-20 + +### Added + +- *(mpt)* Extend `TrieProvider` in `kona-executor` ([#813](https://github.com/op-rs/kona/pull/813)) + +### Fixed + +- *(mpt)* Remove unused collapse ([#808](https://github.com/op-rs/kona/pull/808)) + +### Other + +- v0.6.6 op-alloy ([#804](https://github.com/op-rs/kona/pull/804)) +- *(workspace)* Migrate back to `thiserror` v2 ([#811](https://github.com/op-rs/kona/pull/811)) +- Revert "chore: bump alloy deps ([#788](https://github.com/op-rs/kona/pull/788))" ([#791](https://github.com/op-rs/kona/pull/791)) + +## [0.0.6](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.5...kona-mpt-v0.0.6) - 2024-11-06 + +### Added + +- *(TrieProvider)* Abstract TrieNode retrieval ([#787](https://github.com/op-rs/kona/pull/787)) + +### Other + +- bump alloy deps ([#788](https://github.com/op-rs/kona/pull/788)) + +## [0.0.5](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.4...kona-mpt-v0.0.5) - 2024-10-29 + +### Fixed + +- add feature for `alloy-provider`, fix `test_util` ([#738](https://github.com/op-rs/kona/pull/738)) + +## [0.0.4](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.3...kona-mpt-v0.0.4) - 2024-10-25 + +### Added + +- remove thiserror ([#735](https://github.com/op-rs/kona/pull/735)) +- *(executor)* Clean ups ([#719](https://github.com/op-rs/kona/pull/719)) +- use derive more display ([#675](https://github.com/op-rs/kona/pull/675)) +- kona-providers ([#596](https://github.com/op-rs/kona/pull/596)) +- *(ci)* Split online/offline tests ([#582](https://github.com/op-rs/kona/pull/582)) +- *(mpt)* Migrate to `thiserror` ([#541](https://github.com/op-rs/kona/pull/541)) + +### Fixed + +- *(mpt)* Empty root node case ([#705](https://github.com/op-rs/kona/pull/705)) +- typos ([#690](https://github.com/op-rs/kona/pull/690)) +- *(workspace)* hoist and fix lints ([#577](https://github.com/op-rs/kona/pull/577)) +- *(mpt)* Empty list walker ([#493](https://github.com/op-rs/kona/pull/493)) + +### Other + +- cleans up kona-mpt deps ([#725](https://github.com/op-rs/kona/pull/725)) +- re-org imports ([#711](https://github.com/op-rs/kona/pull/711)) +- *(mpt)* codecov ([#655](https://github.com/op-rs/kona/pull/655)) +- *(mpt)* mpt noop trait impls ([#649](https://github.com/op-rs/kona/pull/649)) +- *(mpt)* account conversion tests ([#647](https://github.com/op-rs/kona/pull/647)) +- doc logos ([#609](https://github.com/op-rs/kona/pull/609)) +- *(workspace)* Allow stdlib in `cfg(test)` ([#548](https://github.com/op-rs/kona/pull/548)) + +## [0.0.3](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.2...kona-mpt-v0.0.3) - 2024-09-04 + +### Added +- *(mpt)* `TrieNode` benchmarks ([#351](https://github.com/op-rs/kona/pull/351)) + +### Fixed +- *(workspace)* Add Unused Dependency Lint ([#453](https://github.com/op-rs/kona/pull/453)) +- *(deps)* Bump Alloy Dependencies ([#409](https://github.com/op-rs/kona/pull/409)) + +### Other +- *(workspace)* Alloy Version Bumps ([#467](https://github.com/op-rs/kona/pull/467)) +- *(workspace)* Update for `op-rs` org transfer ([#474](https://github.com/op-rs/kona/pull/474)) +- *(workspace)* Hoist Dependencies ([#466](https://github.com/op-rs/kona/pull/466)) +- *(bin)* Remove `kt` ([#461](https://github.com/op-rs/kona/pull/461)) +- *(deps)* Bump revm version to v13 ([#422](https://github.com/op-rs/kona/pull/422)) + +## [0.0.2](https://github.com/op-rs/kona/compare/kona-mpt-v0.0.1...kona-mpt-v0.0.2) - 2024-06-22 + +### Added +- *(client)* Derivation integration ([#257](https://github.com/op-rs/kona/pull/257)) +- *(client)* Oracle-backed derive traits ([#252](https://github.com/op-rs/kona/pull/252)) +- *(client)* Account + Account storage hinting in `TrieDB` ([#228](https://github.com/op-rs/kona/pull/228)) +- *(client)* Add `current_output_root` to block executor ([#225](https://github.com/op-rs/kona/pull/225)) +- *(ci)* Dependabot config ([#236](https://github.com/op-rs/kona/pull/236)) +- *(client)* `StatelessL2BlockExecutor` ([#210](https://github.com/op-rs/kona/pull/210)) +- *(mpt)* Block hash walkback ([#199](https://github.com/op-rs/kona/pull/199)) +- *(mpt)* Simplify `TrieDB` ([#198](https://github.com/op-rs/kona/pull/198)) +- *(mpt)* Trie DB commit ([#196](https://github.com/op-rs/kona/pull/196)) +- *(mpt)* Trie node insertion ([#195](https://github.com/op-rs/kona/pull/195)) +- *(host)* Host program scaffold ([#184](https://github.com/op-rs/kona/pull/184)) +- *(workspace)* Client programs in workspace ([#178](https://github.com/op-rs/kona/pull/178)) +- *(mpt)* `TrieCacheDB` scaffold ([#174](https://github.com/op-rs/kona/pull/174)) +- *(mpt)* `TrieNode` retrieval ([#173](https://github.com/op-rs/kona/pull/173)) +- *(mpt)* Refactor `TrieNode` ([#172](https://github.com/op-rs/kona/pull/172)) + +### Fixed +- *(mpt)* Fix extension node truncation ([#300](https://github.com/op-rs/kona/pull/300)) +- *(ci)* Release plz ([#145](https://github.com/op-rs/kona/pull/145)) + +### Other +- version dependencies ([#296](https://github.com/op-rs/kona/pull/296)) +- *(mpt)* Do not expose recursion vars ([#197](https://github.com/op-rs/kona/pull/197)) diff --git a/kona/crates/proof/mpt/Cargo.toml b/kona/crates/proof/mpt/Cargo.toml new file mode 100644 index 0000000000000..757b54af4de54 --- /dev/null +++ b/kona/crates/proof/mpt/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "kona-mpt" +description = "Utilities for interacting with and iterating through a merkle patricia trie" +version = "0.3.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# General +thiserror.workspace = true +serde = { workspace = true, optional = true, features = ["derive", "alloc"] } + +# Revm + Alloy +alloy-rlp.workspace = true +alloy-trie.workspace = true +alloy-primitives = { workspace = true, features = ["rlp"] } + +# Op-alloy +op-alloy-rpc-types-engine.workspace = true + +[dev-dependencies] +# Alloy +alloy-provider = { workspace = true, features = ["reqwest"] } +alloy-consensus.workspace = true +alloy-transport-http.workspace = true +alloy-rpc-types = { workspace = true, features = ["eth", "debug"] } + +# General +rand.workspace = true +reqwest.workspace = true +proptest.workspace = true +tokio = { workspace = true, features = ["full"] } +criterion = { workspace = true, features = ["html_reports"] } +pprof = { workspace = true, features = ["criterion", "flamegraph", "frame-pointer"] } + +[features] +default = [ "serde" ] +serde = [ + "alloy-primitives/serde", + "alloy-trie/serde", + "dep:serde", + "op-alloy-rpc-types-engine/serde", +] + +[[bench]] +name = "trie_node" +harness = false diff --git a/kona/crates/proof/mpt/README.md b/kona/crates/proof/mpt/README.md new file mode 100644 index 0000000000000..98aa19a2b2d67 --- /dev/null +++ b/kona/crates/proof/mpt/README.md @@ -0,0 +1,18 @@ +# `kona-mpt` + +CI +Kona MPT +License +Codecov + +A recursive, in-memory implementation of Ethereum's hexary Merkle Patricia Trie (MPT), supporting: +- Retrieval +- Insertion +- Deletion +- Root Computation + - Trie Node RLP Encoding + +This implementation is intended to serve as a backend for a stateless executor of Ethereum blocks, like +the one in the [`kona-executor`](../executor) crate. Starting with a trie root, the `TrieNode` can be +unravelled to access, insert, or delete values. These operations are all backed by the `TrieProvider`, +which enables fetching the preimages of hashed trie nodes. diff --git a/kona/crates/proof/mpt/benches/trie_node.rs b/kona/crates/proof/mpt/benches/trie_node.rs new file mode 100644 index 0000000000000..a177ca0f1140a --- /dev/null +++ b/kona/crates/proof/mpt/benches/trie_node.rs @@ -0,0 +1,158 @@ +#![allow(missing_docs)] +//! Contains benchmarks for the [TrieNode]. + +use alloy_trie::Nibbles; +use criterion::{Criterion, criterion_group, criterion_main}; +use kona_mpt::{NoopTrieHinter, NoopTrieProvider, TrieNode}; +use pprof::criterion::{Output, PProfProfiler}; +use rand::{Rng, SeedableRng, rngs::StdRng, seq::IteratorRandom}; + +fn trie(c: &mut Criterion) { + let mut g = c.benchmark_group("execution"); + g.sample_size(10); + + // Use pseudo-randomness for reproducibility + let mut rng = StdRng::seed_from_u64(42); + + g.bench_function("Insertion - 4096 nodes", |b| { + let keys = (0..2usize.pow(12)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + + b.iter(|| { + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + }); + }); + + g.bench_function("Insertion - 65,536 nodes", |b| { + let keys = (0..2usize.pow(16)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + + b.iter(|| { + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + }); + }); + + g.bench_function("Delete 16 nodes - 4096 nodes", |b| { + let keys = (0..2usize.pow(12)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + + let rng = &mut rand::rng(); + let keys_to_delete = keys.clone().into_iter().choose_multiple(rng, 16); + + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + b.iter(|| { + let trie = &mut trie.clone(); + for key in &keys_to_delete { + trie.delete(key, &NoopTrieProvider, &NoopTrieHinter).unwrap(); + } + }); + }); + + g.bench_function("Delete 16 nodes - 65,536 nodes", |b| { + let keys = (0..2usize.pow(16)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + let rng = &mut rand::rng(); + let keys_to_delete = keys.into_iter().choose_multiple(rng, 16); + + b.iter(|| { + let trie = &mut trie.clone(); + for key in &keys_to_delete { + trie.delete(key, &NoopTrieProvider, &NoopTrieHinter).unwrap(); + } + }); + }); + + g.bench_function("Open 1024 nodes - 4096 nodes", |b| { + let keys = (0..2usize.pow(12)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + let rng = &mut rand::rng(); + let keys_to_retrieve = keys.into_iter().choose_multiple(rng, 1024); + + b.iter(|| { + for key in &keys_to_retrieve { + trie.open(key, &NoopTrieProvider).unwrap(); + } + }); + }); + + g.bench_function("Open 1024 nodes - 65,536 nodes", |b| { + let keys = (0..2usize.pow(16)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + let rng = &mut rand::rng(); + let keys_to_retrieve = keys.into_iter().choose_multiple(rng, 1024); + + b.iter(|| { + for key in &keys_to_retrieve { + trie.open(key, &NoopTrieProvider).unwrap(); + } + }); + }); + + g.bench_function("Compute root, fully open trie - 4096 nodes", |b| { + let keys = (0..2usize.pow(12)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + b.iter(|| { + let trie = &mut trie.clone(); + trie.blind(); + }); + }); + + g.bench_function("Compute root, fully open trie - 65,536 nodes", |b| { + let keys = (0..2usize.pow(16)) + .map(|_| Nibbles::unpack(rng.random::<[u8; 32]>())) + .collect::>(); + let mut trie = TrieNode::Empty; + for key in &keys { + trie.insert(key, key.to_vec().into(), &NoopTrieProvider).unwrap(); + } + + b.iter(|| { + let trie = &mut trie.clone(); + trie.blind(); + }); + }); +} + +criterion_group! { + name = trie_benches; + config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = trie +} +criterion_main!(trie_benches); diff --git a/kona/crates/proof/mpt/src/errors.rs b/kona/crates/proof/mpt/src/errors.rs new file mode 100644 index 0000000000000..53c66d54b85d3 --- /dev/null +++ b/kona/crates/proof/mpt/src/errors.rs @@ -0,0 +1,45 @@ +//! Errors for the `kona-derive` crate. + +use alloc::string::String; +use thiserror::Error; + +/// A [Result] type alias where the error is [TrieNodeError]. +pub type TrieNodeResult = Result; + +/// An error type for [TrieNode] operations. +/// +/// [TrieNode]: crate::TrieNode +#[derive(Error, Debug, PartialEq, Eq)] +pub enum TrieNodeError { + /// Invalid trie node type encountered. + #[error("Invalid trie node type encountered")] + InvalidNodeType, + /// The path was too short to index. + #[error("Path too short")] + PathTooShort, + /// Failed to decode trie node. + #[error("Failed to decode trie node: {0}")] + RLPError(alloy_rlp::Error), + /// Key does not exist in trie. + #[error("Key does not exist in trie.")] + KeyNotFound, + /// Trie node is not a leaf node. + #[error("Trie provider error: {0}")] + Provider(String), +} + +/// A [Result] type alias where the error is [OrderedListWalkerError]. +pub type OrderedListWalkerResult = Result; + +/// An error type for [OrderedListWalker] operations. +/// +/// [OrderedListWalker]: crate::OrderedListWalker +#[derive(Error, Debug, PartialEq, Eq)] +pub enum OrderedListWalkerError { + /// Iterator has already been hydrated, and cannot be re-hydrated until it is exhausted. + #[error("Iterator has already been hydrated, and cannot be re-hydrated until it is exhausted")] + AlreadyHydrated, + /// Trie node error. + #[error("{0}")] + TrieNode(#[from] TrieNodeError), +} diff --git a/kona/crates/proof/mpt/src/lib.rs b/kona/crates/proof/mpt/src/lib.rs new file mode 100644 index 0000000000000..61552e733c8b8 --- /dev/null +++ b/kona/crates/proof/mpt/src/lib.rs @@ -0,0 +1,34 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), no_std)] + +extern crate alloc; + +mod errors; +pub use errors::{OrderedListWalkerError, OrderedListWalkerResult, TrieNodeError, TrieNodeResult}; + +mod traits; +pub use traits::{TrieHinter, TrieProvider}; + +mod node; +pub use node::TrieNode; + +mod list_walker; +pub use list_walker::OrderedListWalker; + +mod noop; +pub use noop::{NoopTrieHinter, NoopTrieProvider}; + +mod util; +pub use util::ordered_trie_with_encoder; + +// Re-export [alloy_trie::Nibbles]. +pub use alloy_trie::Nibbles; + +#[cfg(test)] +mod test_util; diff --git a/kona/crates/proof/mpt/src/list_walker.rs b/kona/crates/proof/mpt/src/list_walker.rs new file mode 100644 index 0000000000000..2a2104c148767 --- /dev/null +++ b/kona/crates/proof/mpt/src/list_walker.rs @@ -0,0 +1,242 @@ +//! This module contains the [OrderedListWalker] struct, which allows for traversing an MPT root of +//! a derivable ordered list. + +use crate::{ + TrieNode, TrieNodeError, TrieProvider, + errors::{OrderedListWalkerError, OrderedListWalkerResult}, +}; +use alloc::{collections::VecDeque, string::ToString, vec}; +use alloy_primitives::{B256, Bytes}; +use alloy_rlp::EMPTY_STRING_CODE; +use core::marker::PhantomData; + +/// A [OrderedListWalker] allows for traversing over a Merkle Patricia Trie containing a derivable +/// ordered list. +/// +/// Once it has been hydrated with [Self::hydrate], the elements in the derivable list can be +/// iterated over using the [Iterator] implementation. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct OrderedListWalker { + /// The Merkle Patricia Trie root. + root: B256, + /// The leaf nodes of the derived list, in order. [None] if the tree has yet to be fully + /// traversed with [Self::hydrate]. + inner: Option>, + /// Phantom data + _phantom: PhantomData, +} + +impl OrderedListWalker +where + F: TrieProvider, +{ + /// Creates a new [OrderedListWalker], yet to be hydrated. + pub const fn new(root: B256) -> Self { + Self { root, inner: None, _phantom: PhantomData } + } + + /// Creates a new [OrderedListWalker] and hydrates it with [Self::hydrate] and the given fetcher + /// immediately. + pub fn try_new_hydrated(root: B256, fetcher: &F) -> OrderedListWalkerResult { + let mut walker = Self { root, inner: None, _phantom: PhantomData }; + walker.hydrate(fetcher)?; + Ok(walker) + } + + /// Hydrates the [OrderedListWalker]'s iterator with the leaves of the derivable list. If + /// `Self::inner` is [Some], this function will fail fast. + pub fn hydrate(&mut self, fetcher: &F) -> OrderedListWalkerResult<()> { + // Do not allow for re-hydration if `inner` is `Some` and still contains elements. + if self.inner.is_some() && self.inner.as_ref().map(|s| s.len()).unwrap_or_default() > 0 { + return Err(OrderedListWalkerError::AlreadyHydrated); + } + + // Get the preimage to the root node. + let root_trie_node = Self::get_trie_node(self.root, fetcher)?; + + // With small lists the iterator seems to use 0x80 (RLP empty string, unlike the others) + // as key for item 0, causing it to come last. We need to account for this, pulling the + // first element into its proper position. + let mut ordered_list = Self::fetch_leaves(&root_trie_node, fetcher)?; + if !ordered_list.is_empty() { + if ordered_list.len() <= EMPTY_STRING_CODE as usize { + // If the list length is < 0x80, the final element is the first element. + let first = ordered_list.pop_back().expect("Cannot be empty"); + ordered_list.push_front(first); + } else { + // If the list length is > 0x80, the element at index 0x80-1 is the first element. + let first = + ordered_list.remove((EMPTY_STRING_CODE - 1) as usize).expect("Cannot be empty"); + ordered_list.push_front(first); + } + } + + self.inner = Some(ordered_list); + Ok(()) + } + + /// Takes the inner list of the [OrderedListWalker], returning it and setting the inner list to + /// [None]. + pub const fn take_inner(&mut self) -> Option> { + self.inner.take() + } + + /// Traverses a [TrieNode], returning all values of child [TrieNode::Leaf] variants. + fn fetch_leaves( + trie_node: &TrieNode, + fetcher: &F, + ) -> OrderedListWalkerResult> { + match trie_node { + TrieNode::Branch { stack } => { + let mut leaf_values = VecDeque::with_capacity(stack.len()); + for item in stack.iter() { + match item { + TrieNode::Blinded { commitment } => { + // If the string is a hash, we need to grab the preimage for it and + // continue recursing. + let trie_node = Self::get_trie_node(commitment.as_ref(), fetcher)?; + leaf_values.append(&mut Self::fetch_leaves(&trie_node, fetcher)?); + } + TrieNode::Empty => { /* Skip over empty nodes, we're looking for values. */ + } + item => { + // If the item is already retrieved, recurse on it. + leaf_values.append(&mut Self::fetch_leaves(item, fetcher)?); + } + } + } + Ok(leaf_values) + } + TrieNode::Leaf { prefix, value } => { + Ok(vec![(prefix.to_vec().into(), value.clone())].into()) + } + TrieNode::Extension { node, .. } => { + // If the node is a hash, we need to grab the preimage for it and continue + // recursing. If it is already retrieved, recurse on it. + match node.as_ref() { + TrieNode::Blinded { commitment } => { + let trie_node = Self::get_trie_node(commitment.as_ref(), fetcher)?; + Ok(Self::fetch_leaves(&trie_node, fetcher)?) + } + node => Ok(Self::fetch_leaves(node, fetcher)?), + } + } + TrieNode::Empty => Ok(VecDeque::new()), + _ => Err(TrieNodeError::InvalidNodeType.into()), + } + } + + /// Grabs the preimage of `hash` using `fetcher`, and attempts to decode the preimage data into + /// a [TrieNode]. Will error if the conversion of `T` into [B256] fails. + fn get_trie_node(hash: T, fetcher: &F) -> OrderedListWalkerResult + where + T: Into, + { + fetcher + .trie_node_by_hash(hash.into()) + .map_err(|e| TrieNodeError::Provider(e.to_string())) + .map_err(Into::into) + } +} + +impl Iterator for OrderedListWalker +where + F: TrieProvider, +{ + type Item = (Bytes, Bytes); + + fn next(&mut self) -> Option { + match self.inner { + Some(ref mut leaves) => { + let item = leaves.pop_front(); + if leaves.is_empty() { + self.inner = None; + } + item + } + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + NoopTrieProvider, ordered_trie_with_encoder, + test_util::{ + TrieNodeProvider, get_live_derivable_receipts_list, + get_live_derivable_transactions_list, + }, + }; + use alloc::{collections::BTreeMap, string::String, vec::Vec}; + use alloy_consensus::{ReceiptEnvelope, TxEnvelope}; + use alloy_primitives::keccak256; + use alloy_provider::network::eip2718::Decodable2718; + use alloy_rlp::{Decodable, Encodable}; + + #[tokio::test] + async fn test_online_list_walker_receipts() { + let (root, preimages, envelopes) = get_live_derivable_receipts_list().await.unwrap(); + let fetcher = TrieNodeProvider::new(preimages); + let list = OrderedListWalker::try_new_hydrated(root, &fetcher).unwrap(); + + assert_eq!( + list.into_iter() + .map(|(_, rlp)| ReceiptEnvelope::decode_2718(&mut rlp.as_ref()).unwrap()) + .collect::>(), + envelopes + ); + } + + #[tokio::test] + async fn test_online_list_walker_transactions() { + let (root, preimages, envelopes) = get_live_derivable_transactions_list().await.unwrap(); + let fetcher = TrieNodeProvider::new(preimages); + let list = OrderedListWalker::try_new_hydrated(root, &fetcher).unwrap(); + + assert_eq!( + list.into_iter() + .map(|(_, rlp)| TxEnvelope::decode(&mut rlp.as_ref()).unwrap()) + .collect::>(), + envelopes + ); + } + + #[test] + fn test_list_walker() { + const VALUES: [&str; 3] = ["test one", "test two", "test three"]; + + let mut trie = ordered_trie_with_encoder(&VALUES, |v, buf| v.encode(buf)); + let root = trie.root(); + + let preimages = trie.take_proof_nodes().into_inner().into_iter().fold( + BTreeMap::default(), + |mut acc, (_, value)| { + acc.insert(keccak256(value.as_ref()), value); + acc + }, + ); + + let fetcher = TrieNodeProvider::new(preimages); + let list = OrderedListWalker::try_new_hydrated(root, &fetcher).unwrap(); + + assert_eq!( + list.inner + .unwrap() + .iter() + .map(|(_, v)| { String::decode(&mut v.as_ref()).unwrap() }) + .collect::>(), + VALUES + ); + } + + #[test] + fn test_empty_list_walker() { + assert!( + OrderedListWalker::fetch_leaves(&TrieNode::Empty, &NoopTrieProvider) + .expect("Failed to traverse empty trie") + .is_empty() + ); + } +} diff --git a/kona/crates/proof/mpt/src/node.rs b/kona/crates/proof/mpt/src/node.rs new file mode 100644 index 0000000000000..298e122326c52 --- /dev/null +++ b/kona/crates/proof/mpt/src/node.rs @@ -0,0 +1,858 @@ +//! This module contains the [TrieNode] type, which represents a node within a standard Merkle +//! Patricia Trie. + +use crate::{ + TrieHinter, TrieNodeError, TrieProvider, + errors::TrieNodeResult, + util::{rlp_list_element_length, unpack_path_to_nibbles}, +}; +use alloc::{boxed::Box, string::ToString, vec, vec::Vec}; +use alloy_primitives::{B256, Bytes, keccak256}; +use alloy_rlp::{Buf, Decodable, EMPTY_STRING_CODE, Encodable, Header, length_of_length}; +use alloy_trie::{EMPTY_ROOT_HASH, Nibbles}; + +/// The length of the branch list when RLP encoded +const BRANCH_LIST_LENGTH: usize = 17; + +/// The length of a leaf or extension node's RLP encoded list +const LEAF_OR_EXTENSION_LIST_LENGTH: usize = 2; + +/// The number of nibbles traversed in a branch node. +const BRANCH_NODE_NIBBLES: usize = 1; + +/// Prefix for even-nibbled extension node paths. +const PREFIX_EXTENSION_EVEN: u8 = 0; + +/// Prefix for odd-nibbled extension node paths. +const PREFIX_EXTENSION_ODD: u8 = 1; + +/// Prefix for even-nibbled leaf node paths. +const PREFIX_LEAF_EVEN: u8 = 2; + +/// Prefix for odd-nibbled leaf node paths. +const PREFIX_LEAF_ODD: u8 = 3; + +/// Nibble bit width. +const NIBBLE_WIDTH: usize = 4; + +/// A [TrieNode] is a node within a standard Ethereum Merkle Patricia Trie. In this implementation, +/// keys are expected to be fixed-size nibble sequences, and values are arbitrary byte sequences. +/// +/// The [TrieNode] has several variants: +/// - [TrieNode::Empty] represents an empty node. +/// - [TrieNode::Blinded] represents a node that has been blinded by a commitment. +/// - [TrieNode::Leaf] represents a 2-item node with the encoding `rlp([encoded_path, value])`. +/// - [TrieNode::Extension] represents a 2-item pointer node with the encoding `rlp([encoded_path, +/// key])`. +/// - [TrieNode::Branch] represents a node that refers to up to 16 child nodes with the encoding +/// `rlp([ v0, ..., v15, value ])`. +/// +/// In the Ethereum Merkle Patricia Trie, nodes longer than an encoded 32 byte string (33 total +/// bytes) are blinded with [keccak256] hashes. When a node is "opened", it is replaced with the +/// [TrieNode] that is decoded from to the preimage of the hash. +/// +/// The [alloy_rlp::Encodable] and [alloy_rlp::Decodable] traits are implemented for [TrieNode], +/// allowing for RLP encoding and decoding of the types for storage and retrieval. The +/// implementation of these traits will implicitly blind nodes that are longer than 32 bytes in +/// length when encoding. When decoding, the implementation will leave blinded nodes in place. +/// +/// ## SAFETY +/// As this implementation only supports uniform key sizes, the [TrieNode] data structure will fail +/// to behave correctly if confronted with keys of varying lengths. Namely, this is because it does +/// not support the `value` field in branch nodes, just like the Ethereum Merkle Patricia Trie. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TrieNode { + /// An empty [TrieNode] is represented as an [EMPTY_STRING_CODE] (0x80). + Empty, + /// A blinded node is a node that has been blinded by a [keccak256] commitment. + Blinded { + /// The commitment that blinds the node. + commitment: B256, + }, + /// A leaf node is a 2-item node with the encoding `rlp([encoded_path, value])` + Leaf { + /// The key of the leaf node + prefix: Nibbles, + /// The value of the leaf node + value: Bytes, + }, + /// An extension node is a 2-item pointer node with the encoding `rlp([encoded_path, key])` + Extension { + /// The path prefix of the extension + prefix: Nibbles, + /// The pointer to the child node + node: Box, + }, + /// A branch node refers to up to 16 child nodes with the encoding + /// `rlp([ v0, ..., v15, value ])` + Branch { + /// The 16 child nodes and value of the branch. + stack: Vec, + }, +} + +impl TrieNode { + /// Creates a new [TrieNode::Blinded] node. + /// + /// ## Takes + /// - `commitment` - The commitment that blinds the node + /// + /// ## Returns + /// - `Self` - The new blinded [TrieNode]. + pub const fn new_blinded(commitment: B256) -> Self { + Self::Blinded { commitment } + } + + /// Blinds the [TrieNode].. Alternatively, if the [TrieNode] is a [TrieNode::Blinded] node + /// already, its commitment is returned directly. + pub fn blind(&self) -> B256 { + match self { + Self::Blinded { commitment } => *commitment, + Self::Empty => EMPTY_ROOT_HASH, + _ => { + let mut rlp_buf = Vec::with_capacity(self.length()); + self.encode(&mut rlp_buf); + keccak256(rlp_buf) + } + } + } + + /// Unblinds the [TrieNode] if it is a [TrieNode::Blinded] node. + pub fn unblind(&mut self, fetcher: &F) -> TrieNodeResult<()> { + if let Self::Blinded { commitment } = self { + if *commitment == EMPTY_ROOT_HASH { + // If the commitment is the empty root hash, the node is empty, and we don't need to + // reach out to the fetcher. + *self = Self::Empty; + } else { + *self = fetcher + .trie_node_by_hash(*commitment) + .map_err(|e| TrieNodeError::Provider(e.to_string()))?; + } + } + Ok(()) + } + + /// Walks down the trie to a leaf value with the given key, if it exists. Preimages for blinded + /// nodes along the path are fetched using the `fetcher` function, and persisted in the inner + /// [TrieNode] elements. + /// + /// ## Takes + /// - `self` - The root trie node + /// - `path` - The nibbles representation of the path to the leaf node + /// - `fetcher` - The preimage fetcher for intermediate blinded nodes + /// + /// ## Returns + /// - `Err(_)` - Could not retrieve the node with the given key from the trie. + /// - `Ok(None)` - The node with the given key does not exist in the trie. + /// - `Ok(Some(_))` - The value of the node + pub fn open<'a, F: TrieProvider>( + &'a mut self, + path: &Nibbles, + fetcher: &F, + ) -> TrieNodeResult> { + match self { + Self::Branch { stack } => { + let branch_nibble = path.get(0).ok_or(TrieNodeError::PathTooShort)? as usize; + stack + .get_mut(branch_nibble) + .map(|node| node.open(&path.slice(BRANCH_NODE_NIBBLES..), fetcher)) + .unwrap_or(Ok(None)) + } + Self::Leaf { prefix, value } => Ok((path == prefix).then_some(value)), + Self::Extension { prefix, node } => { + if path.slice(..prefix.len()) == *prefix { + // Follow extension branch + node.unblind(fetcher)?; + node.open(&path.slice(prefix.len()..), fetcher) + } else { + Ok(None) + } + } + Self::Blinded { .. } => { + self.unblind(fetcher)?; + self.open(path, fetcher) + } + Self::Empty => Ok(None), + } + } + + /// Inserts a [TrieNode] at the given path into the trie rooted at Self. + /// + /// ## Takes + /// - `self` - The root trie node + /// - `path` - The nibbles representation of the path to the leaf node + /// - `node` - The node to insert at the given path + /// - `fetcher` - The preimage fetcher for intermediate blinded nodes + /// + /// ## Returns + /// - `Err(_)` - Could not insert the node at the given path in the trie. + /// - `Ok(())` - The node was successfully inserted at the given path. + pub fn insert( + &mut self, + path: &Nibbles, + value: Bytes, + fetcher: &F, + ) -> TrieNodeResult<()> { + match self { + Self::Empty => { + // If the trie node is null, insert the leaf node at the current path. + *self = Self::Leaf { prefix: *path, value }; + Ok(()) + } + Self::Leaf { prefix, value: leaf_value } => { + let shared_extension_nibbles = path.common_prefix_length(prefix); + + // If all nibbles are shared, update the leaf node with the new value. + if path == prefix { + *self = Self::Leaf { prefix: *prefix, value }; + return Ok(()); + } + + // Create a branch node stack containing the leaf node and the new value. + let mut stack = vec![Self::Empty; BRANCH_LIST_LENGTH]; + + // Insert the shortened extension into the branch stack. + let extension_nibble = + prefix.get(shared_extension_nibbles).ok_or(TrieNodeError::PathTooShort)? + as usize; + stack[extension_nibble] = Self::Leaf { + prefix: prefix.slice(shared_extension_nibbles + BRANCH_NODE_NIBBLES..), + value: leaf_value.clone(), + }; + + // Insert the new value into the branch stack. + let branch_nibble_new = + path.get(shared_extension_nibbles).ok_or(TrieNodeError::PathTooShort)? as usize; + stack[branch_nibble_new] = Self::Leaf { + prefix: path.slice(shared_extension_nibbles + BRANCH_NODE_NIBBLES..), + value, + }; + + // Replace the leaf node with the branch if no nibbles are shared, else create an + // extension. + if shared_extension_nibbles == 0 { + *self = Self::Branch { stack }; + } else { + let raw_ext_nibbles = path.slice(..shared_extension_nibbles); + *self = Self::Extension { + prefix: raw_ext_nibbles, + node: Box::new(Self::Branch { stack }), + }; + } + Ok(()) + } + Self::Extension { prefix, node } => { + let shared_extension_nibbles = path.common_prefix_length(prefix); + if shared_extension_nibbles == prefix.len() { + node.insert(&path.slice(shared_extension_nibbles..), value, fetcher)?; + return Ok(()); + } + + // Create a branch node stack containing the leaf node and the new value. + let mut stack = vec![Self::Empty; BRANCH_LIST_LENGTH]; + + // Insert the shortened extension into the branch stack. + let extension_nibble = + prefix.get(shared_extension_nibbles).ok_or(TrieNodeError::PathTooShort)? + as usize; + let new_prefix = prefix.slice(shared_extension_nibbles + BRANCH_NODE_NIBBLES..); + stack[extension_nibble] = if new_prefix.is_empty() { + // In the case that the extension node no longer has a prefix, insert the node + // verbatim into the branch. + node.as_ref().clone() + } else { + Self::Extension { prefix: new_prefix, node: node.clone() } + }; + + // Insert the new value into the branch stack. + let branch_nibble_new = + path.get(shared_extension_nibbles).ok_or(TrieNodeError::PathTooShort)? as usize; + stack[branch_nibble_new] = Self::Leaf { + prefix: path.slice(shared_extension_nibbles + BRANCH_NODE_NIBBLES..), + value, + }; + + // Replace the extension node with the branch if no nibbles are shared, else create + // an extension. + if shared_extension_nibbles == 0 { + *self = Self::Branch { stack }; + } else { + let extension = path.slice(..shared_extension_nibbles); + *self = Self::Extension { + prefix: extension, + node: Box::new(Self::Branch { stack }), + }; + } + Ok(()) + } + Self::Branch { stack } => { + // Follow the branch node to the next node in the path. + let branch_nibble = path.get(0).ok_or(TrieNodeError::PathTooShort)? as usize; + stack[branch_nibble].insert(&path.slice(BRANCH_NODE_NIBBLES..), value, fetcher) + } + Self::Blinded { .. } => { + // If a blinded node is approached, reveal the node and continue the insertion + // recursion. + self.unblind(fetcher)?; + self.insert(path, value, fetcher) + } + } + } + + /// Deletes a node in the trie at the given path. + /// + /// ## Takes + /// - `self` - The root trie node + /// - `path` - The nibbles representation of the path to the leaf node + /// + /// ## Returns + /// - `Err(_)` - Could not delete the node at the given path in the trie. + /// - `Ok(())` - The node was successfully deleted at the given path. + pub fn delete( + &mut self, + path: &Nibbles, + fetcher: &F, + hinter: &H, + ) -> TrieNodeResult<()> { + match self { + Self::Empty => Err(TrieNodeError::KeyNotFound), + Self::Leaf { prefix, .. } => { + if path == prefix { + *self = Self::Empty; + Ok(()) + } else { + Err(TrieNodeError::KeyNotFound) + } + } + Self::Extension { prefix, node } => { + let shared_nibbles = path.common_prefix_length(prefix); + if shared_nibbles < prefix.len() { + return Err(TrieNodeError::KeyNotFound); + } else if shared_nibbles == path.len() { + *self = Self::Empty; + return Ok(()); + } + + node.delete(&path.slice(prefix.len()..), fetcher, hinter)?; + + // Simplify extension if possible after the deletion + self.collapse_if_possible(fetcher, hinter) + } + Self::Branch { stack } => { + let branch_nibble = path.get(0).ok_or(TrieNodeError::PathTooShort)? as usize; + stack[branch_nibble].delete(&path.slice(BRANCH_NODE_NIBBLES..), fetcher, hinter)?; + + // Simplify the branch if possible after the deletion + self.collapse_if_possible(fetcher, hinter) + } + Self::Blinded { .. } => { + self.unblind(fetcher)?; + self.delete(path, fetcher, hinter) + } + } + } + + /// If applicable, collapses `self` into a more compact form. + /// + /// ## Takes + /// - `self` - The root trie node + /// + /// ## Returns + /// - `Ok(())` - The node was successfully collapsed + /// - `Err(_)` - Could not collapse the node + fn collapse_if_possible( + &mut self, + fetcher: &F, + hinter: &H, + ) -> TrieNodeResult<()> { + match self { + Self::Extension { prefix, node } => match node.as_mut() { + Self::Extension { prefix: child_prefix, node: child_node } => { + // Double extensions are collapsed into a single extension. + let new_prefix = Nibbles::from_nibbles_unchecked( + [prefix.to_vec(), child_prefix.to_vec()].concat(), + ); + *self = Self::Extension { prefix: new_prefix, node: child_node.clone() }; + } + Self::Leaf { prefix: child_prefix, value: child_value } => { + // If the child node is a leaf, convert the extension into a leaf with the full + // path. + let new_prefix = Nibbles::from_nibbles_unchecked( + [prefix.to_vec(), child_prefix.to_vec()].concat(), + ); + *self = Self::Leaf { prefix: new_prefix, value: child_value.clone() }; + } + Self::Empty => { + // If the child node is empty, convert the extension into an empty node. + *self = Self::Empty; + } + _ => { + // If the child is a (blinded?) branch then no need for collapse + // because deletion did not collapse the (blinded?) branch + } + }, + Self::Branch { stack } => { + // Count non-empty children + let mut non_empty_children = stack + .iter_mut() + .enumerate() + .filter(|(_, node)| !matches!(node, Self::Empty)) + .collect::>(); + + if non_empty_children.len() == 1 { + let (index, non_empty_node) = &mut non_empty_children[0]; + + // If only one non-empty child and no value, convert to extension or leaf + match non_empty_node { + Self::Leaf { prefix, value } => { + let new_prefix = Nibbles::from_nibbles_unchecked( + [&[*index as u8], prefix.to_vec().as_slice()].concat(), + ); + *self = Self::Leaf { prefix: new_prefix, value: value.clone() }; + } + Self::Extension { prefix, node } => { + let new_prefix = Nibbles::from_nibbles_unchecked( + [&[*index as u8], prefix.to_vec().as_slice()].concat(), + ); + *self = Self::Extension { prefix: new_prefix, node: node.clone() }; + } + Self::Branch { .. } => { + *self = Self::Extension { + prefix: Nibbles::from_nibbles_unchecked([*index as u8]), + node: Box::new(non_empty_node.clone()), + }; + } + Self::Blinded { commitment } => { + // In this special case, we need to send a hint to fetch the preimage of + // the blinded node, since it is outside of the paths that have been + // traversed so far. + hinter + .hint_trie_node(*commitment) + .map_err(|e| TrieNodeError::Provider(e.to_string()))?; + + non_empty_node.unblind(fetcher)?; + self.collapse_if_possible(fetcher, hinter)?; + } + _ => {} + }; + } + } + _ => {} + } + Ok(()) + } + + /// Attempts to convert a `path` and `value` into a [TrieNode], if they correspond to a + /// [TrieNode::Leaf] or [TrieNode::Extension]. + /// + /// **Note:** This function assumes that the passed reader has already consumed the RLP header + /// of the [TrieNode::Leaf] or [TrieNode::Extension] node. + fn try_decode_leaf_or_extension_payload(buf: &mut &[u8]) -> TrieNodeResult { + // Decode the path and value of the leaf or extension node. + let path = Bytes::decode(buf).map_err(TrieNodeError::RLPError)?; + let first_nibble = path[0] >> NIBBLE_WIDTH; + let first = match first_nibble { + PREFIX_EXTENSION_ODD | PREFIX_LEAF_ODD => Some(path[0] & 0x0F), + PREFIX_EXTENSION_EVEN | PREFIX_LEAF_EVEN => None, + _ => return Err(TrieNodeError::InvalidNodeType), + }; + + // Check the high-order nibble of the path to determine the type of node. + match first_nibble { + PREFIX_EXTENSION_EVEN | PREFIX_EXTENSION_ODD => { + // Extension node + let extension_node_value = Self::decode(buf).map_err(TrieNodeError::RLPError)?; + Ok(Self::Extension { + prefix: unpack_path_to_nibbles(first, path[1..].as_ref()), + node: Box::new(extension_node_value), + }) + } + PREFIX_LEAF_EVEN | PREFIX_LEAF_ODD => { + // Leaf node + let value = Bytes::decode(buf).map_err(TrieNodeError::RLPError)?; + Ok(Self::Leaf { prefix: unpack_path_to_nibbles(first, path[1..].as_ref()), value }) + } + _ => Err(TrieNodeError::InvalidNodeType), + } + } + + /// Returns the RLP payload length of the [TrieNode]. + pub(crate) fn payload_length(&self) -> usize { + match self { + Self::Empty => 0, + Self::Blinded { commitment } => commitment.len(), + Self::Leaf { prefix, value } => { + let mut encoded_key_len = prefix.len() / 2 + 1; + if encoded_key_len != 1 { + encoded_key_len += length_of_length(encoded_key_len); + } + encoded_key_len + value.length() + } + Self::Extension { prefix, node } => { + let mut encoded_key_len = prefix.len() / 2 + 1; + if encoded_key_len != 1 { + encoded_key_len += length_of_length(encoded_key_len); + } + encoded_key_len + node.blinded_length() + } + Self::Branch { stack } => { + // In branch nodes, if an element is longer than an encoded 32 byte string, it is + // blinded. Assuming we have an open trie node, we must re-hash the + // elements that are longer than an encoded 32 byte string + // in length. + stack.iter().fold(0, |mut acc, node| { + acc += node.blinded_length(); + acc + }) + } + } + } + + /// Returns the encoded length of the trie node, blinding it if it is longer than an encoded + /// [B256] string in length. + /// + /// ## Returns + /// - `usize` - The encoded length of the value, blinded if the raw encoded length is longer + /// than a [B256]. + fn blinded_length(&self) -> usize { + let encoded_len = self.length(); + if encoded_len >= B256::ZERO.len() { B256::ZERO.length() } else { encoded_len } + } +} + +impl Encodable for TrieNode { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let payload_length = self.payload_length(); + match self { + Self::Empty => out.put_u8(EMPTY_STRING_CODE), + Self::Blinded { commitment } => commitment.encode(out), + Self::Leaf { prefix, value } => { + // Encode the leaf node's header and key-value pair. + Header { list: true, payload_length }.encode(out); + alloy_trie::nodes::encode_path_leaf(prefix, true).as_slice().encode(out); + value.encode(out); + } + Self::Extension { prefix, node } => { + // Encode the extension node's header, prefix, and pointer node. + Header { list: true, payload_length }.encode(out); + alloy_trie::nodes::encode_path_leaf(prefix, false).as_slice().encode(out); + if node.length() >= B256::ZERO.len() { + let hash = node.blind(); + hash.encode(out); + } else { + node.encode(out); + } + } + Self::Branch { stack } => { + // In branch nodes, if an element is longer than 32 bytes in length, it is blinded. + // Assuming we have an open trie node, we must re-hash the elements + // that are longer than 32 bytes in length. + Header { list: true, payload_length }.encode(out); + stack.iter().for_each(|node| { + if node.length() >= B256::ZERO.len() { + let hash = node.blind(); + hash.encode(out); + } else { + node.encode(out); + } + }); + } + } + } + + fn length(&self) -> usize { + match self { + Self::Empty => 1, + Self::Blinded { commitment } => commitment.length(), + Self::Leaf { .. } => { + let payload_length = self.payload_length(); + Header { list: true, payload_length }.length() + payload_length + } + Self::Extension { .. } => { + let payload_length = self.payload_length(); + Header { list: true, payload_length }.length() + payload_length + } + Self::Branch { .. } => { + let payload_length = self.payload_length(); + Header { list: true, payload_length }.length() + payload_length + } + } + } +} + +impl Decodable for TrieNode { + /// Attempts to decode the [TrieNode]. + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + // Peek at the header to determine the type of Trie node we're currently decoding. + let header = Header::decode(&mut (**buf).as_ref())?; + + if header.list { + // Peek at the RLP stream to determine the number of elements in the list. + let list_length = rlp_list_element_length(&mut (**buf).as_ref())?; + + match list_length { + BRANCH_LIST_LENGTH => { + let list = Vec::::decode(buf)?; + Ok(Self::Branch { stack: list }) + } + LEAF_OR_EXTENSION_LIST_LENGTH => { + // Advance the buffer to the start of the list payload. + buf.advance(header.length()); + // Decode the leaf or extension node's raw payload. + Self::try_decode_leaf_or_extension_payload(buf) + .map_err(|_| alloy_rlp::Error::UnexpectedList) + } + _ => Err(alloy_rlp::Error::UnexpectedLength), + } + } else { + match header.payload_length { + 0 => { + buf.advance(header.length()); + Ok(Self::Empty) + } + 32 => { + let commitment = B256::decode(buf)?; + Ok(Self::new_blinded(commitment)) + } + _ => Err(alloy_rlp::Error::UnexpectedLength), + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + NoopTrieHinter, NoopTrieProvider, TrieNode, ordered_trie_with_encoder, + test_util::TrieNodeProvider, + }; + use alloc::{collections::BTreeMap, vec, vec::Vec}; + use alloy_primitives::{b256, bytes, hex, keccak256}; + use alloy_rlp::{Decodable, EMPTY_STRING_CODE, Encodable}; + use alloy_trie::{HashBuilder, Nibbles}; + use rand::prelude::IteratorRandom; + + #[test] + fn test_empty_blinded() { + let trie_node = TrieNode::Empty; + assert_eq!(trie_node.blind(), EMPTY_ROOT_HASH); + } + + #[test] + fn test_decode_branch() { + const BRANCH_RLP: [u8; 83] = hex!( + "f851a0eb08a66a94882454bec899d3e82952dcc918ba4b35a09a84acd98019aef4345080808080808080a05d87a81d9bbf5aee61a6bfeab3a5643347e2c751b36789d988a5b6b163d496518080808080808080" + ); + let expected = TrieNode::Branch { + stack: vec![ + TrieNode::new_blinded(b256!( + "eb08a66a94882454bec899d3e82952dcc918ba4b35a09a84acd98019aef43450" + )), + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::new_blinded(b256!( + "5d87a81d9bbf5aee61a6bfeab3a5643347e2c751b36789d988a5b6b163d49651" + )), + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + ], + }; + + let mut rlp_buf = Vec::with_capacity(expected.length()); + expected.encode(&mut rlp_buf); + assert_eq!(rlp_buf.len(), BRANCH_RLP.len()); + assert_eq!(expected.length(), BRANCH_RLP.len()); + + assert_eq!(expected, TrieNode::decode(&mut BRANCH_RLP.as_slice()).unwrap()); + assert_eq!(rlp_buf.as_slice(), &BRANCH_RLP[..]); + } + + #[test] + fn test_encode_decode_extension_open_short() { + const EXTENSION_RLP: [u8; 19] = hex!("d28300646fcd308b8a74657374207468726565"); + + let opened = TrieNode::Leaf { + prefix: Nibbles::from_nibbles([0x00]), + value: bytes!("8a74657374207468726565"), + }; + let expected = + TrieNode::Extension { prefix: Nibbles::unpack(bytes!("646f")), node: Box::new(opened) }; + + let mut rlp_buf = Vec::with_capacity(expected.length()); + expected.encode(&mut rlp_buf); + + assert_eq!(expected, TrieNode::decode(&mut EXTENSION_RLP.as_slice()).unwrap()); + } + + #[test] + fn test_encode_decode_extension_blinded_long() { + const EXTENSION_RLP: [u8; 38] = + hex!("e58300646fa0f3fe8b3c5b21d3e52860f1e4a5825a6100bb341069c1e88f4ebf6bd98de0c190"); + let mut rlp_buf = Vec::new(); + + let opened = + TrieNode::Leaf { prefix: Nibbles::from_nibbles([0x00]), value: [0xFF; 64].into() }; + opened.encode(&mut rlp_buf); + let blinded = TrieNode::new_blinded(keccak256(&rlp_buf)); + + rlp_buf.clear(); + let opened_extension = + TrieNode::Extension { prefix: Nibbles::unpack(bytes!("646f")), node: Box::new(opened) }; + opened_extension.encode(&mut rlp_buf); + + let expected = TrieNode::Extension { + prefix: Nibbles::unpack(bytes!("646f")), + node: Box::new(blinded), + }; + assert_eq!(expected, TrieNode::decode(&mut EXTENSION_RLP.as_slice()).unwrap()); + } + + #[test] + fn test_decode_leaf() { + const LEAF_RLP: [u8; 11] = hex!("ca8320646f8576657262FF"); + let expected = + TrieNode::Leaf { prefix: Nibbles::unpack(bytes!("646f")), value: bytes!("76657262FF") }; + assert_eq!(expected, TrieNode::decode(&mut LEAF_RLP.as_slice()).unwrap()); + } + + #[test] + fn test_retrieve_from_trie_simple() { + const VALUES: [&str; 5] = ["yeah", "dog", ", ", "laminar", "flow"]; + + let mut trie = ordered_trie_with_encoder(&VALUES, |v, buf| { + let mut encoded_value = Vec::with_capacity(v.length()); + v.encode(&mut encoded_value); + TrieNode::new_blinded(keccak256(encoded_value)).encode(buf); + }); + let root = trie.root(); + + let preimages = trie.take_proof_nodes().into_inner().into_iter().fold( + BTreeMap::default(), + |mut acc, (_, value)| { + acc.insert(keccak256(value.as_ref()), value); + acc + }, + ); + let fetcher = TrieNodeProvider::new(preimages); + + let mut root_node = fetcher.trie_node_by_hash(root).unwrap(); + for (i, value) in VALUES.iter().enumerate() { + let path_nibbles = Nibbles::unpack([if i == 0 { EMPTY_STRING_CODE } else { i as u8 }]); + let v = root_node.open(&path_nibbles, &fetcher).unwrap().unwrap(); + + let mut encoded_value = Vec::with_capacity(value.length()); + value.encode(&mut encoded_value); + let mut encoded_node = Vec::new(); + TrieNode::new_blinded(keccak256(&encoded_value)).encode(&mut encoded_node); + + assert_eq!(v, encoded_node.as_slice()); + } + + let commitment = root_node.blind(); + assert_eq!(commitment, root); + } + + #[test] + fn test_insert_static() { + let mut node = TrieNode::Empty; + let noop_fetcher = NoopTrieProvider; + node.insert(&Nibbles::unpack(hex!("012345")), bytes!("01"), &noop_fetcher).unwrap(); + node.insert(&Nibbles::unpack(hex!("012346")), bytes!("02"), &noop_fetcher).unwrap(); + + let expected = TrieNode::Extension { + prefix: Nibbles::from_nibbles([0, 1, 2, 3, 4]), + node: Box::new(TrieNode::Branch { + stack: vec![ + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Leaf { prefix: Nibbles::default(), value: bytes!("01") }, + TrieNode::Leaf { prefix: Nibbles::default(), value: bytes!("02") }, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + TrieNode::Empty, + ], + }), + }; + + assert_eq!(node, expected); + } + + proptest::proptest! { + /// Differential test for inserting an arbitrary number of keys into an empty `TrieNode` / `HashBuilder`. + #[test] + fn diff_hash_builder_insert(mut keys in proptest::collection::vec(proptest::prelude::any::<[u8; 32]>(), 1..4096)) { + // Ensure the keys are sorted; `HashBuilder` expects sorted keys.` + keys.sort(); + + let mut hb = HashBuilder::default(); + let mut node = TrieNode::Empty; + + for key in keys { + hb.add_leaf(Nibbles::unpack(key), key.as_ref()); + node.insert(&Nibbles::unpack(key), key.into(), &NoopTrieProvider).unwrap(); + } + + assert_eq!(node.blind(), hb.root()); + } + + /// Differential test for deleting an arbitrary number of keys from a `TrieNode` / `HashBuilder`. + #[test] + fn diff_hash_builder_delete(mut keys in proptest::collection::vec(proptest::prelude::any::<[u8; 32]>(), 1..4096)) { + // Ensure the keys are sorted; `HashBuilder` expects sorted keys.` + keys.sort(); + + let mut hb = HashBuilder::default(); + let mut node = TrieNode::Empty; + + let mut rng = rand::rng(); + let deleted_keys = + keys.clone().into_iter().choose_multiple(&mut rng, 5.min(keys.len())); + + // Insert the keys into the `HashBuilder` and `TrieNode`. + for key in keys { + // Don't add any keys that are to be deleted from the trie node to the `HashBuilder`. + if !deleted_keys.contains(&key) { + hb.add_leaf(Nibbles::unpack(key), key.as_ref()); + } + node.insert(&Nibbles::unpack(key), key.into(), &NoopTrieProvider).unwrap(); + } + + // Delete the keys that were randomly selected from the trie node. + for deleted_key in deleted_keys { + node.delete(&Nibbles::unpack(deleted_key), &NoopTrieProvider, &NoopTrieHinter) + .unwrap(); + } + + // Blind manually, since the single node remaining may be a leaf or empty node, and always must be blinded. + let mut rlp_buf = Vec::with_capacity(node.length()); + node.encode(&mut rlp_buf); + let trie_root = keccak256(rlp_buf); + + assert_eq!(trie_root, hb.root()); + } + } +} diff --git a/kona/crates/proof/mpt/src/noop.rs b/kona/crates/proof/mpt/src/noop.rs new file mode 100644 index 0000000000000..a3930ee0315d5 --- /dev/null +++ b/kona/crates/proof/mpt/src/noop.rs @@ -0,0 +1,52 @@ +//! Trait implementations for `kona-mpt` traits that are effectively a no-op. +//! Provides trait implementations for downstream users who do not require hinting. + +use crate::{TrieHinter, TrieNode, TrieProvider}; +use alloc::string::String; +use alloy_primitives::{Address, B256, U256}; +use core::fmt::Debug; + +/// The default, no-op implementation of the [TrieProvider] trait, used for testing. +#[derive(Debug, Clone, Copy)] +pub struct NoopTrieProvider; + +impl TrieProvider for NoopTrieProvider { + type Error = String; + + fn trie_node_by_hash(&self, _key: B256) -> Result { + Ok(TrieNode::Empty) + } +} + +/// The default, no-op implementation of the [TrieHinter] trait, used for testing. +#[derive(Debug, Clone, Copy)] +pub struct NoopTrieHinter; + +impl TrieHinter for NoopTrieHinter { + type Error = String; + + fn hint_trie_node(&self, _hash: B256) -> Result<(), Self::Error> { + Ok(()) + } + + fn hint_account_proof(&self, _address: Address, _block_number: u64) -> Result<(), Self::Error> { + Ok(()) + } + + fn hint_storage_proof( + &self, + _address: Address, + _slot: U256, + _block_number: u64, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn hint_execution_witness( + &self, + _parent_hash: B256, + _op_payload_attributes: &op_alloy_rpc_types_engine::OpPayloadAttributes, + ) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/kona/crates/proof/mpt/src/test_util.rs b/kona/crates/proof/mpt/src/test_util.rs new file mode 100644 index 0000000000000..c980d684ebb84 --- /dev/null +++ b/kona/crates/proof/mpt/src/test_util.rs @@ -0,0 +1,154 @@ +//! Testing utilities for `kona-mpt` + +use crate::{TrieNode, TrieProvider, ordered_trie_with_encoder}; +use alloc::{collections::BTreeMap, vec::Vec}; +use alloy_consensus::{Receipt, ReceiptEnvelope, ReceiptWithBloom, TxEnvelope, TxType}; +use alloy_primitives::{B256, Bytes, Log, keccak256}; +use alloy_provider::{Provider, ProviderBuilder, network::eip2718::Encodable2718}; +use alloy_rlp::Decodable; +use alloy_rpc_types::BlockTransactions; +use reqwest::Url; + +const RPC_URL: &str = "https://docs-demo.quiknode.pro/"; + +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +#[error("TestTrieProviderError: {0}")] +pub(crate) struct TestTrieProviderError(&'static str); + +/// Grabs a live merkleized receipts list within a block header. +pub(crate) async fn get_live_derivable_receipts_list() +-> Result<(B256, BTreeMap, Vec), TestTrieProviderError> { + // Initialize the provider. + let provider = + ProviderBuilder::new().connect_http(Url::parse(RPC_URL).expect("invalid rpc url")); + + let block_number = 19005266; + let block = provider + .get_block(block_number.into()) + .full() + .await + .map_err(|_| TestTrieProviderError("Missing block"))? + .ok_or(TestTrieProviderError("Missing block"))?; + let receipts = provider + .get_block_receipts(block_number.into()) + .await + .map_err(|_| TestTrieProviderError("Missing receipts"))? + .ok_or(TestTrieProviderError("Missing receipts"))?; + + let consensus_receipts = receipts + .into_iter() + .map(|r| { + let rpc_receipt = r.inner.as_receipt_with_bloom().expect("Infallible"); + let consensus_receipt = ReceiptWithBloom::new( + Receipt { + status: rpc_receipt.receipt.status, + cumulative_gas_used: rpc_receipt.receipt.cumulative_gas_used, + logs: rpc_receipt + .receipt + .logs + .iter() + .map(|l| Log { address: l.address(), data: l.data().clone() }) + .collect(), + }, + rpc_receipt.logs_bloom, + ); + + match r.transaction_type() { + TxType::Legacy => ReceiptEnvelope::Legacy(consensus_receipt), + TxType::Eip2930 => ReceiptEnvelope::Eip2930(consensus_receipt), + TxType::Eip1559 => ReceiptEnvelope::Eip1559(consensus_receipt), + TxType::Eip4844 => ReceiptEnvelope::Eip4844(consensus_receipt), + TxType::Eip7702 => ReceiptEnvelope::Eip7702(consensus_receipt), + } + }) + .collect::>(); + + // Compute the derivable list + let mut list = + ordered_trie_with_encoder(consensus_receipts.as_ref(), |rlp: &ReceiptEnvelope, buf| { + rlp.encode_2718(buf) + }); + let root = list.root(); + + // Sanity check receipts root is correct + assert_eq!(block.header.receipts_root, root); + + // Construct the mapping of hashed intermediates -> raw intermediates + let preimages = list.take_proof_nodes().into_inner().into_iter().fold( + BTreeMap::default(), + |mut acc, (_, value)| { + acc.insert(keccak256(value.as_ref()), value); + acc + }, + ); + + Ok((root, preimages, consensus_receipts)) +} + +/// Grabs a live merkleized transactions list within a block header. +pub(crate) async fn get_live_derivable_transactions_list() +-> Result<(B256, BTreeMap, Vec), TestTrieProviderError> { + // Initialize the provider. + let provider = + ProviderBuilder::new().connect_http(Url::parse(RPC_URL).expect("invalid rpc url")); + + let block_number = 19005266; + let block = provider + .get_block(block_number.into()) + .full() + .await + .map_err(|_| TestTrieProviderError("Missing block"))? + .ok_or(TestTrieProviderError("Missing block"))?; + + let BlockTransactions::Full(txs) = block.transactions else { + return Err(TestTrieProviderError("Did not fetch full block")); + }; + let consensus_txs = txs.into_iter().map(TxEnvelope::from).collect::>(); + + // Compute the derivable list + let mut list = ordered_trie_with_encoder(consensus_txs.as_ref(), |rlp: &TxEnvelope, buf| { + rlp.encode_2718(buf) + }); + let root = list.root(); + + // Sanity check transaction root is correct + assert_eq!(block.header.transactions_root, root); + + // Construct the mapping of hashed intermediates -> raw intermediates + let preimages = list.take_proof_nodes().into_inner().into_iter().fold( + BTreeMap::default(), + |mut acc, (_, value)| { + acc.insert(keccak256(value.as_ref()), value); + acc + }, + ); + + Ok((root, preimages, consensus_txs)) +} + +/// A mock [TrieProvider] for testing that serves in-memory preimages. +pub(crate) struct TrieNodeProvider { + preimages: BTreeMap, +} + +impl TrieNodeProvider { + pub(crate) const fn new(preimages: BTreeMap) -> Self { + Self { preimages } + } +} + +impl TrieProvider for TrieNodeProvider { + type Error = TestTrieProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + TrieNode::decode( + &mut self + .preimages + .get(&key) + .cloned() + .ok_or(TestTrieProviderError("key not found in trie"))? + .as_ref(), + ) + .map_err(|_| TestTrieProviderError("failed to decode trie node")) + } +} diff --git a/kona/crates/proof/mpt/src/traits.rs b/kona/crates/proof/mpt/src/traits.rs new file mode 100644 index 0000000000000..acf3342155135 --- /dev/null +++ b/kona/crates/proof/mpt/src/traits.rs @@ -0,0 +1,84 @@ +//! Contains the [TrieProvider] trait for fetching trie node preimages, contract bytecode, and +//! headers. + +use crate::TrieNode; +use alloy_primitives::{Address, B256, U256}; +use core::fmt::Display; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// The [TrieProvider] trait defines the synchronous interface for fetching trie node preimages. +pub trait TrieProvider { + /// The error type for fetching trie node preimages. + type Error: Display; + + /// Fetches the preimage for the given trie node hash. + /// + /// ## Takes + /// - `key`: The key of the trie node to fetch. + /// + /// ## Returns + /// - Ok(TrieNode): The trie node preimage. + /// - Err(Self::Error): If the trie node preimage could not be fetched. + fn trie_node_by_hash(&self, key: B256) -> Result; +} + +/// The [TrieHinter] trait defines the synchronous interface for hinting the host to fetch trie +/// node preimages. +pub trait TrieHinter { + /// The error type for hinting trie node preimages. + type Error: Display; + + /// Hints the host to fetch the trie node preimage by hash. + /// + /// ## Takes + /// - `hash`: The hash of the trie node to hint. + /// + /// ## Returns + /// - Ok(()): If the hint was successful. + fn hint_trie_node(&self, hash: B256) -> Result<(), Self::Error>; + + /// Hints the host to fetch the trie node preimages on the path to the given address. + /// + /// ## Takes + /// - `address` - The address of the contract whose trie node preimages are to be fetched. + /// - `block_number` - The block number at which the trie node preimages are to be fetched. + /// + /// ## Returns + /// - Ok(()): If the hint was successful. + /// - Err(Self::Error): If the hint was unsuccessful. + fn hint_account_proof(&self, address: Address, block_number: u64) -> Result<(), Self::Error>; + + /// Hints the host to fetch the trie node preimages on the path to the storage slot within the + /// given account's storage trie. + /// + /// ## Takes + /// - `address` - The address of the contract whose trie node preimages are to be fetched. + /// - `slot` - The storage slot whose trie node preimages are to be fetched. + /// - `block_number` - The block number at which the trie node preimages are to be fetched. + /// + /// ## Returns + /// - Ok(()): If the hint was successful. + /// - Err(Self::Error): If the hint was unsuccessful. + fn hint_storage_proof( + &self, + address: Address, + slot: U256, + block_number: u64, + ) -> Result<(), Self::Error>; + + /// Hints the host to fetch the execution witness for the [OpPayloadAttributes] applied on top + /// of the parent block's state. + /// + /// ## Takes + /// - `parent_hash` - The hash of the parent block. + /// - `op_payload_attributes` - The attributes of the operation payload. + /// + /// ## Returns + /// - Ok(()): If the hint was successful. + /// - Err(Self::Error): If the hint was unsuccessful. + fn hint_execution_witness( + &self, + parent_hash: B256, + op_payload_attributes: &OpPayloadAttributes, + ) -> Result<(), Self::Error>; +} diff --git a/kona/crates/proof/mpt/src/util.rs b/kona/crates/proof/mpt/src/util.rs new file mode 100644 index 0000000000000..23fd1f8bc11e8 --- /dev/null +++ b/kona/crates/proof/mpt/src/util.rs @@ -0,0 +1,90 @@ +//! Utilities for `kona-mpt` + +use alloc::vec::Vec; +use alloy_rlp::{Buf, BufMut, Encodable, Header}; +use alloy_trie::{HashBuilder, Nibbles, proof::ProofRetainer}; + +/// Compute a trie root of the collection of items with a custom encoder. +pub fn ordered_trie_with_encoder(items: &[T], mut encode: F) -> HashBuilder +where + F: FnMut(&T, &mut dyn BufMut), +{ + let mut index_buffer = Vec::new(); + let mut value_buffer = Vec::new(); + let items_len = items.len(); + + // Store preimages for all intermediates + let path_nibbles = (0..items_len) + .map(|i| { + let index = adjust_index_for_rlp(i, items_len); + index_buffer.clear(); + index.encode(&mut index_buffer); + Nibbles::unpack(&index_buffer) + }) + .collect::>(); + + let mut hb = HashBuilder::default().with_proof_retainer(ProofRetainer::new(path_nibbles)); + for i in 0..items_len { + let index = adjust_index_for_rlp(i, items_len); + + index_buffer.clear(); + index.encode(&mut index_buffer); + + value_buffer.clear(); + encode(&items[index], &mut value_buffer); + + hb.add_leaf(Nibbles::unpack(&index_buffer), &value_buffer); + } + + hb +} + +/// Adjust the index of an item for rlp encoding. +pub(crate) const fn adjust_index_for_rlp(i: usize, len: usize) -> usize { + if i > 0x7f { + i + } else if i == 0x7f || i + 1 == len { + 0 + } else { + i + 1 + } +} + +/// Walks through a RLP list's elements and returns the total number of elements in the list. +/// Returns [alloy_rlp::Error::UnexpectedString] if the RLP stream is not a list. +/// +/// ## Takes +/// - `buf` - The RLP stream to walk through +/// +/// ## Returns +/// - `Ok(usize)` - The total number of elements in the list +/// - `Err(_)` - The RLP stream is not a list +pub(crate) fn rlp_list_element_length(buf: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let len_after_consume = buf.len() - header.payload_length; + + let mut list_element_length = 0; + while buf.len() > len_after_consume { + let header = Header::decode(buf)?; + buf.advance(header.payload_length); + list_element_length += 1; + } + Ok(list_element_length) +} + +/// Unpack node path to nibbles. +/// +/// ## Takes +/// - `first` - first nibble of the path if it is odd. Must be <= 0x0F, or will create invalid +/// nibbles. +/// - `rest` - rest of the nibbles packed +/// +/// ## Returns +/// - `Nibbles` - unpacked nibbles +pub(crate) fn unpack_path_to_nibbles(first: Option, rest: &[u8]) -> Nibbles { + let rest = Nibbles::unpack(rest); + Nibbles::from_iter_unchecked(first.into_iter().chain(rest.to_vec())) +} diff --git a/kona/crates/proof/preimage/CHANGELOG.md b/kona/crates/proof/preimage/CHANGELOG.md new file mode 100644 index 0000000000000..edb26a4b8d1c5 --- /dev/null +++ b/kona/crates/proof/preimage/CHANGELOG.md @@ -0,0 +1,87 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.1](https://github.com/op-rs/kona/compare/kona-preimage-v0.2.0...kona-preimage-v0.2.1) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +## [0.2.0](https://github.com/op-rs/kona/compare/kona-preimage-v0.1.0...kona-preimage-v0.2.0) - 2024-12-03 + +### Added + +- *(workspace)* Isolate FPVM-specific platform code ([#821](https://github.com/op-rs/kona/pull/821)) + +## [0.0.5](https://github.com/op-rs/kona/compare/kona-preimage-v0.0.4...kona-preimage-v0.0.5) - 2024-11-20 + +### Added + +- *(preimage)* Decouple from `kona-common` ([#817](https://github.com/op-rs/kona/pull/817)) + +### Other + +- *(driver)* use tracing macros ([#823](https://github.com/op-rs/kona/pull/823)) +- *(workspace)* Reorganize SDK ([#816](https://github.com/op-rs/kona/pull/816)) + +## [0.0.4](https://github.com/op-rs/kona/compare/kona-preimage-v0.0.3...kona-preimage-v0.0.4) - 2024-10-25 + +### Added + +- remove thiserror ([#735](https://github.com/op-rs/kona/pull/735)) +- *(preimage/common)* Migrate to `thiserror` ([#543](https://github.com/op-rs/kona/pull/543)) + +### Fixed + +- hashmap ([#732](https://github.com/op-rs/kona/pull/732)) +- *(workspace)* hoist and fix lints ([#577](https://github.com/op-rs/kona/pull/577)) +- *(preimage)* Improve error differentiation in preimage servers ([#535](https://github.com/op-rs/kona/pull/535)) + +### Other + +- re-org imports ([#711](https://github.com/op-rs/kona/pull/711)) +- *(preimage)* Test Coverage ([#634](https://github.com/op-rs/kona/pull/634)) +- doc logos ([#609](https://github.com/op-rs/kona/pull/609)) +- *(workspace)* Bump dependencies ([#550](https://github.com/op-rs/kona/pull/550)) +- *(workspace)* Allow stdlib in `cfg(test)` ([#548](https://github.com/op-rs/kona/pull/548)) + +## [0.0.3](https://github.com/op-rs/kona/compare/kona-preimage-v0.0.2...kona-preimage-v0.0.3) - 2024-09-04 + +### Added +- *(workspace)* Workspace Re-exports ([#468](https://github.com/op-rs/kona/pull/468)) +- *(client)* providers generic over oracles ([#336](https://github.com/op-rs/kona/pull/336)) + +### Fixed +- *(workspace)* Add Unused Dependency Lint ([#453](https://github.com/op-rs/kona/pull/453)) + +### Other +- *(workspace)* Update for `op-rs` org transfer ([#474](https://github.com/op-rs/kona/pull/474)) +- *(workspace)* Hoist Dependencies ([#466](https://github.com/op-rs/kona/pull/466)) +- *(common)* Remove need for cursors in `NativeIO` ([#416](https://github.com/op-rs/kona/pull/416)) +- *(preimage)* Remove dynamic dispatch ([#354](https://github.com/op-rs/kona/pull/354)) + +## [0.0.2](https://github.com/op-rs/kona/compare/kona-preimage-v0.0.1...kona-preimage-v0.0.2) - 2024-06-22 + +### Added +- *(preimage)* add serde feature flag to preimage crate for keys ([#271](https://github.com/op-rs/kona/pull/271)) +- *(client)* Derivation integration ([#257](https://github.com/op-rs/kona/pull/257)) +- *(ci)* Dependabot config ([#236](https://github.com/op-rs/kona/pull/236)) +- *(client)* `StatelessL2BlockExecutor` ([#210](https://github.com/op-rs/kona/pull/210)) +- *(client)* `BootInfo` ([#205](https://github.com/op-rs/kona/pull/205)) +- *(preimage)* Async client handles ([#200](https://github.com/op-rs/kona/pull/200)) +- *(host)* Add local key value store ([#189](https://github.com/op-rs/kona/pull/189)) +- *(host)* Host program scaffold ([#184](https://github.com/op-rs/kona/pull/184)) +- *(preimage)* Async server components ([#183](https://github.com/op-rs/kona/pull/183)) +- *(precompile)* Add `precompile` key type ([#179](https://github.com/op-rs/kona/pull/179)) +- *(preimage)* `OracleServer` + `HintReader` ([#96](https://github.com/op-rs/kona/pull/96)) +- *(common)* Move from `RegisterSize` to native ptr size type ([#95](https://github.com/op-rs/kona/pull/95)) +- *(workspace)* Add `rustfmt.toml` + +### Other +- *(workspace)* Move `alloy-primitives` to workspace dependencies ([#103](https://github.com/op-rs/kona/pull/103)) +- Make versions of packages independent ([#36](https://github.com/op-rs/kona/pull/36)) diff --git a/kona/crates/proof/preimage/Cargo.toml b/kona/crates/proof/preimage/Cargo.toml new file mode 100644 index 0000000000000..0752d0a7bf797 --- /dev/null +++ b/kona/crates/proof/preimage/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "kona-preimage" +description = "Bindings and types for interacting with the PreimageOracle ABI" +version = "0.3.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# General +tracing.workspace = true +thiserror.workspace = true +async-trait.workspace = true +alloy-primitives.workspace = true + +# `std` feature dependencies +async-channel = { workspace = true, optional = true } + +# `rkyv` feature dependencies +rkyv = { workspace = true, optional = true } + +# `serde` feature dependencies +serde = { workspace = true, optional = true, features = ["derive"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } + +[features] +default = [] +std = [ + "alloy-primitives/std", + "dep:async-channel", + "serde?/std", + "thiserror/std", + "tracing/std", +] +rkyv = [ "dep:rkyv" ] +serde = [ "alloy-primitives/serde", "dep:serde" ] diff --git a/kona/crates/proof/preimage/README.md b/kona/crates/proof/preimage/README.md new file mode 100644 index 0000000000000..1b2710ac4d865 --- /dev/null +++ b/kona/crates/proof/preimage/README.md @@ -0,0 +1,12 @@ +# `kona-preimage` + +CI +Kona Preimage ABI client +License +Codecov + +This crate offers a high-level API over the [`Preimage Oracle`][preimage-abi-spec]. It is `no_std` compatible to be used in +`client` programs, and the `host` handles are `async` colored to allow for the `host` programs to reach out to external +data sources to populate the `Preimage Oracle`. + +[preimage-abi-spec]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle diff --git a/kona/crates/proof/preimage/src/errors.rs b/kona/crates/proof/preimage/src/errors.rs new file mode 100644 index 0000000000000..16b5aae033e19 --- /dev/null +++ b/kona/crates/proof/preimage/src/errors.rs @@ -0,0 +1,53 @@ +//! Errors for the `kona-preimage` crate. + +use alloc::string::String; +use thiserror::Error; + +/// A [PreimageOracleError] is an enum that differentiates pipe-related errors from other errors +/// in the [PreimageOracleServer] and [HintReaderServer] implementations. +/// +/// [PreimageOracleServer]: crate::PreimageOracleServer +/// [HintReaderServer]: crate::HintReaderServer +#[derive(Error, Debug)] +pub enum PreimageOracleError { + /// The pipe has been broken. + #[error(transparent)] + IOError(#[from] ChannelError), + /// The preimage key is invalid. + #[error("Invalid preimage key.")] + InvalidPreimageKey, + /// Key not found. + #[error("Key not found.")] + KeyNotFound, + /// Timeout while waiting for preimage. + #[error("Timeout while waiting for preimage.")] + Timeout, + /// Buffer length mismatch. + #[error("Buffer length mismatch. Expected {0}, got {1}.")] + BufferLengthMismatch(usize, usize), + /// Failed to parse hint. + #[error("Failed to parse hint: {0}")] + HintParseFailed(String), + /// Other errors. + #[error("Error in preimage server: {0}")] + Other(String), +} + +/// A [Result] type for the [PreimageOracleError] enum. +pub type PreimageOracleResult = Result; + +/// A [ChannelError] is an enum that describes the error cases of a [Channel] trait implementation. +/// +/// [Channel]: crate::Channel +#[derive(Error, Debug)] +pub enum ChannelError { + /// The channel is closed. + #[error("Channel is closed.")] + Closed, + /// Unexpected EOF. + #[error("Unexpected EOF in channel read operation.")] + UnexpectedEOF, +} + +/// A [Result] type for the [ChannelError] enum. +pub type ChannelResult = Result; diff --git a/kona/crates/proof/preimage/src/hint.rs b/kona/crates/proof/preimage/src/hint.rs new file mode 100644 index 0000000000000..227d1dacdd7be --- /dev/null +++ b/kona/crates/proof/preimage/src/hint.rs @@ -0,0 +1,223 @@ +use crate::{ + Channel, HintReaderServer, + errors::{PreimageOracleError, PreimageOracleResult}, + traits::{HintRouter, HintWriterClient}, +}; +use alloc::{boxed::Box, format, string::String, vec}; +use async_trait::async_trait; + +/// A [HintWriter] is a high-level interface to the hint channel. It provides a way to write hints +/// to the host. +#[derive(Debug, Clone, Copy)] +pub struct HintWriter { + channel: C, +} + +impl HintWriter { + /// Create a new [HintWriter] from a [Channel]. + pub const fn new(channel: C) -> Self { + Self { channel } + } +} + +#[async_trait] +impl HintWriterClient for HintWriter +where + C: Channel + Send + Sync, +{ + /// Write a hint to the host. This will overwrite any existing hint in the channel, and block + /// until all data has been written. + async fn write(&self, hint: &str) -> PreimageOracleResult<()> { + trace!(target: "hint_writer", "Writing hint \"{hint}\""); + + // Form the hint into a byte buffer. The format is a 4-byte big-endian length prefix + // followed by the hint string. + self.channel.write(u32::to_be_bytes(hint.len() as u32).as_ref()).await?; + self.channel.write(hint.as_bytes()).await?; + + trace!(target: "hint_writer", "Successfully wrote hint"); + + // Read the hint acknowledgement from the host. + let mut hint_ack = [0u8; 1]; + self.channel.read_exact(&mut hint_ack).await?; + + trace!(target: "hint_writer", "Received hint acknowledgement"); + + Ok(()) + } +} + +/// A [HintReader] is a router for hints sent by the [HintWriter] from the client program. It +/// provides a way for the host to prepare preimages for reading. +#[derive(Debug, Clone, Copy)] +pub struct HintReader { + channel: C, +} + +impl HintReader +where + C: Channel, +{ + /// Create a new [HintReader] from a [Channel]. + pub const fn new(channel: C) -> Self { + Self { channel } + } +} + +#[async_trait] +impl HintReaderServer for HintReader +where + C: Channel + Send + Sync, +{ + async fn next_hint(&self, hint_router: &R) -> PreimageOracleResult<()> + where + R: HintRouter + Send + Sync, + { + // Read the length of the raw hint payload. + let mut len_buf = [0u8; 4]; + self.channel.read_exact(&mut len_buf).await?; + let len = u32::from_be_bytes(len_buf); + + // Read the raw hint payload. + let mut raw_payload = vec![0u8; len as usize]; + self.channel.read_exact(raw_payload.as_mut_slice()).await?; + let payload = match String::from_utf8(raw_payload) { + Ok(p) => p, + Err(e) => { + // Write back on error to prevent blocking the client. + self.channel.write(&[0x00]).await?; + + return Err(PreimageOracleError::Other(format!( + "Failed to decode hint payload: {e}" + ))); + } + }; + + trace!(target: "hint_reader", "Successfully read hint: \"{payload}\""); + + // Route the hint + if let Err(e) = hint_router.route_hint(payload).await { + // Write back on error to prevent blocking the client. + self.channel.write(&[0x00]).await?; + + error!(target: "hint_reader", "Failed to route hint: {e}"); + return Err(e); + } + + // Write back an acknowledgement to the client to unblock their process. + self.channel.write(&[0x00]).await?; + + trace!(target: "hint_reader", "Successfully routed and acknowledged hint"); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::native_channel::BidirectionalChannel; + use alloc::{sync::Arc, vec::Vec}; + use tokio::sync::Mutex; + + struct TestRouter { + incoming_hints: Arc>>, + } + + #[async_trait] + impl HintRouter for TestRouter { + async fn route_hint(&self, hint: String) -> PreimageOracleResult<()> { + self.incoming_hints.lock().await.push(hint); + Ok(()) + } + } + + struct TestFailRouter; + + #[async_trait] + impl HintRouter for TestFailRouter { + async fn route_hint(&self, _hint: String) -> PreimageOracleResult<()> { + Err(PreimageOracleError::KeyNotFound) + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_unblock_on_bad_utf8() { + let mock_data = [0xf0, 0x90, 0x28, 0xbc]; + + let hint_channel = BidirectionalChannel::new().unwrap(); + + let client = tokio::task::spawn(async move { + let hint_writer = HintWriter::new(hint_channel.client); + + #[allow(invalid_from_utf8_unchecked)] + hint_writer.write(unsafe { alloc::str::from_utf8_unchecked(&mock_data) }).await + }); + let host = tokio::task::spawn(async move { + let router = TestRouter { incoming_hints: Default::default() }; + + let hint_reader = HintReader::new(hint_channel.host); + hint_reader.next_hint(&router).await + }); + + let (c, h) = tokio::join!(client, host); + c.unwrap().unwrap(); + assert!(h.unwrap().is_err_and(|e| { + let PreimageOracleError::Other(e) = e else { + return false; + }; + e.contains("Failed to decode hint payload") + })); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_unblock_on_fetch_failure() { + const MOCK_DATA: &str = "test-hint 0xfacade"; + + let hint_channel = BidirectionalChannel::new().unwrap(); + + let client = tokio::task::spawn(async move { + let hint_writer = HintWriter::new(hint_channel.client); + + hint_writer.write(MOCK_DATA).await + }); + let host = tokio::task::spawn(async move { + let hint_reader = HintReader::new(hint_channel.host); + hint_reader.next_hint(&TestFailRouter).await + }); + + let (c, h) = tokio::join!(client, host); + c.unwrap().unwrap(); + assert!(h.unwrap().is_err_and(|e| matches!(e, PreimageOracleError::KeyNotFound))); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_hint_client_and_host() { + const MOCK_DATA: &str = "test-hint 0xfacade"; + + let incoming_hints = Arc::new(Mutex::new(Vec::new())); + let hint_channel = BidirectionalChannel::new().unwrap(); + + let client = tokio::task::spawn(async move { + let hint_writer = HintWriter::new(hint_channel.client); + + hint_writer.write(MOCK_DATA).await + }); + let host = tokio::task::spawn({ + let incoming_hints_ref = Arc::clone(&incoming_hints); + async move { + let router = TestRouter { incoming_hints: incoming_hints_ref }; + + let hint_reader = HintReader::new(hint_channel.host); + hint_reader.next_hint(&router).await.unwrap(); + } + }); + + let _ = tokio::join!(client, host); + let mut hints = incoming_hints.lock().await; + + assert_eq!(hints.len(), 1); + let h = hints.remove(0); + assert_eq!(h, MOCK_DATA); + } +} diff --git a/kona/crates/proof/preimage/src/key.rs b/kona/crates/proof/preimage/src/key.rs new file mode 100644 index 0000000000000..0ef1a383ff258 --- /dev/null +++ b/kona/crates/proof/preimage/src/key.rs @@ -0,0 +1,233 @@ +//! Contains the [PreimageKey] type, which is used to identify preimages that may be fetched from +//! the preimage oracle. + +use alloy_primitives::{B256, Keccak256, U256}; +#[cfg(feature = "rkyv")] +use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; +#[cfg(feature = "serde")] +use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize}; + +use crate::errors::PreimageOracleError; + +/// +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[repr(u8)] +#[cfg_attr( + feature = "rkyv", + derive(Archive, RkyvSerialize, RkyvDeserialize), + rkyv(derive(Eq, PartialEq, Ord, PartialOrd, Hash)) +)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] +pub enum PreimageKeyType { + /// Local key types are local to a given instance of a fault-proof and context dependent. + /// Commonly these local keys are mapped to bootstrap data for the fault proof program. + Local = 1, + /// Keccak256 key types are global and context independent. Preimages are mapped from the + /// low-order 31 bytes of the preimage's `keccak256` digest to the preimage itself. + #[default] + Keccak256 = 2, + /// GlobalGeneric key types are reserved for future use. + GlobalGeneric = 3, + /// Sha256 key types are global and context independent. Preimages are mapped from the + /// low-order 31 bytes of the preimage's `sha256` digest to the preimage itself. + Sha256 = 4, + /// Blob key types are global and context independent. Blob keys are constructed as + /// `keccak256(commitment ++ z)`, and then the high-order byte of the digest is set to the + /// type byte. + Blob = 5, + /// Precompile key types are global and context independent. Precompile keys are constructed as + /// `keccak256(precompile_addr ++ input)`, and then the high-order byte of the digest is set to + /// the type byte. + Precompile = 6, +} + +impl TryFrom for PreimageKeyType { + type Error = PreimageOracleError; + + fn try_from(value: u8) -> Result { + let key_type = match value { + 1 => Self::Local, + 2 => Self::Keccak256, + 3 => Self::GlobalGeneric, + 4 => Self::Sha256, + 5 => Self::Blob, + 6 => Self::Precompile, + _ => return Err(PreimageOracleError::InvalidPreimageKey), + }; + Ok(key_type) + } +} + +/// A preimage key is a 32-byte value that identifies a preimage that may be fetched from the +/// oracle. +/// +/// **Layout**: +/// | Bits | Description | +/// |---------|-------------| +/// | [0, 1) | Type byte | +/// | [1, 32) | Data | +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr( + feature = "rkyv", + derive(Archive, RkyvSerialize, RkyvDeserialize), + rkyv(derive(Eq, PartialEq, Ord, PartialOrd, Hash)) +)] +#[cfg_attr(feature = "serde", derive(SerdeSerialize, SerdeDeserialize))] +pub struct PreimageKey { + data: [u8; 31], + key_type: PreimageKeyType, +} + +impl PreimageKey { + /// Creates a new [PreimageKey] from a 32-byte value and a [PreimageKeyType]. The 32-byte value + /// will be truncated to 31 bytes by taking the low-order 31 bytes. + pub fn new(key: [u8; 32], key_type: PreimageKeyType) -> Self { + let mut data = [0u8; 31]; + data.copy_from_slice(&key[1..]); + Self { data, key_type } + } + + /// Creates a new local [PreimageKey] from a 64-bit local identifier. The local identifier will + /// be written into the low-order 8 bytes of the big-endian 31-byte data field. + pub fn new_local(local_ident: u64) -> Self { + let mut data = [0u8; 31]; + data[23..].copy_from_slice(&local_ident.to_be_bytes()); + Self { data, key_type: PreimageKeyType::Local } + } + + /// Creates a new keccak256 [PreimageKey] from a 32-byte keccak256 digest. The digest will be + /// truncated to 31 bytes by taking the low-order 31 bytes. + pub fn new_keccak256(digest: [u8; 32]) -> Self { + Self::new(digest, PreimageKeyType::Keccak256) + } + + /// Creates a new precompile [PreimageKey] from a precompile address and input. The key will be + /// constructed as `keccak256(precompile_addr ++ input)`, and then the high-order byte of the + /// digest will be set to the type byte. + pub fn new_precompile(precompile_addr: [u8; 20], input: &[u8]) -> Self { + let mut data = [0u8; 31]; + + let mut hasher = Keccak256::new(); + hasher.update(precompile_addr); + hasher.update(input); + + data.copy_from_slice(&hasher.finalize()[1..]); + Self { data, key_type: PreimageKeyType::Precompile } + } + + /// Returns the [PreimageKeyType] for the [PreimageKey]. + pub const fn key_type(&self) -> PreimageKeyType { + self.key_type + } + + /// Returns the value of the [PreimageKey] as a [U256]. + pub const fn key_value(&self) -> U256 { + U256::from_be_slice(self.data.as_slice()) + } +} + +impl From for [u8; 32] { + fn from(key: PreimageKey) -> Self { + let mut rendered_key = [0u8; 32]; + rendered_key[0] = key.key_type as u8; + rendered_key[1..].copy_from_slice(&key.data); + rendered_key + } +} + +impl From for B256 { + fn from(value: PreimageKey) -> Self { + let raw: [u8; 32] = value.into(); + Self::from(raw) + } +} + +impl TryFrom<[u8; 32]> for PreimageKey { + type Error = PreimageOracleError; + + fn try_from(value: [u8; 32]) -> Result { + let key_type = PreimageKeyType::try_from(value[0])?; + Ok(Self::new(value, key_type)) + } +} + +impl core::fmt::Display for PreimageKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let raw: [u8; 32] = (*self).into(); + write!(f, "{}", B256::from(raw)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_preimage_key_from_u8() { + assert_eq!(PreimageKeyType::try_from(1).unwrap(), PreimageKeyType::Local); + assert_eq!(PreimageKeyType::try_from(2).unwrap(), PreimageKeyType::Keccak256); + assert_eq!(PreimageKeyType::try_from(3).unwrap(), PreimageKeyType::GlobalGeneric); + assert_eq!(PreimageKeyType::try_from(4).unwrap(), PreimageKeyType::Sha256); + assert_eq!(PreimageKeyType::try_from(5).unwrap(), PreimageKeyType::Blob); + assert_eq!(PreimageKeyType::try_from(6).unwrap(), PreimageKeyType::Precompile); + assert!(PreimageKeyType::try_from(0).is_err()); + assert!(PreimageKeyType::try_from(7).is_err()); + } + + #[test] + fn test_preimage_key_new_local() { + let key = PreimageKey::new_local(0xFFu64); + assert_eq!(key.key_type(), PreimageKeyType::Local); + assert_eq!(key.key_value(), U256::from(0xFFu64)); + } + + #[test] + fn test_preimage_key_value() { + let key = PreimageKey::new([0xFFu8; 32], PreimageKeyType::Local); + assert_eq!( + key.key_value(), + alloy_primitives::uint!( + 452312848583266388373324160190187140051835877600158453279131187530910662655_U256 + ) + ); + } + + #[test] + fn test_preimage_key_roundtrip_b256() { + let key = PreimageKey::new([0xFFu8; 32], PreimageKeyType::Local); + let b256: B256 = key.into(); + let key2 = PreimageKey::try_from(<[u8; 32]>::from(b256)).unwrap(); + assert_eq!(key, key2); + } + + #[test] + fn test_preimage_key_display() { + let key = PreimageKey::new([0xFFu8; 32], PreimageKeyType::Local); + assert_eq!( + key.to_string(), + "0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ); + } + + #[test] + fn test_preimage_keys() { + let types = [ + PreimageKeyType::Local, + PreimageKeyType::Keccak256, + PreimageKeyType::GlobalGeneric, + PreimageKeyType::Sha256, + PreimageKeyType::Blob, + PreimageKeyType::Precompile, + ]; + + for key_type in types { + let key = PreimageKey::new([0xFFu8; 32], key_type); + assert_eq!(key.key_type(), key_type); + + let mut rendered_key = [0xFFu8; 32]; + rendered_key[0] = key_type as u8; + let actual: [u8; 32] = key.into(); + assert_eq!(actual, rendered_key); + } + } +} diff --git a/kona/crates/proof/preimage/src/lib.rs b/kona/crates/proof/preimage/src/lib.rs new file mode 100644 index 0000000000000..f9327992ec2a2 --- /dev/null +++ b/kona/crates/proof/preimage/src/lib.rs @@ -0,0 +1,35 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +#[macro_use] +extern crate tracing; + +pub mod errors; + +mod key; +pub use key::{PreimageKey, PreimageKeyType}; + +mod oracle; +pub use oracle::{OracleReader, OracleServer}; + +mod hint; +pub use hint::{HintReader, HintWriter}; + +mod traits; +pub use traits::{ + Channel, CommsClient, HintReaderServer, HintRouter, HintWriterClient, PreimageFetcher, + PreimageOracleClient, PreimageOracleServer, PreimageServerBackend, +}; + +#[cfg(feature = "std")] +mod native_channel; +#[cfg(feature = "std")] +pub use native_channel::{BidirectionalChannel, NativeChannel}; diff --git a/kona/crates/proof/preimage/src/native_channel.rs b/kona/crates/proof/preimage/src/native_channel.rs new file mode 100644 index 0000000000000..db02e513e69aa --- /dev/null +++ b/kona/crates/proof/preimage/src/native_channel.rs @@ -0,0 +1,62 @@ +//! Native implementation of the [Channel] trait, backed by [async_channel]'s unbounded +//! channel primitives. + +use crate::{ + Channel, + errors::{ChannelError, ChannelResult}, +}; +use async_channel::{Receiver, Sender, unbounded}; +use async_trait::async_trait; +use std::io::Result; + +/// A bidirectional channel, allowing for synchronized communication between two parties. +#[derive(Debug, Clone)] +pub struct BidirectionalChannel { + /// The client handle of the channel. + pub client: NativeChannel, + /// The host handle of the channel. + pub host: NativeChannel, +} + +impl BidirectionalChannel { + /// Creates a [BidirectionalChannel] instance. + pub fn new() -> Result { + let (bw, ar) = unbounded(); + let (aw, br) = unbounded(); + + Ok(Self { + client: NativeChannel { read: ar, write: aw }, + host: NativeChannel { read: br, write: bw }, + }) + } +} + +/// A channel with a receiver and sender. +#[derive(Debug, Clone)] +pub struct NativeChannel { + /// The receiver of the channel. + pub(crate) read: Receiver>, + /// The sender of the channel. + pub(crate) write: Sender>, +} + +#[async_trait] +impl Channel for NativeChannel { + async fn read(&self, buf: &mut [u8]) -> ChannelResult { + let data = self.read.recv().await.map_err(|_| ChannelError::Closed)?; + let len = data.len().min(buf.len()); + buf[..len].copy_from_slice(&data[..len]); + Ok(len) + } + + async fn read_exact(&self, buf: &mut [u8]) -> ChannelResult { + let data = self.read.recv().await.map_err(|_| ChannelError::Closed)?; + buf[..].copy_from_slice(&data[..]); + Ok(buf.len()) + } + + async fn write(&self, buf: &[u8]) -> ChannelResult { + self.write.send(buf.to_vec()).await.map_err(|_| ChannelError::Closed)?; + Ok(buf.len()) + } +} diff --git a/kona/crates/proof/preimage/src/oracle.rs b/kona/crates/proof/preimage/src/oracle.rs new file mode 100644 index 0000000000000..076313049e45f --- /dev/null +++ b/kona/crates/proof/preimage/src/oracle.rs @@ -0,0 +1,248 @@ +use crate::{ + PreimageKey, PreimageOracleClient, PreimageOracleServer, + errors::{PreimageOracleError, PreimageOracleResult}, + traits::{Channel, PreimageFetcher}, +}; +use alloc::{boxed::Box, vec::Vec}; + +/// An [OracleReader] is a high-level interface to the preimage oracle channel. +#[derive(Debug, Clone, Copy)] +pub struct OracleReader { + channel: C, +} + +impl OracleReader +where + C: Channel, +{ + /// Create a new [OracleReader] from a [Channel]. + pub const fn new(channel: C) -> Self { + Self { channel } + } + + /// Set the preimage key for the global oracle reader. This will overwrite any existing key, and + /// block until the host has prepared the preimage and responded with the length of the + /// preimage. + async fn write_key(&self, key: PreimageKey) -> PreimageOracleResult { + // Write the key to the host so that it can prepare the preimage. + let key_bytes: [u8; 32] = key.into(); + self.channel.write(&key_bytes).await?; + + // Read the length prefix and reset the cursor. + let mut length_buffer = [0u8; 8]; + self.channel.read_exact(&mut length_buffer).await?; + Ok(u64::from_be_bytes(length_buffer) as usize) + } +} + +#[async_trait::async_trait] +impl PreimageOracleClient for OracleReader +where + C: Channel + Send + Sync, +{ + /// Get the data corresponding to the currently set key from the host. Return the data in a new + /// heap allocated `Vec` + async fn get(&self, key: PreimageKey) -> PreimageOracleResult> { + trace!(target: "oracle_client", "Requesting data from preimage oracle. Key {key}"); + + let length = self.write_key(key).await?; + + if length == 0 { + return Ok(Default::default()); + } + + let mut data_buffer = alloc::vec![0; length]; + + trace!(target: "oracle_client", "Reading data from preimage oracle. Key {key}"); + + // Grab a read lock on the preimage channel to read the data. + self.channel.read_exact(&mut data_buffer).await?; + + trace!(target: "oracle_client", "Successfully read data from preimage oracle. Key: {key}"); + + Ok(data_buffer) + } + + /// Get the data corresponding to the currently set key from the host. Write the data into the + /// provided buffer + async fn get_exact(&self, key: PreimageKey, buf: &mut [u8]) -> PreimageOracleResult<()> { + trace!(target: "oracle_client", "Requesting data from preimage oracle. Key {key}"); + + // Write the key to the host and read the length of the preimage. + let length = self.write_key(key).await?; + + trace!(target: "oracle_client", "Reading data from preimage oracle. Key {key}"); + + // Ensure the buffer is the correct size. + if buf.len() != length { + return Err(PreimageOracleError::BufferLengthMismatch(length, buf.len())); + } + + if length == 0 { + return Ok(()); + } + + self.channel.read_exact(buf).await?; + + trace!(target: "oracle_client", "Successfully read data from preimage oracle. Key: {key}"); + + Ok(()) + } +} + +/// An [OracleServer] is a router for the host to serve data back to the client [OracleReader]. +#[derive(Debug, Clone, Copy)] +pub struct OracleServer { + channel: C, +} + +impl OracleServer +where + C: Channel, +{ + /// Create a new [OracleServer] from a [Channel]. + pub const fn new(channel: C) -> Self { + Self { channel } + } +} + +#[async_trait::async_trait] +impl PreimageOracleServer for OracleServer +where + C: Channel + Send + Sync, +{ + async fn next_preimage_request(&self, fetcher: &F) -> Result<(), PreimageOracleError> + where + F: PreimageFetcher + Send + Sync, + { + // Read the preimage request from the client, and throw early if there isn't any. + let mut buf = [0u8; 32]; + self.channel.read_exact(&mut buf).await?; + let preimage_key = PreimageKey::try_from(buf)?; + + trace!(target: "oracle_server", "Fetching preimage for key {preimage_key}"); + + // Fetch the preimage value from the preimage getter. + let value = fetcher.get_preimage(preimage_key).await?; + + // Write the length as a big-endian u64 followed by the data. + self.channel.write(value.len().to_be_bytes().as_ref()).await?; + self.channel.write(value.as_ref()).await?; + + trace!(target: "oracle_server", "Successfully wrote preimage data for key {preimage_key}"); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{PreimageKeyType, native_channel::BidirectionalChannel}; + use alloc::sync::Arc; + use alloy_primitives::keccak256; + use std::collections::HashMap; + use tokio::sync::Mutex; + + struct TestFetcher { + preimages: Arc>>>, + } + + #[async_trait::async_trait] + impl PreimageFetcher for TestFetcher { + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { + let read_lock = self.preimages.lock().await; + read_lock.get(&key).cloned().ok_or(PreimageOracleError::KeyNotFound) + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_oracle_reader_get_exact() { + const MOCK_DATA_A: &[u8] = b"1234567890"; + const MOCK_DATA_B: &[u8] = b"FACADE"; + let key_a: PreimageKey = + PreimageKey::new(*keccak256(MOCK_DATA_A), PreimageKeyType::Keccak256); + let key_b: PreimageKey = + PreimageKey::new(*keccak256(MOCK_DATA_B), PreimageKeyType::Keccak256); + + let preimages = { + let mut preimages = HashMap::default(); + preimages.insert(key_a, MOCK_DATA_A.to_vec()); + preimages.insert(key_b, MOCK_DATA_B.to_vec()); + Arc::new(Mutex::new(preimages)) + }; + + let preimage_channel = BidirectionalChannel::new().unwrap(); + + let client = tokio::task::spawn(async move { + let oracle_reader = OracleReader::new(preimage_channel.client); + let mut contents_a = [0u8; 10]; + let mut contents_b = [0u8; 6]; + oracle_reader.get_exact(key_a, &mut contents_a).await.unwrap(); + oracle_reader.get_exact(key_b, &mut contents_b).await.unwrap(); + + (contents_a, contents_b) + }); + tokio::task::spawn(async move { + let oracle_server = OracleServer::new(preimage_channel.host); + let test_fetcher = TestFetcher { preimages: Arc::clone(&preimages) }; + + loop { + match oracle_server.next_preimage_request(&test_fetcher).await { + Err(PreimageOracleError::IOError(_)) => break, + Err(e) => panic!("Unexpected error: {e:?}"), + Ok(_) => {} + } + } + }); + + let (c,) = tokio::join!(client); + let (contents_a, contents_b) = c.unwrap(); + assert_eq!(contents_a, MOCK_DATA_A); + assert_eq!(contents_b, MOCK_DATA_B); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_oracle_client_and_host() { + const MOCK_DATA_A: &[u8] = b"1234567890"; + const MOCK_DATA_B: &[u8] = b"FACADE"; + let key_a: PreimageKey = + PreimageKey::new(*keccak256(MOCK_DATA_A), PreimageKeyType::Keccak256); + let key_b: PreimageKey = + PreimageKey::new(*keccak256(MOCK_DATA_B), PreimageKeyType::Keccak256); + + let preimages = { + let mut preimages = HashMap::default(); + preimages.insert(key_a, MOCK_DATA_A.to_vec()); + preimages.insert(key_b, MOCK_DATA_B.to_vec()); + Arc::new(Mutex::new(preimages)) + }; + + let preimage_channel = BidirectionalChannel::new().unwrap(); + + let client = tokio::task::spawn(async move { + let oracle_reader = OracleReader::new(preimage_channel.client); + let contents_a = oracle_reader.get(key_a).await.unwrap(); + let contents_b = oracle_reader.get(key_b).await.unwrap(); + + (contents_a, contents_b) + }); + tokio::task::spawn(async move { + let oracle_server = OracleServer::new(preimage_channel.host); + let test_fetcher = TestFetcher { preimages: Arc::clone(&preimages) }; + + loop { + match oracle_server.next_preimage_request(&test_fetcher).await { + Err(PreimageOracleError::IOError(_)) => break, + Err(e) => panic!("Unexpected error: {e:?}"), + Ok(_) => {} + } + } + }); + + let (c,) = tokio::join!(client); + let (contents_a, contents_b) = c.unwrap(); + assert_eq!(contents_a, MOCK_DATA_A); + assert_eq!(contents_b, MOCK_DATA_B); + } +} diff --git a/kona/crates/proof/preimage/src/traits.rs b/kona/crates/proof/preimage/src/traits.rs new file mode 100644 index 0000000000000..affa8fe974b52 --- /dev/null +++ b/kona/crates/proof/preimage/src/traits.rs @@ -0,0 +1,144 @@ +use crate::{ + PreimageKey, + errors::{ChannelResult, PreimageOracleResult}, +}; +use alloc::{boxed::Box, string::String, vec::Vec}; +use async_trait::async_trait; + +/// A [PreimageOracleClient] is a high-level interface to read data from the host, keyed by a +/// [PreimageKey]. +#[async_trait] +pub trait PreimageOracleClient { + /// Get the data corresponding to the currently set key from the host. Return the data in a new + /// heap allocated `Vec` + /// + /// # Returns + /// - `Ok(Vec)` if the data was successfully fetched from the host. + /// - `Err(_)` if the data could not be fetched from the host. + async fn get(&self, key: PreimageKey) -> PreimageOracleResult>; + + /// Get the data corresponding to the currently set key from the host. Writes the data into the + /// provided buffer. + /// + /// # Returns + /// - `Ok(())` if the data was successfully written into the buffer. + /// - `Err(_)` if the data could not be written into the buffer. + async fn get_exact(&self, key: PreimageKey, buf: &mut [u8]) -> PreimageOracleResult<()>; +} + +/// A [HintWriterClient] is a high-level interface to the hint pipe. It provides a way to write +/// hints to the host. +#[async_trait] +pub trait HintWriterClient { + /// Write a hint to the host. This will overwrite any existing hint in the pipe, and block until + /// all data has been written. + /// + /// # Returns + /// - `Ok(())` if the hint was successfully written to the host. + /// - `Err(_)` if the hint could not be written to the host. + async fn write(&self, hint: &str) -> PreimageOracleResult<()>; +} + +/// A [CommsClient] is a trait that combines the [PreimageOracleClient] and [HintWriterClient] +pub trait CommsClient: PreimageOracleClient + Clone + HintWriterClient {} + +// Implement the super trait for any type that satisfies the bounds +impl CommsClient for T {} + +/// A [PreimageOracleServer] is a high-level interface to accept read requests from the client and +/// write the preimage data to the client pipe. +#[async_trait] +pub trait PreimageOracleServer { + /// Get the next preimage request and return the response to the client. + /// + /// # Returns + /// - `Ok(())` if the data was successfully written into the client pipe. + /// - `Err(_)` if the data could not be written to the client. + async fn next_preimage_request(&self, get_preimage: &F) -> PreimageOracleResult<()> + where + F: PreimageFetcher + Send + Sync; +} + +/// A [HintReaderServer] is a high-level interface to read preimage hints from the +/// [HintWriterClient] and prepare them for consumption by the client program. +#[async_trait] +pub trait HintReaderServer { + /// Get the next hint request and return the acknowledgement to the client. + /// + /// # Returns + /// - `Ok(())` if the hint was received and the client was notified of the host's + /// acknowledgement. + /// - `Err(_)` if the hint was not received correctly. + async fn next_hint(&self, route_hint: &R) -> PreimageOracleResult<()> + where + R: HintRouter + Send + Sync; +} + +/// A [HintRouter] is a high-level interface to route hints to the appropriate handler. +#[async_trait] +pub trait HintRouter { + /// Routes a hint to the appropriate handler. + /// + /// # Arguments + /// - `hint`: The hint to route. + /// + /// # Returns + /// - `Ok(())` if the hint was successfully routed. + /// - `Err(_)` if the hint could not be routed. + async fn route_hint(&self, hint: String) -> PreimageOracleResult<()>; +} + +/// A [PreimageFetcher] is a high-level interface to fetch preimages during preimage requests. +#[async_trait] +pub trait PreimageFetcher { + /// Get the preimage corresponding to the given key. + /// + /// # Arguments + /// - `key`: The key to fetch the preimage for. + /// + /// # Returns + /// - `Ok(Vec)` if the preimage was successfully fetched. + /// - `Err(_)` if the preimage could not be fetched. + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult>; +} + +/// A [PreimageServerBackend] is a trait that combines the [PreimageFetcher] and [HintRouter] +/// traits. +pub trait PreimageServerBackend: PreimageFetcher + HintRouter {} + +// Implement the super trait for any type that satisfies the bounds +impl PreimageServerBackend for T {} + +/// A [Channel] is a high-level interface to read and write data to a counterparty. +#[async_trait] +pub trait Channel { + /// Asynchronously read data from the channel into the provided buffer. + /// + /// # Arguments + /// - `buf`: The buffer to read data into. + /// + /// # Returns + /// - `Ok(usize)`: The number of bytes read. + /// - `Err(_)` if the data could not be read. + async fn read(&self, buf: &mut [u8]) -> ChannelResult; + + /// Asynchronously read exactly `buf.len()` bytes into `buf` from the channel. + /// + /// # Arguments + /// - `buf`: The buffer to read data into. + /// + /// # Returns + /// - `Ok(())` if the data was successfully read. + /// - `Err(_)` if the data could not be read. + async fn read_exact(&self, buf: &mut [u8]) -> ChannelResult; + + /// Asynchronously write the provided buffer to the channel. + /// + /// # Arguments + /// - `buf`: The buffer to write to the host. + /// + /// # Returns + /// - `Ok(usize)`: The number of bytes written. + /// - `Err(_)` if the data could not be written. + async fn write(&self, buf: &[u8]) -> ChannelResult; +} diff --git a/kona/crates/proof/proof-interop/CHANGELOG.md b/kona/crates/proof/proof-interop/CHANGELOG.md new file mode 100644 index 0000000000000..15c0ae8204fec --- /dev/null +++ b/kona/crates/proof/proof-interop/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.1](https://github.com/op-rs/kona/compare/kona-proof-interop-v0.1.0...kona-proof-interop-v0.1.1) - 2025-01-16 + +### Other + +- Update Maili Deps (#908) diff --git a/kona/crates/proof/proof-interop/Cargo.toml b/kona/crates/proof/proof-interop/Cargo.toml new file mode 100644 index 0000000000000..2d7fdf808d54d --- /dev/null +++ b/kona/crates/proof/proof-interop/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "kona-proof-interop" +description = "OP Stack Proof SDK with Interop support" +version = "0.2.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-preimage.workspace = true +kona-interop = { workspace = true, features = ["serde"] } +kona-proof.workspace = true +kona-mpt.workspace = true +kona-executor.workspace = true +kona-registry.workspace = true +kona-genesis = { workspace = true, features = ["serde"] } +kona-protocol.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-primitives.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-evm = { workspace = true, features = ["op"] } + +# OP Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine.workspace = true +alloy-op-evm.workspace = true + +# revm +revm.workspace = true +op-revm.workspace = true + +# General +serde.workspace = true +tracing.workspace = true +serde_json.workspace = true +async-trait.workspace = true +spin.workspace = true +thiserror.workspace = true + +# Arbitrary +arbitrary = { version = "1.4", features = ["derive"], optional = true } + +[dev-dependencies] +alloy-primitives = { workspace = true, features = ["rlp", "arbitrary"] } +kona-interop = { workspace = true, features = ["arbitrary"] } +arbitrary = { version = "1.4", features = ["derive"] } +rand.workspace = true + +[features] +arbitrary = [ + "alloy-consensus/arbitrary", + "alloy-eips/arbitrary", + "alloy-primitives/arbitrary", + "alloy-rpc-types-engine/arbitrary", + "dep:arbitrary", + "kona-genesis/arbitrary", + "kona-interop/arbitrary", + "kona-protocol/arbitrary", + "op-alloy-consensus/arbitrary", + "op-alloy-rpc-types-engine/arbitrary", + "revm/arbitrary", +] diff --git a/kona/crates/proof/proof-interop/README.md b/kona/crates/proof/proof-interop/README.md new file mode 100644 index 0000000000000..4933aabbc3568 --- /dev/null +++ b/kona/crates/proof/proof-interop/README.md @@ -0,0 +1,8 @@ +# `kona-proof-interop` + +CI +Kona Proof SDK +License +Codecov + +`kona-proof-interop` is an OP Stack state transition proof SDK, with interop support, built on top of [`kona-proof`](../proof/) diff --git a/kona/crates/proof/proof-interop/src/boot.rs b/kona/crates/proof/proof-interop/src/boot.rs new file mode 100644 index 0000000000000..8cd244a7c8101 --- /dev/null +++ b/kona/crates/proof/proof-interop/src/boot.rs @@ -0,0 +1,240 @@ +//! This module contains the prologue phase of the client program, pulling in the boot information +//! through the `PreimageOracle` ABI as local keys. + +use crate::{HintType, INVALID_TRANSITION, INVALID_TRANSITION_HASH, PreState}; +use alloc::{string::ToString, vec::Vec}; +use alloy_primitives::{B256, Bytes, U256}; +use alloy_rlp::Decodable; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_preimage::{ + CommsClient, HintWriterClient, PreimageKey, PreimageKeyType, PreimageOracleClient, + errors::PreimageOracleError, +}; +use kona_proof::errors::OracleProviderError; +use kona_registry::{HashMap, L1_CONFIGS, ROLLUP_CONFIGS}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use tracing::warn; + +/// The local key ident for the L1 head hash. +pub const L1_HEAD_KEY: U256 = U256::from_be_slice(&[1]); + +/// The local key ident for the agreed upon L2 pre-state claim. +pub const L2_AGREED_PRE_STATE_KEY: U256 = U256::from_be_slice(&[2]); + +/// The local key ident for the L2 post-state claim. +pub const L2_CLAIMED_POST_STATE_KEY: U256 = U256::from_be_slice(&[3]); + +/// The local key ident for the L2 claim timestamp. +pub const L2_CLAIMED_TIMESTAMP_KEY: U256 = U256::from_be_slice(&[4]); + +/// The local key ident for the L2 rollup config. +pub const L2_ROLLUP_CONFIG_KEY: U256 = U256::from_be_slice(&[6]); + +/// The local key ident for the l1 config. +pub const L1_CONFIG_KEY: U256 = U256::from_be_slice(&[7]); + +/// The boot information for the interop client program. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BootInfo { + /// The L1 head hash containing the safe L2 chain data that may reproduce the post-state claim. + pub l1_head: B256, + /// The agreed upon superchain pre-state commitment. + pub agreed_pre_state_commitment: B256, + /// The agreed upon superchain pre-state. + pub agreed_pre_state: PreState, + /// The claimed (disputed) superchain post-state commitment. + pub claimed_post_state: B256, + /// The L2 claim timestamp. + pub claimed_l2_timestamp: u64, + /// The rollup config for the L2 chain. + pub rollup_configs: HashMap, + /// The L1 config for the L2 chain. + pub l1_config: L1ChainConfig, +} + +impl BootInfo { + /// Load the boot information from the preimage oracle. + /// + /// ## Takes + /// - `oracle`: The preimage oracle reader. + /// + /// ## Returns + /// - `Ok(BootInfo)`: The boot information. + /// - `Err(_)`: Failed to load the boot information. + pub async fn load(oracle: &O) -> Result + where + O: PreimageOracleClient + HintWriterClient + Clone + Send, + { + let mut l1_head: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L1_HEAD_KEY.to()), l1_head.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_pre: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_AGREED_PRE_STATE_KEY.to()), l2_pre.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_post: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_CLAIMED_POST_STATE_KEY.to()), l2_post.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let l2_claim_block = u64::from_be_bytes( + oracle + .get(PreimageKey::new_local(L2_CLAIMED_TIMESTAMP_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)? + .as_slice() + .try_into() + .map_err(OracleProviderError::SliceConversion)?, + ); + + let raw_pre_state = read_raw_pre_state(oracle, l2_pre).await?; + if raw_pre_state == INVALID_TRANSITION { + warn!( + target: "boot_loader", + "Invalid pre-state, short-circuiting to check post-state claim." + ); + + if l2_post == INVALID_TRANSITION_HASH { + return Err(BootstrapError::InvalidToInvalid); + } else { + return Err(BootstrapError::InvalidPostState(l2_post)); + } + } + + let agreed_pre_state = + PreState::decode(&mut raw_pre_state.as_ref()).map_err(OracleProviderError::Rlp)?; + + let chain_ids: Vec<_> = match agreed_pre_state { + PreState::SuperRoot(ref super_root) => { + super_root.output_roots.iter().map(|r| r.chain_id).collect() + } + PreState::TransitionState(ref transition_state) => { + transition_state.pre_state.output_roots.iter().map(|r| r.chain_id).collect() + } + }; + + // Attempt to load the rollup config from the chain ID. If there is no config for the chain, + // fall back to loading the config from the preimage oracle. + let rollup_configs: HashMap = if chain_ids + .iter() + .all(|id| ROLLUP_CONFIGS.contains_key(id)) + { + chain_ids.iter().map(|id| (*id, ROLLUP_CONFIGS[id].clone())).collect() + } else { + warn!( + target: "boot_loader", + "No rollup config found for chain IDs {:?}, falling back to preimage oracle. This is insecure in production without additional validation!", + chain_ids + ); + let ser_cfg = oracle + .get(PreimageKey::new_local(L2_ROLLUP_CONFIG_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)?; + serde_json::from_slice(&ser_cfg).map_err(OracleProviderError::Serde)? + }; + + // Attempt to load the l1 config from the chain ID. If there is no config for the chain, + // fall back to loading the config from the preimage oracle. + + // Note that there should be only one l1 config per interop cluster. Let's ensure that all + // the chain ids are the same. + let l1_chain_ids = rollup_configs.values().map(|cfg| cfg.l1_chain_id).collect::>(); + if l1_chain_ids.iter().any(|id| *id != l1_chain_ids[0]) { + return Err(BootstrapError::InvalidL1Config); + } + + let l1_chain_id = l1_chain_ids[0]; + + let l1_config = if let Some(config) = L1_CONFIGS.get(&l1_chain_id) { + config.clone() + } else { + warn!( + target: "boot_loader", + "No l1 config found for chain ID {}, falling back to preimage oracle. This is insecure in production without additional validation!", + l1_chain_id + ); + let ser_cfg = oracle + .get(PreimageKey::new_local(L1_CONFIG_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)?; + serde_json::from_slice(&ser_cfg).map_err(OracleProviderError::Serde)? + }; + + Ok(Self { + l1_head, + l1_config, + rollup_configs, + agreed_pre_state_commitment: l2_pre, + agreed_pre_state, + claimed_post_state: l2_post, + claimed_l2_timestamp: l2_claim_block, + }) + } + + /// Returns the [RollupConfig] corresponding to the [PreState::active_l2_chain_id]. + pub fn active_rollup_config(&self) -> Option { + let active_l2_chain_id = self.agreed_pre_state.active_l2_chain_id()?; + self.rollup_configs.get(&active_l2_chain_id).cloned() + } + + /// Returns the [L1ChainConfig] corresponding to the [PreState::active_l2_chain_id] through the + /// l2 [RollupConfig]. + pub fn active_l1_config(&self) -> L1ChainConfig { + self.l1_config.clone() + } + + /// Returns the [RollupConfig] corresponding to the given `chain_id`. + pub fn rollup_config(&self, chain_id: u64) -> Option { + self.rollup_configs.get(&chain_id).cloned() + } +} + +/// An error that occurred during the bootstrapping phase. +#[derive(Debug, Error)] +pub enum BootstrapError { + /// An error occurred while reading from the preimage oracle. + #[error(transparent)] + Oracle(#[from] OracleProviderError), + /// The pre-state is invalid and the post-state claim is not invalid. + #[error("`INVALID` pre-state claim; Post-state {0} unexpected.")] + InvalidPostState(B256), + /// The pre-state is invalid and the post-state claim is also invalid. + #[error("No-op state transition detected; both pre and post states are `INVALID`.")] + InvalidToInvalid, + /// The l1 config is invalid because the chain ids are not the same. + #[error("The l1 config is invalid because the chain ids are not the same.")] + InvalidL1Config, +} + +/// Reads the raw pre-state from the preimage oracle. +pub(crate) async fn read_raw_pre_state( + caching_oracle: &O, + agreed_pre_state_commitment: B256, +) -> Result +where + O: CommsClient, +{ + HintType::AgreedPreState + .with_data(&[agreed_pre_state_commitment.as_ref()]) + .send(caching_oracle) + .await?; + let pre = caching_oracle + .get(PreimageKey::new(*agreed_pre_state_commitment, PreimageKeyType::Keccak256)) + .await + .map_err(OracleProviderError::Preimage)?; + + if pre.is_empty() { + return Err(OracleProviderError::Preimage(PreimageOracleError::Other( + "Invalid pre-state preimage".to_string(), + ))); + } + + Ok(Bytes::from(pre)) +} diff --git a/kona/crates/proof/proof-interop/src/consolidation.rs b/kona/crates/proof/proof-interop/src/consolidation.rs new file mode 100644 index 0000000000000..109735e6b41aa --- /dev/null +++ b/kona/crates/proof/proof-interop/src/consolidation.rs @@ -0,0 +1,303 @@ +//! Interop dependency resolution and consolidation logic. + +use crate::{BootInfo, OptimisticBlock, OracleInteropProvider, PreState}; +use alloc::vec::Vec; +use alloy_consensus::{Header, Sealed}; +use alloy_eips::Encodable2718; +use alloy_evm::{EvmFactory, FromRecoveredTx, FromTxWithEncoded}; +use alloy_op_evm::block::OpTxEnv; +use alloy_primitives::{Address, B256, Bytes, Sealable, TxKind, U256, address}; +use alloy_rpc_types_engine::PayloadAttributes; +use core::fmt::Debug; +use kona_executor::{Eip1559ValidationError, ExecutorError, StatelessL2Builder}; +use kona_interop::{MessageGraph, MessageGraphError}; +use kona_mpt::OrderedListWalker; +use kona_preimage::CommsClient; +use kona_proof::{errors::OracleProviderError, l2::OracleL2ChainProvider}; +use kona_protocol::OutputRoot; +use kona_registry::{HashMap, ROLLUP_CONFIGS}; +use op_alloy_consensus::{InteropBlockReplacementDepositSource, OpTxEnvelope, OpTxType, TxDeposit}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use op_revm::OpSpecId; +use revm::context::BlockEnv; +use thiserror::Error; +use tracing::{error, info}; + +/// The [SuperchainConsolidator] holds a [MessageGraph] and is responsible for recursively +/// consolidating the blocks within the graph, per [message validity rules]. +/// +/// [message validity rules]: https://specs.optimism.io/interop/messaging.html#invalid-messages +#[derive(Debug)] +pub struct SuperchainConsolidator<'a, C, Evm> +where + C: CommsClient, +{ + /// The [BootInfo] of the program. + boot_info: &'a mut BootInfo, + /// The [OracleInteropProvider] used for the message graph. + interop_provider: OracleInteropProvider, + /// The [OracleL2ChainProvider]s used for re-execution of invalid blocks, keyed by chain ID. + l2_providers: HashMap>, + /// The inner [`EvmFactory`] to create EVM instances for re-execution of bad blocks. + evm_factory: Evm, +} + +impl<'a, C, Evm> SuperchainConsolidator<'a, C, Evm> +where + C: CommsClient + Debug + Send + Sync, + Evm: EvmFactory + Send + Sync + Debug + Clone + 'static, + ::Tx: + FromTxWithEncoded + FromRecoveredTx + OpTxEnv, +{ + /// Creates a new [SuperchainConsolidator] with the given providers and [Header]s. + /// + /// [Header]: alloy_consensus::Header + pub const fn new( + boot_info: &'a mut BootInfo, + interop_provider: OracleInteropProvider, + l2_providers: HashMap>, + evm_factory: Evm, + ) -> Self { + Self { boot_info, interop_provider, l2_providers, evm_factory } + } + + /// Recursively consolidates the dependencies of the blocks within the [MessageGraph]. + /// + /// This method will recurse until all invalid cross-chain dependencies have been resolved, + /// re-executing deposit-only blocks for chains with invalid dependencies as needed. + pub async fn consolidate(&mut self) -> Result<(), ConsolidationError> { + info!(target: "superchain_consolidator", "Consolidating superchain"); + + loop { + match self.consolidate_once().await { + Ok(()) => { + info!(target: "superchain_consolidator", "Superchain consolidation complete"); + return Ok(()); + } + Err(ConsolidationError::MessageGraph(MessageGraphError::InvalidMessages(_))) => { + // If invalid messages are still present in the graph, continue the loop. + continue; + } + Err(e) => { + error!(target: "superchain_consolidator", "Error consolidating superchain: {:?}", e); + return Err(e); + } + } + } + } + + /// Performs a single iteration of the consolidation process. + /// + /// Step-wise: + /// 1. Derive a new [MessageGraph] from the current set of local safe [Header]s. + /// 2. Resolve the [MessageGraph]. + /// 3. If any invalid messages are found, re-execute the bad block(s) only deposit transactions, + /// and bubble up the error. + /// + /// [Header]: alloy_consensus::Header + async fn consolidate_once(&mut self) -> Result<(), ConsolidationError> { + // Derive the message graph from the current set of block headers. + let graph = MessageGraph::derive( + self.interop_provider.local_safe_heads(), + &self.interop_provider, + &self.boot_info.rollup_configs, + ) + .await?; + + // Attempt to resolve the message graph. If there were any invalid messages found, we must + // initiate a re-execution of the original block, with only deposit transactions. + if let Err(MessageGraphError::InvalidMessages(invalid_chains)) = graph.resolve().await { + self.re_execute_deposit_only(&invalid_chains.keys().copied().collect::>()) + .await?; + return Err(MessageGraphError::InvalidMessages(invalid_chains).into()); + } + + Ok(()) + } + + /// Re-executes the original blocks, keyed by their chain IDs, with only their deposit + /// transactions. + async fn re_execute_deposit_only( + &mut self, + chain_ids: &[u64], + ) -> Result<(), ConsolidationError> { + for chain_id in chain_ids { + // Find the optimistic block header for the chain ID. + let header = self + .interop_provider + .local_safe_heads() + .get(chain_id) + .ok_or(MessageGraphError::EmptyDependencySet)?; + + // Look up the parent header for the block. + let parent_header = + self.interop_provider.header_by_hash(*chain_id, header.parent_hash).await?; + + // Traverse the transactions trie of the block to re-execute. + let trie_walker = OrderedListWalker::try_new_hydrated( + header.transactions_root, + &self.interop_provider, + ) + .map_err(OracleProviderError::TrieWalker)?; + let transactions = trie_walker.into_iter().map(|(_, rlp)| rlp).collect::>(); + + // Explicitly panic if a block sent off for re-execution already contains nothing but + // deposits. + assert!( + !transactions.iter().all(|f| !f.is_empty() && f[0] == OpTxType::Deposit), + "Impossible case; Block with only deposits found to be invalid. Something has gone horribly wrong!" + ); + + // Fetch the rollup config + provider for the current chain ID. + let rollup_config = ROLLUP_CONFIGS + .get(chain_id) + .or_else(|| self.boot_info.rollup_configs.get(chain_id)) + .ok_or(ConsolidationError::MissingRollupConfig(*chain_id))?; + let l2_provider = self + .l2_providers + .get(chain_id) + .ok_or(ConsolidationError::MissingLocalProvider(*chain_id))?; + + let PreState::TransitionState(ref mut transition_state) = + self.boot_info.agreed_pre_state + else { + return Err(ConsolidationError::InvalidPreStateVariant); + }; + let original_optimistic_block = transition_state + .pending_progress + .iter_mut() + .find(|block| block.block_hash == header.hash()) + .ok_or(MessageGraphError::EmptyDependencySet)?; + + // Filter out all transactions that are not deposits to start. + let mut transactions = transactions + .into_iter() + .filter(|t| !t.is_empty() && t[0] == OpTxType::Deposit) + .collect::>(); + + // Add the deposit replacement system transaction at the end of the list. + transactions.push(Self::craft_replacement_transaction( + header, + original_optimistic_block.output_root, + )); + + // Re-craft the execution payload, trimming off all non-deposit transactions. + let deposit_only_payload = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: header.timestamp, + prev_randao: header.mix_hash, + suggested_fee_recipient: header.beneficiary, + withdrawals: Default::default(), + parent_beacon_block_root: header.parent_beacon_block_root, + }, + transactions: Some(transactions), + no_tx_pool: Some(true), + gas_limit: Some(header.gas_limit), + eip_1559_params: rollup_config + .is_holocene_active(header.timestamp) + .then(|| { + // SAFETY: After the Holocene hardfork, blocks must have the EIP-1559 + // parameters of the chain placed within the + // header's `extra_data` field. This slice index + + // conversion cannot fail unless the protocol rules + // have been violated. + header.extra_data.get(1..9).and_then(|s| s.try_into().ok()).ok_or( + ExecutorError::InvalidExtraData(Eip1559ValidationError::Decode( + op_alloy_consensus::EIP1559ParamError::NoEIP1559Params, + )), + ) + }) + .transpose()?, + min_base_fee: rollup_config + .is_jovian_active(header.timestamp) + .then(|| { + header + .extra_data + .get(9..17) + .and_then(|s| <[u8; 8]>::try_from(s).ok()) + .map(u64::from_be_bytes) + .ok_or(ExecutorError::InvalidExtraData(Eip1559ValidationError::Decode( + op_alloy_consensus::EIP1559ParamError::MinBaseFeeNotSet, + ))) + }) + .transpose()?, + }; + + // Create a new stateless L2 block executor for the current chain. + let mut executor = StatelessL2Builder::new( + rollup_config, + self.evm_factory.clone(), + l2_provider.clone(), + l2_provider.clone(), + parent_header.seal_slow(), + ); + + // Execute the block and take the new header. At this point, the block is guaranteed to + // be canonical. + let new_header = executor.build_block(deposit_only_payload)?.header; + let new_output_root = executor.compute_output_root()?; + + // Replace the original optimistic block with the deposit only block. + *original_optimistic_block = OptimisticBlock::new(new_header.hash(), new_output_root); + + // Replace the original header with the new header. + self.interop_provider.replace_local_safe_head(*chain_id, new_header); + } + + Ok(()) + } + + /// Forms the replacement transaction inserted into a deposit-only block in the event that a + /// block is reduced due to invalid messages. + /// + /// + fn craft_replacement_transaction(old_header: &Sealed
, old_output_root: B256) -> Bytes { + const REPLACEMENT_SENDER: Address = address!("deaddeaddeaddeaddeaddeaddeaddeaddead0002"); + const REPLACEMENT_GAS: u64 = 36000; + + let source = InteropBlockReplacementDepositSource::new(old_output_root); + let output_root = OutputRoot::from_parts( + old_header.state_root, + old_header.withdrawals_root.unwrap_or_default(), + old_header.hash(), + ); + let replacement_tx = OpTxEnvelope::Deposit( + TxDeposit { + source_hash: source.source_hash(), + from: REPLACEMENT_SENDER, + to: TxKind::Call(Address::ZERO), + mint: 0, + value: U256::ZERO, + gas_limit: REPLACEMENT_GAS, + is_system_transaction: false, + input: output_root.encode().into(), + } + .seal(), + ); + + replacement_tx.encoded_2718().into() + } +} + +/// An error type for the [SuperchainConsolidator] struct. +#[derive(Debug, Error)] +pub enum ConsolidationError { + /// An invalid pre-state variant was passed to the consolidator. + #[error("Invalid PreState variant")] + InvalidPreStateVariant, + /// Missing a rollup configuration. + #[error("Missing rollup configuration for chain ID {0}")] + MissingRollupConfig(u64), + /// Missing a local L2 chain provider. + #[error("Missing local L2 chain provider for chain ID {0}")] + MissingLocalProvider(u64), + /// An error occurred during consolidation. + #[error(transparent)] + MessageGraph(#[from] MessageGraphError), + /// An error occurred during execution. + #[error(transparent)] + Executor(#[from] ExecutorError), + /// An error occurred during RLP decoding. + #[error(transparent)] + OracleProvider(#[from] OracleProviderError), +} diff --git a/kona/crates/proof/proof-interop/src/hint.rs b/kona/crates/proof/proof-interop/src/hint.rs new file mode 100644 index 0000000000000..1b02b4c282caa --- /dev/null +++ b/kona/crates/proof/proof-interop/src/hint.rs @@ -0,0 +1,193 @@ +//! This module contains the [HintType] enum. + +use alloc::{string::ToString, vec::Vec}; +use core::{fmt::Display, str::FromStr}; +use kona_proof::{Hint, errors::HintParsingError}; + +/// The [HintType] enum is used to specify the type of hint that was received. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum HintType { + /// A hint that specifies the block header of a layer 1 block. + L1BlockHeader, + /// A hint that specifies the transactions of a layer 1 block. + L1Transactions, + /// A hint that specifies the state node of a layer 1 block. + L1Receipts, + /// A hint that specifies a blob in the layer 1 beacon chain. + L1Blob, + /// A hint that specifies a precompile call on layer 1. + L1Precompile, + /// A hint that specifies the block header of a layer 2 block. + L2BlockHeader, + /// A hint that specifies the transactions of a layer 2 block. + L2Transactions, + /// A hint that specifies the receipts of a layer 2 block. + L2Receipts, + /// A hint that specifies the code of a contract on layer 2. + L2Code, + /// A hint that specifies the preimage of the agreed upon pre-state claim. + AgreedPreState, + /// A hint that specifies the preimage of an L2 output root within the agreed upon pre-state, + /// by chain ID. + L2OutputRoot, + /// A hint that specifies the state node in the L2 state trie. + L2StateNode, + /// A hint that specifies the proof on the path to an account in the L2 state trie. + L2AccountProof, + /// A hint that specifies the proof on the path to a storage slot in an account within in the + /// L2 state trie. + L2AccountStorageProof, + /// A hint that specifies loading the payload witness for an optimistic block. + L2BlockData, + /// A hint that specifies bulk storage of all the code, state and keys generated by an + /// execution witness. + L2PayloadWitness, +} + +impl HintType { + /// Creates a new [Hint] from `self` and the specified data. The data passed will be + /// concatenated into a single byte array before being stored in the resulting [Hint]. + pub fn with_data(self, data: &[&[u8]]) -> Hint { + let total_len = data.iter().map(|d| d.len()).sum(); + let hint_data = data.iter().fold(Vec::with_capacity(total_len), |mut acc, d| { + acc.extend_from_slice(d); + acc + }); + Hint::new(self, hint_data) + } +} + +impl FromStr for HintType { + type Err = HintParsingError; + + fn from_str(value: &str) -> Result { + match value { + "l1-block-header" => Ok(Self::L1BlockHeader), + "l1-transactions" => Ok(Self::L1Transactions), + "l1-receipts" => Ok(Self::L1Receipts), + "l1-blob" => Ok(Self::L1Blob), + "l1-precompile" => Ok(Self::L1Precompile), + "l2-block-header" => Ok(Self::L2BlockHeader), + "l2-transactions" => Ok(Self::L2Transactions), + "l2-receipts" => Ok(Self::L2Receipts), + "l2-code" => Ok(Self::L2Code), + "agreed-pre-state" => Ok(Self::AgreedPreState), + "l2-output-root" => Ok(Self::L2OutputRoot), + "l2-state-node" => Ok(Self::L2StateNode), + "l2-account-proof" => Ok(Self::L2AccountProof), + "l2-account-storage-proof" => Ok(Self::L2AccountStorageProof), + "l2-block-data" => Ok(Self::L2BlockData), + "l2-payload-witness" => Ok(Self::L2PayloadWitness), + _ => Err(HintParsingError(value.to_string())), + } + } +} + +impl From for &str { + fn from(value: HintType) -> Self { + match value { + HintType::L1BlockHeader => "l1-block-header", + HintType::L1Transactions => "l1-transactions", + HintType::L1Receipts => "l1-receipts", + HintType::L1Blob => "l1-blob", + HintType::L1Precompile => "l1-precompile", + HintType::L2BlockHeader => "l2-block-header", + HintType::L2Transactions => "l2-transactions", + HintType::L2Receipts => "l2-receipts", + HintType::L2Code => "l2-code", + HintType::AgreedPreState => "agreed-pre-state", + HintType::L2OutputRoot => "l2-output-root", + HintType::L2StateNode => "l2-state-node", + HintType::L2AccountProof => "l2-account-proof", + HintType::L2AccountStorageProof => "l2-account-storage-proof", + HintType::L2BlockData => "l2-block-data", + HintType::L2PayloadWitness => "l2-payload-witness", + } + } +} + +impl Display for HintType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let s: &str = (*self).into(); + write!(f, "{s}") + } +} + +mod test { + #[test] + fn test_hint_type_from_str() { + use super::HintType; + use crate::alloc::string::ToString; + use core::str::FromStr; + use kona_proof::errors::HintParsingError; + + assert_eq!(HintType::from_str("l1-block-header").unwrap(), HintType::L1BlockHeader); + assert_eq!(HintType::from_str("l1-transactions").unwrap(), HintType::L1Transactions); + assert_eq!(HintType::from_str("l1-receipts").unwrap(), HintType::L1Receipts); + assert_eq!(HintType::from_str("l1-blob").unwrap(), HintType::L1Blob); + assert_eq!(HintType::from_str("l1-precompile").unwrap(), HintType::L1Precompile); + assert_eq!(HintType::from_str("l2-block-header").unwrap(), HintType::L2BlockHeader); + assert_eq!(HintType::from_str("l2-block-data").unwrap(), HintType::L2BlockData); + assert_eq!(HintType::from_str("l2-transactions").unwrap(), HintType::L2Transactions); + assert_eq!(HintType::from_str("l2-receipts").unwrap(), HintType::L2Receipts); + assert_eq!(HintType::from_str("l2-code").unwrap(), HintType::L2Code); + assert_eq!(HintType::from_str("agreed-pre-state").unwrap(), HintType::AgreedPreState); + assert_eq!(HintType::from_str("l2-output-root").unwrap(), HintType::L2OutputRoot); + assert_eq!(HintType::from_str("l2-account-proof").unwrap(), HintType::L2AccountProof); + assert_eq!( + HintType::from_str("l2-account-storage-proof").unwrap(), + HintType::L2AccountStorageProof + ); + assert_eq!(HintType::from_str("l2-block-data").unwrap(), HintType::L2BlockData); + assert_eq!(HintType::from_str("l2-payload-witness").unwrap(), HintType::L2PayloadWitness); + match HintType::from_str("invalid") { + Ok(_) => { + panic!("expected error"); + } + Err(parsing_err) => { + let HintParsingError(str) = parsing_err; + assert_eq!(str, "invalid".to_string()); + } + } + } + + #[test] + fn test_hint_type_to_str() { + use super::HintType; + + assert_eq!(<&str>::from(HintType::L1BlockHeader), "l1-block-header"); + assert_eq!(<&str>::from(HintType::L1Transactions), "l1-transactions"); + assert_eq!(<&str>::from(HintType::L1Receipts), "l1-receipts"); + assert_eq!(<&str>::from(HintType::L1Blob), "l1-blob"); + assert_eq!(<&str>::from(HintType::L1Precompile), "l1-precompile"); + assert_eq!(<&str>::from(HintType::L2BlockHeader), "l2-block-header"); + assert_eq!(<&str>::from(HintType::L2Transactions), "l2-transactions"); + assert_eq!(<&str>::from(HintType::L2Receipts), "l2-receipts"); + assert_eq!(<&str>::from(HintType::L2Code), "l2-code"); + assert_eq!(<&str>::from(HintType::AgreedPreState), "agreed-pre-state"); + assert_eq!(<&str>::from(HintType::L2OutputRoot), "l2-output-root"); + assert_eq!(<&str>::from(HintType::L2StateNode), "l2-state-node"); + assert_eq!(<&str>::from(HintType::L2AccountProof), "l2-account-proof"); + assert_eq!(<&str>::from(HintType::L2AccountStorageProof), "l2-account-storage-proof"); + assert_eq!(<&str>::from(HintType::L2BlockData), "l2-block-data"); + assert_eq!(<&str>::from(HintType::L2PayloadWitness), "l2-payload-witness"); + } + + #[test] + fn test_hint_with_data() { + use super::HintType; + use alloy_primitives::Bytes; + + let hint_data: &[u8] = &[1, 2]; + let l1_block_header = HintType::L1BlockHeader.with_data(&[hint_data]); + assert_eq!(l1_block_header.data, Bytes::from(hint_data)); + } + + #[test] + fn test_hint_fmt() { + use super::HintType; + use alloc::format; + + assert_eq!(format!("{}", HintType::L1BlockHeader), "l1-block-header"); + } +} diff --git a/kona/crates/proof/proof-interop/src/lib.rs b/kona/crates/proof/proof-interop/src/lib.rs new file mode 100644 index 0000000000000..593b89097c06a --- /dev/null +++ b/kona/crates/proof/proof-interop/src/lib.rs @@ -0,0 +1,28 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "arbitrary"), no_std)] + +extern crate alloc; + +mod pre_state; +pub use pre_state::{ + INVALID_TRANSITION, INVALID_TRANSITION_HASH, OptimisticBlock, PreState, + TRANSITION_STATE_MAX_STEPS, TransitionState, +}; + +mod hint; +pub use hint::HintType; + +mod provider; +pub use provider::OracleInteropProvider; + +pub mod boot; +pub use boot::BootInfo; + +mod consolidation; +pub use consolidation::{ConsolidationError, SuperchainConsolidator}; diff --git a/kona/crates/proof/proof-interop/src/pre_state.rs b/kona/crates/proof/proof-interop/src/pre_state.rs new file mode 100644 index 0000000000000..36d473802b190 --- /dev/null +++ b/kona/crates/proof/proof-interop/src/pre_state.rs @@ -0,0 +1,573 @@ +//! Types for the pre-state claims used in the interop proof. + +use alloc::vec::Vec; +use alloy_primitives::{B256, Bytes, b256, keccak256}; +use alloy_rlp::{Buf, Decodable, Encodable, Header, RlpDecodable, RlpEncodable}; +use kona_interop::{OutputRootWithChain, SUPER_ROOT_VERSION, SuperRoot}; +use serde::{Deserialize, Serialize}; + +/// The current [TransitionState] encoding format version. +pub(crate) const TRANSITION_STATE_VERSION: u8 = 255; + +/// The maximum number of steps allowed in a [TransitionState]. +pub const TRANSITION_STATE_MAX_STEPS: u64 = 2u64.pow(7) - 1; + +/// The [Bytes] representation of the string "invalid". +pub const INVALID_TRANSITION: Bytes = Bytes::from_static(b"invalid"); + +/// `keccak256("invalid")` +pub const INVALID_TRANSITION_HASH: B256 = + b256!("ffd7db0f9d5cdeb49c4c9eba649d4dc6d852d64671e65488e57f58584992ac68"); + +/// The [PreState] of the interop proof program can be one of two types: a [SuperRoot] or a +/// [TransitionState]. The [SuperRoot] is the canonical state of the superchain, while the +/// [TransitionState] is a super-structure of the [SuperRoot] that represents the progress of a +/// pending superchain state transition from one [SuperRoot] to the next. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum PreState { + /// The canonical state of the superchain. + SuperRoot(SuperRoot), + /// The progress of a pending superchain state transition. + TransitionState(TransitionState), +} + +impl PreState { + /// Hashes the encoded [PreState] using [keccak256]. + pub fn hash(&self) -> B256 { + let mut rlp_buf = Vec::with_capacity(self.length()); + self.encode(&mut rlp_buf); + keccak256(&rlp_buf) + } + + /// Returns the timestamp of the [PreState]. + pub const fn timestamp(&self) -> u64 { + match self { + Self::SuperRoot(super_root) => super_root.timestamp, + Self::TransitionState(transition_state) => transition_state.pre_state.timestamp, + } + } + + /// Returns the active L2 output root hash of the [PreState]. This is the output root that + /// represents the pre-state of the chain that is to be committed to in the next transition + /// step, or [None] if the [PreState] has already been fully saturated. + pub fn active_l2_output_root(&self) -> Option<&OutputRootWithChain> { + match self { + Self::SuperRoot(super_root) => super_root.output_roots.first(), + Self::TransitionState(transition_state) => { + transition_state.pre_state.output_roots.get(transition_state.step as usize) + } + } + } + + /// Returns the active L2 chain ID of the [PreState]. This is the chain ID of the output root + /// that is to be committed to in the next transition step, or [None] if the [PreState] + /// has already been fully saturated. + pub fn active_l2_chain_id(&self) -> Option { + self.active_l2_output_root().map(|output_root| output_root.chain_id) + } + + /// Transitions to the next state, appending the [OptimisticBlock] to the pending progress. + pub fn transition(self, optimistic_block: Option) -> Option { + match self { + Self::SuperRoot(super_root) => Some(Self::TransitionState(TransitionState::new( + super_root, + alloc::vec![optimistic_block?], + 1, + ))), + Self::TransitionState(mut transition_state) => { + // If the transition state's pending progress contains the same number of states as + // the pre-state's output roots already, then we can either no-op + // the transition or finalize it. + if transition_state.pending_progress.len() == + transition_state.pre_state.output_roots.len() + { + if transition_state.step == TRANSITION_STATE_MAX_STEPS { + let super_root = SuperRoot::new( + transition_state.pre_state.timestamp + 1, + transition_state + .pending_progress + .iter() + .zip(transition_state.pre_state.output_roots.iter()) + .map(|(optimistic_block, pre_state_output)| { + OutputRootWithChain::new( + pre_state_output.chain_id, + optimistic_block.output_root, + ) + }) + .collect(), + ); + return Some(Self::SuperRoot(super_root)); + } else { + transition_state.step += 1; + return Some(Self::TransitionState(transition_state)); + }; + } + + transition_state.pending_progress.push(optimistic_block?); + transition_state.step += 1; + Some(Self::TransitionState(transition_state)) + } + } + } +} + +impl Encodable for PreState { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + Self::SuperRoot(super_root) => { + super_root.encode(out); + } + Self::TransitionState(transition_state) => { + transition_state.encode(out); + } + } + } +} + +impl Decodable for PreState { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + if buf.is_empty() { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + match buf[0] { + TRANSITION_STATE_VERSION => { + let transition_state = TransitionState::decode(buf)?; + Ok(Self::TransitionState(transition_state)) + } + SUPER_ROOT_VERSION => { + let super_root = + SuperRoot::decode(buf).map_err(|_| alloy_rlp::Error::UnexpectedString)?; + Ok(Self::SuperRoot(super_root)) + } + _ => Err(alloy_rlp::Error::Custom("invalid version byte")), + } + } +} + +/// The [TransitionState] is a super-structure of the [SuperRoot] that represents the progress of a +/// pending superchain state transition from one [SuperRoot] to the next. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct TransitionState { + /// The canonical pre-state super root commitment. + pub pre_state: SuperRoot, + /// The progress that has been made in the pending superchain state transition. + pub pending_progress: Vec, + /// The step number of the pending superchain state transition. + pub step: u64, +} + +impl TransitionState { + /// Create a new [TransitionState] with the given pre-state, pending progress, and step number. + pub const fn new( + pre_state: SuperRoot, + pending_progress: Vec, + step: u64, + ) -> Self { + Self { pre_state, pending_progress, step } + } + + /// Hashes the encoded [TransitionState] using [keccak256]. + pub fn hash(&self) -> B256 { + let mut rlp_buf = Vec::with_capacity(self.length()); + self.encode(&mut rlp_buf); + keccak256(&rlp_buf) + } + + /// Returns the RLP payload length of the [TransitionState]. + pub fn payload_length(&self) -> usize { + Header { list: false, payload_length: self.pre_state.encoded_length() }.length() + + self.pre_state.encoded_length() + + self.pending_progress.length() + + self.step.length() + } +} + +impl Encodable for TransitionState { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(TRANSITION_STATE_VERSION); + + Header { list: true, payload_length: self.payload_length() }.encode(out); + + // The pre-state has special encoding, since it is not RLP. We encode the structure, and + // then encode it as a RLP string. + let mut pre_state_buf = Vec::new(); + self.pre_state.encode(&mut pre_state_buf); + Bytes::from(pre_state_buf).encode(out); + + self.pending_progress.encode(out); + self.step.encode(out); + } +} + +impl Decodable for TransitionState { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + if buf.is_empty() { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + let version = buf[0]; + if version != TRANSITION_STATE_VERSION { + return Err(alloy_rlp::Error::Custom("invalid version byte")); + } + buf.advance(1); + + // Decode the RLP header. + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + // The pre-state has special decoding, since it is not RLP. We decode the RLP string, and + // then decode the structure. + let pre_state_buf = Bytes::decode(buf)?; + let pre_state = SuperRoot::decode(&mut pre_state_buf.as_ref()) + .map_err(|_| alloy_rlp::Error::UnexpectedString)?; + + // The rest of the fields are RLP encoded as normal. + let pending_progress = Vec::::decode(buf)?; + let step = u64::decode(buf)?; + + Ok(Self { pre_state, pending_progress, step }) + } +} + +/// A wrapper around a pending output root hash with the block hash it commits to. +#[derive( + Default, Debug, Clone, Eq, PartialEq, RlpEncodable, RlpDecodable, Serialize, Deserialize, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct OptimisticBlock { + /// The block hash of the output root. + pub block_hash: B256, + /// The output root hash. + pub output_root: B256, +} + +impl OptimisticBlock { + /// Create a new [OptimisticBlock] with the given block hash and output root hash. + pub const fn new(block_hash: B256, output_root: B256) -> Self { + Self { block_hash, output_root } + } +} + +#[cfg(test)] +mod test { + use super::{OptimisticBlock, SuperRoot, TransitionState}; + use alloc::{vec, vec::Vec}; + use alloy_primitives::B256; + use alloy_rlp::{Decodable, Encodable}; + use kona_interop::OutputRootWithChain; + + #[test] + fn test_static_transition_state_roundtrip() { + let transition_state = TransitionState::new( + SuperRoot::new( + 10, + vec![ + (OutputRootWithChain::new(1, B256::default())), + (OutputRootWithChain::new(2, B256::default())), + ], + ), + vec![OptimisticBlock::default(), OptimisticBlock::default()], + 1, + ); + + let mut rlp_buf = Vec::with_capacity(transition_state.length()); + transition_state.encode(&mut rlp_buf); + + assert_eq!(transition_state, TransitionState::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_pre_state_roundtrip() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + let pre_state = + super::PreState::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + + let mut rlp_buf = Vec::with_capacity(pre_state.length()); + pre_state.encode(&mut rlp_buf); + assert_eq!(pre_state, super::PreState::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_transition_state_roundtrip() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + let transition_state = + TransitionState::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + + let mut rlp_buf = Vec::with_capacity(transition_state.length()); + transition_state.encode(&mut rlp_buf); + assert_eq!(transition_state, TransitionState::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + /// Helper function to create a test TransitionState with three output roots + fn create_test_transition_state(step: u64, chain_count: u64) -> TransitionState { + const TIMESTAMP: u64 = 10; + + let mut output_roots = Vec::new(); + let mut pending_blocks = Vec::new(); + for x in 1..chain_count + 1 { + output_roots.push(OutputRootWithChain::new(x, B256::ZERO)); + if x != chain_count { + pending_blocks.push(OptimisticBlock::default()); + } + } + + TransitionState::new(SuperRoot::new(TIMESTAMP, output_roots), pending_blocks, step) + } + + // pre_state.transition() with TransitionState variant adds + // OptimisticBlock to pending_progress vec + #[test] + fn test_transition_increments_pending_progress() { + const OUTPUT_ROOTS: u64 = 3; + const INITIAL_STEP: u64 = 1; + + let transition_state = create_test_transition_state(INITIAL_STEP, OUTPUT_ROOTS); + let initial_len = transition_state.pending_progress.len(); + let pre_state = super::PreState::TransitionState(transition_state); + + let new_pre_state = pre_state.transition(Some(OptimisticBlock::default())).unwrap(); + match new_pre_state { + super::PreState::TransitionState(post_transition_state) => { + assert_eq!(initial_len + 1, post_transition_state.pending_progress.len()); + } + _ => panic!("Expected TransitionState"), + } + } + + // TransitionState.hash() matches keccak256 of its RLP encoding + #[test] + fn test_transition_hash() { + const OUTPUT_ROOTS: u64 = 3; + const INITIAL_STEP: u64 = 1; + + let transition_state = create_test_transition_state(INITIAL_STEP, OUTPUT_ROOTS); + let hash = transition_state.hash(); + + let mut rlp_buf = Vec::with_capacity(transition_state.length()); + transition_state.encode(&mut rlp_buf); + let expected_hash = super::keccak256(&rlp_buf); + + assert_eq!(hash, expected_hash); + } + + #[test] + fn test_pre_state_hash_matches_encoded_hash() { + let pre_state = super::PreState::SuperRoot(SuperRoot::new( + 10, + vec![OutputRootWithChain::new(1, B256::ZERO)], + )); + let hash = pre_state.hash(); + + let mut rlp_buf = Vec::with_capacity(pre_state.length()); + pre_state.encode(&mut rlp_buf); + let expected_hash = super::keccak256(&rlp_buf); + + assert_eq!(hash, expected_hash); + } + + // PreState::SuperRoot encodes/decodes correctly via RLP + #[test] + fn test_pre_state_super_root_encode() { + let pre_state = super::PreState::SuperRoot(SuperRoot::new( + 10, + vec![OutputRootWithChain::new(1, B256::ZERO)], + )); + let mut rlp_buf = Vec::new(); + pre_state.encode(&mut rlp_buf); + + let decoded = super::PreState::decode(&mut rlp_buf.as_slice()).unwrap(); + assert_eq!(decoded, pre_state); + } + + // PreState::TransitionState encodes/decodes correctly via RLP + #[test] + fn test_pre_state_transition_state_encode() { + const OUTPUT_ROOTS: u64 = 3; + const INITIAL_STEP: u64 = 1; + let transition_state = create_test_transition_state(INITIAL_STEP, OUTPUT_ROOTS); + let pre_state = super::PreState::TransitionState(transition_state); + let mut rlp_buf = Vec::new(); + pre_state.encode(&mut rlp_buf); + + let decoded = super::PreState::decode(&mut rlp_buf.as_slice()).unwrap(); + assert_eq!(decoded, pre_state); + } + + #[test] + fn test_pre_state_timestamp() { + const TIMESTAMP: u64 = 10; + + let transition_state = TransitionState::new( + SuperRoot::new(TIMESTAMP, vec![OutputRootWithChain::new(1, B256::ZERO)]), + vec![OptimisticBlock::default()], + 1, + ); + + let pre_state = super::PreState::TransitionState(transition_state); + let timestamp = pre_state.timestamp(); + + assert_eq!(TIMESTAMP, timestamp); + } + + // PreState::TransitionState.transition() returns PreState::SuperRoot if transition_state.step + // == TRANSITION_STATE_MAX_STEPS + #[test] + fn test_transition_state_max_steps() { + const OUTPUT_ROOTS: u64 = 2; + const INITIAL_STEP: u64 = super::TRANSITION_STATE_MAX_STEPS - OUTPUT_ROOTS + 1; + + let transition_state = create_test_transition_state(INITIAL_STEP, OUTPUT_ROOTS); + let pre_state = super::PreState::TransitionState(transition_state); + + let new_pre_state_1 = pre_state.transition(Some(OptimisticBlock::default())).unwrap(); + let new_pre_state_2 = new_pre_state_1.transition(Some(OptimisticBlock::default())).unwrap(); + match new_pre_state_2 { + super::PreState::SuperRoot(super_root) => { + let last_output_root = super_root.output_roots.last().unwrap(); + assert_eq!(OUTPUT_ROOTS, last_output_root.chain_id); + } + _ => panic!("Expected SuperRoot"), + } + } + + // PreState::TransitionState.transition() does not add Block if if pending_progress.len() == + // pre_state.output_roots.len() and TRANSITION_STATE_MAX_STEPS not reached + #[test] + fn test_transition_state_step_increment_at_capacity() { + const TIMESTAMP: u64 = 10; + const STEP: u64 = 1; + let transition_state = TransitionState::new( + SuperRoot::new(TIMESTAMP, vec![OutputRootWithChain::new(1, B256::ZERO)]), + vec![OptimisticBlock::default()], + STEP, + ); + let transition_state_pending_progress_len = transition_state.pending_progress.len(); + let pre_state = super::PreState::TransitionState(transition_state); + + let new_pre_state = pre_state.transition(Some(OptimisticBlock::default())).unwrap(); + match new_pre_state { + super::PreState::TransitionState(new_transition_state) => { + // Transition does not increase length + assert_eq!( + transition_state_pending_progress_len, + new_transition_state.pending_progress.len() + ); + } + _ => panic!("Expected TransitionState"), + } + } + + // PreState::TransitionState.active_l2_chain_id() returns the chain ID of the current step + #[test] + fn test_active_l2_chain_id_uses_step_as_index() { + const OUTPUT_ROOTS: u64 = 3; + const INITIAL_STEP: u64 = 1; + const EXPECTED_CHAIN_ID_AT_STEP_1: u64 = 2; + const EXPECTED_CHAIN_ID_AT_STEP_2: u64 = 3; + + let transition_state = create_test_transition_state(INITIAL_STEP, OUTPUT_ROOTS); + let pre_state = super::PreState::TransitionState(transition_state); + + let active_l2_chain_id = pre_state.active_l2_chain_id().unwrap(); + assert_eq!(active_l2_chain_id, EXPECTED_CHAIN_ID_AT_STEP_1); + + let new_pre_state = pre_state.transition(Some(OptimisticBlock::default())).unwrap(); + let active_chain_id = new_pre_state.active_l2_chain_id().unwrap(); + assert_eq!(active_chain_id, EXPECTED_CHAIN_ID_AT_STEP_2); + } + + #[test] + fn test_active_l2_chain_id_uses_step_as_index_super_root() { + const EXPECTED_CHAIN_ID_AT_STEP_1: u64 = 1; + let pre_state = super::PreState::SuperRoot(SuperRoot::new( + 10, + vec![OutputRootWithChain::new(1, B256::ZERO)], + )); + + let active_l2_chain_id = pre_state.active_l2_chain_id().unwrap(); + assert_eq!(active_l2_chain_id, EXPECTED_CHAIN_ID_AT_STEP_1); + } + + #[test] + fn test_super_root_transition_with_none_optimistic_block() { + let super_root = SuperRoot::new(10, vec![OutputRootWithChain::new(1, B256::ZERO)]); + let pre_state = super::PreState::SuperRoot(super_root); + + let result = pre_state.transition(None); + assert!(result.is_none()); + } + + #[test] + fn test_super_root_timestamp() { + const TIMESTAMP: u64 = 42; + let super_root = SuperRoot::new(TIMESTAMP, vec![OutputRootWithChain::new(1, B256::ZERO)]); + let pre_state = super::PreState::SuperRoot(super_root); + + assert_eq!(pre_state.timestamp(), TIMESTAMP); + } + + // PreState::decode returns UnexpectedLength for empty buffers + #[test] + fn test_pre_state_decode_empty_buffer() { + let mut empty_buf: &[u8] = &[]; + let result = super::PreState::decode(&mut empty_buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), alloy_rlp::Error::UnexpectedLength)); + } + + // PreState::decode returns Custom error for invalid version bytes + #[test] + fn test_pre_state_decode_invalid_version() { + let mut buf: &[u8] = &[2]; + let result = super::PreState::decode(&mut buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), alloy_rlp::Error::Custom("invalid version byte"))); + } + + // TransitionState::decode returns UnexpectedLength for empty buffers + #[test] + fn test_transition_state_decode_empty_buffer() { + let mut empty_buf: &[u8] = &[]; + let result = super::TransitionState::decode(&mut empty_buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), alloy_rlp::Error::UnexpectedLength)); + } + + #[test] + fn test_transition_state_decode_invalid_version() { + let mut buf: &[u8] = &[2]; + let result = super::TransitionState::decode(&mut buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), alloy_rlp::Error::Custom("invalid version byte"))); + } + + #[test] + fn test_transition_state_decode_non_list_header() { + let mut buf: &[u8] = &[255, 127]; + let result = super::TransitionState::decode(&mut buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), alloy_rlp::Error::UnexpectedString)); + } + + #[test] + fn test_optimistic_block_constructor() { + let block_hash = B256::random(); + let output_root = B256::random(); + let optimistic_block = OptimisticBlock::new(block_hash, output_root); + assert_eq!(block_hash, optimistic_block.block_hash); + assert_eq!(output_root, optimistic_block.output_root); + } +} diff --git a/kona/crates/proof/proof-interop/src/provider.rs b/kona/crates/proof/proof-interop/src/provider.rs new file mode 100644 index 0000000000000..ada827d2a0dcd --- /dev/null +++ b/kona/crates/proof/proof-interop/src/provider.rs @@ -0,0 +1,270 @@ +//! [InteropProvider] trait implementation using a [CommsClient] data source. + +use crate::{BootInfo, HintType}; +use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec}; +use alloy_consensus::{Header, Sealed}; +use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::{Address, B256}; +use alloy_rlp::Decodable; +use async_trait::async_trait; +use kona_interop::InteropProvider; +use kona_mpt::{OrderedListWalker, TrieHinter, TrieNode, TrieProvider}; +use kona_preimage::{CommsClient, PreimageKey, PreimageKeyType, errors::PreimageOracleError}; +use kona_proof::{eip_2935_history_lookup, errors::OracleProviderError}; +use kona_registry::HashMap; +use op_alloy_consensus::OpReceiptEnvelope; +use spin::RwLock; + +/// A [CommsClient] backed [InteropProvider] implementation. +#[derive(Debug, Clone)] +pub struct OracleInteropProvider { + /// The oracle client. + oracle: Arc, + /// The [BootInfo] for the current program execution. + boot: BootInfo, + /// The local safe head block header cache. + local_safe_heads: HashMap>, + /// The chain ID for the current call context. Used to declare the chain ID for the trie hints. + chain_id: Arc>>, +} + +impl OracleInteropProvider +where + C: CommsClient + Send + Sync, +{ + /// Creates a new [OracleInteropProvider] with the given oracle client and [BootInfo]. + pub fn new( + oracle: Arc, + boot: BootInfo, + local_safe_headers: HashMap>, + ) -> Self { + Self { + oracle, + boot, + local_safe_heads: local_safe_headers, + chain_id: Arc::new(RwLock::new(None)), + } + } + + /// Returns a reference to the local safe heads map. + pub const fn local_safe_heads(&self) -> &HashMap> { + &self.local_safe_heads + } + + /// Replaces a local safe head with the given header. + pub fn replace_local_safe_head(&mut self, chain_id: u64, header: Sealed
) { + self.local_safe_heads.insert(chain_id, header); + } + + /// Fetch the [Header] for the block with the given hash. + pub async fn header_by_hash( + &self, + chain_id: u64, + block_hash: B256, + ) -> Result::Error> { + HintType::L2BlockHeader + .with_data(&[block_hash.as_slice(), chain_id.to_be_bytes().as_ref()]) + .send(self.oracle.as_ref()) + .await?; + + let header_rlp = self + .oracle + .get(PreimageKey::new(*block_hash, PreimageKeyType::Keccak256)) + .await + .map_err(OracleProviderError::Preimage)?; + + Header::decode(&mut header_rlp.as_ref()).map_err(OracleProviderError::Rlp) + } + + /// Fetch the [OpReceiptEnvelope]s for the block with the given hash. + async fn derive_receipts( + &self, + chain_id: u64, + block_hash: B256, + header: &Header, + ) -> Result, ::Error> { + // Send a hint for the block's receipts, and walk through the receipts trie in the header to + // verify them. + HintType::L2Receipts + .with_data(&[block_hash.as_ref(), chain_id.to_be_bytes().as_slice()]) + .send(self.oracle.as_ref()) + .await?; + let trie_walker = OrderedListWalker::try_new_hydrated(header.receipts_root, self) + .map_err(OracleProviderError::TrieWalker)?; + + // Decode the receipts within the receipts trie. + let receipts = trie_walker + .into_iter() + .map(|(_, rlp)| { + let envelope = OpReceiptEnvelope::decode_2718(&mut rlp.as_ref())?; + Ok(envelope) + }) + .collect::, _>>() + .map_err(OracleProviderError::Rlp)?; + + Ok(receipts) + } +} + +#[async_trait] +impl InteropProvider for OracleInteropProvider +where + C: CommsClient + Send + Sync, +{ + type Error = OracleProviderError; + + /// Fetch a [Header] by its number. + async fn header_by_number(&self, chain_id: u64, number: u64) -> Result { + let Some(mut header) = + self.local_safe_heads.get(&chain_id).cloned().map(|h| h.into_inner()) + else { + return Err(PreimageOracleError::Other("Missing local safe header".to_string()).into()); + }; + + // Check if the block number is in range. If not, we can fail early. + if number > header.number { + return Err(OracleProviderError::BlockNumberPastHead(number, header.number)); + } + + // Set the chain ID for the trie hints, and explicitly drop the lock. + let mut chain_id_lock = self.chain_id.write(); + *chain_id_lock = Some(chain_id); + drop(chain_id_lock); + + // Walk back the block headers to the desired block number. + let rollup_config = self.boot.rollup_config(chain_id).ok_or_else(|| { + PreimageOracleError::Other("Missing rollup config for chain ID".to_string()) + })?; + let mut linear_fallback = false; + + while header.number > number { + if rollup_config.is_isthmus_active(header.timestamp) && !linear_fallback { + // If Isthmus is active, the EIP-2935 contract is used to perform leaping lookbacks + // through consulting the ring buffer within the contract. If this + // lookup fails for any reason, we fall back to linear walk back. + let block_hash = match eip_2935_history_lookup(&header, 0, self, self).await { + Ok(hash) => hash, + Err(_) => { + // If the EIP-2935 lookup fails for any reason, attempt fallback to linear + // walk back. + linear_fallback = true; + continue; + } + }; + + header = self.header_by_hash(chain_id, block_hash).await?; + } else { + // Walk back the block headers one-by-one until the desired block number is reached. + header = self.header_by_hash(chain_id, header.parent_hash).await?; + } + } + + Ok(header) + } + + /// Fetch all receipts for a given block by number. + async fn receipts_by_number( + &self, + chain_id: u64, + number: u64, + ) -> Result, Self::Error> { + let header = self.header_by_number(chain_id, number).await?; + self.derive_receipts(chain_id, header.hash_slow(), &header).await + } + + /// Fetch all receipts for a given block by hash. + async fn receipts_by_hash( + &self, + chain_id: u64, + block_hash: B256, + ) -> Result, Self::Error> { + let header = self.header_by_hash(chain_id, block_hash).await?; + self.derive_receipts(chain_id, block_hash, &header).await + } +} + +impl TrieProvider for OracleInteropProvider +where + C: CommsClient + Send + Sync + Clone, +{ + type Error = OracleProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + kona_proof::block_on(async move { + let trie_node_rlp = self + .oracle + .get(PreimageKey::new(*key, PreimageKeyType::Keccak256)) + .await + .map_err(OracleProviderError::Preimage)?; + TrieNode::decode(&mut trie_node_rlp.as_ref()).map_err(OracleProviderError::Rlp) + }) + } +} + +impl TrieHinter for OracleInteropProvider { + type Error = OracleProviderError; + + fn hint_trie_node(&self, hash: B256) -> Result<(), Self::Error> { + kona_proof::block_on(async move { + HintType::L2StateNode + .with_data(&[hash.as_slice()]) + .with_data( + self.chain_id.read().map_or_else(Vec::new, |id| id.to_be_bytes().to_vec()), + ) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_account_proof(&self, address: Address, block_number: u64) -> Result<(), Self::Error> { + kona_proof::block_on(async move { + HintType::L2AccountProof + .with_data(&[block_number.to_be_bytes().as_ref(), address.as_slice()]) + .with_data( + self.chain_id.read().map_or_else(Vec::new, |id| id.to_be_bytes().to_vec()), + ) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_storage_proof( + &self, + address: alloy_primitives::Address, + slot: alloy_primitives::U256, + block_number: u64, + ) -> Result<(), Self::Error> { + kona_proof::block_on(async move { + HintType::L2AccountStorageProof + .with_data(&[ + block_number.to_be_bytes().as_ref(), + address.as_slice(), + slot.to_be_bytes::<32>().as_ref(), + ]) + .with_data( + self.chain_id.read().map_or_else(Vec::new, |id| id.to_be_bytes().to_vec()), + ) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_execution_witness( + &self, + parent_hash: B256, + op_payload_attributes: &op_alloy_rpc_types_engine::OpPayloadAttributes, + ) -> Result<(), Self::Error> { + kona_proof::block_on(async move { + let encoded_attributes = + serde_json::to_vec(op_payload_attributes).map_err(OracleProviderError::Serde)?; + + HintType::L2PayloadWitness + .with_data(&[parent_hash.as_slice(), &encoded_attributes]) + .with_data( + self.chain_id.read().map_or_else(Vec::new, |id| id.to_be_bytes().to_vec()), + ) + .send(self.oracle.as_ref()) + .await + }) + } +} diff --git a/kona/crates/proof/proof/CHANGELOG.md b/kona/crates/proof/proof/CHANGELOG.md new file mode 100644 index 0000000000000..191bfc7556eef --- /dev/null +++ b/kona/crates/proof/proof/CHANGELOG.md @@ -0,0 +1,113 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.3](https://github.com/op-rs/kona/compare/kona-proof-v0.2.2...kona-proof-v0.2.3) - 2025-01-16 + +### Added + +- *(client)* Interop binary (#903) + +### Other + +- Update Maili Deps (#908) + +## [0.2.2](https://github.com/op-rs/kona/compare/kona-proof-v0.2.1...kona-proof-v0.2.2) - 2025-01-13 + +### Other + +- *(deps)* Replace dep `alloy-rs/op-alloy-registry`->`op-rs/maili-registry` (#892) +- *(deps)* Replace dep `alloy-rs/op-alloy-protocol`->`op-rs/maili-protocol` (#890) + +## [0.2.1](https://github.com/op-rs/kona/compare/kona-proof-v0.2.0...kona-proof-v0.2.1) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +## [0.2.0](https://github.com/op-rs/kona/compare/kona-proof-v0.1.0...kona-proof-v0.2.0) - 2024-12-03 + +### Added + +- *(driver)* wait for engine ([#851](https://github.com/op-rs/kona/pull/851)) +- *(client)* Invalidate impossibly old claims ([#852](https://github.com/op-rs/kona/pull/852)) +- *(driver)* refines the executor interface for the driver ([#850](https://github.com/op-rs/kona/pull/850)) +- *(workspace)* Isolate FPVM-specific platform code ([#821](https://github.com/op-rs/kona/pull/821)) + +### Fixed + +- bump ([#865](https://github.com/op-rs/kona/pull/865)) + +### Other + +- update deps and clean up misc features ([#864](https://github.com/op-rs/kona/pull/864)) +- *(host)* Hint Parsing Cleanup ([#844](https://github.com/op-rs/kona/pull/844)) +- *(derive)* remove indexed blob hash ([#847](https://github.com/op-rs/kona/pull/847)) +- L2ExecutePayloadProof Hint Type ([#832](https://github.com/op-rs/kona/pull/832)) + +## [0.0.1](https://github.com/op-rs/kona/releases/tag/kona-proof-v0.0.1) - 2024-11-20 + +### Added + +- *(workspace)* `kona-proof` ([#818](https://github.com/op-rs/kona/pull/818)) + +### Fixed + +- imports ([#829](https://github.com/op-rs/kona/pull/829)) + +### Other + +- op-alloy 0.6.8 ([#830](https://github.com/op-rs/kona/pull/830)) +- *(driver)* use tracing macros ([#823](https://github.com/op-rs/kona/pull/823)) + +## [0.0.4](https://github.com/op-rs/kona/compare/kona-common-v0.0.3...kona-common-v0.0.4) - 2024-10-25 + +### Added + +- remove thiserror ([#735](https://github.com/op-rs/kona/pull/735)) +- *(preimage/common)* Migrate to `thiserror` ([#543](https://github.com/op-rs/kona/pull/543)) + +### Fixed + +- *(workspace)* hoist and fix lints ([#577](https://github.com/op-rs/kona/pull/577)) + +### Other + +- re-org imports ([#711](https://github.com/op-rs/kona/pull/711)) +- *(preimage)* Test Coverage ([#634](https://github.com/op-rs/kona/pull/634)) +- test coverage for common ([#629](https://github.com/op-rs/kona/pull/629)) +- doc logos ([#609](https://github.com/op-rs/kona/pull/609)) +- *(workspace)* Allow stdlib in `cfg(test)` ([#548](https://github.com/op-rs/kona/pull/548)) + +## [0.0.3](https://github.com/op-rs/kona/compare/kona-common-v0.0.2...kona-common-v0.0.3) - 2024-09-04 + +### Added +- add zkvm target for io ([#394](https://github.com/op-rs/kona/pull/394)) + +### Other +- *(workspace)* Update for `op-rs` org transfer ([#474](https://github.com/op-rs/kona/pull/474)) +- *(workspace)* Hoist Dependencies ([#466](https://github.com/op-rs/kona/pull/466)) +- *(bin)* Remove `kt` ([#461](https://github.com/op-rs/kona/pull/461)) +- *(common)* Remove need for cursors in `NativeIO` ([#416](https://github.com/op-rs/kona/pull/416)) + +## [0.0.2](https://github.com/op-rs/kona/compare/kona-common-v0.0.1...kona-common-v0.0.2) - 2024-06-22 + +### Added +- *(client)* Derivation integration ([#257](https://github.com/op-rs/kona/pull/257)) +- *(client/host)* Oracle-backed Blob fetcher ([#255](https://github.com/op-rs/kona/pull/255)) +- *(host)* Host program scaffold ([#184](https://github.com/op-rs/kona/pull/184)) +- *(preimage)* `OracleServer` + `HintReader` ([#96](https://github.com/op-rs/kona/pull/96)) +- *(common)* Move from `RegisterSize` to native ptr size type ([#95](https://github.com/op-rs/kona/pull/95)) +- *(workspace)* Add `rustfmt.toml` + +### Fixed +- *(common)* Pipe IO support ([#282](https://github.com/op-rs/kona/pull/282)) + +### Other +- *(common)* Use `Box::leak` rather than `mem::forget` ([#180](https://github.com/op-rs/kona/pull/180)) +- Add simple blocking async executor ([#38](https://github.com/op-rs/kona/pull/38)) +- Make versions of packages independent ([#36](https://github.com/op-rs/kona/pull/36)) diff --git a/kona/crates/proof/proof/Cargo.toml b/kona/crates/proof/proof/Cargo.toml new file mode 100644 index 0000000000000..fd76c9d839e71 --- /dev/null +++ b/kona/crates/proof/proof/Cargo.toml @@ -0,0 +1,89 @@ +[package] +name = "kona-proof" +description = "OP Stack Proof SDK" +version = "0.3.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-mpt.workspace = true +kona-derive.workspace = true +kona-driver.workspace = true +kona-preimage.workspace = true +kona-executor.workspace = true +kona-protocol.workspace = true +kona-registry.workspace = true +kona-genesis = { workspace = true, features = ["serde"] } + +# Alloy +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-primitives.workspace = true +alloy-trie.workspace = true + +# Op Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types-engine = { workspace = true, features = ["serde"] } + +# Execution +op-revm.workspace = true +alloy-op-evm.workspace = true +alloy-evm = { workspace = true, features = ["op"] } + +# General +lru.workspace = true +spin.workspace = true +serde.workspace = true +tracing.workspace = true +serde_json.workspace = true +async-trait.workspace = true +thiserror.workspace = true +lazy_static.workspace = true + +# KZG +ark-ff.workspace = true +ark-bls12-381.workspace = true + +# `std` feature dependencies +tokio = { workspace = true, features = ["full"], optional = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +rstest.workspace = true +rand.workspace = true +c-kzg.workspace = true +rayon.workspace = true + +[features] +std = [ + "alloy-consensus/std", + "alloy-eips/std", + "alloy-evm/std", + "alloy-op-evm/std", + "alloy-primitives/std", + "alloy-rlp/std", + "alloy-trie/std", + "ark-bls12-381/std", + "ark-ff/std", + "dep:tokio", + "kona-genesis/std", + "kona-preimage/std", + "kona-protocol/std", + "kona-registry/std", + "op-alloy-consensus/std", + "op-alloy-rpc-types-engine/std", + "op-revm/std", + "serde/std", + "serde_json/std", + "spin/std", + "thiserror/std", + "tracing/std", +] diff --git a/kona/crates/proof/proof/README.md b/kona/crates/proof/proof/README.md new file mode 100644 index 0000000000000..e9aec8c680d32 --- /dev/null +++ b/kona/crates/proof/proof/README.md @@ -0,0 +1,8 @@ +# `kona-proof` + +CI +Kona Proof SDK +License +Codecov + +`kona-proof` is an OP Stack state transition proof SDK. diff --git a/kona/crates/proof/proof/src/blocking_runtime.rs b/kona/crates/proof/proof/src/blocking_runtime.rs new file mode 100644 index 0000000000000..9df7e4c46e730 --- /dev/null +++ b/kona/crates/proof/proof/src/blocking_runtime.rs @@ -0,0 +1,64 @@ +//! This module contains a blocking runtime for futures, allowing for synchronous execution of async +//! code in an embedded environment. + +use core::future::Future; + +/// This function blocks on a future in place until it is ready. +#[cfg(feature = "std")] +pub fn block_on(f: impl Future) -> T { + // When running with Tokio, use the appropriate blocking mechanism + if let Ok(runtime) = tokio::runtime::Handle::try_current() { + tokio::task::block_in_place(|| runtime.block_on(f)) + } else { + // Fallback to tokio's block_on if we're not in a runtime + tokio::runtime::Runtime::new().unwrap().block_on(f) + } +} + +/// This function busy waits on a future until it is ready. It uses a no-op waker to poll the future +/// in a thread-blocking loop. +#[cfg(not(feature = "std"))] +pub fn block_on(f: impl Future) -> T { + use alloc::boxed::Box; + use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + let mut f = Box::pin(f); + + // Construct a no-op waker. + fn noop_clone(_: *const ()) -> RawWaker { + noop_raw_waker() + } + const fn noop(_: *const ()) {} + fn noop_raw_waker() -> RawWaker { + let vtable = &RawWakerVTable::new(noop_clone, noop, noop, noop); + RawWaker::new(core::ptr::null(), vtable) + } + let waker = unsafe { Waker::from_raw(noop_raw_waker()) }; + let mut context = Context::from_waker(&waker); + + loop { + // Safety: This is safe because we only poll the future once per loop iteration, + // and we do not move the future after pinning it. + if let Poll::Ready(v) = f.as_mut().poll(&mut context) { + return v; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::future::ready; + + #[test] + fn test_block_on() { + let f = async { 42 }; + assert_eq!(block_on(f), 42); + } + + #[test] + fn test_block_on_ready() { + let f = ready(42); + assert_eq!(block_on(f), 42); + } +} diff --git a/kona/crates/proof/proof/src/boot.rs b/kona/crates/proof/proof/src/boot.rs new file mode 100644 index 0000000000000..d1a72e1886c98 --- /dev/null +++ b/kona/crates/proof/proof/src/boot.rs @@ -0,0 +1,279 @@ +//! This module contains the prologue phase of the client program, pulling in the boot information +//! through the `PreimageOracle` ABI as local keys. + +use crate::errors::OracleProviderError; +use alloy_primitives::{B256, U256}; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_preimage::{PreimageKey, PreimageOracleClient}; +use kona_registry::{L1_CONFIGS, ROLLUP_CONFIGS}; +use serde::{Deserialize, Serialize}; + +/// The local key identifier for the L1 head hash. +/// +/// This key is used to retrieve the L1 block hash that contains all the data +/// necessary to derive the disputed L2 blocks. The L1 head serves as the +/// starting point for L1 data extraction during the derivation process. +pub const L1_HEAD_KEY: U256 = U256::from_be_slice(&[1]); + +/// The local key identifier for the agreed L2 output root. +/// +/// This key retrieves the baseline L2 output root that both parties agree upon. +/// It represents the last known good state before the disputed blocks and serves +/// as the starting point for derivation verification. +pub const L2_OUTPUT_ROOT_KEY: U256 = U256::from_be_slice(&[2]); + +/// The local key identifier for the disputed L2 output root claim. +/// +/// This key retrieves the user's claimed L2 output root at the target block. +/// The fault proof will compare the derived output root against this claim +/// to determine if the claim is valid or invalid. +pub const L2_CLAIM_KEY: U256 = U256::from_be_slice(&[3]); + +/// The local key identifier for the disputed L2 block number. +/// +/// This key retrieves the L2 block number at which the output root disagreement +/// occurs. The derivation process will produce blocks up to this number to +/// verify the claim. +pub const L2_CLAIM_BLOCK_NUMBER_KEY: U256 = U256::from_be_slice(&[4]); + +/// The local key identifier for the L2 chain ID. +/// +/// This key retrieves the L2 network identifier, which is used to load the +/// appropriate rollup configuration and ensure network-specific validation +/// rules are applied correctly. +pub const L2_CHAIN_ID_KEY: U256 = U256::from_be_slice(&[5]); + +/// The local key identifier for the L2 rollup configuration. +/// +/// This key is used as a fallback to retrieve the rollup configuration from +/// the preimage oracle when no hardcoded configuration is available for the +/// given chain ID. Oracle-loaded configs require additional validation. +pub const L2_ROLLUP_CONFIG_KEY: U256 = U256::from_be_slice(&[6]); + +/// The local key identifier for the L1 chain configuration. +/// +/// This key is used as a fallback to retrieve the chain configuration from +/// the preimage oracle when no hardcoded configuration is available for the +/// given chain ID. Oracle-loaded configs require additional validation. +pub const L1_CONFIG_KEY: U256 = U256::from_be_slice(&[7]); + +/// The boot information for the client program. +/// +/// [`BootInfo`] contains all the essential parameters needed to initialize the fault proof +/// client program. It separates verified inputs (cryptographically committed) from user +/// inputs (requiring validation through derivation). +/// +/// This structure is loaded during the prologue phase from the preimage oracle and +/// establishes the initial state for the fault proof computation. +/// +/// # Security Model +/// The boot information follows a two-tier security model: +/// - **Verified inputs**: Committed by the fault proof system, trusted +/// - **User inputs**: Provided by the claimant, must be verified through execution +/// +/// # Usage in Fault Proof +/// 1. Load boot info from preimage oracle during prologue +/// 2. Initialize derivation pipeline with verified L1 head and safe L2 output +/// 3. Derive L2 blocks up to the claimed block number +/// 4. Compare derived output root with user's claim +/// 5. Proof succeeds if outputs match, fails otherwise +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BootInfo { + /// The L1 head hash containing safe L2 chain data for reproduction. + /// + /// This hash identifies the L1 block that contains all the data necessary + /// to derive the L2 chain up to the disputed block. It serves as the + /// starting point for L1 data extraction during derivation. + /// + /// **Security**: Verified input committed by the fault proof system. + pub l1_head: B256, + /// The agreed upon safe L2 output root. + /// + /// This represents the last known good L2 state that both parties agree upon. + /// It serves as the starting point for derivation and the baseline against + /// which the disputed claim is evaluated. + /// + /// **Security**: Verified input committed by the fault proof system. + pub agreed_l2_output_root: B256, + /// The disputed L2 output root claim. + /// + /// This is the user's claim about what the L2 output root should be at the + /// target block number. The fault proof will derive the actual output root + /// and compare it against this claim to determine validity. + /// + /// **Security**: User-submitted input requiring verification. + pub claimed_l2_output_root: B256, + /// The L2 block number being disputed. + /// + /// This specifies the target L2 block number at which the output root + /// disagreement occurs. The derivation process will produce blocks up to + /// this number and compute the resulting output root. + /// + /// **Security**: User-submitted input requiring verification. + pub claimed_l2_block_number: u64, + /// The L2 chain identifier. + /// + /// Used to identify which L2 network this proof applies to and to load + /// the appropriate rollup configuration. This prevents cross-chain + /// replay attacks and ensures proper network-specific validation. + /// + /// **Security**: Verified input committed by the fault proof system. + pub chain_id: u64, + /// The rollup configuration for the L2 chain. + /// + /// Contains all the network-specific parameters needed for proper L2 block + /// derivation, including genesis configuration, system addresses, gas limits, + /// and hard fork activation heights. + /// + /// **Security**: Loaded from registry (secure) or oracle (requires validation). + pub rollup_config: RollupConfig, + /// An optional configuration for the l1 chain associated with the l2 chain. + /// + /// **Security**: Loaded from registry (secure) or oracle (requires validation). + pub l1_config: L1ChainConfig, +} + +impl BootInfo { + /// Load the boot information from the preimage oracle. + /// + /// This method retrieves all the necessary boot parameters from the preimage oracle + /// using predefined local keys. It handles both verified inputs (from the fault proof + /// system) and user-submitted inputs that need validation. + /// + /// # Arguments + /// * `oracle` - The preimage oracle client for reading boot data + /// + /// # Returns + /// * `Ok(BootInfo)` - Successfully loaded and validated boot information + /// * `Err(OracleProviderError)` - Failed to load or parse boot information + /// + /// # Errors + /// This method can fail due to: + /// - **Preimage errors**: Oracle communication failures or missing keys + /// - **Slice conversion errors**: Invalid data format for numeric values + /// - **Serde errors**: Failed to deserialize rollup configuration + /// - **Missing data**: Required boot parameters not available in oracle + /// + /// # Loading Process + /// The method loads boot information in this order: + /// 1. **L1 Head Hash** (`L1_HEAD_KEY`): The L1 block containing safe L2 data + /// 2. **Agreed L2 Output Root** (`L2_OUTPUT_ROOT_KEY`): Known safe L2 state + /// 3. **Claimed L2 Output Root** (`L2_CLAIM_KEY`): User's disputed claim + /// 4. **Claimed Block Number** (`L2_CLAIM_BLOCK_NUMBER_KEY`): Target block height + /// 5. **Chain ID** (`L2_CHAIN_ID_KEY`): L2 network identifier + /// 6. **Rollup Config**: Either from registry (secure) or oracle (fallback) + /// + /// # Rollup Configuration Loading + /// The rollup configuration is loaded with a security preference: + /// - **Primary**: Lookup in hardcoded [`static@ROLLUP_CONFIGS`] registry by chain ID + /// - **Fallback**: Load from oracle using `L2_ROLLUP_CONFIG_KEY` (with warning) + /// + /// The fallback method requires additional validation in production environments + /// as oracle-provided configs are not verified by the fault proof system. + /// + /// # Security Considerations + /// - Verified inputs are cryptographically committed in the fault proof + /// - User inputs (claim data) require verification through the derivation process + /// - Oracle-loaded configs should be validated against known good configurations + /// + /// # Example Usage + /// ```rust,ignore + /// let boot_info = BootInfo::load(&oracle).await?; + /// println!("Loaded boot info for chain {}", boot_info.chain_id); + /// println!("Target block: {}", boot_info.claimed_l2_block_number); + /// ``` + pub async fn load(oracle: &O) -> Result + where + O: PreimageOracleClient + Send, + { + let mut l1_head: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L1_HEAD_KEY.to()), l1_head.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_output_root: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_OUTPUT_ROOT_KEY.to()), l2_output_root.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let mut l2_claim: B256 = B256::ZERO; + oracle + .get_exact(PreimageKey::new_local(L2_CLAIM_KEY.to()), l2_claim.as_mut()) + .await + .map_err(OracleProviderError::Preimage)?; + + let l2_claim_block = u64::from_be_bytes( + oracle + .get(PreimageKey::new_local(L2_CLAIM_BLOCK_NUMBER_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)? + .as_slice() + .try_into() + .map_err(OracleProviderError::SliceConversion)?, + ); + let chain_id = u64::from_be_bytes( + oracle + .get(PreimageKey::new_local(L2_CHAIN_ID_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)? + .as_slice() + .try_into() + .map_err(OracleProviderError::SliceConversion)?, + ); + + // Attempt to load the rollup config from the chain ID. If there is no config for the chain, + // fall back to loading the config from the preimage oracle. + let rollup_config = if let Some(config) = ROLLUP_CONFIGS.get(&chain_id) { + config.clone() + } else { + warn!( + target: "boot_loader", + "No rollup config found for chain ID {}, falling back to preimage oracle. This is insecure in production without additional validation!", + chain_id + ); + let ser_cfg = oracle + .get(PreimageKey::new_local(L2_ROLLUP_CONFIG_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)?; + serde_json::from_slice(&ser_cfg).map_err(OracleProviderError::Serde)? + }; + + // Attempt to load the rollup config from the chain ID. If there is no config for the chain, + // fall back to loading the config from the preimage oracle. + let l1_config = if let Some(config) = L1_CONFIGS.get(&rollup_config.l1_chain_id) { + config.clone() + } else { + warn!( + target: "boot_loader", + "No l1 config found for chain ID {}, falling back to preimage oracle. This is insecure in production without additional validation!", + rollup_config.l1_chain_id + ); + let ser_cfg = oracle + .get(PreimageKey::new_local(L1_CONFIG_KEY.to())) + .await + .map_err(OracleProviderError::Preimage)?; + + serde_json::from_slice(&ser_cfg).map_err(OracleProviderError::Serde)? + }; + + debug!( + target: "boot_loader", + l1_head = %l1_head, + chain_id = chain_id, + claimed_l2_block_number = l2_claim_block, + "Successfully loaded boot information" + ); + + Ok(Self { + l1_head, + agreed_l2_output_root: l2_output_root, + claimed_l2_output_root: l2_claim, + claimed_l2_block_number: l2_claim_block, + chain_id, + rollup_config, + l1_config, + }) + } +} diff --git a/kona/crates/proof/proof/src/caching_oracle.rs b/kona/crates/proof/proof/src/caching_oracle.rs new file mode 100644 index 0000000000000..25e437fabe50e --- /dev/null +++ b/kona/crates/proof/proof/src/caching_oracle.rs @@ -0,0 +1,112 @@ +//! Contains the [CachingOracle], which is a wrapper around an [OracleReader] and [HintWriter] that +//! stores a configurable number of responses in an [LruCache] for quick retrieval. +//! +//! [OracleReader]: kona_preimage::OracleReader +//! [HintWriter]: kona_preimage::HintWriter + +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use async_trait::async_trait; +use core::num::NonZeroUsize; +use kona_preimage::{ + HintWriterClient, PreimageKey, PreimageOracleClient, errors::PreimageOracleResult, +}; +use lru::LruCache; +use spin::Mutex; + +/// A wrapper around an [OracleReader] and [HintWriter] that stores a configurable number of +/// responses in an [LruCache] for quick retrieval. +/// +/// [OracleReader]: kona_preimage::OracleReader +/// [HintWriter]: kona_preimage::HintWriter +#[allow(unreachable_pub)] +#[derive(Debug, Clone)] +pub struct CachingOracle +where + OR: PreimageOracleClient, + HW: HintWriterClient, +{ + /// The spin-locked cache that stores the responses from the oracle. + cache: Arc>>>, + /// Oracle reader type. + oracle_reader: OR, + /// Hint writer type. + hint_writer: HW, +} + +impl CachingOracle +where + OR: PreimageOracleClient, + HW: HintWriterClient, +{ + /// Creates a new [CachingOracle] that wraps the given [OracleReader] and stores up to `N` + /// responses in the cache. + /// + /// [OracleReader]: kona_preimage::OracleReader + pub fn new(cache_size: usize, oracle_reader: OR, hint_writer: HW) -> Self { + Self { + cache: Arc::new(Mutex::new(LruCache::new( + NonZeroUsize::new(cache_size).expect("N must be greater than 0"), + ))), + oracle_reader, + hint_writer, + } + } +} + +/// A trait that provides a method to flush a cache. +pub trait FlushableCache { + /// Flushes the cache, removing all entries. + fn flush(&self); +} + +impl FlushableCache for CachingOracle +where + OR: PreimageOracleClient, + HW: HintWriterClient, +{ + /// Flushes the cache, removing all entries. + fn flush(&self) { + self.cache.lock().clear(); + } +} + +#[async_trait] +impl PreimageOracleClient for CachingOracle +where + OR: PreimageOracleClient + Sync, + HW: HintWriterClient + Sync, +{ + async fn get(&self, key: PreimageKey) -> PreimageOracleResult> { + if let Some(value) = self.cache.lock().get(&key) { + Ok(value.clone()) + } else { + let value = self.oracle_reader.get(key).await?; + self.cache.lock().put(key, value.clone()); + Ok(value) + } + } + + async fn get_exact(&self, key: PreimageKey, buf: &mut [u8]) -> PreimageOracleResult<()> { + if let Some(value) = self.cache.lock().get(&key) { + // SAFETY: The value never enters the cache unless the preimage length matches the + // buffer length, due to the checks in the OracleReader. + buf.copy_from_slice(value.as_slice()); + Ok(()) + } else { + self.oracle_reader.get_exact(key, buf).await?; + self.cache.lock().put(key, buf.to_vec()); + Ok(()) + } + } +} + +#[async_trait] +impl HintWriterClient for CachingOracle +where + OR: PreimageOracleClient + Sync, + HW: HintWriterClient + Sync, +{ + async fn write(&self, hint: &str) -> PreimageOracleResult<()> { + self.hint_writer.write(hint).await + } +} diff --git a/kona/crates/proof/proof/src/eip2935.rs b/kona/crates/proof/proof/src/eip2935.rs new file mode 100644 index 0000000000000..f0a05f129bef5 --- /dev/null +++ b/kona/crates/proof/proof/src/eip2935.rs @@ -0,0 +1,168 @@ +//! EIP-2935 history lookup utilities. + +use crate::errors::OracleProviderError; +use alloc::string::ToString; +use alloy_consensus::Header; +use alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS; +use alloy_primitives::{B256, U256, b256, keccak256}; +use alloy_rlp::Decodable; +use alloy_trie::TrieAccount; +use kona_mpt::{Nibbles, TrieHinter, TrieNode, TrieNodeError, TrieProvider}; +use kona_preimage::errors::PreimageOracleError; + +/// The [`keccak256`] hash of the address of the EIP-2935 history storage contract. +const HASHED_HISTORY_STORAGE_ADDRESS: B256 = + b256!("6c9d57be05dd69371c4dd2e871bce6e9f4124236825bb612ee18a45e5675be51"); + +/// The number of blocks that the EIP-2935 contract serves historical block hashes for. (8192 - 1) +const HISTORY_SERVE_WINDOW: u64 = 2u64.pow(13) - 1; + +/// Performs a historical block hash lookup using the EIP-2935 contract. If the block number is out +/// of bounds of the history lookup window size, the oldest block hash within the window is +/// returned. +pub async fn eip_2935_history_lookup( + header: &Header, + block_number: u64, + provider: &P, + hinter: &H, +) -> Result +where + P: TrieProvider, + H: TrieHinter, +{ + // Compute the storage slot for the block hash. If the distance between the current header and + // the desired block number is within the window size, we compute the slot based on the + // target block number, as the result is present within the ring. Otherwise, we return the + // oldest block in the window. + let slot = if header.number.saturating_sub(block_number) <= HISTORY_SERVE_WINDOW { + block_number + } else { + header.number + } % HISTORY_SERVE_WINDOW; + + // Send a hint to fetch the storage slot proof prior to traversing the state / account tries. + hinter + .hint_storage_proof(HISTORY_STORAGE_ADDRESS, U256::from(slot), header.number) + .map_err(|e| PreimageOracleError::Other(e.to_string()))?; + + // Fetch the trie account for the history accumulator. + let mut state_trie = TrieNode::new_blinded(header.state_root); + let account_key = Nibbles::unpack(HASHED_HISTORY_STORAGE_ADDRESS); + let raw_account = state_trie.open(&account_key, provider)?.ok_or(TrieNodeError::KeyNotFound)?; + let account = + TrieAccount::decode(&mut raw_account.as_ref()).map_err(OracleProviderError::Rlp)?; + + // Fetch the storage slot value from the account. + let mut storage_trie = TrieNode::new_blinded(account.storage_root); + let slot_key = Nibbles::unpack(keccak256(U256::from(slot).to_be_bytes::<32>())); + let slot_value = storage_trie.open(&slot_key, provider)?.ok_or(TrieNodeError::KeyNotFound)?; + + B256::decode(&mut slot_value.as_ref()).map_err(OracleProviderError::Rlp) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::{vec, vec::Vec}; + use alloy_primitives::Bytes; + use alloy_rlp::Encodable; + use alloy_trie::{HashBuilder, proof::ProofRetainer}; + use kona_mpt::NoopTrieHinter; + use kona_registry::HashMap; + use rstest::rstest; + + // Mock TrieProvider implementation for testing EIP-2935 history lookup + #[derive(Default, Clone)] + struct MockTrieProvider { + pub(crate) state_root: B256, + pub(crate) nodes: HashMap, + } + + impl MockTrieProvider { + pub(crate) fn new(ring_index: u64, block_hash: B256) -> Self { + let (storage_root, storage_proof) = { + let slot_key = + Nibbles::unpack(keccak256(U256::from(ring_index).to_be_bytes::<32>())); + let mut storage_hb = + HashBuilder::default().with_proof_retainer(ProofRetainer::new(vec![slot_key])); + + let mut encoded = Vec::with_capacity(block_hash.length()); + block_hash.encode(&mut encoded); + storage_hb.add_leaf(slot_key, encoded.as_slice()); + + let storage_root = storage_hb.root(); + let storage_proof = storage_hb.take_proof_nodes(); + (storage_root, storage_proof) + }; + + let (state_root, state_proof) = { + let account_key = Nibbles::unpack(HASHED_HISTORY_STORAGE_ADDRESS); + let mut state_hb = HashBuilder::default() + .with_proof_retainer(ProofRetainer::new(vec![account_key])); + + let account = + TrieAccount { storage_root, code_hash: keccak256(""), ..Default::default() }; + let mut encoded = Vec::with_capacity(account.length()); + account.encode(&mut encoded); + state_hb.add_leaf(account_key, encoded.as_slice()); + + let state_root = state_hb.root(); + let state_proof = state_hb.take_proof_nodes(); + (state_root, state_proof) + }; + + let nodes = storage_proof + .values() + .chain(state_proof.values()) + .cloned() + .map(|v| (keccak256(v.as_ref()), v)) + .collect::>(); + + Self { state_root, nodes } + } + } + + impl TrieProvider for MockTrieProvider { + type Error = OracleProviderError; + + fn trie_node_by_hash(&self, hash: B256) -> Result { + self.nodes + .get(&hash) + .cloned() + .map(|bytes| TrieNode::decode(&mut bytes.as_ref()).expect("valid node")) + .ok_or(OracleProviderError::TrieNode(TrieNodeError::KeyNotFound)) + } + } + + #[rstest] + #[case::block_number_in_window(1000, 999)] + #[case::block_number_outside_window(9000, 100)] + #[case::block_number_at_window_boundary(8192 * 2, 8192)] + #[case::block_number_at_window_boundary_plus_one(8192 * 2, 8191)] + #[tokio::test] + async fn test_eip_2935_history_lookup( + #[case] head_block_number: u64, + #[case] target_block_number: u64, + ) { + let expected_hash = B256::from([0xFF; 32]); + let ring_index = if head_block_number - target_block_number <= HISTORY_SERVE_WINDOW { + target_block_number + } else { + head_block_number + } % HISTORY_SERVE_WINDOW; + + let provider = MockTrieProvider::new(ring_index, expected_hash); + let header = Header { + number: head_block_number, + state_root: provider.state_root, + ..Default::default() + }; + + let result = + eip_2935_history_lookup(&header, target_block_number, &provider, &NoopTrieHinter) + .await + .unwrap(); + + assert_eq!(result, expected_hash); + } +} diff --git a/kona/crates/proof/proof/src/errors.rs b/kona/crates/proof/proof/src/errors.rs new file mode 100644 index 0000000000000..6956e0280b312 --- /dev/null +++ b/kona/crates/proof/proof/src/errors.rs @@ -0,0 +1,137 @@ +//! Error types for the proof program. +//! +//! This module defines error types used throughout the proof system, including +//! oracle provider errors and hint parsing errors. These errors provide detailed +//! context about failures during proof generation and data retrieval. + +use alloc::string::{String, ToString}; +use kona_derive::{PipelineError, PipelineErrorKind}; +use kona_mpt::{OrderedListWalkerError, TrieNodeError}; +use kona_preimage::errors::PreimageOracleError; +use kona_protocol::{FromBlockError, OpBlockConversionError}; +use thiserror::Error; + +/// Error from an oracle-backed provider. +/// +/// [`OracleProviderError`] represents various failure modes when interacting with +/// oracle-backed data providers. These errors can occur during preimage retrieval, +/// data parsing, or when validating oracle responses. +/// +/// # Error Categories +/// - **Data Availability**: Block numbers beyond chain head, missing preimages +/// - **Communication**: Oracle communication failures, timeouts +/// - **Data Integrity**: Malformed trie data, invalid RLP encoding +/// - **Conversion**: Type conversion failures, slice length mismatches +/// - **Network**: Unknown chain IDs, unsupported configurations +#[derive(Error, Debug)] +pub enum OracleProviderError { + /// Requested block number is past the current chain head. + /// + /// This error occurs when attempting to access block data for a block number + /// that exceeds the highest block available in the chain. It typically indicates + /// that the requested block has not been produced yet or the chain data is stale. + /// + /// # Arguments + /// * `0` - The requested block number + /// * `1` - The current chain head block number + #[error("Block number ({0}) past chain head ({_1})")] + BlockNumberPastHead(u64, u64), + /// Preimage oracle communication or data retrieval error. + /// + /// This error wraps underlying oracle communication failures, including + /// network timeouts, missing preimage keys, or invalid preimage data. + /// It's the most common error type when oracle operations fail. + #[error("Preimage oracle error: {0}")] + Preimage(#[from] PreimageOracleError), + /// Ordered list walker error during trie traversal. + /// + /// This error occurs when walking through ordered lists in Merkle Patricia + /// tries fails due to malformed data, incorrect proofs, or missing nodes. + /// It typically indicates corrupted trie data or invalid proof structures. + #[error("Trie walker error: {0}")] + TrieWalker(#[from] OrderedListWalkerError), + /// Trie node parsing or validation error. + /// + /// This error occurs when processing individual trie nodes fails due to + /// invalid node structure, incorrect hashing, or malformed node data. + /// It indicates fundamental issues with the trie data integrity. + #[error("Trie node error: {0}")] + TrieNode(#[from] TrieNodeError), + /// Block information extraction or conversion error. + /// + /// This error occurs when converting raw block data into structured + /// [`kona_protocol::BlockInfo`] objects fails due to missing fields, invalid data + /// formats, or unsupported block versions. + #[error("From block error: {0}")] + BlockInfo(FromBlockError), + /// OP Stack specific block conversion error. + /// + /// This error occurs when converting between different OP Stack block + /// formats fails due to incompatible data structures, missing OP-specific + /// fields, or version mismatches between block formats. + #[error("Op block conversion error: {0}")] + OpBlockConversion(OpBlockConversionError), + /// RLP (Recursive Length Prefix) encoding or decoding error. + /// + /// This error occurs when parsing or encoding RLP data fails due to + /// malformed input, invalid length prefixes, or unsupported data types. + /// RLP is used extensively for Ethereum data serialization. + #[error("RLP error: {0}")] + Rlp(alloy_rlp::Error), + /// Slice to array conversion error. + /// + /// This error occurs when attempting to convert a byte slice to a fixed-size + /// array fails due to length mismatches. It typically happens when parsing + /// hash values or other fixed-length data from variable-length sources. + #[error("Slice conversion error: {0}")] + SliceConversion(core::array::TryFromSliceError), + /// JSON serialization or deserialization error. + /// + /// This error occurs when parsing JSON data (e.g., rollup configurations) + /// fails due to invalid JSON syntax, missing required fields, or type + /// mismatches between expected and actual data structures. + #[error("Serde error: {0}")] + Serde(serde_json::Error), + /// Unknown or unsupported chain ID. + /// + /// This error occurs when encountering a chain ID that is not recognized + /// by the system. It typically happens when trying to load rollup + /// configurations for networks that are not supported or configured. + /// + /// # Argument + /// * `0` - The unknown chain ID that was encountered + #[error("Unknown chain ID: {0}")] + UnknownChainId(u64), +} + +impl From for PipelineErrorKind { + fn from(val: OracleProviderError) -> Self { + match val { + OracleProviderError::BlockNumberPastHead(_, _) => PipelineError::EndOfSource.crit(), + _ => PipelineError::Provider(val.to_string()).crit(), + } + } +} + +/// Error parsing a hint from string format. +/// +/// [`HintParsingError`] occurs when attempting to parse a hint string fails due to +/// invalid format, unknown hint types, or malformed hint data. Hints are expected +/// to follow the format ` ` where data is hex-encoded. +/// +/// # Common Causes +/// - Invalid hint format (wrong number of parts, missing data) +/// - Unknown hint type strings that don't map to [`crate::HintType`] variants +/// - Malformed hex encoding in hint data +/// - Empty or malformed hint strings +/// +/// # Example Error Scenarios +/// ```text +/// "invalid-hint-type 0x1234" // Unknown hint type +/// "l1-block-header" // Missing hint data +/// "l1-block-header invalid-hex" // Invalid hex encoding +/// "too many parts here" // Wrong format +/// ``` +#[derive(Error, Debug)] +#[error("Hint parsing error: {_0}")] +pub struct HintParsingError(pub String); diff --git a/kona/crates/proof/proof/src/executor.rs b/kona/crates/proof/proof/src/executor.rs new file mode 100644 index 0000000000000..3745c59c8bd77 --- /dev/null +++ b/kona/crates/proof/proof/src/executor.rs @@ -0,0 +1,105 @@ +//! An executor constructor. + +use alloc::boxed::Box; +use alloy_consensus::{Header, Sealed}; +use alloy_evm::{EvmFactory, FromRecoveredTx, FromTxWithEncoded, revm::context::BlockEnv}; +use alloy_op_evm::block::OpTxEnv; +use alloy_primitives::B256; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_driver::Executor; +use kona_executor::{BlockBuildingOutcome, StatelessL2Builder, TrieDBProvider}; +use kona_genesis::RollupConfig; +use kona_mpt::TrieHinter; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use op_revm::OpSpecId; + +/// An executor wrapper type. +#[derive(Debug)] +pub struct KonaExecutor<'a, P, H, Evm> +where + P: TrieDBProvider + Send + Sync + Clone, + H: TrieHinter + Send + Sync + Clone, + Evm: EvmFactory + Send + Sync + Clone, +{ + /// The rollup config for the executor. + rollup_config: &'a RollupConfig, + /// The trie provider for the executor. + trie_provider: P, + /// The trie hinter for the executor. + trie_hinter: H, + /// The evm factory for the executor. + evm_factory: Evm, + /// The executor. + inner: Option>, +} + +impl<'a, P, H, Evm> KonaExecutor<'a, P, H, Evm> +where + P: TrieDBProvider + Send + Sync + Clone, + H: TrieHinter + Send + Sync + Clone, + Evm: EvmFactory + Send + Sync + Clone, +{ + /// Creates a new executor. + pub const fn new( + rollup_config: &'a RollupConfig, + trie_provider: P, + trie_hinter: H, + evm_factory: Evm, + inner: Option>, + ) -> Self { + Self { rollup_config, trie_provider, trie_hinter, evm_factory, inner } + } +} + +#[async_trait] +impl Executor for KonaExecutor<'_, P, H, Evm> +where + P: TrieDBProvider + Debug + Send + Sync + Clone, + H: TrieHinter + Debug + Send + Sync + Clone, + Evm: EvmFactory + Send + Sync + Clone + 'static, + ::Tx: + FromTxWithEncoded + FromRecoveredTx + OpTxEnv, +{ + type Error = kona_executor::ExecutorError; + + /// Waits for the executor to be ready. + async fn wait_until_ready(&mut self) { + /* no-op for the kona executor */ + /* This is used when an engine api is used instead of a stateless block executor */ + } + + /// Updates the safe header. + /// + /// Since the L2 block executor is stateless, on an update to the safe head, + /// a new executor is created with the updated header. + fn update_safe_head(&mut self, header: Sealed
) { + self.inner = Some(StatelessL2Builder::new( + self.rollup_config, + self.evm_factory.clone(), + self.trie_provider.clone(), + self.trie_hinter.clone(), + header, + )); + } + + /// Execute the given payload attributes. + async fn execute_payload( + &mut self, + attributes: OpPayloadAttributes, + ) -> Result { + self.inner.as_mut().map_or_else( + || Err(kona_executor::ExecutorError::MissingExecutor), + |e| e.build_block(attributes), + ) + } + + /// Computes the output root. + fn compute_output_root(&mut self) -> Result { + self.inner.as_mut().map_or_else( + || Err(kona_executor::ExecutorError::MissingExecutor), + |e| e.compute_output_root(), + ) + } +} diff --git a/kona/crates/proof/proof/src/hint.rs b/kona/crates/proof/proof/src/hint.rs new file mode 100644 index 0000000000000..6291a03746667 --- /dev/null +++ b/kona/crates/proof/proof/src/hint.rs @@ -0,0 +1,177 @@ +//! This module contains the [HintType] enum. + +use crate::errors::{HintParsingError, OracleProviderError}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use alloy_primitives::{Bytes, hex}; +use core::{fmt::Display, str::FromStr}; +use kona_preimage::HintWriterClient; + +/// A [Hint] is parsed in the format ` `, where `` is a string that +/// represents the type of hint, and `` is the data associated with the hint (bytes +/// encoded as hex UTF-8). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Hint { + /// The type of hint. + pub ty: HT, + /// The data associated with the hint. + pub data: Bytes, +} + +impl Hint +where + HT: Display, +{ + /// Creates a new [Hint] with the specified type and data. + pub fn new>(ty: HT, data: T) -> Self { + Self { ty, data: data.into() } + } + + /// Splits the [Hint] into its components. + pub fn split(self) -> (HT, Bytes) { + (self.ty, self.data) + } + + /// Appends more data to [Hint::data]. + pub fn with_data>(self, data: T) -> Self { + // No-op if the data is empty. + if data.as_ref().is_empty() { + return self; + } + + let mut hint_data = Vec::with_capacity(self.data.len() + data.as_ref().len()); + hint_data.extend_from_slice(self.data.as_ref()); + hint_data.extend_from_slice(data.as_ref()); + + Self { data: hint_data.into(), ..self } + } + + /// Sends the hint to the passed [HintWriterClient]. + pub async fn send(&self, comms: &T) -> Result<(), OracleProviderError> { + comms.write(&self.encode()).await.map_err(OracleProviderError::Preimage) + } + + /// Encodes the hint as a string. + pub fn encode(&self) -> String { + alloc::format!("{} {}", self.ty, self.data) + } +} + +impl FromStr for Hint +where + HT: FromStr, +{ + type Err = HintParsingError; + + fn from_str(s: &str) -> Result { + let mut parts = s.split(' ').collect::>(); + + if parts.len() != 2 { + return Err(HintParsingError(alloc::format!("Invalid hint format: {s}"))); + } + + let hint_type = parts.remove(0).parse::()?; + let hint_data = + hex::decode(parts.remove(0)).map_err(|e| HintParsingError(e.to_string()))?.into(); + + Ok(Self { ty: hint_type, data: hint_data }) + } +} + +/// The [HintType] enum is used to specify the type of hint that was received. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum HintType { + /// A hint that specifies the block header of a layer 1 block. + L1BlockHeader, + /// A hint that specifies the transactions of a layer 1 block. + L1Transactions, + /// A hint that specifies the state node of a layer 1 block. + L1Receipts, + /// A hint that specifies a blob in the layer 1 beacon chain. + L1Blob, + /// A hint that specifies a precompile call on layer 1. + L1Precompile, + /// A hint that specifies the block header of a layer 2 block. + L2BlockHeader, + /// A hint that specifies the transactions of a layer 2 block. + L2Transactions, + /// A hint that specifies the code of a contract on layer 2. + L2Code, + /// A hint that specifies the preimage of the starting L2 output root on layer 2. + StartingL2Output, + /// A hint that specifies the state node in the L2 state trie. + L2StateNode, + /// A hint that specifies the proof on the path to an account in the L2 state trie. + L2AccountProof, + /// A hint that specifies the proof on the path to a storage slot in an account within in the + /// L2 state trie. + L2AccountStorageProof, + /// A hint that specifies bulk storage of all the code, state and keys generated by an + /// execution witness. + L2PayloadWitness, +} + +impl HintType { + /// Creates a new [Hint] from `self` and the specified data. The data passed will be + /// concatenated into a single byte array before being stored in the resulting [Hint]. + pub fn with_data(self, data: &[&[u8]]) -> Hint { + let total_len = data.iter().map(|d| d.len()).sum(); + let hint_data = data.iter().fold(Vec::with_capacity(total_len), |mut acc, d| { + acc.extend_from_slice(d); + acc + }); + Hint::new(self, hint_data) + } +} + +impl FromStr for HintType { + type Err = HintParsingError; + + fn from_str(value: &str) -> Result { + match value { + "l1-block-header" => Ok(Self::L1BlockHeader), + "l1-transactions" => Ok(Self::L1Transactions), + "l1-receipts" => Ok(Self::L1Receipts), + "l1-blob" => Ok(Self::L1Blob), + "l1-precompile" => Ok(Self::L1Precompile), + "l2-block-header" => Ok(Self::L2BlockHeader), + "l2-transactions" => Ok(Self::L2Transactions), + "l2-code" => Ok(Self::L2Code), + "starting-l2-output" => Ok(Self::StartingL2Output), + "l2-state-node" => Ok(Self::L2StateNode), + "l2-account-proof" => Ok(Self::L2AccountProof), + "l2-account-storage-proof" => Ok(Self::L2AccountStorageProof), + "l2-payload-witness" => Ok(Self::L2PayloadWitness), + _ => Err(HintParsingError(value.to_string())), + } + } +} + +impl From for &str { + fn from(value: HintType) -> Self { + match value { + HintType::L1BlockHeader => "l1-block-header", + HintType::L1Transactions => "l1-transactions", + HintType::L1Receipts => "l1-receipts", + HintType::L1Blob => "l1-blob", + HintType::L1Precompile => "l1-precompile", + HintType::L2BlockHeader => "l2-block-header", + HintType::L2Transactions => "l2-transactions", + HintType::L2Code => "l2-code", + HintType::StartingL2Output => "starting-l2-output", + HintType::L2StateNode => "l2-state-node", + HintType::L2AccountProof => "l2-account-proof", + HintType::L2AccountStorageProof => "l2-account-storage-proof", + HintType::L2PayloadWitness => "l2-payload-witness", + } + } +} + +impl Display for HintType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let s: &str = (*self).into(); + write!(f, "{s}") + } +} diff --git a/kona/crates/proof/proof/src/l1/blob_provider.rs b/kona/crates/proof/proof/src/l1/blob_provider.rs new file mode 100644 index 0000000000000..434652831b701 --- /dev/null +++ b/kona/crates/proof/proof/src/l1/blob_provider.rs @@ -0,0 +1,214 @@ +//! Contains the concrete implementation of the [BlobProvider] trait for the client program. + +use crate::{HintType, errors::OracleProviderError}; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloy_consensus::Blob; +use alloy_eips::eip4844::{FIELD_ELEMENTS_PER_BLOB, IndexedBlobHash}; +use alloy_primitives::keccak256; +use ark_bls12_381::Fr; +use ark_ff::{AdditiveGroup, BigInteger, BigInteger256, Field, PrimeField}; +use async_trait::async_trait; +use core::str::FromStr; +use kona_derive::BlobProvider; +use kona_preimage::{CommsClient, PreimageKey, PreimageKeyType}; +use kona_protocol::BlockInfo; +use spin::Lazy; + +/// An oracle-backed blob provider. +#[derive(Debug, Clone)] +pub struct OracleBlobProvider { + oracle: Arc, +} + +impl OracleBlobProvider { + /// Constructs a new `OracleBlobProvider`. + pub const fn new(oracle: Arc) -> Self { + Self { oracle } + } + + /// Retrieves a blob from the oracle. + /// + /// ## Takes + /// - `block_ref`: The block reference. + /// - `blob_hash`: The blob hash. + /// + /// ## Returns + /// - `Ok(blob)`: The blob. + /// - `Err(e)`: The blob could not be retrieved. + async fn get_blob( + &self, + block_ref: &BlockInfo, + blob_hash: &IndexedBlobHash, + ) -> Result { + let mut blob_req_meta = [0u8; 48]; + blob_req_meta[0..32].copy_from_slice(blob_hash.hash.as_ref()); + blob_req_meta[32..40].copy_from_slice((blob_hash.index).to_be_bytes().as_ref()); + blob_req_meta[40..48].copy_from_slice(block_ref.timestamp.to_be_bytes().as_ref()); + + // Send a hint for the blob commitment and field elements. + HintType::L1Blob.with_data(&[blob_req_meta.as_ref()]).send(self.oracle.as_ref()).await?; + + // Fetch the blob commitment. + let mut commitment = [0u8; 48]; + self.oracle + .get_exact(PreimageKey::new(*blob_hash.hash, PreimageKeyType::Sha256), &mut commitment) + .await + .map_err(OracleProviderError::Preimage)?; + + // Reconstruct the blob from the 4096 field elements. + let mut blob = Blob::default(); + let mut field_element_key = [0u8; 80]; + field_element_key[..48].copy_from_slice(commitment.as_ref()); + for i in 0..FIELD_ELEMENTS_PER_BLOB { + field_element_key[48..] + .copy_from_slice(ROOTS_OF_UNITY[i as usize].into_bigint().to_bytes_be().as_ref()); + + let mut field_element = [0u8; 32]; + self.oracle + .get_exact( + PreimageKey::new(*keccak256(field_element_key), PreimageKeyType::Blob), + &mut field_element, + ) + .await + .map_err(OracleProviderError::Preimage)?; + blob[(i as usize) << 5..(i as usize + 1) << 5].copy_from_slice(field_element.as_ref()); + } + + tracing::info!( + target: "client_blob_oracle", + index = blob_hash.index, + hash = ?blob_hash.hash, + "Retrieved blob" + ); + + Ok(blob) + } +} + +#[async_trait] +impl BlobProvider for OracleBlobProvider { + type Error = OracleProviderError; + + async fn get_and_validate_blobs( + &mut self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result>, Self::Error> { + let mut blobs = Vec::with_capacity(blob_hashes.len()); + for hash in blob_hashes { + blobs.push(Box::new(self.get_blob(block_ref, hash).await?)); + } + Ok(blobs) + } +} + +/// The 4096th bit-reversed roots of unity used in EIP-4844 as predefined evaluation points. +/// +/// See `generate_roots_of_unity` for details on how these roots of unity are generated. +pub static ROOTS_OF_UNITY: Lazy<[Fr; FIELD_ELEMENTS_PER_BLOB as usize]> = + Lazy::new(generate_roots_of_unity); + +/// Generates the 4096th bit-reversed roots of unity used in EIP-4844 as predefined evaluation +/// points. To compute the field element at index i in a blob, the blob polynomial is evaluated at +/// the i'th root of unity. Based on go-kzg-4844: +/// Also, see the consensus specs: +/// - compute_roots_of_unity +/// - bit-reversal permutation: +fn generate_roots_of_unity() -> [Fr; FIELD_ELEMENTS_PER_BLOB as usize] { + const MAX_ORDER_ROOT: u64 = 32; + + let mut roots_of_unity = [Fr::ZERO; FIELD_ELEMENTS_PER_BLOB as usize]; + + // Generator of the largest 2-adic subgroup of order 2^32. + let root_of_unity = Fr::new( + BigInteger256::from_str( + "10238227357739495823651030575849232062558860180284477541189508159991286009131", + ) + .expect("Failed to initialize root of unity"), + ); + + // Find generator subgroup of order x. + // This can be constructed by powering a generator of the largest 2-adic subgroup of order 2^32 + // by an exponent of (2^32)/x, provided x is <= 2^32. + let log_x = FIELD_ELEMENTS_PER_BLOB.trailing_zeros() as u64; + let expo = 1u64 << (MAX_ORDER_ROOT - log_x); + + // Generator has order x now + let generator = root_of_unity.pow([expo]); + + // Compute all relevant roots of unity, i.e. the multiplicative subgroup of size x + let mut current = Fr::ONE; + (0..FIELD_ELEMENTS_PER_BLOB).for_each(|i| { + roots_of_unity[i as usize] = current; + current *= generator; + }); + + let shift_correction = 64 - FIELD_ELEMENTS_PER_BLOB.trailing_zeros(); + (0..FIELD_ELEMENTS_PER_BLOB).for_each(|i| { + // Find index irev, such that i and irev get swapped + let irev = i.reverse_bits() >> shift_correction; + if irev > i { + roots_of_unity.swap(i as usize, irev as usize); + } + }); + + roots_of_unity +} + +#[cfg(test)] +mod test { + use super::ROOTS_OF_UNITY; + use alloy_eips::eip4844::{FIELD_ELEMENTS_PER_BLOB, env_settings::EnvKzgSettings}; + use ark_ff::{BigInteger, PrimeField}; + use c_kzg::{BYTES_PER_BLOB, Blob, Bytes32, Bytes48}; + use rand::Rng; + use rayon::iter::{IntoParallelIterator, ParallelIterator}; + + #[test] + fn test_roots_of_unity() { + // Initiate the default Ethereum KZG settings. + let kzg = EnvKzgSettings::default(); + + // Create a blob with random data + let mut bytes = [0u8; BYTES_PER_BLOB]; + rand::rng().fill(bytes.as_mut_slice()); + + // Ensure the blob is valid by keeping each field element within range. + (0..FIELD_ELEMENTS_PER_BLOB).for_each(|i| { + bytes[(i as usize) << 5] = 0; + }); + + let blob = Blob::new(bytes); + let blob_commitment = { + let raw = kzg.get().blob_to_kzg_commitment(&blob).unwrap(); + Bytes48::new(raw.as_slice().try_into().unwrap()) + }; + + // Validate each field element in the blob + (0..FIELD_ELEMENTS_PER_BLOB).into_par_iter().for_each(|i| { + let field_element = { + let mut fe = [0u8; 32]; + fe.copy_from_slice(&blob[(i as usize) << 5..(i as usize + 1) << 5]); + Bytes32::new(fe) + }; + + let z_bytes = Bytes32::new(ROOTS_OF_UNITY[i as usize].into_bigint().to_bytes_be().try_into().unwrap()); + let (proof, fe) = kzg.get().compute_kzg_proof(&blob, &z_bytes).unwrap(); + + // Ensure the field element matches the expected value + assert_eq!( + fe.as_slice(), + field_element.as_slice(), + "Field element {i} does not match the expected value. Expected: {field_element:?}, Got: {fe:?}" + ); + + // Ensure the proof can be verified + let proof_bytes = Bytes48::new(proof.as_slice().try_into().unwrap()); + let is_valid = kzg.get().verify_kzg_proof(&blob_commitment, &z_bytes, &field_element, &proof_bytes).unwrap(); + assert!( + is_valid, + "KZG proof verification failed for field element {i}. Commitment: {blob_commitment:?}, Z: {z_bytes:?}, Field Element: {field_element:?}, Proof: {proof_bytes:?}" + ); + }); + } +} diff --git a/kona/crates/proof/proof/src/l1/chain_provider.rs b/kona/crates/proof/proof/src/l1/chain_provider.rs new file mode 100644 index 0000000000000..32cffd9365f4a --- /dev/null +++ b/kona/crates/proof/proof/src/l1/chain_provider.rs @@ -0,0 +1,141 @@ +//! Contains the concrete implementation of the [ChainProvider] trait for the proof. + +use crate::{HintType, errors::OracleProviderError}; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloy_consensus::{Header, Receipt, ReceiptEnvelope, TxEnvelope}; +use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::B256; +use alloy_rlp::Decodable; +use async_trait::async_trait; +use kona_derive::ChainProvider; +use kona_mpt::{OrderedListWalker, TrieNode, TrieProvider}; +use kona_preimage::{CommsClient, PreimageKey, PreimageKeyType}; +use kona_protocol::BlockInfo; + +/// The oracle-backed L1 chain provider for the client program. +#[derive(Debug, Clone)] +pub struct OracleL1ChainProvider { + /// The L1 head hash. + pub l1_head: B256, + /// The preimage oracle client. + pub oracle: Arc, +} + +impl OracleL1ChainProvider { + /// Creates a new [OracleL1ChainProvider] with the given boot information and oracle client. + pub const fn new(l1_head: B256, oracle: Arc) -> Self { + Self { l1_head, oracle } + } +} + +#[async_trait] +impl ChainProvider for OracleL1ChainProvider { + type Error = OracleProviderError; + + async fn header_by_hash(&mut self, hash: B256) -> Result { + // Fetch the header RLP from the oracle. + HintType::L1BlockHeader.with_data(&[hash.as_ref()]).send(self.oracle.as_ref()).await?; + let header_rlp = self.oracle.get(PreimageKey::new_keccak256(*hash)).await?; + + // Decode the header RLP into a Header. + Header::decode(&mut header_rlp.as_slice()).map_err(OracleProviderError::Rlp) + } + + async fn block_info_by_number(&mut self, block_number: u64) -> Result { + // Fetch the starting block header. + let mut header = self.header_by_hash(self.l1_head).await?; + + // Check if the block number is in range. If not, we can fail early. + if block_number > header.number { + return Err(OracleProviderError::BlockNumberPastHead(block_number, header.number)); + } + + // Walk back the block headers to the desired block number. + while header.number > block_number { + header = self.header_by_hash(header.parent_hash).await?; + } + + Ok(BlockInfo { + hash: header.hash_slow(), + number: header.number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }) + } + + async fn receipts_by_hash(&mut self, hash: B256) -> Result, Self::Error> { + // Fetch the block header to find the receipts root. + let header = self.header_by_hash(hash).await?; + + // Send a hint for the block's receipts, and walk through the receipts trie in the header to + // verify them. + HintType::L1Receipts.with_data(&[hash.as_ref()]).send(self.oracle.as_ref()).await?; + let trie_walker = OrderedListWalker::try_new_hydrated(header.receipts_root, self) + .map_err(OracleProviderError::TrieWalker)?; + + // Decode the receipts within the receipts trie. + let receipts = trie_walker + .into_iter() + .map(|(_, rlp)| { + let envelope = ReceiptEnvelope::decode_2718(&mut rlp.as_ref())?; + Ok(envelope.as_receipt().expect("Infallible").clone()) + }) + .collect::, _>>() + .map_err(OracleProviderError::Rlp)?; + + Ok(receipts) + } + + async fn block_info_and_transactions_by_hash( + &mut self, + hash: B256, + ) -> Result<(BlockInfo, Vec), Self::Error> { + // Fetch the block header to construct the block info. + let header = self.header_by_hash(hash).await?; + let block_info = BlockInfo { + hash, + number: header.number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }; + + // Send a hint for the block's transactions, and walk through the transactions trie in the + // header to verify them. + HintType::L1Transactions.with_data(&[hash.as_ref()]).send(self.oracle.as_ref()).await?; + let trie_walker = OrderedListWalker::try_new_hydrated(header.transactions_root, self) + .map_err(OracleProviderError::TrieWalker)?; + + // Decode the transactions within the transactions trie. + let transactions = trie_walker + .into_iter() + .map(|(_, rlp)| { + // note: not short-handed for error type coercion w/ `?`. + let rlp = TxEnvelope::decode_2718(&mut rlp.as_ref())?; + Ok(rlp) + }) + .collect::, _>>() + .map_err(OracleProviderError::Rlp)?; + + Ok((block_info, transactions)) + } +} + +impl TrieProvider for OracleL1ChainProvider { + type Error = OracleProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + // On L1, trie node preimages are stored as keccak preimage types in the oracle. We assume + // that a hint for these preimages has already been sent, prior to this call. + crate::block_on(async move { + TrieNode::decode( + &mut self + .oracle + .get(PreimageKey::new(*key, PreimageKeyType::Keccak256)) + .await + .map_err(OracleProviderError::Preimage)? + .as_ref(), + ) + .map_err(OracleProviderError::Rlp) + }) + } +} diff --git a/kona/crates/proof/proof/src/l1/mod.rs b/kona/crates/proof/proof/src/l1/mod.rs new file mode 100644 index 0000000000000..1454ae50ddeb8 --- /dev/null +++ b/kona/crates/proof/proof/src/l1/mod.rs @@ -0,0 +1,10 @@ +//! Contains the L1 constructs of the proof, backed by the preimage oracle ABI as a data source. + +mod pipeline; +pub use pipeline::{OraclePipeline, ProviderAttributesBuilder, ProviderDerivationPipeline}; + +mod blob_provider; +pub use blob_provider::{OracleBlobProvider, ROOTS_OF_UNITY}; + +mod chain_provider; +pub use chain_provider::OracleL1ChainProvider; diff --git a/kona/crates/proof/proof/src/l1/pipeline.rs b/kona/crates/proof/proof/src/l1/pipeline.rs new file mode 100644 index 0000000000000..a27f7e2262e96 --- /dev/null +++ b/kona/crates/proof/proof/src/l1/pipeline.rs @@ -0,0 +1,183 @@ +//! Contains an oracle-backed pipeline. + +use crate::FlushableCache; +use alloc::{boxed::Box, sync::Arc}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_derive::{ + ChainProvider, DataAvailabilityProvider, DerivationPipeline, L2ChainProvider, OriginProvider, + Pipeline, PipelineBuilder, PipelineErrorKind, PipelineResult, PolledAttributesQueueStage, + ResetSignal, Signal, SignalReceiver, StatefulAttributesBuilder, StepResult, +}; +use kona_driver::{DriverPipeline, PipelineCursor}; +use kona_genesis::{L1ChainConfig, RollupConfig, SystemConfig}; +use kona_preimage::CommsClient; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use spin::RwLock; + +/// An oracle-backed derivation pipeline. +pub type ProviderDerivationPipeline = DerivationPipeline< + PolledAttributesQueueStage>, + L2, +>; + +/// An oracle-backed payload attributes builder for the `AttributesQueue` stage of the derivation +/// pipeline. +pub type ProviderAttributesBuilder = StatefulAttributesBuilder; + +/// An oracle-backed derivation pipeline. +#[derive(Debug)] +pub struct OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// The internal derivation pipeline. + pub pipeline: ProviderDerivationPipeline, + /// The caching oracle. + pub caching_oracle: Arc, +} + +impl OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// Constructs a new oracle-backed derivation pipeline. + pub async fn new( + cfg: Arc, + l1_cfg: Arc, + sync_start: Arc>, + caching_oracle: Arc, + da_provider: DA, + chain_provider: L1, + mut l2_chain_provider: L2, + ) -> PipelineResult { + let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l1_cfg, + l2_chain_provider.clone(), + chain_provider.clone(), + ); + + let cfg_for_reset = cfg.clone(); + + let mut pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(da_provider) + .l2_chain_provider(l2_chain_provider.clone()) + .chain_provider(chain_provider) + .builder(attributes) + .origin(sync_start.read().origin()) + .build_polled(); + + // Reset the pipeline to populate the initial system configuration in L1 Traversal. + let l2_safe_head = *sync_start.read().l2_safe_head(); + pipeline + .signal( + ResetSignal { + l2_safe_head, + l1_origin: sync_start.read().origin(), + system_config: l2_chain_provider + .system_config_by_number(l2_safe_head.block_info.number, cfg_for_reset) + .await + .ok(), + } + .signal(), + ) + .await?; + + Ok(Self { pipeline, caching_oracle }) + } +} + +impl DriverPipeline> + for OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// Flushes the cache on re-org. + fn flush(&mut self) { + self.caching_oracle.flush(); + } +} + +#[async_trait] +impl SignalReceiver for OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// Receives a signal from the driver. + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.pipeline.signal(signal).await + } +} + +impl OriginProvider for OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// Returns the optional L1 [BlockInfo] origin. + fn origin(&self) -> Option { + self.pipeline.origin() + } +} + +impl Iterator for OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + type Item = OpAttributesWithParent; + + fn next(&mut self) -> Option { + self.pipeline.next() + } +} + +#[async_trait] +impl Pipeline for OraclePipeline +where + O: CommsClient + FlushableCache + Send + Sync + Debug, + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: L2ChainProvider + Send + Sync + Debug + Clone, + DA: DataAvailabilityProvider + Send + Sync + Debug + Clone, +{ + /// Peeks at the next [OpAttributesWithParent] from the pipeline. + fn peek(&self) -> Option<&OpAttributesWithParent> { + self.pipeline.peek() + } + + /// Attempts to progress the pipeline. + async fn step(&mut self, cursor: L2BlockInfo) -> StepResult { + self.pipeline.step(cursor).await + } + + /// Returns the rollup config. + fn rollup_config(&self) -> &RollupConfig { + self.pipeline.rollup_config() + } + + /// Returns the [SystemConfig] by L2 number. + async fn system_config_by_number( + &mut self, + number: u64, + ) -> Result { + self.pipeline.system_config_by_number(number).await + } +} diff --git a/kona/crates/proof/proof/src/l2/chain_provider.rs b/kona/crates/proof/proof/src/l2/chain_provider.rs new file mode 100644 index 0000000000000..7533617377781 --- /dev/null +++ b/kona/crates/proof/proof/src/l2/chain_provider.rs @@ -0,0 +1,281 @@ +//! Contains the concrete implementation of the [L2ChainProvider] trait for the client program. + +use crate::{HintType, eip2935::eip_2935_history_lookup, errors::OracleProviderError}; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloy_consensus::{BlockBody, Header}; +use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::{Address, B256, Bytes}; +use alloy_rlp::Decodable; +use async_trait::async_trait; +use kona_derive::L2ChainProvider; +use kona_driver::PipelineCursor; +use kona_executor::TrieDBProvider; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_mpt::{OrderedListWalker, TrieHinter, TrieNode, TrieProvider}; +use kona_preimage::{CommsClient, PreimageKey, PreimageKeyType}; +use kona_protocol::{BatchValidationProvider, L2BlockInfo, to_system_config}; +use op_alloy_consensus::{OpBlock, OpTxEnvelope}; +use spin::RwLock; + +/// The oracle-backed L2 chain provider for the client program. +#[derive(Debug, Clone)] +pub struct OracleL2ChainProvider { + /// The L2 safe head block hash. + l2_head: B256, + /// The rollup configuration. + rollup_config: Arc, + /// The preimage oracle client. + oracle: Arc, + /// The derivation pipeline cursor + cursor: Option>>, + /// The L2 chain ID to use for the provider's hints. + chain_id: Option, +} + +impl OracleL2ChainProvider { + /// Creates a new [OracleL2ChainProvider] with the given boot information and oracle client. + pub const fn new(l2_head: B256, rollup_config: Arc, oracle: Arc) -> Self { + Self { l2_head, rollup_config, oracle, cursor: None, chain_id: None } + } + + /// Sets the L2 chain ID to use for the provider's hints. + pub const fn set_chain_id(&mut self, chain_id: Option) { + self.chain_id = chain_id; + } + + /// Updates the derivation pipeline cursor + pub fn set_cursor(&mut self, cursor: Arc>) { + self.cursor = Some(cursor); + } + + /// Fetches the latest known safe head block hash according to the derivation pipeline cursor + /// or uses the initial l2_head value if no cursor is set. + pub async fn l2_safe_head(&self) -> Result { + self.cursor + .as_ref() + .map_or(Ok(self.l2_head), |cursor| Ok(cursor.read().l2_safe_head().block_info.hash)) + } +} + +impl OracleL2ChainProvider { + /// Returns a [Header] corresponding to the given L2 block number, by walking back from the + /// L2 safe head. + async fn header_by_number(&mut self, block_number: u64) -> Result { + // Fetch the starting block header. + let mut header = self.header_by_hash(self.l2_safe_head().await?)?; + + // Check if the block number is in range. If not, we can fail early. + if block_number > header.number { + return Err(OracleProviderError::BlockNumberPastHead(block_number, header.number)); + } + + let mut linear_fallback = false; + while header.number > block_number { + if self.rollup_config.is_isthmus_active(header.timestamp) && !linear_fallback { + // If Isthmus is active, the EIP-2935 contract is used to perform leaping lookbacks + // through consulting the ring buffer within the contract. If this + // lookup fails for any reason, we fall back to linear walk back. + let block_hash = + match eip_2935_history_lookup(&header, block_number, self, self).await { + Ok(hash) => hash, + Err(_) => { + // If the EIP-2935 lookup fails for any reason, attempt fallback to + // linear walk back. + linear_fallback = true; + continue; + } + }; + + header = self.header_by_hash(block_hash)?; + } else { + // Walk back the block headers one-by-one until the desired block number is reached. + header = self.header_by_hash(header.parent_hash)?; + } + } + + Ok(header) + } +} + +#[async_trait] +impl BatchValidationProvider for OracleL2ChainProvider { + type Error = OracleProviderError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + // Get the block at the given number. + let block = self.block_by_number(number).await?; + + // Construct the system config from the payload. + L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) + .map_err(OracleProviderError::BlockInfo) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + // Fetch the header for the given block number. + let header @ Header { transactions_root, timestamp, .. } = + self.header_by_number(number).await?; + let header_hash = header.hash_slow(); + + // Fetch the transactions in the block. + HintType::L2Transactions + .with_data(&[header_hash.as_ref()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await?; + let trie_walker = OrderedListWalker::try_new_hydrated(transactions_root, self) + .map_err(OracleProviderError::TrieWalker)?; + + // Decode the transactions within the transactions trie. + let transactions = trie_walker + .into_iter() + .map(|(_, rlp)| { + let res = OpTxEnvelope::decode_2718(&mut rlp.as_ref())?; + Ok(res) + }) + .collect::, _>>() + .map_err(OracleProviderError::Rlp)?; + + let optimism_block = OpBlock { + header, + body: BlockBody { + transactions, + ommers: Vec::new(), + withdrawals: self + .rollup_config + .is_canyon_active(timestamp) + .then(|| alloy_eips::eip4895::Withdrawals::new(Vec::new())), + }, + }; + Ok(optimism_block) + } +} + +#[async_trait] +impl L2ChainProvider for OracleL2ChainProvider { + type Error = OracleProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error> { + // Get the block at the given number. + let block = self.block_by_number(number).await?; + + // Construct the system config from the payload. + to_system_config(&block, rollup_config.as_ref()) + .map_err(OracleProviderError::OpBlockConversion) + } +} + +impl TrieProvider for OracleL2ChainProvider { + type Error = OracleProviderError; + + fn trie_node_by_hash(&self, key: B256) -> Result { + // On L2, trie node preimages are stored as keccak preimage types in the oracle. We assume + // that a hint for these preimages has already been sent, prior to this call. + crate::block_on(async move { + TrieNode::decode( + &mut self + .oracle + .get(PreimageKey::new(*key, PreimageKeyType::Keccak256)) + .await + .map_err(OracleProviderError::Preimage)? + .as_ref(), + ) + .map_err(OracleProviderError::Rlp) + }) + } +} + +impl TrieDBProvider for OracleL2ChainProvider { + fn bytecode_by_hash(&self, hash: B256) -> Result { + // Fetch the bytecode preimage from the caching oracle. + crate::block_on(async move { + HintType::L2Code + .with_data(&[hash.as_slice()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await?; + self.oracle + .get(PreimageKey::new_keccak256(*hash)) + .await + .map(Into::into) + .map_err(OracleProviderError::Preimage) + }) + } + + fn header_by_hash(&self, hash: B256) -> Result { + // Fetch the header from the caching oracle. + crate::block_on(async move { + HintType::L2BlockHeader + .with_data(&[hash.as_slice()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await?; + let header_bytes = self.oracle.get(PreimageKey::new_keccak256(*hash)).await?; + + Header::decode(&mut header_bytes.as_slice()).map_err(OracleProviderError::Rlp) + }) + } +} + +impl TrieHinter for OracleL2ChainProvider { + type Error = OracleProviderError; + + fn hint_trie_node(&self, hash: B256) -> Result<(), Self::Error> { + crate::block_on(async move { + HintType::L2StateNode + .with_data(&[hash.as_slice()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_account_proof(&self, address: Address, block_number: u64) -> Result<(), Self::Error> { + crate::block_on(async move { + HintType::L2AccountProof + .with_data(&[block_number.to_be_bytes().as_ref(), address.as_slice()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_storage_proof( + &self, + address: alloy_primitives::Address, + slot: alloy_primitives::U256, + block_number: u64, + ) -> Result<(), Self::Error> { + crate::block_on(async move { + HintType::L2AccountStorageProof + .with_data(&[ + block_number.to_be_bytes().as_ref(), + address.as_slice(), + slot.to_be_bytes::<32>().as_ref(), + ]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await + }) + } + + fn hint_execution_witness( + &self, + parent_hash: B256, + op_payload_attributes: &op_alloy_rpc_types_engine::OpPayloadAttributes, + ) -> Result<(), Self::Error> { + crate::block_on(async move { + let encoded_attributes = + serde_json::to_vec(op_payload_attributes).map_err(OracleProviderError::Serde)?; + + HintType::L2PayloadWitness + .with_data(&[parent_hash.as_slice(), &encoded_attributes]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await + }) + } +} diff --git a/kona/crates/proof/proof/src/l2/mod.rs b/kona/crates/proof/proof/src/l2/mod.rs new file mode 100644 index 0000000000000..3926cc91a8596 --- /dev/null +++ b/kona/crates/proof/proof/src/l2/mod.rs @@ -0,0 +1,4 @@ +//! Contains the L2-specific constructs of the client program. + +mod chain_provider; +pub use chain_provider::OracleL2ChainProvider; diff --git a/kona/crates/proof/proof/src/lib.rs b/kona/crates/proof/proof/src/lib.rs new file mode 100644 index 0000000000000..bfb7eda7ecad9 --- /dev/null +++ b/kona/crates/proof/proof/src/lib.rs @@ -0,0 +1,38 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] + +extern crate alloc; + +#[macro_use] +extern crate tracing; + +pub mod l1; + +pub mod l2; + +pub mod sync; + +pub mod errors; + +pub mod executor; + +mod hint; +pub use hint::{Hint, HintType}; + +pub mod boot; +pub use boot::BootInfo; + +mod caching_oracle; +pub use caching_oracle::{CachingOracle, FlushableCache}; + +mod blocking_runtime; +pub use blocking_runtime::block_on; + +mod eip2935; +pub use eip2935::eip_2935_history_lookup; diff --git a/kona/crates/proof/proof/src/sync.rs b/kona/crates/proof/proof/src/sync.rs new file mode 100644 index 0000000000000..46dca4273ad6e --- /dev/null +++ b/kona/crates/proof/proof/src/sync.rs @@ -0,0 +1,46 @@ +//! Sync Start + +use crate::errors::OracleProviderError; +use alloc::sync::Arc; +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::B256; +use core::fmt::Debug; +use kona_derive::ChainProvider; +use kona_driver::{PipelineCursor, TipCursor}; +use kona_protocol::BatchValidationProvider; +use kona_registry::RollupConfig; +use spin::RwLock; + +/// Constructs a [`PipelineCursor`] from the caching oracle, boot info, and providers. +pub async fn new_oracle_pipeline_cursor( + rollup_config: &RollupConfig, + safe_header: Sealed
, + chain_provider: &mut L1, + l2_chain_provider: &mut L2, +) -> Result>, OracleProviderError> +where + L1: ChainProvider + Send + Sync + Debug + Clone, + L2: BatchValidationProvider + Send + Sync + Debug + Clone, + OracleProviderError: + From<::Error> + From<::Error>, +{ + let safe_head_info = l2_chain_provider.l2_block_info_by_number(safe_header.number).await?; + let l1_origin = chain_provider.block_info_by_number(safe_head_info.l1_origin.number).await?; + + // Walk back the starting L1 block by `channel_timeout` to ensure that the full channel is + // captured. + let channel_timeout = rollup_config.channel_timeout(safe_head_info.block_info.timestamp); + let mut l1_origin_number = l1_origin.number.saturating_sub(channel_timeout); + if l1_origin_number < rollup_config.genesis.l1.number { + l1_origin_number = rollup_config.genesis.l1.number; + } + let origin = chain_provider.block_info_by_number(l1_origin_number).await?; + + // Construct the cursor. + let mut cursor = PipelineCursor::new(channel_timeout, origin); + let tip = TipCursor::new(safe_head_info, safe_header, B256::ZERO); + cursor.advance(origin, tip); + + // Wrap the cursor in a shared read-write lock + Ok(Arc::new(RwLock::new(cursor))) +} diff --git a/kona/crates/proof/std-fpvm-proc/CHANGELOG.md b/kona/crates/proof/std-fpvm-proc/CHANGELOG.md new file mode 100644 index 0000000000000..e0b24f048817f --- /dev/null +++ b/kona/crates/proof/std-fpvm-proc/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.2](https://github.com/op-rs/kona/compare/kona-std-fpvm-proc-v0.1.1...kona-std-fpvm-proc-v0.1.2) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) + +## [0.1.1](https://github.com/op-rs/kona/compare/kona-std-fpvm-proc-v0.1.0...kona-std-fpvm-proc-v0.1.1) - 2024-12-03 + +### Other + +- update Cargo.toml dependencies diff --git a/kona/crates/proof/std-fpvm-proc/Cargo.toml b/kona/crates/proof/std-fpvm-proc/Cargo.toml new file mode 100644 index 0000000000000..533b386087d0c --- /dev/null +++ b/kona/crates/proof/std-fpvm-proc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "kona-std-fpvm-proc" +description = "Proc macro entry point for `kona-std-fpvm` targeted programs." +version = "0.2.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lib] +proc-macro = true + +[dependencies] +# General +cfg-if.workspace = true + +# Workspace +kona-std-fpvm.workspace = true + +# Proc Macros +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "2.0", features = ["full"] } + +[package.metadata.cargo-udeps.ignore] +normal = ["kona-std-fpvm"] diff --git a/kona/crates/proof/std-fpvm-proc/src/lib.rs b/kona/crates/proof/std-fpvm-proc/src/lib.rs new file mode 100644 index 0000000000000..16387b252eb0c --- /dev/null +++ b/kona/crates/proof/std-fpvm-proc/src/lib.rs @@ -0,0 +1,45 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{ItemFn, parse_macro_input}; + +#[proc_macro_attribute] +pub fn client_entry(_: TokenStream, input: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(input as ItemFn); + + let fn_body = &input_fn.block; + let fn_name = &input_fn.sig.ident; + + let expanded = quote! { + fn #fn_name() -> Result<(), String> { + match #fn_body { + Ok(_) => kona_std_fpvm::io::exit(0), + Err(e) => { + kona_std_fpvm::io::print_err(alloc::format!("Program encountered fatal error: {:?}\n", e).as_ref()); + kona_std_fpvm::io::exit(1); + } + } + } + + cfg_if::cfg_if! { + if #[cfg(any(target_arch = "mips64", target_arch = "riscv64"))] { + #[doc = "Program entry point"] + #[unsafe(no_mangle)] + pub extern "C" fn _start() { + kona_std_fpvm::alloc_heap!(); + let _ = #fn_name(); + } + + #[panic_handler] + fn panic(info: &core::panic::PanicInfo) -> ! { + let msg = alloc::format!("Panic: {}", info); + kona_std_fpvm::io::print_err(msg.as_ref()); + kona_std_fpvm::io::exit(2) + } + } + } + }; + + TokenStream::from(expanded) +} diff --git a/kona/crates/proof/std-fpvm/CHANGELOG.md b/kona/crates/proof/std-fpvm/CHANGELOG.md new file mode 100644 index 0000000000000..980b2e917a143 --- /dev/null +++ b/kona/crates/proof/std-fpvm/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.2](https://github.com/op-rs/kona/compare/kona-std-fpvm-v0.1.1...kona-std-fpvm-v0.1.2) - 2025-01-07 + +### Added + +- *(build)* Adjust RV target - `riscv64g` -> `riscv64ima` (#868) + +### Fixed + +- op-rs rename (#883) + +## [0.1.1](https://github.com/op-rs/kona/compare/kona-std-fpvm-v0.1.0...kona-std-fpvm-v0.1.1) - 2024-12-03 + +### Other + +- update Cargo.toml dependencies diff --git a/kona/crates/proof/std-fpvm/Cargo.toml b/kona/crates/proof/std-fpvm/Cargo.toml new file mode 100644 index 0000000000000..180d7fadcfe05 --- /dev/null +++ b/kona/crates/proof/std-fpvm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "kona-std-fpvm" +description = "Platform specific APIs for interacting with Fault Proof VM kernels." +version = "0.2.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-preimage.workspace = true + +# External +cfg-if.workspace = true +thiserror.workspace = true +buddy_system_allocator.workspace = true +async-trait.workspace = true + +# `tracing` feature dependencies +tracing = { workspace = true, optional = true } + +[package.metadata.cargo-udeps.ignore] +normal = ["buddy_system_allocator"] + +[features] +tracing = [ "dep:tracing" ] diff --git a/kona/crates/proof/std-fpvm/README.md b/kona/crates/proof/std-fpvm/README.md new file mode 100644 index 0000000000000..6eb566d9bc9e9 --- /dev/null +++ b/kona/crates/proof/std-fpvm/README.md @@ -0,0 +1,10 @@ +# `kona-std-fpvm` + +CI +Kona Proof SDK +License +Codecov + +Platform specific [Fault Proof VM][g-fault-proof-vm] kernel APIs. + +[g-fault-proof-vm]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-vm diff --git a/kona/crates/proof/std-fpvm/src/channel.rs b/kona/crates/proof/std-fpvm/src/channel.rs new file mode 100644 index 0000000000000..7d0abc91a1165 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/channel.rs @@ -0,0 +1,158 @@ +//! This module contains a rudimentary channel between two file descriptors, using [crate::io] +//! for reading and writing from the file descriptors. + +use crate::{FileDescriptor, io}; +use alloc::boxed::Box; +use async_trait::async_trait; +use core::{ + cell::RefCell, + cmp::Ordering, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use kona_preimage::{ + Channel, + errors::{ChannelError, ChannelResult}, +}; + +/// [FileChannel] is a handle for one end of a bidirectional channel. +#[derive(Debug, Clone, Copy)] +pub struct FileChannel { + /// File descriptor to read from + read_handle: FileDescriptor, + /// File descriptor to write to + write_handle: FileDescriptor, +} + +impl FileChannel { + /// Create a new [FileChannel] from two file descriptors. + pub const fn new(read_handle: FileDescriptor, write_handle: FileDescriptor) -> Self { + Self { read_handle, write_handle } + } + + /// Returns a copy of the [FileDescriptor] used for the read end of the channel. + pub const fn read_handle(&self) -> FileDescriptor { + self.read_handle + } + + /// Returns a copy of the [FileDescriptor] used for the write end of the channel. + pub const fn write_handle(&self) -> FileDescriptor { + self.write_handle + } +} + +#[async_trait] +impl Channel for FileChannel { + async fn read(&self, buf: &mut [u8]) -> ChannelResult { + io::read(self.read_handle, buf).map_err(|_| ChannelError::Closed) + } + + async fn read_exact(&self, buf: &mut [u8]) -> ChannelResult { + ReadFuture::new(*self, buf).await.map_err(|_| ChannelError::Closed) + } + + async fn write(&self, buf: &[u8]) -> ChannelResult { + WriteFuture::new(*self, buf).await.map_err(|_| ChannelError::Closed) + } +} + +/// A future that reads from a channel, returning [Poll::Ready] when the buffer is full. +struct ReadFuture<'a> { + /// The channel to read from + channel: FileChannel, + /// The buffer to read into + buf: RefCell<&'a mut [u8]>, + /// The number of bytes read so far + read: usize, +} + +impl<'a> ReadFuture<'a> { + /// Create a new [ReadFuture] from a channel and a buffer. + #[allow(clippy::missing_const_for_fn)] + fn new(channel: FileChannel, buf: &'a mut [u8]) -> Self { + Self { channel, buf: RefCell::new(buf), read: 0 } + } +} + +impl Future for ReadFuture<'_> { + type Output = ChannelResult; + + fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + let mut buf = self.buf.borrow_mut(); + let buf_len = buf.len(); + let chunk_read = io::read(self.channel.read_handle, &mut buf[self.read..]) + .map_err(|_| ChannelError::Closed)?; + + // Drop the borrow on self. + drop(buf); + + self.read += chunk_read; + + match self.read.cmp(&buf_len) { + Ordering::Greater | Ordering::Equal => Poll::Ready(Ok(self.read)), + Ordering::Less => { + // Register the current task to be woken up when it can make progress + ctx.waker().wake_by_ref(); + Poll::Pending + } + } + } +} + +/// A future that writes to a channel, returning [Poll::Ready] when the full buffer has been +/// written. +struct WriteFuture<'a> { + /// The channel to write to + channel: FileChannel, + /// The buffer to write + buf: &'a [u8], + /// The number of bytes written so far + written: usize, +} + +impl<'a> WriteFuture<'a> { + /// Create a new [WriteFuture] from a channel and a buffer. + const fn new(channel: FileChannel, buf: &'a [u8]) -> Self { + Self { channel, buf, written: 0 } + } +} + +impl Future for WriteFuture<'_> { + type Output = ChannelResult; + + fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { + match io::write(self.channel.write_handle(), &self.buf[self.written..]) { + Ok(n) => { + self.written += n; + + match self.written.cmp(&self.buf.len()) { + Ordering::Equal | Ordering::Greater => { + // Finished writing + Poll::Ready(Ok(self.written)) + } + Ordering::Less => { + // Register the current task to be woken up when it can make progress + ctx.waker().wake_by_ref(); + Poll::Pending + } + } + } + Err(_) => Poll::Ready(Err(ChannelError::Closed)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_read_handle() { + let read_handle = FileDescriptor::StdIn; + let write_handle = FileDescriptor::StdOut; + let chan = FileChannel::new(read_handle, write_handle); + let ref_read_handle = chan.read_handle(); + assert_eq!(read_handle, ref_read_handle); + } +} diff --git a/kona/crates/proof/std-fpvm/src/errors.rs b/kona/crates/proof/std-fpvm/src/errors.rs new file mode 100644 index 0000000000000..a8fbec07c74cd --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/errors.rs @@ -0,0 +1,11 @@ +//! Errors for the `kona-std-fpvm` crate. + +use thiserror::Error; + +/// An error that can occur when reading from or writing to a file descriptor. +#[derive(Error, Debug, PartialEq, Eq)] +#[error("IO error (errno: {_0})")] +pub struct IOError(pub i32); + +/// A [Result] type for the [IOError]. +pub type IOResult = Result; diff --git a/kona/crates/proof/std-fpvm/src/io.rs b/kona/crates/proof/std-fpvm/src/io.rs new file mode 100644 index 0000000000000..011b9ad7b86ef --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/io.rs @@ -0,0 +1,93 @@ +//! This module contains the `ClientIO` struct, which is a system call interface for the kernel. + +use crate::{BasicKernelInterface, FileDescriptor, errors::IOResult}; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(target_arch = "mips64")] { + #[doc = "Concrete implementation of the [BasicKernelInterface] trait for the `MIPS64r2` target architecture."] + pub(crate) type ClientIO = crate::mips64::io::Mips64IO; + } else if #[cfg(target_arch = "riscv64")] { + #[doc = "Concrete implementation of the [BasicKernelInterface] trait for the `riscv64` target architecture."] + pub(crate) type ClientIO = crate::riscv64::io::RiscV64IO; + } else { + use std::{fs::File, os::fd::FromRawFd, io::{Read, Write}}; + use crate::errors::IOError; + + #[doc = "Native implementation of the [BasicKernelInterface] trait."] + pub(crate) struct NativeClientIO; + + impl BasicKernelInterface for NativeClientIO { + fn write(fd: FileDescriptor, buf: &[u8]) -> IOResult { + unsafe { + let mut file = File::from_raw_fd(fd as i32); + file.write_all(buf).map_err(|_| IOError(-9))?; + std::mem::forget(file); + Ok(buf.len()) + } + } + + fn read(fd: FileDescriptor, buf: &mut [u8]) -> IOResult { + unsafe { + let mut file = File::from_raw_fd(fd as i32); + file.read_exact(buf).map_err(|_| IOError(-9))?; + std::mem::forget(file); + Ok(buf.len()) + } + } + + fn mmap(_size: usize) -> IOResult { + unimplemented!("mmap is unimplemented for the native target; The default global allocator is favored."); + } + + fn exit(code: usize) -> ! { + std::process::exit(code as i32) + } + } + + #[doc = "Native implementation of the [BasicKernelInterface] trait."] + pub(crate) type ClientIO = NativeClientIO; + } +} + +/// Print the passed string to the standard output [FileDescriptor]. +/// +/// # Panics +/// Panics if the write operation fails. +#[inline] +pub fn print(s: &str) { + ClientIO::write(FileDescriptor::StdOut, s.as_bytes()).expect("Error writing to stdout."); +} + +/// Print the passed string to the standard error [FileDescriptor]. +/// +/// # Panics +/// Panics if the write operation fails. +#[inline] +pub fn print_err(s: &str) { + ClientIO::write(FileDescriptor::StdErr, s.as_bytes()).expect("Error writing to stderr."); +} + +/// Write the passed buffer to the given [FileDescriptor]. +#[inline] +pub fn write(fd: FileDescriptor, buf: &[u8]) -> IOResult { + ClientIO::write(fd, buf) +} + +/// Write the passed buffer to the given [FileDescriptor]. +#[inline] +pub fn read(fd: FileDescriptor, buf: &mut [u8]) -> IOResult { + ClientIO::read(fd, buf) +} + +/// Map new memory of block size `size`. Returns the new heap pointer. +#[inline] +pub fn mmap(size: usize) -> IOResult { + ClientIO::mmap(size) +} + +/// Exit the process with the given exit code. +#[inline] +pub fn exit(code: usize) -> ! { + ClientIO::exit(code) +} diff --git a/kona/crates/proof/std-fpvm/src/lib.rs b/kona/crates/proof/std-fpvm/src/lib.rs new file mode 100644 index 0000000000000..6c7c549025ede --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/lib.rs @@ -0,0 +1,37 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(target_arch = "mips64", feature(asm_experimental_arch))] +#![cfg_attr(any(target_arch = "mips64", target_arch = "riscv64"), no_std)] + +extern crate alloc; + +pub mod errors; + +pub mod io; + +#[cfg(feature = "tracing")] +pub mod tracing; + +pub mod malloc; + +mod traits; +pub use traits::BasicKernelInterface; + +mod types; +pub use types::FileDescriptor; + +mod channel; +pub use channel::FileChannel; + +pub(crate) mod linux; + +#[cfg(target_arch = "mips64")] +pub(crate) mod mips64; + +#[cfg(target_arch = "riscv64")] +pub(crate) mod riscv64; diff --git a/kona/crates/proof/std-fpvm/src/linux.rs b/kona/crates/proof/std-fpvm/src/linux.rs new file mode 100644 index 0000000000000..8bb87e9652df0 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/linux.rs @@ -0,0 +1,32 @@ +//! Linux utilities + +use crate::errors::{IOError, IOResult}; + +/// Converts a return value from a syscall into a [IOResult] type. +#[inline(always)] +#[allow(unused)] +pub(crate) const fn from_ret(value: usize) -> IOResult { + if value > -4096isize as usize { + // Truncation of the error value is guaranteed to never occur due to + // the above check. This is the same check that musl uses: + // https://git.musl-libc.org/cgit/musl/tree/src/internal/syscall_ret.c?h=v1.1.15 + Err(IOError(-(value as i32))) + } else { + Ok(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_ret_io_error() { + assert_eq!(from_ret(-4095isize as usize), Err(IOError(4095))); + } + + #[test] + fn test_from_ret_ok() { + assert_eq!(from_ret(1), Ok(1)); + } +} diff --git a/kona/crates/proof/std-fpvm/src/malloc.rs b/kona/crates/proof/std-fpvm/src/malloc.rs new file mode 100644 index 0000000000000..a97a85eeef3cd --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/malloc.rs @@ -0,0 +1,71 @@ +//! This module contains an implementation of a basic memory allocator for client programs in +//! running on an embedded device. +//! +//! The allocator is a linked list allocator based on the `dlmalloc` algorithm, which is a +//! well-known and widely used allocator software such as OS Kernels. + +/// The global allocator for the program in embedded environments. +#[cfg(any(target_arch = "mips64", target_arch = "riscv64"))] +pub mod global_allocator { + use buddy_system_allocator::LockedHeap; + + /// The maximum block size, as a power of two, for the buddy system allocator. + const HEAP_ORDER: usize = 32; + + /// The global allocator for the program. + #[global_allocator] + static ALLOCATOR: LockedHeap = LockedHeap::empty(); + + /// Initialize the [SpinLockedAllocator] with the following parameters: + /// * `heap_start_addr` is the starting address of the heap memory region, + /// * `heap_size` is the size of the heap memory region in bytes. + /// + /// # Safety + /// This function is unsafe because the caller must ensure: + /// * The allocator has not already been initialized. + /// * The provided memory region must be valid, non-null, and not used by anything else. + /// * After aligning the start and end addresses, the size of the heap must be > 0, or the + /// function will panic. + pub unsafe fn init_allocator(heap_start_addr: usize, heap_size: usize) { + unsafe { ALLOCATOR.lock().init(heap_start_addr, heap_size) } + } +} + +/// Initialize heap memory for the `client` program with the given size. +/// +/// # Safety +#[cfg_attr( + any(target_arch = "mips64", target_arch = "riscv64"), + doc = "See [global_allocator::init_allocator] safety comment." +)] +#[cfg_attr( + not(any(target_arch = "mips64", target_arch = "riscv64")), + doc = "This macro is entirely safe to invoke in non-MIPS and non-RISC-V64 profiles, and functions as a no-op." +)] +#[macro_export] +macro_rules! alloc_heap { + () => {{ + #[cfg(any(target_arch = "mips64", target_arch = "riscv64"))] + { + use $crate::malloc::global_allocator::init_allocator; + + // The maximum heap size is configured to be an inordinate amount of memory (a + // terabyte.) Fault proof VMs do not actually allocate pages when an `mmap` + // is received, but instead allocate new pages on the fly. At startup, we + // request the FPVM's heap pointer to be bumped to make room for any necessary + // allocations throughout the lifecycle of the program. + const MAX_HEAP_SIZE: usize = 1 << 40; + + // SAFETY: If the kernel fails to map the virtual memory, a panic is in order and we + // should exit immediately. Program execution cannot continue. + let region_start = + $crate::io::mmap(MAX_HEAP_SIZE).expect("Kernel failed to map memory"); + + // SAFETY: The memory region, at this point, is guaranteed to be valid and mapped by the + // kernel. + unsafe { + init_allocator(region_start, MAX_HEAP_SIZE); + } + } + }}; +} diff --git a/kona/crates/proof/std-fpvm/src/mips64/io.rs b/kona/crates/proof/std-fpvm/src/mips64/io.rs new file mode 100644 index 0000000000000..9f6495593b186 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/mips64/io.rs @@ -0,0 +1,68 @@ +use crate::{BasicKernelInterface, FileDescriptor, errors::IOResult, mips64::syscall}; + +/// Concrete implementation of the [BasicKernelInterface] trait for the `MIPS64r2` target +/// architecture. Exposes a safe interface for performing IO operations within the kernel. +#[derive(Debug)] +pub(crate) struct Mips64IO; + +/// Relevant system call numbers for the `MIPS64r2` target architecture. +/// +/// See [Cannon System Call Specification](https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#syscalls) +/// +/// **Note**: This is not an exhaustive list of system calls available to the `client` program, +/// only the ones necessary for the [BasicKernelInterface] trait implementation. If an extension +/// trait for the [BasicKernelInterface] trait is created for the `Cannon` kernel, this list should +/// be extended accordingly. +#[repr(usize)] +pub(crate) enum SyscallNumber { + /// Sets the Exited and ExitCode states to true and $a0 respectively. + Exit = 5205, + /// Similar behavior as Linux/MIPS with support for unaligned reads. + Read = 5000, + /// Similar behavior as Linux/MIPS with support for unaligned writes. + Write = 5001, + /// Similar behavior as Linux/MIPS for mapping memory on the host machine. Only accepts 2 + /// arguments for cannon. + Mmap = 5009, +} + +impl BasicKernelInterface for Mips64IO { + fn write(fd: FileDescriptor, buf: &[u8]) -> IOResult { + unsafe { + crate::linux::from_ret(syscall::syscall3( + SyscallNumber::Write as usize, + fd.into(), + buf.as_ptr() as usize, + buf.len(), + )) + } + } + + fn read(fd: FileDescriptor, buf: &mut [u8]) -> IOResult { + unsafe { + crate::linux::from_ret(syscall::syscall3( + SyscallNumber::Read as usize, + fd.into(), + buf.as_ptr() as usize, + buf.len(), + )) + } + } + + fn mmap(size: usize) -> IOResult { + unsafe { + crate::linux::from_ret(syscall::syscall2( + SyscallNumber::Mmap as usize, + 0usize, // anonymous map + size, + )) + } + } + + fn exit(code: usize) -> ! { + unsafe { + let _ = syscall::syscall1(SyscallNumber::Exit as usize, code); + panic!("exit syscall returned unexpectedly with code: {}", code) + } + } +} diff --git a/kona/crates/proof/std-fpvm/src/mips64/mod.rs b/kona/crates/proof/std-fpvm/src/mips64/mod.rs new file mode 100644 index 0000000000000..f4f2e06d2ca2c --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/mips64/mod.rs @@ -0,0 +1,5 @@ +//! This module contains raw syscall bindings for the `MIPS64r2` target architecture, as well as a +//! high-level implementation of the [crate::BasicKernelInterface] trait for the `Cannon` kernel. + +pub(crate) mod io; +mod syscall; diff --git a/kona/crates/proof/std-fpvm/src/mips64/syscall.rs b/kona/crates/proof/std-fpvm/src/mips64/syscall.rs new file mode 100644 index 0000000000000..d5ba34f89a316 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/mips64/syscall.rs @@ -0,0 +1,131 @@ +//! Derived from the syscalls crate +//! +//! MIPS has the following registers: +//! +//! | Symbolic Name | Number | Usage | +//! | ============= | =============== | ============================== | +//! | zero | 0 | Constant 0. | +//! | at | 1 | Reserved for the assembler. | +//! | v0 - v1 | 2 - 3 | Result Registers. | +//! | a0 - a3 | 4 - 7 | Argument Registers 1 ·· · 4. | +//! | t0 - t9 | 8 - 15, 24 - 25 | Temporary Registers 0 · · · 9. | +//! | s0 - s7 | 16 - 23 | Saved Registers 0 ·· · 7. | +//! | k0 - k1 | 26 - 27 | Kernel Registers 0 ·· · 1. | +//! | gp | 28 | Global Data Pointer. | +//! | sp | 29 | Stack Pointer. | +//! | fp | 30 | Frame Pointer. | +//! | ra | 31 | Return Address. | +//! +//! The following registers are used for args 1-6: +//! +//! arg1: %a0 ($4) +//! arg2: %a1 ($5) +//! arg3: %a2 ($6) +//! arg4: %a3 ($7) +//! arg5: (Passed via user stack) +//! arg6: (Passed via user stack) +//! arg7: (Passed via user stack) +//! +//! %v0 is the syscall number. +//! %v0 is the return value. +//! %v1 is the error code +//! %a3 is a boolean indicating that an error occurred. +//! +//! +//! All temporary registers are clobbered (8-15, 24-25). + +use core::arch::asm; + +/// Issues a raw system call with 1 argument. (e.g. exit) +#[inline] +pub(crate) unsafe fn syscall1(n: usize, arg1: usize) -> usize { + let mut err: usize; + let mut ret: usize; + unsafe { + asm!( + "syscall", + inlateout("$2") n => ret, + lateout("$7") err, + in("$4") arg1, + // Clobber all temporary registers + lateout("$8") _, + lateout("$9") _, + lateout("$10") _, + lateout("$11") _, + lateout("$12") _, + lateout("$13") _, + lateout("$14") _, + lateout("$15") _, + lateout("$24") _, + lateout("$25") _, + options(nostack, preserves_flags) + ); + } + + if err == 0 { ret } else { ret.wrapping_neg() } +} + +/// Issues a raw system call with 2 arguments. (e.g. cannon's flavor of mmap) +/// +/// # Safety +/// +/// Running a system call is inherently unsafe. It is the caller's +/// responsibility to ensure safety. +#[inline] +pub(crate) unsafe fn syscall2(n: usize, arg1: usize, arg2: usize) -> usize { + let mut err: usize; + let mut ret: usize; + unsafe { + asm!( + "syscall", + inlateout("$2") n => ret, + lateout("$7") err, + in("$4") arg1, + in("$5") arg2, + // All temporary registers are always clobbered + lateout("$8") _, + lateout("$9") _, + lateout("$10") _, + lateout("$11") _, + lateout("$12") _, + lateout("$13") _, + lateout("$14") _, + lateout("$15") _, + lateout("$24") _, + lateout("$25") _, + options(nostack, preserves_flags) + ); + } + if err == 0 { ret } else { ret.wrapping_neg() } +} + +/// Issues a raw system call with 3 arguments. (e.g. read, write) +#[inline] +pub(crate) unsafe fn syscall3(n: usize, arg1: usize, arg2: usize, arg3: usize) -> usize { + let mut err: usize; + let mut ret: usize; + unsafe { + asm!( + "syscall", + inlateout("$2") n => ret, + lateout("$7") err, + in("$4") arg1, + in("$5") arg2, + in("$6") arg3, + // Clobber all temporary registers + lateout("$8") _, + lateout("$9") _, + lateout("$10") _, + lateout("$11") _, + lateout("$12") _, + lateout("$13") _, + lateout("$14") _, + lateout("$15") _, + lateout("$24") _, + lateout("$25") _, + options(nostack, preserves_flags) + ); + } + + if err == 0 { ret } else { ret.wrapping_neg() } +} diff --git a/kona/crates/proof/std-fpvm/src/riscv64/io.rs b/kona/crates/proof/std-fpvm/src/riscv64/io.rs new file mode 100644 index 0000000000000..e680d0d50d387 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/riscv64/io.rs @@ -0,0 +1,71 @@ +use crate::{BasicKernelInterface, FileDescriptor, errors::IOResult, riscv64::syscall}; + +/// Concrete implementation of the [`KernelIO`] trait for the `riscv64` target architecture. +#[derive(Debug)] +pub(crate) struct RiscV64IO; + +/// Relevant system call numbers for the `riscv64` target architecture. +/// +/// See https://jborza.com/post/2021-05-11-riscv-linux-syscalls/ +/// +/// **Note**: This is not an exhaustive list of system calls available to the `client` program, +/// only the ones necessary for the [BasicKernelInterface] trait implementation. If an extension +/// trait for the [BasicKernelInterface] trait is created for the linux kernel, this list +/// should be extended accordingly. +#[repr(usize)] +pub(crate) enum SyscallNumber { + /// Sets the Exited and ExitCode states to true and $a0 respectively. + Exit = 93, + /// Similar behavior as Linux with support for unaligned reads. + Read = 63, + /// Similar behavior as Linux with support for unaligned writes. + Write = 64, + /// Similar behavior as Linux for mapping memory on the host machine. + Mmap = 222, +} + +impl BasicKernelInterface for RiscV64IO { + fn write(fd: FileDescriptor, buf: &[u8]) -> IOResult { + unsafe { + crate::linux::from_ret(syscall::syscall3( + SyscallNumber::Write as usize, + fd.into(), + buf.as_ptr() as usize, + buf.len(), + )) + } + } + + fn read(fd: FileDescriptor, buf: &mut [u8]) -> IOResult { + unsafe { + crate::linux::from_ret(syscall::syscall3( + SyscallNumber::Read as usize, + fd.into(), + buf.as_ptr() as usize, + buf.len(), + )) + } + } + + fn mmap(size: usize) -> IOResult { + // https://github.com/ethereum-optimism/asterisc/blob/master/rvgo/fast/vm.go#L360-L398 + unsafe { + crate::linux::from_ret(syscall::syscall6( + SyscallNumber::Mmap as usize, + 0usize, // address hint - 0 for anonymous maps + size, // block size + 0usize, // prot, ignored. + 0x20, // flags - set MAP_ANONYMOUS + u64::MAX as usize, // fd = -1, anonymous memory maps only. + 0usize, // offset - ignored, anonymous memory maps only. + )) + } + } + + fn exit(code: usize) -> ! { + unsafe { + let _ = syscall::syscall1(SyscallNumber::Exit as usize, code); + panic!() + } + } +} diff --git a/kona/crates/proof/std-fpvm/src/riscv64/mod.rs b/kona/crates/proof/std-fpvm/src/riscv64/mod.rs new file mode 100644 index 0000000000000..f90970ea7ccd2 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/riscv64/mod.rs @@ -0,0 +1,5 @@ +//! This module contains raw syscall bindings for the `riscv64imac` target architecture, as well as +//! a high-level implementation of the [crate::BasicKernelInterface] trait for the kernel. + +pub(crate) mod io; +mod syscall; diff --git a/kona/crates/proof/std-fpvm/src/riscv64/syscall.rs b/kona/crates/proof/std-fpvm/src/riscv64/syscall.rs new file mode 100644 index 0000000000000..f8ad635939b59 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/riscv64/syscall.rs @@ -0,0 +1,88 @@ +//! Derived from the syscalls crate +//! +//! Unsafe system call interface for the `riscv64` target architecture. +//! +//! List of RISC-V system calls: +//! +//! **Registers used for system calls** +//! | Register Number | Description | +//! |=================|====================| +//! | %a0 | arg1, return value | +//! | %a1 | arg2 | +//! | %a2 | arg3 | +//! | %a3 | arg4 | +//! | %a4 | arg5 | +//! | %a5 | arg6 | +//! | %a7 | syscall number | + +use core::arch::asm; + +/// Issues a raw system call with 1 argument. (e.g. exit) +#[inline] +pub(crate) unsafe fn syscall1(syscall_number: usize, arg1: usize) -> usize { + let mut ret: usize; + unsafe { + asm!( + "ecall", + in("a7") syscall_number, + inlateout("a0") arg1 => ret, + options(nostack, preserves_flags) + ); + } + ret +} + +/// Issues a raw system call with 3 arguments. (e.g. read, write) +#[inline] +pub(crate) unsafe fn syscall3( + syscall_number: usize, + arg1: usize, + arg2: usize, + arg3: usize, +) -> usize { + let mut ret: usize; + unsafe { + asm!( + "ecall", + in("a7") syscall_number, + inlateout("a0") arg1 => ret, + in("a1") arg2, + in("a2") arg3, + options(nostack, preserves_flags) + ); + } + ret +} + +/// Issues a raw system call with 6 arguments. (e.g. mmap) +/// +/// # Safety +/// +/// Running a system call is inherently unsafe. It is the caller's +/// responsibility to ensure safety. +#[inline] +pub(crate) unsafe fn syscall6( + syscall_number: usize, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, + arg5: usize, + arg6: usize, +) -> usize { + let mut ret: usize; + unsafe { + asm!( + "ecall", + in("a7") syscall_number, + inlateout("a0") arg1 => ret, + in("a1") arg2, + in("a2") arg3, + in("a3") arg4, + in("a4") arg5, + in("a5") arg6, + options(nostack, preserves_flags) + ); + } + ret +} diff --git a/kona/crates/proof/std-fpvm/src/tracing.rs b/kona/crates/proof/std-fpvm/src/tracing.rs new file mode 100644 index 0000000000000..4ceb4f3308b59 --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/tracing.rs @@ -0,0 +1,97 @@ +//! This module contains + +use crate::io; +use alloc::{ + format, + string::{String, ToString}, + vec::Vec, +}; +use tracing::{ + Event, Level, Metadata, Subscriber, + field::{Field, Visit}, + span::{Attributes, Id, Record}, +}; + +/// Custom [Subscriber] implementation that uses [crate::io] to write log entries to +/// [crate::FileDescriptor::StdOut]. +#[derive(Debug, Clone)] +pub struct FpvmTracingSubscriber { + min_level: Level, +} + +impl FpvmTracingSubscriber { + /// Create a new [FpvmTracingSubscriber] with the specified minimum log level. + pub const fn new(min_level: Level) -> Self { + Self { min_level } + } +} + +impl Subscriber for FpvmTracingSubscriber { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, _span: &Attributes<'_>) -> Id { + Id::from_u64(1) + } + + fn record(&self, _span: &Id, _values: &Record<'_>) {} + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &Event<'_>) { + let metadata = event.metadata(); + // Comparisons for the [Level] type are inverted. See the [Level] documentation for more + // information. + if *metadata.level() > self.min_level { + return; + } + + let mut visitor = FieldVisitor::new(); + event.record(&mut visitor); + + let formatted_message = if visitor.fields.is_empty() { + visitor.message + } else if visitor.message.is_empty() { + visitor.fields.join(", ") + } else { + format!("{} {}", visitor.message, visitor.fields.join(", ")) + }; + + io::print(&format!("[{}] {}: {}", metadata.level(), metadata.target(), formatted_message)); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} +} + +/// Custom [`Visit`] implementation to extract log field values. +struct FieldVisitor { + message: String, + fields: Vec, +} + +impl FieldVisitor { + const fn new() -> Self { + Self { message: String::new(), fields: Vec::new() } + } +} + +impl Visit for FieldVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn core::fmt::Debug) { + if field.name() == "message" { + self.message = format!("{value:?}"); + } else { + self.fields.push(format!("{}={:?}", field.name(), value)); + } + } + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "message" { + self.message = value.to_string(); + } else { + self.fields.push(format!("{}={}", field.name(), value)); + } + } +} diff --git a/kona/crates/proof/std-fpvm/src/traits/basic.rs b/kona/crates/proof/std-fpvm/src/traits/basic.rs new file mode 100644 index 0000000000000..a5763ec418bad --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/traits/basic.rs @@ -0,0 +1,28 @@ +//! Defines the [BasicKernelInterface] trait, which describes the functionality of several system +//! calls inside of the kernel. + +use crate::{FileDescriptor, errors::IOResult}; + +/// The [BasicKernelInterface] trait describes the functionality of several core system calls inside +/// of the kernel. +/// +/// Commonly, embedded proving environments delegate IO operations to custom file descriptors. +/// This trait is a safe wrapper around the raw system calls available to the `client` program +/// for host<->client communication. +/// +/// In cases where the set of system calls defined in this trait need to be extended, an additional +/// trait should be created that extends this trait. +pub trait BasicKernelInterface { + /// Write the given buffer to the given file descriptor. + fn write(fd: FileDescriptor, buf: &[u8]) -> IOResult; + + /// Read from the given file descriptor into the passed buffer. + fn read(fd: FileDescriptor, buf: &mut [u8]) -> IOResult; + + /// Map new memory with block size `size`. Returns the new heap pointer. + fn mmap(size: usize) -> IOResult; + + /// Exit the process with the given exit code. The implementation of this function + /// should always panic after invoking the `EXIT` syscall. + fn exit(code: usize) -> !; +} diff --git a/kona/crates/proof/std-fpvm/src/traits/mod.rs b/kona/crates/proof/std-fpvm/src/traits/mod.rs new file mode 100644 index 0000000000000..96f9699ae74cd --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/traits/mod.rs @@ -0,0 +1,10 @@ +//! Contains common traits for the `client` role. +//! +//! When developing a new `client` program, these traits are implemented on an +//! architecture-specific type that provides the concrete implementation of the +//! kernel interfaces. The `client` program then uses these traits to perform operations +//! without needing to know the underlying implementation, which allows the same `client` +//! program to be compiled and ran on different target architectures. + +mod basic; +pub use basic::BasicKernelInterface; diff --git a/kona/crates/proof/std-fpvm/src/types.rs b/kona/crates/proof/std-fpvm/src/types.rs new file mode 100644 index 0000000000000..ceb984cf77a0a --- /dev/null +++ b/kona/crates/proof/std-fpvm/src/types.rs @@ -0,0 +1,67 @@ +//! This module contains the local types for the `kona-std-fpvm` crate. + +/// File descriptors available to the `client` within the FPVM kernel. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileDescriptor { + /// Read-only standard input stream. + StdIn, + /// Write-only standard output stream. + StdOut, + /// Write-only standard error stream. + StdErr, + /// Read-only. Used to read the status of pre-image hinting. + HintRead, + /// Write-only. Used to provide pre-image hints + HintWrite, + /// Read-only. Used to read pre-images. + PreimageRead, + /// Write-only. Used to request pre-images. + PreimageWrite, +} + +impl From for usize { + fn from(fd: FileDescriptor) -> Self { + match fd { + FileDescriptor::StdIn => 0, + FileDescriptor::StdOut => 1, + FileDescriptor::StdErr => 2, + FileDescriptor::HintRead => 3, + FileDescriptor::HintWrite => 4, + FileDescriptor::PreimageRead => 5, + FileDescriptor::PreimageWrite => 6, + } + } +} + +impl From for i32 { + fn from(fd: FileDescriptor) -> Self { + usize::from(fd) as Self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_descriptor_into_usize() { + assert_eq!(usize::from(FileDescriptor::StdIn), 0); + assert_eq!(usize::from(FileDescriptor::StdOut), 1); + assert_eq!(usize::from(FileDescriptor::StdErr), 2); + assert_eq!(usize::from(FileDescriptor::HintRead), 3); + assert_eq!(usize::from(FileDescriptor::HintWrite), 4); + assert_eq!(usize::from(FileDescriptor::PreimageRead), 5); + assert_eq!(usize::from(FileDescriptor::PreimageWrite), 6); + } + + #[test] + fn test_file_descriptor_into_i32() { + assert_eq!(i32::from(FileDescriptor::StdIn), 0); + assert_eq!(i32::from(FileDescriptor::StdOut), 1); + assert_eq!(i32::from(FileDescriptor::StdErr), 2); + assert_eq!(i32::from(FileDescriptor::HintRead), 3); + assert_eq!(i32::from(FileDescriptor::HintWrite), 4); + assert_eq!(i32::from(FileDescriptor::PreimageRead), 5); + assert_eq!(i32::from(FileDescriptor::PreimageWrite), 6); + } +} diff --git a/kona/crates/protocol/derive/CHANGELOG.md b/kona/crates/protocol/derive/CHANGELOG.md new file mode 100644 index 0000000000000..8d9747929f907 --- /dev/null +++ b/kona/crates/protocol/derive/CHANGELOG.md @@ -0,0 +1,373 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.3](https://github.com/op-rs/kona/compare/kona-derive-v0.2.2...kona-derive-v0.2.3) - 2025-01-16 + +### Other + +- Update Maili Deps (#908) +- fix some typos in comment (#906) + +## [0.2.2](https://github.com/op-rs/kona/compare/kona-derive-v0.2.1...kona-derive-v0.2.2) - 2025-01-13 + +### Other + +- Move temporary error logs to lower level than WARN ([#897](https://github.com/op-rs/kona/pull/897)) +- *(deps)* Replace dep `alloy-rs/op-alloy-registry`->`op-rs/maili-registry` (#892) +- *(deps)* Replace dep `alloy-rs/op-alloy-protocol`->`op-rs/maili-protocol` (#890) +- *(derive)* Test Ignoring EIP-7702 (#887) + +## [0.2.1](https://github.com/op-rs/kona/compare/kona-derive-v0.2.0...kona-derive-v0.2.1) - 2025-01-07 + +### Fixed + +- op-rs rename (#883) +- *(derive)* `BatchStream` Past batch handling (#876) +- *(derive)* make tests compile (#878) + +### Other + +- Bump Dependencies (#880) +- patch for rust 1.81 ([#867](https://github.com/op-rs/kona/pull/867)) + +## [0.2.0](https://github.com/op-rs/kona/compare/kona-derive-v0.1.0...kona-derive-v0.2.0) - 2024-12-03 + +### Added + +- *(workspace)* Isolate FPVM-specific platform code ([#821](https://github.com/op-rs/kona/pull/821)) + +### Fixed + +- bump ([#855](https://github.com/op-rs/kona/pull/855)) +- nightly lint ([#858](https://github.com/op-rs/kona/pull/858)) + +### Other + +- *(derive)* remove indexed blob hash ([#847](https://github.com/op-rs/kona/pull/847)) + +## [0.0.7](https://github.com/op-rs/kona/compare/kona-derive-v0.0.6...kona-derive-v0.0.7) - 2024-11-20 + +### Added + +- *(driver)* Abstract, Default Pipeline ([#796](https://github.com/op-rs/kona/pull/796)) + +### Other + +- *(derive)* remove batch reader ([#826](https://github.com/op-rs/kona/pull/826)) +- op-alloy 0.6.8 ([#830](https://github.com/op-rs/kona/pull/830)) +- *(driver)* use tracing macros ([#823](https://github.com/op-rs/kona/pull/823)) +- *(deps)* op-alloy 0.6.7 ([#824](https://github.com/op-rs/kona/pull/824)) +- v0.6.6 op-alloy ([#804](https://github.com/op-rs/kona/pull/804)) +- *(workspace)* Migrate back to `thiserror` v2 ([#811](https://github.com/op-rs/kona/pull/811)) +- *(derive)* Re-export types ([#790](https://github.com/op-rs/kona/pull/790)) +- Revert "chore: bump alloy deps ([#788](https://github.com/op-rs/kona/pull/788))" ([#791](https://github.com/op-rs/kona/pull/791)) + +## [0.0.6](https://github.com/op-rs/kona/compare/kona-derive-v0.0.5...kona-derive-v0.0.6) - 2024-11-06 + +### Added + +- *(derive)* `From for PipelineErrorKind` ([#780](https://github.com/op-rs/kona/pull/780)) +- *(client)* Remove `anyhow` ([#779](https://github.com/op-rs/kona/pull/779)) +- *(derive)* sources docs ([#754](https://github.com/op-rs/kona/pull/754)) + +### Fixed + +- *(derive)* Data Availability Provider Abstraction ([#782](https://github.com/op-rs/kona/pull/782)) +- *(derive)* hoist types out of traits ([#781](https://github.com/op-rs/kona/pull/781)) +- *(client)* Trace extension support ([#778](https://github.com/op-rs/kona/pull/778)) +- *(derive)* use signal value updated with system config. ([#776](https://github.com/op-rs/kona/pull/776)) + +### Other + +- bump alloy deps ([#788](https://github.com/op-rs/kona/pull/788)) +- *(derive)* pipeline error test coverage ([#784](https://github.com/op-rs/kona/pull/784)) +- Only fill blob data when there is no calldata ([#764](https://github.com/op-rs/kona/pull/764)) +- *(derive)* touchup kona-derive readme ([#762](https://github.com/op-rs/kona/pull/762)) +- *(derive)* Error Exports ([#758](https://github.com/op-rs/kona/pull/758)) +- *(derive)* Cleanup Exports ([#757](https://github.com/op-rs/kona/pull/757)) + +## [0.0.5](https://github.com/op-rs/kona/compare/kona-derive-v0.0.4...kona-derive-v0.0.5) - 2024-10-29 + +### Added + +- *(derive)* use upstream op-alloy batch types ([#746](https://github.com/op-rs/kona/pull/746)) +- *(derive)* Remove metrics ([#743](https://github.com/op-rs/kona/pull/743)) +- *(derive)* sys config accessor ([#722](https://github.com/op-rs/kona/pull/722)) + +### Fixed + +- tracing_subscriber problem in `kona-derive` tests ([#741](https://github.com/op-rs/kona/pull/741)) +- *(derive)* Holocene action tests / fixes ([#733](https://github.com/op-rs/kona/pull/733)) + +### Other + +- *(derive)* import hygiene ([#744](https://github.com/op-rs/kona/pull/744)) + +## [0.0.4](https://github.com/op-rs/kona/compare/kona-derive-v0.0.3...kona-derive-v0.0.4) - 2024-10-25 + +### Added + +- remove thiserror ([#735](https://github.com/op-rs/kona/pull/735)) +- *(derive)* `BatchProvider` multiplexed stage ([#726](https://github.com/op-rs/kona/pull/726)) +- *(derive)* hoist stage traits ([#723](https://github.com/op-rs/kona/pull/723)) +- frame queue test asserter ([#619](https://github.com/op-rs/kona/pull/619)) +- *(workspace)* Distribute pipeline, not providers ([#717](https://github.com/op-rs/kona/pull/717)) +- *(derive)* `BatchValidator` stage ([#703](https://github.com/op-rs/kona/pull/703)) +- *(derive)* Add `ChannelAssembler` size limitation ([#700](https://github.com/op-rs/kona/pull/700)) +- *(derive)* signal receiver logic ([#696](https://github.com/op-rs/kona/pull/696)) +- *(derive)* Stage multiplexer ([#693](https://github.com/op-rs/kona/pull/693)) +- *(derive)* `Past` batch validity variant ([#684](https://github.com/op-rs/kona/pull/684)) +- codecov sources ([#657](https://github.com/op-rs/kona/pull/657)) +- frame queue tests ([#613](https://github.com/op-rs/kona/pull/613)) +- *(derive)* Holocene flush signal ([#612](https://github.com/op-rs/kona/pull/612)) +- *(derive)* Add `Signal` API ([#611](https://github.com/op-rs/kona/pull/611)) +- *(derive)* BatchQueue Update [Holocene] ([#601](https://github.com/op-rs/kona/pull/601)) +- *(derive)* bump op-alloy dep ([#605](https://github.com/op-rs/kona/pull/605)) +- kona-providers ([#596](https://github.com/op-rs/kona/pull/596)) +- *(derive)* Span batch prefix checks ([#592](https://github.com/op-rs/kona/pull/592)) +- *(derive)* `BatchStream` buffering ([#590](https://github.com/op-rs/kona/pull/590)) +- *(derive)* BatchStreamProvider ([#591](https://github.com/op-rs/kona/pull/591)) +- *(derive)* Refactor out Online Providers ([#569](https://github.com/op-rs/kona/pull/569)) +- *(derive)* interleaved channel tests ([#585](https://github.com/op-rs/kona/pull/585)) +- *(derive)* Holocene Buffer Flushing ([#575](https://github.com/op-rs/kona/pull/575)) +- *(derive)* Holocene Channel Bank Checks ([#572](https://github.com/op-rs/kona/pull/572)) +- *(derive)* Holocene Frame Queue ([#579](https://github.com/op-rs/kona/pull/579)) +- *(derive)* Holocene Activation ([#574](https://github.com/op-rs/kona/pull/574)) +- *(derive)* wire up the batch span stage ([#567](https://github.com/op-rs/kona/pull/567)) +- *(derive)* New BatchStream Stage for Holocene ([#566](https://github.com/op-rs/kona/pull/566)) +- *(derive)* Hoist AttributesBuilder ([#571](https://github.com/op-rs/kona/pull/571)) +- *(derive)* Touchup Docs ([#555](https://github.com/op-rs/kona/pull/555)) +- *(derive)* Latest BN ([#521](https://github.com/op-rs/kona/pull/521)) +- Remove L2 Execution Payload ([#542](https://github.com/op-rs/kona/pull/542)) +- *(derive)* Typed error handling ([#540](https://github.com/op-rs/kona/pull/540)) +- *(primitives)* Remove Attributes ([#529](https://github.com/op-rs/kona/pull/529)) +- large dependency update ([#528](https://github.com/op-rs/kona/pull/528)) +- *(primitives)* reuse op-alloy-protocol channel and block types ([#499](https://github.com/op-rs/kona/pull/499)) + +### Fixed + +- hashmap ([#732](https://github.com/op-rs/kona/pull/732)) +- *(derive)* SpanBatch element limit + channel RLP size limit ([#692](https://github.com/op-rs/kona/pull/692)) +- *(derive)* Holocene `SpanBatch` prefix checks ([#688](https://github.com/op-rs/kona/pull/688)) +- *(derive)* Retain L1 blocks ([#683](https://github.com/op-rs/kona/pull/683)) +- *(executor)* Holocene EIP-1559 params in Header ([#622](https://github.com/op-rs/kona/pull/622)) +- derive pipeline params ([#587](https://github.com/op-rs/kona/pull/587)) +- *(workspace)* hoist and fix lints ([#577](https://github.com/op-rs/kona/pull/577)) +- *(derive)* move attributes builder trait ([#570](https://github.com/op-rs/kona/pull/570)) +- *(client)* Channel reader error handling ([#539](https://github.com/op-rs/kona/pull/539)) +- *(derive)* Sequence window expiry ([#532](https://github.com/op-rs/kona/pull/532)) +- *(primitives)* use consensus hardforks ([#497](https://github.com/op-rs/kona/pull/497)) +- *(primitives)* re-use op-alloy frame type ([#492](https://github.com/op-rs/kona/pull/492)) + +### Other + +- re-org imports ([#711](https://github.com/op-rs/kona/pull/711)) +- hoist trait test utilities ([#708](https://github.com/op-rs/kona/pull/708)) +- *(workspace)* Removes Primitives ([#638](https://github.com/op-rs/kona/pull/638)) +- *(derive)* Add tracing to `ChannelAssembler` ([#701](https://github.com/op-rs/kona/pull/701)) +- *(derive)* remove span batch todo comments ([#682](https://github.com/op-rs/kona/pull/682)) +- refactor test utils ([#677](https://github.com/op-rs/kona/pull/677)) +- *(derive)* stage coverage ([#673](https://github.com/op-rs/kona/pull/673)) +- *(executor)* Use Upstreamed op-alloy Methods ([#651](https://github.com/op-rs/kona/pull/651)) +- *(derive)* Test and Clean Batch Types ([#670](https://github.com/op-rs/kona/pull/670)) +- *(derive)* Test Stage Resets and Flushes ([#669](https://github.com/op-rs/kona/pull/669)) +- *(derive)* Batch Timestamp Tests ([#664](https://github.com/op-rs/kona/pull/664)) +- *(derive)* test channel reader flushing ([#661](https://github.com/op-rs/kona/pull/661)) +- *(derive)* adds more channel bank coverage ([#659](https://github.com/op-rs/kona/pull/659)) +- *(derive)* test channel reader resets ([#660](https://github.com/op-rs/kona/pull/660)) +- *(derive)* test channel bank reset ([#658](https://github.com/op-rs/kona/pull/658)) +- *(derive)* hoist attributes queue test utils ([#654](https://github.com/op-rs/kona/pull/654)) +- *(derive)* Pipeline Core Test Coverage ([#642](https://github.com/op-rs/kona/pull/642)) +- *(derive)* Single Batch Test Coverage ([#643](https://github.com/op-rs/kona/pull/643)) +- *(derive)* Blob Source Test Coverage ([#631](https://github.com/op-rs/kona/pull/631)) +- refactor test providers ([#623](https://github.com/op-rs/kona/pull/623)) +- doc logos ([#609](https://github.com/op-rs/kona/pull/609)) +- use alloy primitives map ([#586](https://github.com/op-rs/kona/pull/586)) +- *(derive)* [Holocene] Drain previous channel in one iteration ([#583](https://github.com/op-rs/kona/pull/583)) +- channel reader docs ([#568](https://github.com/op-rs/kona/pull/568)) +- Bumps Dependency Versions ([#520](https://github.com/op-rs/kona/pull/520)) +- *(primitives)* rm RawTransaction ([#505](https://github.com/op-rs/kona/pull/505)) + +## [0.0.3](https://github.com/op-rs/kona/compare/kona-derive-v0.0.2...kona-derive-v0.0.3) - 2024-09-04 + +### Added +- Run cargo hack against workspace ([#485](https://github.com/op-rs/kona/pull/485)) +- *(workspace)* Workspace Re-exports ([#468](https://github.com/op-rs/kona/pull/468)) +- *(ci)* Add scheduled FPP differential tests ([#408](https://github.com/op-rs/kona/pull/408)) +- *(derive+trusted-sync)* online blob provider with fallback ([#410](https://github.com/op-rs/kona/pull/410)) +- increase granularity ([#365](https://github.com/op-rs/kona/pull/365)) +- *(derive)* histogram for number of channels for given frame counts ([#337](https://github.com/op-rs/kona/pull/337)) +- *(derive)* track the current channel size ([#331](https://github.com/op-rs/kona/pull/331)) +- *(derive)* more stage metrics ([#326](https://github.com/op-rs/kona/pull/326)) +- *(derive)* Granular Provider Metrics ([#325](https://github.com/op-rs/kona/pull/325)) +- *(derive)* Stage Level Metrics ([#309](https://github.com/op-rs/kona/pull/309)) +- *(examples)* Trusted Sync Metrics ([#308](https://github.com/op-rs/kona/pull/308)) + +### Fixed +- downgrade for release plz ([#458](https://github.com/op-rs/kona/pull/458)) +- *(workspace)* Add Unused Dependency Lint ([#453](https://github.com/op-rs/kona/pull/453)) +- *(derive)* remove fpvm tests ([#447](https://github.com/op-rs/kona/pull/447)) +- *(derive)* Granite Hardfork Support ([#420](https://github.com/op-rs/kona/pull/420)) +- remove data iter option ([#405](https://github.com/op-rs/kona/pull/405)) +- *(deps)* Bump Alloy Dependencies ([#409](https://github.com/op-rs/kona/pull/409)) +- *(kona-derive)* Remove SignedRecoverable Shim ([#400](https://github.com/op-rs/kona/pull/400)) +- *(derive)* Pipeline Reset ([#383](https://github.com/op-rs/kona/pull/383)) +- *(examples)* Start N Blocks Back from Tip ([#349](https://github.com/op-rs/kona/pull/349)) +- *(derive)* Unused var w/o `metrics` feature ([#345](https://github.com/op-rs/kona/pull/345)) +- *(derive)* bind the Pipeline trait to Iterator ([#334](https://github.com/op-rs/kona/pull/334)) +- *(derive)* prefix all metric names ([#330](https://github.com/op-rs/kona/pull/330)) +- *(examples)* don't panic on validation fetch failure ([#327](https://github.com/op-rs/kona/pull/327)) +- *(derive)* Warnings with metrics macro ([#322](https://github.com/op-rs/kona/pull/322)) + +### Other +- *(workspace)* Alloy Version Bumps ([#467](https://github.com/op-rs/kona/pull/467)) +- *(workspace)* Update for `op-rs` org transfer ([#474](https://github.com/op-rs/kona/pull/474)) +- *(workspace)* Hoist Dependencies ([#466](https://github.com/op-rs/kona/pull/466)) +- *(derive)* reset docs ([#464](https://github.com/op-rs/kona/pull/464)) +- *(derive)* Remove udeps ([#462](https://github.com/op-rs/kona/pull/462)) +- *(bin)* Remove `kt` ([#461](https://github.com/op-rs/kona/pull/461)) +- refactor types out of kona-derive ([#454](https://github.com/op-rs/kona/pull/454)) +- *(derive)* Channel timeout ([#437](https://github.com/op-rs/kona/pull/437)) +- *(derive)* remove previous stage trait ([#423](https://github.com/op-rs/kona/pull/423)) +- *(examples)* Add logs to trusted-sync ([#415](https://github.com/op-rs/kona/pull/415)) +- *(derive)* refine channel frame count buckets ([#378](https://github.com/op-rs/kona/pull/378)) +- *(derive)* Remove noisy batch logs ([#329](https://github.com/op-rs/kona/pull/329)) +- clean up trusted sync loop ([#318](https://github.com/op-rs/kona/pull/318)) +- *(docs)* Label Cleanup ([#307](https://github.com/op-rs/kona/pull/307)) +- *(derive)* add targets to stage logs ([#310](https://github.com/op-rs/kona/pull/310)) + +## [0.0.2](https://github.com/op-rs/kona/compare/kona-derive-v0.0.1...kona-derive-v0.0.2) - 2024-06-22 + +### Added +- *(fjord)* fjord parameter changes ([#284](https://github.com/op-rs/kona/pull/284)) +- *(client/host)* Oracle-backed Blob fetcher ([#255](https://github.com/op-rs/kona/pull/255)) +- *(kona-derive)* Towards Derivation ([#243](https://github.com/op-rs/kona/pull/243)) +- *(kona-derive)* Updated interface ([#230](https://github.com/op-rs/kona/pull/230)) +- *(ci)* Dependabot config ([#236](https://github.com/op-rs/kona/pull/236)) +- *(client)* `StatelessL2BlockExecutor` ([#210](https://github.com/op-rs/kona/pull/210)) +- Pipeline Builder ([#217](https://github.com/op-rs/kona/pull/217)) +- Minimal ResetProvider Implementation ([#208](https://github.com/op-rs/kona/pull/208)) +- refactor the pipeline builder ([#209](https://github.com/op-rs/kona/pull/209)) +- refactor reset provider ([#207](https://github.com/op-rs/kona/pull/207)) +- *(preimage)* Async server components ([#183](https://github.com/op-rs/kona/pull/183)) +- *(workspace)* Client programs in workspace ([#178](https://github.com/op-rs/kona/pull/178)) +- *(primitives)* move attributes into primitives ([#163](https://github.com/op-rs/kona/pull/163)) +- *(derive)* return the concrete online attributes queue type from the online stack constructor ([#158](https://github.com/op-rs/kona/pull/158)) +- *(derive)* Abstract Alt DA out of `kona-derive` ([#156](https://github.com/op-rs/kona/pull/156)) +- *(derive)* Online Data Source Factory Wiring ([#150](https://github.com/op-rs/kona/pull/150)) +- *(plasma)* Implements Plasma Support for kona derive ([#152](https://github.com/op-rs/kona/pull/152)) +- *(derive)* Pipeline Builder ([#127](https://github.com/op-rs/kona/pull/127)) +- *(primitives)* kona-derive type refactor ([#135](https://github.com/op-rs/kona/pull/135)) +- *(derive)* Span Batch Validation ([#121](https://github.com/op-rs/kona/pull/121)) +- *(derive)* Use `L2ChainProvider` for system config fetching in attributes builder ([#123](https://github.com/op-rs/kona/pull/123)) +- *(derive)* Online Blob Provider ([#117](https://github.com/op-rs/kona/pull/117)) +- *(derive)* payload builder tests ([#106](https://github.com/op-rs/kona/pull/106)) +- *(derive)* deposit derivation testing ([#115](https://github.com/op-rs/kona/pull/115)) +- *(derive)* Build `L1BlockInfoTx` in payload builder ([#102](https://github.com/op-rs/kona/pull/102)) +- *(derive)* `L2ChainProvider` w/ `op-alloy-consensus` ([#98](https://github.com/op-rs/kona/pull/98)) +- *(derive)* Add `L1BlockInfoTx` ([#100](https://github.com/op-rs/kona/pull/100)) +- *(derive)* Payload Attribute Building ([#92](https://github.com/op-rs/kona/pull/92)) +- *(derive)* Online `ChainProvider` ([#93](https://github.com/op-rs/kona/pull/93)) +- *(derive)* Move to `tracing` for telemetry ([#94](https://github.com/op-rs/kona/pull/94)) +- *(derive)* Batch Queue Logging ([#86](https://github.com/op-rs/kona/pull/86)) +- *(derive)* Add `ecrecover` trait + features ([#90](https://github.com/op-rs/kona/pull/90)) +- *(derive)* Use upstream alloy ([#89](https://github.com/op-rs/kona/pull/89)) +- *(derive)* add next_attributes test +- *(workspace)* Add `rustfmt.toml` +- *(derive)* `SpanBatch` type implementation WIP +- *(derive)* Reorganize modules +- *(derive)* `add_txs` function +- *(derive)* Derive raw batches, mocks +- *(derive)* Refactor serialization; `SpanBatchPayload` WIP +- *(derive)* fixed bytes and encoding +- *(derive)* raw span type refactoring +- *(types)* span batches +- *(derive)* Channel Reader Implementation ([#65](https://github.com/op-rs/kona/pull/65)) +- *(derive)* share the rollup config across stages using an arc +- *(derive)* Test Utilities ([#62](https://github.com/op-rs/kona/pull/62)) +- Single batch type ([#43](https://github.com/op-rs/kona/pull/43)) +- *(derive)* channel bank ([#46](https://github.com/op-rs/kona/pull/46)) +- Frame queue stage ([#45](https://github.com/op-rs/kona/pull/45)) +- L1 retrieval ([#44](https://github.com/op-rs/kona/pull/44)) +- System config update event parsing ([#42](https://github.com/op-rs/kona/pull/42)) +- Add OP receipt fields ([#41](https://github.com/op-rs/kona/pull/41)) +- Add `TxDeposit` type ([#40](https://github.com/op-rs/kona/pull/40)) +- L1 traversal ([#39](https://github.com/op-rs/kona/pull/39)) + +### Fixed +- *(derive)* Fjord brotli decompression ([#298](https://github.com/op-rs/kona/pull/298)) +- *(examples)* Dynamic Rollup Config Loading ([#293](https://github.com/op-rs/kona/pull/293)) +- type re-exports ([#280](https://github.com/op-rs/kona/pull/280)) +- *(kona-derive)* reuse upstream reqwest provider ([#229](https://github.com/op-rs/kona/pull/229)) +- Derivation Pipeline ([#220](https://github.com/op-rs/kona/pull/220)) +- *(derive)* Alloy EIP4844 Blob Type ([#215](https://github.com/op-rs/kona/pull/215)) +- Strong Error Typing ([#187](https://github.com/op-rs/kona/pull/187)) +- *(derive)* inline blob verification into the blob provider ([#175](https://github.com/op-rs/kona/pull/175)) +- *(derive)* fix span batch utils read_tx_data() ([#170](https://github.com/op-rs/kona/pull/170)) +- *(derive)* Ethereum Data Source ([#159](https://github.com/op-rs/kona/pull/159)) +- *(derive)* remove unnecessary online feature decorator ([#160](https://github.com/op-rs/kona/pull/160)) +- *(ci)* Release plz ([#145](https://github.com/op-rs/kona/pull/145)) +- *(derive)* move span batch conversion to try from trait ([#142](https://github.com/op-rs/kona/pull/142)) +- *(derive)* Small Fixes and Span Batch Validation Fix ([#139](https://github.com/op-rs/kona/pull/139)) +- *(workspace)* Release plz ([#138](https://github.com/op-rs/kona/pull/138)) +- *(workspace)* Release plz ([#137](https://github.com/op-rs/kona/pull/137)) +- *(derive)* Rebase span batch validation tests ([#125](https://github.com/op-rs/kona/pull/125)) +- *(derive)* Span batch bitlist encoding ([#122](https://github.com/op-rs/kona/pull/122)) +- *(derive)* Doc Touchups and Telemetry ([#105](https://github.com/op-rs/kona/pull/105)) +- *(derive)* Derive full `SpanBatch` in channel reader ([#97](https://github.com/op-rs/kona/pull/97)) +- *(derive)* Stage Decoupling ([#88](https://github.com/op-rs/kona/pull/88)) +- *(derive)* add back removed test +- *(derive)* lints +- *(derive)* extend attributes queue unit test +- *(derive)* successful payload attributes building tests +- *(derive)* error equality fixes and tests +- *(derive)* rework abstractions and attributes queue testing +- *(derive)* attributes queue +- *(derive)* hoist params +- *(derive)* merge upstream changes +- *(derive)* fix bricked arc stage param construction ([#84](https://github.com/op-rs/kona/pull/84)) +- *(derive)* l1 retrieval docs ([#80](https://github.com/op-rs/kona/pull/80)) +- *(derive)* clean up frame queue docs +- *(derive)* frame queue error bubbling and docs +- *(derive)* rebase +- *(derive)* merge upstream changes +- *(derive)* refactor tx enveloped +- *(derive)* refactor span batch tx types +- *(derive)* bitlist alignment +- *(derive)* span batch tx rlp +- *(derive)* span type encodings and decodings +- *(derive)* more types +- *(derive)* small l1 retrieval doc comment fix ([#61](https://github.com/op-rs/kona/pull/61)) + +### Other +- version dependencies ([#296](https://github.com/op-rs/kona/pull/296)) +- payload decoding tests ([#287](https://github.com/op-rs/kona/pull/287)) +- payload decoding tests ([#289](https://github.com/op-rs/kona/pull/289)) +- re-export input types ([#279](https://github.com/op-rs/kona/pull/279)) +- *(deps)* fast forward op alloy dep ([#267](https://github.com/op-rs/kona/pull/267)) +- *(derive)* cleanup pipeline tracing ([#264](https://github.com/op-rs/kona/pull/264)) +- *(derive)* online module touchups ([#265](https://github.com/op-rs/kona/pull/265)) +- *(derive)* Sources Touchups ([#266](https://github.com/op-rs/kona/pull/266)) +- *(kona-derive)* Online Pipeline Cleanup ([#241](https://github.com/op-rs/kona/pull/241)) +- *(derive)* data source unit tests ([#181](https://github.com/op-rs/kona/pull/181)) +- *(workspace)* Move `alloy-primitives` to workspace dependencies ([#103](https://github.com/op-rs/kona/pull/103)) +- *(ci)* Fail CI on doclint failure ([#101](https://github.com/op-rs/kona/pull/101)) +- *(derive)* cleanups ([#91](https://github.com/op-rs/kona/pull/91)) +- Merge branch 'main' into refcell/data-sources +- Merge pull request [#87](https://github.com/op-rs/kona/pull/87) from op-rs/refcell/origin-providers +- Merge branch 'main' into refcell/channel-bank-tests +- Merge branch 'main' into refcell/payload-queue +- *(derive)* L1Traversal Doc and Test Cleanup ([#79](https://github.com/op-rs/kona/pull/79)) +- Merge pull request [#67](https://github.com/op-rs/kona/pull/67) from op-rs/refcell/batch-queue +- *(derive)* Channel reader tests + fixes, batch type fixes +- *(derive)* `RawSpanBatch` diff decoding/encoding test +- *(derive)* rebase + move `alloy` module +- *(derive)* Clean up RLP encoding + use `TxType` rather than ints +- Update `derive` lint rules ([#47](https://github.com/op-rs/kona/pull/47)) +- scaffold ([#37](https://github.com/op-rs/kona/pull/37)) +- Make versions of packages independent ([#36](https://github.com/op-rs/kona/pull/36)) diff --git a/kona/crates/protocol/derive/Cargo.toml b/kona/crates/protocol/derive/Cargo.toml new file mode 100644 index 0000000000000..4fe3311d4c423 --- /dev/null +++ b/kona/crates/protocol/derive/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "kona-derive" +description = "A no_std derivation pipeline implementation for the OP Stack" +version = "0.4.5" +resolver = "2" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Protocol +kona-macros.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true +kona-hardforks.workspace = true + +# Alloy +alloy-eips.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-rlp = { workspace = true, features = ["derive"] } +alloy-consensus = { workspace = true, features = ["k256"] } +alloy-primitives = { workspace = true, features = ["rlp", "k256", "map"] } + +# Op Alloy +op-alloy-rpc-types-engine.workspace = true +op-alloy-consensus = { workspace = true, features = ["k256"] } + +# General +tracing.workspace = true +async-trait.workspace = true +thiserror.workspace = true +serde = { workspace = true, optional = true } + +# `test-utils` feature dependencies +spin = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true, features = ["fmt"] } + +# `metrics` feature +metrics = { workspace = true, optional = true } + +[dev-dependencies] +spin.workspace = true +proptest.workspace = true +serde_json.workspace = true +kona-registry.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } +tracing = { workspace = true, features = ["std"] } +alloy-primitives = { workspace = true, features = ["rlp", "k256", "map", "arbitrary"] } +op-alloy-consensus = { workspace = true, features = ["k256"] } + +[features] +default = [] +metrics = [ "dep:metrics" ] +serde = [ + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-primitives/serde", + "alloy-rpc-types-engine/serde", + "dep:serde", + "kona-genesis/serde", + "kona-protocol/serde", + "op-alloy-consensus/serde", + "op-alloy-rpc-types-engine/serde", + "tracing-subscriber?/serde", +] +test-utils = [ + "dep:spin", + "dep:tracing-subscriber", + "kona-protocol/test-utils", +] diff --git a/kona/crates/protocol/derive/README.md b/kona/crates/protocol/derive/README.md new file mode 100644 index 0000000000000..bf6b19f50e398 --- /dev/null +++ b/kona/crates/protocol/derive/README.md @@ -0,0 +1,66 @@ +# `kona-derive` + +CI +Kona Derive +License +Codecov + +A `no_std` compatible implementation of the OP Stack's [derivation pipeline][derive]. + +[derive]: (https://specs.optimism.io/protocol/derivation.html#l2-chain-derivation-specification). + +## Usage + +The intended way of working with `kona-derive` is to use the [`DerivationPipeline`][dp] which implements the [`Pipeline`][p] trait. To create an instance of the [`DerivationPipeline`][dp], it's recommended to use the [`PipelineBuilder`][pb] as follows. + +```rust,ignore +use std::sync::Arc; +use kona_genesis::RollupConfig; +use kona_derive::EthereumDataSource; +use kona_derive::PipelineBuilder; +use kona_derive::StatefulAttributesBuilder; + +let chain_provider = todo!(); +let l2_chain_provider = todo!(); +let blob_provider = todo!(); +let l1_origin = todo!(); + +let cfg = Arc::new(RollupConfig::default()); +let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l2_chain_provider.clone(), + chain_provider.clone(), +); +let dap = EthereumDataSource::new( + chain_provider.clone(), + blob_provider, + cfg.as_ref() +); + +// Construct a new derivation pipeline. +let pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(dap) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(attributes) + .origin(l1_origin) + .build(); +``` + +[p]: ./src/traits/pipeline.rs +[pb]: ./src/pipeline/builder.rs +[dp]: ./src/pipeline/core.rs + +## Features + +The most up-to-date feature list will be available on the [docs.rs `Feature Flags` tab][ff] of the `kona-derive` crate. + +Some features include the following. +- `serde`: Serialization and Deserialization support for `kona-derive` types. +- `test-utils`: Test utilities for downstream libraries. + +By default, `kona-derive` enables the `serde` feature. + +[ap]: https://docs.rs/crate/alloy-providers/latest +[ff]: https://docs.rs/crate/kona-derive/latest/features diff --git a/kona/crates/protocol/derive/src/attributes/mod.rs b/kona/crates/protocol/derive/src/attributes/mod.rs new file mode 100644 index 0000000000000..5691685f99a74 --- /dev/null +++ b/kona/crates/protocol/derive/src/attributes/mod.rs @@ -0,0 +1,6 @@ +//! Module containing the [AttributesBuilder] trait implementations. +//! +//! [AttributesBuilder]: crate::traits::AttributesBuilder + +mod stateful; +pub use stateful::StatefulAttributesBuilder; diff --git a/kona/crates/protocol/derive/src/attributes/stateful.rs b/kona/crates/protocol/derive/src/attributes/stateful.rs new file mode 100644 index 0000000000000..f874c51a6a8fb --- /dev/null +++ b/kona/crates/protocol/derive/src/attributes/stateful.rs @@ -0,0 +1,658 @@ +//! The [`AttributesBuilder`] and it's default implementation. + +use crate::{ + AttributesBuilder, BuilderError, ChainProvider, L2ChainProvider, PipelineEncodingError, + PipelineError, PipelineErrorKind, PipelineResult, +}; +use alloc::{boxed::Box, fmt::Debug, string::ToString, sync::Arc, vec, vec::Vec}; +use alloy_consensus::{Eip658Value, Receipt}; +use alloy_eips::{BlockNumHash, eip2718::Encodable2718}; +use alloy_primitives::{Address, B256, Bytes}; +use alloy_rlp::Encodable; +use alloy_rpc_types_engine::PayloadAttributes; +use async_trait::async_trait; +use kona_genesis::{L1ChainConfig, RollupConfig}; +use kona_hardforks::{Hardfork, Hardforks}; +use kona_protocol::{ + DEPOSIT_EVENT_ABI_HASH, L1BlockInfoTx, L2BlockInfo, Predeploys, decode_deposit, +}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// A stateful implementation of the [`AttributesBuilder`]. +#[derive(Debug, Default)] +pub struct StatefulAttributesBuilder +where + L1P: ChainProvider + Debug, + L2P: L2ChainProvider + Debug, +{ + /// The rollup config. + rollup_cfg: Arc, + /// The L1 config. + l1_cfg: Arc, + /// The system config fetcher. + config_fetcher: L2P, + /// The L1 receipts fetcher. + receipts_fetcher: L1P, +} + +impl StatefulAttributesBuilder +where + L1P: ChainProvider + Debug, + L2P: L2ChainProvider + Debug, +{ + /// Create a new [`StatefulAttributesBuilder`] with the given epoch. + pub const fn new( + rcfg: Arc, + l1_cfg: Arc, + sys_cfg_fetcher: L2P, + receipts: L1P, + ) -> Self { + Self { + rollup_cfg: rcfg, + l1_cfg, + config_fetcher: sys_cfg_fetcher, + receipts_fetcher: receipts, + } + } +} + +#[async_trait] +impl AttributesBuilder for StatefulAttributesBuilder +where + L1P: ChainProvider + Debug + Send, + L2P: L2ChainProvider + Debug + Send, +{ + async fn prepare_payload_attributes( + &mut self, + l2_parent: L2BlockInfo, + epoch: BlockNumHash, + ) -> PipelineResult { + let l1_header; + let deposit_transactions: Vec; + + let mut sys_config = self + .config_fetcher + .system_config_by_number(l2_parent.block_info.number, self.rollup_cfg.clone()) + .await + .map_err(Into::into)?; + + // If the L1 origin changed in this block, then we are in the first block of the epoch. + // In this case we need to fetch all transaction receipts from the L1 origin block so + // we can scan for user deposits. + let sequence_number = if l2_parent.l1_origin.number != epoch.number { + let header = + self.receipts_fetcher.header_by_hash(epoch.hash).await.map_err(Into::into)?; + if l2_parent.l1_origin.hash != header.parent_hash { + return Err(PipelineErrorKind::Reset( + BuilderError::BlockMismatchEpochReset( + epoch, + l2_parent.l1_origin, + header.parent_hash, + ) + .into(), + )); + } + let receipts = + self.receipts_fetcher.receipts_by_hash(epoch.hash).await.map_err(Into::into)?; + let deposits = + derive_deposits(epoch.hash, &receipts, self.rollup_cfg.deposit_contract_address) + .await + .map_err(|e| PipelineError::BadEncoding(e).crit())?; + sys_config + .update_with_receipts( + &receipts, + self.rollup_cfg.l1_system_config_address, + self.rollup_cfg.is_ecotone_active(header.timestamp), + ) + .map_err(|e| PipelineError::SystemConfigUpdate(e).crit())?; + l1_header = header; + deposit_transactions = deposits; + 0 + } else { + #[allow(clippy::collapsible_else_if)] + if l2_parent.l1_origin.hash != epoch.hash { + return Err(PipelineErrorKind::Reset( + BuilderError::BlockMismatch(epoch, l2_parent.l1_origin).into(), + )); + } + + let header = + self.receipts_fetcher.header_by_hash(epoch.hash).await.map_err(Into::into)?; + l1_header = header; + deposit_transactions = vec![]; + l2_parent.seq_num + 1 + }; + + // Sanity check the L1 origin was correctly selected to maintain the time invariant + // between L1 and L2. + let next_l2_time = l2_parent.block_info.timestamp + self.rollup_cfg.block_time; + if next_l2_time < l1_header.timestamp { + return Err(PipelineErrorKind::Reset( + BuilderError::BrokenTimeInvariant( + l2_parent.l1_origin, + next_l2_time, + BlockNumHash { hash: l1_header.hash_slow(), number: l1_header.number }, + l1_header.timestamp, + ) + .into(), + )); + } + + let mut upgrade_transactions: Vec = vec![]; + if self.rollup_cfg.is_ecotone_active(next_l2_time) && + !self.rollup_cfg.is_ecotone_active(l2_parent.block_info.timestamp) + { + upgrade_transactions = Hardforks::ECOTONE.txs().collect(); + } + if self.rollup_cfg.is_fjord_active(next_l2_time) && + !self.rollup_cfg.is_fjord_active(l2_parent.block_info.timestamp) + { + upgrade_transactions.append(&mut Hardforks::FJORD.txs().collect()); + } + if self.rollup_cfg.is_isthmus_active(next_l2_time) && + !self.rollup_cfg.is_isthmus_active(l2_parent.block_info.timestamp) + { + upgrade_transactions.append(&mut Hardforks::ISTHMUS.txs().collect()); + } + if self.rollup_cfg.is_jovian_active(next_l2_time) && + !self.rollup_cfg.is_jovian_active(l2_parent.block_info.timestamp) + { + upgrade_transactions.append(&mut Hardforks::JOVIAN.txs().collect()); + } + if self.rollup_cfg.is_interop_active(next_l2_time) && + !self.rollup_cfg.is_interop_active(l2_parent.block_info.timestamp) + { + upgrade_transactions.append(&mut Hardforks::INTEROP.txs().collect()); + } + + // Build and encode the L1 info transaction for the current payload. + let (_, l1_info_tx_envelope) = L1BlockInfoTx::try_new_with_deposit_tx( + &self.rollup_cfg, + &self.l1_cfg, + &sys_config, + sequence_number, + &l1_header, + next_l2_time, + ) + .map_err(|e| { + PipelineError::AttributesBuilder(BuilderError::Custom(e.to_string())).crit() + })?; + let mut encoded_l1_info_tx = Vec::with_capacity(l1_info_tx_envelope.length()); + l1_info_tx_envelope.encode_2718(&mut encoded_l1_info_tx); + + let mut txs = + Vec::with_capacity(1 + deposit_transactions.len() + upgrade_transactions.len()); + txs.push(encoded_l1_info_tx.into()); + txs.extend(deposit_transactions); + txs.extend(upgrade_transactions); + + let mut withdrawals = None; + if self.rollup_cfg.is_canyon_active(next_l2_time) { + withdrawals = Some(Vec::default()); + } + + let mut parent_beacon_root = None; + if self.rollup_cfg.is_ecotone_active(next_l2_time) { + // if the parent beacon root is not available, default to zero hash + parent_beacon_root = Some(l1_header.parent_beacon_block_root.unwrap_or_default()); + } + + Ok(OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_l2_time, + prev_randao: l1_header.mix_hash, + suggested_fee_recipient: Predeploys::SEQUENCER_FEE_VAULT, + parent_beacon_block_root: parent_beacon_root, + withdrawals, + }, + transactions: Some(txs), + no_tx_pool: Some(true), + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(sys_config.gas_limit).to_be_bytes(), + )), + eip_1559_params: sys_config.eip_1559_params( + &self.rollup_cfg, + l2_parent.block_info.timestamp, + next_l2_time, + ), + min_base_fee: self + .rollup_cfg + .is_jovian_active(next_l2_time) + .then(|| sys_config.min_base_fee.unwrap_or_default()), /* Default to zero if not + * set at Jovian */ + }) + } +} + +/// Derive deposits as `Vec` for transaction receipts. +/// +/// Successful deposits must be emitted by the deposit contract and have the correct event +/// signature. So the receipt address must equal the specified deposit contract and the first topic +/// must be the [`DEPOSIT_EVENT_ABI_HASH`]. +async fn derive_deposits( + block_hash: B256, + receipts: &[Receipt], + deposit_contract: Address, +) -> Result, PipelineEncodingError> { + let mut global_index = 0; + let mut res = Vec::new(); + for r in receipts.iter() { + if Eip658Value::Eip658(false) == r.status { + continue; + } + for l in r.logs.iter() { + let curr_index = global_index; + global_index += 1; + if l.data.topics().first().is_none_or(|i| *i != DEPOSIT_EVENT_ABI_HASH) { + continue; + } + if l.address != deposit_contract { + continue; + } + let decoded = decode_deposit(block_hash, curr_index, l)?; + res.push(decoded); + } + } + Ok(res) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::ResetError, + test_utils::{TestChainProvider, TestSystemConfigL2Fetcher}, + }; + use alloc::vec; + use alloy_consensus::Header; + use alloy_primitives::{B256, Log, LogData, U64, U256, address}; + use kona_genesis::{HardForkConfig, SystemConfig}; + use kona_protocol::{BlockInfo, DepositError}; + use kona_registry::L1Config; + + fn generate_valid_log() -> Log { + let deposit_contract = address!("1111111111111111111111111111111111111111"); + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + let from = address!("2222222222222222222222222222222222222222"); + let mut from_bytes = vec![0u8; 32]; + from_bytes[12..32].copy_from_slice(from.as_slice()); + let to = address!("3333333333333333333333333333333333333333"); + let mut to_bytes = vec![0u8; 32]; + to_bytes[12..32].copy_from_slice(to.as_slice()); + Log { + address: deposit_contract, + data: LogData::new_unchecked( + vec![ + DEPOSIT_EVENT_ABI_HASH, + B256::from_slice(&from_bytes), + B256::from_slice(&to_bytes), + B256::default(), + ], + Bytes::from(data), + ), + } + } + + fn generate_valid_receipt() -> Receipt { + let mut bad_dest_log = generate_valid_log(); + bad_dest_log.data.topics_mut()[1] = B256::default(); + let mut invalid_topic_log = generate_valid_log(); + invalid_topic_log.data.topics_mut()[0] = B256::default(); + Receipt { + status: Eip658Value::Eip658(true), + logs: vec![generate_valid_log(), bad_dest_log, invalid_topic_log], + ..Default::default() + } + } + + #[tokio::test] + async fn test_derive_deposits_empty() { + let receipts = vec![]; + let deposit_contract = Address::default(); + let result = derive_deposits(B256::default(), &receipts, deposit_contract).await; + assert!(result.unwrap().is_empty()); + } + + #[tokio::test] + async fn test_derive_deposits_non_deposit_events_filtered_out() { + let deposit_contract = address!("1111111111111111111111111111111111111111"); + let mut invalid = generate_valid_receipt(); + invalid.logs[0].data = LogData::new_unchecked(vec![], Bytes::default()); + let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid]; + let result = derive_deposits(B256::default(), &receipts, deposit_contract).await; + assert_eq!(result.unwrap().len(), 5); + } + + #[tokio::test] + async fn test_derive_deposits_non_deposit_contract_addr() { + let deposit_contract = address!("1111111111111111111111111111111111111111"); + let mut invalid = generate_valid_receipt(); + invalid.logs[0].address = Address::default(); + let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid]; + let result = derive_deposits(B256::default(), &receipts, deposit_contract).await; + assert_eq!(result.unwrap().len(), 5); + } + + #[tokio::test] + async fn test_derive_deposits_decoding_errors() { + let deposit_contract = address!("1111111111111111111111111111111111111111"); + let mut invalid = generate_valid_receipt(); + invalid.logs[0].data = + LogData::new_unchecked(vec![DEPOSIT_EVENT_ABI_HASH], Bytes::default()); + let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid]; + let result = derive_deposits(B256::default(), &receipts, deposit_contract).await; + let downcasted = result.unwrap_err(); + assert_eq!(downcasted, DepositError::UnexpectedTopicsLen(1).into()); + } + + #[tokio::test] + async fn test_derive_deposits_succeeds() { + let deposit_contract = address!("1111111111111111111111111111111111111111"); + let receipts = vec![generate_valid_receipt(), generate_valid_receipt()]; + let result = derive_deposits(B256::default(), &receipts, deposit_contract).await; + assert_eq!(result.unwrap().len(), 4); + } + + #[tokio::test] + async fn test_prepare_payload_block_mismatch_epoch_reset() { + let cfg = Arc::new(RollupConfig::default()); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header::default(); + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() }, + l1_origin: BlockNumHash { hash: B256::left_padding_from(&[0xFF]), number: 2 }, + seq_num: 0, + }; + // This should error because the l2 parent's l1_origin.hash should equal the epoch header + // hash. Here we use the default header whose hash will not equal the custom `l2_hash`. + let expected = + BuilderError::BlockMismatchEpochReset(epoch, l2_parent.l1_origin, B256::default()); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, PipelineErrorKind::Reset(expected.into())); + } + + #[tokio::test] + async fn test_prepare_payload_block_mismatch() { + let cfg = Arc::new(RollupConfig::default()); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header::default(); + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() }, + l1_origin: BlockNumHash { hash: B256::ZERO, number: l2_number }, + seq_num: 0, + }; + // This should error because the l2 parent's l1_origin.hash should equal the epoch hash + // Here the default header is used whose hash will not equal the custom `l2_hash` above. + let expected = BuilderError::BlockMismatch(epoch, l2_parent.l1_origin); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, PipelineErrorKind::Reset(ResetError::AttributesBuilder(expected))); + } + + #[tokio::test] + async fn test_prepare_payload_broken_time_invariant() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { block_time, ..Default::default() }); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() }, + l1_origin: BlockNumHash { hash, number: l2_number }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let block_id = BlockNumHash { hash, number: 0 }; + let expected = BuilderError::BrokenTimeInvariant( + l2_parent.l1_origin, + next_l2_time, + block_id, + timestamp, + ); + let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err(); + assert_eq!(err, PipelineErrorKind::Reset(ResetError::AttributesBuilder(expected))); + } + + #[tokio::test] + async fn test_prepare_payload_without_forks() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { block_time, ..Default::default() }); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: l2_number, + timestamp, + parent_hash: hash, + }, + l1_origin: BlockNumHash { hash, number: l2_number }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + suggested_fee_recipient: Predeploys::SEQUENCER_FEE_VAULT, + parent_beacon_block_root: None, + withdrawals: None, + }, + transactions: payload.transactions.clone(), + no_tx_pool: Some(true), + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + eip_1559_params: None, + min_base_fee: None, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_prepare_payload_with_canyon() { + let block_time = 10; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { + block_time, + hardforks: HardForkConfig { canyon_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: l2_number, + timestamp, + parent_hash: hash, + }, + l1_origin: BlockNumHash { hash, number: l2_number }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + suggested_fee_recipient: Predeploys::SEQUENCER_FEE_VAULT, + parent_beacon_block_root: None, + withdrawals: Some(Vec::default()), + }, + transactions: payload.transactions.clone(), + no_tx_pool: Some(true), + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + eip_1559_params: None, + min_base_fee: None, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_prepare_payload_with_ecotone() { + let block_time = 2; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { + block_time, + hardforks: HardForkConfig { ecotone_time: Some(102), ..Default::default() }, + ..Default::default() + }); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let parent_beacon_block_root = Some(header.parent_beacon_block_root.unwrap_or_default()); + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: l2_number, + timestamp, + parent_hash: hash, + }, + l1_origin: BlockNumHash { hash, number: l2_number }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + suggested_fee_recipient: Predeploys::SEQUENCER_FEE_VAULT, + parent_beacon_block_root, + withdrawals: Some(vec![]), + }, + transactions: payload.transactions.clone(), + no_tx_pool: Some(true), + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + eip_1559_params: None, + min_base_fee: None, + }; + assert_eq!(payload, expected); + assert_eq!(payload.transactions.unwrap().len(), 7); + } + + #[tokio::test] + async fn test_prepare_payload_with_fjord() { + let block_time = 2; + let timestamp = 100; + let cfg = Arc::new(RollupConfig { + block_time, + hardforks: HardForkConfig { fjord_time: Some(102), ..Default::default() }, + ..Default::default() + }); + let l1_cfg = Arc::new(L1Config::sepolia().into()); + let l2_number = 1; + let mut fetcher = TestSystemConfigL2Fetcher::default(); + fetcher.insert(l2_number, SystemConfig::default()); + let mut provider = TestChainProvider::default(); + let header = Header { timestamp, ..Default::default() }; + let prev_randao = header.mix_hash; + let hash = header.hash_slow(); + provider.insert_header(hash, header); + let mut builder = StatefulAttributesBuilder::new(cfg, l1_cfg, fetcher, provider); + let epoch = BlockNumHash { hash, number: l2_number }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + hash: B256::ZERO, + number: l2_number, + timestamp, + parent_hash: hash, + }, + l1_origin: BlockNumHash { hash, number: l2_number }, + seq_num: 0, + }; + let next_l2_time = l2_parent.block_info.timestamp + block_time; + let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap(); + let expected = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_l2_time, + prev_randao, + suggested_fee_recipient: Predeploys::SEQUENCER_FEE_VAULT, + parent_beacon_block_root: Some(B256::ZERO), + withdrawals: Some(vec![]), + }, + transactions: payload.transactions.clone(), + no_tx_pool: Some(true), + gas_limit: Some(u64::from_be_bytes( + alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(), + )), + eip_1559_params: None, + min_base_fee: None, + }; + assert_eq!(payload.transactions.as_ref().unwrap().len(), 10); + assert_eq!(payload, expected); + } +} diff --git a/kona/crates/protocol/derive/src/errors/attributes.rs b/kona/crates/protocol/derive/src/errors/attributes.rs new file mode 100644 index 0000000000000..494186dc2e675 --- /dev/null +++ b/kona/crates/protocol/derive/src/errors/attributes.rs @@ -0,0 +1,35 @@ +//! Error types for kona's attributes builder. + +use alloc::string::String; +use alloy_eips::BlockNumHash; +use alloy_primitives::B256; +use thiserror::Error; + +/// An [`AttributesBuilder`] Error. +/// +/// [`AttributesBuilder`]: crate::traits::AttributesBuilder +#[derive(Error, Clone, Debug, PartialEq, Eq)] +pub enum BuilderError { + /// Mismatched blocks. + #[error("Block mismatch. Expected {0:?}, got {1:?}")] + BlockMismatch(BlockNumHash, BlockNumHash), + /// Mismatched blocks for the start of an Epoch. + #[error("Block mismatch on epoch reset. Expected {0:?}, got {1:?}")] + BlockMismatchEpochReset(BlockNumHash, BlockNumHash, B256), + /// [`SystemConfig`] update failed. + /// + /// [`SystemConfig`]: kona_genesis::SystemConfig + #[error("System config update failed")] + SystemConfigUpdate, + /// Broken time invariant between L2 and L1. + #[error( + "Time invariant broken. L1 origin: {0:?} | Next L2 time: {1} | L1 block: {2:?} | L1 timestamp {3:?}" + )] + BrokenTimeInvariant(BlockNumHash, u64, BlockNumHash, u64), + /// Attributes unavailable. + #[error("Attributes unavailable")] + AttributesUnavailable, + /// A custom error. + #[error("Error in attributes builder: {0}")] + Custom(String), +} diff --git a/kona/crates/protocol/derive/src/errors/mod.rs b/kona/crates/protocol/derive/src/errors/mod.rs new file mode 100644 index 0000000000000..be426bed9d16b --- /dev/null +++ b/kona/crates/protocol/derive/src/errors/mod.rs @@ -0,0 +1,17 @@ +//! Error types for the kona derivation pipeline. +//! +//! This module contains comprehensive error types for the derivation pipeline, organized +//! by severity and category. The error system provides detailed context for debugging +//! and enables sophisticated error handling and recovery strategies. + +mod attributes; +pub use attributes::BuilderError; + +mod stages; +pub use stages::BatchDecompressionError; + +mod pipeline; +pub use pipeline::{PipelineEncodingError, PipelineError, PipelineErrorKind, ResetError}; + +mod sources; +pub use sources::{BlobDecodingError, BlobProviderError}; diff --git a/kona/crates/protocol/derive/src/errors/pipeline.rs b/kona/crates/protocol/derive/src/errors/pipeline.rs new file mode 100644 index 0000000000000..8253a695c95ba --- /dev/null +++ b/kona/crates/protocol/derive/src/errors/pipeline.rs @@ -0,0 +1,439 @@ +//! This module contains derivation errors thrown within the pipeline. + +use crate::BuilderError; +use alloc::string::String; +use alloy_primitives::B256; +use kona_genesis::SystemConfigUpdateError; +use kona_protocol::{DepositError, SpanBatchError}; +use thiserror::Error; + +/// [crate::ensure] is a short-hand for bubbling up errors in the case of a condition not being met. +#[macro_export] +macro_rules! ensure { + ($cond:expr, $err:expr) => { + if !($cond) { + return Err($err); + } + }; +} + +/// A top-level severity filter for [`PipelineError`] that categorizes errors by handling strategy. +/// +/// The [`PipelineErrorKind`] wrapper provides a severity classification system that enables +/// sophisticated error handling in the derivation pipeline. Different error types require +/// different response strategies: +/// +/// - **Temporary**: Retry-able errors that may resolve with more data +/// - **Critical**: Fatal errors that require external intervention +/// - **Reset**: Errors that require pipeline state reset but allow continued operation +/// +/// # Error Handling Strategy +/// ```text +/// Temporary -> Retry operation, may succeed with more data +/// Critical -> Stop derivation, external intervention required +/// Reset -> Reset pipeline state, continue with clean slate +/// ``` +/// +/// # Usage in Pipeline +/// Error kinds are used by pipeline stages to determine appropriate error handling: +/// - Temporary errors trigger retries in the main derivation loop +/// - Critical errors halt derivation and bubble up to the caller +/// - Reset errors trigger pipeline resets with appropriate recovery logic +#[derive(Error, Debug, PartialEq, Eq)] +pub enum PipelineErrorKind { + /// A temporary error that may resolve with additional data or time. + /// + /// Temporary errors indicate transient conditions such as insufficient data, + /// network timeouts, or resource unavailability. These errors suggest that + /// retrying the operation may succeed once the underlying condition resolves. + /// + /// # Examples + /// - Not enough L1 data available yet + /// - Network communication timeouts + /// - Insufficient channel data for frame assembly + /// + /// # Handling + /// The pipeline typically retries temporary errors in a loop, waiting for + /// conditions to improve or for additional data to become available. + #[error("Temporary error: {0}")] + Temporary(#[source] PipelineError), + /// A critical error that requires external intervention to resolve. + /// + /// Critical errors indicate fundamental issues that cannot be resolved through + /// retries or pipeline resets. These errors require external intervention such + /// as updated L1 data, configuration changes, or system fixes. + /// + /// # Examples + /// - Data source completely exhausted + /// - Fundamental configuration errors + /// - Irrecoverable data corruption + /// + /// # Handling + /// Critical errors halt the derivation process and are returned to the caller + /// for external resolution. The pipeline cannot continue without intervention. + #[error("Critical error: {0}")] + Critical(#[source] PipelineError), + /// A reset error that requires pipeline state reset but allows continued operation. + /// + /// Reset errors indicate conditions that invalidate the current pipeline state + /// but can be resolved by resetting to a known good state and continuing + /// derivation. These typically occur due to chain reorganizations or state + /// inconsistencies. + /// + /// # Examples + /// - L1 chain reorganization detected + /// - Block hash mismatches indicating reorg + /// - Hard fork activation requiring state reset + /// + /// # Handling + /// Reset errors trigger pipeline state cleanup and reset to a safe state, + /// after which derivation can continue with fresh state. + #[error("Pipeline reset: {0}")] + Reset(#[from] ResetError), +} + +/// An error encountered during derivation pipeline processing. +/// +/// [`PipelineError`] represents specific error conditions that can occur during the +/// various stages of L2 block derivation from L1 data. Each error variant provides +/// detailed context about the failure mode and suggests appropriate recovery strategies. +/// +/// # Error Categories +/// +/// ## Data Availability Errors +/// - [`Self::Eof`]: No more data available from source +/// - [`Self::NotEnoughData`]: Insufficient data for current operation +/// - [`Self::MissingL1Data`]: Required L1 data not available +/// - [`Self::EndOfSource`]: Data source completely exhausted +/// +/// ## Stage-Specific Errors +/// - [`Self::ChannelProviderEmpty`]: No channels available for processing +/// - [`Self::ChannelReaderEmpty`]: Channel reader has no data +/// - [`Self::BatchQueueEmpty`]: No batches available for processing +/// +/// ## Validation Errors +/// - [`Self::InvalidBatchType`]: Unsupported or malformed batch type +/// - [`Self::InvalidBatchValidity`]: Batch failed validation checks +/// - [`Self::BadEncoding`]: Data decoding/encoding failures +/// +/// ## System Errors +/// - [`Self::SystemConfigUpdate`]: System configuration update failures +/// - [`Self::AttributesBuilder`]: Block attribute construction failures +/// - [`Self::Provider`]: External provider communication failures +#[derive(Error, Debug, PartialEq, Eq)] +pub enum PipelineError { + /// End of file: no more data available from the channel bank. + /// + /// This error indicates that the channel bank has been completely drained + /// and no additional frame data is available for processing. It typically + /// occurs at the end of a derivation sequence when all available L1 data + /// has been consumed. + /// + /// # Recovery + /// Usually indicates completion of derivation for available data. May + /// require waiting for new L1 blocks to provide additional frame data. + #[error("EOF")] + Eof, + /// Insufficient data available to complete the current processing stage. + /// + /// This error indicates that the current operation requires more data than + /// is currently available, but additional data may become available in the + /// future. It suggests that retrying the operation later may succeed. + /// + /// # Common Scenarios + /// - Partial frame received, waiting for completion + /// - Channel assembly requires more frames + /// - Batch construction needs additional channel data + /// + /// # Recovery + /// Retry the operation after more L1 data becomes available or after + /// waiting for network propagation delays. + #[error("Not enough data")] + NotEnoughData, + /// No channels are available in the [`ChannelProvider`]. + /// + /// This error occurs when the channel provider stage has no assembled + /// channels ready for reading. It typically indicates that frame assembly + /// is still in progress or that no valid channels have been constructed + /// from available L1 data. + /// + /// [`ChannelProvider`]: crate::stages::ChannelProvider + #[error("The channel provider is empty")] + ChannelProviderEmpty, + /// The channel has already been fully processed by the [`ChannelAssembler`] stage. + /// + /// This error indicates an attempt to reprocess a channel that has already + /// been assembled and consumed. It suggests a logic error in channel tracking + /// or an attempt to double-process the same channel data. + /// + /// [`ChannelAssembler`]: crate::stages::ChannelAssembler + #[error("Channel already built")] + ChannelAlreadyBuilt, + /// Failed to locate the requested channel in the [`ChannelProvider`]. + /// + /// This error occurs when attempting to access a specific channel that + /// is not available in the channel provider's cache or storage. It may + /// indicate a channel ID mismatch or premature channel eviction. + /// + /// [`ChannelProvider`]: crate::stages::ChannelProvider + #[error("Channel not found in channel provider")] + ChannelNotFound, + /// No channel data returned by the [`ChannelReader`] stage. + /// + /// This error indicates that the channel reader stage has no channels + /// available for reading. It typically occurs when all channels have + /// been consumed or when no valid channels have been assembled yet. + /// + /// [`ChannelReader`]: crate::stages::ChannelReader + #[error("The channel reader has no channel available")] + ChannelReaderEmpty, + /// The [`BatchQueue`] contains no batches ready for processing. + /// + /// This error occurs when the batch queue stage has no assembled batches + /// available for attribute generation. It indicates that batch assembly + /// is still in progress or that no valid batches have been constructed. + /// + /// [`BatchQueue`]: crate::stages::BatchQueue + #[error("The batch queue has no batches available")] + BatchQueueEmpty, + /// Required L1 origin information is missing from the previous pipeline stage. + /// + /// This error indicates a pipeline stage dependency violation where a stage + /// expects L1 origin information that wasn't provided by the preceding stage. + /// It suggests a configuration or sequencing issue in the pipeline setup. + #[error("Missing L1 origin from previous stage")] + MissingOrigin, + /// Required L1 data is missing from the [`L1Retrieval`] stage. + /// + /// This error occurs when the L1 retrieval stage cannot provide the + /// requested L1 block data, transactions, or receipts. It may indicate + /// network issues, data availability problems, or L1 node synchronization lag. + /// + /// [`L1Retrieval`]: crate::stages::L1Retrieval + #[error("L1 Retrieval missing data")] + MissingL1Data, + /// Invalid or unsupported batch type encountered during processing. + /// + /// This error occurs when a pipeline stage receives a batch type that + /// it cannot process or that violates the expected batch format. It + /// indicates either malformed L1 data or unsupported batch versions. + #[error("Invalid batch type passed to stage")] + InvalidBatchType, + /// Batch failed validation checks during processing. + /// + /// This error indicates that a batch contains invalid data that fails + /// validation rules such as timestamp constraints, parent hash checks, + /// or format requirements. It suggests potentially malicious or corrupted L1 data. + #[error("Invalid batch validity")] + InvalidBatchValidity, + /// [`SystemConfig`] update operation failed. + /// + /// This error occurs when attempting to update the system configuration + /// fails due to invalid parameters, version mismatches, or other + /// configuration-related issues. + /// + /// [`SystemConfig`]: kona_genesis::SystemConfig + #[error("Error updating system config: {0}")] + SystemConfigUpdate(SystemConfigUpdateError), + /// Block attributes construction failed with detailed error information. + /// + /// This error wraps [`BuilderError`] variants that occur during the + /// construction of block attributes from batch data. It indicates issues + /// with attribute validation, formatting, or consistency checks. + #[error("Attributes builder error: {0}")] + AttributesBuilder(#[from] BuilderError), + /// Data encoding or decoding operation failed. + /// + /// This error wraps [`PipelineEncodingError`] variants that occur during + /// serialization or deserialization of pipeline data structures. It + /// indicates malformed input data or encoding format violations. + #[error("Decode error: {0}")] + BadEncoding(#[from] PipelineEncodingError), + /// The data source has been completely exhausted and cannot provide more data. + /// + /// This error indicates that the underlying L1 data source has reached + /// its end and no additional data will become available. It typically + /// occurs when derivation has caught up to the L1 chain head. + #[error("Data source exhausted")] + EndOfSource, + /// External provider communication or operation failed. + /// + /// This error wraps failures from external data providers such as L1 + /// nodes, blob providers, or other data sources. It includes network + /// failures, API errors, and provider-specific issues. + #[error("Provider error: {0}")] + Provider(String), + /// The pipeline received an unsupported signal type. + /// + /// This error occurs when a pipeline stage receives a signal that it + /// cannot process or that is not supported in the current configuration. + /// It indicates a protocol version mismatch or configuration issue. + #[error("Unsupported signal")] + UnsupportedSignal, +} + +impl PipelineError { + /// Wraps this [`PipelineError`] as a [PipelineErrorKind::Critical]. + /// + /// Critical errors indicate fundamental issues that cannot be resolved through + /// retries or pipeline resets. They require external intervention to resolve. + /// + /// # Usage + /// Use this method when an error condition is unrecoverable and requires + /// halting the derivation process for external intervention. + /// + /// # Example + /// ```rust,ignore + /// if data_source_corrupted { + /// return Err(PipelineError::Provider("corrupted data".to_string()).crit()); + /// } + /// ``` + pub const fn crit(self) -> PipelineErrorKind { + PipelineErrorKind::Critical(self) + } + + /// Wraps this [`PipelineError`] as a [PipelineErrorKind::Temporary]. + /// + /// Temporary errors indicate transient conditions that may resolve with + /// additional data, time, or retries. The pipeline can attempt to recover + /// by retrying the operation. + /// + /// # Usage + /// Use this method when an error condition might resolve if the operation + /// is retried, particularly for data availability or network issues. + /// + /// # Example + /// ```rust,ignore + /// if insufficient_data { + /// return Err(PipelineError::NotEnoughData.temp()); + /// } + /// ``` + pub const fn temp(self) -> PipelineErrorKind { + PipelineErrorKind::Temporary(self) + } +} + +/// A reset error +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum ResetError { + /// The batch has a bad parent hash. + /// The first argument is the expected parent hash, and the second argument is the actual + /// parent hash. + #[error("Bad parent hash: expected {0}, got {1}")] + BadParentHash(B256, B256), + /// The batch has a bad timestamp. + /// The first argument is the expected timestamp, and the second argument is the actual + /// timestamp. + #[error("Bad timestamp: expected {0}, got {1}")] + BadTimestamp(u64, u64), + /// L1 origin mismatch. + #[error("L1 origin mismatch. Expected {0:?}, got {1:?}")] + L1OriginMismatch(u64, u64), + /// The stage detected a block reorg. + /// The first argument is the expected block hash. + /// The second argument is the parent_hash of the next l1 origin block. + #[error("L1 reorg detected: expected {0}, got {1}")] + ReorgDetected(B256, B256), + /// Attributes builder error variant, with [`BuilderError`]. + #[error("Attributes builder error: {0}")] + AttributesBuilder(#[from] BuilderError), + /// A Holocene activation temporary error. + #[error("Holocene activation reset")] + HoloceneActivation, + /// The next l1 block provided to the managed traversal stage is not the expected one. + #[error("Next L1 block hash mismatch: expected {0}, got {1}")] + NextL1BlockHashMismatch(B256, B256), +} + +impl ResetError { + /// Wrap [`ResetError`] as a [PipelineErrorKind::Reset]. + pub const fn reset(self) -> PipelineErrorKind { + PipelineErrorKind::Reset(self) + } +} + +/// A decoding error. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum PipelineEncodingError { + /// The buffer is empty. + #[error("Empty buffer")] + EmptyBuffer, + /// Deposit decoding error. + #[error("Error decoding deposit: {0}")] + DepositError(#[from] DepositError), + /// Alloy RLP Encoding Error. + #[error("RLP error: {0}")] + AlloyRlpError(alloy_rlp::Error), + /// Span Batch Error. + #[error("{0}")] + SpanBatchError(#[from] SpanBatchError), +} + +#[cfg(test)] +mod tests { + use super::*; + use core::error::Error; + + #[test] + fn test_pipeline_error_kind_source() { + let err = PipelineErrorKind::Temporary(PipelineError::Eof); + assert!(err.source().is_some()); + + let err = PipelineErrorKind::Critical(PipelineError::Eof); + assert!(err.source().is_some()); + + let err = PipelineErrorKind::Reset(ResetError::BadParentHash( + Default::default(), + Default::default(), + )); + assert!(err.source().is_some()); + } + + #[test] + fn test_pipeline_error_source() { + let err = PipelineError::AttributesBuilder(BuilderError::BlockMismatch( + Default::default(), + Default::default(), + )); + assert!(err.source().is_some()); + + let encoding_err = PipelineEncodingError::EmptyBuffer; + let err: PipelineError = encoding_err.into(); + assert!(err.source().is_some()); + + let err = PipelineError::Eof; + assert!(err.source().is_none()); + } + + #[test] + fn test_pipeline_encoding_error_source() { + let err = PipelineEncodingError::DepositError(DepositError::UnexpectedTopicsLen(0)); + assert!(err.source().is_some()); + + let err = SpanBatchError::TooBigSpanBatchSize; + let err: PipelineEncodingError = err.into(); + assert!(err.source().is_some()); + + let err = PipelineEncodingError::EmptyBuffer; + assert!(err.source().is_none()); + } + + #[test] + fn test_reset_error_kinds() { + let reset_errors = [ + ResetError::BadParentHash(Default::default(), Default::default()), + ResetError::BadTimestamp(0, 0), + ResetError::L1OriginMismatch(0, 0), + ResetError::ReorgDetected(Default::default(), Default::default()), + ResetError::AttributesBuilder(BuilderError::BlockMismatch( + Default::default(), + Default::default(), + )), + ResetError::HoloceneActivation, + ]; + for error in reset_errors.into_iter() { + let expected = PipelineErrorKind::Reset(error.clone()); + assert_eq!(error.reset(), expected); + } + } +} diff --git a/kona/crates/protocol/derive/src/errors/sources.rs b/kona/crates/protocol/derive/src/errors/sources.rs new file mode 100644 index 0000000000000..2a16c80db0928 --- /dev/null +++ b/kona/crates/protocol/derive/src/errors/sources.rs @@ -0,0 +1,77 @@ +//! Error types for sources. + +use crate::{PipelineError, PipelineErrorKind}; +use alloc::string::{String, ToString}; +use thiserror::Error; + +/// Blob Decoding Error +#[derive(Error, Debug, PartialEq, Eq)] +pub enum BlobDecodingError { + /// Invalid field element + #[error("Invalid field element")] + InvalidFieldElement, + /// Invalid encoding version + #[error("Invalid encoding version")] + InvalidEncodingVersion, + /// Invalid length + #[error("Invalid length")] + InvalidLength, + /// Missing Data + #[error("Missing data")] + MissingData, +} + +/// An error returned by the [`BlobProviderError`]. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum BlobProviderError { + /// The number of specified blob hashes did not match the number of returned sidecars. + #[error("Blob sidecar length mismatch: expected {0}, got {1}")] + SidecarLengthMismatch(usize, usize), + /// Slot derivation error. + #[error("Failed to derive slot")] + SlotDerivation, + /// Blob decoding error. + #[error("Blob decoding error: {0}")] + BlobDecoding(#[from] BlobDecodingError), + /// Error pertaining to the backend transport. + #[error("{0}")] + Backend(String), +} + +impl From for PipelineErrorKind { + fn from(val: BlobProviderError) -> Self { + match val { + BlobProviderError::SidecarLengthMismatch(_, _) => { + PipelineError::Provider(val.to_string()).crit() + } + BlobProviderError::SlotDerivation => PipelineError::Provider(val.to_string()).crit(), + BlobProviderError::BlobDecoding(_) => PipelineError::Provider(val.to_string()).crit(), + BlobProviderError::Backend(_) => PipelineError::Provider(val.to_string()).temp(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::error::Error; + + #[test] + fn test_blob_decoding_error_source() { + let err: BlobProviderError = BlobDecodingError::InvalidFieldElement.into(); + assert!(err.source().is_some()); + } + + #[test] + fn test_from_blob_provider_error() { + let err: PipelineErrorKind = BlobProviderError::SlotDerivation.into(); + assert!(matches!(err, PipelineErrorKind::Critical(_))); + + let err: PipelineErrorKind = BlobProviderError::SidecarLengthMismatch(1, 2).into(); + assert!(matches!(err, PipelineErrorKind::Critical(_))); + + let err: PipelineErrorKind = + BlobProviderError::BlobDecoding(BlobDecodingError::InvalidFieldElement).into(); + assert!(matches!(err, PipelineErrorKind::Critical(_))); + } +} diff --git a/kona/crates/protocol/derive/src/errors/stages.rs b/kona/crates/protocol/derive/src/errors/stages.rs new file mode 100644 index 0000000000000..1570a0d7ff54f --- /dev/null +++ b/kona/crates/protocol/derive/src/errors/stages.rs @@ -0,0 +1,12 @@ +//! Error types for derivation pipeline stages. + +use kona_protocol::MAX_SPAN_BATCH_ELEMENTS; +use thiserror::Error; + +/// A frame decompression error. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum BatchDecompressionError { + /// The buffer exceeds the [`MAX_SPAN_BATCH_ELEMENTS`] protocol parameter. + #[error("The batch exceeds the maximum number of elements: {max_size}", max_size = MAX_SPAN_BATCH_ELEMENTS)] + BatchTooLarge, +} diff --git a/kona/crates/protocol/derive/src/lib.rs b/kona/crates/protocol/derive/src/lib.rs new file mode 100644 index 0000000000000..004a656940549 --- /dev/null +++ b/kona/crates/protocol/derive/src/lib.rs @@ -0,0 +1,56 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "metrics"), no_std)] + +extern crate alloc; + +#[macro_use] +extern crate tracing; + +mod attributes; +pub use attributes::StatefulAttributesBuilder; + +mod errors; +pub use errors::{ + BatchDecompressionError, BlobDecodingError, BlobProviderError, BuilderError, + PipelineEncodingError, PipelineError, PipelineErrorKind, ResetError, +}; + +mod pipeline; +pub use pipeline::{ + AttributesQueueStage, BatchProviderStage, BatchStreamStage, ChannelProviderStage, + ChannelReaderStage, DerivationPipeline, FrameQueueStage, IndexedAttributesQueueStage, + L1RetrievalStage, PipelineBuilder, PolledAttributesQueueStage, +}; + +mod sources; +pub use sources::{BlobData, BlobSource, CalldataSource, EthereumDataSource}; + +mod stages; +pub use stages::{ + AttributesQueue, BatchProvider, BatchQueue, BatchStream, BatchStreamProvider, BatchValidator, + ChannelAssembler, ChannelBank, ChannelProvider, ChannelReader, ChannelReaderProvider, + FrameQueue, FrameQueueProvider, IndexedTraversal, L1Retrieval, L1RetrievalProvider, + NextBatchProvider, NextFrameProvider, PollingTraversal, TraversalStage, +}; + +mod traits; +pub use traits::{ + AttributesBuilder, AttributesProvider, BatchValidationProviderDerive, BlobProvider, + ChainProvider, DataAvailabilityProvider, L2ChainProvider, NextAttributes, OriginAdvancer, + OriginProvider, Pipeline, ResetProvider, SignalReceiver, +}; + +mod types; +pub use types::{ActivationSignal, PipelineResult, ResetSignal, Signal, StepResult}; + +mod metrics; +pub use metrics::Metrics; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/kona/crates/protocol/derive/src/metrics/mod.rs b/kona/crates/protocol/derive/src/metrics/mod.rs new file mode 100644 index 0000000000000..4207b000f7c12 --- /dev/null +++ b/kona/crates/protocol/derive/src/metrics/mod.rs @@ -0,0 +1,270 @@ +//! Metrics for the derivation pipeline. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the pipeline origin gauge. + pub const PIPELINE_ORIGIN: &str = "kona_derive_pipeline_origin"; + + /// Identifier for the latest l2 block the pipeline stepped on. + pub const PIPELINE_STEP_BLOCK: &str = "kona_derive_pipeline_step_block"; + + /// Identifier for if the batch reader is set. + pub const PIPELINE_BATCH_READER_SET: &str = "kona_derive_batch_reader_set"; + + /// Identifier to track the amount of time it takes to advance the pipeline origin. + pub const PIPELINE_ORIGIN_ADVANCE: &str = "kona_derive_pipeline_origin_advance"; + + /// Identifier for the histogram that tracks when the system config is updated. + pub const SYSTEM_CONFIG_UPDATE: &str = "kona_derive_system_config_update"; + + /// Identifier for the number of frames in the frame queue pipeline stage. + pub const PIPELINE_FRAME_QUEUE_BUFFER: &str = "kona_derive_frame_queue_buffer"; + + /// Identifier for the frame queue buffer memory overhead gauge. + pub const PIPELINE_FRAME_QUEUE_MEM: &str = "kona_derive_frame_queue_mem"; + + /// Identifier for the number of channels held in the pipeline. + pub const PIPELINE_CHANNEL_BUFFER: &str = "kona_derive_channel_buffer"; + + /// Identifier for the channel buffer memory overhead gauge. + pub const PIPELINE_CHANNEL_MEM: &str = "kona_derive_channel_mem"; + + /// Identifier for a gauge that tracks the number of blocks until the next channel times out. + pub const PIPELINE_CHANNEL_TIMEOUT: &str = "kona_derive_blocks_until_channel_timeout"; + + /// Identifier for the gauge that tracks the maximum rlp byte size per channel. + pub const PIPELINE_MAX_RLP_BYTES: &str = "kona_derive_max_rlp_bytes"; + + /// Identifier for the batch stream stage singular batch buffer size. + pub const PIPELINE_BATCH_BUFFER: &str = "kona_derive_batch_buffer"; + + /// Identifier for the batch stream stage batch memory overhead gauge. + pub const PIPELINE_BATCH_MEM: &str = "kona_derive_batch_mem"; + + /// Identifier for the size of batches read by the channel reader. + pub const PIPELINE_READ_BATCHES: &str = "kona_derive_read_batches"; + + /// Identifier for the gauge that tracks the number of pipeline steps. + pub const PIPELINE_STEPS: &str = "kona_derive_pipeline_steps"; + + /// Identifier for the gauge that tracks the number of prepared attributes. + pub const PIPELINE_PREPARED_ATTRIBUTES: &str = "kona_derive_prepared_attributes"; + + /// Identifier tracking the number of pipeline signals. + pub const PIPELINE_SIGNALS: &str = "kona_derive_pipeline_signals"; + + /// Identifier that tracks the batch validator l1 blocks start. + pub const PIPELINE_L1_BLOCKS_START: &str = "kona_derive_l1_blocks_start"; + + /// Identifier that tracks the batch validator l1 blocks end. + pub const PIPELINE_L1_BLOCKS_END: &str = "kona_derive_l1_blocks_end"; + + /// Identifier to track the size of the current derived span batch. + pub const PIPELINE_DERIVED_SPAN_SIZE: &str = "kona_derive_span_size"; + + /// Identifier to track the number of transactions in the latest derived payload attributes. + pub const PIPELINE_LATEST_PAYLOAD_TX_COUNT: &str = "kona_derive_payload_tx_count"; + + /// Identifier for the data availability provider data. + pub const PIPELINE_DATA_AVAILABILITY_PROVIDER: &str = "kona_derive_dap_sources"; + + /// Identifier for a gauge that tracks batch validity. + pub const PIPELINE_BATCH_VALIDITY: &str = "kona_derive_batch_validity"; + + /// Identifier for the histogram that tracks the amount of time it takes to validate a + /// span batch. + pub const PIPELINE_CHECK_BATCH_PREFIX: &str = "kona_derive_check_batch_prefix_duration"; + + /// Identifier for the histogram that tracks the amount of time it takes to build payload + /// attributes. + pub const PIPELINE_ATTRIBUTES_BUILD_DURATION: &str = "kona_derive_attributes_build_duration"; + + /// Identifier for the gauge that tracks the number of payload attributes buffered in the + /// pipeline. + pub const PIPELINE_PAYLOAD_ATTRIBUTES_BUFFER: &str = "kona_derive_payload_attributes_buffer"; + + /// Identifier for a gauge that tracks the latest block number for a system config update. + pub const PIPELINE_LATEST_SYS_CONFIG_UPDATE: &'static str = + "kona_genesis_latest_system_config_update"; + + /// Identifier for a gauge that tracks the block height at which a system config update errored. + pub const PIPELINE_SYS_CONFIG_UPDATE_ERROR: &'static str = + "kona_genesis_sys_config_update_error"; + + /// Gauge that tracks the latest decompressed batch size. + pub const PIPELINE_LATEST_DECOMPRESSED_BATCH_SIZE: &str = + "kona_derive_latest_decompressed_batch_size"; + + /// Gauge that tracks the latest decompressed batch type. + pub const PIPELINE_LATEST_DECOMPRESSED_BATCH_TYPE: &str = + "kona_derive_latest_decompressed_batch_type"; +} + +impl Metrics { + /// Initializes metrics. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics. + #[cfg(feature = "metrics")] + pub fn describe() { + metrics::describe_gauge!( + Self::PIPELINE_SYS_CONFIG_UPDATE_ERROR, + "The block height at which a system config update errored" + ); + metrics::describe_gauge!( + Self::PIPELINE_LATEST_SYS_CONFIG_UPDATE, + "The latest block number for a system config update" + ); + metrics::describe_gauge!( + Self::PIPELINE_LATEST_DECOMPRESSED_BATCH_SIZE, + "The latest decompressed batch size" + ); + metrics::describe_gauge!( + Self::PIPELINE_LATEST_DECOMPRESSED_BATCH_TYPE, + "The latest decompressed batch type" + ); + metrics::describe_gauge!( + Self::PIPELINE_ORIGIN, + "The block height of the pipeline l1 origin" + ); + metrics::describe_gauge!( + Self::PIPELINE_BATCH_VALIDITY, + "The validity of the batch being processed", + ); + metrics::describe_gauge!( + Self::PIPELINE_DATA_AVAILABILITY_PROVIDER, + "The source of pipeline data" + ); + metrics::describe_gauge!( + Self::PIPELINE_DERIVED_SPAN_SIZE, + "The number of payload attributes in the current span" + ); + metrics::describe_gauge!( + Self::PIPELINE_LATEST_PAYLOAD_TX_COUNT, + "The number of transactions in the latest derived payload attributes" + ); + metrics::describe_gauge!(Self::PIPELINE_READ_BATCHES, "The read batches"); + metrics::describe_gauge!(Self::PIPELINE_BATCH_READER_SET, "If the batch reader is set"); + metrics::describe_gauge!(Self::PIPELINE_L1_BLOCKS_START, "Earliest l1 blocks height"); + metrics::describe_gauge!(Self::PIPELINE_L1_BLOCKS_END, "Latest l1 blocks height"); + metrics::describe_gauge!( + Self::PIPELINE_STEP_BLOCK, + "The latest L2 block height that the pipeline stepped on" + ); + metrics::describe_histogram!( + Self::PIPELINE_CHECK_BATCH_PREFIX, + "The time it takes to validate a span batch" + ); + metrics::describe_histogram!( + Self::PIPELINE_ORIGIN_ADVANCE, + "The amount of time it takes to advance the pipeline origin" + ); + metrics::describe_histogram!( + Self::SYSTEM_CONFIG_UPDATE, + "The time it takes to update the system config" + ); + metrics::describe_gauge!( + Self::PIPELINE_FRAME_QUEUE_BUFFER, + "The number of frames in the frame queue" + ); + metrics::describe_gauge!( + Self::PIPELINE_FRAME_QUEUE_MEM, + "The memory size of frames held in the frame queue" + ); + metrics::describe_gauge!( + Self::PIPELINE_CHANNEL_BUFFER, + "The number of channels in the channel assembler stage" + ); + metrics::describe_gauge!( + Self::PIPELINE_CHANNEL_MEM, + "The memory size of channels held in the channel assembler stage" + ); + metrics::describe_gauge!( + Self::PIPELINE_CHANNEL_TIMEOUT, + "The number of blocks until the next channel times out" + ); + metrics::describe_gauge!( + Self::PIPELINE_MAX_RLP_BYTES, + "The maximum rlp byte size of a channel" + ); + metrics::describe_gauge!( + Self::PIPELINE_BATCH_BUFFER, + "The number of batches held in the batch stream stage" + ); + metrics::describe_gauge!( + Self::PIPELINE_BATCH_MEM, + "The memory size of batches held in the batch stream stage" + ); + metrics::describe_gauge!( + Self::PIPELINE_STEPS, + "The total number of pipeline steps on the derivation pipeline" + ); + metrics::describe_gauge!( + Self::PIPELINE_PREPARED_ATTRIBUTES, + "The total number of prepared attributes generated by the derivation pipeline" + ); + metrics::describe_gauge!( + Self::PIPELINE_SIGNALS, + "Number of times the pipeline has been signalled" + ); + metrics::describe_histogram!( + Self::PIPELINE_ATTRIBUTES_BUILD_DURATION, + "The time it takes to build payload attributes" + ); + metrics::describe_gauge!( + Self::PIPELINE_PAYLOAD_ATTRIBUTES_BUFFER, + "The number of payload attributes buffered in the pipeline" + ); + } + + /// Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn zero() { + // The batch reader is by default not set. + kona_macros::set!(gauge, Self::PIPELINE_BATCH_READER_SET, 0); + + // No source data is initially read. + kona_macros::set!(gauge, Self::PIPELINE_DATA_AVAILABILITY_PROVIDER, "source", "blobs", 0); + kona_macros::set!( + gauge, + Self::PIPELINE_DATA_AVAILABILITY_PROVIDER, + "source", + "calldata", + 0 + ); + + // Manually translate a value of `0` for sys config update as no update yet. + kona_macros::set!(gauge, Self::PIPELINE_LATEST_SYS_CONFIG_UPDATE, 0); + kona_macros::set!(gauge, Self::PIPELINE_SYS_CONFIG_UPDATE_ERROR, 0); + + // Pipeline signals start at zero. + kona_macros::set!(gauge, Self::PIPELINE_SIGNALS, "type", "reset", 0); + kona_macros::set!(gauge, Self::PIPELINE_SIGNALS, "type", "activation", 0); + kona_macros::set!(gauge, Self::PIPELINE_SIGNALS, "type", "flush_channel", 0); + + // No batches are initially read. + kona_macros::set!(gauge, Self::PIPELINE_READ_BATCHES, "type", "single", 0); + kona_macros::set!(gauge, Self::PIPELINE_READ_BATCHES, "type", "span", 0); + + // Cumulative counters start at zero. + kona_macros::set!(gauge, Self::PIPELINE_STEPS, 0); + kona_macros::set!(gauge, Self::PIPELINE_PREPARED_ATTRIBUTES, 0); + + // All buffers can be zeroed out since they are expected to return to zero. + kona_macros::set!(gauge, Self::PIPELINE_BATCH_BUFFER, 0); + kona_macros::set!(gauge, Self::PIPELINE_CHANNEL_BUFFER, 0); + kona_macros::set!(gauge, Self::PIPELINE_FRAME_QUEUE_BUFFER, 0); + kona_macros::set!(gauge, Self::PIPELINE_PAYLOAD_ATTRIBUTES_BUFFER, 0); + } +} diff --git a/kona/crates/protocol/derive/src/pipeline/builder.rs b/kona/crates/protocol/derive/src/pipeline/builder.rs new file mode 100644 index 0000000000000..9655be1ccf8ad --- /dev/null +++ b/kona/crates/protocol/derive/src/pipeline/builder.rs @@ -0,0 +1,177 @@ +//! Contains the `PipelineBuilder` object that is used to build a `DerivationPipeline`. + +use crate::{ + AttributesBuilder, AttributesQueue, BatchProvider, BatchStream, ChainProvider, ChannelProvider, + ChannelReader, DataAvailabilityProvider, DerivationPipeline, FrameQueue, + IndexedAttributesQueueStage, IndexedTraversal, L1Retrieval, L2ChainProvider, + PolledAttributesQueueStage, PollingTraversal, +}; +use alloc::sync::Arc; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::BlockInfo; + +/// The `PipelineBuilder` constructs a [`DerivationPipeline`] using a builder pattern. +#[derive(Debug)] +pub struct PipelineBuilder +where + B: AttributesBuilder + Send + Debug, + P: ChainProvider + Clone + Send + Sync + Debug, + T: L2ChainProvider + Clone + Send + Sync + Debug, + D: DataAvailabilityProvider + Send + Sync + Debug, +{ + l2_chain_provider: Option, + dap_source: Option, + chain_provider: Option

, + builder: Option, + origin: Option, + rollup_config: Option>, +} + +impl Default for PipelineBuilder +where + B: AttributesBuilder + Send + Debug, + P: ChainProvider + Clone + Send + Sync + Debug, + T: L2ChainProvider + Clone + Send + Sync + Debug, + D: DataAvailabilityProvider + Send + Sync + Debug, +{ + fn default() -> Self { + Self { + l2_chain_provider: None, + dap_source: None, + chain_provider: None, + builder: None, + origin: None, + rollup_config: None, + } + } +} + +impl PipelineBuilder +where + B: AttributesBuilder + Send + Debug, + P: ChainProvider + Clone + Send + Sync + Debug, + T: L2ChainProvider + Clone + Send + Sync + Debug, + D: DataAvailabilityProvider + Send + Sync + Debug, +{ + /// Creates a new pipeline builder. + pub fn new() -> Self { + Self::default() + } + + /// Sets the rollup config for the pipeline. + pub fn rollup_config(mut self, rollup_config: Arc) -> Self { + self.rollup_config = Some(rollup_config); + self + } + + /// Sets the origin L1 block for the pipeline. + pub const fn origin(mut self, origin: BlockInfo) -> Self { + self.origin = Some(origin); + self + } + + /// Sets the data availability provider for the pipeline. + pub fn dap_source(mut self, dap_source: D) -> Self { + self.dap_source = Some(dap_source); + self + } + + /// Sets the builder for the pipeline. + pub fn builder(mut self, builder: B) -> Self { + self.builder = Some(builder); + self + } + + /// Sets the l2 chain provider for the pipeline. + pub fn l2_chain_provider(mut self, l2_chain_provider: T) -> Self { + self.l2_chain_provider = Some(l2_chain_provider); + self + } + + /// Sets the chain provider for the pipeline. + pub fn chain_provider(mut self, chain_provider: P) -> Self { + self.chain_provider = Some(chain_provider); + self + } + + /// Builds a derivation pipeline with the [`PolledAttributesQueueStage`]. + pub fn build_polled(self) -> DerivationPipeline, T> { + self.into() + } + + /// Builds a derivation pipeline with the [`IndexedAttributesQueueStage`]. + pub fn build_indexed(self) -> DerivationPipeline, T> { + self.into() + } +} + +impl From> + for DerivationPipeline, T> +where + B: AttributesBuilder + Send + Debug, + P: ChainProvider + Clone + Send + Sync + Debug, + T: L2ChainProvider + Clone + Send + Sync + Debug, + D: DataAvailabilityProvider + Send + Sync + Debug, +{ + fn from(builder: PipelineBuilder) -> Self { + // Extract the builder fields. + let rollup_config = builder.rollup_config.expect("rollup_config must be set"); + let chain_provider = builder.chain_provider.expect("chain_provider must be set"); + let l2_chain_provider = builder.l2_chain_provider.expect("chain_provider must be set"); + let dap_source = builder.dap_source.expect("dap_source must be set"); + let attributes_builder = builder.builder.expect("builder must be set"); + + // Compose the stage stack. + let mut l1_traversal = PollingTraversal::new(chain_provider, Arc::clone(&rollup_config)); + l1_traversal.block = Some(builder.origin.expect("origin must be set")); + let l1_retrieval = L1Retrieval::new(l1_traversal, dap_source); + let frame_queue = FrameQueue::new(l1_retrieval, Arc::clone(&rollup_config)); + let channel_provider = ChannelProvider::new(Arc::clone(&rollup_config), frame_queue); + let channel_reader = ChannelReader::new(channel_provider, Arc::clone(&rollup_config)); + let batch_stream = + BatchStream::new(channel_reader, rollup_config.clone(), l2_chain_provider.clone()); + let batch_provider = + BatchProvider::new(rollup_config.clone(), batch_stream, l2_chain_provider.clone()); + let attributes = + AttributesQueue::new(rollup_config.clone(), batch_provider, attributes_builder); + + // Create the pipeline. + Self::new(attributes, rollup_config, l2_chain_provider) + } +} + +impl From> + for DerivationPipeline, T> +where + B: AttributesBuilder + Send + Debug, + P: ChainProvider + Clone + Send + Sync + Debug, + T: L2ChainProvider + Clone + Send + Sync + Debug, + D: DataAvailabilityProvider + Send + Sync + Debug, +{ + fn from(builder: PipelineBuilder) -> Self { + // Extract the builder fields. + let rollup_config = builder.rollup_config.expect("rollup_config must be set"); + let chain_provider = builder.chain_provider.expect("chain_provider must be set"); + let l2_chain_provider = builder.l2_chain_provider.expect("l2_chain_provider must be set"); + let dap_source = builder.dap_source.expect("dap_source must be set"); + let attributes_builder = builder.builder.expect("builder must be set"); + + // Compose the stage stack. + let mut l1_traversal = IndexedTraversal::new(chain_provider, Arc::clone(&rollup_config)); + l1_traversal.block = Some(builder.origin.expect("origin must be set")); + let l1_retrieval = L1Retrieval::new(l1_traversal, dap_source); + let frame_queue = FrameQueue::new(l1_retrieval, Arc::clone(&rollup_config)); + let channel_provider = ChannelProvider::new(Arc::clone(&rollup_config), frame_queue); + let channel_reader = ChannelReader::new(channel_provider, Arc::clone(&rollup_config)); + let batch_stream = + BatchStream::new(channel_reader, rollup_config.clone(), l2_chain_provider.clone()); + let batch_provider = + BatchProvider::new(rollup_config.clone(), batch_stream, l2_chain_provider.clone()); + let attributes = + AttributesQueue::new(rollup_config.clone(), batch_provider, attributes_builder); + + // Create the pipeline. + Self::new(attributes, rollup_config, l2_chain_provider) + } +} diff --git a/kona/crates/protocol/derive/src/pipeline/core.rs b/kona/crates/protocol/derive/src/pipeline/core.rs new file mode 100644 index 0000000000000..6071a07d31283 --- /dev/null +++ b/kona/crates/protocol/derive/src/pipeline/core.rs @@ -0,0 +1,368 @@ +//! Contains the core derivation pipeline. + +use crate::{ + ActivationSignal, L2ChainProvider, NextAttributes, OriginAdvancer, OriginProvider, Pipeline, + PipelineError, PipelineErrorKind, PipelineResult, ResetSignal, Signal, SignalReceiver, + StepResult, +}; +use alloc::{boxed::Box, collections::VecDeque, sync::Arc}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; + +/// The derivation pipeline is responsible for deriving L2 inputs from L1 data. +#[derive(Debug)] +pub struct DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send, + P: L2ChainProvider + Send + Sync + Debug, +{ + /// A handle to the next attributes. + pub attributes: S, + /// Reset provider for the pipeline. + /// A list of prepared [`OpAttributesWithParent`] to be used by the derivation pipeline + /// consumer. + pub prepared: VecDeque, + /// The rollup config. + pub rollup_config: Arc, + /// The L2 Chain Provider used to fetch the system config on reset. + pub l2_chain_provider: P, +} + +impl DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send, + P: L2ChainProvider + Send + Sync + Debug, +{ + /// Creates a new instance of the [`DerivationPipeline`]. + pub const fn new( + attributes: S, + rollup_config: Arc, + l2_chain_provider: P, + ) -> Self { + Self { attributes, prepared: VecDeque::new(), rollup_config, l2_chain_provider } + } +} + +impl OriginProvider for DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send, + P: L2ChainProvider + Send + Sync + Debug, +{ + fn origin(&self) -> Option { + self.attributes.origin() + } +} + +impl Iterator for DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send + Sync, + P: L2ChainProvider + Send + Sync + Debug, +{ + type Item = OpAttributesWithParent; + + fn next(&mut self) -> Option { + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_PAYLOAD_ATTRIBUTES_BUFFER, + self.prepared.len().saturating_sub(1) as f64 + ); + self.prepared.pop_front() + } +} + +#[async_trait] +impl SignalReceiver for DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send + Sync, + P: L2ChainProvider + Send + Sync + Debug, +{ + /// Signals the pipeline by calling the [`SignalReceiver::signal`] method. + /// + /// During a [`Signal::Reset`], each stage is recursively called from the top-level + /// [crate::stages::AttributesQueue] to the bottom [crate::PollingTraversal] + /// with a head-recursion pattern. This effectively clears the internal state + /// of each stage in the pipeline from bottom on up. + /// + /// [`Signal::Activation`] does a similar thing to the reset, with different + /// holocene-specific reset rules. + /// + /// ### Parameters + /// + /// The `signal` is contains the signal variant with any necessary parameters. + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + mut s @ Signal::Reset(ResetSignal { l2_safe_head, .. }) | + mut s @ Signal::Activation(ActivationSignal { l2_safe_head, .. }) => { + let system_config = self + .l2_chain_provider + .system_config_by_number( + l2_safe_head.block_info.number, + Arc::clone(&self.rollup_config), + ) + .await + .map_err(Into::into)?; + s = s.with_system_config(system_config); + match self.attributes.signal(s).await { + Ok(()) => trace!(target: "pipeline", "Stages reset"), + Err(err) => { + if let PipelineErrorKind::Temporary(PipelineError::Eof) = err { + trace!(target: "pipeline", "Stages reset with EOF"); + } else { + error!(target: "pipeline", "Stage reset errored: {:?}", err); + return Err(err); + } + } + } + } + Signal::FlushChannel => { + self.attributes.signal(signal).await?; + } + Signal::ProvideBlock(_) => { + self.attributes.signal(signal).await?; + } + } + kona_macros::inc!( + gauge, + crate::metrics::Metrics::PIPELINE_SIGNALS, + "type" => signal.to_string(), + ); + Ok(()) + } +} + +#[async_trait] +impl Pipeline for DerivationPipeline +where + S: NextAttributes + SignalReceiver + OriginProvider + OriginAdvancer + Debug + Send + Sync, + P: L2ChainProvider + Send + Sync + Debug, +{ + /// Peeks at the next prepared [`OpAttributesWithParent`] from the pipeline. + fn peek(&self) -> Option<&OpAttributesWithParent> { + self.prepared.front() + } + + /// Returns the rollup config. + fn rollup_config(&self) -> &RollupConfig { + &self.rollup_config + } + + /// Returns the [`SystemConfig`] by L2 number. + async fn system_config_by_number( + &mut self, + number: u64, + ) -> Result { + self.l2_chain_provider + .system_config_by_number(number, self.rollup_config.clone()) + .await + .map_err(Into::into) + } + + /// Attempts to progress the pipeline. + /// + /// ## Returns + /// + /// A [PipelineError::Eof] is returned if the pipeline is blocked by waiting for new L1 data. + /// Any other error is critical and the derivation pipeline should be reset. + /// An error is expected when the underlying source closes. + /// + /// When [DerivationPipeline::step] returns [Ok(())], it should be called again, to continue the + /// derivation process. + /// + /// [`PipelineError`]: crate::errors::PipelineError + async fn step(&mut self, cursor: L2BlockInfo) -> StepResult { + kona_macros::inc!(gauge, crate::metrics::Metrics::PIPELINE_STEPS); + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_STEP_BLOCK, + cursor.block_info.number as f64 + ); + match self.attributes.next_attributes(cursor).await { + Ok(a) => { + trace!(target: "pipeline", "Prepared L2 attributes: {:?}", a); + kona_macros::inc!( + gauge, + crate::metrics::Metrics::PIPELINE_PAYLOAD_ATTRIBUTES_BUFFER + ); + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_LATEST_PAYLOAD_TX_COUNT, + a.attributes.transactions.as_ref().map_or(0.0, |txs| txs.len() as f64) + ); + if !a.is_last_in_span { + kona_macros::inc!(gauge, crate::metrics::Metrics::PIPELINE_DERIVED_SPAN_SIZE); + } else { + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_DERIVED_SPAN_SIZE, + 0 + ); + } + self.prepared.push_back(a); + kona_macros::inc!(gauge, crate::metrics::Metrics::PIPELINE_PREPARED_ATTRIBUTES); + StepResult::PreparedAttributes + } + Err(err) => match err { + PipelineErrorKind::Temporary(PipelineError::Eof) => { + trace!(target: "pipeline", "Pipeline advancing origin"); + if let Err(e) = self.attributes.advance_origin().await { + return StepResult::OriginAdvanceErr(e); + } + StepResult::AdvancedOrigin + } + PipelineErrorKind::Temporary(_) => { + trace!(target: "pipeline", "Attributes queue step failed due to temporary error: {:?}", err); + StepResult::StepFailed(err) + } + _ => { + warn!(target: "pipeline", "Attributes queue step failed: {:?}", err); + StepResult::StepFailed(err) + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DerivationPipeline, test_utils::*}; + use alloc::{string::ToString, sync::Arc}; + use alloy_rpc_types_engine::PayloadAttributes; + use kona_genesis::{RollupConfig, SystemConfig}; + use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; + use op_alloy_rpc_types_engine::OpPayloadAttributes; + + fn default_test_payload_attributes() -> OpAttributesWithParent { + OpAttributesWithParent { + attributes: OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 0, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + withdrawals: None, + parent_beacon_block_root: None, + }, + transactions: None, + no_tx_pool: None, + gas_limit: None, + eip_1559_params: None, + min_base_fee: None, + }, + parent: Default::default(), + derived_from: Default::default(), + is_last_in_span: false, + } + } + + #[test] + fn test_pipeline_next_attributes_empty() { + let mut pipeline = new_test_pipeline(); + let result = pipeline.next(); + assert_eq!(result, None); + } + + #[test] + fn test_pipeline_next_attributes_with_peek() { + let mut pipeline = new_test_pipeline(); + let expected = default_test_payload_attributes(); + pipeline.prepared.push_back(expected.clone()); + + let result = pipeline.peek(); + assert_eq!(result, Some(&expected)); + + let result = pipeline.next(); + assert_eq!(result, Some(expected)); + } + + #[tokio::test] + async fn test_derivation_pipeline_missing_block() { + let mut pipeline = new_test_pipeline(); + let cursor = L2BlockInfo::default(); + let result = pipeline.step(cursor).await; + assert_eq!( + result, + StepResult::OriginAdvanceErr( + PipelineError::Provider("Block not found".to_string()).temp() + ) + ); + } + + #[tokio::test] + async fn test_derivation_pipeline_prepared_attributes() { + let rollup_config = Arc::new(RollupConfig::default()); + let l2_chain_provider = TestL2ChainProvider::default(); + let expected = default_test_payload_attributes(); + let attributes = TestNextAttributes { next_attributes: Some(expected) }; + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Step on the pipeline and expect the result. + let cursor = L2BlockInfo::default(); + let result = pipeline.step(cursor).await; + assert_eq!(result, StepResult::PreparedAttributes); + } + + #[tokio::test] + async fn test_derivation_pipeline_advance_origin() { + let rollup_config = Arc::new(RollupConfig::default()); + let l2_chain_provider = TestL2ChainProvider::default(); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Step on the pipeline and expect the result. + let cursor = L2BlockInfo::default(); + let result = pipeline.step(cursor).await; + assert_eq!(result, StepResult::AdvancedOrigin); + } + + #[tokio::test] + async fn test_derivation_pipeline_signal_activation() { + let rollup_config = Arc::new(RollupConfig::default()); + let mut l2_chain_provider = TestL2ChainProvider::default(); + l2_chain_provider.system_configs.insert(0, SystemConfig::default()); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Signal the pipeline to reset. + let result = pipeline.signal(ActivationSignal::default().signal()).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_derivation_pipeline_flush_channel() { + let rollup_config = Arc::new(RollupConfig::default()); + let l2_chain_provider = TestL2ChainProvider::default(); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Signal the pipeline to reset. + let result = pipeline.signal(Signal::FlushChannel).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_derivation_pipeline_signal_reset_missing_sys_config() { + let rollup_config = Arc::new(RollupConfig::default()); + let l2_chain_provider = TestL2ChainProvider::default(); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Signal the pipeline to reset. + let result = pipeline.signal(ResetSignal::default().signal()).await.unwrap_err(); + assert_eq!(result, PipelineError::Provider("System config not found".to_string()).temp()); + } + + #[tokio::test] + async fn test_derivation_pipeline_signal_reset_ok() { + let rollup_config = Arc::new(RollupConfig::default()); + let mut l2_chain_provider = TestL2ChainProvider::default(); + l2_chain_provider.system_configs.insert(0, SystemConfig::default()); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + // Signal the pipeline to reset. + let result = pipeline.signal(ResetSignal::default().signal()).await; + assert!(result.is_ok()); + } +} diff --git a/kona/crates/protocol/derive/src/pipeline/mod.rs b/kona/crates/protocol/derive/src/pipeline/mod.rs new file mode 100644 index 0000000000000..4778363ffb9de --- /dev/null +++ b/kona/crates/protocol/derive/src/pipeline/mod.rs @@ -0,0 +1,14 @@ +//! Module containing the derivation pipeline. + +mod builder; +pub use builder::PipelineBuilder; + +mod core; +pub use core::DerivationPipeline; + +mod types; +pub use types::{ + AttributesQueueStage, BatchProviderStage, BatchStreamStage, ChannelProviderStage, + ChannelReaderStage, FrameQueueStage, IndexedAttributesQueueStage, L1RetrievalStage, + PolledAttributesQueueStage, +}; diff --git a/kona/crates/protocol/derive/src/pipeline/types.rs b/kona/crates/protocol/derive/src/pipeline/types.rs new file mode 100644 index 0000000000000..10a5e77475e7f --- /dev/null +++ b/kona/crates/protocol/derive/src/pipeline/types.rs @@ -0,0 +1,35 @@ +//! Type aliases for the stages in the derivation pipeline. + +use crate::{ + AttributesQueue, BatchProvider, BatchStream, ChannelProvider, ChannelReader, FrameQueue, + IndexedTraversal, L1Retrieval, PollingTraversal, +}; + +/// Type alias for the [`L1Retrieval`] stage. +pub type L1RetrievalStage = L1Retrieval; + +/// Type alias for the [`FrameQueue`] stage. +pub type FrameQueueStage = FrameQueue>; + +/// Type alias for the [`ChannelProvider`] stage. +pub type ChannelProviderStage = ChannelProvider>; + +/// Type alias for the [`ChannelReader`] stage. +pub type ChannelReaderStage = ChannelReader>; + +/// Type alias for the [`BatchStream`] stage. +pub type BatchStreamStage = BatchStream, F>; + +/// Type alias for the [`BatchProvider`] stage. +pub type BatchProviderStage = BatchProvider, F>; + +/// Type alias for the [`AttributesQueue`] stage. +pub type AttributesQueueStage = AttributesQueue, B>; + +/// Type alias for the [`AttributesQueue`] stage that uses a [`PollingTraversal`] stage. +pub type PolledAttributesQueueStage = + AttributesQueueStage, F, B>; + +/// Type alias for the [`AttributesQueue`] stage that uses a [`IndexedTraversal`] stage. +pub type IndexedAttributesQueueStage = + AttributesQueueStage, F, B>; diff --git a/kona/crates/protocol/derive/src/sources/blob_data.rs b/kona/crates/protocol/derive/src/sources/blob_data.rs new file mode 100644 index 0000000000000..62b7e8282b45b --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/blob_data.rs @@ -0,0 +1,288 @@ +//! Contains the `BlobData` struct. + +use crate::BlobDecodingError; +use alloc::{boxed::Box, vec}; +use alloy_eips::eip4844::{BYTES_PER_BLOB, Blob, VERSIONED_HASH_VERSION_KZG}; +use alloy_primitives::Bytes; + +/// The blob encoding version +pub(crate) const BLOB_ENCODING_VERSION: u8 = 0; + +/// Maximum blob data size +pub(crate) const BLOB_MAX_DATA_SIZE: usize = (4 * 31 + 3) * 1024 - 4; // 130044 + +/// Blob Encoding/Decoding Rounds +pub(crate) const BLOB_ENCODING_ROUNDS: usize = 1024; + +/// The Blob Data +#[derive(Default, Clone, Debug)] +pub struct BlobData { + /// The blob data + pub(crate) data: Option, + /// The calldata + pub(crate) calldata: Option, +} + +impl BlobData { + /// Decodes the blob into raw byte data. + /// Returns a [`BlobDecodingError`] if the blob is invalid. + pub(crate) fn decode(&self) -> Result { + let data = self.data.as_ref().ok_or(BlobDecodingError::MissingData)?; + + // Validate the blob encoding version + if data[VERSIONED_HASH_VERSION_KZG as usize] != BLOB_ENCODING_VERSION { + return Err(BlobDecodingError::InvalidEncodingVersion); + } + + // Decode the 3 byte big endian length value into a 4 byte integer + let length = u32::from_be_bytes([0, data[2], data[3], data[4]]) as usize; + + // Validate the length + if length > BLOB_MAX_DATA_SIZE { + return Err(BlobDecodingError::InvalidLength); + } + + // Round 0 copies the remaining 27 bytes of the first field element + let mut output = vec![0u8; BLOB_MAX_DATA_SIZE]; + output[0..27].copy_from_slice(&data[5..32]); + + // Process the remaining 3 field elements to complete round 0 + let mut output_pos = 28; + let mut input_pos = 32; + let mut encoded_byte = [0u8; 4]; + encoded_byte[0] = data[0]; + + for b in encoded_byte.iter_mut().skip(1) { + let (enc, opos, ipos) = + self.decode_field_element(output_pos, input_pos, &mut output)?; + *b = enc; + output_pos = opos; + input_pos = ipos; + } + + // Reassemble the 4 by 6 bit encoded chunks into 3 bytes of output + output_pos = self.reassemble_bytes(output_pos, &encoded_byte, &mut output); + + // In each remaining round, decode 4 field elements (128 bytes) of the + // input into 127 bytes of output + for _ in 1..BLOB_ENCODING_ROUNDS { + // Break early if the output position is greater than the length + if output_pos >= length { + break; + } + + for d in &mut encoded_byte { + let (enc, opos, ipos) = + self.decode_field_element(output_pos, input_pos, &mut output)?; + *d = enc; + output_pos = opos; + input_pos = ipos; + } + output_pos = self.reassemble_bytes(output_pos, &encoded_byte, &mut output); + } + + // Validate the remaining bytes + for o in output.iter().skip(length) { + if *o != 0u8 { + return Err(BlobDecodingError::InvalidFieldElement); + } + } + + // Validate the remaining bytes + output.truncate(length); + for i in input_pos..BYTES_PER_BLOB { + if data[i] != 0 { + return Err(BlobDecodingError::InvalidFieldElement); + } + } + + Ok(Bytes::from(output)) + } + + /// Decodes the next input field element by writing its lower 31 bytes into its + /// appropriate place in the output and checking the high order byte is valid. + /// Returns a [`BlobDecodingError`] if a field element is seen with either of its + /// two high order bits set. + pub(crate) fn decode_field_element( + &self, + output_pos: usize, + input_pos: usize, + output: &mut [u8], + ) -> Result<(u8, usize, usize), BlobDecodingError> { + let Some(data) = self.data.as_ref() else { + return Err(BlobDecodingError::MissingData); + }; + + // two highest order bits of the first byte of each field element should always be 0 + if data[input_pos] & 0b1100_0000 != 0 { + return Err(BlobDecodingError::InvalidFieldElement); + } + output[output_pos..output_pos + 31].copy_from_slice(&data[input_pos + 1..input_pos + 32]); + Ok((data[input_pos], output_pos + 32, input_pos + 32)) + } + + /// Reassemble 4 by 6 bit encoded chunks into 3 bytes of output and place them in their + /// appropriate output positions. + pub(crate) fn reassemble_bytes( + &self, + mut output_pos: usize, + encoded_byte: &[u8], + output: &mut [u8], + ) -> usize { + output_pos -= 1; + let x = (encoded_byte[0] & 0b0011_1111) | ((encoded_byte[1] & 0b0011_0000) << 2); + let y = (encoded_byte[1] & 0b0000_1111) | ((encoded_byte[3] & 0b0000_1111) << 4); + let z = (encoded_byte[2] & 0b0011_1111) | ((encoded_byte[3] & 0b0011_0000) << 2); + output[output_pos - 32] = z; + output[output_pos - (32 * 2)] = y; + output[output_pos - (32 * 3)] = x; + output_pos + } + + /// Fills in the pointers to the fetched blob bodies. + /// There should be exactly one placeholder blobOrCalldata + /// element for each blob, otherwise an error is returned. + pub(crate) fn fill( + &mut self, + blobs: &[Box], + index: usize, + ) -> Result { + // Do not fill if there is calldata here + if self.calldata.is_some() { + return Ok(false); + } + + if index >= blobs.len() { + return Err(BlobDecodingError::InvalidLength); + } + + if blobs[index].is_empty() { + return Err(BlobDecodingError::MissingData); + } + + self.data = Some(Bytes::from(*blobs[index])); + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reassemble_bytes() { + let blob_data = BlobData::default(); + let mut output = vec![0u8; 128]; + let encoded_byte = [0x00, 0x00, 0x00, 0x00]; + let output_pos = blob_data.reassemble_bytes(127, &encoded_byte, &mut output); + assert_eq!(output_pos, 126); + assert_eq!(output, vec![0u8; 128]); + } + + #[test] + fn test_cannot_fill_empty_calldata() { + let mut blob_data = BlobData { calldata: Some(Bytes::new()), ..Default::default() }; + let blobs = vec![Box::new(Blob::with_last_byte(1u8))]; + assert_eq!(blob_data.fill(&blobs, 0), Ok(false)); + } + + #[test] + fn test_fill_oob_index() { + let mut blob_data = BlobData::default(); + let blobs = vec![Box::new(Blob::with_last_byte(1u8))]; + assert_eq!(blob_data.fill(&blobs, 1), Err(BlobDecodingError::InvalidLength)); + } + + #[test] + fn test_fill_zero_blob() { + let mut blob_data = BlobData::default(); + let blobs = vec![Box::new(Blob::ZERO)]; + // consider a blob made entirely of zero bytes a regular blob + assert_eq!(blob_data.fill(&blobs, 0), Ok(true)); + } + + #[test] + fn test_fill_blob() { + let mut blob_data = BlobData::default(); + let blobs = vec![Box::new(Blob::with_last_byte(1u8))]; + assert_eq!(blob_data.fill(&blobs, 0), Ok(true)); + let expected = Bytes::from([&[0u8; 131071][..], &[1u8]].concat()); + assert_eq!(blob_data.data, Some(expected)); + } + + #[test] + fn test_blob_data_decode_missing_data() { + let blob_data = BlobData::default(); + assert_eq!(blob_data.decode(), Err(BlobDecodingError::MissingData)); + } + + #[test] + fn test_blob_data_decode_invalid_encoding_version() { + let blob_data = BlobData { data: Some(Bytes::from(vec![1u8; 32])), ..Default::default() }; + assert_eq!(blob_data.decode(), Err(BlobDecodingError::InvalidEncodingVersion)); + } + + #[test] + fn test_blob_data_decode_invalid_length() { + let mut data = vec![0u8; 32]; + data[VERSIONED_HASH_VERSION_KZG as usize] = BLOB_ENCODING_VERSION; + data[2] = 0xFF; + data[3] = 0xFF; + data[4] = 0xFF; + let blob_data = BlobData { data: Some(Bytes::from(data)), ..Default::default() }; + assert_eq!(blob_data.decode(), Err(BlobDecodingError::InvalidLength)); + } + + #[test] + fn test_blob_data_decode() { + let mut data = vec![0u8; alloy_eips::eip4844::BYTES_PER_BLOB]; + data[VERSIONED_HASH_VERSION_KZG as usize] = BLOB_ENCODING_VERSION; + data[2] = 0x00; + data[3] = 0x00; + data[4] = 0x01; + let blob_data = BlobData { data: Some(Bytes::from(data)), ..Default::default() }; + assert_eq!(blob_data.decode(), Ok(Bytes::from(vec![0u8; 1]))); + } + + #[test] + fn test_blob_data_decode_invalid_field_element() { + let mut data = vec![0u8; alloy_eips::eip4844::BYTES_PER_BLOB + 10]; + data[VERSIONED_HASH_VERSION_KZG as usize] = BLOB_ENCODING_VERSION; + data[2] = 0x00; + data[3] = 0x00; + data[4] = 0x01; + data[33] = 0x01; + let blob_data = BlobData { data: Some(Bytes::from(data)), ..Default::default() }; + assert_eq!(blob_data.decode(), Err(BlobDecodingError::InvalidFieldElement)); + } + + #[test] + fn test_decode_field_element_missing_data() { + let blob_data = BlobData::default(); + assert_eq!( + blob_data.decode_field_element(0, 0, &mut []), + Err(BlobDecodingError::MissingData) + ); + } + + #[test] + fn test_decode_field_element_invalid_field_element() { + let mut data = vec![0u8; 32]; + data[0] = 0b1100_0000; + let blob_data = BlobData { data: Some(Bytes::from(data)), ..Default::default() }; + assert_eq!( + blob_data.decode_field_element(0, 0, &mut []), + Err(BlobDecodingError::InvalidFieldElement) + ); + } + + #[test] + fn test_decode_field_element() { + let mut data = vec![0u8; 32]; + data[1..32].copy_from_slice(&[1u8; 31]); + let blob_data = BlobData { data: Some(Bytes::from(data)), ..Default::default() }; + let mut output = vec![0u8; 31]; + assert_eq!(blob_data.decode_field_element(0, 0, &mut output), Ok((0, 32, 32))); + assert_eq!(output, vec![1u8; 31]); + } +} diff --git a/kona/crates/protocol/derive/src/sources/blobs.rs b/kona/crates/protocol/derive/src/sources/blobs.rs new file mode 100644 index 0000000000000..f258beac5e46d --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/blobs.rs @@ -0,0 +1,355 @@ +//! Blob Data Source + +use crate::{ + BlobData, BlobProvider, BlobProviderError, ChainProvider, DataAvailabilityProvider, + PipelineError, PipelineResult, +}; +use alloc::{boxed::Box, string::ToString, vec::Vec}; +use alloy_consensus::{ + Transaction, TxEip4844Variant, TxEnvelope, TxType, transaction::SignerRecoverable, +}; +use alloy_eips::eip4844::IndexedBlobHash; +use alloy_primitives::{Address, Bytes}; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// A data iterator that reads from a blob. +#[derive(Debug, Clone)] +pub struct BlobSource +where + F: ChainProvider + Send, + B: BlobProvider + Send, +{ + /// Chain provider. + pub chain_provider: F, + /// Fetches blobs. + pub blob_fetcher: B, + /// The address of the batcher contract. + pub batcher_address: Address, + /// Data. + pub data: Vec, + /// Whether the source is open. + pub open: bool, +} + +impl BlobSource +where + F: ChainProvider + Send, + B: BlobProvider + Send, +{ + /// Creates a new blob source. + pub const fn new(chain_provider: F, blob_fetcher: B, batcher_address: Address) -> Self { + Self { chain_provider, blob_fetcher, batcher_address, data: Vec::new(), open: false } + } + + fn extract_blob_data( + &self, + txs: Vec, + batcher_address: Address, + ) -> (Vec, Vec) { + let mut index: u64 = 0; + let mut data = Vec::new(); + let mut hashes = Vec::new(); + for tx in txs { + let (tx_kind, calldata, blob_hashes) = match &tx { + TxEnvelope::Legacy(tx) => (tx.tx().to(), tx.tx().input.clone(), None), + TxEnvelope::Eip2930(tx) => (tx.tx().to(), tx.tx().input.clone(), None), + TxEnvelope::Eip1559(tx) => (tx.tx().to(), tx.tx().input.clone(), None), + TxEnvelope::Eip4844(blob_tx_wrapper) => match blob_tx_wrapper.tx() { + TxEip4844Variant::TxEip4844(tx) => { + (tx.to(), tx.input.clone(), Some(tx.blob_versioned_hashes.clone())) + } + TxEip4844Variant::TxEip4844WithSidecar(tx) => { + let tx = tx.tx(); + (tx.to(), tx.input.clone(), Some(tx.blob_versioned_hashes.clone())) + } + }, + _ => continue, + }; + let Some(to) = tx_kind else { continue }; + + if to != self.batcher_address { + index += blob_hashes.map_or(0, |h| h.len() as u64); + continue; + } + if tx.recover_signer().unwrap_or_default() != batcher_address { + index += blob_hashes.map_or(0, |h| h.len() as u64); + continue; + } + if tx.tx_type() != TxType::Eip4844 { + let blob_data = BlobData { data: None, calldata: Some(calldata.to_vec().into()) }; + data.push(blob_data); + continue; + } + if !calldata.is_empty() { + let hash = match &tx { + TxEnvelope::Legacy(tx) => Some(tx.hash()), + TxEnvelope::Eip2930(tx) => Some(tx.hash()), + TxEnvelope::Eip1559(tx) => Some(tx.hash()), + TxEnvelope::Eip4844(blob_tx_wrapper) => Some(blob_tx_wrapper.hash()), + _ => None, + }; + warn!(target: "blob_source", "Blob tx has calldata, which will be ignored: {hash:?}"); + } + let blob_hashes = if let Some(b) = blob_hashes { + b + } else { + continue; + }; + for hash in blob_hashes { + let indexed = IndexedBlobHash { hash, index }; + hashes.push(indexed); + data.push(BlobData::default()); + index += 1; + } + } + #[cfg(feature = "metrics")] + metrics::gauge!( + crate::metrics::Metrics::PIPELINE_DATA_AVAILABILITY_PROVIDER, + "source" => "blobs", + ) + .increment(data.len() as f64); + (data, hashes) + } + + /// Loads blob data into the source if it is not open. + async fn load_blobs( + &mut self, + block_ref: &BlockInfo, + batcher_address: Address, + ) -> Result<(), BlobProviderError> { + if self.open { + return Ok(()); + } + + let info = self + .chain_provider + .block_info_and_transactions_by_hash(block_ref.hash) + .await + .map_err(|e| BlobProviderError::Backend(e.to_string()))?; + + let (mut data, blob_hashes) = self.extract_blob_data(info.1, batcher_address); + + // If there are no hashes, set the calldata and return. + if blob_hashes.is_empty() { + self.open = true; + self.data = data; + return Ok(()); + } + + let blobs = + self.blob_fetcher.get_and_validate_blobs(block_ref, &blob_hashes).await.map_err( + |e| { + warn!(target: "blob_source", "Failed to fetch blobs: {e}"); + BlobProviderError::Backend(e.to_string()) + }, + )?; + + // Fill the blob pointers. + let mut blob_index = 0; + for blob in data.iter_mut() { + match blob.fill(&blobs, blob_index) { + Ok(should_increment) => { + if should_increment { + blob_index += 1; + } + } + Err(e) => { + return Err(e.into()); + } + } + } + + self.open = true; + self.data = data; + Ok(()) + } + + /// Extracts the next data from the source. + fn next_data(&mut self) -> PipelineResult { + if self.data.is_empty() { + return Err(PipelineError::Eof.temp()); + } + + Ok(self.data.remove(0)) + } +} + +#[async_trait] +impl DataAvailabilityProvider for BlobSource +where + F: ChainProvider + Sync + Send, + B: BlobProvider + Sync + Send, +{ + type Item = Bytes; + + async fn next( + &mut self, + block_ref: &BlockInfo, + batcher_address: Address, + ) -> PipelineResult { + self.load_blobs(block_ref, batcher_address).await?; + + let next_data = self.next_data()?; + if let Some(c) = next_data.calldata { + return Ok(c); + } + + // Decode the blob data to raw bytes. + // Otherwise, ignore blob and recurse next. + match next_data.decode() { + Ok(d) => Ok(d), + Err(_) => { + warn!(target: "blob_source", "Failed to decode blob data, skipping"); + self.next(block_ref, batcher_address).await + } + } + } + + fn clear(&mut self) { + self.data.clear(); + self.open = false; + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{ + errors::PipelineErrorKind, + test_utils::{TestBlobProvider, TestChainProvider}, + }; + use alloc::vec; + use alloy_rlp::Decodable; + + pub(crate) fn default_test_blob_source() -> BlobSource { + let chain_provider = TestChainProvider::default(); + let blob_fetcher = TestBlobProvider::default(); + let batcher_address = Address::default(); + BlobSource::new(chain_provider, blob_fetcher, batcher_address) + } + + pub(crate) fn valid_blob_txs() -> Vec { + // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 + let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap(); + let eip4844 = TxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); + vec![eip4844] + } + + #[tokio::test] + async fn test_load_blobs_open() { + let mut source = default_test_blob_source(); + source.open = true; + assert!(source.load_blobs(&BlockInfo::default(), Address::ZERO).await.is_ok()); + } + + #[tokio::test] + async fn test_load_blobs_chain_provider_err() { + let mut source = default_test_blob_source(); + assert!(matches!( + source.load_blobs(&BlockInfo::default(), Address::ZERO).await, + Err(BlobProviderError::Backend(_)) + )); + } + + #[tokio::test] + async fn test_load_blobs_chain_provider_empty_txs() { + let mut source = default_test_blob_source(); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, Vec::new()); + assert!(!source.open); // Source is not open by default. + assert!(source.load_blobs(&BlockInfo::default(), Address::ZERO).await.is_ok()); + assert!(source.data.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_load_blobs_chain_provider_4844_txs_blob_fetch_error() { + let mut source = default_test_blob_source(); + let block_info = BlockInfo::default(); + let batcher_address = + alloy_primitives::address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2"); + source.batcher_address = + alloy_primitives::address!("11E9CA82A3a762b4B5bd264d4173a242e7a77064"); + let txs = valid_blob_txs(); + source.blob_fetcher.should_error = true; + source.chain_provider.insert_block_with_transactions(1, block_info, txs); + assert!(matches!( + source.load_blobs(&BlockInfo::default(), batcher_address).await, + Err(BlobProviderError::Backend(_)) + )); + } + + #[tokio::test] + async fn test_load_blobs_chain_provider_4844_txs_succeeds() { + use alloy_consensus::Blob; + + let mut source = default_test_blob_source(); + let block_info = BlockInfo::default(); + let batcher_address = + alloy_primitives::address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2"); + source.batcher_address = + alloy_primitives::address!("11E9CA82A3a762b4B5bd264d4173a242e7a77064"); + let txs = valid_blob_txs(); + source.chain_provider.insert_block_with_transactions(1, block_info, txs); + let hashes = [ + alloy_primitives::b256!( + "012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a" + ), + alloy_primitives::b256!( + "0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4" + ), + alloy_primitives::b256!( + "013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7" + ), + alloy_primitives::b256!( + "01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1" + ), + alloy_primitives::b256!( + "011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549" + ), + ]; + for hash in hashes { + source.blob_fetcher.insert_blob(hash, Blob::with_last_byte(1u8)); + } + source.load_blobs(&BlockInfo::default(), batcher_address).await.unwrap(); + assert!(source.open); + assert!(!source.data.is_empty()); + } + + #[tokio::test] + async fn test_open_empty_data_eof() { + let mut source = default_test_blob_source(); + source.open = true; + + let err = source.next(&BlockInfo::default(), Address::ZERO).await.unwrap_err(); + assert!(matches!(err, PipelineErrorKind::Temporary(PipelineError::Eof))); + } + + #[tokio::test] + async fn test_open_calldata() { + let mut source = default_test_blob_source(); + source.open = true; + source.data.push(BlobData { data: None, calldata: Some(Bytes::default()) }); + + let data = source.next(&BlockInfo::default(), Address::ZERO).await.unwrap(); + assert_eq!(data, Bytes::default()); + } + + #[tokio::test] + async fn test_open_blob_data_decode_missing_data() { + let mut source = default_test_blob_source(); + source.open = true; + source.data.push(BlobData { data: Some(Bytes::from(&[1; 32])), calldata: None }); + + let err = source.next(&BlockInfo::default(), Address::ZERO).await.unwrap_err(); + assert!(matches!(err, PipelineErrorKind::Temporary(PipelineError::Eof))); + } + + #[tokio::test] + async fn test_blob_source_pipeline_error() { + let mut source = default_test_blob_source(); + let err = source.next(&BlockInfo::default(), Address::ZERO).await.unwrap_err(); + assert!(matches!(err, PipelineErrorKind::Temporary(PipelineError::Provider(_)))); + } +} diff --git a/kona/crates/protocol/derive/src/sources/calldata.rs b/kona/crates/protocol/derive/src/sources/calldata.rs new file mode 100644 index 0000000000000..2475fe9a86092 --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/calldata.rs @@ -0,0 +1,279 @@ +//! CallData Source + +use crate::{ChainProvider, DataAvailabilityProvider, PipelineError, PipelineResult}; +use alloc::{boxed::Box, collections::VecDeque}; +use alloy_consensus::{Transaction, TxEnvelope, transaction::SignerRecoverable}; +use alloy_primitives::{Address, Bytes}; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// A data iterator that reads from calldata. +#[derive(Debug, Clone)] +pub struct CalldataSource +where + CP: ChainProvider + Send, +{ + /// The chain provider to use for the calldata source. + pub chain_provider: CP, + /// The batch inbox address. + pub batch_inbox_address: Address, + /// Current calldata. + pub calldata: VecDeque, + /// Whether the calldata source is open. + pub open: bool, +} + +impl CalldataSource { + /// Creates a new calldata source. + pub const fn new(chain_provider: CP, batch_inbox_address: Address) -> Self { + Self { chain_provider, batch_inbox_address, calldata: VecDeque::new(), open: false } + } + + /// Loads the calldata into the source if it is not open. + async fn load_calldata( + &mut self, + block_ref: &BlockInfo, + batcher_address: Address, + ) -> Result<(), CP::Error> { + if self.open { + return Ok(()); + } + + let (_, txs) = + self.chain_provider.block_info_and_transactions_by_hash(block_ref.hash).await?; + + self.calldata = txs + .iter() + .filter_map(|tx| { + let (tx_kind, data) = match tx { + TxEnvelope::Legacy(tx) => (tx.tx().to(), tx.tx().input()), + TxEnvelope::Eip2930(tx) => (tx.tx().to(), tx.tx().input()), + TxEnvelope::Eip1559(tx) => (tx.tx().to(), tx.tx().input()), + _ => return None, + }; + let to = tx_kind?; + + if to != self.batch_inbox_address { + return None; + } + if tx.recover_signer().ok()? != batcher_address { + return None; + } + Some(data.to_vec().into()) + }) + .collect::>(); + + #[cfg(feature = "metrics")] + metrics::gauge!( + crate::metrics::Metrics::PIPELINE_DATA_AVAILABILITY_PROVIDER, + "source" => "calldata", + ) + .increment(self.calldata.len() as f64); + + self.open = true; + + Ok(()) + } +} + +#[async_trait] +impl DataAvailabilityProvider for CalldataSource { + type Item = Bytes; + + async fn next( + &mut self, + block_ref: &BlockInfo, + batcher_address: Address, + ) -> PipelineResult { + self.load_calldata(block_ref, batcher_address).await.map_err(Into::into)?; + self.calldata.pop_front().ok_or(PipelineError::Eof.temp()) + } + + fn clear(&mut self) { + self.calldata.clear(); + self.open = false; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{errors::PipelineErrorKind, test_utils::TestChainProvider}; + use alloc::{vec, vec::Vec}; + use alloy_consensus::{Signed, TxEip2930, TxEip4844, TxEip4844Variant, TxEip7702, TxLegacy}; + use alloy_primitives::{Address, Signature, TxKind, address}; + + pub(crate) fn test_legacy_tx(to: Address) -> TxEnvelope { + let sig = Signature::test_signature(); + TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )) + } + + pub(crate) fn test_eip2930_tx(to: Address) -> TxEnvelope { + let sig = Signature::test_signature(); + TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )) + } + + pub(crate) fn test_eip7702_tx(to: Address) -> TxEnvelope { + let sig = Signature::test_signature(); + TxEnvelope::Eip7702(Signed::new_unchecked( + TxEip7702 { to, ..Default::default() }, + sig, + Default::default(), + )) + } + + pub(crate) fn test_blob_tx(to: Address) -> TxEnvelope { + let sig = Signature::test_signature(); + TxEnvelope::Eip4844(Signed::new_unchecked( + TxEip4844Variant::TxEip4844(TxEip4844 { to, ..Default::default() }), + sig, + Default::default(), + )) + } + + pub(crate) fn default_test_calldata_source() -> CalldataSource { + CalldataSource::new(TestChainProvider::default(), Default::default()) + } + + #[tokio::test] + async fn test_clear_calldata() { + let mut source = default_test_calldata_source(); + source.open = true; + source.calldata.push_back(Bytes::default()); + source.clear(); + assert!(source.calldata.is_empty()); + assert!(!source.open); + } + + #[tokio::test] + async fn test_load_calldata_open() { + let mut source = default_test_calldata_source(); + source.open = true; + assert!(source.load_calldata(&BlockInfo::default(), Address::ZERO).await.is_ok()); + } + + #[tokio::test] + async fn test_load_calldata_provider_err() { + let mut source = default_test_calldata_source(); + assert!(source.load_calldata(&BlockInfo::default(), Address::ZERO).await.is_err()); + } + + #[tokio::test] + async fn test_load_calldata_chain_provider_empty_txs() { + let mut source = default_test_calldata_source(); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, Vec::new()); + assert!(!source.open); // Source is not open by default. + assert!(source.load_calldata(&BlockInfo::default(), Address::ZERO).await.is_ok()); + assert!(source.calldata.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_wrong_batch_inbox_address() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + let block_info = BlockInfo::default(); + let tx = test_legacy_tx(batch_inbox_address); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx]); + assert!(!source.open); // Source is not open by default. + assert!(source.load_calldata(&BlockInfo::default(), Address::ZERO).await.is_ok()); + assert!(source.calldata.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_wrong_signer() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + source.batch_inbox_address = batch_inbox_address; + let block_info = BlockInfo::default(); + let tx = test_legacy_tx(batch_inbox_address); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx]); + assert!(!source.open); // Source is not open by default. + assert!(source.load_calldata(&BlockInfo::default(), Address::ZERO).await.is_ok()); + assert!(source.calldata.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_valid_legacy_tx() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + source.batch_inbox_address = batch_inbox_address; + let tx = test_legacy_tx(batch_inbox_address); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); + assert!(!source.open); // Source is not open by default. + assert!( + source.load_calldata(&BlockInfo::default(), tx.recover_signer().unwrap()).await.is_ok() + ); + assert!(!source.calldata.is_empty()); // Calldata is NOT empty. + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_valid_eip2930_tx() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + source.batch_inbox_address = batch_inbox_address; + let tx = test_eip2930_tx(batch_inbox_address); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); + assert!(!source.open); // Source is not open by default. + assert!( + source.load_calldata(&BlockInfo::default(), tx.recover_signer().unwrap()).await.is_ok() + ); + assert!(!source.calldata.is_empty()); // Calldata is NOT empty. + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_blob_tx_ignored() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + source.batch_inbox_address = batch_inbox_address; + let tx = test_blob_tx(batch_inbox_address); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); + assert!(!source.open); // Source is not open by default. + assert!( + source.load_calldata(&BlockInfo::default(), tx.recover_signer().unwrap()).await.is_ok() + ); + assert!(source.calldata.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_load_calldata_eip7702_tx_ignored() { + let batch_inbox_address = address!("0123456789012345678901234567890123456789"); + let mut source = default_test_calldata_source(); + source.batch_inbox_address = batch_inbox_address; + let tx = test_eip7702_tx(batch_inbox_address); + let block_info = BlockInfo::default(); + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); + assert!(!source.open); // Source is not open by default. + assert!( + source.load_calldata(&BlockInfo::default(), tx.recover_signer().unwrap()).await.is_ok() + ); + assert!(source.calldata.is_empty()); + assert!(source.open); + } + + #[tokio::test] + async fn test_next_err_loading_calldata() { + let mut source = default_test_calldata_source(); + assert!(matches!( + source.next(&BlockInfo::default(), Address::ZERO).await, + Err(PipelineErrorKind::Temporary(_)) + )); + } +} diff --git a/kona/crates/protocol/derive/src/sources/ethereum.rs b/kona/crates/protocol/derive/src/sources/ethereum.rs new file mode 100644 index 0000000000000..6b434d86e8e3d --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/ethereum.rs @@ -0,0 +1,162 @@ +//! Contains the [EthereumDataSource], which is a concrete implementation of the +//! [DataAvailabilityProvider] trait for the Ethereum protocol. + +use crate::{ + BlobProvider, BlobSource, CalldataSource, ChainProvider, DataAvailabilityProvider, + PipelineResult, +}; +use alloc::{boxed::Box, fmt::Debug}; +use alloy_primitives::{Address, Bytes}; +use async_trait::async_trait; +use kona_genesis::RollupConfig; +use kona_protocol::BlockInfo; + +/// A factory for creating an Ethereum data source provider. +#[derive(Debug, Clone)] +pub struct EthereumDataSource +where + C: ChainProvider + Send + Clone, + B: BlobProvider + Send + Clone, +{ + /// The ecotone timestamp. + pub ecotone_timestamp: Option, + /// The blob source. + pub blob_source: BlobSource, + /// The calldata source. + pub calldata_source: CalldataSource, +} + +impl EthereumDataSource +where + C: ChainProvider + Send + Clone + Debug, + B: BlobProvider + Send + Clone + Debug, +{ + /// Instantiates a new [`EthereumDataSource`]. + pub const fn new( + blob_source: BlobSource, + calldata_source: CalldataSource, + cfg: &RollupConfig, + ) -> Self { + Self { ecotone_timestamp: cfg.hardforks.ecotone_time, blob_source, calldata_source } + } + + /// Instantiates a new [`EthereumDataSource`] from parts. + pub fn new_from_parts(provider: C, blobs: B, cfg: &RollupConfig) -> Self { + Self { + ecotone_timestamp: cfg.hardforks.ecotone_time, + blob_source: BlobSource::new(provider.clone(), blobs, cfg.batch_inbox_address), + calldata_source: CalldataSource::new(provider, cfg.batch_inbox_address), + } + } +} + +#[async_trait] +impl DataAvailabilityProvider for EthereumDataSource +where + C: ChainProvider + Send + Sync + Clone + Debug, + B: BlobProvider + Send + Sync + Clone + Debug, +{ + type Item = Bytes; + + async fn next( + &mut self, + block_ref: &BlockInfo, + batcher_address: Address, + ) -> PipelineResult { + let ecotone_enabled = + self.ecotone_timestamp.map(|e| block_ref.timestamp >= e).unwrap_or(false); + if ecotone_enabled { + self.blob_source.next(block_ref, batcher_address).await + } else { + self.calldata_source.next(block_ref, batcher_address).await + } + } + + fn clear(&mut self) { + self.blob_source.clear(); + self.calldata_source.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + BlobData, + test_utils::{TestBlobProvider, TestChainProvider}, + }; + use alloc::vec; + use alloy_consensus::TxEnvelope; + use alloy_eips::eip2718::Decodable2718; + use alloy_primitives::{Address, address}; + use kona_genesis::{HardForkConfig, RollupConfig, SystemConfig}; + use kona_protocol::BlockInfo; + + fn default_test_blob_source() -> BlobSource { + let chain_provider = TestChainProvider::default(); + let blob_fetcher = TestBlobProvider::default(); + let batcher_address = Address::default(); + BlobSource::new(chain_provider, blob_fetcher, batcher_address) + } + + #[tokio::test] + async fn test_clear_ethereum_data_source() { + let chain = TestChainProvider::default(); + let blob = TestBlobProvider::default(); + let cfg = RollupConfig::default(); + let mut calldata = CalldataSource::new(chain.clone(), Address::ZERO); + calldata.calldata.insert(0, Default::default()); + calldata.open = true; + let mut blob = BlobSource::new(chain, blob, Address::ZERO); + blob.data = vec![Default::default()]; + blob.open = true; + let mut data_source = EthereumDataSource::new(blob, calldata, &cfg); + + data_source.clear(); + assert!(data_source.blob_source.data.is_empty()); + assert!(!data_source.blob_source.open); + assert!(data_source.calldata_source.calldata.is_empty()); + assert!(!data_source.calldata_source.open); + } + + #[tokio::test] + async fn test_open_blob_source() { + let chain = TestChainProvider::default(); + let mut blob = default_test_blob_source(); + blob.open = true; + blob.data.push(BlobData { data: None, calldata: Some(Bytes::default()) }); + let calldata = CalldataSource::new(chain.clone(), Address::ZERO); + let cfg = RollupConfig { + hardforks: HardForkConfig { ecotone_time: Some(0), ..Default::default() }, + ..Default::default() + }; + + // Should successfully retrieve a blob batch from the block + let mut data_source = EthereumDataSource::new(blob, calldata, &cfg); + let data = data_source.next(&BlockInfo::default(), Address::ZERO).await.unwrap(); + assert_eq!(data, Bytes::default()); + } + + #[tokio::test] + async fn test_open_ethereum_calldata_source_pre_ecotone() { + let mut chain = TestChainProvider::default(); + let blob = TestBlobProvider::default(); + let batcher_address = address!("6887246668a3b87F54DeB3b94Ba47a6f63F32985"); + let batch_inbox = address!("FF00000000000000000000000000000000000010"); + let block_ref = BlockInfo { number: 10, ..Default::default() }; + + let mut cfg = RollupConfig::default(); + cfg.genesis.system_config = Some(SystemConfig { batcher_address, ..Default::default() }); + cfg.batch_inbox_address = batch_inbox; + + // load a test batcher transaction + let raw_batcher_tx = include_bytes!("../../testdata/raw_batcher_tx.hex"); + let tx = TxEnvelope::decode_2718(&mut raw_batcher_tx.as_ref()).unwrap(); + chain.insert_block_with_transactions(10, block_ref, vec![tx]); + + // Should successfully retrieve a calldata batch from the block + let mut data_source = EthereumDataSource::new_from_parts(chain, blob, &cfg); + let calldata_batch = data_source.next(&block_ref, batcher_address).await.unwrap(); + assert_eq!(calldata_batch.len(), 119823); + } +} diff --git a/kona/crates/protocol/derive/src/sources/mod.rs b/kona/crates/protocol/derive/src/sources/mod.rs new file mode 100644 index 0000000000000..d7a5e5f1455bb --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/mod.rs @@ -0,0 +1,20 @@ +//! The data source module. +//! +//! Data sources are data providers for the kona derivation pipeline. +//! They implement the [DataAvailabilityProvider] trait, providing a way +//! to iterate over data for a given (L2) [BlockInfo]. +//! +//! [DataAvailabilityProvider]: crate::traits::DataAvailabilityProvider +//! [BlockInfo]: kona_protocol::BlockInfo + +mod blob_data; +pub use blob_data::BlobData; + +mod ethereum; +pub use ethereum::EthereumDataSource; + +mod blobs; +pub use blobs::BlobSource; + +mod calldata; +pub use calldata::CalldataSource; diff --git a/kona/crates/protocol/derive/src/sources/variant.rs b/kona/crates/protocol/derive/src/sources/variant.rs new file mode 100644 index 0000000000000..6d76c34bf31d9 --- /dev/null +++ b/kona/crates/protocol/derive/src/sources/variant.rs @@ -0,0 +1,78 @@ +//! Data source + +use crate::{ + BlobSource, CalldataSource, + AsyncIterator, BlobProvider, ChainProvider, + PipelineResult, +}; +use alloc::boxed::Box; +use alloy_primitives::Bytes; +use async_trait::async_trait; + +/// An enum over the various data sources. +#[derive(Debug, Clone)] +pub enum EthereumDataSourceVariant +where + CP: ChainProvider + Send, + B: BlobProvider + Send, +{ + /// A calldata source. + Calldata(CalldataSource), + /// A blob source. + Blob(BlobSource), +} + +#[async_trait] +impl AsyncIterator for EthereumDataSourceVariant +where + CP: ChainProvider + Send, + B: BlobProvider + Send, +{ + type Item = Bytes; + + async fn next(&mut self) -> PipelineResult { + match self { + Self::Calldata(c) => c.next().await, + Self::Blob(b) => b.next().await, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestChainProvider; + use kona_protocol::BlockInfo; + + use crate::{ + BlobData, EthereumDataSourceVariant, + test_utils::TestBlobProvider, + }; + + #[tokio::test] + async fn test_variant_next_calldata() { + let chain = TestChainProvider::default(); + let block_ref = BlockInfo::default(); + let mut source = + CalldataSource::new(chain, Default::default(), block_ref, Default::default()); + source.open = true; + source.calldata.push_back(Default::default()); + let mut variant: EthereumDataSourceVariant = + EthereumDataSourceVariant::Calldata(source); + assert!(variant.next().await.is_ok()); + } + + #[tokio::test] + async fn test_variant_next_blob() { + let chain = TestChainProvider::default(); + let blob = TestBlobProvider::default(); + let block_ref = BlockInfo::default(); + let mut source = + BlobSource::new(chain, blob, Default::default(), block_ref, Default::default()); + source.open = true; + source.data.push(BlobData { calldata: Some(Default::default()), ..Default::default() }); + let mut variant: EthereumDataSourceVariant = + EthereumDataSourceVariant::Blob(source); + assert!(variant.next().await.is_ok()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/attributes_queue.rs b/kona/crates/protocol/derive/src/stages/attributes_queue.rs new file mode 100644 index 0000000000000..e3746e195d7a0 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/attributes_queue.rs @@ -0,0 +1,405 @@ +//! Contains the logic for the `AttributesQueue` stage. + +use crate::{ + errors::{PipelineError, ResetError}, + traits::{ + AttributesBuilder, AttributesProvider, NextAttributes, OriginAdvancer, OriginProvider, + SignalReceiver, + }, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, sync::Arc}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent, SingleBatch}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// [`AttributesQueue`] accepts batches from the [`BatchQueue`] stage +/// and transforms them into [`OpPayloadAttributes`]. +/// +/// The outputted payload attributes cannot be buffered because each batch->attributes +/// transformation pulls in data about the current L2 safe head. +/// +/// [`AttributesQueue`] also buffers batches that have been output because +/// multiple batches can be created at once. +/// +/// This stage can be reset by clearing its batch buffer. +/// This stage does not need to retain any references to L1 blocks. +/// +/// [`BatchQueue`]: crate::stages::BatchQueue +#[derive(Debug)] +pub struct AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + AB: AttributesBuilder + Debug, +{ + /// The rollup config. + pub cfg: Arc, + /// The previous stage of the derivation pipeline. + pub prev: P, + /// Whether the current batch is the last in its span. + pub is_last_in_span: bool, + /// The current batch being processed. + pub batch: Option, + /// The attributes builder. + pub builder: AB, +} + +impl AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + AB: AttributesBuilder + Debug, +{ + /// Create a new [`AttributesQueue`] stage. + pub const fn new(cfg: Arc, prev: P, builder: AB) -> Self { + Self { cfg, prev, is_last_in_span: false, batch: None, builder } + } + + /// Loads a [`SingleBatch`] from the [`AttributesProvider`] if needed. + pub async fn load_batch(&mut self, parent: L2BlockInfo) -> PipelineResult { + if self.batch.is_none() { + let batch = self.prev.next_batch(parent).await?; + self.batch = Some(batch); + self.is_last_in_span = self.prev.is_last_in_span(); + } + self.batch.as_ref().cloned().ok_or(PipelineError::Eof.temp()) + } + + /// Returns the next [`OpAttributesWithParent`] from the current batch. + pub async fn next_attributes( + &mut self, + parent: L2BlockInfo, + ) -> PipelineResult { + let batch = match self.load_batch(parent).await { + Ok(batch) => batch, + Err(e) => { + return Err(e); + } + }; + + // Construct the payload attributes from the loaded batch. + #[cfg(feature = "metrics")] + let start = std::time::Instant::now(); + let attributes = match self.create_next_attributes(batch, parent).await { + Ok(attributes) => attributes, + Err(e) => { + return Err(e); + } + }; + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + let populated_attributes = + OpAttributesWithParent::new(attributes, parent, Some(origin), self.is_last_in_span); + kona_macros::record!( + histogram, + crate::metrics::Metrics::PIPELINE_ATTRIBUTES_BUILD_DURATION, + start.elapsed().as_secs_f64() + ); + + // Clear out the local state once payload attributes are prepared. + self.batch = None; + self.is_last_in_span = false; + Ok(populated_attributes) + } + + /// Creates the next attributes, transforming a [`SingleBatch`] into [`OpPayloadAttributes`]. + /// This sets `no_tx_pool` and appends the batched txs to the attributes tx list. + pub async fn create_next_attributes( + &mut self, + batch: SingleBatch, + parent: L2BlockInfo, + ) -> PipelineResult { + // Sanity check parent hash + if batch.parent_hash != parent.block_info.hash { + return Err(ResetError::BadParentHash(batch.parent_hash, parent.block_info.hash).into()); + } + + // Sanity check timestamp + let actual = parent.block_info.timestamp + self.cfg.block_time; + if actual != batch.timestamp { + return Err(ResetError::BadTimestamp(batch.timestamp, actual).into()); + } + + // Prepare the payload attributes + let tx_count = batch.transactions.len(); + let mut attributes = self.builder.prepare_payload_attributes(parent, batch.epoch()).await?; + attributes.no_tx_pool = Some(true); + match attributes.transactions { + Some(ref mut txs) => txs.extend(batch.transactions), + None => { + if !batch.transactions.is_empty() { + attributes.transactions = Some(batch.transactions); + } + } + } + + info!( + target: "attributes_queue", + txs = tx_count, + timestamp = batch.timestamp, + "generated attributes in payload queue", + ); + + Ok(attributes) + } +} + +#[async_trait] +impl OriginAdvancer for AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug + Send, + AB: AttributesBuilder + Debug + Send, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl NextAttributes for AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug + Send, + AB: AttributesBuilder + Debug + Send, +{ + async fn next_attributes( + &mut self, + parent: L2BlockInfo, + ) -> PipelineResult { + self.next_attributes(parent).await + } +} + +impl OriginProvider for AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + AB: AttributesBuilder + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl SignalReceiver for AttributesQueue +where + P: AttributesProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + AB: AttributesBuilder + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + s @ Signal::Reset(_) | s @ Signal::Activation(_) => { + self.prev.signal(s).await?; + self.batch = None; + self.is_last_in_span = false; + } + s @ Signal::FlushChannel => { + self.batch = None; + self.prev.signal(s).await?; + } + s @ Signal::ProvideBlock(_) => { + self.prev.signal(s).await?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::{BuilderError, PipelineErrorKind}, + test_utils::{TestAttributesBuilder, TestAttributesProvider, new_test_attributes_provider}, + types::ResetSignal, + }; + use alloc::{sync::Arc, vec, vec::Vec}; + use alloy_primitives::{Address, B256, Bytes, b256}; + use alloy_rpc_types_engine::PayloadAttributes; + + fn default_optimism_payload_attributes() -> OpPayloadAttributes { + OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 0, + suggested_fee_recipient: Address::default(), + prev_randao: B256::default(), + withdrawals: None, + parent_beacon_block_root: None, + }, + no_tx_pool: Some(false), + transactions: None, + gas_limit: None, + eip_1559_params: None, + min_base_fee: None, + } + } + + fn new_attributes_queue( + cfg: Option, + origin: Option, + batches: Vec>, + ) -> AttributesQueue { + let cfg = cfg.unwrap_or_default(); + let mock_batch_queue = new_test_attributes_provider(origin, batches); + let mock_attributes_builder = TestAttributesBuilder::default(); + AttributesQueue::new(Arc::new(cfg), mock_batch_queue, mock_attributes_builder) + } + + #[tokio::test] + async fn test_attributes_queue_flush() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + attributes_queue.batch = Some(SingleBatch::default()); + assert!(!attributes_queue.prev.flushed); + attributes_queue.signal(Signal::FlushChannel).await.unwrap(); + assert!(attributes_queue.prev.flushed); + assert!(attributes_queue.batch.is_none()); + } + + #[tokio::test] + async fn test_attributes_queue_reset() { + let cfg = RollupConfig::default(); + let mock = new_test_attributes_provider(None, vec![]); + let mock_builder = TestAttributesBuilder::default(); + let mut aq = AttributesQueue::new(Arc::new(cfg), mock, mock_builder); + aq.batch = Some(SingleBatch::default()); + assert!(!aq.prev.reset); + aq.signal(ResetSignal::default().signal()).await.unwrap(); + assert!(aq.batch.is_none()); + assert!(aq.prev.reset); + } + + #[tokio::test] + async fn test_load_batch_eof() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let parent = L2BlockInfo::default(); + let result = attributes_queue.load_batch(parent).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_load_batch_last_in_span() { + let mut attributes_queue = new_attributes_queue(None, None, vec![Ok(Default::default())]); + let parent = L2BlockInfo::default(); + let result = attributes_queue.load_batch(parent).await.unwrap(); + assert_eq!(result, Default::default()); + assert!(attributes_queue.is_last_in_span); + } + + #[tokio::test] + async fn test_create_next_attributes_bad_parent_hash() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let bad_hash = b256!("6666666666666666666666666666666666666666666666666666666666666666"); + let parent = L2BlockInfo { + block_info: BlockInfo { hash: bad_hash, ..Default::default() }, + ..Default::default() + }; + let batch = SingleBatch::default(); + let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err(); + assert_eq!( + result, + PipelineErrorKind::Reset(ResetError::BadParentHash(Default::default(), bad_hash)) + ); + } + + #[tokio::test] + async fn test_create_next_attributes_bad_timestamp() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let parent = L2BlockInfo::default(); + let batch = SingleBatch { timestamp: 1, ..Default::default() }; + let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err(); + assert_eq!(result, PipelineErrorKind::Reset(ResetError::BadTimestamp(1, 0))); + } + + #[tokio::test] + async fn test_create_next_attributes_bad_parent_timestamp() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let parent = L2BlockInfo { + block_info: BlockInfo { timestamp: 2, ..Default::default() }, + ..Default::default() + }; + let batch = SingleBatch { timestamp: 1, ..Default::default() }; + let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err(); + assert_eq!(result, PipelineErrorKind::Reset(ResetError::BadTimestamp(1, 2))); + } + + #[tokio::test] + async fn test_create_next_attributes_bad_config_timestamp() { + let cfg = RollupConfig { block_time: 1, ..Default::default() }; + let mut attributes_queue = new_attributes_queue(Some(cfg), None, vec![]); + let parent = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let batch = SingleBatch { timestamp: 1, ..Default::default() }; + let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err(); + assert_eq!(result, PipelineErrorKind::Reset(ResetError::BadTimestamp(1, 2))); + } + + #[tokio::test] + async fn test_create_next_attributes_preparation_fails() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let parent = L2BlockInfo::default(); + let batch = SingleBatch::default(); + let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err(); + assert_eq!( + result, + PipelineError::AttributesBuilder(BuilderError::AttributesUnavailable).crit() + ); + } + + #[tokio::test] + async fn test_create_next_attributes_success() { + let cfg = RollupConfig::default(); + let mock = new_test_attributes_provider(None, vec![]); + let mut payload_attributes = default_optimism_payload_attributes(); + let mock_builder = + TestAttributesBuilder { attributes: vec![Ok(payload_attributes.clone())] }; + let mut aq = AttributesQueue::new(Arc::new(cfg), mock, mock_builder); + let parent = L2BlockInfo::default(); + let txs = vec![Bytes::default(), Bytes::default()]; + let batch = SingleBatch { transactions: txs.clone(), ..Default::default() }; + let attributes = aq.create_next_attributes(batch, parent).await.unwrap(); + // update the expected attributes + payload_attributes.no_tx_pool = Some(true); + match payload_attributes.transactions { + Some(ref mut t) => t.extend(txs), + None => payload_attributes.transactions = Some(txs), + } + assert_eq!(attributes, payload_attributes); + } + + #[tokio::test] + async fn test_next_attributes_load_batch_eof() { + let mut attributes_queue = new_attributes_queue(None, None, vec![]); + let parent = L2BlockInfo::default(); + let result = attributes_queue.next_attributes(parent).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_next_attributes_load_batch_last_in_span() { + let cfg = RollupConfig::default(); + let mock = + new_test_attributes_provider(Some(Default::default()), vec![Ok(Default::default())]); + let mut pa = default_optimism_payload_attributes(); + let mock_builder = TestAttributesBuilder { attributes: vec![Ok(pa.clone())] }; + let mut aq = AttributesQueue::new(Arc::new(cfg), mock, mock_builder); + // If we load the batch, we should get the last in span. + // But it won't take it so it will be available in the next_attributes call. + let _ = aq.load_batch(L2BlockInfo::default()).await.unwrap(); + assert!(aq.is_last_in_span); + assert!(aq.batch.is_some()); + // This should successfully construct the next payload attributes. + // It should also reset the last in span flag and clear the batch. + let attributes = aq.next_attributes(L2BlockInfo::default()).await.unwrap(); + pa.no_tx_pool = Some(true); + let populated_attributes = OpAttributesWithParent { + attributes: pa, + parent: L2BlockInfo::default(), + derived_from: Some(BlockInfo::default()), + is_last_in_span: true, + }; + assert_eq!(attributes, populated_attributes); + assert!(!aq.is_last_in_span); + assert!(aq.batch.is_none()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/batch/batch_provider.rs b/kona/crates/protocol/derive/src/stages/batch/batch_provider.rs new file mode 100644 index 0000000000000..d6036e283cdca --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/batch/batch_provider.rs @@ -0,0 +1,309 @@ +//! This module contains the [`BatchProvider`] stage. + +use super::NextBatchProvider; +use crate::{ + AttributesProvider, BatchQueue, BatchValidator, L2ChainProvider, OriginAdvancer, + OriginProvider, PipelineError, PipelineResult, Signal, SignalReceiver, +}; +use alloc::{boxed::Box, sync::Arc}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo, SingleBatch}; + +/// The [`BatchProvider`] stage is a mux between the [`BatchQueue`] and [`BatchValidator`] stages. +/// +/// Rules: +/// When Holocene is not active, the [`BatchQueue`] is used. +/// When Holocene is active, the [`BatchValidator`] is used. +/// +/// When transitioning between the two stages, the mux will reset the active stage, but +/// retain `l1_blocks`. +#[derive(Debug)] +pub struct BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + F: L2ChainProvider + Clone + Debug, +{ + /// The rollup configuration. + pub cfg: Arc, + /// The L2 chain provider. + pub provider: F, + /// The previous stage of the derivation pipeline. + /// + /// If this is set to [`None`], the multiplexer has been activated and the active stage + /// owns the previous stage. + /// + /// Must be [`None`] if `batch_queue` or `batch_validator` is [`Some`]. + pub prev: Option

, + /// The batch queue stage of the provider. + /// + /// Must be [`None`] if `prev` or `batch_validator` is [`Some`]. + pub batch_queue: Option>, + /// The batch validator stage of the provider. + /// + /// Must be [`None`] if `prev` or `batch_queue` is [`Some`]. + pub batch_validator: Option>, +} + +impl BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + F: L2ChainProvider + Clone + Debug, +{ + /// Creates a new [`BatchProvider`] with the given configuration and previous stage. + pub const fn new(cfg: Arc, prev: P, provider: F) -> Self { + Self { cfg, provider, prev: Some(prev), batch_queue: None, batch_validator: None } + } + + /// Attempts to update the active stage of the mux. + pub(crate) fn attempt_update(&mut self) -> PipelineResult<()> { + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + if let Some(prev) = self.prev.take() { + // On the first call to `attempt_update`, we need to determine the active stage to + // initialize the mux with. + if self.cfg.is_holocene_active(origin.timestamp) { + self.batch_validator = Some(BatchValidator::new(self.cfg.clone(), prev)); + } else { + self.batch_queue = + Some(BatchQueue::new(self.cfg.clone(), prev, self.provider.clone())); + } + } else if self.batch_queue.is_some() && self.cfg.is_holocene_active(origin.timestamp) { + // If the batch queue is active and Holocene is also active, transition to the batch + // validator. + let batch_queue = self.batch_queue.take().expect("Must have batch queue"); + let mut bv = BatchValidator::new(self.cfg.clone(), batch_queue.prev); + bv.l1_blocks = batch_queue.l1_blocks; + self.batch_validator = Some(bv); + } else if self.batch_validator.is_some() && !self.cfg.is_holocene_active(origin.timestamp) { + // If the batch validator is active, and Holocene is not active, it indicates an L1 + // reorg around Holocene activation. Transition back to the batch queue + // until Holocene re-activates. + let batch_validator = self.batch_validator.take().expect("Must have batch validator"); + let mut bq = + BatchQueue::new(self.cfg.clone(), batch_validator.prev, self.provider.clone()); + bq.l1_blocks = batch_validator.l1_blocks; + self.batch_queue = Some(bq); + } + Ok(()) + } +} + +#[async_trait] +impl OriginAdvancer for BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + F: L2ChainProvider + Clone + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.attempt_update()?; + + if let Some(batch_validator) = self.batch_validator.as_mut() { + batch_validator.advance_origin().await + } else if let Some(batch_queue) = self.batch_queue.as_mut() { + batch_queue.advance_origin().await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +impl OriginProvider for BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + F: L2ChainProvider + Clone + Debug, +{ + fn origin(&self) -> Option { + self.batch_validator.as_ref().map_or_else( + || { + self.batch_queue.as_ref().map_or_else( + || self.prev.as_ref().and_then(|prev| prev.origin()), + |batch_queue| batch_queue.origin(), + ) + }, + |batch_validator| batch_validator.origin(), + ) + } +} + +#[async_trait] +impl SignalReceiver for BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + F: L2ChainProvider + Clone + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.attempt_update()?; + + if let Some(batch_validator) = self.batch_validator.as_mut() { + batch_validator.signal(signal).await + } else if let Some(batch_queue) = self.batch_queue.as_mut() { + batch_queue.signal(signal).await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +#[async_trait] +impl AttributesProvider for BatchProvider +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug + Send, + F: L2ChainProvider + Clone + Send + Debug, +{ + fn is_last_in_span(&self) -> bool { + self.batch_validator.as_ref().map_or_else( + || self.batch_queue.as_ref().is_some_and(|batch_queue| batch_queue.is_last_in_span()), + |batch_validator| batch_validator.is_last_in_span(), + ) + } + + async fn next_batch(&mut self, parent: L2BlockInfo) -> PipelineResult { + self.attempt_update()?; + + if let Some(batch_validator) = self.batch_validator.as_mut() { + batch_validator.next_batch(parent).await + } else if let Some(batch_queue) = self.batch_queue.as_mut() { + batch_queue.next_batch(parent).await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +#[cfg(test)] +mod test { + use super::BatchProvider; + use crate::{ + test_utils::{TestL2ChainProvider, TestNextBatchProvider}, + traits::{OriginProvider, SignalReceiver}, + types::ResetSignal, + }; + use alloc::{sync::Arc, vec}; + use kona_genesis::{HardForkConfig, RollupConfig}; + use kona_protocol::BlockInfo; + + #[test] + fn test_batch_provider_validator_active() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + assert!(batch_provider.attempt_update().is_ok()); + assert!(batch_provider.prev.is_none()); + assert!(batch_provider.batch_queue.is_none()); + assert!(batch_provider.batch_validator.is_some()); + } + + #[test] + fn test_batch_provider_batch_queue_active() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + assert!(batch_provider.attempt_update().is_ok()); + assert!(batch_provider.prev.is_none()); + assert!(batch_provider.batch_queue.is_some()); + assert!(batch_provider.batch_validator.is_none()); + } + + #[test] + fn test_batch_provider_transition_stage() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(2), ..Default::default() }, + ..Default::default() + }); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + batch_provider.attempt_update().unwrap(); + + // Update the L1 origin to Holocene activation. + let Some(ref mut stage) = batch_provider.batch_queue else { + panic!("Expected BatchQueue"); + }; + stage.prev.origin = Some(BlockInfo { number: 1, timestamp: 2, ..Default::default() }); + + // Transition to the BatchValidator stage. + batch_provider.attempt_update().unwrap(); + assert!(batch_provider.batch_queue.is_none()); + assert!(batch_provider.batch_validator.is_some()); + + assert_eq!(batch_provider.origin().unwrap().number, 1); + } + + #[test] + fn test_batch_provider_transition_stage_backwards() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(2), ..Default::default() }, + ..Default::default() + }); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + batch_provider.attempt_update().unwrap(); + + // Update the L1 origin to Holocene activation. + let Some(ref mut stage) = batch_provider.batch_queue else { + panic!("Expected BatchQueue"); + }; + stage.prev.origin = Some(BlockInfo { number: 1, timestamp: 2, ..Default::default() }); + + // Transition to the BatchValidator stage. + batch_provider.attempt_update().unwrap(); + assert!(batch_provider.batch_queue.is_none()); + assert!(batch_provider.batch_validator.is_some()); + + // Update the L1 origin to before Holocene activation, to simulate a re-org. + let Some(ref mut stage) = batch_provider.batch_validator else { + panic!("Expected BatchValidator"); + }; + stage.prev.origin = Some(BlockInfo::default()); + + batch_provider.attempt_update().unwrap(); + assert!(batch_provider.batch_queue.is_some()); + assert!(batch_provider.batch_validator.is_none()); + } + + #[tokio::test] + async fn test_batch_provider_reset_bq() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + // Reset the batch provider. + batch_provider.signal(ResetSignal::default().signal()).await.unwrap(); + + let Some(bq) = batch_provider.batch_queue else { + panic!("Expected BatchQueue"); + }; + assert!(bq.l1_blocks.len() == 1); + } + + #[tokio::test] + async fn test_batch_provider_reset_validator() { + let provider = TestNextBatchProvider::new(vec![]); + let l2_provider = TestL2ChainProvider::default(); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut batch_provider = BatchProvider::new(cfg, provider, l2_provider); + + // Reset the batch provider. + batch_provider.signal(ResetSignal::default().signal()).await.unwrap(); + + let Some(bv) = batch_provider.batch_validator else { + panic!("Expected BatchValidator"); + }; + assert!(bv.l1_blocks.len() == 1); + } +} diff --git a/kona/crates/protocol/derive/src/stages/batch/batch_queue.rs b/kona/crates/protocol/derive/src/stages/batch/batch_queue.rs new file mode 100644 index 0000000000000..95fd042cc7be6 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/batch/batch_queue.rs @@ -0,0 +1,1109 @@ +//! This module contains the `BatchQueue` stage implementation. + +use super::NextBatchProvider; +use crate::{ + errors::{PipelineEncodingError, PipelineError, PipelineErrorKind, ResetError}, + traits::{AttributesProvider, L2ChainProvider, OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, ResetSignal, Signal}, +}; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{ + Batch, BatchValidity, BatchWithInclusionBlock, BlockInfo, L2BlockInfo, SingleBatch, +}; + +/// [`BatchQueue`] is responsible for ordering unordered batches +/// and generating empty batches when the sequence window has passed. +/// +/// It receives batches that are tagged with the L1 Inclusion block of the batch. +/// It only considers batches that are inside the sequencing window of a specific L1 Origin. +/// It tries to eagerly pull batches based on the current L2 safe head. +/// Otherwise it filters/creates an entire epoch's worth of batches at once. +/// +/// This stage tracks a range of L1 blocks with the assumption that all batches with an L1 inclusion +/// block inside that range have been added to the stage by the time that it attempts to advance a +/// full epoch. +/// +/// It is internally responsible for making sure that batches with L1 inclusions block outside it's +/// working range are not considered or pruned. +#[derive(Debug)] +pub struct BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + /// The rollup config. + pub cfg: Arc, + /// The previous stage of the derivation pipeline. + pub prev: P, + /// The l1 block ref + pub origin: Option, + /// A consecutive, time-centric window of L1 Blocks. + /// Every L1 origin of unsafe L2 Blocks must be included in this list. + /// If every L2 Block corresponding to a single L1 Block becomes safe, + /// the block is popped from this list. + /// If new L2 Block's L1 origin is not included in this list, fetch and + /// push it to the list. + pub l1_blocks: Vec, + /// A set of batches in order from when we've seen them. + pub batches: Vec, + /// A set of cached [`SingleBatch`]es derived from [`SpanBatch`]es. + /// + /// [`SpanBatch`]: kona_protocol::SpanBatch + pub next_spans: Vec, + /// Used to validate the batches. + pub fetcher: BF, +} + +impl BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + /// Creates a new [`BatchQueue`] stage. + #[allow(clippy::missing_const_for_fn)] + pub fn new(cfg: Arc, prev: P, fetcher: BF) -> Self { + Self { + cfg, + prev, + origin: None, + l1_blocks: Default::default(), + batches: Default::default(), + next_spans: Default::default(), + fetcher, + } + } + + /// Pops the next batch from the current queued up span-batch cache. + /// The parent is used to set the parent hash of the batch. + /// The parent is verified when the batch is later validated. + pub fn pop_next_batch(&mut self, parent: L2BlockInfo) -> Option { + if self.next_spans.is_empty() { + panic!("Invalid state: must have next spans to pop"); + } + let mut next = self.next_spans.remove(0); + next.parent_hash = parent.block_info.hash; + Some(next) + } + + /// Derives the next batch to apply on top of the current L2 safe head. + /// Follows the validity rules imposed on consecutive batches. + /// Based on currently available buffered batch and L1 origin information. + /// A [PipelineError::Eof] is returned if no batch can be derived yet. + pub async fn derive_next_batch( + &mut self, + empty: bool, + parent: L2BlockInfo, + ) -> PipelineResult { + // Cannot derive a batch if no origin was prepared. + if self.l1_blocks.is_empty() { + return Err(PipelineError::MissingOrigin.crit()); + } + + // Get the epoch + let epoch = self.l1_blocks[0]; + info!(target: "batch_queue", "Deriving next batch for epoch: {}", epoch.number); + + // Note: epoch origin can now be one block ahead of the L2 Safe Head + // This is in the case where we auto generate all batches in an epoch & advance the epoch + // but don't advance the L2 Safe Head's epoch + if parent.l1_origin != epoch.id() && parent.l1_origin.number != epoch.number - 1 { + return Err(PipelineErrorKind::Reset(ResetError::L1OriginMismatch( + parent.l1_origin.number, + epoch.number - 1, + ))); + } + + // Find the first-seen batch that matches all validity conditions. + // We may not have sufficient information to proceed filtering, and then we stop. + // There may be none: in that case we force-create an empty batch + let mut next_batch = None; + let next_timestamp = parent.block_info.timestamp + self.cfg.block_time; + + let origin = self.origin.ok_or(PipelineError::MissingOrigin.crit())?; + + // Go over all batches, in order of inclusion, and find the first batch we can accept. + // Filter in-place by only remembering the batches that may be processed in the future, or + // any undecided ones. + let mut remaining = Vec::new(); + for i in 0..self.batches.len() { + let batch = &self.batches[i]; + let validity = + batch.check_batch(&self.cfg, &self.l1_blocks, parent, &mut self.fetcher).await; + match validity { + BatchValidity::Future => { + // Drop Future batches post-holocene. + // + // See: + if !self.cfg.is_holocene_active(origin.timestamp) { + remaining.push(batch.clone()); + } else { + self.prev.flush(); + warn!(target: "batch_queue", "[HOLOCENE] Dropping future batch with parent: {}", parent.block_info.number); + } + } + BatchValidity::Drop => { + // If we drop a batch, flush previous batches buffered in the BatchStream + // stage. + self.prev.flush(); + warn!(target: "batch_queue", "Dropping batch with parent: {}", parent.block_info); + continue; + } + BatchValidity::Accept => { + next_batch = Some(batch.clone()); + // Don't keep the current batch in the remaining items since we are processing + // it now, but retain every batch we didn't get to yet. + remaining.extend_from_slice(&self.batches[i + 1..]); + break; + } + BatchValidity::Undecided => { + remaining.extend_from_slice(&self.batches[i..]); + self.batches = remaining; + return Err(PipelineError::Eof.temp()); + } + BatchValidity::Past => { + if !self.cfg.is_holocene_active(origin.timestamp) { + error!(target: "batch_queue", "BatchValidity::Past is not allowed pre-holocene"); + return Err(PipelineError::InvalidBatchValidity.crit()); + } + + warn!(target: "batch_queue", "[HOLOCENE] Dropping outdated batch with parent: {}", parent.block_info.number); + continue; + } + } + } + self.batches = remaining; + + if let Some(nb) = next_batch { + info!(target: "batch_queue", "Next batch found for timestamp {}", nb.batch.timestamp()); + return Ok(nb.batch); + } + + // If the current epoch is too old compared to the L1 block we are at, + // i.e. if the sequence window expired, we create empty batches for the current epoch + let expiry_epoch = epoch.number + self.cfg.seq_window_size; + let force_empty_batches = + (expiry_epoch == origin.number && empty) || expiry_epoch < origin.number; + let first_of_epoch = epoch.number == parent.l1_origin.number + 1; + + // If the sequencer window did not expire, + // there is still room to receive batches for the current epoch. + // No need to force-create empty batch(es) towards the next epoch yet. + if !force_empty_batches { + return Err(PipelineError::Eof.temp()); + } + + info!( + target: "batch_queue", + "Generating empty batches for epoch: {} | parent: {}", + epoch.number, parent.l1_origin.number + ); + + // The next L1 block is needed to proceed towards the next epoch. + if self.l1_blocks.len() < 2 { + return Err(PipelineError::Eof.temp()); + } + + let next_epoch = self.l1_blocks[1]; + + // Fill with empty L2 blocks of the same epoch until we meet the time of the next L1 origin, + // to preserve that L2 time >= L1 time. If this is the first block of the epoch, always + // generate a batch to ensure that we at least have one batch per epoch. + if next_timestamp < next_epoch.timestamp || first_of_epoch { + info!(target: "batch_queue", "Generating empty batch for epoch: {}", epoch.number); + return Ok(Batch::Single(SingleBatch { + parent_hash: parent.block_info.hash, + epoch_num: epoch.number, + epoch_hash: epoch.hash, + timestamp: next_timestamp, + transactions: Vec::new(), + })); + } + + // At this point we have auto generated every batch for the current epoch + // that we can, so we can advance to the next epoch. + info!( + target: "batch_queue", + "Advancing to next epoch: {}, timestamp: {}, epoch timestamp: {}", + next_epoch.number, next_timestamp, next_epoch.timestamp + ); + self.l1_blocks.remove(0); + Err(PipelineError::Eof.temp()) + } + + /// Adds a batch to the queue. + pub async fn add_batch(&mut self, batch: Batch, parent: L2BlockInfo) -> PipelineResult<()> { + if self.l1_blocks.is_empty() { + error!(target: "batch_queue", "Cannot add batch without an origin"); + panic!("Cannot add batch without an origin"); + } + let origin = self.origin.ok_or(PipelineError::MissingOrigin.crit())?; + let data = BatchWithInclusionBlock { inclusion_block: origin, batch }; + // If we drop the batch, validation logs the drop reason with WARN level. + let validity = + data.check_batch(&self.cfg, &self.l1_blocks, parent, &mut self.fetcher).await; + // Post-Holocene, future batches are dropped due to prevent gaps. + let drop = validity.is_drop() || + (self.cfg.is_holocene_active(origin.timestamp) && validity.is_future()); + if drop { + self.prev.flush(); + return Ok(()); + } else if validity.is_outdated() { + // If the batch is outdated, we drop it without flushing the previous stage. + return Ok(()); + } + self.batches.push(data); + Ok(()) + } +} + +#[async_trait] +impl OriginAdvancer for BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + BF: L2ChainProvider + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl AttributesProvider for BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + BF: L2ChainProvider + Send + Debug, +{ + /// Returns the next valid batch upon the given safe head. + /// Also returns the boolean that indicates if the batch is the last block in the batch. + async fn next_batch(&mut self, parent: L2BlockInfo) -> PipelineResult { + if !self.next_spans.is_empty() { + // There are cached singular batches derived from the span batch. + // Check if the next cached batch matches the given parent block. + if self.next_spans[0].timestamp == parent.block_info.timestamp + self.cfg.block_time { + return self.pop_next_batch(parent).ok_or(PipelineError::BatchQueueEmpty.crit()); + } + // Parent block does not match the next batch. + // Means the previously returned batch is invalid. + // Drop cached batches and find another batch. + warn!( + target: "batch_queue", + "Parent block does not match the next batch. Dropping {} cached batches.", + self.next_spans.len() + ); + self.next_spans.clear(); + } + + // If the epoch is advanced, update the l1 blocks. + // Advancing epoch must be done after the pipeline successfully applies the entire span + // batch to the chain. + // Because the span batch can be reverted during processing the batch, then we must + // preserve existing l1 blocks to verify the epochs of the next candidate batch. + if !self.l1_blocks.is_empty() && parent.l1_origin.number > self.l1_blocks[0].number { + for (i, block) in self.l1_blocks.iter().enumerate() { + if parent.l1_origin.number == block.number { + self.l1_blocks.drain(0..i); + info!(target: "batch_queue", "Advancing epoch"); + break; + } + } + // If the origin of the parent block is not included, we must advance the origin. + } + + // NOTE: The origin is used to determine if it's behind. + // It is the future origin that gets saved into the l1 blocks array. + // We always update the origin of this stage if it's not the same so + // after the update code runs, this is consistent. + let origin_behind = + self.prev.origin().map_or(true, |origin| origin.number < parent.l1_origin.number); + + // Advance the origin if needed. + // The entire pipeline has the same origin. + // Batches prior to the l1 origin of the l2 safe head are not accepted. + if self.origin != self.prev.origin() { + self.origin = self.prev.origin(); + if !origin_behind { + let origin = match self.origin.as_ref().ok_or(PipelineError::MissingOrigin.crit()) { + Ok(o) => o, + Err(e) => { + return Err(e); + } + }; + self.l1_blocks.push(*origin); + } else { + // This is to handle the special case of startup. + // At startup, the batch queue is reset and includes the + // l1 origin. That is the only time where immediately after + // reset is called, the origin behind is false. + self.l1_blocks.clear(); + } + info!(target: "batch_queue", "Advancing batch queue origin: {:?}", self.origin); + } + + // Load more data into the batch queue. + let mut out_of_data = false; + match self.prev.next_batch(parent, &self.l1_blocks).await { + Ok(b) => { + if !origin_behind { + self.add_batch(b, parent).await.ok(); + } else { + warn!(target: "batch_queue", "Dropping batch: Origin is behind"); + } + } + Err(e) => { + if let PipelineErrorKind::Temporary(PipelineError::Eof) = e { + out_of_data = true; + } else { + return Err(e); + } + } + } + + // Skip adding the data unless up to date with the origin, + // but still fully empty the previous stages. + if origin_behind { + if out_of_data { + return Err(PipelineError::Eof.temp()); + } + return Err(PipelineError::NotEnoughData.temp()); + } + + // Attempt to derive more batches. + let batch = match self.derive_next_batch(out_of_data, parent).await { + Ok(b) => b, + Err(e) => match e { + PipelineErrorKind::Temporary(PipelineError::Eof) => { + if out_of_data { + return Err(PipelineError::Eof.temp()); + } + return Err(PipelineError::NotEnoughData.temp()); + } + _ => return Err(e), + }, + }; + + // If the next batch is derived from the span batch, it's the last batch of the span. + // For singular batches, the span batch cache should be empty. + match batch { + Batch::Single(sb) => Ok(sb), + Batch::Span(sb) => { + let batches = match sb.get_singular_batches(&self.l1_blocks, parent).map_err(|e| { + PipelineError::BadEncoding(PipelineEncodingError::SpanBatchError(e)).crit() + }) { + Ok(b) => b, + Err(e) => { + return Err(e); + } + }; + self.next_spans = batches; + let nb = match self + .pop_next_batch(parent) + .ok_or(PipelineError::BatchQueueEmpty.crit()) + { + Ok(b) => b, + Err(e) => { + return Err(e); + } + }; + Ok(nb) + } + } + } + + /// Returns if the previous batch was the last in the span. + fn is_last_in_span(&self) -> bool { + self.next_spans.is_empty() + } +} + +impl OriginProvider for BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl SignalReceiver for BatchQueue +where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + BF: L2ChainProvider + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + s @ Signal::Reset(ResetSignal { l1_origin, .. }) => { + self.prev.signal(s).await?; + self.origin = Some(l1_origin); + self.batches.clear(); + // Include the new origin as an origin to build on. + // This is only for the initialization case. + // During normal resets we will later throw out this block. + self.l1_blocks.clear(); + self.l1_blocks.push(l1_origin); + self.next_spans.clear(); + } + s @ Signal::Activation(_) | s @ Signal::FlushChannel => { + self.prev.signal(s).await?; + self.batches.clear(); + self.next_spans.clear(); + } + s @ Signal::ProvideBlock(_) => { + self.prev.signal(s).await?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{ + CollectingLayer, TestL2ChainProvider, TestNextBatchProvider, TraceStorage, + }; + use alloc::vec; + use alloy_consensus::Header; + use alloy_eips::{BlockNumHash, eip2718::Decodable2718}; + use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, b256}; + use alloy_rlp::{BytesMut, Encodable}; + use kona_genesis::{ChainGenesis, HardForkConfig, MAX_RLP_BYTES_PER_CHANNEL_FJORD}; + use kona_protocol::{BatchReader, L1BlockInfoBedrock, L1BlockInfoTx}; + use op_alloy_consensus::{OpBlock, OpTxEnvelope, OpTxType, TxDeposit}; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + + fn new_batch_reader() -> BatchReader { + let file_contents = + alloc::string::String::from_utf8_lossy(include_bytes!("../../../testdata/batch.hex")); + let file_contents = &(&*file_contents)[..file_contents.len() - 1]; + let data = alloy_primitives::hex::decode(file_contents).unwrap(); + let bytes: alloy_primitives::Bytes = data.into(); + BatchReader::new(bytes, MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize) + } + + #[test] + fn test_pop_next_batch() { + let cfg = Arc::new(RollupConfig::default()); + let mock = TestNextBatchProvider::new(vec![]); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo::default(); + let sb = SingleBatch::default(); + bq.next_spans.push(sb.clone()); + let next = bq.pop_next_batch(parent).unwrap(); + assert_eq!(next, sb); + assert!(bq.next_spans.is_empty()); + } + + #[tokio::test] + async fn test_batch_queue_reset() { + let cfg = Arc::new(RollupConfig::default()); + let mock = TestNextBatchProvider::new(vec![]); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.l1_blocks.push(BlockInfo::default()); + bq.next_spans.push(SingleBatch::default()); + bq.batches.push(BatchWithInclusionBlock { + inclusion_block: BlockInfo::default(), + batch: Batch::Single(SingleBatch::default()), + }); + assert!(!bq.prev.reset); + bq.signal(ResetSignal::default().signal()).await.unwrap(); + assert!(bq.prev.reset); + assert_eq!(bq.origin, Some(BlockInfo::default())); + assert!(bq.batches.is_empty()); + assert_eq!(bq.l1_blocks, vec![BlockInfo::default()]); + assert!(bq.next_spans.is_empty()); + } + + #[tokio::test] + async fn test_batch_queue_flush() { + let cfg = Arc::new(RollupConfig::default()); + let mock = TestNextBatchProvider::new(vec![]); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.l1_blocks.push(BlockInfo::default()); + bq.next_spans.push(SingleBatch::default()); + bq.batches.push(BatchWithInclusionBlock { + inclusion_block: BlockInfo::default(), + batch: Batch::Single(SingleBatch::default()), + }); + bq.signal(Signal::FlushChannel).await.unwrap(); + assert!(bq.prev.flushed); + assert!(bq.batches.is_empty()); + assert!(!bq.l1_blocks.is_empty()); + assert!(bq.next_spans.is_empty()); + } + + #[tokio::test] + async fn test_holocene_add_batch_valid() { + // Construct a future single batch. + let cfg = Arc::new(RollupConfig { + max_sequencer_drift: 700, + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + assert!(cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo { + block_info: BlockInfo { timestamp: 100, ..Default::default() }, + ..Default::default() + }; + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + bq.add_batch(Batch::Single(batch), parent).await.unwrap(); + assert_eq!(bq.batches.len(), 1); + } + + #[tokio::test] + async fn test_holocene_add_batch_future() { + // Construct a future single batch. + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + assert!(cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo::default(); + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + bq.add_batch(Batch::Single(batch), parent).await.unwrap(); + assert!(bq.batches.is_empty()); + } + + #[tokio::test] + async fn test_add_batch_drop() { + // Construct a single batch with BatchValidity::Drop. + let cfg = Arc::new(RollupConfig::default()); + assert!(!cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo { + block_info: BlockInfo { timestamp: 101, ..Default::default() }, + ..Default::default() + }; + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + bq.add_batch(Batch::Single(batch), parent).await.unwrap(); + assert!(bq.batches.is_empty()); + } + + #[tokio::test] + async fn test_add_old_batch_drop_holocene() { + // Construct a single batch with BatchValidity::Past. + let cfg = Arc::new(RollupConfig { + block_time: 2, + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + assert!(cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo { + block_info: BlockInfo { timestamp: 101, ..Default::default() }, + ..Default::default() + }; + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + bq.add_batch(Batch::Single(batch), parent).await.unwrap(); + assert!(bq.batches.is_empty()); + } + + #[tokio::test] + async fn test_derive_next_batch_missing_origin() { + let data = vec![Ok(Batch::Single(SingleBatch::default()))]; + let cfg = Arc::new(RollupConfig::default()); + let mock = TestNextBatchProvider::new(data); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo::default(); + let result = bq.derive_next_batch(false, parent).await.unwrap_err(); + assert_eq!(result, PipelineError::MissingOrigin.crit()); + } + + #[tokio::test] + async fn test_derive_next_batch_invalid_parent() { + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let result = bq.derive_next_batch(false, parent).await.unwrap_err(); + assert_eq!(result, PipelineError::MissingOrigin.crit()); + } + + #[tokio::test] + async fn test_derive_next_batch_no_batches() { + // Setup + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + bq.origin = Some(BlockInfo::default()); + bq.l1_blocks.push(BlockInfo::default()); + + // Assertions + assert!(bq.batches.is_empty()); + assert_eq!(bq.l1_blocks.len(), 1); + let result = bq.derive_next_batch(true, L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + assert!(bq.is_last_in_span()); + } + + #[tokio::test] + async fn test_derive_next_batch_dont_force_empty_batches() { + // Setup + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + bq.origin = Some(BlockInfo::default()); + bq.l1_blocks.push(BlockInfo::default()); + + // Assertions + assert!(bq.batches.is_empty()); + assert_eq!(bq.l1_blocks.len(), 1); + let result = bq.derive_next_batch(false, L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + assert!(bq.is_last_in_span()); + } + + #[tokio::test] + async fn test_derive_next_batch_advances_l1_blocks() { + // Setup + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + bq.origin = Some(BlockInfo::default()); + bq.l1_blocks.push(BlockInfo::default()); + bq.l1_blocks.push(BlockInfo::default()); + + // Assertions + assert!(bq.batches.is_empty()); + assert_eq!(bq.l1_blocks.len(), 2); + let result = bq.derive_next_batch(true, L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + assert!(bq.is_last_in_span()); + assert_eq!(bq.l1_blocks.len(), 1); + } + + #[tokio::test] + async fn test_derive_next_batch_future_batch() { + // Construct a future single batch. + let cfg = Arc::new(RollupConfig::default()); + assert!(!cfg.is_holocene_active(0)); // Asserts holocene is not active. + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo::default(); + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + bq.add_batch(Batch::Single(batch), parent).await.unwrap(); + assert_eq!(bq.batches.len(), 1); + + // Derive next batch + let result = bq.derive_next_batch(true, L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + assert!(bq.is_last_in_span()); + assert_eq!(bq.batches.len(), 1); + } + + #[tokio::test] + async fn test_holocene_derive_next_batch_future() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + // Construct a future single batch. + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + assert!(cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 0, + epoch_hash: B256::default(), + timestamp: 100, + transactions: Vec::new(), + }; + let parent = L2BlockInfo::default(); + + // Setup batch queue deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + + // Configure batch queue + let mut bq = BatchQueue::new(cfg.clone(), mock, fetcher); + bq.origin = Some(BlockInfo::default()); // Set the origin + bq.l1_blocks.push(BlockInfo::default()); // Push the origin into the l1 blocks + bq.l1_blocks.push(BlockInfo::default()); // Push the next origin into the bq + + // Add the batch to the batch queue + let data = BatchWithInclusionBlock { + inclusion_block: parent.block_info, + batch: Batch::Single(batch), + }; + bq.batches.push(data); + assert_eq!(bq.batches.len(), 1); + + // Derive next batch + let result = bq.derive_next_batch(true, L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(result, PipelineError::Eof.temp()); + assert!(bq.is_last_in_span()); + assert!(bq.batches.is_empty()); + + // Validate logs + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let warn_str = "Dropping batch with parent"; + assert!(logs[0].contains(warn_str)); + } + + #[tokio::test] + async fn test_next_batch_cached_single_batch() { + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let sb = SingleBatch::default(); + bq.next_spans.push(sb.clone()); + let next = bq.next_batch(L2BlockInfo::default()).await.unwrap(); + assert_eq!(next, sb); + assert!(bq.next_spans.is_empty()); + } + + #[tokio::test] + async fn test_next_batch_clear_next_spans() { + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig { block_time: 100, ..Default::default() }); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let sb = SingleBatch::default(); + bq.next_spans.push(sb.clone()); + let res = bq.next_batch(L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(res, PipelineError::NotEnoughData.temp()); + assert!(bq.is_last_in_span()); + } + + #[tokio::test] + async fn test_next_batch_not_enough_data() { + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let batch = reader.next_batch(cfg.as_ref()).unwrap(); + let mock = TestNextBatchProvider::new(vec![Ok(batch)]); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let res = bq.next_batch(L2BlockInfo::default()).await.unwrap_err(); + assert_eq!(res, PipelineError::NotEnoughData.temp()); + assert!(bq.is_last_in_span()); + } + + #[tokio::test] + async fn test_next_batch_origin_behind() { + let mut reader = new_batch_reader(); + let cfg = Arc::new(RollupConfig::default()); + let mut batch_vec: Vec> = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + batch_vec.push(Ok(batch)); + } + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo::default()); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let res = bq.next_batch(parent).await.unwrap_err(); + assert_eq!(res, PipelineError::NotEnoughData.temp()); + } + + #[tokio::test] + async fn test_next_batch_missing_origin() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let mut reader = new_batch_reader(); + let payload_block_hash = + b256!("4444444444444444444444444444444444444444444444444444444444444444"); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 100, + max_sequencer_drift: 10000000, + seq_window_size: 10000000, + genesis: ChainGenesis { + l2: BlockNumHash { number: 8, hash: payload_block_hash }, + l1: BlockNumHash { number: 16988980031808077784, ..Default::default() }, + ..Default::default() + }, + batch_inbox_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + ..Default::default() + }); + let mut batch_vec: Vec> = vec![]; + let mut batch_txs: Vec = vec![]; + let mut second_batch_txs: Vec = vec![]; + while let Some(batch) = reader.next_batch(cfg.as_ref()) { + if let Batch::Span(span) = &batch { + batch_txs.extend(span.batches[0].transactions.clone()); + second_batch_txs.extend(span.batches[1].transactions.clone()); + } + batch_vec.push(Ok(batch)); + } + // Insert a deposit transaction in the front of the second batch txs + let expected = L1BlockInfoBedrock { + number: 16988980031808077784, + time: 1697121143, + base_fee: 10419034451, + block_hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"), + sequence_number: 4, + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + l1_fee_overhead: U256::from(0xbc), + l1_fee_scalar: U256::from(0xa6fe0), + }; + let deposit_tx_calldata: Bytes = L1BlockInfoTx::Bedrock(expected).encode_calldata(); + let tx = TxDeposit { + source_hash: B256::left_padding_from(&[0xde, 0xad]), + from: Address::left_padding_from(&[0xbe, 0xef]), + mint: 1, + gas_limit: 2, + to: TxKind::Call(Address::left_padding_from(&[3])), + value: U256::from(4_u64), + input: deposit_tx_calldata, + is_system_transaction: false, + }; + let mut buf = BytesMut::new(); + tx.encode(&mut buf); + let prefixed = [&[OpTxType::Deposit as u8], &buf[..]].concat(); + second_batch_txs.insert(0, Bytes::copy_from_slice(&prefixed)); + let mut mock = TestNextBatchProvider::new(batch_vec); + let origin_check = + b256!("8527cdb6f601acf9b483817abd1da92790c92b19000000000000000000000000"); + mock.origin = Some(BlockInfo { + number: 16988980031808077784, + timestamp: 1639845845, + parent_hash: Default::default(), + hash: origin_check, + }); + let origin = mock.origin; + + let parent_check = + b256!("01ddf682e2f8a6f10c2207e02322897e65317196000000000000000000000000"); + let block_nine = L2BlockInfo { + block_info: BlockInfo { + number: 9, + timestamp: 1639845645, + parent_hash: parent_check, + hash: origin_check, + }, + ..Default::default() + }; + let block_seven = L2BlockInfo { + block_info: BlockInfo { + number: 7, + timestamp: 1639845745, + parent_hash: parent_check, + hash: origin_check, + }, + ..Default::default() + }; + let batch_txs = batch_txs + .into_iter() + .map(|tx| OpTxEnvelope::decode_2718(&mut &tx[..]).unwrap()) + .collect(); + let second_batch_txs = second_batch_txs + .into_iter() + .map(|tx| OpTxEnvelope::decode_2718(&mut &tx[..]).unwrap()) + .collect(); + let block = OpBlock { + header: Header { number: 8, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: batch_txs, + ommers: Vec::new(), + withdrawals: None, + }, + }; + let second = OpBlock { + header: Header { number: 9, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: second_batch_txs, + ommers: Vec::new(), + withdrawals: None, + }, + }; + let fetcher = TestL2ChainProvider { + blocks: vec![block_nine, block_seven], + op_blocks: vec![block, second], + ..Default::default() + }; + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo { + block_info: BlockInfo { + number: 9, + timestamp: 1639845745, + parent_hash: parent_check, + hash: origin_check, + }, + l1_origin: BlockNumHash { number: 16988980031808077784, hash: origin_check }, + ..Default::default() + }; + let res = bq.next_batch(parent).await.unwrap_err(); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 2); + let str = alloc::format!("Advancing batch queue origin: {origin:?}"); + assert!(logs[0].contains(&str)); + assert!(logs[1].contains("Deriving next batch for epoch: 16988980031808077784")); + let warns = trace_store.get_by_level(Level::WARN); + assert_eq!(warns.len(), 1); + assert!(warns[0].contains("span batch has no new blocks after safe head")); + assert_eq!(res, PipelineError::NotEnoughData.temp()); + } + + #[tokio::test] + async fn test_batch_queue_empty_bytes() { + let data = vec![Ok(Batch::Single(SingleBatch::default()))]; + let cfg = Arc::new(RollupConfig::default()); + let mock = TestNextBatchProvider::new(data); + let fetcher = TestL2ChainProvider::default(); + let mut bq = BatchQueue::new(cfg, mock, fetcher); + let parent = L2BlockInfo::default(); + let batch = bq.next_batch(parent).await.unwrap(); + assert_eq!(batch, SingleBatch::default()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/batch/batch_stream.rs b/kona/crates/protocol/derive/src/stages/batch/batch_stream.rs new file mode 100644 index 0000000000000..1039248d39bbf --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/batch/batch_stream.rs @@ -0,0 +1,543 @@ +//! This module contains the `BatchStream` stage. + +use crate::{ + L2ChainProvider, NextBatchProvider, OriginAdvancer, OriginProvider, PipelineError, + PipelineResult, Signal, SignalReceiver, +}; +use alloc::{boxed::Box, collections::VecDeque, sync::Arc}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{ + Batch, BatchValidity, BatchWithInclusionBlock, BlockInfo, L2BlockInfo, SingleBatch, SpanBatch, + SpanBatchError, +}; + +/// Provides [`Batch`]es for the [`BatchStream`] stage. +#[async_trait] +pub trait BatchStreamProvider { + /// Returns the next [`Batch`] in the [`BatchStream`] stage. + async fn next_batch(&mut self) -> PipelineResult; + + /// Drains the recent `Channel` if an invalid span batch is found post-holocene. + fn flush(&mut self); +} + +/// [`BatchStream`] stage in the derivation pipeline. +/// +/// This stage is introduced in the [`Holocene`] hardfork. +/// It slots in between the [`ChannelReader`] and [`BatchQueue`] +/// stages, buffering span batches until they are validated. +/// +/// [`Holocene`]: https://specs.optimism.io/protocol/holocene/overview.html +/// [`ChannelReader`]: crate::stages::ChannelReader +/// [`BatchQueue`]: crate::stages::BatchQueue +#[derive(Debug)] +pub struct BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + /// The previous stage in the derivation pipeline. + pub prev: P, + /// There can only be a single staged span batch. + pub span: Option, + /// A buffer of single batches derived from the [`SpanBatch`]. + pub buffer: VecDeque, + /// A reference to the rollup config, used to check + /// if the [`BatchStream`] stage should be activated. + pub config: Arc, + /// Used to validate the batches. + pub fetcher: BF, +} + +impl BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + /// Create a new [`BatchStream`] stage. + pub const fn new(prev: P, config: Arc, fetcher: BF) -> Self { + Self { prev, span: None, buffer: VecDeque::new(), config, fetcher } + } + + /// Returns if the [`BatchStream`] stage is active based on the + /// origin timestamp and holocene activation timestamp. + pub fn is_active(&self) -> PipelineResult { + let origin = self.prev.origin().ok_or(PipelineError::MissingOrigin.crit())?; + Ok(self.config.is_holocene_active(origin.timestamp)) + } + + /// Gets a [`SingleBatch`] from the in-memory buffer. + pub fn get_single_batch( + &mut self, + parent: L2BlockInfo, + l1_origins: &[BlockInfo], + ) -> Result, SpanBatchError> { + trace!(target: "batch_span", "Attempting to get a SingleBatch from buffer len: {}", self.buffer.len()); + + self.try_hydrate_buffer(parent, l1_origins)?; + Ok(self.buffer.pop_front()) + } + + /// Hydrates the buffer with single batches derived from the span batch, if there is one + /// queued up. + pub fn try_hydrate_buffer( + &mut self, + parent: L2BlockInfo, + l1_origins: &[BlockInfo], + ) -> Result<(), SpanBatchError> { + if let Some(span) = self.span.take() { + self.buffer.extend(span.get_singular_batches(l1_origins, parent)?); + } + #[cfg(feature = "metrics")] + { + let batch_count = self.buffer.len() as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_BUFFER, batch_count); + let batch_size = std::mem::size_of_val(&self.buffer) as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_MEM, batch_size); + } + Ok(()) + } +} + +#[async_trait] +impl NextBatchProvider for BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + BF: L2ChainProvider + Send + Debug, +{ + fn flush(&mut self) { + if self.is_active().unwrap_or(false) { + self.prev.flush(); + self.span = None; + self.buffer.clear(); + } + } + + fn span_buffer_size(&self) -> usize { + self.buffer.len() + } + + async fn next_batch( + &mut self, + parent: L2BlockInfo, + l1_origins: &[BlockInfo], + ) -> PipelineResult { + // If the stage is not active, "pass" the next batch + // through this stage to the BatchQueue stage. + if !self.is_active()? { + trace!(target: "batch_span", "BatchStream stage is inactive, pass-through."); + return self.prev.next_batch().await; + } + + // If the buffer is empty, attempt to pull a batch from the previous stage. + if self.buffer.is_empty() { + // Safety: bubble up any errors from the batch reader. + let batch_with_inclusion = BatchWithInclusionBlock::new( + self.origin().ok_or(PipelineError::MissingOrigin.crit())?, + self.prev.next_batch().await?, + ); + + // If the next batch is a singular batch, it is immediately + // forwarded to the `BatchQueue` stage. Otherwise, we buffer + // the span batch in this stage if it passes the validity checks. + match batch_with_inclusion.batch { + Batch::Single(b) => return Ok(Batch::Single(b)), + Batch::Span(b) => { + #[cfg(feature = "metrics")] + let start = std::time::Instant::now(); + let (validity, _) = b + .check_batch_prefix( + self.config.as_ref(), + l1_origins, + parent, + &batch_with_inclusion.inclusion_block, + &mut self.fetcher, + ) + .await; + kona_macros::record!( + histogram, + crate::metrics::Metrics::PIPELINE_CHECK_BATCH_PREFIX, + start.elapsed().as_secs_f64() + ); + + kona_macros::inc!( + gauge, + crate::metrics::Metrics::PIPELINE_BATCH_VALIDITY, + "validity" => validity.to_string(), + ); + + match validity { + BatchValidity::Accept => self.span = Some(b), + BatchValidity::Drop => { + // Flush the stage. + self.flush(); + + return Err(PipelineError::NotEnoughData.temp()); + } + BatchValidity::Past => { + if !self.is_active()? { + error!(target: "batch_stream", "BatchValidity::Past is not allowed pre-holocene"); + return Err(PipelineError::InvalidBatchValidity.crit()); + } + + return Err(PipelineError::NotEnoughData.temp()); + } + BatchValidity::Undecided | BatchValidity::Future => { + return Err(PipelineError::NotEnoughData.temp()); + } + } + } + } + } + + // Attempt to pull a SingleBatch out of the SpanBatch. + match self.get_single_batch(parent, l1_origins) { + Ok(Some(single_batch)) => Ok(Batch::Single(single_batch)), + Ok(None) => Err(PipelineError::NotEnoughData.temp()), + Err(e) => { + warn!(target: "batch_span", "Extracting singular batches from span batch failed: {}", e); + // If singular batch extraction fails, it should be handled the same as a + // dropped batch during span batch prefix checks. + self.flush(); + Err(PipelineError::NotEnoughData.temp()) + } + } + } +} + +#[async_trait] +impl OriginAdvancer for BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, + BF: L2ChainProvider + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +impl OriginProvider for BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, + BF: L2ChainProvider + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl SignalReceiver for BatchStream +where + P: BatchStreamProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug + Send, + BF: L2ChainProvider + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.prev.signal(signal).await?; + self.buffer.clear(); + self.span.take(); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + test_utils::{CollectingLayer, TestBatchStreamProvider, TestL2ChainProvider, TraceStorage}, + types::ResetSignal, + }; + use alloc::vec; + use alloy_consensus::{BlockBody, Header}; + use alloy_eips::{BlockNumHash, NumHash}; + use alloy_primitives::{FixedBytes, b256}; + use kona_genesis::{ChainGenesis, HardForkConfig}; + use kona_protocol::{SingleBatch, SpanBatchElement}; + use op_alloy_consensus::OpBlock; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + #[tokio::test] + async fn test_batch_stream_flush() { + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(vec![]); + let mut stream = BatchStream::new(prev, config, TestL2ChainProvider::default()); + stream.buffer.push_back(SingleBatch::default()); + stream.span = Some(SpanBatch::default()); + assert!(!stream.buffer.is_empty()); + assert!(stream.span.is_some()); + stream.flush(); + assert!(stream.buffer.is_empty()); + assert!(stream.span.is_none()); + } + + #[tokio::test] + async fn test_batch_stream_reset() { + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(vec![]); + let mut stream = BatchStream::new(prev, config.clone(), TestL2ChainProvider::default()); + stream.buffer.push_back(SingleBatch::default()); + stream.span = Some(SpanBatch::default()); + assert!(!stream.prev.reset); + stream.signal(ResetSignal::default().signal()).await.unwrap(); + assert!(stream.prev.reset); + assert!(stream.buffer.is_empty()); + assert!(stream.span.is_none()); + } + + #[tokio::test] + async fn test_batch_stream_flush_channel() { + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(vec![]); + let mut stream = BatchStream::new(prev, config.clone(), TestL2ChainProvider::default()); + stream.buffer.push_back(SingleBatch::default()); + stream.span = Some(SpanBatch::default()); + assert!(!stream.prev.flushed); + stream.signal(Signal::FlushChannel).await.unwrap(); + assert!(stream.prev.flushed); + assert!(stream.buffer.is_empty()); + assert!(stream.span.is_none()); + } + + #[tokio::test] + async fn test_batch_stream_inactive() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let data = vec![Ok(Batch::Single(SingleBatch::default()))]; + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(100), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(data); + let mut stream = BatchStream::new(prev, config.clone(), TestL2ChainProvider::default()); + + // The stage should not be active. + assert!(!stream.is_active().unwrap()); + + // The next batch should be passed through to the [BatchQueue] stage. + let batch = stream.next_batch(Default::default(), &[]).await.unwrap(); + assert_eq!(batch, Batch::Single(SingleBatch::default())); + + let logs = trace_store.get_by_level(tracing::Level::TRACE); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("BatchStream stage is inactive, pass-through.")); + } + + #[tokio::test] + async fn test_span_buffer() { + let mock_batch = SpanBatch { + batches: vec![ + SpanBatchElement { epoch_num: 1, timestamp: 2, ..Default::default() }, + SpanBatchElement { epoch_num: 1, timestamp: 4, ..Default::default() }, + ], + ..Default::default() + }; + let mock_origins = [BlockInfo { number: 1, timestamp: 12, ..Default::default() }]; + + let data = vec![Ok(Batch::Span(mock_batch.clone()))]; + let config = Arc::new(RollupConfig { + block_time: 2, + hardforks: HardForkConfig { + delta_time: Some(0), + holocene_time: Some(0), + ..Default::default() + }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(data); + let provider = TestL2ChainProvider::default(); + let mut stream = BatchStream::new(prev, config.clone(), provider); + + // The stage should be active. + assert!(stream.is_active().unwrap()); + + // The next batches should be single batches derived from the span batch. + let batch = stream.next_batch(Default::default(), &mock_origins).await.unwrap(); + if let Batch::Single(single) = batch { + assert_eq!(single.epoch_num, 1); + assert_eq!(single.timestamp, 2); + } else { + panic!("Wrong batch type"); + } + + let batch = stream.next_batch(Default::default(), &mock_origins).await.unwrap(); + if let Batch::Single(single) = batch { + assert_eq!(single.epoch_num, 1); + assert_eq!(single.timestamp, 4); + } else { + panic!("Wrong batch type"); + } + + let err = stream.next_batch(Default::default(), &mock_origins).await.unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + assert_eq!(stream.span_buffer_size(), 0); + assert!(stream.span.is_none()); + + // Add more data into the provider, see if the buffer is re-hydrated. + stream.prev.batches.push(Ok(Batch::Span(mock_batch.clone()))); + + // The next batches should be single batches derived from the span batch. + let batch = stream.next_batch(Default::default(), &mock_origins).await.unwrap(); + if let Batch::Single(single) = batch { + assert_eq!(single.epoch_num, 1); + assert_eq!(single.timestamp, 2); + } else { + panic!("Wrong batch type"); + } + + let batch = stream.next_batch(Default::default(), &mock_origins).await.unwrap(); + if let Batch::Single(single) = batch { + assert_eq!(single.epoch_num, 1); + assert_eq!(single.timestamp, 4); + } else { + panic!("Wrong batch type"); + } + + let err = stream.next_batch(Default::default(), &mock_origins).await.unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + assert_eq!(stream.span_buffer_size(), 0); + assert!(stream.span.is_none()); + } + + #[tokio::test] + async fn test_span_batch_extraction_error_flushes_stage() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let config = Arc::new(RollupConfig { + seq_window_size: 100, + block_time: 10, + hardforks: HardForkConfig { + delta_time: Some(0), + holocene_time: Some(0), + ..Default::default() + }, + genesis: ChainGenesis { + l2: BlockNumHash { number: 40, hash: parent_hash }, + ..Default::default() + }, + ..Default::default() + }); + + let l1_block = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: l1_block.id(), + ..Default::default() + }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 0, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let op_block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: BlockBody { transactions: vec![], ommers: vec![], withdrawals: None }, + }; + + let span_batch = SpanBatch { + batches: vec![ + SpanBatchElement { epoch_num: 9, timestamp: 10, ..Default::default() }, + SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }, + SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }, + ], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + + let mut prev = TestBatchStreamProvider::new(vec![Ok(Batch::Span(span_batch))]); + prev.origin = Some(l1_block); + + let mut provider = TestL2ChainProvider::default(); + provider.blocks.push(l2_parent); + provider.op_blocks.push(op_block); + + let mut stream = BatchStream::new(prev, config, provider); + let err = stream.next_batch(l2_safe_head, &l1_blocks).await.unwrap_err(); + + assert_eq!(err, PipelineError::NotEnoughData.temp()); + assert!(stream.span.is_none()); + assert_eq!(stream.span_buffer_size(), 0); + + let logs = trace_store.get_by_level(tracing::Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("Extracting singular batches from span batch failed: Future batch L1 origin before safe head")); + } + + #[tokio::test] + async fn test_single_batch_pass_through() { + let data = vec![Ok(Batch::Single(SingleBatch::default()))]; + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(data); + let mut stream = BatchStream::new(prev, config.clone(), TestL2ChainProvider::default()); + + // The stage should be active. + assert!(stream.is_active().unwrap()); + + // The next batch should be passed through to the [BatchQueue] stage. + let batch = stream.next_batch(Default::default(), &[]).await.unwrap(); + assert!(matches!(batch, Batch::Single(_))); + assert_eq!(stream.span_buffer_size(), 0); + assert!(stream.span.is_none()); + } + + #[tokio::test] + async fn test_past_span_batch() { + let mock_batch = SpanBatch { + batches: vec![ + SpanBatchElement { epoch_num: 1, timestamp: 2, ..Default::default() }, + SpanBatchElement { epoch_num: 1, timestamp: 4, ..Default::default() }, + ], + ..Default::default() + }; + let mock_origins = [BlockInfo { number: 1, timestamp: 12, ..Default::default() }]; + let data = vec![Ok(Batch::Span(mock_batch))]; + + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let prev = TestBatchStreamProvider::new(data); + let mut stream = BatchStream::new(prev, config.clone(), TestL2ChainProvider::default()); + + // The stage should be active. + assert!(stream.is_active().unwrap()); + + let parent = L2BlockInfo { + block_info: BlockInfo { number: 10, timestamp: 100, ..Default::default() }, + l1_origin: NumHash::default(), + seq_num: 0, + }; + + // `next_batch` should return an error if the span batch is in the past. + let err = stream.next_batch(parent, &mock_origins).await.unwrap_err(); + assert_eq!(err, PipelineError::NotEnoughData.temp()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/batch/batch_validator.rs b/kona/crates/protocol/derive/src/stages/batch/batch_validator.rs new file mode 100644 index 0000000000000..31ca05820fe59 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/batch/batch_validator.rs @@ -0,0 +1,631 @@ +//! Contains the [BatchValidator] stage. + +use super::NextBatchProvider; +use crate::{ + errors::{PipelineError, PipelineErrorKind, ResetError}, + traits::{AttributesProvider, OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, ResetSignal, Signal}, +}; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{Batch, BatchValidity, BlockInfo, L2BlockInfo, SingleBatch}; + +/// The [`BatchValidator`] stage is responsible for validating the [`SingleBatch`]es from +/// the [`BatchStream`] [`AttributesQueue`]'s consumption. +/// +/// [`BatchStream`]: crate::stages::BatchStream +/// [`AttributesQueue`]: crate::stages::attributes_queue::AttributesQueue +#[derive(Debug)] +pub struct BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The rollup configuration. + pub cfg: Arc, + /// The previous stage of the derivation pipeline. + pub prev: P, + /// The L1 origin of the batch sequencer. + pub origin: Option, + /// A consecutive, time-centric window of L1 Blocks. + /// Every L1 origin of unsafe L2 Blocks must be included in this list. + /// If every L2 Block corresponding to a single L1 Block becomes safe, + /// the block is popped from this list. + /// If new L2 Block's L1 origin is not included in this list, fetch and + /// push it to the list. + pub l1_blocks: Vec, +} + +impl

BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Create a new [`BatchValidator`] stage. + pub const fn new(cfg: Arc, prev: P) -> Self { + Self { cfg, prev, origin: None, l1_blocks: Vec::new() } + } + + /// Returns `true` if the pipeline origin is behind the parent origin. + /// + /// ## Takes + /// - `parent`: The parent block of the current batch. + /// + /// ## Returns + /// - `true` if the origin is behind the parent origin. + fn origin_behind(&self, parent: &L2BlockInfo) -> bool { + self.prev.origin().is_none_or(|origin| origin.number < parent.l1_origin.number) + } + + /// Updates the [`BatchValidator`]'s view of the L1 origin blocks. + /// + /// ## Takes + /// - `parent`: The parent block of the current batch. + /// + /// ## Returns + /// - `Ok(())` if the update was successful. + /// - `Err(PipelineError)` if the update failed. + pub(crate) fn update_origins(&mut self, parent: &L2BlockInfo) -> PipelineResult<()> { + // NOTE: The origin is used to determine if it's behind. + // It is the future origin that gets saved into the l1 blocks array. + // We always update the origin of this stage if it's not the same so + // after the update code runs, this is consistent. + let origin_behind = self.origin_behind(parent); + + // Advance the origin if needed. + // The entire pipeline has the same origin. + // Batches prior to the l1 origin of the l2 safe head are not accepted. + if self.origin != self.prev.origin() { + self.origin = self.prev.origin(); + if !origin_behind { + let origin = self.origin.as_ref().ok_or(PipelineError::MissingOrigin.crit())?; + self.l1_blocks.push(*origin); + } else { + // This is to handle the special case of startup. + // At startup, the batch validator is reset and includes the + // l1 origin. That is the only time when immediately after + // reset is called, the origin behind is false. + self.l1_blocks.clear(); + } + debug!( + target: "batch_validator", + "Advancing batch validator origin to L1 block #{}.{}", + self.origin.map(|b| b.number).unwrap_or_default(), + if origin_behind { " (origin behind)" } else { Default::default() } + ); + } + + // If the epoch is advanced, update the l1 blocks. + // Advancing epoch must be done after the pipeline successfully applies the entire span + // batch to the chain. + // Because the span batch can be reverted during processing the batch, then we must + // preserve existing l1 blocks to verify the epochs of the next candidate batch. + if !self.l1_blocks.is_empty() && parent.l1_origin.number > self.l1_blocks[0].number { + for (i, block) in self.l1_blocks.iter().enumerate() { + if parent.l1_origin.number == block.number { + self.l1_blocks.drain(0..i); + debug!(target: "batch_validator", "Advancing internal L1 epoch"); + break; + } + } + // If the origin of the parent block is not included, we must advance the origin. + } + + #[cfg(feature = "metrics")] + { + if let Some(origin) = self.l1_blocks.first() { + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_L1_BLOCKS_START, + origin.number as f64 + ); + let last = self.l1_blocks.last().unwrap_or(origin); + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_L1_BLOCKS_END, + last.number as f64 + ); + } + } + + Ok(()) + } + + /// Attempts to derive an empty batch, if the sequencing window is expired. + /// + /// ## Takes + /// - `parent`: The parent block of the current batch. + /// + /// ## Returns + /// - `Ok(SingleBatch)` if an empty batch was derived. + /// - `Err(PipelineError)` if an empty batch could not be derived. + pub(crate) fn try_derive_empty_batch( + &mut self, + parent: &L2BlockInfo, + ) -> PipelineResult { + let epoch = self.l1_blocks[0]; + + // If the current epoch is too old compared to the L1 block we are at, + // i.e. if the sequence window expired, we create empty batches for the current epoch + let stage_origin = self.origin.ok_or(PipelineError::MissingOrigin.crit())?; + let expiry_epoch = epoch.number + self.cfg.seq_window_size; + let force_empty_batches = expiry_epoch <= stage_origin.number; + let first_of_epoch = epoch.number == parent.l1_origin.number + 1; + let next_timestamp = parent.block_info.timestamp + self.cfg.block_time; + + // If the sequencer window did not expire, + // there is still room to receive batches for the current epoch. + // No need to force-create empty batch(es) towards the next epoch yet. + if !force_empty_batches { + return Err(PipelineError::Eof.temp()); + } + + // The next L1 block is needed to proceed towards the next epoch. + if self.l1_blocks.len() < 2 { + return Err(PipelineError::Eof.temp()); + } + + let next_epoch = self.l1_blocks[1]; + + // Fill with empty L2 blocks of the same epoch until we meet the time of the next L1 origin, + // to preserve that L2 time >= L1 time. If this is the first block of the epoch, always + // generate a batch to ensure that we at least have one batch per epoch. + if next_timestamp < next_epoch.timestamp || first_of_epoch { + info!(target: "batch_validator", "Generating empty batch for epoch #{}", epoch.number); + return Ok(SingleBatch { + parent_hash: parent.block_info.hash, + epoch_num: epoch.number, + epoch_hash: epoch.hash, + timestamp: next_timestamp, + transactions: Vec::new(), + }); + } + + // At this point we have auto generated every batch for the current epoch + // that we can, so we can advance to the next epoch. + debug!( + target: "batch_validator", + "Advancing batch validator epoch: {}, timestamp: {}, epoch timestamp: {}", + next_epoch.number, next_timestamp, next_epoch.timestamp + ); + self.l1_blocks.remove(0); + Err(PipelineError::Eof.temp()) + } +} + +#[async_trait] +impl

AttributesProvider for BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn next_batch(&mut self, parent: L2BlockInfo) -> PipelineResult { + // Update the L1 origin blocks within the stage. + self.update_origins(&parent)?; + + // If the origin is behind, we must drain previous stages to catch up. + let stage_origin = self.origin.ok_or(PipelineError::MissingOrigin.crit())?; + if self.origin_behind(&parent) || parent.l1_origin.number == stage_origin.number { + self.prev.next_batch(parent, self.l1_blocks.as_ref()).await?; + return Err(PipelineError::NotEnoughData.temp()); + } + + // At least the L1 origin of the safe block and the L1 origin of the following block must + // be included in the l1 blocks. + if self.l1_blocks.len() < 2 { + return Err(PipelineError::MissingOrigin.crit()); + } + + // Note: epoch origin can now be one block ahead of the L2 Safe Head + // This is in the case where we auto generate all batches in an epoch & advance the epoch + // but don't advance the L2 Safe Head's epoch + let epoch = self.l1_blocks[0]; + if parent.l1_origin != epoch.id() && parent.l1_origin.number != epoch.number - 1 { + return Err(PipelineErrorKind::Reset(ResetError::L1OriginMismatch( + parent.l1_origin.number, + epoch.number - 1, + ))); + } + + // Pull the next batch from the previous stage. + let next_batch = match self.prev.next_batch(parent, self.l1_blocks.as_ref()).await { + Ok(batch) => batch, + Err(PipelineErrorKind::Temporary(PipelineError::Eof)) => { + return self.try_derive_empty_batch(&parent); + } + Err(e) => { + return Err(e); + } + }; + + // The batch must be a single batch - this stage does not support span batches. + let Batch::Single(mut next_batch) = next_batch else { + error!( + target: "batch_validator", + "BatchValidator received a batch that is not a SingleBatch" + ); + return Err(PipelineError::InvalidBatchType.crit()); + }; + next_batch.parent_hash = parent.block_info.hash; + + // Check the validity of the single batch before forwarding it. + match next_batch.check_batch( + self.cfg.as_ref(), + self.l1_blocks.as_ref(), + parent, + &stage_origin, + ) { + BatchValidity::Accept => { + info!(target: "batch_validator", "Found next batch (epoch #{})", next_batch.epoch_num); + Ok(next_batch) + } + BatchValidity::Past => { + warn!(target: "batch_validator", "Dropping old batch"); + Err(PipelineError::NotEnoughData.temp()) + } + BatchValidity::Drop => { + warn!(target: "batch_validator", "Invalid singular batch, flushing current channel."); + self.prev.flush(); + Err(PipelineError::NotEnoughData.temp()) + } + BatchValidity::Undecided => Err(PipelineError::NotEnoughData.temp()), + BatchValidity::Future => { + error!(target: "batch_validator", "Future batch detected in BatchValidator."); + Err(PipelineError::InvalidBatchValidity.crit()) + } + } + } + + fn is_last_in_span(&self) -> bool { + self.prev.span_buffer_size() == 0 + } +} + +impl

OriginProvider for BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl

OriginAdvancer for BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl

SignalReceiver for BatchValidator

+where + P: NextBatchProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + s @ Signal::Reset(ResetSignal { l1_origin, .. }) => { + self.prev.signal(s).await?; + self.origin = Some(l1_origin); + // Include the new origin as an origin to build on. + // This is only for the initialization case. + // During normal resets we will later throw out this block. + self.l1_blocks.clear(); + self.l1_blocks.push(l1_origin); + } + s @ Signal::Activation(_) | s @ Signal::FlushChannel | s @ Signal::ProvideBlock(_) => { + self.prev.signal(s).await?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::{ + AttributesProvider, BatchValidator, NextBatchProvider, OriginAdvancer, PipelineError, + PipelineErrorKind, PipelineResult, ResetError, ResetSignal, Signal, SignalReceiver, + test_utils::{CollectingLayer, TestNextBatchProvider, TraceStorage}, + }; + use alloc::{sync::Arc, vec, vec::Vec}; + use alloy_eips::{BlockNumHash, NumHash}; + use alloy_primitives::B256; + use kona_genesis::{HardForkConfig, RollupConfig}; + use kona_protocol::{Batch, BlockInfo, L2BlockInfo, SingleBatch, SpanBatch}; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + + #[tokio::test] + async fn test_batch_validator_origin_behind_eof() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo::default()); + let mut bv = BatchValidator::new(cfg, mock); + bv.origin = Some(BlockInfo { number: 1, ..Default::default() }); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 5, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.next_batch(mock_parent).await.unwrap_err(), PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_batch_validator_origin_behind_startup() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo::default()); + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + l2_safe_head: L2BlockInfo::new( + BlockInfo::default(), + NumHash::new(1, Default::default()), + 0, + ), + system_config: None, + })) + .await + .unwrap(); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 2, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.l1_blocks.len(), 1); + bv.update_origins(&mock_parent).unwrap(); + assert_eq!(bv.l1_blocks.len(), 0); + } + + #[tokio::test] + async fn test_batch_validator_origin_behind_advance() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo { number: 2, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + l2_safe_head: L2BlockInfo::new( + BlockInfo::default(), + NumHash::new(1, Default::default()), + 0, + ), + system_config: None, + })) + .await + .unwrap(); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 1, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.l1_blocks.len(), 1); + bv.update_origins(&mock_parent).unwrap(); + assert_eq!(bv.l1_blocks.len(), 2); + } + + #[tokio::test] + async fn test_batch_validator_advance_epoch() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo { number: 2, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + l2_safe_head: L2BlockInfo::new( + BlockInfo::default(), + NumHash::new(1, Default::default()), + 0, + ), + system_config: None, + })) + .await + .unwrap(); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 2, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.l1_blocks.len(), 1); + assert_eq!(bv.l1_blocks[0].number, 1); + assert_eq!(bv.next_batch(mock_parent).await.unwrap_err(), PipelineError::Eof.temp()); + assert_eq!(bv.l1_blocks.len(), 1); + assert_eq!(bv.l1_blocks[0].number, 2); + } + + #[tokio::test] + async fn test_batch_validator_origin_behind_drain_prev() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new( + (0..5).map(|_| Ok(Batch::Single(SingleBatch::default()))).collect(), + ); + mock.origin = Some(BlockInfo::default()); + let mut bv = BatchValidator::new(cfg, mock); + bv.origin = Some(BlockInfo::default()); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 5, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.prev.span_buffer_size(), 5); + for i in 0..5 { + assert_eq!( + bv.next_batch(mock_parent).await.unwrap_err(), + PipelineError::NotEnoughData.temp() + ); + assert_eq!(bv.prev.span_buffer_size(), 4 - i); + } + assert_eq!(bv.next_batch(mock_parent).await.unwrap_err(), PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_batch_validator_l1_origin_mismatch() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![Ok(Batch::Single(SingleBatch::default()))]); + mock.origin = Some(BlockInfo { number: 1, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + bv.origin = Some(BlockInfo::default()); + bv.l1_blocks.push(BlockInfo::default()); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 0, hash: [0xFF; 32].into() }, + ..Default::default() + }; + + assert!(matches!( + bv.next_batch(mock_parent).await.unwrap_err(), + PipelineErrorKind::Reset(ResetError::L1OriginMismatch(_, _)) + )); + } + + #[tokio::test] + async fn test_batch_validator_received_span_batch() { + let cfg = Arc::new(RollupConfig::default()); + let mut mock = TestNextBatchProvider::new(vec![Ok(Batch::Span(SpanBatch::default()))]); + mock.origin = Some(BlockInfo { number: 1, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + bv.origin = Some(BlockInfo::default()); + bv.l1_blocks.push(BlockInfo::default()); + + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 0, ..Default::default() }, + ..Default::default() + }; + + assert_eq!( + bv.next_batch(mock_parent).await.unwrap_err(), + PipelineError::InvalidBatchType.crit() + ); + assert_eq!(bv.next_batch(mock_parent).await.unwrap_err(), PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_batch_validator_next_batch_valid() { + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + block_time: 2, + max_sequencer_drift: 700, + ..Default::default() + }); + assert!(cfg.is_holocene_active(0)); + let batch = SingleBatch { + parent_hash: B256::default(), + epoch_num: 2, + epoch_hash: B256::default(), + timestamp: 4, + transactions: Vec::new(), + }; + let parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 0, ..Default::default() }, + block_info: BlockInfo { timestamp: 2, ..Default::default() }, + ..Default::default() + }; + + // Setup batch validator deps + let batch_vec = vec![PipelineResult::Ok(Batch::Single(batch.clone()))]; + let mut mock = TestNextBatchProvider::new(batch_vec); + mock.origin = Some(BlockInfo { number: 1, ..Default::default() }); + + // Configure batch validator + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + ..Default::default() + })) + .await + .unwrap(); + bv.l1_blocks.push(BlockInfo { number: 1, ..Default::default() }); + + // Grab the next batch. + let produced_batch = bv.next_batch(parent).await.unwrap(); + assert_eq!(batch, produced_batch); + } + + #[tokio::test] + async fn test_batch_validator_next_batch_sequence_window_expired() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let cfg = Arc::new(RollupConfig { seq_window_size: 5, ..Default::default() }); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo { number: 1, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + ..Default::default() + })) + .await + .unwrap(); + + // Advance the origin of the previous stage to block #6. + for _ in 0..6 { + bv.advance_origin().await.unwrap(); + } + + // The sequence window is expired, so we should generate an empty batch. + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 0, ..Default::default() }, + ..Default::default() + }; + assert!(bv.next_batch(mock_parent).await.unwrap().transactions.is_empty()); + + let trace_lock = trace_store.lock(); + assert_eq!(trace_lock.iter().filter(|(l, _)| matches!(l, &Level::DEBUG)).count(), 1); + assert_eq!(trace_lock.iter().filter(|(l, _)| matches!(l, &Level::INFO)).count(), 1); + assert!(trace_lock[0].1.contains("Advancing batch validator origin")); + assert!(trace_lock[1].1.contains("Generating empty batch for epoch")); + } + + #[tokio::test] + async fn test_batch_validator_next_batch_sequence_window_expired_advance_epoch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let cfg = Arc::new(RollupConfig { seq_window_size: 5, ..Default::default() }); + let mut mock = TestNextBatchProvider::new(vec![]); + mock.origin = Some(BlockInfo { number: 1, ..Default::default() }); + let mut bv = BatchValidator::new(cfg, mock); + + // Reset the pipeline to add the L1 origin to the stage. + bv.signal(Signal::Reset(ResetSignal { + l1_origin: BlockInfo { number: 1, ..Default::default() }, + ..Default::default() + })) + .await + .unwrap(); + + // Advance the origin of the previous stage to block #6. + for _ in 0..6 { + bv.advance_origin().await.unwrap(); + } + + // The sequence window is expired, so we should generate an empty batch. + let mock_parent = L2BlockInfo { + l1_origin: BlockNumHash { number: 1, ..Default::default() }, + ..Default::default() + }; + assert_eq!(bv.next_batch(mock_parent).await.unwrap_err(), PipelineError::Eof.temp()); + + let trace_lock = trace_store.lock(); + assert_eq!(trace_lock.iter().filter(|(l, _)| matches!(l, &Level::DEBUG)).count(), 2); + assert!(trace_lock[0].1.contains("Advancing batch validator origin")); + assert!(trace_lock[1].1.contains("Advancing batch validator epoch")); + } +} diff --git a/kona/crates/protocol/derive/src/stages/batch/mod.rs b/kona/crates/protocol/derive/src/stages/batch/mod.rs new file mode 100644 index 0000000000000..dbc052650945e --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/batch/mod.rs @@ -0,0 +1,58 @@ +//! Contains stages pertaining to the processing of [Batch]es. +//! +//! Sitting after the [ChannelReader] stage, the [`BatchStream`] and [`BatchProvider`] stages are +//! responsible for validating and ordering the [Batch]es. The [`BatchStream`] stage is +//! responsible for streaming [SingleBatch]es from [SpanBatch]es, while the [`BatchProvider`] +//! stage is responsible for ordering and validating the [Batch]es for the [AttributesQueue] +//! stage. +//! +//! [Batch]: kona_protocol::Batch +//! [SingleBatch]: kona_protocol::SingleBatch +//! [SpanBatch]: kona_protocol::SpanBatch +//! [ChannelReader]: crate::stages::channel::ChannelReader +//! [AttributesQueue]: crate::stages::attributes_queue::AttributesQueue + +use crate::types::PipelineResult; +use alloc::boxed::Box; +use async_trait::async_trait; +use kona_protocol::{Batch, BlockInfo, L2BlockInfo}; + +mod batch_stream; +pub use batch_stream::{BatchStream, BatchStreamProvider}; + +mod batch_queue; +pub use batch_queue::BatchQueue; + +mod batch_validator; +pub use batch_validator::BatchValidator; + +mod batch_provider; +pub use batch_provider::BatchProvider; + +/// Provides [`Batch`]es for the [`BatchQueue`] and [`BatchValidator`] stages. +#[async_trait] +pub trait NextBatchProvider { + /// Returns the next [`Batch`] in the [`ChannelReader`] stage, if the stage is not complete. + /// This function can only be called once while the stage is in progress, and will return + /// [`None`] on subsequent calls unless the stage is reset or complete. If the stage is + /// complete and the batch has been consumed, an [PipelineError::Eof] error is returned. + /// + /// [`ChannelReader`]: crate::stages::ChannelReader + /// [PipelineError::Eof]: crate::errors::PipelineError::Eof + async fn next_batch( + &mut self, + parent: L2BlockInfo, + l1_origins: &[BlockInfo], + ) -> PipelineResult; + + /// Returns the number of [`SingleBatch`]es that are currently buffered in the [`BatchStream`] + /// from a [`SpanBatch`]. + /// + /// [`SpanBatch`]: kona_protocol::SpanBatch + /// [`SingleBatch`]: kona_protocol::SingleBatch + fn span_buffer_size(&self) -> usize; + + /// Allows the stage to flush the buffer in the [crate::stages::BatchStream] + /// if an invalid single batch is found. Pre-holocene hardfork, this will be a no-op. + fn flush(&mut self); +} diff --git a/kona/crates/protocol/derive/src/stages/channel/channel_assembler.rs b/kona/crates/protocol/derive/src/stages/channel/channel_assembler.rs new file mode 100644 index 0000000000000..248149bd9db4b --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/channel/channel_assembler.rs @@ -0,0 +1,393 @@ +//! This module contains the [ChannelAssembler] stage. + +use super::{ChannelReaderProvider, NextFrameProvider}; +use crate::{ + errors::PipelineError, + traits::{OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::{Bytes, hex}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::{ + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK, MAX_RLP_BYTES_PER_CHANNEL_FJORD, RollupConfig, +}; +use kona_protocol::{BlockInfo, Channel}; + +/// The [`ChannelAssembler`] stage is responsible for assembling the [`Frame`]s from the +/// [`FrameQueue`] stage into a raw compressed [`Channel`]. +/// +/// [`Frame`]: kona_protocol::Frame +/// [`FrameQueue`]: crate::stages::FrameQueue +/// [`Channel`]: kona_protocol::Channel +#[derive(Debug)] +pub struct ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The rollup configuration. + pub cfg: Arc, + /// The previous stage of the derivation pipeline. + pub prev: P, + /// The current [`Channel`] being assembled. + pub channel: Option, +} + +impl

ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Creates a new [`ChannelAssembler`] stage with the given configuration and previous stage. + pub const fn new(cfg: Arc, prev: P) -> Self { + Self { cfg, prev, channel: None } + } + + /// Returns whether or not the channel currently being assembled has timed out. + pub fn is_timed_out(&self) -> PipelineResult { + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + let is_timed_out = self + .channel + .as_ref() + .map(|c| { + c.open_block_number() + self.cfg.channel_timeout(origin.timestamp) < origin.number + }) + .unwrap_or_default(); + + Ok(is_timed_out) + } +} + +#[async_trait] +impl

ChannelReaderProvider for ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn next_data(&mut self) -> PipelineResult> { + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + + // Time out the channel if it has timed out. + if let Some(channel) = self.channel.as_ref() { + if self.is_timed_out()? { + warn!( + target: "channel_assembler", + "Channel (ID: {}) timed out at L1 origin #{}, open block #{}. Discarding channel.", + hex::encode(channel.id()), + origin.number, + channel.open_block_number() + ); + self.channel = None; + } + } + + // Grab the next frame from the previous stage. + let next_frame = self.prev.next_frame().await?; + + // Start a new channel if the frame number is 0. + if next_frame.number == 0 { + info!( + target: "channel_assembler", + "Starting new channel (ID: {}) at L1 origin #{}", + hex::encode(next_frame.id), + origin.number + ); + self.channel = Some(Channel::new(next_frame.id, origin)); + } + + let count = if self.channel.is_some() { 1 } else { 0 }; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_CHANNEL_BUFFER, count); + + if let Some(channel) = self.channel.as_mut() { + // Track the number of blocks until the channel times out. + let timeout = channel.open_block_number() + self.cfg.channel_timeout(origin.timestamp); + let margin = timeout.saturating_sub(origin.number) as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_CHANNEL_TIMEOUT, margin); + + // Add the frame to the channel. If this fails, return NotEnoughData and discard the + // frame. + debug!( + target: "channel_assembler", + "Adding frame #{} to channel (ID: {}) at L1 origin #{}", + next_frame.number, + hex::encode(channel.id()), + origin.number + ); + if channel.add_frame(next_frame, origin).is_err() { + error!( + target: "channel_assembler", + "Failed to add frame to channel (ID: {}) at L1 origin #{}", + hex::encode(channel.id()), + origin.number + ); + return Err(PipelineError::NotEnoughData.temp()); + } + + let size = channel.size() as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_CHANNEL_MEM, size); + + let max_rlp_bytes_per_channel = if self.cfg.is_fjord_active(origin.timestamp) { + MAX_RLP_BYTES_PER_CHANNEL_FJORD + } else { + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK + }; + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_MAX_RLP_BYTES, + max_rlp_bytes_per_channel as f64 + ); + if channel.size() > max_rlp_bytes_per_channel as usize { + warn!( + target: "channel_assembler", + "Compressed channel size exceeded max RLP bytes per channel, dropping channel (ID: {}) with {} bytes", + hex::encode(channel.id()), + channel.size() + ); + self.channel = None; + return Err(PipelineError::NotEnoughData.temp()); + } + + // If the channel is ready, forward the channel to the next stage. + if channel.is_ready() { + let channel_bytes = + channel.frame_data().ok_or(PipelineError::ChannelNotFound.crit())?; + + info!( + target: "channel_assembler", + "Channel (ID: {}) ready for decompression.", + hex::encode(channel.id()), + ); + + // Reset the channel and return the compressed bytes. + self.channel = None; + return Ok(Some(channel_bytes)); + } + } + + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_CHANNEL_MEM, 0); + + Err(PipelineError::NotEnoughData.temp()) + } +} + +#[async_trait] +impl

OriginAdvancer for ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +impl

OriginProvider for ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl

SignalReceiver for ChannelAssembler

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.prev.signal(signal).await?; + self.channel = None; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::ChannelAssembler; + use crate::{ + ChannelReaderProvider, PipelineError, + test_utils::{CollectingLayer, TestNextFrameProvider, TraceStorage}, + }; + use alloc::{sync::Arc, vec}; + use kona_genesis::{ + HardForkConfig, MAX_RLP_BYTES_PER_CHANNEL_BEDROCK, MAX_RLP_BYTES_PER_CHANNEL_FJORD, + RollupConfig, + }; + use kona_protocol::BlockInfo; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + + #[tokio::test] + async fn test_assembler_channel_timeout() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let mock = TestNextFrameProvider::new(frames.into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig::default()); + let mut assembler = ChannelAssembler::new(cfg, mock); + + // Set the origin to default block info @ block # 0. + assembler.prev.block_info = Some(BlockInfo::default()); + + // Read in the first frame. Since the frame isn't the last, the assembler + // should return None. + assert!(assembler.channel.is_none()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_some()); + + // Push the origin forward past channel timeout. + assembler.prev.block_info = + Some(BlockInfo { number: assembler.cfg.channel_timeout(0) + 1, ..Default::default() }); + + // Assert that the assembler has timed out the channel. + assert!(assembler.is_timed_out().unwrap()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_none()); + + // Assert that the info log was emitted. + let info_logs = trace_store.get_by_level(Level::INFO); + assert_eq!(info_logs.len(), 1); + let info_str = "Starting new channel"; + assert!(info_logs[0].contains(info_str)); + + // Assert that the warning log was emitted. + let warning_logs = trace_store.get_by_level(Level::WARN); + assert_eq!(warning_logs.len(), 1); + let warn_str = "timed out at L1 origin"; + assert!(warning_logs[0].contains(warn_str)); + } + + #[tokio::test] + async fn test_assembler_non_starting_frame() { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let mock = TestNextFrameProvider::new(frames.into_iter().map(Ok).collect()); + let cfg = Arc::new(RollupConfig::default()); + let mut assembler = ChannelAssembler::new(cfg, mock); + + // Send in the second frame first. This should result in no channel being created, + // and the frame being discarded. + assert!(assembler.channel.is_none()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_none()); + } + + #[tokio::test] + async fn test_assembler_already_built() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let mock = TestNextFrameProvider::new(frames.clone().into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig::default()); + let mut assembler = ChannelAssembler::new(cfg, mock); + + // Send in the first frame. This should result in a channel being created. + assert!(assembler.channel.is_none()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_some()); + + // Send in a malformed second frame. This should result in an error in `add_frame`. + assembler.prev.data.push(Ok(frames[1].clone()).map(|mut f| { + f.id = Default::default(); + f + })); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_some()); + + // Send in the second frame again. This should return the channel bytes. + assert!(assembler.next_data().await.unwrap().is_some()); + assert!(assembler.channel.is_none()); + + // Assert that the error log was emitted. + let error_logs = trace_store.get_by_level(Level::ERROR); + assert_eq!(error_logs.len(), 1); + let error_str = "Failed to add frame to channel"; + assert!(error_logs[0].contains(error_str)); + } + + #[tokio::test] + async fn test_assembler_size_limit_exceeded_bedrock() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let mut frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + frames[1].data = vec![0; MAX_RLP_BYTES_PER_CHANNEL_BEDROCK as usize]; + let mock = TestNextFrameProvider::new(frames.into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig::default()); + + let mut assembler = ChannelAssembler::new(cfg, mock); + + // Send in the first frame. This should result in a channel being created. + assert!(assembler.channel.is_none()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_some()); + + // Send in the second frame. This should result in the channel being dropped due to the size + // limit being reached. + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_none()); + + let trace_store_lock = trace_store.lock(); + assert_eq!(trace_store_lock.iter().filter(|(l, _)| matches!(l, &Level::WARN)).count(), 1); + + let (_, message) = + trace_store_lock.iter().find(|(l, _)| matches!(l, &Level::WARN)).unwrap(); + assert!(message.contains("Compressed channel size exceeded max RLP bytes per channel")); + } + + #[tokio::test] + async fn test_assembler_size_limit_exceeded_fjord() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let mut frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + frames[1].data = vec![0; MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize]; + let mock = TestNextFrameProvider::new(frames.into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { fjord_time: Some(0), ..Default::default() }, + ..Default::default() + }); + + let mut assembler = ChannelAssembler::new(cfg, mock); + + // Send in the first frame. This should result in a channel being created. + assert!(assembler.channel.is_none()); + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_some()); + + // Send in the second frame. This should result in the channel being dropped due to the size + // limit being reached. + assert_eq!(assembler.next_data().await.unwrap_err(), PipelineError::NotEnoughData.temp()); + assert!(assembler.channel.is_none()); + + let trace_store_lock = trace_store.lock(); + assert_eq!(trace_store_lock.iter().filter(|(l, _)| matches!(l, &Level::WARN)).count(), 1); + + let (_, message) = + trace_store_lock.iter().find(|(l, _)| matches!(l, &Level::WARN)).unwrap(); + assert!(message.contains("Compressed channel size exceeded max RLP bytes per channel")); + } +} diff --git a/kona/crates/protocol/derive/src/stages/channel/channel_bank.rs b/kona/crates/protocol/derive/src/stages/channel/channel_bank.rs new file mode 100644 index 0000000000000..1a1f912d2da05 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/channel/channel_bank.rs @@ -0,0 +1,557 @@ +//! This module contains the `ChannelBank` struct. + +use crate::{ + ChannelReaderProvider, NextFrameProvider, OriginAdvancer, OriginProvider, PipelineError, + PipelineErrorKind, PipelineResult, Signal, SignalReceiver, +}; +use alloc::{boxed::Box, collections::VecDeque, sync::Arc}; +use alloy_primitives::{Bytes, hex, map::HashMap}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, Channel, ChannelId, Frame}; + +/// The maximum size of a channel bank. +pub(crate) const MAX_CHANNEL_BANK_SIZE: usize = 100_000_000; + +/// The maximum size of a channel bank after the Fjord Hardfork. +pub(crate) const FJORD_MAX_CHANNEL_BANK_SIZE: usize = 1_000_000_000; + +/// [`ChannelBank`] is a stateful stage that does the following: +/// 1. Unmarshalls frames from L1 transaction data +/// 2. Applies those frames to a channel +/// 3. Attempts to read from the channel when it is ready +/// 4. Prunes channels (not frames) when the channel bank is too large. +/// +/// Note: we prune before we ingest data. +/// As we switch between ingesting data & reading, the prune step occurs at an odd point +/// Specifically, the channel bank is not allowed to become too large between successive calls +/// to `IngestData`. This means that we can do an ingest and then do a read while becoming too +/// large. [`ChannelBank`] buffers channel frames, and emits full channel data +#[derive(Debug)] +pub struct ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The rollup configuration. + pub cfg: Arc, + /// Map of channels by ID. + pub channels: HashMap, + /// Channels in FIFO order. + pub channel_queue: VecDeque, + /// The previous stage of the derivation pipeline. + pub prev: P, +} + +impl

ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Create a new [`ChannelBank`] stage. + pub fn new(cfg: Arc, prev: P) -> Self { + Self { cfg, channels: HashMap::default(), channel_queue: VecDeque::new(), prev } + } + + /// Returns the size of the channel bank by accumulating over all channels. + pub fn size(&self) -> usize { + self.channels.iter().fold(0, |acc, (_, c)| acc + c.size()) + } + + /// Prunes the Channel bank, until it is below the max channel bank size. + /// Prunes from the high-priority channel since it failed to be read. + pub fn prune(&mut self) -> PipelineResult<()> { + let mut total_size = self.size(); + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + let max_channel_bank_size = if self.cfg.is_fjord_active(origin.timestamp) { + FJORD_MAX_CHANNEL_BANK_SIZE + } else { + MAX_CHANNEL_BANK_SIZE + }; + while total_size > max_channel_bank_size { + let id = + self.channel_queue.pop_front().ok_or(PipelineError::ChannelProviderEmpty.crit())?; + let channel = self.channels.remove(&id).ok_or(PipelineError::ChannelNotFound.crit())?; + total_size -= channel.size(); + } + Ok(()) + } + + /// Adds new L1 data to the channel bank. Should only be called after all data has been read. + pub fn ingest_frame(&mut self, frame: Frame) -> PipelineResult<()> { + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + + // Get the channel for the frame, or create a new one if it doesn't exist. + let current_channel = match self.channels.get_mut(&frame.id) { + Some(c) => c, + None => { + let channel = Channel::new(frame.id, origin); + self.channel_queue.push_back(frame.id); + self.channels.insert(frame.id, channel); + self.channels.get_mut(&frame.id).expect("Channel must be in queue") + } + }; + + // Check if the channel is not timed out. If it has, ignore the frame. + if current_channel.open_block_number() + self.cfg.channel_timeout(origin.timestamp) < + origin.number + { + warn!( + target: "channel_bank", + "Channel (ID: {}) timed out", hex::encode(frame.id) + ); + return Ok(()); + } + + // Ingest the frame. If it fails, ignore the frame. + let frame_id = frame.id; + if current_channel.add_frame(frame, origin).is_err() { + warn!(target: "channel_bank", "Failed to add frame to channel: {:?}", frame_id); + return Ok(()); + } + + self.prune() + } + + /// Read the raw data of the first channel, if it's timed-out or closed. + /// + /// Returns an error if there is nothing new to read. + pub fn read(&mut self) -> PipelineResult> { + // Bail if there are no channels to read from. + if self.channel_queue.is_empty() { + trace!(target: "channel_bank", "No channels to read from"); + return Err(PipelineError::Eof.temp()); + } + + // Return an `Ok(None)` if the first channel is timed out. There may be more timed + // out channels at the head of the queue and we want to remove them all. + let first = self.channel_queue[0]; + let channel = + self.channels.get(&first).ok_or(PipelineError::ChannelProviderEmpty.crit())?; + let origin = self.origin().ok_or(PipelineError::ChannelProviderEmpty.crit())?; + if channel.open_block_number() + self.cfg.channel_timeout(origin.timestamp) < origin.number + { + warn!( + target: "channel_bank", + "Channel (ID: {}) timed out", hex::encode(first) + ); + self.channels.remove(&first); + self.channel_queue.pop_front(); + return Ok(None); + } + + // At this point we have removed all timed out channels from the front of the + // `channel_queue`. Pre-Canyon we simply check the first index. + // Post-Canyon we read the entire channelQueue for the first ready channel. + // If no channel is available, we return `PipelineError::Eof`. + // Canyon is activated when the first L1 block whose time >= CanyonTime, not on the L2 + // timestamp. + if !self.cfg.is_canyon_active(origin.timestamp) { + return self.try_read_channel_at_index(0).map(Some); + } + + let channel_data = + (0..self.channel_queue.len()).find_map(|i| self.try_read_channel_at_index(i).ok()); + channel_data.map_or_else(|| Err(PipelineError::Eof.temp()), |data| Ok(Some(data))) + } + + /// Attempts to read the channel at the specified index. If the channel is not ready or timed + /// out, it will return an error. + /// If the channel read was successful, it will remove the channel from the channel queue. + fn try_read_channel_at_index(&mut self, index: usize) -> PipelineResult { + let channel_id = self.channel_queue[index]; + let channel = + self.channels.get(&channel_id).ok_or(PipelineError::ChannelProviderEmpty.crit())?; + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + + let timed_out = channel.open_block_number() + self.cfg.channel_timeout(origin.timestamp) < + origin.number; + if timed_out || !channel.is_ready() { + return Err(PipelineError::Eof.temp()); + } + + let frame_data = channel.frame_data(); + self.channels.remove(&channel_id); + self.channel_queue.remove(index); + + frame_data.ok_or(PipelineError::ChannelProviderEmpty.crit()) + } +} + +#[async_trait] +impl

OriginAdvancer for ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl

ChannelReaderProvider for ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn next_data(&mut self) -> PipelineResult> { + match self.read() { + Err(e) => { + if !matches!(e, PipelineErrorKind::Temporary(PipelineError::Eof)) { + return Err(PipelineError::ChannelProviderEmpty.crit()); + } + } + data => return data, + }; + + // Load the data into the channel bank + let frame = match self.prev.next_frame().await { + Ok(f) => f, + Err(e) => { + return Err(e); + } + }; + let res = self.ingest_frame(frame); + res?; + Err(PipelineError::NotEnoughData.temp()) + } +} + +impl

OriginProvider for ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl

SignalReceiver for ChannelBank

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.prev.signal(signal).await?; + self.channels.clear(); + self.channel_queue = VecDeque::with_capacity(10); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + test_utils::{CollectingLayer, TestNextFrameProvider, TraceStorage}, + types::ResetSignal, + }; + use alloc::{vec, vec::Vec}; + use kona_genesis::HardForkConfig; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + + #[test] + fn test_try_read_channel_at_index_missing_channel() { + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + channel_bank.channel_queue.push_back([0xFF; 16]); + let err = channel_bank.try_read_channel_at_index(0).unwrap_err(); + assert_eq!(err, PipelineError::ChannelProviderEmpty.crit()); + } + + #[test] + fn test_try_read_channel_at_index_missing_origin() { + let mut mock = TestNextFrameProvider::new(vec![]); + mock.block_info = None; + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + channel_bank.channel_queue.push_back([0xFF; 16]); + channel_bank.channels.insert([0xFF; 16], Channel::new([0xFF; 16], BlockInfo::default())); + let err = channel_bank.try_read_channel_at_index(0).unwrap_err(); + assert_eq!(err, PipelineError::MissingOrigin.crit()); + } + + #[test] + fn test_try_read_channel_at_index_timed_out() { + let mut mock = TestNextFrameProvider::new(vec![]); + mock.block_info = Some(BlockInfo { number: 10, ..Default::default() }); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + channel_bank.channel_queue.push_back([0xFF; 16]); + channel_bank.channels.insert([0xFF; 16], Channel::new([0xFF; 16], BlockInfo::default())); + let err = channel_bank.try_read_channel_at_index(0).unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + } + + #[test] + fn test_try_read_channel_at_index() { + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + let id: ChannelId = [0xFF; 16]; + channel_bank.channel_queue.push_back(id); + let mut channel = Channel::new(id, BlockInfo::default()); + channel + .add_frame( + Frame { id, number: 0, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 1, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 2, data: b"seven__".to_vec(), is_last: true }, + BlockInfo::default(), + ) + .unwrap(); + assert!(channel.is_ready()); + channel_bank.channels.insert([0xFF; 16], channel); + let frame_data = channel_bank.try_read_channel_at_index(0).unwrap(); + assert_eq!( + frame_data, + alloy_primitives::bytes!("736576656e5f5f736576656e5f5f736576656e5f5f") + ); + } + + #[test] + fn test_read_channel_canyon_not_active() { + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + let id: ChannelId = [0xFF; 16]; + channel_bank.channel_queue.push_back(id); + let mut channel = Channel::new(id, BlockInfo::default()); + channel + .add_frame( + Frame { id, number: 0, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 1, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 2, data: b"seven__".to_vec(), is_last: true }, + BlockInfo::default(), + ) + .unwrap(); + assert!(channel.is_ready()); + channel_bank.channels.insert([0xFF; 16], channel); + let frame_data = channel_bank.read().unwrap(); + assert_eq!( + frame_data, + Some(alloy_primitives::bytes!("736576656e5f5f736576656e5f5f736576656e5f5f")) + ); + } + + #[test] + fn test_read_channel_active() { + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { canyon_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut channel_bank = ChannelBank::new(cfg, mock); + let id: ChannelId = [0xFF; 16]; + channel_bank.channel_queue.push_back(id); + let mut channel = Channel::new(id, BlockInfo::default()); + channel + .add_frame( + Frame { id, number: 0, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 1, data: b"seven__".to_vec(), is_last: false }, + BlockInfo::default(), + ) + .unwrap(); + channel + .add_frame( + Frame { id, number: 2, data: b"seven__".to_vec(), is_last: true }, + BlockInfo::default(), + ) + .unwrap(); + assert!(channel.is_ready()); + channel_bank.channels.insert([0xFF; 16], channel); + let frame_data = channel_bank.read().unwrap(); + assert_eq!( + frame_data, + Some(alloy_primitives::bytes!("736576656e5f5f736576656e5f5f736576656e5f5f")) + ); + } + + #[test] + fn test_ingest_empty_origin() { + let mut mock = TestNextFrameProvider::new(vec![]); + mock.block_info = None; + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + let frame = Frame::default(); + let err = channel_bank.ingest_frame(frame).unwrap_err(); + assert_eq!(err, PipelineError::MissingOrigin.crit()); + } + + #[tokio::test] + async fn test_reset() { + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + channel_bank.channels.insert([0xFF; 16], Channel::default()); + channel_bank.channel_queue.push_back([0xFF; 16]); + assert!(!channel_bank.prev.reset); + channel_bank.signal(ResetSignal::default().signal()).await.unwrap(); + assert_eq!(channel_bank.channels.len(), 0); + assert_eq!(channel_bank.channel_queue.len(), 0); + assert!(channel_bank.prev.reset); + } + + #[test] + fn test_ingest_invalid_frame() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let mock = TestNextFrameProvider::new(vec![]); + let mut channel_bank = ChannelBank::new(Arc::new(RollupConfig::default()), mock); + let frame = Frame { id: [0xFF; 16], ..Default::default() }; + assert_eq!(channel_bank.size(), 0); + assert!(channel_bank.channels.is_empty()); + assert_eq!(trace_store.lock().iter().filter(|(l, _)| matches!(l, &Level::WARN)).count(), 0); + assert_eq!(channel_bank.ingest_frame(frame.clone()), Ok(())); + assert_eq!(channel_bank.size(), kona_protocol::FRAME_OVERHEAD); + assert_eq!(channel_bank.channels.len(), 1); + // This should fail since the frame is already ingested. + assert_eq!(channel_bank.ingest_frame(frame), Ok(())); + assert_eq!(channel_bank.size(), kona_protocol::FRAME_OVERHEAD); + assert_eq!(channel_bank.channels.len(), 1); + assert_eq!(trace_store.lock().iter().filter(|(l, _)| matches!(l, &Level::WARN)).count(), 1); + } + + #[test] + fn test_ingest_and_prune_channel_bank() { + let mut frames = crate::frames!(0xFF, 0, vec![0xDD; 50], 100000); + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + // Ingest frames until the channel bank is full and it stops increasing in size + let mut current_size = 0; + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + while channel_bank.size() > current_size { + current_size = channel_bank.size(); + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + assert!(channel_bank.size() <= MAX_CHANNEL_BANK_SIZE); + } + // There should be a bunch of frames leftover + assert!(!frames.is_empty()); + // If we ingest one more frame, the channel bank should prune + // and the size should be the same + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + assert_eq!(channel_bank.size(), current_size); + } + + #[test] + fn test_ingest_and_prune_channel_bank_fjord() { + let mut frames = crate::frames!(0xFF, 0, vec![0xDD; 50], 100000); + let mock = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { fjord_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut channel_bank = ChannelBank::new(cfg, mock); + // Ingest frames until the channel bank is full and it stops increasing in size + let mut current_size = 0; + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + while channel_bank.size() > current_size { + current_size = channel_bank.size(); + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + assert!(channel_bank.size() <= FJORD_MAX_CHANNEL_BANK_SIZE); + } + // There should be a bunch of frames leftover + assert!(!frames.is_empty()); + // If we ingest one more frame, the channel bank should prune + // and the size should be the same + let next_frame = frames.pop().unwrap(); + channel_bank.ingest_frame(next_frame).unwrap(); + assert_eq!(channel_bank.size(), current_size); + } + + #[tokio::test] + async fn test_read_empty_channel_bank() { + let frames = [crate::frame!(0xFF, 0, vec![0xDD; 50], true)]; + let mock = TestNextFrameProvider::new(vec![Ok(frames[0].clone())]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_bank = ChannelBank::new(cfg, mock); + let err = channel_bank.read().unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + let err = channel_bank.next_data().await.unwrap_err(); + assert_eq!(err, PipelineError::NotEnoughData.temp()); + } + + #[tokio::test] + async fn test_channel_timeout() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + let configs: [RollupConfig; 2] = [ + kona_registry::ROLLUP_CONFIGS.get(&10).cloned().unwrap(), + kona_registry::ROLLUP_CONFIGS.get(&8453).cloned().unwrap(), + ]; + + for cfg in configs { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let mock = TestNextFrameProvider::new(frames.into_iter().map(Ok).collect::>()); + let cfg = Arc::new(cfg); + let mut channel_bank = ChannelBank::new(cfg.clone(), mock); + + // Ingest first frame + let err = channel_bank.next_data().await.unwrap_err(); + assert_eq!(err, PipelineError::NotEnoughData.temp()); + + for _ in 0..cfg.channel_timeout + 1 { + channel_bank.advance_origin().await.unwrap(); + } + + // There should be an in-progress channel. + assert_eq!(channel_bank.channels.len(), 1); + assert_eq!(channel_bank.channel_queue.len(), 1); + + // Should be `Ok(())`, channel timed out. + channel_bank.next_data().await.unwrap(); + + // The channel should have been pruned. + assert_eq!(channel_bank.channels.len(), 0); + assert_eq!(channel_bank.channel_queue.len(), 0); + + // Ensure the channel was successfully timed out. + let (_, warning_trace) = trace_store + .lock() + .iter() + .find(|(l, _)| matches!(l, &Level::WARN)) + .cloned() + .unwrap(); + assert!(warning_trace.contains("timed out")); + } + } +} diff --git a/kona/crates/protocol/derive/src/stages/channel/channel_provider.rs b/kona/crates/protocol/derive/src/stages/channel/channel_provider.rs new file mode 100644 index 0000000000000..688f9468a8c96 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/channel/channel_provider.rs @@ -0,0 +1,364 @@ +//! This module contains the [ChannelProvider] stage. + +use super::{ChannelAssembler, ChannelBank, ChannelReaderProvider, NextFrameProvider}; +use crate::{ + errors::PipelineError, + traits::{OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::Bytes; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::BlockInfo; + +/// The [`ChannelProvider`] stage is a mux between the [`ChannelBank`] and [`ChannelAssembler`] +/// stages. +/// +/// Rules: +/// When Holocene is not active, the [`ChannelBank`] is used. +/// When Holocene is active, the [`ChannelAssembler`] is used. +#[derive(Debug)] +pub struct ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The rollup configuration. + pub cfg: Arc, + /// The previous stage of the derivation pipeline. + /// + /// If this is set to [`None`], the multiplexer has been activated and the active stage + /// owns the previous stage. + /// + /// Must be [`None`] if `channel_bank` or `channel_assembler` is [`Some`]. + pub prev: Option

, + /// The channel bank stage of the provider. + /// + /// Must be [`None`] if `prev` or `channel_assembler` is [`Some`]. + pub channel_bank: Option>, + /// The channel assembler stage of the provider. + /// + /// Must be [`None`] if `prev` or `channel_bank` is [`Some`]. + pub channel_assembler: Option>, +} + +impl

ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Creates a new [`ChannelProvider`] with the given configuration and previous stage. + pub const fn new(cfg: Arc, prev: P) -> Self { + Self { cfg, prev: Some(prev), channel_bank: None, channel_assembler: None } + } + + /// Attempts to update the active stage of the mux. + pub(crate) fn attempt_update(&mut self) -> PipelineResult<()> { + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + if let Some(prev) = self.prev.take() { + // On the first call to `attempt_update`, we need to determine the active stage to + // initialize the mux with. + if self.cfg.is_holocene_active(origin.timestamp) { + self.channel_assembler = Some(ChannelAssembler::new(self.cfg.clone(), prev)); + } else { + self.channel_bank = Some(ChannelBank::new(self.cfg.clone(), prev)); + } + } else if self.channel_bank.is_some() && self.cfg.is_holocene_active(origin.timestamp) { + // If the channel bank is active and Holocene is also active, transition to the channel + // assembler. + let channel_bank = self.channel_bank.take().expect("Must have channel bank"); + self.channel_assembler = + Some(ChannelAssembler::new(self.cfg.clone(), channel_bank.prev)); + } else if self.channel_assembler.is_some() && !self.cfg.is_holocene_active(origin.timestamp) + { + // If the channel assembler is active, and Holocene is not active, it indicates an L1 + // reorg around Holocene activation. Transition back to the channel bank + // until Holocene re-activates. + let channel_assembler = + self.channel_assembler.take().expect("Must have channel assembler"); + self.channel_bank = Some(ChannelBank::new(self.cfg.clone(), channel_assembler.prev)); + } + Ok(()) + } +} + +#[async_trait] +impl

OriginAdvancer for ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.attempt_update()?; + + if let Some(channel_assembler) = self.channel_assembler.as_mut() { + channel_assembler.advance_origin().await + } else if let Some(channel_bank) = self.channel_bank.as_mut() { + channel_bank.advance_origin().await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +impl

OriginProvider for ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.channel_assembler.as_ref().map_or_else( + || { + self.channel_bank.as_ref().map_or_else( + || self.prev.as_ref().and_then(|prev| prev.origin()), + |channel_bank| channel_bank.origin(), + ) + }, + |channel_assembler| channel_assembler.origin(), + ) + } +} + +#[async_trait] +impl

SignalReceiver for ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.attempt_update()?; + + if let Some(channel_assembler) = self.channel_assembler.as_mut() { + channel_assembler.signal(signal).await + } else if let Some(channel_bank) = self.channel_bank.as_mut() { + channel_bank.signal(signal).await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +#[async_trait] +impl

ChannelReaderProvider for ChannelProvider

+where + P: NextFrameProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn next_data(&mut self) -> PipelineResult> { + self.attempt_update()?; + + if let Some(channel_assembler) = self.channel_assembler.as_mut() { + channel_assembler.next_data().await + } else if let Some(channel_bank) = self.channel_bank.as_mut() { + channel_bank.next_data().await + } else { + Err(PipelineError::NotEnoughData.temp()) + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + ChannelProvider, ChannelReaderProvider, OriginProvider, PipelineError, ResetSignal, + SignalReceiver, test_utils::TestNextFrameProvider, + }; + use alloc::{sync::Arc, vec}; + use kona_genesis::{HardForkConfig, RollupConfig}; + use kona_protocol::BlockInfo; + + #[test] + fn test_channel_provider_assembler_active() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + assert!(channel_provider.attempt_update().is_ok()); + assert!(channel_provider.prev.is_none()); + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_some()); + } + + #[test] + fn test_channel_provider_bank_active() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + assert!(channel_provider.attempt_update().is_ok()); + assert!(channel_provider.prev.is_none()); + assert!(channel_provider.channel_bank.is_some()); + assert!(channel_provider.channel_assembler.is_none()); + } + + #[test] + fn test_channel_provider_retain_current_bank() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + // Assert the multiplexer hasn't been initialized. + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_none()); + assert!(channel_provider.prev.is_some()); + + // Load in the active stage. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_some()); + assert!(channel_provider.channel_assembler.is_none()); + assert!(channel_provider.prev.is_none()); + // Ensure the active stage is retained on the second call. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_some()); + assert!(channel_provider.channel_assembler.is_none()); + assert!(channel_provider.prev.is_none()); + } + + #[test] + fn test_channel_provider_retain_current_assembler() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + // Assert the multiplexer hasn't been initialized. + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_none()); + assert!(channel_provider.prev.is_some()); + + // Load in the active stage. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_some()); + assert!(channel_provider.prev.is_none()); + // Ensure the active stage is retained on the second call. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_some()); + assert!(channel_provider.prev.is_none()); + } + + #[test] + fn test_channel_provider_transition_stage() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(2), ..Default::default() }, + ..Default::default() + }); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + channel_provider.attempt_update().unwrap(); + + // Update the L1 origin to Holocene activation. + let Some(ref mut stage) = channel_provider.channel_bank else { + panic!("Expected ChannelBank"); + }; + stage.prev.block_info = Some(BlockInfo { number: 1, timestamp: 2, ..Default::default() }); + + // Transition to the ChannelAssembler stage. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_some()); + + assert_eq!(channel_provider.origin().unwrap().number, 1); + } + + #[test] + fn test_channel_provider_transition_stage_backwards() { + let provider = TestNextFrameProvider::new(vec![]); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(2), ..Default::default() }, + ..Default::default() + }); + let mut channel_provider = ChannelProvider::new(cfg, provider); + + channel_provider.attempt_update().unwrap(); + + // Update the L1 origin to Holocene activation. + let Some(ref mut stage) = channel_provider.channel_bank else { + panic!("Expected ChannelBank"); + }; + stage.prev.block_info = Some(BlockInfo { number: 1, timestamp: 2, ..Default::default() }); + + // Transition to the ChannelAssembler stage. + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_none()); + assert!(channel_provider.channel_assembler.is_some()); + + // Update the L1 origin to before Holocene activation, to simulate a re-org. + let Some(ref mut stage) = channel_provider.channel_assembler else { + panic!("Expected ChannelAssembler"); + }; + stage.prev.block_info = Some(BlockInfo::default()); + + channel_provider.attempt_update().unwrap(); + assert!(channel_provider.channel_bank.is_some()); + assert!(channel_provider.channel_assembler.is_none()); + } + + #[tokio::test] + async fn test_channel_provider_reset_bank() { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let provider = TestNextFrameProvider::new(frames.into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig::default()); + let mut channel_provider = ChannelProvider::new(cfg.clone(), provider); + + // Load in the first frame. + assert_eq!( + channel_provider.next_data().await.unwrap_err(), + PipelineError::NotEnoughData.temp() + ); + let Some(channel_bank) = channel_provider.channel_bank.as_mut() else { + panic!("Expected ChannelBank"); + }; + // Ensure a channel is in the queue. + assert!(channel_bank.channel_queue.len() == 1); + + // Reset the channel provider. + channel_provider.signal(ResetSignal::default().signal()).await.unwrap(); + + // Ensure the channel queue is empty after reset. + let Some(channel_bank) = channel_provider.channel_bank.as_mut() else { + panic!("Expected ChannelBank"); + }; + assert!(channel_bank.channel_queue.is_empty()); + } + + #[tokio::test] + async fn test_channel_provider_reset_assembler() { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let provider = TestNextFrameProvider::new(frames.into_iter().rev().map(Ok).collect()); + let cfg = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mut channel_provider = ChannelProvider::new(cfg.clone(), provider); + + // Load in the first frame. + assert_eq!( + channel_provider.next_data().await.unwrap_err(), + PipelineError::NotEnoughData.temp() + ); + let Some(channel_assembler) = channel_provider.channel_assembler.as_mut() else { + panic!("Expected ChannelAssembler"); + }; + // Ensure a channel is being built. + assert!(channel_assembler.channel.is_some()); + + // Reset the channel provider. + channel_provider.signal(ResetSignal::default().signal()).await.unwrap(); + + // Ensure the channel assembler is empty after reset. + let Some(channel_assembler) = channel_provider.channel_assembler.as_mut() else { + panic!("Expected ChannelAssembler"); + }; + assert!(channel_assembler.channel.is_none()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/channel/channel_reader.rs b/kona/crates/protocol/derive/src/stages/channel/channel_reader.rs new file mode 100644 index 0000000000000..48aa0d293ba0f --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/channel/channel_reader.rs @@ -0,0 +1,294 @@ +//! This module contains the `ChannelReader` struct. + +use crate::{ + BatchStreamProvider, OriginAdvancer, OriginProvider, PipelineError, PipelineResult, Signal, + SignalReceiver, +}; +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::Bytes; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::{ + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK, MAX_RLP_BYTES_PER_CHANNEL_FJORD, RollupConfig, +}; +use kona_protocol::{Batch, BatchReader, BlockInfo}; +use tracing::{debug, warn}; + +/// The [`ChannelReader`] provider trait. +#[async_trait] +pub trait ChannelReaderProvider { + /// Pulls the next piece of data from the channel bank. Note that it attempts to pull data out + /// of the channel bank prior to loading data in (unlike most other stages). This is to + /// ensure maintain consistency around channel bank pruning which depends upon the order + /// of operations. + async fn next_data(&mut self) -> PipelineResult>; +} + +/// [`ChannelReader`] is a stateful stage that reads [`Batch`]es from `Channel`s. +/// +/// The [`ChannelReader`] pulls `Channel`s from the channel bank as raw data +/// and pipes it into a `BatchReader`. Since the raw data is compressed, +/// the `BatchReader` first decompresses the data using the first bytes as +/// a compression algorithm identifier. +/// +/// Once the data is decompressed, it is decoded into a `Batch` and passed +/// to the next stage in the pipeline. +#[derive(Debug)] +pub struct ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The previous stage of the derivation pipeline. + pub prev: P, + /// The batch reader. + pub next_batch: Option, + /// The rollup configuration. + pub cfg: Arc, +} + +impl

ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Create a new [`ChannelReader`] stage. + pub const fn new(prev: P, cfg: Arc) -> Self { + Self { prev, next_batch: None, cfg } + } + + /// Creates the batch reader from available channel data. + async fn set_batch_reader(&mut self) -> PipelineResult<()> { + if self.next_batch.is_none() { + let channel = + self.prev.next_data().await?.ok_or(PipelineError::ChannelReaderEmpty.temp())?; + + let origin = self.prev.origin().ok_or(PipelineError::MissingOrigin.crit())?; + let max_rlp_bytes_per_channel = if self.cfg.is_fjord_active(origin.timestamp) { + MAX_RLP_BYTES_PER_CHANNEL_FJORD + } else { + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK + }; + + self.next_batch = + Some(BatchReader::new(&channel[..], max_rlp_bytes_per_channel as usize)); + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_READER_SET, 1); + } + Ok(()) + } + + /// Forces the read to continue with the next channel, resetting any + /// decoding / decompression state to a fresh start. + pub fn next_channel(&mut self) { + self.next_batch = None; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_READER_SET, 0); + } +} + +#[async_trait] +impl

OriginAdvancer for ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl

BatchStreamProvider for ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + /// This method is called by the BatchStream if an invalid span batch is found. + /// In the case of an invalid span batch, the associated channel must be flushed. + /// + /// See: + /// + /// SAFETY: Only called post-holocene activation. + fn flush(&mut self) { + debug!(target: "channel_reader", "[POST-HOLOCENE] Flushing channel"); + self.next_channel(); + } + + async fn next_batch(&mut self) -> PipelineResult { + if let Err(e) = self.set_batch_reader().await { + debug!(target: "channel_reader", "Failed to set batch reader: {:?}", e); + self.next_channel(); + return Err(e); + } + + // SAFETY: The batch reader must be set above. + let next_batch = self.next_batch.as_mut().expect("Batch reader must be set"); + match next_batch.decompress() { + Ok(()) => { + // Record the decompressed size and type. + let size = next_batch.decompressed.len() as f64; + let ty = if next_batch.brotli_used { + BatchReader::CHANNEL_VERSION_BROTLI + } else { + BatchReader::ZLIB_DEFLATE_COMPRESSION_METHOD + }; + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_LATEST_DECOMPRESSED_BATCH_SIZE, + size + ); + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_LATEST_DECOMPRESSED_BATCH_TYPE, + ty as f64 + ); + } + Err(err) => { + debug!(target: "channel_reader", ?err, "Failed to decompress batch"); + self.next_channel(); + return Err(PipelineError::NotEnoughData.temp()); + } + } + + // Read the next batch from the reader's decompressed data + match next_batch.next_batch(self.cfg.as_ref()).ok_or(PipelineError::NotEnoughData.temp()) { + Ok(batch) => { + kona_macros::inc!( + gauge, + crate::metrics::Metrics::PIPELINE_READ_BATCHES, + "type" => batch.to_string(), + ); + Ok(batch) + } + Err(e) => { + self.next_channel(); + Err(e) + } + } + } +} + +impl

OriginProvider for ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl

SignalReceiver for ChannelReader

+where + P: ChannelReaderProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug + Send, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::FlushChannel => { + // Drop the current in-progress channel. + warn!(target: "channel_reader", "Flushed channel"); + self.next_batch = None; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_BATCH_READER_SET, 0); + } + s => { + self.prev.signal(s).await?; + self.next_channel(); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + errors::PipelineErrorKind, test_utils::TestChannelReaderProvider, types::ResetSignal, + }; + use alloc::vec; + use kona_genesis::HardForkConfig; + + fn new_compressed_batch_data() -> Bytes { + let file_contents = + alloc::string::String::from_utf8_lossy(include_bytes!("../../../testdata/batch.hex")); + let file_contents = &(&*file_contents)[..file_contents.len() - 1]; + let data = alloy_primitives::hex::decode(file_contents).unwrap(); + data.into() + } + + #[tokio::test] + async fn test_flush_channel_reader() { + let mock = TestChannelReaderProvider::new(vec![Ok(Some(new_compressed_batch_data()))]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + reader.next_batch = Some(BatchReader::new( + new_compressed_batch_data(), + MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize, + )); + reader.signal(Signal::FlushChannel).await.unwrap(); + assert!(reader.next_batch.is_none()); + } + + #[tokio::test] + async fn test_reset_channel_reader() { + let mock = TestChannelReaderProvider::new(vec![Ok(None)]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + reader.next_batch = Some(BatchReader::new( + vec![0x00, 0x01, 0x02], + MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize, + )); + assert!(!reader.prev.reset); + reader.signal(ResetSignal::default().signal()).await.unwrap(); + assert!(reader.next_batch.is_none()); + assert!(reader.prev.reset); + } + + #[tokio::test] + async fn test_next_batch_batch_reader_set_fails() { + let mock = TestChannelReaderProvider::new(vec![Err(PipelineError::Eof.temp())]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + assert_eq!(reader.next_batch().await, Err(PipelineError::Eof.temp())); + assert!(reader.next_batch.is_none()); + } + + #[tokio::test] + async fn test_next_batch_batch_reader_no_data() { + let mock = TestChannelReaderProvider::new(vec![Ok(None)]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + assert!(matches!( + reader.next_batch().await.unwrap_err(), + PipelineErrorKind::Temporary(PipelineError::ChannelReaderEmpty) + )); + assert!(reader.next_batch.is_none()); + } + + #[tokio::test] + async fn test_next_batch_batch_reader_not_enough_data() { + let mut first = new_compressed_batch_data(); + let second = first.split_to(first.len() / 2); + let mock = TestChannelReaderProvider::new(vec![Ok(Some(first)), Ok(Some(second))]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + assert_eq!(reader.next_batch().await, Err(PipelineError::NotEnoughData.temp())); + assert!(reader.next_batch.is_none()); + } + + #[tokio::test] + async fn test_next_batch_succeeds() { + let raw = new_compressed_batch_data(); + let mock = TestChannelReaderProvider::new(vec![Ok(Some(raw))]); + let mut reader = ChannelReader::new(mock, Arc::new(RollupConfig::default())); + let res = reader.next_batch().await.unwrap(); + matches!(res, Batch::Span(_)); + assert!(reader.next_batch.is_some()); + } + + #[tokio::test] + async fn test_flush_post_holocene() { + let raw = new_compressed_batch_data(); + let config = Arc::new(RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }); + let mock = TestChannelReaderProvider::new(vec![Ok(Some(raw))]); + let mut reader = ChannelReader::new(mock, config); + let res = reader.next_batch().await.unwrap(); + matches!(res, Batch::Span(_)); + assert!(reader.next_batch.is_some()); + reader.flush(); + assert!(reader.next_batch.is_none()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/channel/mod.rs b/kona/crates/protocol/derive/src/stages/channel/mod.rs new file mode 100644 index 0000000000000..85518fd5481a4 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/channel/mod.rs @@ -0,0 +1,38 @@ +//! Stages pertaining to the reading and decoding of channels. +//! +//! Sitting after the [FrameQueue] stage, the [ChannelBank] and [ChannelAssembler] stages are +//! responsible for reading and decoding the [Frame]s into [Channel]s. The [ChannelReader] stage +//! is responsible for decoding the [Channel]s into [Batch]es, forwarding the [Batch]es to the +//! [BatchQueue] stage. +//! +//! [Frame]: kona_protocol::Frame +//! [Channel]: kona_protocol::Channel +//! [Batch]: kona_protocol::Batch +//! [FrameQueue]: crate::stages::FrameQueue +//! [BatchQueue]: crate::stages::BatchQueue + +use crate::types::PipelineResult; +use alloc::boxed::Box; +use async_trait::async_trait; +use kona_protocol::Frame; + +pub(crate) mod channel_provider; +pub use channel_provider::ChannelProvider; + +pub(crate) mod channel_bank; +pub use channel_bank::ChannelBank; + +pub(crate) mod channel_assembler; +pub use channel_assembler::ChannelAssembler; + +pub(crate) mod channel_reader; +pub use channel_reader::{ChannelReader, ChannelReaderProvider}; + +/// Provides frames for the [`ChannelBank`] and [`ChannelAssembler`] stages. +#[async_trait] +pub trait NextFrameProvider { + /// Retrieves the next [`Frame`] from the [`FrameQueue`] stage. + /// + /// [`FrameQueue`]: crate::stages::FrameQueue + async fn next_frame(&mut self) -> PipelineResult; +} diff --git a/kona/crates/protocol/derive/src/stages/frame_queue.rs b/kona/crates/protocol/derive/src/stages/frame_queue.rs new file mode 100644 index 0000000000000..4a993a65c9ded --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/frame_queue.rs @@ -0,0 +1,548 @@ +//! This module contains the [FrameQueue] stage of the derivation pipeline. + +use crate::{ + NextFrameProvider, OriginAdvancer, OriginProvider, PipelineError, PipelineResult, Signal, + SignalReceiver, +}; +use alloc::{boxed::Box, collections::VecDeque, sync::Arc}; +use alloy_primitives::Bytes; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, Frame}; + +/// Provides data frames for the [`FrameQueue`] stage. +#[async_trait] +pub trait FrameQueueProvider { + /// An item that can be converted into a byte array. + type Item: Into; + + /// Retrieves the next data item from the L1 retrieval stage. + /// If there is data, it pushes it into the next stage. + /// If there is no data, it returns an error. + async fn next_data(&mut self) -> PipelineResult; +} + +/// The [`FrameQueue`] stage of the derivation pipeline. +/// This stage takes the output of the [`L1Retrieval`] stage and parses it into frames. +/// +/// [`L1Retrieval`]: crate::stages::L1Retrieval +#[derive(Debug)] +pub struct FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// The previous stage in the pipeline. + pub prev: P, + /// The current frame queue. + pub queue: VecDeque, + /// The rollup config. + pub rollup_config: Arc, +} + +impl

FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + /// Create a new [`FrameQueue`] stage with the given previous [`L1Retrieval`] stage. + /// + /// [`L1Retrieval`]: crate::stages::L1Retrieval + pub const fn new(prev: P, cfg: Arc) -> Self { + Self { prev, queue: VecDeque::new(), rollup_config: cfg } + } + + /// Returns if holocene is active. + pub fn is_holocene_active(&self, origin: BlockInfo) -> bool { + self.rollup_config.is_holocene_active(origin.timestamp) + } + + /// Prunes frames if Holocene is active. + pub fn prune(&mut self, origin: BlockInfo) { + if !self.is_holocene_active(origin) { + return; + } + + let mut i = 0; + while i < self.queue.len() - 1 { + let prev_frame = &self.queue[i]; + let next_frame = &self.queue[i + 1]; + let extends_channel = prev_frame.id == next_frame.id; + + // If the frames are in the same channel, and the frame numbers are not sequential, + // drop the next frame. + if extends_channel && prev_frame.number + 1 != next_frame.number { + self.queue.remove(i + 1); + continue; + } + + // If the frames are in the same channel, and the previous is last, drop the next frame. + if extends_channel && prev_frame.is_last { + self.queue.remove(i + 1); + continue; + } + + // If the frames are in different channels, the next frame must be first. + if !extends_channel && next_frame.number != 0 { + self.queue.remove(i + 1); + continue; + } + + // If the frames are in different channels, and the current channel is not last, walk + // back the channel and drop all prev frames. + if !extends_channel && !prev_frame.is_last && next_frame.number == 0 { + // Find the index of the first frame in the queue with the same channel ID + // as the previous frame. + let first_frame = + self.queue.iter().position(|f| f.id == prev_frame.id).expect("infallible"); + + // Drain all frames from the previous channel. + let drained = self.queue.drain(first_frame..=i); + i = i.saturating_sub(drained.len()); + continue; + } + + i += 1; + } + } + + /// Loads more frames into the [`FrameQueue`]. + pub async fn load_frames(&mut self) -> PipelineResult<()> { + // Skip loading frames if the queue is not empty. + if !self.queue.is_empty() { + return Ok(()); + } + + let data = match self.prev.next_data().await { + Ok(data) => data, + Err(e) => { + debug!(target: "frame_queue", "Failed to retrieve data: {:?}", e); + // SAFETY: Bubble up potential EOF error without wrapping. + return Err(e); + } + }; + + let Ok(frames) = Frame::parse_frames(&data.into()) else { + // There may be more frames in the queue for the + // pipeline to advance, so don't return an error here. + error!(target: "frame_queue", "Failed to parse frames from data."); + return Ok(()); + }; + + // Optimistically extend the queue with the new frames. + self.queue.extend(frames); + + // Update metrics with last frame count + kona_macros::set!( + gauge, + crate::metrics::Metrics::PIPELINE_FRAME_QUEUE_BUFFER, + self.queue.len() as f64 + ); + let queue_size = self.queue.iter().map(|f| f.size()).sum::() as f64; + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_FRAME_QUEUE_MEM, queue_size); + + // Prune frames if Holocene is active. + let origin = self.origin().ok_or(PipelineError::MissingOrigin.crit())?; + self.prune(origin); + + Ok(()) + } +} + +#[async_trait] +impl

OriginAdvancer for FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl

NextFrameProvider for FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn next_frame(&mut self) -> PipelineResult { + self.load_frames().await?; + + // If we did not add more frames but still have more data, retry this function. + if self.queue.is_empty() { + trace!(target: "frame_queue", "Queue is empty after fetching data. Retrying next_frame."); + return Err(PipelineError::NotEnoughData.temp()); + } + + Ok(self.queue.pop_front().expect("Frame queue impossibly empty")) + } +} + +impl

OriginProvider for FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Debug, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl

SignalReceiver for FrameQueue

+where + P: FrameQueueProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send + Debug, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.prev.signal(signal).await?; + self.queue = VecDeque::default(); + Ok(()) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{test_utils::TestFrameQueueProvider, types::ResetSignal}; + use alloc::vec; + use kona_genesis::HardForkConfig; + + #[tokio::test] + async fn test_frame_queue_reset() { + let mock = TestFrameQueueProvider::new(vec![]); + let mut frame_queue = FrameQueue::new(mock, Default::default()); + assert!(!frame_queue.prev.reset); + frame_queue.signal(ResetSignal::default().signal()).await.unwrap(); + assert_eq!(frame_queue.queue.len(), 0); + assert!(frame_queue.prev.reset); + } + + #[tokio::test] + async fn test_frame_queue_empty_bytes() { + let data = vec![Ok(Bytes::from(vec![0x00]))]; + let mut mock = TestFrameQueueProvider::new(data); + mock.set_origin(BlockInfo::default()); + let mut frame_queue = FrameQueue::new(mock, Default::default()); + assert!(!frame_queue.is_holocene_active(BlockInfo::default())); + let err = frame_queue.next_frame().await.unwrap_err(); + assert_eq!(err, PipelineError::NotEnoughData.temp()); + } + + #[tokio::test] + async fn test_frame_queue_no_frames_decoded() { + let data = vec![Err(PipelineError::Eof.temp()), Ok(Bytes::default())]; + let mut mock = TestFrameQueueProvider::new(data); + mock.set_origin(BlockInfo::default()); + let mut frame_queue = FrameQueue::new(mock, Default::default()); + assert!(!frame_queue.is_holocene_active(BlockInfo::default())); + let err = frame_queue.next_frame().await.unwrap_err(); + assert_eq!(err, PipelineError::NotEnoughData.temp()); + } + + #[tokio::test] + async fn test_frame_queue_wrong_derivation_version() { + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_origin(BlockInfo::default()) + .with_raw_frames(Bytes::from(vec![0x01])) + .with_expected_err(PipelineError::NotEnoughData.temp()) + .build(); + assert.holocene_active(false); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_frame_queue_frame_too_short() { + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_origin(BlockInfo::default()) + .with_raw_frames(Bytes::from(vec![0x00, 0x01])) + .with_expected_err(PipelineError::NotEnoughData.temp()) + .build(); + assert.holocene_active(false); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_frame_queue_single_frame() { + let frames = [crate::frame!(0xFF, 0, vec![0xDD; 50], true)]; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_expected_frames(&frames) + .with_origin(BlockInfo::default()) + .with_frames(&frames) + .build(); + assert.holocene_active(false); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_frame_queue_multiple_frames() { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], false), + crate::frame!(0xFF, 2, vec![0xDD; 50], true), + ]; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_expected_frames(&frames) + .with_origin(BlockInfo::default()) + .with_frames(&frames) + .build(); + assert.holocene_active(false); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_frame_queue_missing_origin() { + let frames = [crate::frame!(0xFF, 0, vec![0xDD; 50], true)]; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_expected_frames(&frames) + .with_frames(&frames) + .build(); + assert.holocene_active(false); + assert.missing_origin().await; + } + + #[tokio::test] + async fn test_holocene_valid_frames() { + let frames = [ + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], false), + crate::frame!(0xFF, 2, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_single_frame() { + let frames = [crate::frame!(0xFF, 1, vec![0xDD; 50], true)]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_unordered_frames() { + let frames = [ + // -- First Channel -- + crate::frame!(0xEE, 0, vec![0xDD; 50], false), + crate::frame!(0xEE, 1, vec![0xDD; 50], false), + crate::frame!(0xEE, 2, vec![0xDD; 50], true), + crate::frame!(0xEE, 3, vec![0xDD; 50], false), // Dropped + // -- Next Channel -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&[&frames[0..3], &frames[4..]].concat()) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_non_sequential_frames() { + let frames = [ + // -- First Channel -- + crate::frame!(0xEE, 0, vec![0xDD; 50], false), + crate::frame!(0xEE, 1, vec![0xDD; 50], false), + crate::frame!(0xEE, 3, vec![0xDD; 50], true), // Dropped + crate::frame!(0xEE, 4, vec![0xDD; 50], false), // Dropped + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames[0..2]) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_unclosed_channel() { + let frames = [ + // -- First Channel -- + crate::frame!(0xEE, 0, vec![0xDD; 50], false), + crate::frame!(0xEE, 1, vec![0xDD; 50], false), + crate::frame!(0xEE, 2, vec![0xDD; 50], false), + crate::frame!(0xEE, 3, vec![0xDD; 50], false), + // -- Next Channel -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames[4..]) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_unstarted_channel() { + let frames = [ + // -- First Channel -- + crate::frame!(0xDD, 0, vec![0xDD; 50], false), + crate::frame!(0xDD, 1, vec![0xDD; 50], false), + crate::frame!(0xDD, 2, vec![0xDD; 50], false), + crate::frame!(0xDD, 3, vec![0xDD; 50], true), + // -- Second Channel -- + crate::frame!(0xEE, 1, vec![0xDD; 50], false), // Dropped + crate::frame!(0xEE, 2, vec![0xDD; 50], true), // Dropped + // -- Third Channel -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&[&frames[0..4], &frames[6..]].concat()) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_unclosed_channel_with_invalid_start() { + let frames = [ + // -- First Channel -- + crate::frame!(0xEE, 0, vec![0xDD; 50], false), + crate::frame!(0xEE, 1, vec![0xDD; 50], false), + crate::frame!(0xEE, 2, vec![0xDD; 50], false), + crate::frame!(0xEE, 3, vec![0xDD; 50], false), + // -- Next Channel -- + crate::frame!(0xFF, 1, vec![0xDD; 50], false), // Dropped + crate::frame!(0xFF, 2, vec![0xDD; 50], true), // Dropped + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames[0..4]) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_replace_channel() { + let frames = [ + // -- First Channel - VALID & CLOSED -- + crate::frame!(0xDD, 0, vec![0xDD; 50], false), + crate::frame!(0xDD, 1, vec![0xDD; 50], true), + // -- Second Channel - VALID & NOT CLOSED / DROPPED -- + crate::frame!(0xEE, 0, vec![0xDD; 50], false), + crate::frame!(0xEE, 1, vec![0xDD; 50], false), + // -- Third Channel - VALID & CLOSED / REPLACES CHANNEL #2 -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&[&frames[0..2], &frames[4..]].concat()) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_interleaved_invalid_channel() { + let frames = [ + // -- First channel is dropped since it is replaced by the second channel -- + // -- Second channel is dropped since it isn't closed -- + crate::frame!(0x01, 0, vec![0xDD; 50], false), + crate::frame!(0x02, 0, vec![0xDD; 50], false), + crate::frame!(0x01, 1, vec![0xDD; 50], true), + crate::frame!(0x02, 1, vec![0xDD; 50], false), + // -- Third Channel - VALID & CLOSED -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&frames[4..]) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } + + #[tokio::test] + async fn test_holocene_interleaved_valid_channel() { + let frames = [ + // -- First channel is dropped since it is replaced by the second channel -- + // -- Second channel is successfully closed so it's valid -- + crate::frame!(0x01, 0, vec![0xDD; 50], false), + crate::frame!(0x02, 0, vec![0xDD; 50], false), + crate::frame!(0x01, 1, vec![0xDD; 50], true), + crate::frame!(0x02, 1, vec![0xDD; 50], true), + // -- Third Channel - VALID & CLOSED -- + crate::frame!(0xFF, 0, vec![0xDD; 50], false), + crate::frame!(0xFF, 1, vec![0xDD; 50], true), + ]; + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let assert = crate::test_utils::FrameQueueBuilder::new() + .with_rollup_config(&cfg) + .with_origin(BlockInfo::default()) + .with_expected_frames(&[&frames[1..2], &frames[3..]].concat()) + .with_frames(&frames) + .build(); + assert.holocene_active(true); + assert.next_frames().await; + } +} diff --git a/kona/crates/protocol/derive/src/stages/l1_retrieval.rs b/kona/crates/protocol/derive/src/stages/l1_retrieval.rs new file mode 100644 index 0000000000000..f4c7541a955b7 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/l1_retrieval.rs @@ -0,0 +1,252 @@ +//! Contains the [L1Retrieval] stage of the derivation pipeline. + +use crate::{ + ActivationSignal, DataAvailabilityProvider, FrameQueueProvider, OriginAdvancer, OriginProvider, + PipelineError, PipelineErrorKind, PipelineResult, ResetSignal, Signal, SignalReceiver, +}; +use alloc::boxed::Box; +use alloy_primitives::Address; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// Provides L1 blocks for the [`L1Retrieval`] stage. +/// This is the previous stage in the pipeline. +#[async_trait] +pub trait L1RetrievalProvider { + /// Returns the next L1 [`BlockInfo`] in the [`PollingTraversal`] stage, if the stage is not + /// complete. This function can only be called once while the stage is in progress, and will + /// return [`None`] on subsequent calls unless the stage is reset or complete. If the stage + /// is complete and the [`BlockInfo`] has been consumed, an [PipelineError::Eof] error is + /// returned. + /// + /// [`PollingTraversal`]: crate::PollingTraversal + async fn next_l1_block(&mut self) -> PipelineResult>; + + /// Returns the batcher [`Address`] from the [kona_genesis::SystemConfig]. + fn batcher_addr(&self) -> Address; +} + +/// The [`L1Retrieval`] stage of the derivation pipeline. +/// +/// For each L1 [`BlockInfo`] pulled from the [`PollingTraversal`] stage, [`L1Retrieval`] fetches +/// the associated data from a specified [`DataAvailabilityProvider`]. +/// +/// [`PollingTraversal`]: crate::PollingTraversal +#[derive(Debug)] +pub struct L1Retrieval +where + DAP: DataAvailabilityProvider, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver, +{ + /// The previous stage in the pipeline. + pub prev: P, + /// The data availability provider to use for the L1 retrieval stage. + pub provider: DAP, + /// The current block ref. + pub next: Option, +} + +impl L1Retrieval +where + DAP: DataAvailabilityProvider, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver, +{ + /// Creates a new [`L1Retrieval`] stage with the previous [`PollingTraversal`] stage and given + /// [`DataAvailabilityProvider`]. + /// + /// [`PollingTraversal`]: crate::PollingTraversal + pub const fn new(prev: P, provider: DAP) -> Self { + Self { prev, provider, next: None } + } +} + +#[async_trait] +impl OriginAdvancer for L1Retrieval +where + DAP: DataAvailabilityProvider + Send, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.prev.advance_origin().await + } +} + +#[async_trait] +impl FrameQueueProvider for L1Retrieval +where + DAP: DataAvailabilityProvider + Send, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send, +{ + type Item = DAP::Item; + + async fn next_data(&mut self) -> PipelineResult { + if self.next.is_none() { + self.next = Some( + self.prev + .next_l1_block() + .await? // SAFETY: This question mark bubbles up the Eof error. + .ok_or(PipelineError::MissingL1Data.temp())?, + ); + } + // SAFETY: The above check ensures that `next` is not None. + let next = self.next.as_ref().expect("infallible"); + + match self.provider.next(next, self.prev.batcher_addr()).await { + Ok(data) => Ok(data), + Err(e) => { + if let PipelineErrorKind::Temporary(PipelineError::Eof) = e { + self.next = None; + self.provider.clear(); + } + Err(e) + } + } + } +} + +impl OriginProvider for L1Retrieval +where + DAP: DataAvailabilityProvider, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver, +{ + fn origin(&self) -> Option { + self.prev.origin() + } +} + +#[async_trait] +impl SignalReceiver for L1Retrieval +where + DAP: DataAvailabilityProvider + Send, + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + self.prev.signal(signal).await?; + match signal { + Signal::Reset(ResetSignal { l1_origin, .. }) | + Signal::Activation(ActivationSignal { l1_origin, .. }) => { + self.next = Some(l1_origin); + } + _ => {} + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{TestDAP, TraversalTestHelper}; + use alloc::vec; + use alloy_primitives::Bytes; + + #[tokio::test] + async fn test_l1_retrieval_flush_channel() { + let traversal = TraversalTestHelper::new_populated(); + let dap = TestDAP { results: vec![] }; + let mut retrieval = L1Retrieval::new(traversal, dap); + retrieval.prev.block = None; + assert!(retrieval.prev.block.is_none()); + retrieval.next = None; + retrieval.signal(Signal::FlushChannel).await.unwrap(); + assert!(retrieval.next.is_none()); + assert!(retrieval.prev.block.is_none()); + } + + #[tokio::test] + async fn test_l1_retrieval_activation_signal() { + let traversal = TraversalTestHelper::new_populated(); + let dap = TestDAP { results: vec![] }; + let mut retrieval = L1Retrieval::new(traversal, dap); + retrieval.prev.block = None; + assert!(retrieval.prev.block.is_none()); + retrieval.next = None; + retrieval + .signal( + ActivationSignal { system_config: Some(Default::default()), ..Default::default() } + .signal(), + ) + .await + .unwrap(); + assert!(retrieval.next.is_some()); + assert_eq!(retrieval.prev.block, Some(BlockInfo::default())); + } + + #[tokio::test] + async fn test_l1_retrieval_reset_signal() { + let traversal = TraversalTestHelper::new_populated(); + let dap = TestDAP { results: vec![] }; + let mut retrieval = L1Retrieval::new(traversal, dap); + retrieval.prev.block = None; + assert!(retrieval.prev.block.is_none()); + retrieval.next = None; + retrieval + .signal( + ResetSignal { system_config: Some(Default::default()), ..Default::default() } + .signal(), + ) + .await + .unwrap(); + assert!(retrieval.next.is_some()); + assert_eq!(retrieval.prev.block, Some(BlockInfo::default())); + } + + #[tokio::test] + async fn test_l1_retrieval_origin() { + let traversal = TraversalTestHelper::new_populated(); + let dap = TestDAP { results: vec![] }; + let retrieval = L1Retrieval::new(traversal, dap); + let expected = BlockInfo::default(); + assert_eq!(retrieval.origin(), Some(expected)); + } + + #[tokio::test] + async fn test_l1_retrieval_next_data() { + let traversal = TraversalTestHelper::new_populated(); + let results = vec![Err(PipelineError::Eof.temp()), Ok(Bytes::default())]; + let dap = TestDAP { results }; + let mut retrieval = L1Retrieval::new(traversal, dap); + assert_eq!(retrieval.next, None); + let data = retrieval.next_data().await.unwrap(); + assert_eq!(data, Bytes::default()); + } + + #[tokio::test] + async fn test_l1_retrieval_next_data_respect_next() { + let mut traversal = TraversalTestHelper::new_populated(); + traversal.done = true; + let results = vec![Err(PipelineError::Eof.temp()), Ok(Bytes::default())]; + let dap = TestDAP { results }; + let mut retrieval = L1Retrieval::new(traversal, dap); + retrieval.next = Some(BlockInfo::default()); + let data = retrieval.next_data().await.unwrap(); + assert_eq!(data, Bytes::default()); + let err = retrieval.next_data().await.unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + assert!(retrieval.next.is_none()); + } + + #[tokio::test] + async fn test_l1_retrieval_next_data_l1_block_errors() { + let mut traversal = TraversalTestHelper::new_populated(); + traversal.done = true; + let results = vec![Err(PipelineError::Eof.temp()), Ok(Bytes::default())]; + let dap = TestDAP { results }; + let mut retrieval = L1Retrieval::new(traversal, dap); + assert_eq!(retrieval.next, None); + let err = retrieval.next_data().await.unwrap_err(); + assert_eq!(err, PipelineError::Eof.temp()); + assert!(retrieval.next.is_none()); + } + + #[tokio::test] + async fn test_l1_retrieval_existing_data_errors() { + let traversal = TraversalTestHelper::new_populated(); + let dap = TestDAP { results: vec![Err(PipelineError::Eof.temp())] }; + let mut retrieval = + L1Retrieval { prev: traversal, provider: dap, next: Some(BlockInfo::default()) }; + let data = retrieval.next_data().await.unwrap_err(); + assert_eq!(data, PipelineError::Eof.temp()); + assert!(retrieval.next.is_none()); + } +} diff --git a/kona/crates/protocol/derive/src/stages/mod.rs b/kona/crates/protocol/derive/src/stages/mod.rs new file mode 100644 index 0000000000000..db2f23b6871d9 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/mod.rs @@ -0,0 +1,39 @@ +//! This module contains each stage of the derivation pipeline. +//! +//! It offers a high-level API to functionally apply each stage's output as an input to the next +//! stage, until finally arriving at the produced execution payloads. +//! +//! **Stages:** +//! +//! 1. L1 Traversal +//! 2. L1 Retrieval +//! 3. Frame Queue +//! 4. Channel Provider +//! 5. Channel Reader (Batch Decoding) +//! 6. Batch Stream (Introduced in the Holocene Hardfork) +//! 7. Batch Queue +//! 8. Payload Attributes Derivation +//! 9. (Omitted) Engine Queue + +mod traversal; +pub use traversal::{IndexedTraversal, PollingTraversal, TraversalStage}; + +mod l1_retrieval; +pub use l1_retrieval::{L1Retrieval, L1RetrievalProvider}; + +mod frame_queue; +pub use frame_queue::{FrameQueue, FrameQueueProvider}; + +mod channel; +pub use channel::{ + ChannelAssembler, ChannelBank, ChannelProvider, ChannelReader, ChannelReaderProvider, + NextFrameProvider, +}; + +mod batch; +pub use batch::{ + BatchProvider, BatchQueue, BatchStream, BatchStreamProvider, BatchValidator, NextBatchProvider, +}; + +mod attributes_queue; +pub use attributes_queue::AttributesQueue; diff --git a/kona/crates/protocol/derive/src/stages/traversal/indexed.rs b/kona/crates/protocol/derive/src/stages/traversal/indexed.rs new file mode 100644 index 0000000000000..c763788f0fcb3 --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/traversal/indexed.rs @@ -0,0 +1,342 @@ +//! Contains the [`IndexedTraversal`] stage of the derivation pipeline. + +use crate::{ + ActivationSignal, ChainProvider, L1RetrievalProvider, OriginAdvancer, OriginProvider, + PipelineError, PipelineResult, ResetError, ResetSignal, Signal, SignalReceiver, +}; +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::Address; +use async_trait::async_trait; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::BlockInfo; + +/// The [`IndexedTraversal`] stage of the derivation pipeline. +/// +/// This stage sits at the bottom of the pipeline, holding a handle to the data source +/// (a [`ChainProvider`] implementation) and the current L1 [`BlockInfo`] in the pipeline, +/// which are used to traverse the L1 chain. When the [`IndexedTraversal`] stage is advanced, +/// it fetches the next L1 [`BlockInfo`] from the data source and updates the [`SystemConfig`] +/// with the receipts from the block. +#[derive(Debug, Clone)] +pub struct IndexedTraversal { + /// The current block in the traversal stage. + pub block: Option, + /// The data source for the traversal stage. + pub data_source: Provider, + /// Indicates whether the block has been consumed by other stages. + pub done: bool, + /// The system config. + pub system_config: SystemConfig, + /// A reference to the rollup config. + pub rollup_config: Arc, +} + +#[async_trait] +impl L1RetrievalProvider for IndexedTraversal { + fn batcher_addr(&self) -> Address { + self.system_config.batcher_address + } + + async fn next_l1_block(&mut self) -> PipelineResult> { + if !self.done { + self.done = true; + Ok(self.block) + } else { + Err(PipelineError::Eof.temp()) + } + } +} + +impl IndexedTraversal { + /// Creates a new [`IndexedTraversal`] instance. + pub fn new(data_source: F, cfg: Arc) -> Self { + Self { + block: Some(BlockInfo::default()), + data_source, + done: false, + system_config: SystemConfig::default(), + rollup_config: cfg, + } + } + + /// Update the origin block in the traversal stage. + fn update_origin(&mut self, block: BlockInfo) { + self.done = false; + self.block = Some(block); + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_ORIGIN, block.number as f64); + } + + /// Provide the next block to the traversal stage. + async fn provide_next_block(&mut self, block_info: BlockInfo) -> PipelineResult<()> { + if !self.done { + debug!(target: "traversal", "Not finished consuming block, ignoring provided block."); + return Ok(()); + } + let Some(block) = self.block else { + return Err(PipelineError::MissingOrigin.temp()); + }; + if block.number + 1 != block_info.number { + // Safe to ignore. + // The next step will exhaust l1 and get the correct next l1 block. + return Ok(()); + } + if block.hash != block_info.parent_hash { + return Err( + ResetError::NextL1BlockHashMismatch(block.hash, block_info.parent_hash).reset() + ); + } + + // Fetch receipts for the next l1 block and update the system config. + let receipts = + self.data_source.receipts_by_hash(block_info.hash).await.map_err(Into::into)?; + + let addr = self.rollup_config.l1_system_config_address; + let active = self.rollup_config.is_ecotone_active(block_info.timestamp); + match self.system_config.update_with_receipts(&receipts[..], addr, active) { + Ok(true) => { + let next = block_info.number as f64; + kona_macros::set!(gauge, crate::Metrics::PIPELINE_LATEST_SYS_CONFIG_UPDATE, next); + info!(target: "traversal", "System config updated at block {next}."); + } + Ok(false) => { /* Ignore, no update applied */ } + Err(err) => { + error!(target: "traversal", ?err, "Failed to update system config at block {}", block_info.number); + kona_macros::set!( + gauge, + crate::Metrics::PIPELINE_SYS_CONFIG_UPDATE_ERROR, + block_info.number as f64 + ); + return Err(PipelineError::SystemConfigUpdate(err).crit()); + } + } + + // Update the origin block. + self.update_origin(block_info); + + Ok(()) + } +} + +#[async_trait] +impl OriginAdvancer for IndexedTraversal { + async fn advance_origin(&mut self) -> PipelineResult<()> { + if !self.done { + debug!(target: "traversal", "Not finished consuming block, ignoring advance call."); + return Ok(()); + } + return Err(PipelineError::Eof.temp()); + } +} + +impl OriginProvider for IndexedTraversal { + fn origin(&self) -> Option { + self.block + } +} + +#[async_trait] +impl SignalReceiver for IndexedTraversal { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::Reset(ResetSignal { l1_origin, system_config, .. }) | + Signal::Activation(ActivationSignal { l1_origin, system_config, .. }) => { + self.update_origin(l1_origin); + self.system_config = system_config.expect("System config must be provided."); + } + Signal::ProvideBlock(block_info) => self.provide_next_block(block_info).await?, + _ => {} + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{errors::PipelineErrorKind, test_utils::TestChainProvider}; + use alloc::vec; + use alloy_consensus::Receipt; + use alloy_primitives::{B256, Bytes, Log, LogData, address, b256, hex}; + use kona_genesis::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + + const L1_SYS_CONFIG_ADDR: Address = address!("1337000000000000000000000000000000000000"); + + fn new_update_batcher_log() -> Log { + Log { + address: L1_SYS_CONFIG_ADDR, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, // Update type + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + } + } + + fn new_receipts() -> alloc::vec::Vec { + let mut receipt = + Receipt { status: alloy_consensus::Eip658Value::Eip658(true), ..Receipt::default() }; + let bad = Log::new( + Address::from([2; 20]), + vec![CONFIG_UPDATE_TOPIC, B256::default()], + Bytes::default(), + ) + .unwrap(); + receipt.logs = vec![new_update_batcher_log(), bad, new_update_batcher_log()]; + vec![receipt.clone(), Receipt::default(), receipt] + } + + fn new_test_managed( + blocks: alloc::vec::Vec, + receipts: alloc::vec::Vec, + ) -> IndexedTraversal { + let mut provider = TestChainProvider::default(); + let rollup_config = RollupConfig { + l1_system_config_address: L1_SYS_CONFIG_ADDR, + ..RollupConfig::default() + }; + for (i, block) in blocks.iter().enumerate() { + provider.insert_block(i as u64, *block); + } + for (i, receipt) in receipts.iter().enumerate() { + let hash = blocks.get(i).map(|b| b.hash).unwrap_or_default(); + provider.insert_receipts(hash, vec![receipt.clone()]); + } + IndexedTraversal::new(provider, Arc::new(rollup_config)) + } + + fn new_populated_test_managed() -> IndexedTraversal { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = new_receipts(); + new_test_managed(blocks, receipts) + } + + #[test] + fn test_managed_traversal_batcher_address() { + let mut traversal = new_populated_test_managed(); + traversal.system_config.batcher_address = L1_SYS_CONFIG_ADDR; + assert_eq!(traversal.batcher_addr(), L1_SYS_CONFIG_ADDR); + } + + #[tokio::test] + async fn test_managed_traversal_activation_signal() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + let cfg = SystemConfig::default(); + traversal.done = true; + assert!( + traversal + .signal(Signal::Activation(ActivationSignal { + system_config: Some(cfg), + ..Default::default() + })) + .await + .is_ok() + ); + assert_eq!(traversal.origin(), Some(BlockInfo::default())); + assert_eq!(traversal.system_config, cfg); + assert!(!traversal.done); + } + + #[tokio::test] + async fn test_managed_traversal_reset_signal() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + let cfg = SystemConfig::default(); + traversal.done = true; + assert!( + traversal + .signal(Signal::Reset(ResetSignal { + system_config: Some(cfg), + ..Default::default() + })) + .await + .is_ok() + ); + assert_eq!(traversal.origin(), Some(BlockInfo::default())); + assert_eq!(traversal.system_config, cfg); + assert!(!traversal.done); + } + + #[tokio::test] + async fn test_managed_traversal_next_l1_block() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + } + + #[tokio::test] + async fn test_managed_traversal_missing_receipts() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let mut traversal = new_test_managed(blocks, vec![]); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + // provide_next_block will fail due to missing receipts + let next_block = BlockInfo { number: 1, ..BlockInfo::default() }; + let err = traversal.provide_next_block(next_block).await.unwrap_err(); + matches!(err, PipelineErrorKind::Temporary(PipelineError::Provider(_))); + } + + #[tokio::test] + async fn test_managed_traversal_reorgs() { + let hash = b256!("3333333333333333333333333333333333333333333333333333333333333333"); + let block = BlockInfo { hash, number: 0, ..BlockInfo::default() }; + let blocks = vec![block]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + traversal.block = Some(block); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(block)); + // provide_next_block will fail due to hash mismatch (simulate reorg) + let next_block = BlockInfo { number: 1, ..BlockInfo::default() }; + let err = traversal.provide_next_block(next_block).await.unwrap_err(); + assert_eq!(err, ResetError::NextL1BlockHashMismatch(hash, next_block.parent_hash).reset()); + } + + #[tokio::test] + async fn test_managed_traversal_missing_blocks() { + let mut traversal = new_test_managed(vec![], vec![]); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + // provide_next_block will fail due to missing origin + let next_block = BlockInfo { number: 1, ..BlockInfo::default() }; + let err = traversal.provide_next_block(next_block).await.unwrap_err(); + matches!(err, PipelineErrorKind::Temporary(PipelineError::MissingOrigin)); + } + + #[tokio::test] + async fn test_managed_traversal_system_config_update_fails() { + let first = b256!("3333333333333333333333333333333333333333333333333333333333333333"); + let second = b256!("4444444444444444444444444444444444444444444444444444444444444444"); + let block1 = BlockInfo { hash: first, ..BlockInfo::default() }; + let block2 = BlockInfo { number: 1, hash: second, ..BlockInfo::default() }; + let blocks = vec![block1, block2]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + traversal.block = Some(block1); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(block1)); + // provide_next_block will fail due to system config update error + let err = traversal.provide_next_block(block2).await.unwrap_err(); + matches!(err, PipelineErrorKind::Critical(PipelineError::SystemConfigUpdate(_))); + } + + #[tokio::test] + async fn test_managed_traversal_system_config_updated() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = new_receipts(); + let mut traversal = new_test_managed(blocks, receipts); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + // provide_next_block should update system config + let next_block = BlockInfo { number: 1, ..BlockInfo::default() }; + assert!(traversal.provide_next_block(next_block).await.is_ok()); + let expected = address!("000000000000000000000000000000000000bEEF"); + assert_eq!(traversal.system_config.batcher_address, expected); + } +} diff --git a/kona/crates/protocol/derive/src/stages/traversal/mod.rs b/kona/crates/protocol/derive/src/stages/traversal/mod.rs new file mode 100644 index 0000000000000..dac09066187be --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/traversal/mod.rs @@ -0,0 +1,25 @@ +//! Contains various traversal stages for kona's derivation pipeline. +//! +//! The traversal stage sits at the bottom of the pipeline, and is responsible for +//! providing the next block to the next stage in the pipeline. +//! +//! ## Types +//! +//! - [`IndexedTraversal`]: A passive traversal stage that receives the next block through a signal. +//! - [`PollingTraversal`]: An active traversal stage that polls for the next block through its +//! provider. + +mod indexed; +pub use indexed::IndexedTraversal; + +mod polling; +pub use polling::PollingTraversal; + +/// The type of traversal stage used in the derivation pipeline. +#[derive(Debug, Clone)] +pub enum TraversalStage { + /// A passive traversal stage that receives the next block through a signal. + Managed, + /// An active traversal stage that polls for the next block through its provider. + Polling, +} diff --git a/kona/crates/protocol/derive/src/stages/traversal/polling.rs b/kona/crates/protocol/derive/src/stages/traversal/polling.rs new file mode 100644 index 0000000000000..1c6fcdd4c2bdb --- /dev/null +++ b/kona/crates/protocol/derive/src/stages/traversal/polling.rs @@ -0,0 +1,313 @@ +//! Contains the [`PollingTraversal`] stage of the derivation pipeline. + +use crate::{ + ActivationSignal, ChainProvider, L1RetrievalProvider, OriginAdvancer, OriginProvider, + PipelineError, PipelineResult, ResetError, ResetSignal, Signal, SignalReceiver, +}; +use alloc::{boxed::Box, sync::Arc}; +use alloy_primitives::Address; +use async_trait::async_trait; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::BlockInfo; + +/// The [`PollingTraversal`] stage of the derivation pipeline. +/// +/// This stage sits at the bottom of the pipeline, holding a handle to the data source +/// (a [`ChainProvider`] implementation) and the current L1 [`BlockInfo`] in the pipeline, +/// which are used to traverse the L1 chain. When the [`PollingTraversal`] stage is advanced, +/// it fetches the next L1 [`BlockInfo`] from the data source and updates the [`SystemConfig`] +/// with the receipts from the block. +#[derive(Debug, Clone)] +pub struct PollingTraversal { + /// The current block in the traversal stage. + pub block: Option, + /// The data source for the traversal stage. + pub data_source: Provider, + /// Signals whether or not the traversal stage is complete. + pub done: bool, + /// The system config. + pub system_config: SystemConfig, + /// A reference to the rollup config. + pub rollup_config: Arc, +} + +#[async_trait] +impl L1RetrievalProvider for PollingTraversal { + fn batcher_addr(&self) -> Address { + self.system_config.batcher_address + } + + async fn next_l1_block(&mut self) -> PipelineResult> { + if !self.done { + self.done = true; + Ok(self.block) + } else { + Err(PipelineError::Eof.temp()) + } + } +} + +impl PollingTraversal { + /// Creates a new [`PollingTraversal`] instance. + pub fn new(data_source: F, cfg: Arc) -> Self { + Self { + block: Some(BlockInfo::default()), + data_source, + done: false, + system_config: SystemConfig::default(), + rollup_config: cfg, + } + } + + /// Update the origin block in the traversal stage. + fn update_origin(&mut self, block: BlockInfo) { + self.done = false; + self.block = Some(block); + kona_macros::set!(gauge, crate::metrics::Metrics::PIPELINE_ORIGIN, block.number as f64); + } +} + +#[async_trait] +impl OriginAdvancer for PollingTraversal { + /// Advances the internal state of the [`PollingTraversal`] stage to the next L1 block. + /// This function fetches the next L1 [`BlockInfo`] from the data source and updates the + /// [`SystemConfig`] with the receipts from the block. + async fn advance_origin(&mut self) -> PipelineResult<()> { + // Advance start time for metrics. + #[cfg(feature = "metrics")] + let start_time = std::time::Instant::now(); + + // Pull the next block or return EOF. + // PipelineError::EOF has special handling further up the pipeline. + let block = match self.block { + Some(block) => block, + None => { + warn!(target: "l1_traversal", "Missing current block, can't advance origin with no reference."); + return Err(PipelineError::Eof.temp()); + } + }; + let next_l1_origin = + self.data_source.block_info_by_number(block.number + 1).await.map_err(Into::into)?; + + // Check block hashes for reorgs. + if block.hash != next_l1_origin.parent_hash { + return Err(ResetError::ReorgDetected(block.hash, next_l1_origin.parent_hash).into()); + } + + // Fetch receipts for the next l1 block and update the system config. + let receipts = + self.data_source.receipts_by_hash(next_l1_origin.hash).await.map_err(Into::into)?; + + let addr = self.rollup_config.l1_system_config_address; + let active = self.rollup_config.is_ecotone_active(next_l1_origin.timestamp); + match self.system_config.update_with_receipts(&receipts[..], addr, active) { + Ok(true) => { + let next = next_l1_origin.number as f64; + kona_macros::set!(gauge, crate::Metrics::PIPELINE_LATEST_SYS_CONFIG_UPDATE, next); + info!(target: "l1_traversal", "System config updated at block {next}."); + } + Ok(false) => { /* Ignore, no update applied */ } + Err(err) => { + error!(target: "l1_traversal", ?err, "Failed to update system config at block {}", next_l1_origin.number); + kona_macros::set!( + gauge, + crate::Metrics::PIPELINE_SYS_CONFIG_UPDATE_ERROR, + next_l1_origin.number as f64 + ); + return Err(PipelineError::SystemConfigUpdate(err).crit()); + } + } + + let prev_block_holocene = self.rollup_config.is_holocene_active(block.timestamp); + let next_block_holocene = self.rollup_config.is_holocene_active(next_l1_origin.timestamp); + + // Update the block origin regardless of if a holocene activation is required. + self.update_origin(next_l1_origin); + + // Record the origin as advanced. + #[cfg(feature = "metrics")] + { + let duration = start_time.elapsed(); + kona_macros::record!( + histogram, + crate::metrics::Metrics::PIPELINE_ORIGIN_ADVANCE, + duration.as_secs_f64() + ); + } + + // If the prev block is not holocene, but the next is, we need to flag this + // so the pipeline driver will reset the pipeline for holocene activation. + if !prev_block_holocene && next_block_holocene { + return Err(ResetError::HoloceneActivation.reset()); + } + + Ok(()) + } +} + +impl OriginProvider for PollingTraversal { + fn origin(&self) -> Option { + self.block + } +} + +#[async_trait] +impl SignalReceiver for PollingTraversal { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::Reset(ResetSignal { l1_origin, system_config, .. }) | + Signal::Activation(ActivationSignal { l1_origin, system_config, .. }) => { + self.update_origin(l1_origin); + self.system_config = system_config.expect("System config must be provided."); + } + Signal::ProvideBlock(_) => { + /* Not supported in this stage. */ + warn!(target: "traversal", "ProvideBlock signal not supported in PollingTraversal stage."); + return Err(PipelineError::UnsupportedSignal.temp()); + } + _ => {} + } + + Ok(()) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{errors::PipelineErrorKind, test_utils::TraversalTestHelper}; + use alloc::vec; + use alloy_primitives::{address, b256}; + + #[test] + fn test_l1_traversal_batcher_address() { + let mut traversal = TraversalTestHelper::new_populated(); + traversal.system_config.batcher_address = TraversalTestHelper::L1_SYS_CONFIG_ADDR; + assert_eq!(traversal.batcher_addr(), TraversalTestHelper::L1_SYS_CONFIG_ADDR); + } + + #[tokio::test] + async fn test_l1_traversal_flush_channel() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert!(traversal.advance_origin().await.is_ok()); + traversal.done = true; + assert!(traversal.signal(Signal::FlushChannel).await.is_ok()); + assert_eq!(traversal.origin(), Some(BlockInfo::default())); + assert!(traversal.done); + } + + #[tokio::test] + async fn test_l1_traversal_activation_signal() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert!(traversal.advance_origin().await.is_ok()); + let cfg = SystemConfig::default(); + traversal.done = true; + assert!( + traversal + .signal( + ActivationSignal { system_config: Some(cfg), ..Default::default() }.signal() + ) + .await + .is_ok() + ); + assert_eq!(traversal.origin(), Some(BlockInfo::default())); + assert_eq!(traversal.system_config, cfg); + assert!(!traversal.done); + } + + #[tokio::test] + async fn test_l1_traversal_reset_signal() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert!(traversal.advance_origin().await.is_ok()); + let cfg = SystemConfig::default(); + traversal.done = true; + assert!( + traversal + .signal(ResetSignal { system_config: Some(cfg), ..Default::default() }.signal()) + .await + .is_ok() + ); + assert_eq!(traversal.origin(), Some(BlockInfo::default())); + assert_eq!(traversal.system_config, cfg); + assert!(!traversal.done); + } + + #[tokio::test] + async fn test_l1_traversal() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + assert!(traversal.advance_origin().await.is_ok()); + } + + #[tokio::test] + async fn test_l1_traversal_missing_receipts() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, vec![]); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + matches!( + traversal.advance_origin().await.unwrap_err(), + PipelineErrorKind::Temporary(PipelineError::Provider(_)) + ); + } + + #[tokio::test] + async fn test_l1_traversal_reorgs() { + let hash = b256!("3333333333333333333333333333333333333333333333333333333333333333"); + let block = BlockInfo { hash, ..BlockInfo::default() }; + let blocks = vec![block, block]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert!(traversal.advance_origin().await.is_ok()); + let err = traversal.advance_origin().await.unwrap_err(); + assert_eq!(err, ResetError::ReorgDetected(block.hash, block.parent_hash).into()); + } + + #[tokio::test] + async fn test_l1_traversal_missing_blocks() { + let mut traversal = TraversalTestHelper::new_from_blocks(vec![], vec![]); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + matches!( + traversal.advance_origin().await.unwrap_err(), + PipelineErrorKind::Temporary(PipelineError::Provider(_)) + ); + } + + #[tokio::test] + async fn test_l1_traversal_system_config_update_fails() { + let first = b256!("3333333333333333333333333333333333333333333333333333333333333333"); + let second = b256!("4444444444444444444444444444444444444444444444444444444444444444"); + let block1 = BlockInfo { hash: first, ..BlockInfo::default() }; + let block2 = BlockInfo { hash: second, ..BlockInfo::default() }; + let blocks = vec![block1, block2]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert!(traversal.advance_origin().await.is_ok()); + // Only the second block should fail since the second receipt + // contains invalid logs that will error for a system config update. + let err = traversal.advance_origin().await.unwrap_err(); + matches!(err, PipelineErrorKind::Critical(PipelineError::SystemConfigUpdate(_))); + } + + #[tokio::test] + async fn test_l1_traversal_system_config_updated() { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = TraversalTestHelper::new_receipts(); + let mut traversal = TraversalTestHelper::new_from_blocks(blocks, receipts); + assert_eq!(traversal.next_l1_block().await.unwrap(), Some(BlockInfo::default())); + assert_eq!(traversal.next_l1_block().await.unwrap_err(), PipelineError::Eof.temp()); + assert!(traversal.advance_origin().await.is_ok()); + let expected = address!("000000000000000000000000000000000000bEEF"); + assert_eq!(traversal.system_config.batcher_address, expected); + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/attributes_queue.rs b/kona/crates/protocol/derive/src/test_utils/attributes_queue.rs new file mode 100644 index 0000000000000..91eb407ba6162 --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/attributes_queue.rs @@ -0,0 +1,101 @@ +//! Testing utilities for the attributes queue stage. + +use crate::{ + errors::{BuilderError, PipelineError, PipelineErrorKind}, + traits::{ + AttributesBuilder, AttributesProvider, OriginAdvancer, OriginProvider, SignalReceiver, + }, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, string::ToString, vec::Vec}; +use alloy_eips::BlockNumHash; +use async_trait::async_trait; +use kona_protocol::{BlockInfo, L2BlockInfo, SingleBatch}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use thiserror::Error; + +/// An error returned by the [`TestAttributesBuilder`]. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum TestAttributesBuilderError {} + +/// A mock implementation of the [`AttributesBuilder`] for testing. +#[derive(Debug, Default)] +pub struct TestAttributesBuilder { + /// The attributes to return. + pub attributes: Vec>, +} + +#[async_trait] +impl AttributesBuilder for TestAttributesBuilder { + /// Prepares the [`OpPayloadAttributes`] for the next payload. + async fn prepare_payload_attributes( + &mut self, + _l2_parent: L2BlockInfo, + _epoch: BlockNumHash, + ) -> PipelineResult { + match self.attributes.pop() { + Some(Ok(attrs)) => Ok(attrs), + Some(Err(err)) => { + Err(PipelineErrorKind::Temporary(BuilderError::Custom(err.to_string()).into())) + } + None => Err(PipelineErrorKind::Critical(BuilderError::AttributesUnavailable.into())), + } + } +} + +/// A mock implementation of the [`AttributesProvider`] stage for testing. +#[derive(Debug, Default)] +pub struct TestAttributesProvider { + /// The origin of the L1 block. + origin: Option, + /// A list of batches to return. + batches: Vec>, + /// Tracks if the provider has been reset. + pub reset: bool, + /// Tracks if the provider has been flushed. + pub flushed: bool, +} + +impl OriginProvider for TestAttributesProvider { + fn origin(&self) -> Option { + self.origin + } +} + +#[async_trait] +impl OriginAdvancer for TestAttributesProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait] +impl SignalReceiver for TestAttributesProvider { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::FlushChannel => self.flushed = true, + Signal::Reset { .. } => self.reset = true, + _ => {} + } + Ok(()) + } +} + +#[async_trait] +impl AttributesProvider for TestAttributesProvider { + async fn next_batch(&mut self, _parent: L2BlockInfo) -> PipelineResult { + self.batches.pop().ok_or(PipelineError::Eof.temp())? + } + + fn is_last_in_span(&self) -> bool { + self.batches.is_empty() + } +} + +/// Creates a new [`TestAttributesProvider`] with the given origin and batches. +pub const fn new_test_attributes_provider( + origin: Option, + batches: Vec>, +) -> TestAttributesProvider { + TestAttributesProvider { origin, batches, reset: false, flushed: false } +} diff --git a/kona/crates/protocol/derive/src/test_utils/batch_provider.rs b/kona/crates/protocol/derive/src/test_utils/batch_provider.rs new file mode 100644 index 0000000000000..aa2c1f5c3367f --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/batch_provider.rs @@ -0,0 +1,75 @@ +//! A mock implementation of the [`NextBatchProvider`] stage for testing. + +use crate::{ + errors::PipelineError, + stages::NextBatchProvider, + traits::{OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, vec::Vec}; +use async_trait::async_trait; +use kona_protocol::{Batch, BlockInfo, L2BlockInfo}; + +/// A mock provider for the [`NextBatchProvider`] stage. +#[derive(Debug, Default)] +pub struct TestNextBatchProvider { + /// The origin of the L1 block. + pub origin: Option, + /// A list of batches to return. + pub batches: Vec>, + /// Tracks if the provider has been flushed. + pub flushed: bool, + /// Tracks if the reset method was called. + pub reset: bool, +} + +impl TestNextBatchProvider { + /// Creates a new [`TestNextBatchProvider`] with the given origin and batches. + pub fn new(batches: Vec>) -> Self { + Self { origin: Some(BlockInfo::default()), batches, flushed: false, reset: false } + } +} + +impl OriginProvider for TestNextBatchProvider { + fn origin(&self) -> Option { + self.origin + } +} + +#[async_trait] +impl NextBatchProvider for TestNextBatchProvider { + fn flush(&mut self) { + self.flushed = true; + } + + fn span_buffer_size(&self) -> usize { + self.batches.len() + } + + async fn next_batch(&mut self, _: L2BlockInfo, _: &[BlockInfo]) -> PipelineResult { + self.batches.pop().ok_or(PipelineError::Eof.temp())? + } +} + +#[async_trait] +impl OriginAdvancer for TestNextBatchProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.origin = self.origin.map(|mut origin| { + origin.number += 1; + origin + }); + Ok(()) + } +} + +#[async_trait] +impl SignalReceiver for TestNextBatchProvider { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::Reset { .. } => self.reset = true, + Signal::FlushChannel => self.flushed = true, + _ => {} + } + Ok(()) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/batch_stream.rs b/kona/crates/protocol/derive/src/test_utils/batch_stream.rs new file mode 100644 index 0000000000000..7172be604cd2d --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/batch_stream.rs @@ -0,0 +1,67 @@ +//! A mock implementation of the [`BatchStream`] stage for testing. +//! +//! [`BatchStream`]: crate::stages::BatchStream + +use crate::{ + BatchStreamProvider, OriginAdvancer, OriginProvider, PipelineError, PipelineResult, Signal, + SignalReceiver, +}; +use alloc::{boxed::Box, vec::Vec}; +use async_trait::async_trait; +use kona_protocol::{Batch, BlockInfo}; + +/// A mock provider for the [`BatchStream`] stage. +/// +/// [`BatchStream`]: crate::stages::BatchStream +#[derive(Debug, Default)] +pub struct TestBatchStreamProvider { + /// The origin of the L1 block. + pub origin: Option, + /// A list of batches to return. + pub batches: Vec>, + /// Whether the reset method was called. + pub reset: bool, + /// Whether the provider was flushed. + pub flushed: bool, +} + +impl TestBatchStreamProvider { + /// Creates a new [`TestBatchStreamProvider`] with the given origin and batches. + pub fn new(batches: Vec>) -> Self { + Self { origin: Some(BlockInfo::default()), batches, reset: false, flushed: false } + } +} + +impl OriginProvider for TestBatchStreamProvider { + fn origin(&self) -> Option { + self.origin + } +} + +#[async_trait] +impl BatchStreamProvider for TestBatchStreamProvider { + fn flush(&mut self) {} + + async fn next_batch(&mut self) -> PipelineResult { + self.batches.pop().ok_or(PipelineError::Eof.temp())? + } +} + +#[async_trait] +impl OriginAdvancer for TestBatchStreamProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait] +impl SignalReceiver for TestBatchStreamProvider { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::Reset { .. } => self.reset = true, + Signal::FlushChannel => self.flushed = true, + _ => {} + } + Ok(()) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/blob_provider.rs b/kona/crates/protocol/derive/src/test_utils/blob_provider.rs new file mode 100644 index 0000000000000..df3e2d9f04981 --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/blob_provider.rs @@ -0,0 +1,51 @@ +//! An implementation of the [BlobProvider] trait for tests. + +use crate::{BlobProvider, errors::BlobProviderError}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_eips::eip4844::{Blob, IndexedBlobHash}; +use alloy_primitives::{B256, map::HashMap}; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// A mock blob provider for testing. +#[derive(Debug, Clone, Default)] +pub struct TestBlobProvider { + /// Maps block hashes to blob data. + pub blobs: HashMap, + /// whether the blob provider should return an error. + pub should_error: bool, +} + +impl TestBlobProvider { + /// Insert a blob into the mock blob provider. + pub fn insert_blob(&mut self, hash: B256, blob: Blob) { + self.blobs.insert(hash, blob); + } + + /// Clears blobs from the mock blob provider. + pub fn clear(&mut self) { + self.blobs.clear(); + } +} + +#[async_trait] +impl BlobProvider for TestBlobProvider { + type Error = BlobProviderError; + + async fn get_and_validate_blobs( + &mut self, + _block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result>, Self::Error> { + if self.should_error { + return Err(BlobProviderError::SlotDerivation); + } + let mut blobs = Vec::new(); + for blob_hash in blob_hashes { + if let Some(data) = self.blobs.get(&blob_hash.hash) { + blobs.push(Box::new(*data)); + } + } + Ok(blobs) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/chain_providers.rs b/kona/crates/protocol/derive/src/test_utils/chain_providers.rs new file mode 100644 index 0000000000000..090eeb9804ffc --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/chain_providers.rs @@ -0,0 +1,215 @@ +//! Test Utilities for chain provider traits + +use crate::{ + errors::{PipelineError, PipelineErrorKind}, + traits::{ChainProvider, L2ChainProvider}, +}; +use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec}; +use alloy_consensus::{Header, Receipt, TxEnvelope}; +use alloy_primitives::{B256, map::HashMap}; +use async_trait::async_trait; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, BlockInfo, L2BlockInfo}; +use op_alloy_consensus::OpBlock; +use thiserror::Error; + +/// A mock chain provider for testing. +#[derive(Debug, Clone, Default)] +pub struct TestChainProvider { + /// Maps block numbers to block information using a tuple list. + pub blocks: Vec<(u64, BlockInfo)>, + /// Maps block hashes to header information using a tuple list. + pub headers: Vec<(B256, Header)>, + /// Maps block hashes to receipts using a tuple list. + pub receipts: Vec<(B256, Vec)>, + /// Maps block hashes to transactions using a tuple list. + pub transactions: Vec<(B256, Vec)>, +} + +impl TestChainProvider { + /// Insert a block into the mock chain provider. + pub fn insert_block(&mut self, number: u64, block: BlockInfo) { + self.blocks.push((number, block)); + } + + /// Insert a block with transactions into the mock chain provider. + pub fn insert_block_with_transactions( + &mut self, + number: u64, + block: BlockInfo, + txs: Vec, + ) { + self.blocks.push((number, block)); + self.transactions.push((block.hash, txs)); + } + + /// Insert receipts into the mock chain provider. + pub fn insert_receipts(&mut self, hash: B256, receipts: Vec) { + self.receipts.push((hash, receipts)); + } + + /// Insert a header into the mock chain provider. + pub fn insert_header(&mut self, hash: B256, header: Header) { + self.headers.push((hash, header)); + } + + /// Clears headers from the mock chain provider. + pub fn clear_headers(&mut self) { + self.headers.clear(); + } + + /// Clears blocks from the mock chain provider. + pub fn clear_blocks(&mut self) { + self.blocks.clear(); + } + + /// Clears receipts from the mock chain provider. + pub fn clear_receipts(&mut self) { + self.receipts.clear(); + } + + /// Clears all blocks and receipts from the mock chain provider. + pub fn clear(&mut self) { + self.clear_blocks(); + self.clear_receipts(); + self.clear_headers(); + } +} + +/// An error for the [`TestChainProvider`] and [`TestL2ChainProvider`]. +#[derive(Error, Debug)] +pub enum TestProviderError { + /// The block was not found. + #[error("Block not found")] + BlockNotFound, + /// The header was not found. + #[error("Header not found")] + HeaderNotFound, + /// The receipts were not found. + #[error("Receipts not found")] + ReceiptsNotFound, + /// The L2 block was not found. + #[error("L2 Block not found")] + L2BlockNotFound, + /// The system config was not found. + #[error("System config not found")] + SystemConfigNotFound(u64), +} + +impl From for PipelineErrorKind { + fn from(val: TestProviderError) -> Self { + PipelineError::Provider(val.to_string()).temp() + } +} + +#[async_trait] +impl ChainProvider for TestChainProvider { + type Error = TestProviderError; + + async fn header_by_hash(&mut self, hash: B256) -> Result { + if let Some((_, header)) = self.headers.iter().find(|(_, b)| b.hash_slow() == hash) { + Ok(header.clone()) + } else { + Err(TestProviderError::HeaderNotFound) + } + } + + async fn block_info_by_number(&mut self, _number: u64) -> Result { + if let Some((_, block)) = self.blocks.iter().find(|(n, _)| *n == _number) { + Ok(*block) + } else { + Err(TestProviderError::BlockNotFound) + } + } + + async fn receipts_by_hash(&mut self, _hash: B256) -> Result, Self::Error> { + if let Some((_, receipts)) = self.receipts.iter().find(|(h, _)| *h == _hash) { + Ok(receipts.clone()) + } else { + Err(TestProviderError::ReceiptsNotFound) + } + } + + async fn block_info_and_transactions_by_hash( + &mut self, + hash: B256, + ) -> Result<(BlockInfo, Vec), Self::Error> { + let block = self + .blocks + .iter() + .find(|(_, b)| b.hash == hash) + .map(|(_, b)| *b) + .ok_or_else(|| TestProviderError::BlockNotFound)?; + let txs = self + .transactions + .iter() + .find(|(h, _)| *h == hash) + .map(|(_, txs)| txs.clone()) + .unwrap_or_default(); + Ok((block, txs)) + } +} + +/// An [`L2ChainProvider`] implementation for testing. +#[derive(Debug, Default, Clone)] +pub struct TestL2ChainProvider { + /// Blocks + pub blocks: Vec, + /// Short circuit the block return to be the first block. + pub short_circuit: bool, + /// Blocks + pub op_blocks: Vec, + /// System configs + pub system_configs: HashMap, +} + +impl TestL2ChainProvider { + /// Creates a new [`TestL2ChainProvider`] with the given origin and batches. + pub const fn new( + blocks: Vec, + op_blocks: Vec, + system_configs: HashMap, + ) -> Self { + Self { blocks, short_circuit: false, op_blocks, system_configs } + } +} + +#[async_trait] +impl BatchValidationProvider for TestL2ChainProvider { + type Error = TestProviderError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + if self.short_circuit { + return self.blocks.first().copied().ok_or_else(|| TestProviderError::BlockNotFound); + } + self.blocks + .iter() + .find(|b| b.block_info.number == number) + .cloned() + .ok_or_else(|| TestProviderError::BlockNotFound) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + self.op_blocks + .iter() + .find(|p| p.header.number == number) + .cloned() + .ok_or_else(|| TestProviderError::L2BlockNotFound) + } +} + +#[async_trait] +impl L2ChainProvider for TestL2ChainProvider { + type Error = TestProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + _: Arc, + ) -> Result::Error> { + self.system_configs + .get(&number) + .ok_or_else(|| TestProviderError::SystemConfigNotFound(number)) + .cloned() + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/channel_provider.rs b/kona/crates/protocol/derive/src/test_utils/channel_provider.rs new file mode 100644 index 0000000000000..74211a1e5a7ee --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/channel_provider.rs @@ -0,0 +1,65 @@ +//! Mock testing utilities for the [ChannelBank] stage. +//! +//! [ChannelBank]: crate::stages::ChannelBank + +use crate::{ + errors::PipelineError, + stages::NextFrameProvider, + traits::{OriginAdvancer, OriginProvider, SignalReceiver}, + types::{PipelineResult, Signal}, +}; +use alloc::{boxed::Box, vec::Vec}; +use async_trait::async_trait; +use kona_protocol::{BlockInfo, Frame}; + +/// A mock [`NextFrameProvider`] for testing the [`ChannelBank`] stage. +/// +/// [`ChannelBank`]: crate::stages::ChannelBank +#[derive(Debug, Default)] +pub struct TestNextFrameProvider { + /// The data to return. + pub data: Vec>, + /// The block info + pub block_info: Option, + /// Tracks if the channel bank provider has been reset. + pub reset: bool, +} + +impl TestNextFrameProvider { + /// Creates a new [`TestNextFrameProvider`] with the given data. + pub fn new(data: Vec>) -> Self { + Self { data, block_info: Some(BlockInfo::default()), reset: false } + } +} + +impl OriginProvider for TestNextFrameProvider { + fn origin(&self) -> Option { + self.block_info + } +} + +#[async_trait] +impl OriginAdvancer for TestNextFrameProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + self.block_info = self.block_info.map(|mut bi| { + bi.number += 1; + bi + }); + Ok(()) + } +} + +#[async_trait] +impl NextFrameProvider for TestNextFrameProvider { + async fn next_frame(&mut self) -> PipelineResult { + self.data.pop().unwrap_or(Err(PipelineError::Eof.temp())) + } +} + +#[async_trait] +impl SignalReceiver for TestNextFrameProvider { + async fn signal(&mut self, _: Signal) -> PipelineResult<()> { + self.reset = true; + Ok(()) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/channel_reader.rs b/kona/crates/protocol/derive/src/test_utils/channel_reader.rs new file mode 100644 index 0000000000000..5c3f23d1afba8 --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/channel_reader.rs @@ -0,0 +1,60 @@ +//! Test utilities for the [ChannelReader] stage. +//! +//! [ChannelReader]: crate::stages::ChannelReader + +use crate::{ + ChannelReaderProvider, OriginAdvancer, OriginProvider, PipelineError, PipelineResult, Signal, + SignalReceiver, +}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_primitives::Bytes; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// A mock [`ChannelReaderProvider`] for testing the [`ChannelReader`] stage. +/// +/// [`ChannelReader`]: crate::stages::ChannelReader +#[derive(Debug, Default)] +pub struct TestChannelReaderProvider { + /// The data to return. + pub data: Vec>>, + /// The origin block info + pub block_info: Option, + /// Tracks if the channel reader provider has been reset. + pub reset: bool, +} + +impl TestChannelReaderProvider { + /// Creates a new [`TestChannelReaderProvider`] with the given data. + pub fn new(data: Vec>>) -> Self { + Self { data, block_info: Some(BlockInfo::default()), reset: false } + } +} + +impl OriginProvider for TestChannelReaderProvider { + fn origin(&self) -> Option { + self.block_info + } +} + +#[async_trait] +impl OriginAdvancer for TestChannelReaderProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait] +impl ChannelReaderProvider for TestChannelReaderProvider { + async fn next_data(&mut self) -> PipelineResult> { + self.data.pop().unwrap_or(Err(PipelineError::Eof.temp())) + } +} + +#[async_trait] +impl SignalReceiver for TestChannelReaderProvider { + async fn signal(&mut self, _: Signal) -> PipelineResult<()> { + self.reset = true; + Ok(()) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/data_availability_provider.rs b/kona/crates/protocol/derive/src/test_utils/data_availability_provider.rs new file mode 100644 index 0000000000000..d4445e71750fd --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/data_availability_provider.rs @@ -0,0 +1,28 @@ +//! An implementation of the [DataAvailabilityProvider] trait for tests. + +use crate::{errors::PipelineError, traits::DataAvailabilityProvider, types::PipelineResult}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_primitives::{Address, Bytes}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_protocol::BlockInfo; + +/// Mock data availability provider +#[derive(Debug, Default)] +pub struct TestDAP { + /// Specifies the stage results. + pub results: Vec>, +} + +#[async_trait] +impl DataAvailabilityProvider for TestDAP { + type Item = Bytes; + + async fn next(&mut self, _: &BlockInfo, _: Address) -> PipelineResult { + self.results.pop().unwrap_or(Err(PipelineError::Eof.temp())) + } + + fn clear(&mut self) { + self.results.clear(); + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/frame_queue.rs b/kona/crates/protocol/derive/src/test_utils/frame_queue.rs new file mode 100644 index 0000000000000..79e5bddecafed --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/frame_queue.rs @@ -0,0 +1,65 @@ +//! Mock types for the frame queue stage. + +use crate::{ + FrameQueueProvider, OriginAdvancer, OriginProvider, PipelineError, PipelineResult, Signal, + SignalReceiver, +}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_primitives::Bytes; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// A mock [`FrameQueueProvider`] for testing the frame queue stage. +/// +/// [`FrameQueue`]: crate::stages::FrameQueue +#[derive(Debug, Default)] +pub struct TestFrameQueueProvider { + /// The data to return. + pub data: Vec>, + /// The origin to return. + pub origin: Option, + /// Whether the reset method was called. + pub reset: bool, +} + +impl TestFrameQueueProvider { + /// Creates a new [`TestFrameQueueProvider`] with the given data. + pub const fn new(data: Vec>) -> Self { + Self { data, origin: None, reset: false } + } + + /// Sets the origin for the [`TestFrameQueueProvider`]. + pub const fn set_origin(&mut self, origin: BlockInfo) { + self.origin = Some(origin); + } +} + +impl OriginProvider for TestFrameQueueProvider { + fn origin(&self) -> Option { + self.origin + } +} + +#[async_trait] +impl OriginAdvancer for TestFrameQueueProvider { + async fn advance_origin(&mut self) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait] +impl FrameQueueProvider for TestFrameQueueProvider { + type Item = Bytes; + + async fn next_data(&mut self) -> PipelineResult { + self.data.pop().unwrap_or(Err(PipelineError::Eof.temp())) + } +} + +#[async_trait] +impl SignalReceiver for TestFrameQueueProvider { + async fn signal(&mut self, _: Signal) -> PipelineResult<()> { + self.reset = true; + Ok(()) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/frames.rs b/kona/crates/protocol/derive/src/test_utils/frames.rs new file mode 100644 index 0000000000000..aee7e783624dc --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/frames.rs @@ -0,0 +1,132 @@ +//! Frames + +use crate::{ + FrameQueue, NextFrameProvider, OriginProvider, PipelineError, PipelineErrorKind, + test_utils::TestFrameQueueProvider, +}; +use alloc::{sync::Arc, vec, vec::Vec}; +use alloy_primitives::Bytes; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, DERIVATION_VERSION_0, Frame}; + +/// A [`FrameQueue`] builder. +#[derive(Debug, Default)] +pub struct FrameQueueBuilder { + origin: Option, + config: Option, + mock: Option, + expected_frames: Vec, + expected_err: Option, +} + +fn encode_frames(frames: &[Frame]) -> Bytes { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[DERIVATION_VERSION_0]); + for frame in frames.iter() { + bytes.extend_from_slice(&frame.encode()); + } + Bytes::from(bytes) +} + +impl FrameQueueBuilder { + /// Create a new [`FrameQueueBuilder`] instance. + pub const fn new() -> Self { + Self { origin: None, config: None, mock: None, expected_frames: vec![], expected_err: None } + } + + /// Sets the rollup config. + pub fn with_rollup_config(mut self, config: &RollupConfig) -> Self { + self.config = Some(config.clone()); + self + } + + /// Set the origin block. + pub const fn with_origin(mut self, origin: BlockInfo) -> Self { + self.origin = Some(origin); + self + } + + /// With expected frames. + pub fn with_expected_frames(mut self, frames: &[Frame]) -> Self { + self.expected_frames = frames.to_vec(); + self + } + + /// Sets the expected error type. + pub fn with_expected_err(mut self, err: PipelineErrorKind) -> Self { + self.expected_err = Some(err); + self + } + + /// With raw frames. + pub fn with_raw_frames(mut self, raw: Bytes) -> Self { + let mock = self.mock.unwrap_or_else(|| TestFrameQueueProvider::new(vec![Ok(raw)])); + self.mock = Some(mock); + self + } + + /// Adds frames to the mock provider. + pub fn with_frames(mut self, frames: &[Frame]) -> Self { + let encoded = encode_frames(frames); + let mock = self.mock.unwrap_or_else(|| TestFrameQueueProvider::new(vec![Ok(encoded)])); + self.mock = Some(mock); + self + } + + /// Build the [`FrameQueue`]. + pub fn build(self) -> FrameQueueAsserter { + let mut mock = self.mock.unwrap_or_else(|| TestFrameQueueProvider::new(vec![])); + if let Some(origin) = self.origin { + mock.set_origin(origin); + } + let config = self.config.unwrap_or_default(); + let config = Arc::new(config); + let err = self.expected_err.unwrap_or_else(|| PipelineError::Eof.temp()); + FrameQueueAsserter::new(FrameQueue::new(mock, config), self.expected_frames, err) + } +} + +/// The [`FrameQueueAsserter`] validates frame queue outputs. +#[derive(Debug)] +pub struct FrameQueueAsserter { + inner: FrameQueue, + expected_frames: Vec, + expected_err: PipelineErrorKind, +} + +impl FrameQueueAsserter { + /// Create a new [`FrameQueueAsserter`] instance. + pub const fn new( + inner: FrameQueue, + expected_frames: Vec, + expected_err: PipelineErrorKind, + ) -> Self { + Self { inner, expected_frames, expected_err } + } + + /// Asserts that holocene is active. + pub fn holocene_active(&self, active: bool) { + let holocene = self.inner.is_holocene_active(self.inner.origin().unwrap_or_default()); + if !active { + assert!(!holocene); + } else { + assert!(holocene); + } + } + + /// Asserts that the frame queue returns with a missing origin error. + pub async fn missing_origin(mut self) { + let err = self.inner.next_frame().await.unwrap_err(); + assert_eq!(err, PipelineError::MissingOrigin.crit()); + } + + /// Asserts that the frame queue produces the expected frames. + pub async fn next_frames(mut self) { + for eframe in self.expected_frames.into_iter() { + let frame = self.inner.next_frame().await.expect("unexpected frame"); + assert_eq!(frame, eframe); + } + let err = self.inner.next_frame().await.unwrap_err(); + assert_eq!(err, self.expected_err); + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/macros.rs b/kona/crates/protocol/derive/src/test_utils/macros.rs new file mode 100644 index 0000000000000..4fbe58e5ff7cd --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/macros.rs @@ -0,0 +1,19 @@ +//! Macros used across test utilities. + +/// A shorthand syntax for constructing [kona_protocol::Frame]s. +#[macro_export] +macro_rules! frame { + ($id:expr, $number:expr, $data:expr, $is_last:expr) => { + kona_protocol::Frame { id: [$id; 16], number: $number, data: $data, is_last: $is_last } + }; +} + +/// A shorthand syntax for constructing a list of [kona_protocol::Frame]s. +#[macro_export] +macro_rules! frames { + ($id:expr, $number:expr, $data:expr, $count:expr) => {{ + let mut frames = vec![$crate::frame!($id, $number, $data, false); $count]; + frames[$count - 1].is_last = true; + frames + }}; +} diff --git a/kona/crates/protocol/derive/src/test_utils/mod.rs b/kona/crates/protocol/derive/src/test_utils/mod.rs new file mode 100644 index 0000000000000..55b14938c5deb --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/mod.rs @@ -0,0 +1,52 @@ +//! Test Utilities for `kona-derive`. + +mod pipeline; +pub use pipeline::{ + TestAttributesQueue, TestBatchProvider, TestBatchStream, TestChannelProvider, + TestChannelReader, TestFrameQueue, TestL1Retrieval, TestNextAttributes, TestPipeline, + TestPollingTraversal, new_test_pipeline, +}; + +mod traversal; +pub use traversal::TraversalTestHelper; + +mod blob_provider; +pub use blob_provider::TestBlobProvider; + +mod chain_providers; +pub use chain_providers::{TestChainProvider, TestL2ChainProvider, TestProviderError}; + +mod data_availability_provider; +pub use data_availability_provider::TestDAP; + +mod batch_provider; +pub use batch_provider::TestNextBatchProvider; + +mod attributes_queue; +pub use attributes_queue::{ + TestAttributesBuilder, TestAttributesBuilderError, TestAttributesProvider, + new_test_attributes_provider, +}; + +mod batch_stream; +pub use batch_stream::TestBatchStreamProvider; + +mod channel_provider; +pub use channel_provider::TestNextFrameProvider; + +mod channel_reader; +pub use channel_reader::TestChannelReaderProvider; + +mod frame_queue; +pub use frame_queue::TestFrameQueueProvider; + +mod tracing; +pub use tracing::{CollectingLayer, TraceStorage}; + +mod sys_config_fetcher; +pub use sys_config_fetcher::{TestSystemConfigL2Fetcher, TestSystemConfigL2FetcherError}; + +mod frames; +pub use frames::{FrameQueueAsserter, FrameQueueBuilder}; + +mod macros; diff --git a/kona/crates/protocol/derive/src/test_utils/pipeline.rs b/kona/crates/protocol/derive/src/test_utils/pipeline.rs new file mode 100644 index 0000000000000..e1b968cd91ecc --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/pipeline.rs @@ -0,0 +1,96 @@ +//! Test Utilities for the [`DerivationPipeline`] +//! as well as its stages and providers. + +use crate::{ + BatchProvider, PipelineResult, + test_utils::{TestChainProvider, TestL2ChainProvider}, +}; +use alloc::{boxed::Box, sync::Arc}; +use kona_genesis::RollupConfig; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; + +// Re-export these types used internally to the test pipeline. +use crate::{ + AttributesQueue, BatchStream, ChannelProvider, ChannelReader, DerivationPipeline, FrameQueue, + L1Retrieval, NextAttributes, OriginAdvancer, OriginProvider, PipelineBuilder, PipelineError, + PollingTraversal, Signal, SignalReceiver, + test_utils::{TestAttributesBuilder, TestDAP}, +}; + +/// A fully custom [`NextAttributes`]. +#[derive(Default, Debug, Clone)] +pub struct TestNextAttributes { + /// The next [`OpAttributesWithParent`] to return. + pub next_attributes: Option, +} + +#[async_trait::async_trait] +impl SignalReceiver for TestNextAttributes { + /// Resets the derivation stage to its initial state. + async fn signal(&mut self, _: Signal) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait::async_trait] +impl OriginProvider for TestNextAttributes { + /// Returns the current origin. + fn origin(&self) -> Option { + Some(BlockInfo::default()) + } +} + +#[async_trait::async_trait] +impl OriginAdvancer for TestNextAttributes { + /// Advances the origin to the given block. + async fn advance_origin(&mut self) -> PipelineResult<()> { + Ok(()) + } +} + +#[async_trait::async_trait] +impl NextAttributes for TestNextAttributes { + /// Returns the next valid [`OpAttributesWithParent`]. + async fn next_attributes(&mut self, _: L2BlockInfo) -> PipelineResult { + self.next_attributes.take().ok_or(PipelineError::Eof.temp()) + } +} + +/// A [`PollingTraversal`] using test providers and sources. +pub type TestPollingTraversal = PollingTraversal; + +/// An [`L1Retrieval`] stage using test providers and sources. +pub type TestL1Retrieval = L1Retrieval; + +/// A [`FrameQueue`] using test providers and sources. +pub type TestFrameQueue = FrameQueue; + +/// A [`ChannelProvider`] using test providers and sources. +pub type TestChannelProvider = ChannelProvider; + +/// A [`ChannelReader`] using test providers and sources. +pub type TestChannelReader = ChannelReader; + +/// A [`BatchStream`] using test providers and sources. +pub type TestBatchStream = BatchStream; + +/// A [`BatchProvider`] using test providers and sources. +pub type TestBatchProvider = BatchProvider; + +/// An [`AttributesQueue`] using test providers and sources. +pub type TestAttributesQueue = AttributesQueue; + +/// A [`DerivationPipeline`] using test providers and sources. +pub type TestPipeline = DerivationPipeline; + +/// Constructs a [`DerivationPipeline`] using test providers and sources. +pub fn new_test_pipeline() -> TestPipeline { + PipelineBuilder::new() + .rollup_config(Arc::new(RollupConfig::default())) + .origin(BlockInfo::default()) + .dap_source(TestDAP::default()) + .builder(TestAttributesBuilder::default()) + .chain_provider(TestChainProvider::default()) + .l2_chain_provider(TestL2ChainProvider::default()) + .build_polled() +} diff --git a/kona/crates/protocol/derive/src/test_utils/sys_config_fetcher.rs b/kona/crates/protocol/derive/src/test_utils/sys_config_fetcher.rs new file mode 100644 index 0000000000000..65f1d7d121aca --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/sys_config_fetcher.rs @@ -0,0 +1,75 @@ +//! Implements a mock [`L2ChainProvider`] and [`BatchValidationProvider`] for testing. + +use crate::{ + errors::{PipelineError, PipelineErrorKind}, + traits::L2ChainProvider, +}; +use alloc::{boxed::Box, string::ToString, sync::Arc}; +use alloy_primitives::map::HashMap; +use async_trait::async_trait; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, L2BlockInfo}; +use op_alloy_consensus::OpBlock; +use thiserror::Error; + +/// A mock implementation of the [`L2ChainProvider`] and [`BatchValidationProvider`] for testing. +#[derive(Debug, Default)] +pub struct TestSystemConfigL2Fetcher { + /// A map from [u64] block number to a [`SystemConfig`]. + pub system_configs: HashMap, +} + +impl TestSystemConfigL2Fetcher { + /// Inserts a new system config into the mock fetcher with the given block number. + pub fn insert(&mut self, number: u64, config: SystemConfig) { + self.system_configs.insert(number, config); + } + + /// Clears all system configs from the mock fetcher. + pub fn clear(&mut self) { + self.system_configs.clear(); + } +} + +/// An error returned by the [`TestSystemConfigL2Fetcher`]. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum TestSystemConfigL2FetcherError { + /// The system config was not found. + #[error("system config not found: {0}")] + NotFound(u64), +} + +impl From for PipelineErrorKind { + fn from(val: TestSystemConfigL2FetcherError) -> Self { + PipelineError::Provider(val.to_string()).temp() + } +} + +#[async_trait] +impl BatchValidationProvider for TestSystemConfigL2Fetcher { + type Error = TestSystemConfigL2FetcherError; + + async fn block_by_number(&mut self, _: u64) -> Result { + unimplemented!() + } + + async fn l2_block_info_by_number(&mut self, _: u64) -> Result { + unimplemented!() + } +} + +#[async_trait] +impl L2ChainProvider for TestSystemConfigL2Fetcher { + type Error = TestSystemConfigL2FetcherError; + + async fn system_config_by_number( + &mut self, + number: u64, + _: Arc, + ) -> Result::Error> { + self.system_configs + .get(&number) + .cloned() + .ok_or_else(|| TestSystemConfigL2FetcherError::NotFound(number)) + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/tracing.rs b/kona/crates/protocol/derive/src/test_utils/tracing.rs new file mode 100644 index 0000000000000..dec0208410512 --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/tracing.rs @@ -0,0 +1,57 @@ +//! This module contains a subscriber layer for `tracing-subscriber` that collects traces and their +//! log levels. + +use alloc::{format, string::String, sync::Arc, vec::Vec}; +use spin::Mutex; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::{Layer, layer::Context}; + +/// The storage for the collected traces. +#[derive(Debug, Default, Clone)] +pub struct TraceStorage(pub Arc>>); + +impl TraceStorage { + /// Returns the items in the storage that match the specified level. + pub fn get_by_level(&self, level: Level) -> Vec { + self.0 + .lock() + .iter() + .filter_map(|(l, message)| if *l == level { Some(message.clone()) } else { None }) + .collect() + } + + /// Locks the storage and returns the items. + pub fn lock(&self) -> spin::MutexGuard<'_, Vec<(Level, String)>> { + self.0.lock() + } + + /// Returns if the storage is empty. + pub fn is_empty(&self) -> bool { + self.0.lock().is_empty() + } +} + +/// A subscriber layer that collects traces and their log levels. +#[derive(Debug, Default)] +pub struct CollectingLayer { + /// The storage for the collected traces. + pub storage: TraceStorage, +} + +impl CollectingLayer { + /// Creates a new collecting layer with the specified storage. + pub const fn new(storage: TraceStorage) -> Self { + Self { storage } + } +} + +impl Layer for CollectingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = *metadata.level(); + let message = format!("{event:?}"); + + let mut storage = self.storage.0.lock(); + storage.push((level, message)); + } +} diff --git a/kona/crates/protocol/derive/src/test_utils/traversal.rs b/kona/crates/protocol/derive/src/test_utils/traversal.rs new file mode 100644 index 0000000000000..811fd30602846 --- /dev/null +++ b/kona/crates/protocol/derive/src/test_utils/traversal.rs @@ -0,0 +1,73 @@ +//! Contains helper methods for testing the traversal stages in the pipeline. + +use crate::{PollingTraversal, test_utils::TestChainProvider}; +use alloc::{sync::Arc, vec}; +use alloy_consensus::Receipt; +use alloy_primitives::{Address, B256, Bytes, Log, LogData, address, hex}; +use kona_genesis::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC, RollupConfig}; +use kona_protocol::BlockInfo; + +/// [`TraversalTestHelper`] encapsulates useful testing methods for traversal stages. +#[derive(Debug, Clone)] +pub struct TraversalTestHelper; + +impl TraversalTestHelper { + /// The address of the l1 system config contract. + pub const L1_SYS_CONFIG_ADDR: Address = address!("1337000000000000000000000000000000000000"); + + /// Creates a new [`Log`] for the update batcher event. + pub fn new_update_batcher_log() -> Log { + Log { + address: Self::L1_SYS_CONFIG_ADDR, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, // Update type + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + } + } + + /// Creates a new [`Receipt`] with the update batcher log and a bad log. + pub fn new_receipts() -> alloc::vec::Vec { + let mut receipt = + Receipt { status: alloy_consensus::Eip658Value::Eip658(true), ..Receipt::default() }; + let bad = Log::new( + Address::from([2; 20]), + vec![CONFIG_UPDATE_TOPIC, B256::default()], + Bytes::default(), + ) + .unwrap(); + receipt.logs = vec![Self::new_update_batcher_log(), bad, Self::new_update_batcher_log()]; + vec![receipt.clone(), Receipt::default(), receipt] + } + + /// Creates a new [`PollingTraversal`] with the given blocks and receipts. + pub fn new_from_blocks( + blocks: alloc::vec::Vec, + receipts: alloc::vec::Vec, + ) -> PollingTraversal { + let mut provider = TestChainProvider::default(); + let rollup_config = RollupConfig { + l1_system_config_address: Self::L1_SYS_CONFIG_ADDR, + ..RollupConfig::default() + }; + for (i, block) in blocks.iter().enumerate() { + provider.insert_block(i as u64, *block); + } + for (i, receipt) in receipts.iter().enumerate() { + let hash = blocks.get(i).map(|b| b.hash).unwrap_or_default(); + provider.insert_receipts(hash, vec![receipt.clone()]); + } + PollingTraversal::new(provider, Arc::new(rollup_config)) + } + + /// Creates a new [`PollingTraversal`] with two default blocks and populated receipts. + pub fn new_populated() -> PollingTraversal { + let blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let receipts = Self::new_receipts(); + Self::new_from_blocks(blocks, receipts) + } +} diff --git a/kona/crates/protocol/derive/src/traits/attributes.rs b/kona/crates/protocol/derive/src/traits/attributes.rs new file mode 100644 index 0000000000000..1f02848b16419 --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/attributes.rs @@ -0,0 +1,51 @@ +//! Contains traits for working with payload attributes and their providers. + +use core::fmt::Debug; + +use crate::PipelineResult; +use alloc::boxed::Box; +use alloy_eips::BlockNumHash; +use async_trait::async_trait; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent, SingleBatch}; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// [`AttributesProvider`] is a trait abstraction that generalizes the [`BatchQueue`] stage. +/// +/// [`BatchQueue`]: crate::stages::BatchQueue +#[async_trait] +pub trait AttributesProvider { + /// Returns the next valid batch upon the given safe head. + async fn next_batch(&mut self, parent: L2BlockInfo) -> PipelineResult; + + /// Returns whether the current batch is the last in its span. + fn is_last_in_span(&self) -> bool; +} + +/// [`NextAttributes`] defines the interface for pulling attributes from +/// the top level `AttributesQueue` stage of the pipeline. +#[async_trait] +pub trait NextAttributes { + /// Returns the next [`OpAttributesWithParent`] from the current batch. + async fn next_attributes( + &mut self, + parent: L2BlockInfo, + ) -> PipelineResult; +} + +/// The [`AttributesBuilder`] is responsible for preparing [`OpPayloadAttributes`] +/// that can be used to construct an L2 Block containing only deposits. +#[async_trait] +pub trait AttributesBuilder: Debug + Send { + /// Prepares a template [`OpPayloadAttributes`] that is ready to be used to build an L2 + /// block. The block will contain deposits only, on top of the given L2 parent, with the L1 + /// origin set to the given epoch. + /// By default, the [`OpPayloadAttributes`] template will have `no_tx_pool` set to true, + /// and no sequencer transactions. The caller has to modify the template to add transactions. + /// This can be done by either setting the `no_tx_pool` to false as sequencer, or by appending + /// batch transactions as the verifier. + async fn prepare_payload_attributes( + &mut self, + l2_parent: L2BlockInfo, + epoch: BlockNumHash, + ) -> PipelineResult; +} diff --git a/kona/crates/protocol/derive/src/traits/data_sources.rs b/kona/crates/protocol/derive/src/traits/data_sources.rs new file mode 100644 index 0000000000000..6368a72fe0492 --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/data_sources.rs @@ -0,0 +1,43 @@ +//! Contains traits that describe the functionality of various data sources used in the derivation +//! pipeline's stages. + +use crate::{PipelineErrorKind, PipelineResult}; +use alloc::{boxed::Box, fmt::Debug, string::ToString, vec::Vec}; +use alloy_eips::eip4844::{Blob, IndexedBlobHash}; +use alloy_primitives::{Address, Bytes}; +use async_trait::async_trait; +use core::fmt::Display; +use kona_protocol::BlockInfo; + +/// The BlobProvider trait specifies the functionality of a data source that can provide blobs. +#[async_trait] +pub trait BlobProvider { + /// The error type for the [`BlobProvider`]. + type Error: Display + ToString + Into; + + /// Fetches blobs for a given block ref and the blob hashes. + async fn get_and_validate_blobs( + &mut self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result>, Self::Error>; +} + +/// Describes the functionality of a data source that can provide data availability information. +#[async_trait] +pub trait DataAvailabilityProvider { + /// The item type of the data iterator. + type Item: Send + Sync + Debug + Into; + + /// Returns the next data for the given [`BlockInfo`], looking for transactions sent by the + /// `batcher_addr`. Returns a `PipelineError::Eof` if there is no more data for the given + /// block ref. + async fn next( + &mut self, + block_ref: &BlockInfo, + batcher_addr: Address, + ) -> PipelineResult; + + /// Clears the data source for the next block ref. + fn clear(&mut self); +} diff --git a/kona/crates/protocol/derive/src/traits/mod.rs b/kona/crates/protocol/derive/src/traits/mod.rs new file mode 100644 index 0000000000000..91f24c10fda1f --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/mod.rs @@ -0,0 +1,20 @@ +//! This module contains all of the traits describing functionality of portions of the derivation +//! pipeline. + +mod pipeline; +pub use pipeline::Pipeline; + +mod providers; +pub use providers::{BatchValidationProviderDerive, ChainProvider, L2ChainProvider}; + +mod attributes; +pub use attributes::{AttributesBuilder, AttributesProvider, NextAttributes}; + +mod data_sources; +pub use data_sources::{BlobProvider, DataAvailabilityProvider}; + +mod reset; +pub use reset::ResetProvider; + +mod stages; +pub use stages::{OriginAdvancer, OriginProvider, SignalReceiver}; diff --git a/kona/crates/protocol/derive/src/traits/pipeline.rs b/kona/crates/protocol/derive/src/traits/pipeline.rs new file mode 100644 index 0000000000000..6d5c9a6374aec --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/pipeline.rs @@ -0,0 +1,28 @@ +//! Defines the interface for the core derivation pipeline. + +use alloc::boxed::Box; +use async_trait::async_trait; +use core::iter::Iterator; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; + +use crate::{OriginProvider, PipelineErrorKind, StepResult}; + +/// This trait defines the interface for interacting with the derivation pipeline. +#[async_trait] +pub trait Pipeline: OriginProvider + Iterator { + /// Peeks at the next [`OpAttributesWithParent`] from the pipeline. + fn peek(&self) -> Option<&OpAttributesWithParent>; + + /// Attempts to progress the pipeline. + async fn step(&mut self, cursor: L2BlockInfo) -> StepResult; + + /// Returns the rollup config. + fn rollup_config(&self) -> &RollupConfig; + + /// Returns the [`SystemConfig`] by L2 number. + async fn system_config_by_number( + &mut self, + number: u64, + ) -> Result; +} diff --git a/kona/crates/protocol/derive/src/traits/providers.rs b/kona/crates/protocol/derive/src/traits/providers.rs new file mode 100644 index 0000000000000..4e13d5d0f6ba7 --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/providers.rs @@ -0,0 +1,61 @@ +//! Chain providers for the derivation pipeline. + +use crate::PipelineErrorKind; +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloy_consensus::{Header, Receipt, TxEnvelope}; +use alloy_primitives::B256; +use async_trait::async_trait; +use core::fmt::Display; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, BlockInfo}; + +/// Describes the functionality of a data source that can provide information from the blockchain. +#[async_trait] +pub trait ChainProvider { + /// The error type for the [`ChainProvider`]. + type Error: Display + Into; + + /// Fetch the L1 [`Header`] for the given [`B256`] hash. + async fn header_by_hash(&mut self, hash: B256) -> Result; + + /// Returns the block at the given number, or an error if the block does not exist in the data + /// source. + async fn block_info_by_number(&mut self, number: u64) -> Result; + + /// Returns all receipts in the block with the given hash, or an error if the block does not + /// exist in the data source. + async fn receipts_by_hash(&mut self, hash: B256) -> Result, Self::Error>; + + /// Returns the [`BlockInfo`] and list of [`TxEnvelope`]s from the given block hash. + async fn block_info_and_transactions_by_hash( + &mut self, + hash: B256, + ) -> Result<(BlockInfo, Vec), Self::Error>; +} + +/// Describes the functionality of a data source that fetches safe blocks. +#[async_trait] +pub trait L2ChainProvider: BatchValidationProviderDerive { + /// The error type for the [`L2ChainProvider`]. + type Error: Display + Into; + + /// Returns the [`SystemConfig`] by L2 number. + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error>; +} + +/// A super-trait for [`BatchValidationProvider`] that binds `Self::Error` to have a conversion into +/// [`PipelineErrorKind`]. +pub trait BatchValidationProviderDerive: BatchValidationProvider {} + +// Auto-implement the [BatchValidationProviderDerive] trait for all types that implement +// [BatchValidationProvider] where the error can be converted into [PipelineErrorKind]. +impl BatchValidationProviderDerive for T +where + T: BatchValidationProvider, + ::Error: Into, +{ +} diff --git a/kona/crates/protocol/derive/src/traits/reset.rs b/kona/crates/protocol/derive/src/traits/reset.rs new file mode 100644 index 0000000000000..cd506ceb49347 --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/reset.rs @@ -0,0 +1,16 @@ +//! Traits for resetting stages. + +use alloc::boxed::Box; +use async_trait::async_trait; +use kona_genesis::SystemConfig; +use kona_protocol::BlockInfo; + +/// Provides the [`BlockInfo`] and [`SystemConfig`] for the stack to reset the stages. +#[async_trait] +pub trait ResetProvider { + /// Returns the current [`BlockInfo`] for the pipeline to reset. + async fn block_info(&self) -> BlockInfo; + + /// Returns the current [`SystemConfig`] for the pipeline to reset. + async fn system_config(&self) -> SystemConfig; +} diff --git a/kona/crates/protocol/derive/src/traits/stages.rs b/kona/crates/protocol/derive/src/traits/stages.rs new file mode 100644 index 0000000000000..6cb4b82cba273 --- /dev/null +++ b/kona/crates/protocol/derive/src/traits/stages.rs @@ -0,0 +1,28 @@ +//! This module contains common traits for stages within the derivation pipeline. + +use alloc::boxed::Box; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +use crate::{PipelineResult, Signal}; + +/// Providers a way for the pipeline to accept a signal from the driver. +#[async_trait] +pub trait SignalReceiver { + /// Receives a signal from the driver. + async fn signal(&mut self, signal: Signal) -> PipelineResult<()>; +} + +/// Provides a method for accessing the pipeline's current L1 origin. +pub trait OriginProvider { + /// Returns the optional L1 [`BlockInfo`] origin. + fn origin(&self) -> Option; +} + +/// Defines a trait for advancing the L1 origin of the pipeline. +#[async_trait] +pub trait OriginAdvancer { + /// Advances the internal state of the lowest stage to the next l1 origin. + /// This method is the equivalent of the reference implementation `advance_l1_block`. + async fn advance_origin(&mut self) -> PipelineResult<()>; +} diff --git a/kona/crates/protocol/derive/src/types/mod.rs b/kona/crates/protocol/derive/src/types/mod.rs new file mode 100644 index 0000000000000..2ab302ab2575d --- /dev/null +++ b/kona/crates/protocol/derive/src/types/mod.rs @@ -0,0 +1,7 @@ +//! Primitive types for `kona-derive`. + +mod results; +pub use results::{PipelineResult, StepResult}; + +mod signals; +pub use signals::{ActivationSignal, ResetSignal, Signal}; diff --git a/kona/crates/protocol/derive/src/types/results.rs b/kona/crates/protocol/derive/src/types/results.rs new file mode 100644 index 0000000000000..a701c8796618a --- /dev/null +++ b/kona/crates/protocol/derive/src/types/results.rs @@ -0,0 +1,19 @@ +//! Result types for the `kona-derive` pipeline. + +use crate::PipelineErrorKind; + +/// A result type for the derivation pipeline stages. +pub type PipelineResult = Result; + +/// A pipeline error. +#[derive(Debug, PartialEq, Eq)] +pub enum StepResult { + /// Attributes were successfully prepared. + PreparedAttributes, + /// Origin was advanced. + AdvancedOrigin, + /// Origin advance failed. + OriginAdvanceErr(PipelineErrorKind), + /// Step failed. + StepFailed(PipelineErrorKind), +} diff --git a/kona/crates/protocol/derive/src/types/signals.rs b/kona/crates/protocol/derive/src/types/signals.rs new file mode 100644 index 0000000000000..0f91294aa8f9f --- /dev/null +++ b/kona/crates/protocol/derive/src/types/signals.rs @@ -0,0 +1,127 @@ +//! Signal types for the `kona-derive` pipeline. +//! +//! Signals are the primary method of communication in the downwards direction +//! of the pipeline. They allow the pipeline driver to perform actions such as +//! resetting all stages in the pipeline through message passing. + +use kona_genesis::SystemConfig; +use kona_protocol::{BlockInfo, L2BlockInfo}; + +/// A signal to send to the pipeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Signal { + /// Reset the pipeline. + Reset(ResetSignal), + /// Hardfork Activation. + Activation(ActivationSignal), + /// Flush the currently active channel. + FlushChannel, + /// Provide a new L1 block to the L1 traversal stage. + ProvideBlock(BlockInfo), +} + +impl core::fmt::Display for Signal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Reset(_) => write!(f, "reset"), + Self::Activation(_) => write!(f, "activation"), + Self::FlushChannel => write!(f, "flush_channel"), + Self::ProvideBlock(_) => write!(f, "provide_block"), + } + } +} + +impl Signal { + /// Sets the [`SystemConfig`] for the signal. + pub const fn with_system_config(self, system_config: SystemConfig) -> Self { + match self { + Self::Reset(reset) => reset.with_system_config(system_config).signal(), + Self::Activation(activation) => activation.with_system_config(system_config).signal(), + Self::FlushChannel => Self::FlushChannel, + Self::ProvideBlock(block) => Self::ProvideBlock(block), + } + } +} + +/// A pipeline reset signal. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct ResetSignal { + /// The L2 safe head to reset to. + pub l2_safe_head: L2BlockInfo, + /// The L1 origin to reset to. + pub l1_origin: BlockInfo, + /// The optional [`SystemConfig`] to reset with. + pub system_config: Option, +} + +impl ResetSignal { + /// Creates a new [Signal::Reset] from the [`ResetSignal`]. + pub const fn signal(self) -> Signal { + Signal::Reset(self) + } + + /// Sets the [`SystemConfig`] for the signal. + pub const fn with_system_config(self, system_config: SystemConfig) -> Self { + Self { system_config: Some(system_config), ..self } + } +} + +/// A pipeline hardfork activation signal. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct ActivationSignal { + /// The L2 safe head to reset to. + pub l2_safe_head: L2BlockInfo, + /// The L1 origin to reset to. + pub l1_origin: BlockInfo, + /// The optional [`SystemConfig`] to reset with. + pub system_config: Option, +} + +impl ActivationSignal { + /// Creates a new [Signal::Activation] from the [`ActivationSignal`]. + pub const fn signal(self) -> Signal { + Signal::Activation(self) + } + + /// Sets the [`SystemConfig`] for the signal. + pub const fn with_system_config(self, system_config: SystemConfig) -> Self { + Self { system_config: Some(system_config), ..self } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reset_signal() { + let signal = ResetSignal::default(); + assert_eq!(signal.signal(), Signal::Reset(signal)); + } + + #[test] + fn test_activation_signal() { + let signal = ActivationSignal::default(); + assert_eq!(signal.signal(), Signal::Activation(signal)); + } + + #[test] + fn test_signal_with_system_config() { + let signal = ResetSignal::default(); + let system_config = SystemConfig::default(); + assert_eq!( + signal.with_system_config(system_config).signal(), + Signal::Reset(ResetSignal { system_config: Some(system_config), ..signal }) + ); + + let signal = ActivationSignal::default(); + let system_config = SystemConfig::default(); + assert_eq!( + signal.with_system_config(system_config).signal(), + Signal::Activation(ActivationSignal { system_config: Some(system_config), ..signal }) + ); + + assert_eq!(Signal::FlushChannel.with_system_config(system_config), Signal::FlushChannel); + } +} diff --git a/kona/crates/protocol/derive/testdata/batch.hex b/kona/crates/protocol/derive/testdata/batch.hex new file mode 100644 index 0000000000000..f85db4153e011 --- /dev/null +++ b/kona/crates/protocol/derive/testdata/batch.hex @@ -0,0 +1 @@ +78daccccc37628800100d1d8b66ddb4e5e6cdbb66ddbb66ddbb66ddb664fffa18bce6a56b71335a009b035b509be326d12b213fb6c6e578ce41c29be52f3a83fd009e7589c586a7d33a4afea79b3b4e83179690577837c746a2a0e50df1f82201482201080f0bf034010088102050505030503070906070507030b010b030501090b838c00010f030f0d8d008c000d060e0b0d8b880086050d03068a0c018e040f0f0d09040d8d0e0d0f0d0b8186040f010f090d0749090d0d0e094901010a8a86020e0981864a0305024b0f0f0d818905028f000b058e0405810203060e0605060f820406010604010101090c03050a098e028588080901010d0df0ff1ad0ffc0206b666f4f70aabce3d704a6aa2d483fa9f6dc97c1c875839635aad64da8d13799402f315c4638f4aa668f70e5be0dabe414555e0a26397aa6677cbdebb870c547d15b7057f208fd134c2792829f46cfca730ff61ae98c9ce1e6f3b55a78d3ff672f7d547a9c770702465b9459df0f43712ebd0927cbd859352c8d51a416ddabc504fbb2d20be72283d12930fda6f075a496f1269ab70967638f0be1cd861b9688c458f62bf777c5eba910183dac2ca8ba0ddceea022370b5348a029dbd5eab85c6f60cf78d048eb1d92a091ceeade1085f58be262208a7a0d69ffec511845040bd173c37eb1574ed46145a54da9d4b8d23e050e4017062badfc43f4d8cdfd550499c2086cbd1451710e1e7dd785b0f83b4cfeb1654fd004e1be30528f213099a653bd82f506fe3297f82a482549d8681cd1d540dca50af874563b64604d52d3e3ee65c30d94f22605ad62c0451bf478e8601d66b1ec7bf6e32928544a1ea8d6bcf94fdc544cd877aec6f8048d202f257af9743fb011e65230f981cb8b0295448023e3f720d58535e24a3c5c0a4b10adc0ddc1fd8e4ab4fa2b4036676df50424c3a8415a030f83325a5d0c2ebd705b5b08dadd9f3380d5ce28faa8fcccd9f684f5fba05694c316b7abd3d86a4ddbdf1d05281c131068cd8e600032f4a838dafa296c318f927db33d59c2d691b4bcf78b9f9cf63283377d2b2446268ac50bf8b1c983cf203a20329dc82921d7596f58ac2cf68d9e338d33efef3e81e5195a80070a4eb8066a1c91f18274b7550fce3ae60d2c16bac423c1d35baadbce6159f057daffbe139f25e8805e580e427acb96b351c2ca7319149c86c39d8a1594dc710400de6917b38e6cf8c74f026807d2d649ad6301aaeb5fdc31b602a1397d8b6c6866a67bee25857fa3eed5db61aa9db9e1fcfd541bda389a3e0a0f6871385158c4a05fbc8bb1d10f0663af5742205a33cbb748df336b86fcc3c6e46a79f58c847e1a7ffa18bad31a8431b3933dbddc621a33176f5ff9a6f79fd839cbdaaef7c0b374bba4e392e04ce1ee6a65b3d338cca70184db6ff2ce67cb9e7c4be1ec3faac58a1713ecdc95c872bb9e3d85fa1033878ad2a3a7a9b2d583fd808447e6520c2e93223fb332f5a0c5e3738de7dfe160a21dfe781bb8bb05e94f6b9c02a3119a12b22c5915eecf893ac9fa2e04e368dd4aa113c1137123af46f6d7a4fd50f7394c667f95284b4856d686411bfddee26654ef08e00afbb74921ba65b2daa952719a76cd3492ea485c4a689732dea23ae14c6a4dc69233653a07fe1ad802c0da96a1a14cb0c0c6ec6fabfc7e9aba1636a74d0a865257c6565efe2f684441ce7627406f403b3a72c1f40bbcf80dee28113d0b41a9b48ca14ffa14670e050db153061aa5eb5484059201c41131384ab8d2fd9bc1e75f713e3edf736c415c20a9783af8b2892f2bb3f22e0e5c131b71e3ac8114a3fdf7b3f9fe3d2cd2614c6bb8af67f7639bfed2d6d4800a4f2019bb5f06bda939a832aa08ff50c1633da7487e4e37f2523bce44689ea6fae38ac4e4ae0c55a0ebe8fcb6379b833fe326d23ff511e0d787314f30beca2cb67b75761977d02fcfdfb78c707f3cb485cf2ca534800832c0403e57634b93855c43a1368348df35a3b7e61651a688f49f9ddd44c56ca2902df236050c94298f522deee5662b1a5e64a16c6621d5bb7145c0dde4b7167e97289db803065df321918cce87a3140cd03ce591d37ef11de2d06a86038250444537b10a749fc1c467aa79702b16ce477b1f09a395571155c5da1666fc600958d1d506b47ff36d30622415ec4e9b3247de33ce1a8b5251a1454cdeb70c5f2aa9fad1d0fb39fc63e2eaba294359c52656b171213b73e04a604e4188ebdf6692cd54ba391befc2a0d9fadb96a47fb22af1e2229e7209dc8c3d0a5201e4096621940500c50b47963c1ad08e838345feb1b4972a9bdf7d7319f1671f8256db7a2e513a37ac3c71e4871b3fa384768aab21e84b43d8d732a14cc54a56ac640410aa7177006055982811ba68d5d12e466563916b6f387cd64fd842279550d793bd464144b9e8aadbd625ba011b3b238a022cce16cfb66a17e0ccab5eeb9377320e35672cab1accd0c179a16ea1f7c8de3f5b345516b3711d53a566955f4768d17fb779b82bb923d9360c10105ea21ce1c6cfe10a6ac5be194412b4fe6fe3cfd4276b5770f60516b0beb568ca42ceda51d722251717698b2e6d91c32256dbf77299240492a8546b3601bd88b7537230df8662773e58f83783693dae50aaeae9db47701a8f9aa53f5272d6b80687993c6223bd0d2f2bbed43889145e09f586ed565a5c97c63df19fa9f7e3991db55a019048a2fc1415b82f1833c3cf86f2cdcbca05251ffb23592f0d2afc2d173625571916f20495d7202ac0e46cabb3c3926bd09c7d10b0707c59d810a8d9139ae1adb4703f9c514856a195bcbd018f1e85bf384f5e5979b18eddb7c60c8afa56d7baf937be1fa2287f17c9cfcad4f18e8e2f4dcb8b5480bb3d1ef037a8d9aa7099b05b1def88393611ea16f7f66edadff20f6116ddc766046b694751ce7e54fe1befa489f36373705183cb51ae70e3f26485c0b07379aacde0776be541504bc7099426456481f9c382cfa8dc48d2a2bf5f58bbaedadb5fe63fea92ededdcf7d7cbcef141ec4f132e01db6e1becde49546c44defa1a8b92e41e4dc94e4cfe342bf831ba7e4d865dcd7be0f5796cef9bfee744509ebf1aa7d35cba24ed1961f73c6193ec17fb48bef5ee6cb99a662687e8415c95c6a18608d946fd0722572c6005dc95f9b2038e689e7eb5d4293b22718254acf6a5957ca699a93392329d735aa2f8046c22fc5f4e87a88fb85b17af16ac9e94f9b3d1eb18a0bc0f5aba0a5ec4dddd684db339963ab8981ef2722d9e8f2dfc946d12c809d766e9e08d7b480d2adeede777dfb7f82d81c973b9248ba484c260abb101e52ebcbac00a97560944ac47ef3084e19077a95df47ac688421feb3e57ecd10dd665f9e581f22742d5d5e6b3e963ac3fe1c8260ba815d1d7fab916b83193fabe9a7598ca1d932786e9612a76aa6ccf3e3407c4584d737dc7d8b8f3e182747979c6c60cf0447f523451447091ebe9dfb078b0da7ef3b5c63d685a7bde2cef89aa14b342e637ccfbb484ecc817d7a42353f36e320a0999a0a60270a969f3bf041e15f47e6a4593a99fd9fe030b17b25fbdcb9951ddc5b7050b5a760a1c7fc5c985f39720e8e8d54e050c145b890207e67385539d41203ecf4938dda7b5eac1248297b9b3946738d399c2f6513c253547fc33978147d0e1f07a827bf7c45a564450b81920a5d50a739df98e1ff30be68b6ccb0aa2e23d8faa400c1f4fd8ddefbfd85a7a0337cbdcef927af509e4f5441e266457a245d904cbe9e3c79da3979ca6191cffa00053db64387f953133c76be9bf12d3eb4884ff69dad1c78d9f4661894c2916171f499d17bee5dc281fb12e0b2c01ed7cd760831f84dc0ea378f6b992284a6d7027998a35434a93cfffdb8742961cd5568e89fb9d94cbb313f00808cbd62801bcdd462a389ba53dc0782ec26eda4de14ef7cdc87e2f972191398839933e9a96e651b650828d719a1410ac162e047717f742ba68df5a431cc9b6922e11b7e6c95fdaf69aea51a8ba9a2ee2c742683043aa8b3c20cb478d1c2046ee3ebb5276fa95b934fd892895d3236b531ee05e61abcc215cb406e41c473e9652b942e3971efb3b28add1283ebb9f18c4794ccca24709b374219de3c8a2f1b16bfe9706567da301ae6282e4b31767b5dbed7633bdaea0ca759b9160bc0985b56544b817f65e46ac11f13d23be60a0d8adf1a4c89279e17ac1890f85b03dfb656c284330925326a9418ccd0dfd30b7d07edbac16edc024bd9564994d6338cb91c1ef59fff372f48550109a3805e727895b51b73a26a45fadc22bd13723be328794aee2c0824b47b2c498799bd780cbaca08cedb2c0a36149af9a041037dfedc43a42d440adcd6646dda109d8da645c7e99158d52c72c918f0acadf3bd27c88cef00255f02b40282caeb9ece18845d8de4925d7511478f3415bd6bae3d00d5da130aa06a065de678b24a6ed251d1653e63d6d339e9e18f614b3c9af92da3a647e774b1a379f8e6577caf7cd7f2e1ea196bebe27087c0a2e1a3bf2b65902b8ed988e254740e5c4cfe9f400a790d0a1225adfe6bb4293e626677e47bd9ca96738ad08e5ff0cdf632b92fdfc5ecb443d0059734783d11256ec7a88bc0fd4b156608a0c1237c3b0515e2d9a789f3a62eb76e4f7651e6c4baef378a552dc4608626d62be599e4bc3762c2fe75bc1d0e264dc165404a3e0ffee07ca219df332e3c883f2e69b453c1e4ed845b3a299751a68a9491aea4daba5a7a7d74d136ee2060fdeeb371f28cb16dd26b200b5bb01d9466102b59773a8f68b19f08ff8c5ebe64467ed44d0776b91870c6ae42b9f315c0cab197a798601045a5877eaab71cd7c4b7b97c5d8b7670e3b9244ca0ac17827efd512a911a2893ab20db8da9b1bf2bbb27bba9f1458754788cab1467c4c6772ee5369db454571afcc0989e9654fbcf6d5258452a365e4a144d9a6908831c107b4d26ac46e58c0b9147266d9212b51dee450b517c2885041bdbe5de6410f3f034fe42fa79a57b02fccbc02c8ab8f22b43f67869cf7548af5a4445d432cef85e1718c5ca8a25d1fcb89425a96a78a8eccc2138a1c0debc10d5f3a38b4f4ac7653e2974254c192232e65936715c81c93cf162c06b3aef739d08189bf7a9d9c945f820edbdf51bbc476ba592c901e1afceeab061bab4fd3fe8be4c1f675ca3f8568dcfa8bae95d8fd4a1882c0d62cd84efbe467b75f6e716d8cbb104283c68a6445545e94d6f940a09d7bd109322126fdcbee0ab36bd7750ca3716f6e6f91944c44eeba11c168e333e0a2f4ae879cb0e3566016f62ca87ff51edb1496664ed24653a8cce41b937461d7dd6eeb607f9d4ebea6a047a85b25878914670580e7183d4a679af0957394b1a4113f7747471321512d45c6ab70b4cea77eca40ec1b8cb5e243b5215c74aaea281a0b65a7556fec3ec166ac256c2e8e037f9dd4123b6e1d53495d6f89be1428e9db8842c2dff77be3bb8bb1001283400ff8807d954ad7ff5478046c462e9da1770d45eb961ed89dcb79f7949b2105186063d6826018a53cbcc1a785f1369e5e58791cae3aabef809162af801be8d70d37f650b5c17dd80e333c1f028e47ef910905bb060d31bf40c2991faaaf405eda5ab9e1a294b6a6ab50aca41696bc1f68a6f0ebb289da445446796017a22fb36fff555231d4a7ee1fc36a77623f3c968dea5d8eb07b620155dbb86f5739b52adeedc4f8c58215da11f3314ec6b4a221b5037eb0d7ea4a48596de5220969647396c64483965db2dada1998ced063d7d8e19961ddfb931af3de0db4633cfdc47aaf2879b6d1c8083ce10f18c7e6e0b940ad6ae7b8511d74904d6f23de9e39df323f17deb28e812b4bb699cdfbf668f22850e61ca619cb92ab46d2c9b2ba175d0c1f8689c97263af8d106c9f04e63ff1b771b15e5c5ecd6e30ab2e8ce9828624ca7b3f8110051a6be30975e7cb9204141309b2e2717871425b0291e0ed831985c2275974f803bdfc0ad6d26a184d97c86f7f7f766fbb47c2227d1fe3a93baf0de6bd44ad10b955258d1abb4db42aac317dbb810af5059802dfafe36ef6de7722969414b31d2410ebc886bf1ac30c9101618a33b129a95f923002eed81ed1f89b01e262196085d39046b4eaf7788b994807a143d1303627258d7414f921e3186b2d20393b921b2b9b83fdf180cae05891b6218e4251fde64b95fb74ab02b876284d0cfc7a30e8dd2e5fbbe1f47f8f07e0143f28fa1923c1a1fa375fb117c1df3c3dd49e80c845ae0d1f43d18b04089611264cb4fd751e4353ce0d11936d3872ce66719dba52ce447f84242227ff92afbad13bf43de75416f28481c85e756a0efc25ed21f6ff953fdbd7e553c1561f9d7739756b6025f9c2873a002213647fb95a14cf6e20da527f30a2e2aebca1ac5e3c2e02f2b33fd30f44ad203adb6de3cde8a5361da0ebea29cf9552e6fd730f967be9a9c290653d60b5bb20ec089a71e619e28f2ff40fb60784aa57ada13d64e3b337d785905c3ead08052252d613c3a92e273050fae0dac87b8e67f732aad2c0ac98739095067d01ff5de75cdfca54c79c6fac00aa98a47eb77e0b5382448111ca6116157ee7c70f46c23f67af8be5a4a8e64bd3a6900d492cb5f7a4a7034c8f6240f960b59e4e07fa0c4081a234f96b6ff123b452b722da23b286cb208243f756d11b1ba461d132509b56c811b802a0f7a454eed6c8d2df007bd90cef29b419c4be4774440c4d30747fc6db08dbc0f6af8ad1cd43eaa42eaff72e5d8ed66db9a340e17e8700b1f5fe3cd24aa8e6dd71fa5620abf2f7901db60a1faa41ca062e38476e931316cc9b2f14f49c5dcdc8b172b74358332bf626ba15592d2d5ec9758b67ee555494484425259092e2839bf1a6fa9516c23a8cf27319725b12c8d6327306bb0cf58505f11cdedaa340e7e84f3e850ac8203132735cffb98469cd6f062ac043532b3358f4fcd9e6675b9e8bff0c75fe920444dab8a837224bc0bab60cfff6c3365bf94e94122e3ffb499e0fb98fe916acef0537b6e2d38aa8ddb4a239b20d613f3a5af211f06daaf52e03bce83a794a80eb24f4ec84ec297206d5733660d31661063e0a57fa85a219d0052af5e5f5052c1662f2a5afb9205fd124f61ff65af2bc51db9137fd27d51edd13b56fee03d7fd5246341739edd99a1a29fbb81161942a92bde1602de938b540be6ca7cd304d5e8faa0e301cb629691e9e53ce21df368078abe4d23eaee9d72cad3e293225b03a3a8d6801b8257ea4e84cde957121428c9542f2ee6e9bb2deb7b399413d2f1d7eaa0c2f5b39bfc2641d2e89445e071ff476f1522561b8757ab6c14247e1391d616c15a267707f3ee6af984e1d7d73b2e48d40ba5f98ecadf727b285c259e14975128be5144c4fada5cb3f708ab18a5deb4ba927b361b26bb262d68dcf6e31287bf5439a4d6941a6292bff06b5abe03a0dbd003199784a6f98cf51cff275dee5e4422324167318f97a257ee067da43bbab834ef581b4cb181840ea8087120cc1ca552f5b1d8c4e2ac74f3a1c8f8ba4e1a46553d4bea4fbf1ed0c4a6af397493b6b0cc28df9d9d3167cbdd353b1abe73bb1bf15e5a38c4bb0780eceefce19bc9354c9b5ba1470cc6944b9f57899099c362086c6b6657eede017398e8affeb621c0084b94089e54c7358ed25eab59254bafd0c94bcbded90ae8701b447d073a4fbb3fe14981eb491fc336df76548f823370be197c8189830098a9822bfc549811f9bd3df25bba72b3a47c1d3abb26dec8ec3d51bf367810ae5db4587bbd6b823b3264ca37c07ffea4887cd711a4a4708677c9bb5c599d25810eaed811001a048680b2477c7414e338f6cef07e4623cc4b1863002b030491d8bbef0652221b74bc710b1adad1420aa205f1e914c22c4d93782768f75b4b344c73f1630afb96930d10177c9aba48ed15d7773b778f1c5e66a89f461c4a7fd2b369647023889adca0e7dc3e258da771244f1c80e11949572fcfd4bbe5135dd53be5f33cc5678d85b04f4b430a83ed9a19df58e011f28ac13d88438a94498f6dcce8b826cfea7316f0ad72611c91b196c19f8466c54952562a94654e4fa1e42f6b012aad2392d021556e1e152eed3c32ca2caae3b14371e0c80ac57b492306924c0395abac18ff9e582df5c2c43e3cfecd47aff68381cc68445ea40282186bbaedf57898790414b3e1b2bbd60499a03b01d2e6e92a841b8fb714a4701558d6f74c9387f4ebbd3e702691e6ab3dec806e86ee1dbb9a85e144b2b7d4bbf983ec43cc23a7db62bef9235900244fdaa6b7e04b1bbc23a1730fd115b9fdf347ec98be9e5d43d267e770447e7ad41a03ddfd549060a0082be8460a2594a93c72f5375f5d37eff20a00114b5dd0692dd2cf50f22620c0b3e90724fd0188352695ea9402568526f41ce07c44776274a5030eb613fa06742a632924b101c92b81affea93c3699f64316229bffdd12548b5d547aaac711825ec2a5d89da7fad90df3ce5797767795cdccf1d77fd7321edd8679f64a66821e8eb4160f741e57bd4533d6a8f8dc29ef80cf1b3293ea28275b6980b1c3e9eed6728d618a6541288dc775f2f3a8cdc95f7b114ea4ac2359eb4613c476baa78930cec04817a5a71291ff4ae930c8cc0cccf361c4b205210e9b923850dea121eecea5e33c0f4eafdb1984e9e9dea0c33116a118bbce9848cb1137c86a1af59feddad95d488ecd3dd8a007a427e36dcade05f4d4817d575d553ac258a6777d5ced2a1bafb3b1664d30df034320d0dcb4339610fdd9f2ffcf9e84e30f37439a5b407cdc787ef4bb50f74924d47da1864c425474ce602aae2a636e2219637519a6709ccde1a7d1397c9800ec51332902b2dee287c7aef88c707994b0032b02b03b6b0355471cf4b4d9dbcda16c81afbfe8f669dd4e2d72eb245de01ef9ba00bceba5ab8f4f5457faa1fd96778c47bba423e30dafd29212c8b5fcbd7315c8c02ca9a328267503fd280ec73b5aa08b6ed4922c44c59ab13fb73a246cdfa3b5ee4231c1e3ba6ebb27fd351b2abf940ed37dc4b6d907910cde3ee6211e1653b901ac0d15178643cef087e8bb905915566d303f3b72d657399f182791721d787afdc19ba6ee5360d6b3decb12930eb7be059230e6a89da5209acca12c1872cfb92ff5347faa3247eb2f1f1c3b3731734cda7832d60798c0b2b627f7c50098725f591fb774fe5ca15654ba57e56d8eccabd9868ef2f97a9c050c5cd0a5c64cfcf19797a6c08bce34a23b3c3a5a2615e7e8528d91b580b2dc7720973563f0fa92877e9b01517f3a55f1e652dff34d9f7a2b86da91d8e826c3248f9c9d1b4ec4cdcf224d021c75c70599bfc9ffc4fb73afe8806342f51496a194d82abd835f1ad22b63af176a9a3637a6ff7e78eecbebd624b07005cf2aaa652b9d26530217b20a911b00d1112f7653e9fe07a76dcacc9a1ff3ba282af321692c34024327682c196e7bcaa78d6519ddaefccd11ae96a003a998d8f72eac1fdf1e0358174f354ecdd5264a789d2b806b35f492887cf50032f881a2de4c547dd1de685855eca7d63140412bc3a052196d3519db3142f192d6cf2092bf0d3132ccf5ad0302d937262a15fce7c9b3010dd26a904e1a102c56749f034c98d9ae033d31568f5443ba15a3727a48e11b7a029ac0075ee93e828506c4e79a718bc96583000b81e53c39d5bf6aa80f88a6a1db34ff17ca6f4d3a4269e690313ae72e51060b55aaac8e6f98aa66dd20be79138ab440f65501d0253bdb44338767105bb21c3433d0c63ee17eaa23e0cbf8dc608fbd3c009fc2fca8b947eb5447e923f4fec4a851331aeee719c07dd1cb44abe86e5bbd3876a42f72affeb14e03b95d57a66e6bf1d6c053bdf83444945c45cbe3e8566effadaa16ba6490b604bf1ad7d2d945302aa58b5bf508628b3ade35710b580c231d34dff08a2b866669f297f35db249f19edd2094e66966438f82b56db24acb6b41e3b2e688a1cbe99531bfe97568144bac1f542cba7fb1f82aeca705f94932895426b8f0c33e945263764d5d3c6d4487ce0d7e8fb285f74c93464f74806823f0e164254d4563d9fc5401dd93808116da1d634e4a63663630112ddbc635936f717bf15d7e17aac45482877fbcdf6aa3314402331b967a0c9ebc12bc3383980d4665e7344ae1055f713e1b9d4892564ef11e79e33e687e04977dce4e76c6f4a44a3c22be43ae7c065f43eb268be8f5cec3b83b90f4cbbd1f2cd6debba81654c7f2b4ebfb9fc44c5e2a07ef47fb63d3d93eaa198340991e211405a62c881dcd057d388935eaa30c22fbf709fc2d343057d06187d4795d46092d9bc0c54b29ae50ec95d3ad693dc09c24832c79355b4acce1db4d2a4336f733daeea4427d02d5b1a44f01d335bc821e012edddbcd67ac90a9854c347eb8fc663214abeb8e16a62cd7f25a1842dfcb2eeade1471b57fc99f24991c6b3a71ff07db2304e2988497960360fb9383cb175a099bb460ec3e726da918159bd782ff2b738d0ff683443007c013576ff72d3178bd20e3dcd803780d083191c436c31758d7c0fe26c075cdebcb9aa17d230b58af73f027cc265b91a6f7a41ec3ce3e74c410f72247d328c7eb45611e5858791f5036bf5b0ef3ec1bf0a87c58bcaf0363d7454bf6846c628d58c8a5a91f510d9612d7992e3950c5ed5fb0f86a25986fc47a44deff4dee240ffb58ec24b5539081a42c5456cebf2d5d26fa70c92bc4ad0a9d612326e7e688461159dce3a459131e0b75476a4bbdbd5ec5ed7c8f86d2fa646e0d5bb4eec164ac9c5fb50c5f072a08dfb8360a62a1e7bfb49309ab254647e294d585848265db8f0b861df84c2bc7751fee9da72035d89b6201f1c87a380fb7d9fadc0b21989c77cd4d330f9c3dc5017e0f26d48d8c4a8237a2f974c308d88545511d2855674a450cd646fe22ad5d12e73e4e2914e04eecd0c811c94dd09c3e7e6438d4b877f69b1dc96a39d5562787cd811613c93bdc4c8ae1018915eee29425c5b0ca2fb0e302bf1ae0c8f764f1b8f8a5f23de31971bb2feaebfd775640f843608ebcf1ac56466f15fcf244e8ad6de231e389206b955ee34008b18eacafd02a4d835debd4a94e8d45ba9b7eca33b35efd15df4aa695f60ac83906672deadcd631e850764c42dce97df15deddd7871562fedaef437a0262525b6ff3d26dfa4aa5a2a04c6a0b3337d47c09f5058854808949def21ceec726173f92bd58bbc8e219d76bde1da3d6a1a07fa29ca255d60952c1c53aa80c7b10b487f2c9bfe5dd7fe6185c8d24ae776519042bb81b649b61971ee2c76427c80ea4a0b016ea216f97ac8d9d3ec31849fb9d0e8750fdb9233a9ebe6ff8219b62d481e0a3ef52cf26f1fd68a4f4778b158bcabcb1fb1281987a008a09c76161610ab6bf2db70af71264d15ecc011ae30890de59cbfc2ddcca8e5ee0e3572ad8e029127189b39c2d1cbddc51100c061c83485565acaeb88c324faf573a21c203b4bccae90ccfa63ce137d9be3f2546df731e7d67c464469158a0f0642b8ae26780d0f9a877ff31425202e32aba84f23632946da9e3e5c6293fb99c0da4c13245750b70face11d57c7b2e330fec472239322d791c1cdd36d49ae8ccc4a9dfc92598fb21fc8e58bf6209380251a5b402e85b5394af229c61d6fa8db58e600b8defdf23679d3b88485f32a5a2eabc38b0a67550650322e14f74e9d62523fc1e296ba882e9013ed3d0e38af4d0f25ed10c19da631ed36982cc626bff01c391352f4efb866ee92fccd32670ec925901463b276cea81140a8882f23b33f245800a53b56970eddaf2babbf5b1c444036bd154af8512cec990d1edace0987d8e05e62b0dd1d9a9eb3c1cb2cda14648fbf3b50df22293b89d076482b09f25d4750667f5a189fad0dcc544e6d558e4bdc9eae09107b203662bd204461fde38ed31a4fce82c00ea882334fcf20d13c95aed9fda162490008a5502189f1bd0299839783010be6d47d20549d08af080ba89f0ccfefa8a2c3cb0accafa2e7f0fe5bb80091cb4ed2642c1607a6b6badf4b68aa4367ee567a58339ebc670c495d38234060b170550688c05edc6b1a79eb158ddab776493620fafb1e2473f02031b9219e995096aaf31408ecadaa514c81ee8eb7e8c816fc69ece1670983e2cf14b132a53fa352382d8a37eff1bb4db89e6e9e63397c330c1dfd1545d2a3c9df38fd661ca02bc9c458ee488a72797ae1905ab6158288f2b0e7cdacf825eafdbfc010c8709871f2ccc838ab24d731bb6fd2493f49240656e927706aa1d683aa7aef4a2a839a4e75ac0268e4b3c409b3782c161929abaccb3a081f3ab744844e5a414a74571984f79e421bea5c864af9f0b8165f9714fd8e9d47918f8800a0aecb63f4cebb202db6e80547190d5908f690eaebd45996df2114c4df376dfe273a6d0ace7aee32b89d26ace94b07373aff745bbe46c0090e62dc9f7406a0d2efd22077ca2d49b99bcffc7ef91767507e09653f534d0506af8fa84172834466609b7a0a9361745397ebe935247795e249102ec22c84047c22c86277882b9585d29ee1e8de31e5afcfbe1fd2fd6e7b749d90745786b5c7246cb35db358a5577061f1d5177ef631b2fdf7a058f8f806f3ac7b52243534f426eefc4cdae7f9034c07aa1005e4363a67ec81cfc7507e5c3f061a23e8176891c2f9703f3f767e66829e366666ffc2715bfe0ba49fb139a1f90b403fdf54faf4783b8756ca0800ae643ef36dd754ed652e1dfc8b1bb4a7e4e5b67db780419ebd5b1e46fa57cd72720d9c00d1ffb05fb9dcc46cdab1e19114a7e587caadb587aa980e9f33e425fd71c0caa54e19451caeae2b140121318d234a8e68f946a6141e94d74294d658f3e431f31f1150c8f7030c027485c489975ae9b801933011756b264075bd92d78bb6edef95e9a0bb8595d8407e632d73f1794c5b445693a1ee8eaf0e5d0a5b5918870a4dc33b122f0fafdfc2313d10e897c91565849260990a7302c56ba2d5e3db8975a27fe113e720c32a4a588678302122104b6b4aeceac022e1ef0881af2e5c7d5546ad40c9b811c6c8ef48e9b5f7e855b3053381d72b2c547fdaeac466bfd078388ce34fe75627ff2cd5bde44d8741806dac673a7dd69f62dbce3acea26d23f753d45d1435db49897c1a5b5ee5e7433b5ae2f07dd061d366c28d9616482ec8532579b50330ce233d0b9a48e7c42753f796661556328079d9121396afc16cf1b8acc21ac4440df783e76ad9f2d0d45c75750f71e761f9f29478c8846d0b07ff88ec7d7acf6488e845099808c695241ad8b359e3305fbdfdf115a1520ecc8e56a90268c11f804700f0a81fc176ccd7dad0c243f3cb8d9ff1c98596621d20e35115c9a00c512185c3fbb613fcc7f8bdb5d0a711bd057959edbb36eaad75ad624d78e16e01b98652f4aeae848a3955ba3f15783ca7305e8e9268cf968d4818ee98e51948d574669695282e8757731e9a40a8adcb5f5c773733ecf2e1e15123ac1a3566eba6e2f028f59eab789af673d78ed9d21c8ed8aed475fb1400a59eb7053f7341d89a575a8c83ea73fb2c4ba74296f0dff4c7948c589830164cc9738997c03aaa28114ee4eab8dcd3f4be4f8f637ed4278c52b59c169bd82997a9582f0a9f0c4eb378acd65e5c9044be0a2c3e9a6edd2e7453861e1c6fc49f5d72e5e2eac21d8fd9d9854f5e89c0c5cdc7ee34e48e36f86741966a3809520a9c8ef548a446e94b2bc4d632ac044f9ccc3ca736dfbd6f2688eb5e5f1266c7337e86c36c06ce1de6f12261055fa4f2d7881750a5f492aef3261274c5b509048443e3b7ad93f9b95403794af9f0d3d479b3dfac39a4119a6b7e1d6a200ae9bbdfb0607e628e082c16671a525dfe715658bf9156ffcd3a21d20db778a489d738edd78c758d42e0d9d7a9ef5533851be73fe2ea06ad2e01a19d19ac4c4aad03e6d0bc94467cd1388bf7554a5bd284428af183fc499dc06efe3984f714fbdbdcb9a7de1941eecedc8a0d412e779e6da263216f33e8c32af11732d7bae8ab6b16083b1bb2d65e07d8e28f0114fa78a6b2492e663c056f4a5504b3f43bb0212316feee4c365f3ef92fa82f586c3478aa20ce6f33b6da502e6e3cd165254f7b1ba800b21e3721769c1a976c39a737f60fb0cb0893b7a587c2dfd03768e514d83d18cb6847911fc6fe7a7b824367ce9b6d1571faa02524fa452a1bf1bf5edb1d43494bcd9b077803732d99943a6d133f9bdad562e7d4e08bd20b4187e8b6df9c2456579dc11e08e5c1dd0e30fb258bbf8cdfcef8118ee6c8a0c098cc401149bec0e6280e3eaade5340c40c84fb7d434778d74d1b3740b5114391071d81e463267a0394e91f5eb2877a8547531e78eb9412eabe283eb482917c5936c620f14e7a11dd6e81f7f96db8412a86da562d9d219b480bb6d9b6dd8449a3ee59d619e19b9b1ac08c5c2f1492d7e773490d632ff0d87f92e058bad992e4f7d1fe7ccdabd14e87d5c1ec61a0d6211a9cc2e7fb688194c2811d0c998a1f25f219f97d35cb189083a781a2a9a36131cf90bc34474a9fc02631634e41c1dc78e0612b8f2be29f602ff6bcabef1ed98992c8c5ff386e425f01ca81155e143b8ab810c389b02448fbe416bb2c094e7258b1e63f09566316579f767313a1adf6a3c23243c168d9026ce6634df263f6bbda7168c9eb226e28c10531ac609ce5791e1ead613228210cfe168fdb20c3f77c10c2ab35805c67f88890ae75c78b8f7eecc3b0e34c78dd197d96072b6d28aaec461bb4d2edb95507d587716c2855f3658a1070ad0b0b59f86765ba3a303761dde58bc4b3749c2b8e1654465557aaab6196da513f56fe81322b4b5aa7d9a6f817db4f4b67443a4628a90799d4fe5ba1a8630b0be6c3e4095cbb8f090fe3742df1c2bec067458edb7a2dd068510936d9faf79d6935eccb7a0372461b9b4625ec3b61e7f6315b9495aeadfeec3aefe73bc62745f15258bd0d9d2e3d79808342002b0c7adce6ebf232f1b904a183215d8589e9a1b1b83dc1a437cbd479426097475dad1af38a9b8555ffc6e3201e659e245138c7dc52a4c04c30c5ecde34e716216f7b6947700f480e60b7f26bc57cca61907d635e702cbc7e9aae5c98fad67348b9adeaf9b0e9fd2d33e3fc61151b17887077e083ce6abb0b3e9272013e28c80cbdf27822e424b209b4640b79114f37ce710322076734e8adf5698a20afd3c0c0f7182ca275478e62e06067d980267bf2966301dfed7df7ccf77d2378f41ed3079e67e463488dec7403b20ce3818387c0aca035e82bfbbb8ce59b43394b42696be64ba260c3ede13eee049a929ba52157f3ef7e46e79d647f0478e042daf54f07923dc0ea34d56ea52faea9f25d6265b65b634b0638b999cd8bf175459532a93ac90e287739bd5973bbb0fcd6682e35e05c6335ddb40488e0bef72ff4a25290013683647d9124e92680209eb809e5b9413cd7c0c88c2204dcfaba8fea2200269be73f60b4b40f9a83a8635ecdd8c59ada7a0b44afcc69a2b552b9eaf5d62348118d3c055aa43bd2e5a3bc74059be08ff3cbefc402f83056c1ac8b1a6ca86cd906f5f747c23f7252d5c6b97ceb81c40a3cb9982a99400f45e7e02ef414dcca0cb6778d896b9cee3ebaa9fc7baab14a8243a826ad6567107c839c48096f8a6eedec7435a22e5591bf09c5080e24ff7b062c7d3f00921311f3f60c10b98ff4c9ca54231e5d2b855bfaa705f994a34568c03dbf8671700842b90d480c5093a1afb122178926c1e6404cab7c742eda65dbbcd1e27eba9b838be0d6cddb284b0ca177196bf5f0c7c968dfef35471452e515b9d61fd38812907666e016f968346a1ac97b929c6d2066ae36dcf1e79682c8d41e06dcd6d39d014ce62db8b284de8c7f9a49864b3b07431a985afeb37ea819ffd9c5c61bf1982a04c589fe366570a799dd7e4eb7c061826c96558e0af64f261e324ea68ad96d255e452a18c7b3cd7cf62c85967ccc53e3d1d7152c0225176f9e72df31316f2fdc795da50b2830939d0827696c9c543b06da57010698c05a899b41291488b5df85092fbdc3766bebb03582631108c3b810eac082fb0526fa9f4fb95b4056169ea05c0115549d45a0dc9f3f6afa6a8db1cc71de92db2eb4caf46090e61f26e1806925675182cac767d91bd8417a71e9c62314a4e2760f6081692b9b8145cee2aaacc382c17c9d6e868fb4f2aa240900d35bc0b63756aa536b9bcd0cd17af625882bdddf03167d4f9410b9f93833c406089adf4eaad197ce39fe89429eaf94cf557d274cc5a1b557d3e0dddf52a8bf83f9b658fe2471e60341bdd4eac01a888306e2f6d0d5930a4089d33c27df4ce74d022a921bd805a3979590bb2e1077ea4458dc9665ad27a6bade2087911154cc68008de2454a3b0791e37e79f7a4fd606fd5d549c4015554174e317bb2eaeaf71317b79a362e7ed0474b9c7a8f6e8a5ced86c192539673231b45bca540fa2885dac47d2de81090fe0dfabd63991925394809f7a2a4fbac5f969ee05bc98b1173ffb9c24cdca6de02cd5ef81f0e178ae9ce0a62129f3ced470b64548267340d3c9558eabb6f73367cd40da658bd6f74a1cb0114f5de68f2076d2ca71a9af34c661e657eb09874a39b06a0ad52b4f59786a4b313d3a4a3e0b31c3bce4726845039df5b68250ba5e71852e675e9e681db7ef5582f7b2bef02672f7ac3ffecc5d8cdaeaf1c032b5d9c49b8b1fde74f902d5983d35361d6b11e3e0f8a71ad2bf0fe74400cee5c80bf4f6eed3bc016edffe81c9334247a30ea64712f84666c3556c576365e93d9cf9759428b19a0fd54656e137f6368e3d43cd73e641b74cc4dfd0e847cef3cbe3a5417e536927a469004593d17d0e3f603213e2e79cc52819cb68ace35fdff5ba4a78b0a3e2aff04b1f4b3003a64a3196723dfb338e1d0b7643c1869559116e667ec3d81722f4174de4e80be0b8b069c4a240cbdfecd334ccddc02242f150377db67b5f751404527528999a92d48a52735efb1173f3f64394d5d76e674ae0b1a8e59be7f8fe3a63b6bbef4e18f3aec3b6d458fd006032057d644536d2d354edde8abdd2414518b598bc69434eae3b3b6e0fbb129fe14551ab8b07d79177c33eb73ad4f41299bd5fd943883d4c5ad3c3bd033cf6aa49d571c4198d471646e426fe96033735ba6eb34752f4b15e2b788bcfb5605033f7567c9a71c67459c0e6210b0c4b9aa24614551d65885e2f2e8f0046af3a0561ca201bba2acc157ec42ca5d04d4890db110e026f03f80eda4ffe02702b27aabe3d86be415e9a3ce693a71cae3456b7a4f661edbda45528b613d81eee09c7c6be9ef05dc86d97b46346f5e60ad0f5e2bd0efc8258e23fd8ed4d2ec96f7a26adf95260057c7fdaf0d4f7a1520107a38de0faf21c249105a7cd960cf8ca01dbf3f775d212bf0cf199d5f9cd13bd406659cdf73a92befa37b5468786edbe99e33505eb5596e5671e99639d9d58a7c097080b844853f880d970c1f5d0bee1b6a69bfa10fb77e3961b76f2ea1fffbb2ac4011497896738f37ab544c43490991b6b01b5facb9c1dd98b23600700b507d9eb9163d864b6937ae2c684b31d63a53974ca6c4cb0258a7a78e9e37eee27275ab70f85b7c078b6abc7ae469e9e8be3fecb677d96e7c4f2d9bce80ea7c26bc60553792864b6a0e76adce25007ded8b8dd7a00d46ac05005803621d03a9a40c5fa1fa24b10179b499ca35c1d2d208cdb0a630a0b80637196a432095723a08610555fedf957f8f7b4b75bd524347b39ca0e4178bd56b855d245c18e515bf831634e70f0247950a93f00e23e1cfd0fe3e6053f8d66f8211db6391cd5a82858886702641c36552a4af0f818611cc370a6c366d8f857442775b392b569d34f607c34ae911bdccb5d9481ee066d892d9911ad4d8215d6ea7918f1016aa8443db09491a162cede43244e28a8628da06d6983233f225e01f9710ddc31862e6e5a3fa1bb09131eb0a0fdb278ead5d9cb440c18e2f80982c6d2fa468f89ae2069f54bfcb5f2d2cd01a2d94d169871516570deaea0b9df4b4c22fd636be654b8fd3dba214e2759e423b574aa5ed48e6cb050ca59fdc1354d0e3f79d4313d56a5328b6d4a2d9ddb4628471ebf2ef145e6840977c056c91665604b62186cddd527682e17eae406692c8ef4519eb126b82a22237a234ef14e0d5f8f5da99c2a2b53af64fc9fa02dbd1619695f5d5a1f5b601228191b7ee1cbb0975a47159113b822421a6679126720849287b329ce561295349b89cfc3368718ca32397646522b1525cf2a5e9f70032a0553d3757845943e3a34acb47f62cee33e7091fae4987795cfb6f1d116dd8dccc918d4bacc991440f62517f85c071b9d3510b92810858338ab86869be8ea775f660d67c6a0b4c2c6569a3858c7dcb1ba3156c7bf8e9908090bca993d42f9cef82553eb4f8e46eca98437dde8aabe991a441ba025046e120dfbc6659226c10d3978d75a1e5b55784bcc5ae90c2fe1c01b3ba69095a27132a46cfe3e67f133316638c206b093aca2cfbc360a3da261039e5ae781e1e709d2de483b786af7c613408add9b581bc013d8cb64283c373e9fd70891d532b6c6285a89a7c408c429b69d8fa6d35edf62caf44075b4954d64e8dc7690ec9cdff81e71f60cc0c10900d5fddde561b5ce0b2502199e0142877a0c061b4e7cc3fc7580e3ff3e5af66ea05233fcb27690f96c814e34c22a409db4768617b1dba32d174e265cd1ed8181221b6def58beff9e9c5a90c38de0a291eae52447e3a3af289a5d6b6dc6ed112f8c175ef48f6684ad912ce46ed45f531e6a05df0dd93e49cf6a2f34b087d60ac110ce54cedccf6c5464743ebe171c70b91ba01c16fec673a40856449cd27b51b9132505f1520ddd68c83c9931ea568dfc5c24cef168b25a831d59587f6d32838c79aec6686e86765f82aeb3e9797d18323423cfc73dd7bf61ba60d6556ebb43a6faa0c918951926838f759b0450854f3728615abb33ad6b918d3391ae2ca0a43a7583de27f4e3f1cc9cdff33e10d0adcb946c026970e2f93c2a860ee79e5edb15c3e2e328d3a99b9e149a06c52a9c32a62f2baabc8653e008ac8ce0dbd8a5b7ef48895aa15aa535a5117619e88e3f417b8aabe4720dc77be1e601aea888a79b09712f0e805435d04d7555158cfdd9e85f7fced75e9fee09d32a41c11a6305838568184ffad5cd99f613f78afa9a7ad5d795fb956dd4152ce819fd8dbd491b31de8e5a50a3c2e1a3c0153f21499e2b7dbc83856fe7243bf7587e8718b0ca4b6cc368de0c55c483e1cadf716c645b963ec12a9e0155072e0abcabef69334d93ad6c5dd8043b66ea962cfa027096fc9b0fc75c9d7432c3807f7a0319fb59d5a9dd13dc10fb05ef1b8b310806df4a9bc1b2f8cb1fc479a5bd9c7cf19d2dd11c37b5550cee5695799775d3f488e937f0cef975f6d5859e6f6a3a07358c25b2e86dc99f9ab9875ee4a37cfa89bc61eaffaa20f92a78491f9cc4b710708f6e88b9edef3ca98fe059832f9d6780d5153e1de2ad16badf8daf98fbf812a27af3f239dfcb994a1c1bd2e424d84f2025ddeb817440f02db0d6c9cd1fb68d7378ecc7fb6f3d2b61db9fbb03a103b852949407a9b6e4b334695ccb9f8c1598683454be2603ba9abe7f156c08b1b0758d932bfa6b132bfbced3e7dbfcd535d5759bedae9419019e9dae9a944301994ac1bbc288d8665c60f57675f0dd63884a789c7b726c4b5468501334fcb82f12a4cc2b9d5b6ca8f68d0fdd18ac613a804d3ce0c270d5a4c71e14ef7e954de63b9cd4bdce25d08fb512f52cddd2f31124978cc8bf9adda597091dbd4db6e2cb4139a801cc14a9d2e2872a2c9fe50cdc7450255dcb06f3d05075c05fad0d5d4c36905e1b14c31afb45e31df46dde71d883d4b7df093fd1fa4b475d16a8a90c624a96848b3f75ed354441302e37f63bbe1e694edc5780f88dd4bdd5293732f031f4898aa3c4c377737b69721f64dd5655c46434a58b6cc1112842b45112ef435fc8ea61629335b304d6b6c4b513defa577dec6319c475fa8e43fcacc8eca6a102f829715a9f3e898b34e733650872fd55934b9222085b668bef0d25c72a2b9675364633fc79c03a3cab84835dda18039928d5bcc2d32674d4e04e4641201823a010f16c03bc713c55e96aaee873be1363a236aec66fac57fbc34bd6a4aa17ed89ec393998c92494dbdaa06376a126a87363218ce5cdc124fb1bb7d8b43d495558965425c2a869a663f68bbf1abb4546fedcb72dc88b3ae23f24150fbb96320b3ee1c99abf3b24dd116eaec4de65a3f9887049b1b2d6417603082162d4e7a227e1283d853b4dfe912afb5431450561c7c44b35d50a60a484118cbabf1c9f71c0951999ec818ad9ab8038659f1befa8deb5691bb76f767c5419e1cd58be95fa1918a071f0a1625839b30c7cf90adf49eeebce130410bcd6b23bfabaa416cbe9b910d40bc545a117a61000479b001f42af87d40ac23c059824c8bce4fe358ab1ddc4f752db61ad0b726452d4b29b3f52f1dfb4f393a7c5432cf19f1ab8fa0e9128bacaa1d68cdea2ef71119ce0f9f81dc18abd8a22d543b6b514923dd7ea98f49e44de8daee537e5b278cd314a1248dc7d8367f9a455207a619a70256cf81dc81d65a2c72937c91547b0eec965fba5b8276f1f48b566c5057e7bf1b8bd0128ad84489c7c14c491fee96353a83e27208cdb3e9bd8c4ad16b127eb402b1ab3c9593c7eecbf4be89d957743602fc4bfb6aed9bfa283e86b8bdfc3e314d952ff02133c6027674c614e974b168bd0005ee2919812e56321c0df16bd35832e5a6cb452af34076f43b56bba2637c92436abed63e8029a487d079ae3cc9c97a26ddbc119a8b0c44f843651a6a7fb1e1e25074077baaa757265b61b3a5511676ccd62014eff14aa70745955166da0da68a7456b1f046c883384be1f680e5c8e6da99d56bdfb8fae23b6e26175eec20c7047d5c6e78ac6d2a10f3d20d9dd1e0fbd3089f7f45f7604538ce1d3fb7a2ff35dfc0420454aa06768deb361570bafd15456c9add98ccb0310dbb3c2abad3e3abb3c08014aa2df8a2867e9736f7817fb31f3f44dc0bf4d8e4a9ea0252189f82bac1ea213f4eaeb148a05328259c3bdb5e85fe31e551604445d578f155e037759c1dbeeab79481593419da8f811ce99c7ee02d9e5dbe5d6041d31d9f8aa1530d5e56576eb89bf4fb809f32803b41af6649ed5975aeb428b0469c4f039342e4d0e9a3f8b486bb5632d91af5ef7b2fe73d1bceee308de64487c6757e7ba3ad0df9d4caa27d49415ebe5c7615f7baa659140b5db80da5952b76e2d82214a2fd778fd672fc0ac9b2ef96e8047386e0543f1f1432b4e65fe868b691878b058f7b4dd1d6069e0c925da55e0eb05d6e6ca43815ffa553bb035d529122a043bebd4f9396d3cc75034f277a70b92d675a1fe62e9ba0e5957b20379ef7e38e56d8fd81a4883e072e8a2ff3b1d4a0a92b47b2ec7877957a4e33a2f5a7c8818df235dcfe079b784472bcb5e48251dd1b3977e14bc9c3153b3ac8dc6b6f4e07c90ff4d3bdfb58347764d3be3f0eebb0c6bd1c1e0c550f057d056573940d607b6a9d87ab6fa1420ebf6abed96e1b2efbc7b5a1957544c66b3d171bd732d8915b80245c60fbad758b3fb03f324ea7e3e8c0e3eb17884ba993ba3fe9edf1d297bc752eeb780d55e196cd86dd3610a3fab3c178ff23155267674e5db69365ec139f798c240bec6671be1859f0f1fac16cbc96d56ca04a593788339254a4a8eae04cf46a655c25a2127cd9aae4d46fea5929d6eb1ae686b5c328969cf900d79d4a2940944f2e160a8d992ffa58f0fffbbf06bee5c55be7c449e29c9765ae97d61a8c1291ccbac511b89c40f6a829046232d79a81e9748d94be92ec36f534a2b34026e601349c5da96ade36bc16d56883dcc4674126f31d2798a678ea9e7b9771ccb1e34e8a66901640a54bac734dd453d307333e575378807e50c572ef28934f22b456bb3f3c6f6363b376b756cbca7597f24789fa708566ed18f1a206278a8127645eca6ce3ef925a805024f3570fb6301b6ea7dfceb5a2cc30150968dfe02d77d25abce79cf4e3c2be9436afe2d655d2d203be7a4a2a3318af6905cfa64dfbb1b2bc1600fd6ef84d930bf11f856e754ce78e66f8d09b722eeb78c63636236aa24058133b977235a2c39039d8cd43e250aefdafea616942ac74dc7171eb6b0aa2bdc4ec3c6db68a32f627a8a746c784455edf66b72efe23ff921ede2aa28c96993e4d4b19a0faf32bc6a86bcb360156ceb4f86cee8f4518be25a30adeedd6ef7019e16c0d87af10a14b675c5b03afaae6da0b84e7433cf91510565f598b3cd8df1b59c07781a83200abe42ed0415d5118be5f6d72dbc74b02aa12449c196b566c0f0d45f70585425003be93741234b8bb4df3fc567ff5d0cf8d758a7e79dc3cc734e43d6f3259413064350af8ae8082cd9aba4b884227fd59dba5a46de6715fce27340d597593278d22660c30ef3b8ae73a614ed87d58718fc6c69a4dbfbd0de56ddba804a74139efea8afc0012dd83d10c4982d7c62aabbac8136aaef516056f8cf37736688b2cee822341e6201d2e44e8ef62d8a32f758ea1ad0c388b7f54a22e7168c7d13a340985eecc8448f7ccfe4d389c17fbf03a88c8a727967920dde01a799d40292967cc58f273177653ff8c5b0e01b896f0be1b10fc9912f35c9a0673f3d8f728b3886cbd3bfce32de60bf9344967e6c364ab2819b3efde9bead8e6a4c884e44816d8b60e7260468b5cd815d1e3c311d0f8667776a55d68a52be4b97feeef837670e6feb2be3304b0982befe45769e074114ebc1ee5332c2d96033e5309bf8f9c799a2ad10eec1be9fb379c736731b7714fb7bd3bfee84682f42e689d57f968d2e8866e057fa3230ea9f910496c5cf1a9a56882a14ec6a12ad85c635b5c81444bf5e3fbd8a141251a36db0d90d003a684a25904614c5ea7d4a88c6bd930d5ad3dea1474e56f6f8eb3c294b9fa7c67b7886a6663e4b8bf932ea19336d3778c2e118b1df4209540b015269d408d3c484e13e67d1dc002cde0de390c324307951eb3346d172d7bb9bb915e0340d15474348482780b1001e34038d1a41efa80abc585ad7e38610a76a8bfcdea4c5f1a59edbd18cb1c2102fd87f000040ffbf0b442e7d328ff2eebb6c00409bb1477aae928a572eef02f3d65c96a49b65583f7437a990a4ae32a9bf58735be15915c194ae8db95ee3cb066f5c1b4e0fb8e918ede8adf4265a5526017ce70f6f009decec569f93fced1b081ba809d8bbcf727512f1e4f12212f768cc31a2a8a0a27b23f68685423b8af6110a96842042b6d810930f81a0ed627021e3e7086675e1c49f8746ae7fbdbc16fffbd3e8a5ba85f09cde85d6790f0bbe7255364ef2ec5d10b0ff6d2f8ec26894eca46fc53b89d50fd587e0e1602ba07e838e8bfa1dbd3dba485b2597aec1f2449058c747b5890f9080c98cc6997eaf96c8d7fffbf355460e84e6fce49fffafb8c6422ed217c638fc4d2f6589bf2129bf767653d6d345fccbde2730765fe82d2e282889710b7aed0dc246ebab52371110355fd8899fc1bfb7d71500c599a326d1d745966ea823c46c89e98f70e76be51934d7ca5795b4fb7829ce0b1562b4125e96610f55c0fa8ff5fb6443db25df29563321b41acc3f524d36b7e2952489dcdbd49f25209819394a9c6a6223727e941a7cf1365f9733e6c20bcd427208749d2d437c96a3c07f4609ff5447d040bd39223da20aaa8a4db360d3d5f94b9fb42fb288a82799845ff12cf145ad0946d8a89ac40f3c533b0c90316ed751f937ba383cdc534d4e0c642c8b69cc394094679e4e9602a9f2a8d86a82ae836b84ecec0bc782c618e66886fb707336545b88facd0c10294fa42de765f440817699ca1fd4789de01d08a19fe8481b9f1179684aa9412911f8ac9bea9cd3affe7a0149cbd84cfd59e9751d6664384e8df090626189e3cc73c9d17252975b34309f149002640cc28597098a9cf7a35ca91917d45930b034843770e5dd72945fc725025686bee91c5849aff54c93c4487cc6b1b5278c5190ad740a01a539391d817edbac87a039a834a238000d787cfb6bd547d3ceceaf79919b1e50a02ffa2025805d3ce25322f7c080fd8ad40c27217ab833294c2b47726f10f46efc5ff8d7f1b9039c7cf0357cc4f9fb47dd5f6442000f3e2c186de73b56bc0f44926fabcf3f13c1a9b71b2af1ab93970b522124c2c269bfa91ebb927b0ab8377794a4d94853fa8518811b06d5ce1d3970fe58f06814bc0eb6fa876fd1b4114bd792115cd91c5e71f76b5937a74ab8c88c87cf7fca0a015c2be43e3c9a7d727ef8ba2c1b1d1a92f2c9536a3a9a085d56f54ab1e017ad98b2b87d2184069bd1dced0dc26051be39e30253409ef79197dd870ff37323d11b487a8beec972acb2e1a234324c0eadf7c34ca77f0caddf7a21ddcdae24cffdc6b08cabbba42571f2587325e4ba8906283c748ed88f6a5ee8fc5f2b7e345ab04c46afad9e6a058ab8fd8e4910febe6210d1a4b16e26720de9b436b7aae548dfda2b44563c99cd71f3064296be787c52df6d6574c6989fd656f16d467f3f4639d327f32032d5708a71b1272591df4687ba84aed9f36d756a4a0e33b2c3e1f9b2afcb1ea883a668b89bd6702d3c2b86d0161fb1e85c9e591fde5e9c3c263af5d6a9ea77fd8eb91a33f6916651b5d133b8f8e9b886c7106f4420f05a408af73506e01338266efacf6e1683c876a00ed92fbd5c41f78f5150b2babe0bfae9497a76fa7cc76c6c1df6d4a0891761cddb4ad0baeb73af782a9291c3d915ec7950f36effd5aeacbe47a0a9ca1ea4c09dcaafe2e71f7f9d5a39b35689f6392958fa08f1464dcd2893406da7d861a937d741fc892c54dc63f371812713ab4f2521e0886e36346a5b11c65279687aa0dd3ae89d947c2976985601ab6f917c26a9b0df82396b66ab7d5d5d3e732725c7eb17239dd7111a43c70a187035333a44aa3020bf6c01f3830fd18ced18d279c50953dbcd244e5c00399068143b0b48a399bbbcb5f4db435ca7b861d2e815369d8e76bace433905490ee2020c5efd790743435a9c17fa272ba23bf5a0adfcb7da081368f3e8248f2b4aae40ba1503d2bd3ae6af38d21f28483bb1c66b5cba728022402e5943f1b203bf4e33daec72133cb89baa16c659b7b5ea23461812bf7bdf5160a28097468fd2766f59a46142377168c5f484678aaca6276cc3ad15dda5fe7e82395912e9802d175766ef3dd51aab9cca27d341e111545dbf7d99f3e92cf8c855961932b96aeaab786d06a953ff850ec8e84feea94fbb5979f93eb36af635575a209eda6d5e5fcf5b19b1742aefd4a63b3749ad25cae3b3dc1e3c18d490b3a3199d8a3d7ec1f8836a52d41e5ab85436869fa70cca1734f3b338c2b4cda95825d0bce429f60ee9cba47e7c6fbbc1652a6f7bbc0c7d60fc886b485387b781810a8db7a589c1b6b2bb371a161298359204dfcb10856462df468b1af0684b98ca00db0a6e6220d179b2b567997d09a0fdb3465fc225328a72dbc3cacaf96fe37e8c20c6a153b02103783911f83bb6493084e47b9a840e44c3dea3eea2ec2051c5a6327bae0d28f74561a9778b80e380029e92995bbb891e7a5f9e36786ee0edb780f4003d6a4cb6ecb94fb53a2307c55b045e513499ab313246bf2508a3109da044ec3a4f81c5e075f0ec4c2905e661aec24586c5c0e84603a33440ed06cf6a5629e744a201a49299041ad033401c0c866830d5029de9ab2de22b35f458d7d1b1cbf2022014ec2b1c359631f5c8ccc95f1a7173ea2f8495591ad1cafff3f2184e37e2bebd2aea8628473cc276a3013fe9d463a391a7913696b479be6524fb1c0f8397c34f6e13c10d7c8c263eaf6f66276f6ef4cd2753f2ffdf3fcb9f2b6dfdb8912a609898f46adcde6c1ed47d19da567562569fa1342e98f93947bbbf5f6f4ad11dd1968de29f9eecdb72188f093c18658b0957c5c745556364f85e7aef2d19903e740fc5276c095f73f66e0fc04dda6e98f6538b108152fb63221ed99e804e46d14f376b7b39c33b823a358dcb7a91a8629a5bd93fc3085c457a12ba15a0f1d26dbda2910cbaaf88ae44e9169560f0522037956bfbe3f1521b0afa55106b04381d27a3fcba9be8d5be626aba6f254b5c01230e0a7dc19ceae028c379771e5b66a2f4b04c92cff49afd84c36cdc2e9d497569a2d36640a33c5c1fbc184683b56dcbcee990a06b399735770fa393bd8e8d5208c5b2039565721a87450c812de87d05e4bbed2582e7587f8605a80a56c335c591c25db3ec88cc276ae90da1391400dd01adcff8e26305aa04574a746e7ac2116dadc6d3e01ff0f004128d11d56d099cab53e8c8e450b0a30f48f6b257ba57d1eb9ff1ffcb2dafe2f59b6175cc54800866e033edf95d7a3c33325962eebb4b0077fb0f1d0ace4d321e27461a266a0ba471ba913b7d25857b60840b9944c8d9afd06b43ad6bdf4ba199825e78c157a483f3940fa564ee9f4628850f76ca53bb9be175d772498ee964b86d474e7a5dfdc3f3a85905871c242eeb137ed1ae82742aeebcd1c5e914ed695910ee6eea62ec3da82751f1d7f11b6b493cdb84048535c1b3c25b8c91f88dec4849f89d4a65e11b79499fe7484ea6e12dcef3fa3fc8d4be039c188bece02fb88161e9e88b99b9fe59edd14ef815206d179564de3ca0f75bec0fe92dafa6a77d90db806dfcd1be395678de4cc6a8798b40b4eb27f7a922e53a60853cd0e70c6524fd62d5ad95ab7a85d2a373b831ec5b7a887f689a0b698c8352f9ee1f600076eae8f5b5e72307473a7826906199343f8817e9bd793b43e8f4b05abf4d8fcc2e08a7728f2c1ddbd16abde1ed948ab223630393e77c3de4cce59dbcefcafe50d2bd2da1a0311b9dd8b012a14752cba3c2a46d7135e5378b874efaaad88c8f4c2b6bdf4c3793086766ecd202d2ba46cc657f2ff6ee3e6941a451df334db4d3781e604a2eda13874419c32c32543060da3db72a436ecdd91b3da8b4408ccf5dcdde87c90ca410aa1af6efedc9af5a35927715e7ac443979f75f774174be0066138acbc7776b6ba2d322a308d7ad7f3df8873b005cb18f609a3c72bf4d9303e4a26e43d43ad88ae371b8cbeb4a26945e57b4797510778ca4ac462a343ef2d62e3ae9adc8004a4a3eaae8b696d34d986d471dd420493d83763d42166a4e46dcc070ae8384e0d3a3f789b1c544210636a1097b23cd3955c02413719eb67ac91f32bbaa7a6a9d4e726a9a5bdeb46c82e3e950938baadca06f8ba09187ea7877db850a08bb43c6ba8360d6aec8aaba795ab855a514193e612747cb6191b97a27dc86400c5192940251105d63acb4b618ade129a0f94375cfeec9a334f04291cecaa3180269f1d8e0a24d9412c133ad27b3cd49e728bf35cf0e0fdf543eb42830c0f8c2caa2f425ba167a2810afceb2cc9d61effa702179e7ae59fcc47a3e47a3073a035d41b02566173d35a7a798aded8bb27e070c39d925e6468afa45e0739c0de4d111b3dfd1accf644fe25be3f9ce3218905277026314e6cf9baeafb9985cf9b67ef457b9266066c6ee30fe104900080f49b660ce854cdaba5fe805585774e1bdae702b05c8a752d5a60156047b18ae3202bfae61cc1c47c33a61ff42009ce36d77d7dfae642e72c2389a7c3bb99c615ebaac5e57a1dd8414c97d94c9dd2bf12407ac08fb76c56325ecafc25e6145ab0c9ef6ccc141c7beb6f2eae784e886c5725569d6ac6c510c61ecfb384389578d7a20ba5ea8bc3d4fdc6a8082ab4b789f79a869c700c650b672d8991c55bdf356527a2fd59900e6432ea7c609d48cff200e5c74690a96cd1b5fb0bac4a833fab3dc6589490ac26cadf7c2bdb5f92b8a787541944754e5322c85b78d9f644b678bec18bed0c171f2a9296592dce0ee57db028b6f1d73f4c1e27568ef72b114287fd9bda326e11be333d50b03ac4f6dcd5aeff44637f50aaefbf374bb15dfe3611660828a794c5a9708a455162188988635411f6c74ef85490a771d6184aa4eb1fedfa0cd72ccd1dd98500b5433230a3d0ba1b17dd080c64f8c615ff0025e70977855dcdd81ee1baa6fd27f079902ec472516f69e928b86a039e2ffa390db1e4fe4698e5654dc0161bfca89ceb199e54773973787be1a4ca7119146a72bf83d33d1f9050ba0b9424266fc73ef5a3f9d2833877ce5ab88ab13eda12e14e412ee243fcb09743d05417413f982559be6d85ed47b82394c44a7b479d1f8b5008f53887ff138b81d33a359d08da08766d508bb2eb164336765d65adc8cb1b703f57d81a513bad92460ac2406ddc5fb60947918e834a79c5912094e9dc406e0da39bcc9a99332d4dc59ecc563aeaa918c7c88a17e142e20dc4e7f8fc22c7a420339d8dade9af977070e7bf359aedc784c2af064d83f4365c9fea9f496182878cb796e57c5766847142e2cdeb59b8b478f9f3dd2016cdeee9f145ec5dd0589bddfdd56203739c0f1339a9c80143c55e8f0a14b18f46a8334cae866642533ac297fe2271d48dd4a65c0183a162bc75ea289f1edb274f1b204c62039a6d982a6a84d75168d13463f6161e591df3f0af0eda83a504e3185f4fb0b4dcd8a243c6ebb543a5b7ba33ad551c68897ec2c8e7a55eaf0a4efd457002c3d25f4c06342cca58a9b1461f4f02bc0446119cd665c33835704e980d56b5cc8944c1abe020e9e8e7bcf961fc3742b608ac96a82100ee1c08e447ca09ba080a2601e300beb3459e940d84435ef84c51c51b8601d2318af2ecfb65b31392303ae8220fc634018a24f20f8aeeeaed8a63d27c5b0fde38a550ebdb8a0a5d1b7c76ea9af6e4bf8fd020f4760de46338583d6529ec879852625309ebdc4227d35170dda1eaba2dd01badc8d5c08f2f6928835093320e7e6435945234a50de3c6a8a18ccbab14c7946d06786e5a3741014476866d4c93bd0dfe5f1da1396a3a223ba32705029df4c313c77af0aad60c7f889891e664412009a7c4273ef52782d1606854df19afd10810b9449c07bc34b5f4755375b71ed8472b794fd108991c568412ee66840173fd0029bf912ea9de30ead7863d703c7a27979d3877b2f1ba6627b9b8f18be8ce932d46fd4730aa46d3166917d0a30e5ad8bda74a9a525a6053937498cbe04679672462eef81b3292d2acf982f931576f18bb59d38292c14816205954f074930c8229dfe233774599b368a102d9778282d78d45279bee9d6dc97f3303e7195fcf32be9e98865af76190a66dc3e0eb0b97bce93307d17cbb02c7739cac90f4685c0a0656b33a693541f06ea4f345716bbe6af4fc918d98c5d152508338cb2eba2dcf25fa99af55aead3ef4ab3d2ae0b8964cb7cf8d78407c2367528542808964a120474e9932277cec1776dbb52cf13ce853fd65b0ba997fffba4d1df9313b167a22ed9ec5d56bf73dbd9ddceabe0e335c55241b7a27921e48066d463356f622877b55199766899a6d24dbba37513a20b2f4d90ff9e82353356960d11809be01bcd4e66e362fa9c832389c941a41295e710b09f1fce0503511cc7c3265d94ab913bf8a5210a45425af2935b8c2c3a0220cfa05e86e919bc97a47398316bbfbd757cdc4eaa865bbe570391d527ed51cdc314e329e290d570196594fcba7a0364269c3b7ec942c6a461f00994d3148d42282af415aa1827f3fdd30edb3c967e8163994d46de71d5df2a5b82005eab91c9125e99591fa71e7d62925b952aceef08c29feb5d7c23a675f311d5b3e235b5186d0cf403a730ca0631ea367840e2a48c62a498954a6ef850a02dd7f238445859b869d98a28e2978f13dbdff7cc150149f835bca03699f2309dc6ca490d7a0ba36ba2f8444bda95df0885a4300dc8a08a13409d644f3beaa91db1bbc0ce9107e5620b41934585d42cd915c4958f39643bda97b3d7b26707c9dd543ea7be0ae8871a65fbdb97f637e842db93d60e4e10ca81486b6f6ce9b989bef27aa35c801c9235214256ee10e79191e29c215bfaa401eee4460a2ee5b1c4b4fb8f9e8e16548da63d7a99633f309ffa2f5239e38641567e29a31cbab421831def9795f5ad47edc3b2afe1ff187b0d2d900358ea20cfdf89cb170af73f704febe77b0e6c1b1f3926b5a845168fa2215667cc0036933ed5df948d4cd0d7ad920d96c1b9b67ddd55dea7f79c2d4d5f3fd5d6893d3bce7dd3c8dcaab1d12bea4783336f99a56958969efc5c1338aefeba92a90b1dbb79ab815f3ec551e6151095eccc3d388adfd32718f9fb006f048c06c6e93c955092ac8238a291a320566001cb2ab3ae11429f735f64d6091cda9a7852aa415c18fa2a799493143c0a3ba74ac6b95c57fc8339edabf209b55710af6c3c9ceaff2e4b410dbd51e7d6e30191fef0975fa4da749ffab020ac505d7a53789c80996b5d96acf1239db0cabff4b9c083c14b489389e252590e2ba44fb8b727fc503cfff200f0105167d4c6f107d1fa5cf6e7bdf5587bde6e913b33decadc634279fb4299f921bb1b3fbb9aaa6285fdc28eae8d4c5746c79c6a7b52b27334b1f94c9ae82d5ea534fd074f0017ac3aebe08a5365d46bba8b624b052be07cca6c35f6b9eff74f80c2f9a43dd7741105fc94663b4b073989557d2172b59c42f8273562b057cbed519c5728a2d8ec67c99756cc4b55ac1a639c6d545c20b641de448f53d9b53f97d9d740283c64907e540ad1706934a02c7e35e8cbd9abe0aa246b07acd7e9ff2b22ebad3a2f8361fe736f26026e5d1404c903bd30ae78f9c71a72994cec0d062f5be477f77cf940ba7b10f0fe1167aec685af39b9b44db57c8dddb63d0cc9ba207a3de91ac309a7b6fb6956cefeb4e25da7b77a12f91e212fa69ded946c0e86581cd03a45d28a018632fa174917f7264812f490fc06af4580d0fa3ad3624e501af8fe217a8f9ade391c0453281a7702510d00844da7c505c2ce48a5c5f8d674bda1a6829299b472c16d8e5f120cd2392918ece8e820e29780e51a90c196c660036389e2cf5fdf867832267c6da16c7609ff6374246e2450c592df89b37296c96a8f4f495cca0b25331c96a2a2b7152fe9c915b8ccd16ae71d6177743c50577792c179a2922a473f95872b67bfae958419448026e2942a18628e1843a385ffcc38dc92d874b26d3d6530b3710863707623a4ea256e182a1f3d6e1d0ffd92ca12881dee9872b0e79fa794876d1a76efd473cc8d9c3cce267765149c33b35f2fb0495f508fb93b7f3a5daf1f375e4cbba10acb2069d73d302f4ffe774b7c9c17431858daedee70a37a8b10c6da82a4029a0179e853ab72bf928abaf16c652ca773fa4900578ae42e6896ec86d9b195f87985737dab5142c08e1f011454dc50816f65a1e97ebd61a3d2fed679e1f8df10a972d17c2f35f80b7285002d6ef3d0d543598cd79a4c3ffc88db64cdec4f9bc166d023a7273fee4691d4b1d94db687b1f5a20a450bd62be762f39b9ada6fac83e75a4ac3848638baebf1e3b97455b9d5a2268a5ff78fd030d0b62c5cbc91424e3555b5afa6be485b3f9fbf7d123c203bee9d8c29c639544883e8220cfe1852f3956ace1469e6a1383febdeaf1f37fd9d24ad8f98e1be9a467da969c8407a342bb2c6740113a530b60c5bac9273a92f73617be16d5b39137a9d5cdf93ee42b9dbd104b8dd61cee14e0f291de273726590e91783a1583ba5885961ac7c8f6a270097710cd45ae483d5a60ffe15261bc4e39be21be731ecdd5f1612ea78ecaff41af9542d68c75c302b7c6b3023e394304c95cd269c4e4db627d5ac6e175aa199a8dcaff1128a6873a18b8a8575fb04f568973007347c3b1be8692bfe7a0a0adfdd550ed8328e15c80dfb538047329d175fe6b66a74ffa772bd9abbd1ea75f1f6af9887fa2cf34b4ddaa40d67bd0227520f1b48fc33d7ca743c0bf4665a8559b0490f39d7e4b14c37913e3f6a90b910fb923bf07e6d1a87dad760e60428140eb1cb4fa03e18b0fdea47f4a6018ad3cb932948b22c044689e7c1c19852e4a9d209646645dac6fa30846d6d0e1faa28fb1e405d50df210f7e83b13a9936a3d8a480a54482e4994b3d915f0a9fae752c04b00ec207fa7717114c92d066271abe8cb793376c85c878b21ddb68c3a88afc2c996ec9a2fccd8fbe4bff20fd26c17db18e9834b3d0003a38f5f3c7e420b62059dedeefdc74b28814d9e72c604a093da98fc2703abcbb0f288c433b14342a6eef115bc7a74937e5ae716cce1d95e3003e7d54835c02f3b9a7928ff7a22493996aa422d0e11ee711d0c65d228142d69b8bc4313419e68f8b86c7fad0a6c257f7f5b2577c0ca53dac1e96ab33d1a31c3662fa05b9ce5167b6d4d2902359a7665b7e24f97a3e4f32769b48c697574c0196f064c0842737596c90383ea33120b806e98c622c63275d907afef41125016624edb9cf848ffa88d408efef675797a2bf15e33083551254f38e0f105dd3c99864633363204058a368fd8b48803962825045b72dc3ba42e8172445cb1508414b0bbd569168086c564f03ca942f1eba8506ba0641afa341920e139f0ca20defa70cf2dad38139796e3f210c74c1ccdd3f1257259cf7d15539c379bb3ebc86a337b5acbc21668465bd7300956dcc6dec29dae2b851b7a7a5b6bb387161cfcecac58a253ff802402695c11d12e5a0383c10cf89c02f37496ab1ed7d7bc786b4f506d5372436e8dbc87f054b9c99ce54d047c8f91df198bfade84d13398bc0ae4e13314b1dbe1c5d5023dd5205c142e41d36b00d99030f65730d163e088db3504ec0d3d33d942963afc662c1931331c19ab84fe293290b3b33e214e4f88b3374eee4b3db6ec478673efed736f5a9bdb5c3e7ad786c68a41b5b2cd846bf2e2efb1b9c98acc5bfac10ac78b4d8ec64c804b839304aaa833f184f6e8506c5fe3bd42101c4336033c50a23db0bc536eab0178aa5e19b2a7a9f14588e860f24e0e015672f981100de2cfa8dc446cfc6ad5c59c7dc71859c89e0fda1934e3ae349e573aa8fcd6b3389812e55e4a15e78a0bc28c20642988ee815c35b9fc4a5bdd7a537d571b17777ac5b47eef3167216267d45194053435a47e52f38d1b2c8ae384d975641c9a0649beb348fadfddc8ef6bbdaea06ef197c5e84e251d67827a81a25db5434c2d5d11133f60402f7f9934e7521f3888623fef468df25d7145edd9975b78a4410070ef58750d94bc0abba4dc749eea98acacbd87a4fe77210c7aa2f74b5f93f31f24fdece49ea860a52ee7b7dd22617b9ced11db4045ae79203e9184f609db56878de637d5528f6643e7e0c12dd11389764201dcd2e78f04f7afe03774a01b752ee53f385c8bb07ca57805ece7ffd27f6c79662c2aa8ca1cc4848d0632797e96fa0329b8841dc81e9bc0308bf126881b4688bfd2bec37978b1d7ec3e7a37ee5e65b7fbff6ac7d7b482c77bbf9ce75e83049b4510a31bfaab0079afdfd561ac36e7693a07324ad123f3063a47b448056188698739bb1abc2bfd4cf35a945763806818b92f671376cc821e6ec45642bf19dc6c4b7f708be2a11c3fda20a35094d5424d608be6fdd12d70151e98c11c820a8693849e9c63be8535be694ce0db3bcdf68f2108e18a144ddddc881bd416975c91c4bd700025a1999387843f273cafb27d423757465267aec9654de32906308d3804d5e121376fc7fc3ab6cc171de925021dfb3e4e9a09b463e2b70ccde871d9632850005869eca737dee4e6d3adbc688e909668274b0f7a518717831d633ad86798d645458a448b5abe778811132285fffdc8f28ffb5fbbe7f7608996c6d74cb85803f8e19cd46f69812a139966f897bef0979bc602648cda773dfc94bb55e03eb4be1e6b48c090b785f5978f9e0ad240a8d6a02b0283bbfb3d94cbb579ec999b3d19e5c52311db2a93d5a63fc055f340d53e0e706ca5bd84fcebe367f95a13343d957f11740c56be1bd3868dc29bdc1f179b20a9084360759c941200621ab21bfec59ae5f1caebb6da7a5fa97f0857a6942ba36102d1835db08023d3626b182806c33c09beeb9d359aaa695711d8fe2799e1a06e05d9ef202a82256022f906fda262960bd81e399adab173b7533bd70ec06f66259a66cb604ada7cc824b1def0636323ab5023886c8fbb07fd88cead1fcc60498b6ab27f522dd036817784f084535b6a81396bf562aaa25ad35a7a909829684d36d01da07e7d449cd07ff54c51f353d665a5a031c8c780044b2925e46e19ff178d28385e901353ce3a76cf2cb0e49cffdbe28518c3720067e835195b151bbbe6836011152b845f9f40d0648c825adde0037552b684c29452e7e630a0f8d3e9ef488896ba60805fcd631cb0f2627d873ddde41a5144c27897b355dbacd36a8e55148c1adfc8db7a93f3c0f96c764c0d1f859222ecec13f7ee8ed2c663fad1080e75a452e1ab9429d9ef529829a03372bb92608eebaa9ec840608dd598fe5da557871a899af5555eb9f6066655457ba668697e4c9c096e4e625d16d9a74d65c87b3e267c315088fa562f49bcce0031442f71c6163a9c7f728dacdb76f977593c8ab065abdf9bbf707764c41f3452cd2ccc651804f464de3c88e58b0d0c100eaaf747e7d08c01a37b221e23e8b3211912097b20a9c9fed743ae4cfddc9aa35fa73eb31cecc24cbc9efecd5db08b7e276ab4295c39e2ae9e05654588445b31dfb34ea6f623353b7d765a0b7a7e606091538ee77ed2cc4d71791e05c8c9c1fff5026007ef8c4543ba0a0500efe252a4b65b7e4fa9bb5cadab5ce9fc43bf2186221e933f126cdf88e04c06e5f56d3e164a53ef8290a75cca705fa2c941466b33d0f64cd4caf28118b81684511aa9860fbc42fbb5214104e5ba4685d5b9c3cfcbbd22202b594ec3a6980724c03e89efd6c34b008dae3c61e072fececaef4dad4b77763052576c1184e664d6a8407ac4249951e86d8492be0efc221102a6de427fe62a7b53607f3be2756324036d39be5638a42e4a0605465b7ab0ac4a6d8bd690a2fc5614859dded8ff02450e26328de6363d4edc61658554ef5f23f4f0056dfeb2ca1f4a17468d7ce21437d8d4155722bd0418cd0990c4a1e8f53b2c700078d562a45fd74e09ed72101839729c352d38b76aa38d386e5cbe39fb7a1f57a41102fb7fb887450ec155f29b18cc8430bfe8da1c14cb4c5f76f64be8df0b4d1a6d9233a4557097e17b093d8fd2b521f515da9737f24b81ab195dc9967607564bd4fa3d010a0c9333dcbfa6fa93af9407211190ddc95821ff2abf51f0dd46734b06372464dcd6129bdb9ce645cf12bc0e88fb1e0fb5e198ae12e101366ab5bf47906387021793d7eea5982a431a90b15870f4a58058cc29b421e7e6ffda75e8777bef92ab62e050d69ad0676d2ccb93ded79e99088f0e55e67f7a684d52129af4147773965d00d781ba26ca78253256ccad1b5096b1c3a89437512908850bd9c3ce02c8a7050746f88652f3e5b14f7de16a0e1fdda3cea6eed17dbc43b36474a8e8ef6c46354e9a4cb124bc4e64a7a98a1327b960b14a845a5c2d44ca5be88614574920dfff1017709405b5dfd72f2b2e314c99badf16756f45576533fe6efb77623d8df2d0f324399d4374127f168a08ff32594c0db240845a67f419ed89b817ed3a6bf89e37d34846753469e196b094fcecc238d54344a7052970f4db2123180375227a929d780238e08c0cdda553aac7a2ce2de4bab0c2fa0f6eeccbf31d16ab05ed6dafd47a572bfec74dd4a9dabb634cb79be51e9dc4657d913f86718bb7fa76ac10f30643bd72f77c927a4bde54f9ebfc7ac3e8794a25ec4386601b8d22ed7b2d3fb7377265ef09961c0a1a0bdf0588a52876545b8ed8a5e6713f2387e2b712865ba195ef82f9cd3b86ea35c4c2ad4133f60553925a0963de2bc2bd4ab6dcfe23a74851659d66a556a83394011b1578088572fbff7b95a979ab5e7e4aee9e0f69a40a5597161ef2a85ce42cecf01741b12ad2ce442f9607a784f03e5d22bb421a0f49c60d27aa3f756542b3dec52c20d172cce082035fa5716b9eabdf1a926ad2b41ee140d2a377e076c46ca45d34877db637bd82605fed68944f296242c62c97289d84051d87104fda40d1fa871045739f5b7b9d74cf467a156cbb7c0d35d988df44c8d6e4ce1405fe0bc1b6f0c596f2e7b64432fd6f5e5e3f43ac092bc03a6cc5bf15e413d8ea82e241d0a24e0fb6ac71fe5133bc8f6c1fd8560128c608fa482975ddc18cdea0f2b36a54b8c34ef37b0e2366a3fff28e06f3b12cf943675e97793851d52b60a79144cb33a5b0f9c15e45382d055625f35f62ba1e42feb8fb8c9303041fcf7e36241b3830b9576212c52c208304ba868994675dcfcec9995a1d1630161844693c86ab3e00c93ee00dbdd0e2495580a82aeb46992b63753c7dae083074ec944903710bad7b90e25167e8d1bc994a98cb7748e7f547a2b3402d4c9fdcc3e4f05a972966737a5e3a80c2effcc0c47e8a04c737fcdb0fbc26fa3c680baa684a6fbfecc36fe85cb58d062766e8537cf3cb6eea93d605c3e33acf69faa182647ecdf5f0814ebe3d83932d6e10d47ef3277a8fa4cc2edd146ecd22f3a77be27c30c4a7fa4f060470578aff579721ee7eec7d82a5dca0283a527f054ae1ed9391a1e525669cad9c245f962829f9fb37693383991c9df03f636fa00224cb8d8aa88b1388b60fa38f9bce55047e409d4c81ba2391ff3fdf4a422b152a2541aa5147fed0adf217c44a6e5b02d1d61816e6e483b9a8674b55059e7d8fa006d3df0f3585ddb9b801010543db48e657723bb08e3b7ccc2f88c8ecfe3f20d6f420fb3e1624729e95ebe46147198a2ed8cb1fad48cb7a3dad4a2eb1443108f39e1e07b62cd81391f28bc8f89a02b5ba90f88fff06074cb7463e909f38eaa8f71adf3855b4772ec9b8a9f5532c3cf0c52c393bb025e477f5e9680730c094715e06e1d1ddf3a906f39545f83c4c18b2cdd3c6d79979313da124f4aa1e54b8736e2e1bb8b7734cd6c2b29289d2faac78827c73b5f2964bbaedee0a15986f135f67c6cf04e8b07d68b4f3fd8227129d02f570426dba127f4235465a9e9b45f27866ece71ba2c74c641f38ace6518dff1bab356b2859377e422f3c88990552a3b703575b3b6b1b4723d2baf194776a9e80de7c3650ca577feb929ec647b881132d726304d2e263c5c0932d6bf9ec2e0e000c9481b4f29a0d6c3bb3df8222b39f8159aeeb3e6f93af932a1749750e8ea7596c05117caa82c2e9c5fecf0ca15ad078fdc540fd0486f6bcde8765821e2a8007c694c34bbc7a96a3bd92fe7a60852117742e6b55cb0bcb12ee931b0aa578d195115d48283139aa9ad3332b4726ffaff4b64ed8064e41a3db3b12a950d927519e883f26ca66d5bb92ae34e87b49ff8d9583138efee228ceff9d2eb43a3538012e1675d969178538b9daee75f304f7cba048112103a9027bb52d06572adc361873a885c147dea47fa000b01835e8c121aae9899649f297d07ff5a6af059fc895fbb6eb72244621599559000d4fd5afbc3291462d9c9e56d1a5f4da4dec6a188379bd244d790523f459e5ff9c530170d50c48f657c223ea84ac14a918404d7b67ddb0aa4c6fd92ec602b30ce5c9091a1095839c776d6971a4064cb19d298034159ad8e447bf663208ddbc2fbe932a236874726c84b3fc7f27e21413feb1a0670cfd9c90bd5fe8837ae3c310b376e5ac08c20a73d362ae1030e3bd3bc6dc977c17f7f5d7916457413a064a20647d4e1765b1aa2f2efbe48ea38b02a981dc05056249924c6a610646715674cd0afb2cd5ebb8aef752289e57be66b4a7644fdd1d5e9092b07d0ba0a83be855c9a7624b5817e7694cb45c6814f0b133b02401e7a694e77c14eb1e9c0ac09bd9d9b04ef22091a2241071b3b2c53320d65233f1938b78acc53ce9e1270db6b5440c7a8bc745b93fb406870de1d8d9e68f0363b8087ed5e91818acf87409fa3780305a1bb82b356b6e26e9bd2873afba83c7e95abeeeee8f23ef565f2519a8616e56d5264e0c56c7e0203c70857e801f5d847728e0450da78f5a3b6a9d5c54024dd0184b75d35671ab90412e1ef7a3f44698c6567520852ec9ee7cd95797076ff25d5b5ab423f792f041e1a4d5dc79bd67e8d4d1791e06c70162885e6572412b88d8a32cccc8b1875c6f8f4083f6de569a63a1382c108da572c1d2cda4367d598844a959ceec13601106ee816b2e4410e9736c27dd23c616f19f3e954ae12eba3c01daf2ed22b649b50f1512af32d3424c85371461c46a4ce066379277664346bc56a845f04ab3a1feec294b7b6ab64980c64c18d385e9dbc60af35a5c417582d0d8b2d7d7c87733f9b3591ab4759a9958875a6c0f50067943882c187b39df2d3ee5fd4a935b62f67ad5b2dac64c2275d0451ac2d6b18073bbe7df2e0a60dc64d028a223802c59212ec7b713d806978cc353baad4af6cd14c1fae9fbe030f8a92156f5fc04e6d3b888ad74147c40d9b27866d56a631d9cae8169f7f699b0475b113ccc9ffedd5e820a74bce9a601d68bcbb100de476ca70bfc6d3114b707455b9b47be6f3e48133cc3493c6bf1e68069195211c0e70a2a64be69482fa107362ccd1d1b7e5a3a0bf2b00a5c5d52823ed5c40d8a166606f578e6b5174f7ef1b3d4a3726caff4917c1b7f080cebb8cab4097781cf753c203bfdddf21163197af2ea284dfdef0b0dd4c3495e13003fe46cae184fea822073add3c4366f39a0d4b06a3ca9dce395494726369a3993dc9d1d847024abe47a56b9fea4e9a97a13c76056b03164ff18c9510af99a60b30115706926d4f0ea3d4d4b05b26a8778cbf359f09f7252ab76257adc5b7741096d46c6b28cfd2dd1d3e5bc550b98fb84b14798a43e6da2b2132cd340415caa16d7912deaed4686af88b6d627754a4196eda35d51488ce5e8c6eb9dc9b9a30f997f90b49c95d9e3e00952556d56180ba9b2d10b14d149fa27f4ffa59b6af91629ef811817487e884fca4c4020c60548f47d8cab817003553bfc83d8aae53eaa546ab3ac92fd10fb4dd919930de8003ccb5560e2cf21e907fd2410ab78168f8fa14b6740a619ed721b5ffc2655ef01c2cf1494459c46ac2aa665208fb7e90fb852183680c2168f55fa737fa6f0d289577c0083d0117e719faa9adad7b93ff8b73dda935ee8f2241d4a2911b81bec72d949999f8a7cfd7be09c58366cf94ab6c7a8d60a769498d3c1a968e04cdc0cf3288802ed2fd5c5d776b1d5a4a162686c8bdd485265769e8c998b4ca8a34efc720f89714170056c2717a02e8cd6da41aa5f66100518f1421f63a7ed79600122df7b955aba0b5b8c41d133bf69551f57addfe88662ca26992e5acd103def6023fccdedc9dd5353a2b45f26944a79f0ff3fdfe3c89688520d39dfb2382f53050cc5526a1ea6ccef185d7360febc8bfafc80fced014406216a57ca0f213bae3d5e81df23ccf446f396edf4990877c00dee5e9cd7a2231f828f29f114ffc8c031d38f3023205f25f8a3c049bb7c8d3ea93bc13cb5d2b670f52b792b493ca8c96adf4f73e32b3dd85467b240329e99dbf7e0f9dd0153caa195664f76e2b4bf4950e9a1cae054cf1fd14084c6548bc3ad5e2decd3c82477e0d709db071b089af91c0c04d7ce551d7713f30c70c4d6208f65c9c3be88038423d157884f4cc8da92a4840a85239f739647612d8ab884e3b8efbcd313730ca246cc198a414e1af06882266a7e61a36d55e9647eda96f3b3208fd859528226449f332fee39232417b98e0b6f4d09180fb6be842afc28f78aa1d65686e3b31c557f6b43be6e94f61db385a58dc782a1830e95be2966b93a6f0aa043d55625b32a294a7c14d7aae404e7ade11a710b458203510b8222b7989a96bd83e8afaf45b55574e2375178d358b1ecf237852f70b9ef2e107cfada1453842b39b7eaee0819cec1f16742a02247a5994f679f26e66c048155bf565d99066bedc825f20b4d63116e0871b3e7ead8b55854609978673df12318fd950cd4bbe9464cdd095eedb4d21fa66c7ac17776a44d95bdd6307aa12a2d137097defbbfa84f908a3a08ba9366bf2bf40e90e4f224aaf50b4c7af1b8e92c3c1fe0dd395baac1571cd490447f67bf986d7afe6315b716f0e85fb3d61209a4612a5a3dfebc617ca1828e892237e301fbe1a3d55feffb9108fc0c781b57343724c6b8f6742d5ea06f34d93de51176c3a975254c63a454ed75227a40445cded4dc5a79415d74f8f64be47fc0ca359c746f5c73848fcebd40f3c5c7d0986a30adb53e5705f406071e150649575b504af651b07d24c6bbdd609e5b32bf537b9ce51034b124590a316ade16f424f93736b0ca607b7e80c927f007a5b5942777863a41713e2b225a0202441de2ececcb341f12710d07b3649eb57dc2c7dde9765378970f2d1c2d84450537a472d5fcb1499da44e28d530339d6eb0ac7a87d2fdc62304b5ce55275ccc242e61e17ec6abe682ef00f03bf2e7c1e4e113cc83064489680735993c4a951a334c46482e50b0cde0f5ae08c6b3c996b86c169111d87d3ca40c6708e7281a3741e4bd6a049cbb38aebec17bf70f8597f921c486ec19f35730d2d67b1118e63cee8f4dd007c1f6f935b0c7c280c29bd232dbd3e88d6706b8fc13f9c22bf078b11e7351c2062976ef0550197521055a1c66817362dbd4bfb846da46e7d50055c0a0eb7071ea8f60b1dbe79defea5049baadff1022c44b014f5eea34218c114410e0d45bfa3e077f1dd2e17b3fa7bcec514722eb426560c166b6495c2667c367d7891146e78e1c642947302c9096cb1cbb091f4f5549284ff28be717a12c45c5d4dfb4041186df3172cd29ac6fea2e973eb4aa62f0a87cd6a6a75c44e7211acbbcd5186248561ef401572f0f8c480fe0aade858987f13ad6325f41bb52449fc6dcc9788df7f610a8afcfeff42a4f987d89479083178cba92ea25038089bca8070b2029bf63c8dcf019e76c78f9a27cc65bff597fec2489a99871d0cca5d55c2328146ab7709d0a5c915b01c17cc693827de4e910d9dadd4051cdbe90f4ba4e4b2d55887bc47953b45eacfd84dca4b3d30496f7affb5bd55a3bf966e4e66e8cb39efefc4aa98124719bff2c6353ceefd7159c855e45f937e612444b9d82070f45b3a59485eaaeb1b0455dc2bb503d15e95397a2092a96760a78fbff804591fdf969b0821aa96c97dc2ea119c614c91a3ee892c961bc9d4217bbbd6cec990f34b9a3ef71e1b096eac5fa26db8f8e87437f249bfe23356ee832bdbb5849b74c3d4c2cd4efba21c577c45d8502c154a351fde0b73f94e24f4882127df356e8c62824efbc7026422437470c61f41aa18b6574345050bfd7218cb987a9ad0cbcc85cd5f4ae558c231beecf4b431bd80db0d599b56c713ee311761724665a2a9ab243802b10bcf7b4d8f0973a7166dbec7d3dbfd2f5a759210776ed7c3d1ae7317c4f21f5f0145ed65ffe4db16d0bdbc16c59749c12e2b1bdd70640a6621dd1cf6d9bae9774045d6d32d212bee2603fababd87fa509a8acfdaada70f885bcbb948e64672910d68c26c491ccac44e483a53d564f300bd143510c7abd146280a6921fe995b9579e909ac6f2b2fc20db84d1c06c3ac8b1c0c50d957b929e202043b477e2cba0a7556c0c1a0c071950d722fac312a239a3de5f580b5136eb9aaeaaa259b29668e192a9c4038692ef388ef6e6c451e5cfb81675b960719e46cbdb85923af4057490a0b1cc43a78a69f799aff2fd0c65c8336b01975120af61c7d50ad02c60afa28227de75237ba3426a5744e1c8cdb390c8c7ab7ee623cf918332827a719cc4b014093ad22d43b36cb55c005dbc7f45a6df8bd1d5121164f28bd5b5d314f822e059b2640309627645904e66633842c478e5973fe6a100fd19315b5fc48d20ba22a0ab83932733ecaecedf056e6a79d10566193e435e51d64ed102b46d05135826dc8a305ecc50b1d9b2ae871b7e0629c361b7112e46cce1c5b9181d55f293eace9bcdac7184d269cf153ddc8b9e599156aad7fa21442ca5d3f378c405bdf4b98d198967abf1e250b9c19ef07196190a38254e18b1d13a4894a05f8a34c78653cf7b6fb1ce2f11a0f1037269b2b76c8ce80190ce838af73551d39492294f75ed6365dc3a8d03bc21ab88994e9503021a988caf85d3dd8d35c3cd48a1c6fde81389d5fc1eb8173852672406ac57d1fb1c735bf74daf28507fc4bc3caedcee95c9a900c247f5ce55853c5e80cd4e3c624003fe19613b0742590abebf2123217dbfc05714b24169be603a3cc6677e0cc4ee8e72e9eb5b261549b9f98dcd8b25c32e1c2786fb8667d5e6269b9d9bd541f215bfd1259d1f0065eb5d4bc612d41b820666d91d2fa9248c9b7999fce2f7c9865f103908e50675f425daa216ffa74f0793d74ccbd7cfa14ae09cb72348ddb18b4d5728b615394dbfa22c9322c2211cc67897c2786340c9db0ae8dc3b56f747fb7923954ac63f43cf199d33c84515b9feb483d9186aa723f322486b0d6e94da62efb900b943b7f8d457190e0d4faf6a636c10de7765543589321265bc43c6da501acc26496642b6676380d2650e26841a466c5eb3037b3242517404d159404109b61989643ceefb42e4ae410eef52c3755069582f997d449a917314489c0ae0966d7ce598df588b4d14b60e3fd11a3fd5621a585826c4eda6e4a22375ee02d76e6671e47bb3ba8000de1c5d6125ddc4b5fc4dc0031a821167d5000a45d07caff5749cf4f04bb0a86168b3c5302c38aaee9fc784201d367f91d460af392102f79040d51e3326217166256ceb7aa1469184ec0e2c36f2dbed6f9c17a4d4c030a99fb82b3b0c99011cd2c36a33462c9eabee6ee432239eb0993df72d80f061bfc35869365f2d3f7db680baf79487b90011d0133cf4267da3f786e5d90f0a29fda3cf4c8081fc648c2d4438d2e29ea1f8bfdc80ce7b359c0fff9570ce7913316d75798e6b995825ecc0f28c62acb06f3fa31cff4b8a8efd3eee0413a203e384ce552b0ffe5fd3b45a69c8a6317f0a8d01ac5a7f10a62e6135ee2f137976ce251f48201039a8859b235ec7376cfa0053d16653f86136a61554dfcba3188380e5a2c870c6ca6936d30c8f4092b06ddfb33822e39acfb25b348fda587425cca7302e347a8630379c16fa0c7b367f54476c5d788fa12b17a9b0ae6b3c7b70ae110f38acbcdcdbe4bb93f39214e0ea2b182b8ec2a42d3433a2b942f878532b621b32cde20aa626d126df9e743539d268e491c007c1d0e566e2a94ea5d782e5f9e6e8424653f0afd40194cc95f0c713dbb537eb0424357cec89afef798bd4a2a2a7fd148c24c30b3d8ed3a3033bd2aa33f47f4b3ca3116e383651a80f3e6c7ada8077747f987b301f51398b0997bf84cec30d4bb688f87e8a9a340085cc46e5fb0fb253bd61bf91376210e669a9ab95dcda774ec44a2f3dbfd095967fc33576b14a4026f5b52c08508ad83fe6ddc708c9e64c9bb47d9635f18502140e6914164e3aecdd3919321d001953d6ba4dceb1ffd2b9e5d1d89aeee2febe91d4e088f2bf5ffba9fba237837e4248f1c57eeb16442329110446140ef15b5333f11640bd2d4aa955bbd3339dd616df7ba68cebab2a8a35e168612b1ee3166fcdac05bfaaa3cad7234c5adc961cff44959a37257f60d356f9451d00cca63955b6f54d3172f3127a28a439cd4849035acad65f639b599257f415a9b029139462d21c2c68a8c1a2d795fdde43c7fab45906b9c7e8e4bd15a75c79b4f0421782f58c544ec487e043485505338817856abfdbc5a106c3bd627810623c74840da003a91b98502d9dcc934114c7ba73db5ce6604e423702a40a5aea19fafb9b34133707487da5b4c356d2f4592b9897149bd39b598bb17113eebd2a9c62abc419c9ce8b9611ed86b6ddbe67e84c6c04c42bf660445a06d7921f9fb7ebdad6e0452577e9e98de20c169616b5496a7047b8391b42f245cb63eb6ccd3ec59ff7318a7197cda657b452f11798af30dd9826f39abf1208eb9c4bf80d2eb854353468f83f8402716f059561badb9d6d660302867329653879088a772228a468071e7a53bf06d64fc4c645ac9175b5c7a801c9d1eb7a51959fbe1c7c0c32f855594cee7afa73e96554c70e193554376e2380edbe1a8f138421644ed1d39c58c3e276e1656a468f85582ec1a76779b3409f4f93daf1b9941a9171c08e2f82d33f610043d2d06fa77344e4c03d553ad9b84278fd747a69a9eb76c8982cce7e9630d1d994f278db1580bcdb7b2576541fa6450b09d5e59c5f7cfc072151bc22a0ffe32fca283ab14a8f142472cdb371b5e312c2f66835895b50a1ddc5faa1d06b8cac22432d2b686103835bb2cb769c09373a75a00ea3072381fd54ea9dd12e51b4b67d848241cc9398ce17d1292f1ef2e48ddb471d777ac6660217c51e2b5e10d02e1e02d80b984d7b684ed93f91f973ab7b3522b6c0bd29b552f6414f73aa751167aab2e9ed1d4157dd1168692286df427d8f6c3463720d117b166789a9810a5d08c6c866cce67f33e7961d561f7cec610a676a68e30df5215426fcd26fa2d9119e7c81d5b6aee625e21518fd691d4839797323eee345970e81c223bfbeb49a5c1ac4e1c404f89a7955e1dbcf622809a3669bd54adad62390df0351f027151680179a34fbd676d1481ae2c167880e96008b80047307938682f8874f69719d6057be7408985af68aa3f5bd2e3b5e6bd3df4a34f1474a528404d27e3383f55eb29d9c5506d653db66e5fb2254e0e3cb1a7f1fa6d77f94ddd1c58c30ba2defb7923cc789793f764814c1a3939503f5058ec75bf96fecf456a0a770c940adaae444e014a7552b7af744da5b62c1b86c5cd8ac108f3552a14a82ea2a688b89bea5ded07b7e38c7726f728f5fc1e3aaccd1a11664db9bc29818667892f0ececbe33c583c514a44dbb81bcfc060155c705f1cccf437cd99694a9213d30064fec2e33862fdf3cb68249830d92a2ea19a97684a6ee98332ea044221edfb5ea49732392f6f29eba3726efe9bc625f23816d557914000f2a1b6a7da1cc0ebb4b4445a2423550ae038424c4dd0e627fb585bd1f86d19da67a59b8d8530ee903a194d89ef7ccd6edb45fb0a76d7c2e6a05e46f9d2fe0a5030d3a0161273b1f877dbaee6bd6ed0a47aa1a8c5268efcb480f70ede4c3502ff93bc24b7f89c6182a07dea4969ec7cb74e0474625d149a56166304de2b9f0869e68b644188ad7582a6f34222daa4e34117dc1a59b499538bdbc0986fdcf87aa711e3f8357692e2fd01138002c5b84ef5c9e9e670c24ce01b632961042ddc8237b26c29f7e3fe8ebb503bc01258d101d899aa43e13f7b52f6444af46836a34d99ba0ff41c92c9b4740314d7e091b7f0cf8b243b101c0d9af171762f05f96539dfd399ad2c6290bc6878dbccdbe94b96a19edda6fff96d160eeb4c7f0b63280605721d5303c71bae98004ae6d846829683c74e2eedbe7834816563cacbdfed92a05b185ac2936e7ee585326cff616a8d3866c873c8bec5a044c6825880aa160988f6c8a323fbce50cefb357d78a6f0c55fbb3da346b5959085070ac2fb2f6d55f9a23cf417d078612b9326581c55e190f58d275f4b8330b8757a3fa512a87dcd553bcee4c57627d63aa950298dcb3a0b95c23a72dac636494a531609b0bd628f38cd72af3e41b5a084165883ca0642923f59b6a5b16c19d0c9537f609bbc4f2873184f396dc959545be5ac630a5d6a7bc8d2cdb075db2afe2d67275f55cccfbd1b6c6fbe64bd462a40b89b29c997635052671a0a05a0b94a4f4a370c6e951cf9d301b65d7eefcb4949f7de68ce144e5158f11d0c1617e03dca315bfeb1cd66eda218d9a08b14313403c94a2c479c78c31c8b7f59159a4f09397410b00f877ce7a798f5554f21b387be63ac1f07790833e600e3dbba85442e96623c7f69e5f28d1b67ee7d870120cf543a8c551e3a4de9bff79e2bd0ce088eee1970853c3ecc54e72c64af832b578fc9bffbfeb77d7885ba7c781062a3042a1b5f2106df677fd0d4f618445265be8ed1d20dc49d5f30109527f788ba1e2308e981574e392ff3289a39cda1d964200a74b03ef425f46aac67b304c462cf580a5e4e3244901022d3ff90458a3301a2739909d4bb41c668ee932ba5f7e26777d719353be8401b4b0c629abf339b0f1347ee84b02127661b954e5d5c10747f2461f7fc41aa2ab48ed39b949ecf70eeddaf547d2ef82538e15069a4b3bd172731611ec56f6e4001b3ed1db6d8ee000040ffbf624d2e90622921814012caecae678f6b0be8895ce4bc503b97156d523b5987e6c610f6f4fa3b7acef5aed8726c50c596ac18e058c1d28983550bc56a243d15dc27f75621c2b98caebb28befcd59c200db58284bc944d44563793d612d4697341f697411ab48df646e7e59b2717a5825d93e55d5effe0d523a773118d0cd8dd301881da4b2c89e9603cf3326733bc45de9dc61756cb0d16c812bf2a6e527fb23293d3ee56a71c8d1f231eed20e31a764115dea47da3d819072a3fcfcd27d2f4f878bee5daf185e5813417b3c51ad1e1ff68e11ed99ba81c166e53b814ac25a8feec38e689703a588438b6131afa8d48437458d9e9491f76146b1fd0c8f90246b728ce8fa8928539b02a2705868d9b2c716473e4f82c73582d83e6442b9ae7fdd40a89f08ad5cb8f041df9dddefc2c5a5b9876d9f3b0ba2e0be627bdc6c158de3e8bed341dd1a4c0a24977104d6d5c962e7a23d48127d534de0cec6c1c121538c2f67a1adfcb4f376265db5978b6cc42a75b5fce42f0990c8b886b37454dff1f11dc761620bb165441d2e1eb56021a5e8126432e4c25a259fac7f72ae3147c53052ff4d86206ef47a36931209efbad64602841d928056b8514122adaaecf9182479bdba61ee36b6f1f0d8b5af09de25c623cf9e9f1e83e021a96cc4fa6449c58f1daab3c16fc8e7073508020fa49dcdab8ffa29daa07ef0cac2f23c8bd089090a04a34acc2461019fdbd57585fe782397f1bcdcdc165d4641607ea2de413736df2bef5767061b434975304d748f37f709acaf2f0d062a5f6a619dba45f514dec91d080f11392408cdfb3d144d38d1c269027854bc9cf923cac0b5d20287aa5f64e6ab4b6a12844365bc81a900d443f1565c0285d1d39c39f6e7117dac3ed8f823ce09d730ef1629a4e617980df0384904fd5b59b451c9b1736c9f86d2171aa2d23a8bbd1a17760b4f04be4c78f592a28ae0f933f73748f4659dc3db6d07141ad05af53f82540bc1a49456fac1289854d16aecdb57d55e0400e8641a504043b775a230a5e5b02996a8199b2be852071fe4021d287055ec8bc0a0fd7649520b96f788e5d03e5605f3af882fce5471e6b155023a5cb5965ec12e53e8627d9da5408b70a8b2f29a657af1935b39bceff435fd1c65e82b475744ddd0cbb87049e0b2d15dcd52f5e5527bc154ecee845698120c19280c77bbaf756c621326ddd186d1f07a3b7dfec034dd1c93d1ba70836988ccb73a33f66fc6e9e36314d56b83f42f9fb29004d96cd078a8a46c8bc7ac4d198038a93fbc577c84d6dac76ce8e048ea4ef7465f912ad05b958d25af5e32f06d9feeeb9bfcf5ba2bec55606ad3c3170c5dd5ad622dd0fb1f14308a8d2f3c86e61e7fca0a21229a84fb2917c2a5efacb9681f48b74615735a9105482b2e73821574013738570ce69818a8e82da13ed39a0e5a7a266205b200fae19344dba6c22e376abf55ba039f169372a9358ceab3a6b570fe2fc4d9b08959bd57dadf88b38eef8cb9d86ea77a1ce33d0b32036c769c1e1690cefa4c2bb9b05d603b105eb86f5b226addce88ec63f8ec1a7d0590436946658d3a5a5bd978d86a06c673e00bf630b6f1e42227bb959dcfbea42dbf2cf1bd69373334b1946d691bad5ebf1c84cc4b486191d4a719f32f7dd5709986d05d8f4ebe5aa31a1883bda5f8168f927e2ffa95c89755713fbd4a2515e64e18ed0400f14f8c8ccdb10651648697108e932a32d064654ecf7bf6fd2dce5dc913016d0aea229788a8fe2e751b55e516285ab1dabd8edcb55e6c72fa1371f830b76342ec3e52e733a09c37ca53efbb6b3ffd2141f48a105a1e5d722e913c6de8afdbefb8a59967b1774dca1c7e436d25ce48066bb2787d435a334b298b6f21d2ed4edbe58af0947ef080f9295f7764715b1d9a2b0d3968269f224318c359b4f2d5663d01e235f27cb3920690ddd8b49c7e6309272fa73131e0e2b0302628854e4a20621e793a4b08120cf0ae623075db28768b2be308d874ef7ed18b5448f5f5b606e0284b1ba5e74c90e8e1539b943484fd2a022e7986b8615ecfa4343f8c48e699c372286967afd626359841f467dd3579a667fa71d8d811ba03dda26388a0486b7af1dcf29a81c6a38b07efdcba2d0b57d2805baafad5b1a27fefe5ad0afa0d70560714972026f4835fd4c93c4838b594860ed7b88bb64a17e402428fb1ef77515a3aaceaaa585232c08e00ac6bed06504577da9d5b639678c1e2ae40c0e427b8fd97a49315eb0e4ecdfb6545e4a04528828ac5d7a9ca5dc0d15af371093ce18a7522ab36384a752e335753636e8890c95afe5bdb9806e7d96582b1025c1605c3333129bf3673c3d7afb881f389ce23773f514c35f0f18ec220c86fe751ebeb1a76093135ddb446dd9afca7f6f525723a22bd19c13604d297431a94af2c4a17d62c6d738a74f072c87b8250962ec34578fff041e8e766359711deba7578b4a9bfb41589b6d93a5c925e87e0444c536c671ffa14f8297744699c77d53372ec0370b952937dc81528b42d2c65cdb8259398afbf9b508a61c8eba3caf148cb9795e0058a6d6a713b367cdae9039e08b1b1c777d762d0018945343ab7bce04ed4dc0b060b5d8edfb7bd77d07f03e0457daf02195ea54a4c81e720f6c457d0687c4d8b31d0db049096126b88c1bab581b92f85c5330d1b8e959cc493e9669789795183b6ec5668a1bc775c23f13db774da0848b109ae94b215662a277b7b5304b09d7edb989af8e324f8b893e68c57be0c184fc087e190c9376b5f0648cef05cede21d06ba4d79ca9c5e26ebe598249674cf4cfa2ac4d4a1800a4470298c8286a96e48ce0904a7cc9a47b6fa6ec81bd6b31587787386aeaa45a6c2fae80247687eb91703060cda0a0e2782feb0ee98b00b20bdbd1961a52a68101737b433f35c8a3378ec0b37122f9b9408a8c10c16147ea0dd0eb846c64ca790edc596ea1aaa5bc9e6754964cd6298234d84aa38bc73b214ed0564fbf092cbdeb4c39052751b275f72cb50cfa6c0e3f5a2af43b145752ab28f0a80ef13b7fc82f1cb7a0c38e8896bec98c6db01317bd983b0ae1dc736c586dc3e606e3cdfd49353648423efb1f105e4c85e75f22a61dd471714e8c0c6ae13a0ed16cbb0a4e545ad539e87b14b8bd07af77b3ec8f904ed7adc53c55fd4d5fa02a5dc0fb459c0691f7775994e5d83f95d0143aee569c6365771fdbabe1633584f44e06fc3babbdf1a3e09fb3ea0df7942a9489168b67a5f9e6171beeef3fadbf013602ff2baea850ee540db44cfd4636350d7f3dd014092ecda7ee6850fc20fa1dff1a7076479d3a441578ffcb4d4d8e915c58f743973b9ec0f32bbe02bc4775a57fb7cc4b1b4744281ac3094c124d5fed62f3b8aebd3efa281bb860833528438ea7204597c687f771b08198b9bd6c0fa276edb20216fc79c730fe0a8c350fa0863bec0848ef2915fb4ef945f9f17c8773cce7672c0197d7d3eb8053a94b303f526ede9b8cbaa33508155f4c0b684f594e4097c0041b50dda30b9bf54ae188fc87398ab60deaf166dc686c7c4157b67773cd4069464f364b76af81f9d8403b59133a95550db28643131b1f510d88efebc4c3053be51cfcbcc05f6057929fb6fb7d0346862478ab28ee532aeb089c506521f2ec2907d11a9e8e0f3fb82730cb82315e29adc987c069ebffc69a0b4fbe0964b8ae6a7a36b0419d38bf7cbe2e1818c7916fdfc0decb761025d846594fad0bd80db5e3868e3ca68726fe40582163a9cc34f76a95225ad7f1dcdf461b96080bd9e2a4ca35554b3becfe086054a42597cbb8de091fe2f27d7a69e711da5ff7b52d0bc32ba1f75c376d0e32304a4c41e43d0375abf3920f2a73a239cdcf5b3d0322123590908598a9a4b55040f79c1e42449e5572bc65a03c2362dfa390ca69ebc30f2765c9c873f4087ab3d168930bf8e1069fb6c5191dba966816cdd7fea9b6e72711203987959b336897f993d13ead5f7178754aeaa0500054cbee34a3ac9cfe922e3ea02428adebfb53f3450f41f0772e86359e6deb5c8166965660343e269ab968bbdb8863090839dd18743cb350ab0448fe18d07e3bb04ef51c98f8a5717eb16b0477bb618af2512c6617df6426029d94b0d5b3f14af62000176e2846a9810a7862e4ff2af9214a9084dd4d57cf2782e0103c8ecce5a63d6894d08564070c0d62f68e75dc3099160738f8e302b8f17c280fed11c220fa2372bc172f0cf3b8189c093056d77ff490b5d6e6dd13d76b2907d3e3770d13d96e8f62c31d49c213139cc369b1c35f7941e2b2f038dc4afd1d4c8ab8bdcf93adaca21630fe4301202b062c6b4dd25977bd95b95a1d0e3b1e22cd5bf05c18204935a44a605b8be37d3c30c786979a825cb5d10df642061eddd56cd632bb243647721c5a8d1100012c7a14f4aed7c464b3699c224393820d7c213021597a527ab533be6ade80458e9903f20817619ccd2714fac67d19e77484bbe79adf403ef7d25548805d000cbcc3b1d575c35151136d9fc41ad0c38e67091f04c977ab10051433350631b3a4f6a1dd87b8a77c95d922ae885eb7cb6e51a47af1e0e66447b33d776307df16e5416c5232b05bb8ffcbf5e22defa3c0332368554519773ae2cb7603f4b7f953a4f06613803f14a53c14ac08c15eda269c9b3b76acdad5b78144cb7beba0707d8d07996da56c750da5904dff939c6e253e378989bcb102512122e45f1d8c358bd3368ad3466698a3a5444f99d0d75f7ca6e9f0f8606c9da0598f315a27699f81d786a935f019992371b1b74b532d49fd147ef4514453d9a64ad3e84037cd1f0f227b09fcaddd257284166af937c9ac126bc7315dbbeaa7f8773ed1a1fbdb1f9688613b677854a595b9494453db01fcd43ee03befc7291661368b5e2c275869a41d806388c26f59cd69f8d8637f583682df0cd9a758966dad64019bb1f510266c8f492d239221ef1ba8a642b98d7de9b85758fb1ef996c51ae0efd36107ba127ce88fe9292b639031670625c318eefd69b2e7ec1325cbe81ef18a171ee284b353d5741c2d3b2bbd28113ec958f77e99de982c0a5999442b99d61e07120f171d0d40303620af6d3ffcffb675fcc53ea97fced012b17b92fbc8deac6a55750028f776416489b090ec9c59aaefdd85954deb2ebff8dc6226852ba20a011dc2edceb970472a8ed1e4f62de204cc003dfb862d4d37514158afb7bfbc13fd5d0544ac90eedad92d35242fff847dd96a4e2a8e46c586d19d79641b6acd6c0970f49dec314cff36a32e02204c00b6fc22581f44adaa9839246603c3b5da5ce14fe44738b8e5ffef88c36030ec82421a5da273634df99ec640050ee143bee26603029f05db3e67928623a132705c5b2cc1565929542238082547e1b9aa2abb33eba32f98617ec75eef55008c1e9834faccbef2ec98bad1aa3911e0f98d402b6c601590dcc0931caaa83b254eb2376860d6b15d61871a0ff0ba2296901b4ad67772f6a81a2eb3bc00d8126f79c689770ea7b04ff8898f116a6a0d39942826e2ec5298a3950739952512a2cca5819ebf389059aecd3a0fcfde13e1ac9a71c60ec05c782ee6ff7564c492971f8c1509639064d86568d4ab6c6f68ba5f72fd0e9a462342b42157a5c21588dcfdea2a8f6c9bf6b0e8d2e235b5318b5093ab7d8b232690695939bc7149ed4c0c3253847eeb04b245c517c8ac605840193eabc56165027a24964b3dfb2d9f2d5bed63a5778c59ddb66ffc5c8d09b29e93588d305d272041a34a418474268e643cea67a9b71ad0b60228e8977c63194ec6ce7d3b327d290d8dd3bf0d6a90ff09a2f7e61355298d57e21d4cade827013c0bf0169a3ad9c1419eba284cf7fe52870d30904095db06935d0d6d231d0cbb5d5a8b7b74ed4cad1f512442c2dd828929944f2f6122c7d9aeae5a965a93ae425dac87f1ee249592b9f37e29f9b843670a026106613ea8a49b86613dec9a860510a6a9ede191b4992f3a081b47fc87c8d78247752744fbf72f4fad70b848671525231825934b40a6af9f0fc21d79ada6161a04dfa5ea734ef6b9575d2e375fbf23eda9f1c510f41f13e70e627d63bdbe752312f0383b9efab711388c4906cfe54a49b461232115791e58911856d0f803c06f6c4288adbe24604f568742ec6dfa13842bea9dffa3e15a72aa1c78d9b3e74a80d2cd7f2843a628032752300baa3de52b0efa0a1320fbbb5731329fe0176c943c1d4db146af5c3a578c8efbec428d086e162ab2967886342c34b5fa963c6cc64b4bdfe142b16d83976e42dbcf5493b8b2e62bbaaa669320dc1738c988864bbe6f2dc2b1cd400237f880ccffc780ba0b0e3cd7df7f32ce0406bc0d98ab0b83d214d75e6981648194fddc58d9672e7d4694f011b6a52a75e1d43bf2cd1eb5a758a45a06100e548cdf1a461df3374f043a50fa95dc7b95a169e46526441dd437dcd7e6c0c56e341d661675e357c51733807ad5930e8252f4c5c354c11445d1ff77043d8c20cda66b118d3f1f9e641578858d6dfe02426d98644c7619e70a7a54526ce7d2766db28e439d064269a3bb31d74600140d0fe70d5d043350d4315a4c3b2b1aa94dfeea0517a4e446d4808ca8cb1c0ec4a550a1472ca860672900b819d1a71b3640e6eaab2d4b2c4f38c1fd86748a01c49d049de5edc104574086906913cd2293f318f13f1bef26d15a612635f3d778d55042616c5382d4337724f63c98b4ad273d254de8d009b65b034ea04d20fd555c43fc785c0f4ce29fb562da7df33366ef6ad8912324c7f98ab5aef9c47220ca693a78f4c95e90296f3ab354cd95134fdb5542e99600ea1c9ad80eea56f37ed45a28921efa2c7b618cb05b9fcae77e2c5954d328114dbbd254d000600ab6856aeebf5c8d4e11f2bc888999c8290e20682aacd274397933e348f48dd2019918fd5a53cc4dfd53509ec57ff52d25183a632a93180cd85206090cdf63125fcc9e185e623f4bcd2ce2ec12b8644176665c1a05e45df5b2e9753d1e644539272ce914d2bc2f0ef2c391e259a919057e609db9a499c46e0f7e89e1c08797eed47fe86f25d111374c201f7e89d55b123c77ebb912ff79b3f3a82e59e9b98edf251905849e19f778fc7312a8a0f0b6712085f69b7a2909ea2fe56ce6f88bdefbef6bd3d721e8392942181ec97a01c8c1c4e8c77093eb60b723e6a092ddc503e26921d06bf38ab364475bd72a13d22d3aa7e432dce5128904d1485fe1433958659672640f3f8a4c11ae6eaa13819518fc9632dd56f74f8aafff433f71ebba31a7a3ef0206d4169301d3da392b0d427a9309305672c8f90e1be5246c69e196faaa4d26abaf32e3f55b4f3cf96b8d40b982986457c43f166356fe0ca45ee1eb93c60bed4d97058e9dace97c2f2fa8a20f755dd505be99a09e3afb4d17ad640a8d65e52fecfd155952d2233043b281f289a22ef1481a85f573d85f5550e116108c8ae04156e9a2fd57e84fc4350618856615325d442e489455abe2d1f9078cb88f5bd4be4eab5fc51e20f772140b1d511e0c394c96968be8545ead05b4e8a216db124fdd7b68baad58bc9fc35c0156d32e2076af524048c7457fe6fa0a494b3182c9ad7e9617ca3edd9fb36fe2a7b2233afbe73386f5be543f49532070501a6174f3b9d10b1c67cbf693b89ff58ceb08115385cf687eaa03bc6b12c6ee54737d01a8faba1c9f52eae05f1b353ac866c52314a3886960f76787e8e99a28ab24dbc221fabe8de4a919c1a0fbb26b0181ac4635881d45f53448875db3c8358b57c7ad89cdf9bbb3c4ad86b581317a970a84eaed064389b1c618f8a2a79f654999d352dde5866d7a32f5e6a8966ee774b118084f5347ab76ed3127cca9a019f9a394fbafe2fcb5afc40214ebbc2ebc9fef58cde9cd8cd5ecad8e9719af4939c7bc2a2552038404013162c98aaae4dd47950e107ba310426f994906862e12b47fa6f7b79b4663ce6dc3f939a1aa0ce2efc88b7508198592edacb672303a582c73e7a1aeee4b7608bf0acaf60acda058d92408a01d3fb257b47907276a5a6c82b04a582d5a027de7c992398bf69e1c80be6e7f7b9039e0142b0edf6190486fdd794c7c7dbccd8da54165ac3fa709e01228de9be596997ecd5d326307ea6c0fa4d19163bab548cd4cd865aa3a94613559b0835efe609a60b4f3ea8183a266147070c2e983efaec1fec72ef4b1b661d7a24452c9b023e541ec6f98f675e11ff3ef94b8f20b7c720c9931a85be2a0642474404b249d756b3064f81fcb7779a524162e2e4e4f1be56a37063fffea25a3a4b77058913a437ec04bc9996ee7b18a4abe699f24c42c30ea66ca8e4f20f4135efe70c839a101205bfe7337ec6d5ca525b2536a8ab5b8d80ad4f5742ee9c1547c8ac4179661516922e090aafe21d0d5b2934fa57e410620968f002d4314ee24355250ce7b4f0a7842913a72212207322f1f3be5cc870e47742f5bb684ee5a6b4bf9e83e5bd40f879b87161b14a4291381b346387a90aff194541ef714aa51417d61791e00f2d35391caffe1bcee0387eef3e6a644578b8a8206f6aff7bff52672ce61710104b86ffab4325d960eb0f4ab156cb1ac800529e56141d5d59667c977bfb6d49eaa4293679463ba93e515b4bc06f8f991383cc4c7482ef60b4d5a9285fc8ffeedd1a30dbae296532c8b31dd5d033cacea55fb55f5ebd30be898c0f66f237d1edadd659dce4ebda3099beeee4c7903ad6f448ffcc9092ff42682b6bd778194f957ede46c40c703766f49b9f92600515d392c3360f334f5e28b7df8d251f50df0e19f3b116baf8e0e347db2aaf52f371f149cd1da55c725968b4efd42e58537f71e74d03a05feb5adedf709bcaf13e4c7c0760afda28ad362a85fb5345d47b50df58e79c57a19d818c0abed7c745c8a814df538895e5e0abf8a05988609ab7bccd4bda9900365a207855c607ebf8fb3e813bf7716e1eb7bb2fb9e004c4a0f657cc100a6e84f71e557d5fae44dadfa6e2b0275360356e7ff80d1efb8a6ef3f32e1b7a6d0ac42b7e9f7a47dc0363d8f5bdaae1b92eb7d58d0b7b32361ed5796233e1af2daad29be5c17fa78550af7dcf3d22f1b40e51d135fcdb19f0bc73d3f1e144de0ab84b0c90a838a4f19c29bf85d3a91a0790825b8f06f83d628fa02552e33352359284e386974b9807151b62ad7eff0d8e8c1c28d68fe7fd6ab620fb1f30d29a0e2da931696f1b29c1a1701de9073d0b86d55bfabefb6c3bdf1e715f820b9889dc7081c469ed4598b512ca64ed9cbb185ff9b20d6292e8b9d9754d10d0102b1f67dcf4224aea5f57cf314bf9c1e95a962cea25e589bbfe04628545b271bed0389fac8458f7347d6418e1ee36b3f174c19a0e4765b4a1e43f03704a8ee717759c66f24c41f502bbab0961b2129144480c1b42f63eaa651052547a37ddec1e970c85fe5b01c3ea363ec6d2cc89ae4f5391691ae357ba5a86693d134ca467d9d53d0b4f27e3ef140e1d22aceb99ec9855f1506973ec05ab37395d0606289d5b2a3c8e31b206e8458611f2bce01f56d808f93508e0ec754945d429ac4a63d7e00540b793f97a726a0e3b04270b704187e6b57b6b71de2930afc22f54811e0f7fe2c557cca5534cb040ef54b8c6bdcb607cd0a9d3661a7c7e84c72c5c892ac09fa72a216c60089b746a00db47db043e91132d7af9ddcf11c735348f3ae62ad8f8588ac6340d2e1f5c4558089aa67d8414d61aa018f01eb91c69710b0ea082d33b2ca99cc48059d767e5714f21c612dd870ca79d6da64775988334f52ea1b95abf61be3983ca6dea6deff153019088642970a539ec0b91e5f40b3df758f5831f6435d48ebe7238d5e45bc01837f1f7a5972c7ea8b4a0bb4cdafd6da1e025ddbf857ba6e9d3d11a4026c5da04109ebaf047331b278440434b00b7bd0afe12a1a3f208df238dd3a75d53a11e992045911aea37c54588b864ce6beba55ee80570edba9e8912c82cee0d3d1851784bcf21cbd0bc96f0bcc15d9ec2b8b22d87c5236a96fd4a42b921384567b1218530c4f0c04f81ff84879e622fff849b21e481473301c8b515b3ed6200163304f50c8dd455f14de11c49a03547bc1474f41922aa1c30fd61f7608184272cf731b0f41a0f0c9107d8ae0d5f1c75072007074c65ab2e8f22f75e58884b50c453f3dfc7745779e8b9c0e3f9bb1585c7d94beb1f68075a10624573dc36821015bd4aa93db0f381641bc71c489a319de912cb6bf6f7ad37a51d888dcbbcac8821d18899b47c61d25d7c26a96aa03f188d130aac89c639692b999a6c904763bd064bc6501fdf198f80709ef1f59c936eaa3dfda4ebe72e6e85d8f87d7b75de8788cb0860bd9390eb873c5bd67932bf10b16490506c280692f9e246bbece07f002c9aea339ee1047d407b8d8e0a4fc6c69dbfca87c290f7165072a57cfe17e38f75f3252d4d75392560eb2d5f3646c1eabecb4dbf5738711c1ce953eb5ccb693ea461c07cad0237a429cda37a7039410c01a57f1e998503cf6b4fc6b83a17ba8acb11e6f57e93e22c1c28fc5afa8016fc407a7a517022388ba60f39cdab5b4548d7ac841f3d212283c824c70dac3fb1992b1441089965d16f297eff28e04980d4f900e2594d08cb8c2f99afef0e448d4bed8bbc7085be9c0886ae61f6897341e4e324763eb85cfa14582b2620547dfee3a6b5448fb1b127d04fbd5c1a485bc5aebf467eb80254fe8d4fbb95229afe7654b8045bb9893dd03254a12153ad820b88c6ad8429c5ccea84e870010c5dd165dee8094e87e714b2016be066ef547c63712e2c3af91f44edaa0a9c15548675cefbf281cf242ca3fd25cf3a99f6b4eddf75e0d545ce6e23d7dc392fdcb2953ab6411f3827d225a4c534d7037ba03e388d08db20a50805c22739ca42d0f8cafbcc3c83976e3840d93c0c9a938a3d284225862d224a2698c63f525e6ea727f71d5d0fc5777d30144454da305940bb87be07265037c87a322b8755d904eb85017b7dbab16a62ee40c46ce0a925fdeec8a3133bfad03dd9564bf298f39e8f676445b6ae12968b47c0c1d7c77d9492c09a47b22de971684d58772186175c36bd3738b9c67d4aee7d02d154bdaaef65bd084a054b16c5b7fa20064e1a8eea5b9991e9e77fa4ad3aab6916ad9db8db17d96d27f78173fc7e5c73d26f6d893f55490b5ccd1399d507e6f6c5c5f021f5b02b3512e7dcc876dee71f09b044a89bd0cb446867cf5c24df57b7f3486aad5f47fcf86d2fa1559c689d989a68bea80d567abdcaeb9200d67e6e6521ff2751c8469db48280984ad16a72aa5d92514d5c8351265b3c0c0ee32892b47286e64139a719d830c85b48eb016ec258e2b201ba499873611847a250c99b5e19f0aa11e353ec7d19f1c256ae08dc2932e1f03293fae2859cfabe89bffcee07416802722bd11d0ee255894aa497fbb196a3ce75094d3badf7d8813c4e737247c174f43f974099ebf5650d300773823290317867ce241cf6b75b5fd6aeaf582c8148bc0c3249332b3b4acb347458a103e2cdc2952b016a4861853a66423bd4aac80bc8d3d782aa208dda13dd8a7a7e9eda1893a95191bde647161729026c9222a6d616ca7a5f065e50cd4cd12dd67ee4233fda758f58651bf73112e76ba94b8822a83e9a5e3c34d0a66ffd1789ead204ee2cf0b1d3c126db7bdda373da2bd49cc5f1da6623811d83d77bf87e671a48bb4bc02ee4b203201fcd45c2612573f5de88474c7b78ed2b31b3bd290639218d8a2c440150b6691b5254f1860df232d63cd0646936d12f1620183d6547947fe149e32243b0f1fe992fc115427cc576f9c575386c4ef4b65994028912fe0b66cdca976dd4480b80a56b49c04e666d623bb975abd66c8f05eea38b5dd2ef1b4dc1a0915eccbc797e16b66e8bf22da308fcac6ceb7a030789114437e200897a2af5b6ae21a2cba84e696a56413e512d1712bda3ade9ceb6759ec057ec78c3f3b20319e4f01256eeda09da593a88ead55af3711562bc03bb2ead7583377ed741cb42f8fcbc0236eafc75fe07e355317a5493c72303bf5e4b4d7c53de9677de6b083c279f516d3f919886b09c472a1a1be0cf5c12e85038ae7ffb345366249d32daad5c009d24c66c05f063f4e11e8861070215656fcd3a063e4e64bf31013917e47f070125d6cc5fee5b7265a8553b22af9c3329e56d390d4a3cad3ee240f8888eccc1cd381bce40fda60c7242fdb1714c81cdd410629da24a792fa0bfa9842158e198abee4a07741a7e79be8a97162ff6df5c76952d37140c8634efb117bdbb0c476c48a2f0b421acb3731f80a0169fce49c271673e7881d710022d0e7fca7f44e4271f0fdbaa78a0feed7527f144409dd59f5ea450ad4f59623762e7b28d0f71f59c96f8002a1c1698750dc2fbd34e128e33c15189ed142074d530d5b5a9bf8e540085a186ac1c0fa015a2d731f0e52e19c3745e9522c2e814661d34a034ef51b11af45cd0eeaf1c55a9aac9ac99b1fed9a8a2bde33ea4c9bddd3ecb7f02bca6c1121acb6b6b54eef19c0b088bd9047cfc783883da027c0b05aff510fa40dfe2a51fc409c70aa69d53331a95fe2bf95d12a9925bb10b8636d97f99fcfd32ef4e54ace2a403b6a61b322a99561faf4c6a09855630b8b67d863ae37aa5e3d0b0807be34fb7526513ccbecdca08c60c364c2cb1d3bcdd61f7a08a200b433a8159f9ee11e25655cd03141723795a42f9568e682f105d5dfd29568c7a69e015d02be3351c38d74fdac44e365de176a1925afc7413954898c5652b50a1faa5242dbfeb79d73feddf0b86b613a34ef90b17c3337ea40432abc176786eaeaefb10148fdeb3180c11da7ba157709a6bfa7a96cf91f3f336300f33567572999b4743159483dd54c0b7dba560b0af48a70c3713fa3561535c53a6e160370066b35fdddae9442d8105c027766bf5d0081ef93e4e06632802b79c02c09609d79b8ffd7a2e94e5c576c883e5a8dc8f42a4ced5fd08c87b78cb9949e6eb9737812ce55940116f9c6170fd08ededa8228734a804222539c599043008dc037d6d088e79d8c082bd6ffdc438dfb896fed76ebbd7c9a970f8798d0c650dd23bab002d716ff3271f33e53e249a56ede18d747689a418621b916692f6c10ede957d9e3d83a3abf814e5d69df2e1ed450d47102c54a10e0d0ebe4cec30385111aaed928f816c6731210e004424d2a8e71af106d03642d9437496999e86e768409f853854f7ff0b82cb73e799bbf754d527dd71ec64f5f4ff81dbfa2354f7e549c5dbf65fd4a9ce0072a5890b0a6a4400647111bdc2d55e03c3d58d4eda66a5bdf8603e44c430d94947da07ea5601d9d211bf9d3efa5ef19ea68fa7b6488d90e6e2277013fd987fa82eeb4713e9ffa3baae225314589c60a1389af1fcaa6588e9ed99f222cddffe26c8e2c5a6f4a1f909be0ad760e901c05bd6a5e4223a060f9ac8a3dd37439c1eeec8f60a8374408000cac8bfed9cb4491bef51090fa5003b4cabc7bf81cddca71b3e37fc9eaf5bac188bab5bb7f47f7f29fd698a89cc3d5095d378eef28da3ae66daf1838583449d1146a98e26f0e0ed8aec57e38e942502f803a033eaed4bcc9128c42bf5a4ef9f34ed0e4e72d9e28b620ffbbfe5bfd19285e85ecb34fa8c27e94daf66d8dae241a19f4927486cfebcb5e4ef21c0956437688cfc889dca52e4bbc542d1fe449a337165dd26cf39239d24a580df0583671cdc2030deaa99ce6ad97fc31d75decbf98374b3c06d426342944d6d783f4a11ab181f97ea3811ead021273dbe147fcfb533b19bf2569457b25eacf480c23836737d376f9f8d382ccb9ad31f691fc6b7f5e2debe2863fa120225b9990248a962301b6762f26662bf8afa725dbb3b455a9e5786887ad85f1805168cbc04422cbd48f0e88e51d160070e02b3135f078c6c9e486066297aaf8c258bd5d2ca7335a10b1611c180f492cf45428430c5b9d33550006ce1146c605522d3229022b214ee24e5511abb25c99acd95eae2e03b91de9eba34f5fdbc55aa2b62f535e9154394cd8f89c7c50b9b035322ca22e76f1de0f16c84eaf9b4a81d1a5920aef2b21e2fb26a917f6487196aebbe9ff36d028d26f2a5e2617a4793d577909176d92e1bb75fa2b80c1692994a37d41acff9434c937acb865a18fa0f4a375f9a0fea1b8cc7a11834c2bb5906f4deed7be9b45af870484ebe95d9734be8d17be68679e0526169201c99275781ce7f924e314428d2bb6ce8058711dac1353399f34b73c1c9630155d331f35a881808caa97a634350e9454c45f2fe0c32d3813a66f4fb86a1f3a9036580079b7d105aafbcbb25ece3baf5a0c8496ff95af5b818447f5a9a92bc45c137e475e23a0c0dadf967edec262577425a4853241f19390285f6d8ee6055a9469f07368cd5ce803711b10f32ac4180fe1af17d994e8e9e2e971660b1b24fbbc894fc8c7b7073b292c2e38ca5d2afe7197d9d9e41360894d9f5f285627fc12012759974fc5eacc2f9ac4b5875d6ec18d3005aa9fb4e9ce0f3e3ea9ed292cec4cf63bfcb758843dfc0870de3a4a14ff0f8cef85c2235be6fa7889ad2ceee10cfdff8e82e5f7edcb6c6997d81ad1bcb6947991b4b4696564f5644992390f0e761e4f81e8c016ade5284646c3d8f4510e8f1bb93fd371d13dabd58ce4f5f40b09eed8d407506460b95896fa2029bffe63c35a12f2820fedb4390f88a18c31a84e2122dbdd8eb45607256740a6c851b5edca9969c56b0fe651b0a6efea2d733914c60f4d1ac0286a013a159715e898a1d7a9cef975a1c9260d742c832e3a177e94c37c0fdb2619b0cc05a358df89048edf77033a3bb4b88a2ec47858c10c847f074e56fe48e24259a81e71b8a4e6f10c74ad4e56a60f6ed2157441c0b109cc6f406a8b4013b709c8b0d12c4d61a48e370b1c18c7dbb04556c9554ff49654966420e493d1ec2538189d6071b22af17a7212a9888d8c75cc347ba5322a3b333c476643c69eb0a4ef607a6cfba7fee2eff0da47620879f1540a85ba84bc34172f250da3d4fc754328d0197521024b4dda1a052b7e51ca84dd0ef4d5b6eed64c26a3f4841be561e484025cd14e59dbd2b4893313a527fcf37622303d11e8b213e18b1a77a81fd05ea56bc48bbb194ebb6581befe460a976eb09bf14f5fab212e39e176c4bd82d90bd71787ed82d044046456a7ee232600676f632480e5f4e058aadf4c9272ef7a84a10763bea8950a32cce6bdde9536ff3bc30620a227319f5eb8afafe3ff0f2942e6337335a6e72c43313dad15c0d4ad08da74ccfff8ebf97266d93f43972b4f0b42c5c3f64c69fb81181aae54810675aaff6b7899de9b1a0ab03fef70f04b3ddc159a212f4e6cfcf083cd472ccedcfbf08dfe96890d054664587926177cc804df924961ce400df126073d7c9357716dcbdc600e1b436d958b1cd1b0f25a57f877861da2e0b14eda815baf009fe1e49f8ee76986f4d262a2dcd18b6d0d0971424651beafcad484cadbd8850f0b0d1b3bbcde1f666209c7a113105c791d05cb8100b5e5458e4f46970b52528a65127230cf57c3bb288767313ca4de5ee583b8d905843c85c665c5c22ae821b68b60a4387acc4be0ef5ef1879de7d41767beadbbb32423ba09f4713782d442fc2ebcb0e74d4d9f3d308cda27f301c1353b653ee89a02d419a2887ccf7a6553befa513bd0f2d666ab7e6e54a66211df5d73365ac1950de20d91858b2d7d7390074ee08a88f930cc87ba0f9dee03263e0764ec428ee4ea3246e0053a1688d3fc24a0d12b5792506e7e1c6d8a5cd7e3692b54eb2fec64c5b08362cf98d17a1cbdc57d454ff4906050f7dd6fb1da1d047a5195cc16bcb3adcb95a94f3a0d148055120bd9e713c765c336064711b90c78871f532d6eaa9a61a5c1c4cd3a1407fa4021295c57ea2c3511c07347afc0a0984f624e6b7642893002e2e4c54db18a8b1670a9012a9857658377f09249e1879035de77642ed8bae95079ba11de543c09ae699d494db8fa5079621d8522a19c9d7c5dc4009835be34d3fedcfe9e3dc00934cc29f1334d13a8a5cf64539f12ce61b5e7c5057319ee4a70005b988534420393f39a49d229cf937b553da956c4a1670d5d07fa7d934b8869c52da0d408e3d655ffaa374238c306940401fefffdb9be388580018102ec25cebc7f139cdbf61c9046aabbd301c49fa0ae2d3cf05b3612985efb26e3c0ea8f9357bfa77d8fcba887f8ae44919efbd04e94725b65e766fdc7f6e96090702135c13430b7379a12274890130596de7fb88b947b490f19bc136a686f79a4299caf6e578331f0a3d461f31789ea2ccd48fcec7abfb31abef0fcb7e35157229db9afeb89b694ddfa1afa1fb2b9530dd1f6c7e5eec64c916ca8b9cb1b62db1d9244f26284388cd2ff0653cf6fa4d8c889b215185c53b30ecf97992f613702c113ff20115b8a5dca2e08dc36f95130422cfee0066510c90a6f1d45f736b77188c6b3a9750764dc4a397fa242b2a17431e91cb83c64b32c10efac9c1166bb23d2be130b85ff69954e4de236a45830cfe670507b1cc663b42fe5d1d7a8342508c30f6c4a85301cf14767ebc4855831f06cf32c58d426f31cb283f979745d76d43651043888e83d6f7a76cf6d7d6aaaa8ecdb73f019a5868d02f19076c13033b07b0273d8dc54ea1c7f58a8b704b48867a89a2e5b4d72b00bcad75e87930d7764dc796aac6c950fed774fff9c3251451f90737afb4dd1ad33d7a68081e7721acce4ba8fe14ff0d5b4b0cc6df26d04374d012c0e416244e451f81dd0eae878779c2f4af64697f42373f8290da11b861b8c8ad4b42a72b78efa7ea9fedf7a0d7b71012d6cc2242664f7094a5ce3f91f42c9ecd4c282d0a83241b105061f1159cfd92ea3a72ec53d38c374798a3048024000d944fdf9974f45f5fed78661a838d88a5545caeb571f4986d2dabda77b19ffc6bd439944d02e9a8dff30a1f5c98cf90aec7ee40865f66ed2df98f20c25e52785646577f285bb9fafe429df49972f39ddf82b2e65650ceb94f1c1f5d6f0a92a4385d188ba32b974cdd35f06c6d0238e6e2d0d8dd2db556c46983f5e2099a2632eeab45e2b31c9b7c2424013cd46031244a47b1ef459d4958601d18e8c0ebb5b847be68ff93609f9494bd5bb1972c8e6106b95a522dfe7e823315d0bf6fbc310f6907bc217b17dfe2b1409ab6ae01ea6cc4c83b2a40f1c490ad1dd755fa13bdbb91ef44c5b63bde9175bc28003176bf9f45797a19c0bcaa79914812f527c5928ec9ae0f33bc7c167445d49de074a7d6ddb61dc08fda200ba5d35eafe103cdbfd910e1826d9e149e66058adcc412a28e7fc1f7719c0a6753f0d0e2b258a230269c1ea680086836bca6dfe4dfd8aca62bb566c5a00f079d3141e4bf3d6bd296b5653abb5fd0401d8303be1da2d58a640b942b9bcb4819a535a157ea6b89d8c24d6bab01c8098b23344ee01301c184050a05cc31d193bf6d3de313b0cd0c416c96011e05f3c450b477e7b4b3fcf712d96951da05e222d3c9c90710b562aba5522278b268820caf25c6168b3414cbd4456fb830d4bde9a9292a345fc0c390a50a614534511dc99582b81156acf0e69693b459669ea0df0fa3af6e91633032ed9071e6c934eaaab1067015c3c31b12279149c9305dfa836bc363d03ce6035e6a3ca06d581bf011816bf32fea30fc12cf476c15905d07faca97b3ac0557d34041d876b510d970cd8fd6c9dd638b030f25b54d9adc8ac3145f8a86692ff9e81c4c020525eee576de5231edc363714a16e1524c94f19e02253b094cc8adb4d4076d04682a24424f523db77d990c2271d5fec7cda87e346643ade338d9ad538f63fe2a115affa7d12967b80cacfb378d7c64af1763b09c11775706c699616101271a14d27437fb662f7a6df96c0ebc18a72fa5294711364377cc8e5c51e39a39391cb7fcdc1377027fd09dfa4dd96836eff5eb54494871e0525a263f63e2102681731ac762d719b0ce796b2b386a6b8ef735a5290f5bf987ce95c3a908219c9b3f84ffbd386657df213de42db473c5765596faad56853c655e6d8e55f1d97d86af5e4cdff3c94ef3f4584b3ab6ca3b040dec45a1d182538ce30f9cca73b2f6134688364a80340df54c8fa705b3a4f36130cb28633c3ac3b0efd03424f2f8f448d70e5641fa3e3c869897c75ccd452b3819655768411792c8af7ae0be13bc0269cbdd07b4d4035eac1f5017e60728ff0c71f912897a1fffeafa6e709502f3ad94e69f0633d03a5268e2c6b07f942f78e5316b02636da753089cbd4faf58c7f7e524bb19819a0b9e4d9d50a5774a914146d139a268b472129ade4563570c7f363ddc5f960f965f5cdc84a7272b76c41a18961791365bdb22b7bb78230b80f37f8c70af086c617edd7be3c677f9faee4b7bf64a70aed0fa9fe0bcc6a999a48d838c7bcf6ed026273573d424f262318fbf486a5af427fc38519e06f0e2af277f070ca2c1710e47ca26fe8c02b05124109227b89b0ad97dcd2ba97ad529c105fc37a0e2e9521dc1a727275218020eb1a210cd5889006aba11fca65162964295c886636e7eb24a552ceefcbb28503b1ec5cd7faabd025e899ab289c5419e38f48024bc40d96d275b9c27324ef955f1d9aec23da3a3069815fcf2707c1960c2a1fcfe2b33432128530a4a37707cfe048275c6d493858892cff49bb557ca9426cb079ae8c890752a4ce309f7059d30c470076a6bdf7b1313ebcf8fe36024a65e35354cfba6cb1f85318632658155c3095378d4094013a18a12eef60fcd82416ca47c22fbf0a4c8d34ea3f46b84e43ec5c5b2689bf961a4210edf8e25bf6fee1f5e23c5d5a5ef1594621b46540251d27641cc3a6244b872db489186e396ce51d373c788c9d338583a49cd681b07d5d0a002e5cefce0eca575462ae69c0cc28cca7b4f85a58c81b1ea420ca8bb6927ca0c15d303e5bdaf96b8b7172d1b289f4307d001227bb72c93e2c25f1df7476c20540547fdd4443165c9bef30ebf9608df76a8afccb52a07a34cae8b695459b038d9da8d0fe9e4943c94bcfbef300f3598f6082df807a802d08e2dd34bbadfd10a133105896feba6b428741cd3aa69f20c11ddd8b73df19f0c8e43e86a8670e183ebf2afb8b7c46b9d1dd36188d0d1259768cb2da064773d1f41602daa011311c0e2443848300725d48df3aa06942187294ae3da1ac7da3fbf0842e0c4fd70ba3274ad3fbddce35b45e938898ec0b7a33dcba1b0c97620b99db0de1a13f86eba5729557c545d7721d21db14440268e52778aec869d72a931cc0d70ed456574238fd9dd60811f749fd5283e5e230761e55c025acd0e4f6d031bd360aaa8edefa4b3cdb58ca118e14f817d31287e7b7d4100777e9bd65d6b0982ade9b44d3c91d03f030769b70d87f19ef32bf8b0b20567decc3b0f34efb387f08ba5eed303c7e9ccff9c7f767794cdd1df0f7a24c9cfecfeaf59e91c68f705db01cf585540d9c90e10a1e6a6ceb352eea66f25289382b96901ffdfac5559bf1888998ee4bc1524c851d70b3a8842c77596539ee6c92dce69dc28bc632bf278822d615b33d52b9af0e8e481f9841f214102e5c0816d8ef6257dc1c27f50a7c34291301056b33a89959ae46bd601168162359993c836d6380ecc7bcb9329f677d4574a9033d731431344c8db8114952acdac0e18f5c0d4728a7f8094436bb31eb47584b9773915a1a3f27f260be8657fb6cb3755564058b760d651869110115c682beece0dbc5ece70171bf64562ac37a502825c6dd3629787fb8cc8fa89d1221cb484aff4265c0a5dcd5268c9cb8d1c214a873db5465e17a45b31a29f3c6349acf89b58235068ce84e929558482088f52b4a33a9b49eb090217baaacca0eb79686cbad1856bcf5bc8789411d1bd599e07f24c0c28773e57285fbc3019176081267fec0496d241a8ac592d352e1c55487f1e6bfcb8a5ba7bed8e5461cc6d62a3681317fd1072757e0f3f2453ef078847538d54bc94a1434fe551e3c54c77e1bff3ecc4fedce38cb8fbbf2a28b71203b48cb497c3201046d4ff35fbdc6c9b6a0ff6ac77382608be25af9187dc1180752000ea5ebc92cb7030eae15218d7dbbf1f0b8de8b832d876536dd924dabaabdab7a9cfd180a27bc411d01c2a54e2a6ff5cc8f6d1d196dde70fff737fc383071ab4169d3a4de7538b8f319e0b0bfd9cac138baf425d68177acfd891645502ea7832527e062900f6be5e6005ad9fbbdac6c0dc0fcffaea23810b15cb2561281ded7b66a2b60ee25ea703022270dbde46c8c61a3dfd9115e0253ec78ab409a655019d42a98d11eb79550f15761cd441e1450eab66ed39c0ad7c7cb302caa3dcae59be62fca02552909afa9d0c475265458cf2df4ff58393c9fcddeae4e97efc0563e52fc67baf3c6a2f04dfd5c105f349b58f5c90d1aa7df9e52a603f9714bfe43e31f7fe726e9ee5be21ed84ce5358191a74b032cf169e7f8283498b6d45b4edd0eec3c321c9952c7c65a79ccb659a039172041f7590d4522cad259312f9e6d898af51cf77017ea33cf73bed93ec3e4c0f9d9a7aecdda8ecd3e8d3eb6d2a41824f889f617db66b86fece1adfcdb17086d0aeee1fc89beaf7a4b1bc0a293f701f3e08f66e3c6d2c8db17dc37ccf754fbbd32a48ca55b3a0bf8a4a30194935252ec4db5a5751daddcb41f786a14a08dd1fd25ec52c17271b60768a23600c7d825b7778a73c4bc82257f5466dd98a07ed05703de6352d329798693fda827844223596886025d62514d1b90adfe9e198a88857f615f3422ae42b3d3f540e1ad2ec1a9c76f30d355bf8156101f389acee1fc9ebec419cca9dce2337856029272a3c740bdf6a44a7f7d86fb7d6da76ac20eb9ebb9a96ac8d8ab0ebbc0c566b74feff1d770cb345f4791470ace6a6825a0501837e5b243e0c2064663ff015cf7f63e571c5ec9f2c1f169b7f832e0585089dbc714811e05a7d1c49c11fd3379334d3bbf4a3fe91f47d3e5989092bc36477de1ec6e57b4a33712215a98ae7a211d8a3e723e7ac367e53aaf2536915114d3632f6c7a95af160a1ebaa6044f8b6eb78ac230c219fc39edfc0c1be63a1e2722e73a5c97b48fc3a7fe38bdf8b423cc8faa073d77c3c84534b9084fa3b603e8d32db41356ba55f34be964bd93e91f26cc0a663ea0cec02d321793a5a27bdf3c2b13551afd78953d6b25e5e8d66888f8364b063e4fe57fb025a596103c5fc64176a9e88ba7fefd0fa7538031b767e498632a7edf35417a6331b72ea756909e6af1a6f8f55a226742bbb8d2e5c01cf3e7ff81818bc7efba4df47f8b48e7125c500c02db2203d083c6a257e2b8c2db023d19da6c26c3a9a90a50b4624a7b9ad05df69191dd4c70449509db83278dbc96e3e83be3d2b5ad1b6428867eda9e4f814513781cd3953a9ef0981da7f06265540e0cd30b861e950167c1d01018e7a1670ead2c698d9c369b636a7b62d3a4069444d2de5666acd20670666e9c395bd65f5890a9aed163712b18dfe5271adbe0ab26951235c2d58eef3c64acfedf039a45af620eb5828e115eaf448edf90e6ae4dabffb8989906b9055e70fdd2aae82ddeca0bf9d41a155a9b1384d627ce7f699f0046a85aee8d0d6904b78660d18b3827cc899766d3b36d7aab42e359590a1d95959fc956dadb9c455d8ae59fe1e52c34f70f8100c50881dee2af380b97c4796ef6a5f006e52253047d4376ccd34082f5da8d7b4d8e34afa7f677b5415a9fedb14b029b35c66d9e3234f52921d3f42ee8753f559dd4548a67a2242942c8899d2cda67d5d9a8300955146604f9a5cc767db0e75563424f3349f4c96ff14c062ade3500a3383d793e82dac9f438a35481c8e5adfa998f3382a323d5b1ac1a3c7fdc562795686e00b6854e17aebe6054cfd068348fb8bd39a48830f2bb15ecd57a55f3eb58dd56cc8b73d9579808bf420372cdfd85dd4560c17b7e709e93d9235c2008c8248061a0b5064cb438830afd210fa763c63bce6bddb39f375dd60c977d64a34f6c455815ba02ec697f4424beaa6718d6a7fbcf66b7b7c4442e12fddc3fb4ab0e0b3468248ea1748061c0a05c3f456bcb49f493640d979540a1efa94ab338fe77dd933ab89251e619588c848950c8725f4c745185b417b25eca4dddbe71b7ff95bbf27d5fa0cd537b2fbafc6c52926b9d2f4a708b23d2bc37fc7334e981db0e7dea0533fe3e74b83084ab17e1f96f039b98ec61f0d1597840394e77361a86bf3e773c94b202bc5596804550e9b306d46c237d0af106e858347182cb722e39936f73ed51f035d8ad35eec6c5cd7e67458f77bd3fe5c95d40d88b768a58df27bfaead2fd7d5c19453d0af30122816e249bc6212c645449d7abb623f8964344bc46fdb87b8ac04aa18e65eb62d1c12fb411b39f8db16cbccfca8c305d440b0b41ac7ce84a6e2982f1dc39cb96afbd48df1c20f7a26f979ce35a1c3bb9860d10c80337501004c66a47b78c6a9fa0a569bf791fe3185f2529bf25b26c8db5e6e4163df0b7890e391c74f3af261b4769f9a9a899432bd48f2f2881d5d20e2e374c73accf130d7d7e0bcd0b7cc4ea67f1b4d3dd9acc9f3b9938d3068d7a15accc951dac941b32fec08d757404011826168d90b4c8205216a44fae80e81bfa9f39fa2a1b5abef88eff889ba317d910428b6df76a027b24c76a70239140611507650567b21e43282f9c66071b967e638646fce0770b6706c1632cfeb36efd909e349d828c5f19fde1705918ce0b7e73a1122ae60709239d7c2d94a1c02a14c09b8d5f823a6cd4e31ab81d63e81592432759107005d664057b8cc5e3dc72a99b47862ca9d9ea038418189a4519e95d828ce48bde72560dd27cdc081d382b5b29c8d2ac152524b365a84ece21d74c5e80345cc5c1d13031ae58d669d53f2c8bc830c66cf42ebee9915ea48edc82bd0b8e47b6d8687bc31bfcb60edf8833fb2c902dc1f2c5698ae692efe3608790e0dc65cd8d31655ad92168e705d187f286c1a94f08ec6f2a207ae7089521bf94971a78201c807c9ef1c145cdece5c3491cd41d66d9f05e3a23a7a95b3ba622931d321785e65a260f657c4926ac3a516afd2dd50acd2ec28e6b1140deba45373ec27b32bb2cf17b7c9000040ffbf136afe97d0c513e6389ea1a724894c59be47e321fa2d2deea6e9fb7d9aa8c0c4526ec34b61cf6df291bccbbf4474ab2842f3cdc877b82dc80d9ad7bbff951393ec263ec294749650bfab6437e805d5ff20653fdc8c715749732e8369239795f23dd20925cd326d5a64bc430a8cbdb02d0b9bad328422c28d0702ce6b2fda964bff91f3e04336d15c572488f3ba2d7b23498f0ee574873e791aa736e79639d36097b1de49cb314b1ed5fe5bba981dd700cad0261cad6f0c01b422bc7a1ac95a53a354318cc02d5cb066791d39828f4a93909e48ce60a059d683bdb5adf6315f1c3013346d27878cc1f6e49cc386624766e2541b48b1d26a3dc262cd182ca106f9d0884620402742809476b61c0986129d6ba0ce2a3d811334b2eedfb21c58e3d21505dae9308c5f0ba84093d673f21b6002c6a48c3a63635a18065d2dc22bf5eccccf56999a9cedc45e8402e292138c65685aa07ef7846947d3533a3e8c7b78e62e96b02a6797cf6489ccf037d17da8b2f2fb85e495db6538f4289611071a12ab5b05bcc54f2f16f287d793c04d3f7514939cbe1807fd71336e1a97877c426174ab4d7add558687687c5fb5bf9c1ed1595cfbc2e2c71cabcb2a155373e0b673624abb87140b170be5b6fcaa044948e7e9015d4d850ab31644738e25478bc68d3115317036f7fbdf226e534c9e4e70f163bd4be153f30cdeb85f9b05235430f80547256cdce74d80693840a0fb1ef14adc22da88040ec764d66b5ef0423ba0aa424794e77facc270008028d388aae10a5b44101ebbbaba2ea907f59bcc0820bd5b5847076564b01155df092cd06f79ca28013b3e3bda69706bff4348f3fdaad54d0414bee6e2741cd94cff77abbf45ce1e77ab923a6cb990e22acf2daba1c252b39d102819daa76d6c50ecb2d40613b67a41ab02c18fd4711cc5b20970768568eab166ecdbcdf2a8fbcf965d0a6adcc914e5d4dbed738fdc313208c215e559e072e88f4b471c6d12f2b2385e0665175e95e67852ac0b53410c6fd5f1a4f29b37811c9c0d14640389973f61b255063c2dea148baf3e4fa7a16bc1e94318298f8df556f2d8628259bdba9698fb02f2c0cb229e5546c278e30896c0474884e34ebf38a101c184097e4472259d9ffa83d71fe1fdc7c6b438b583b1e66e4b2f0cf52bf2c2b6b02941498e2431fdfff071c84dce10c7ee08a8942a798a18e7eb5c2d2932cca304721d160eba1c7bd54a0a6f05d16aef88a6d4590d8d2ae6c37b4552cb2b779b57905e76cca68ea027da30d4e27bc9c80ad03fcb11e8bb08f2ed22aa4f4bf378b40788fe42dff43d725cc74d528497e98ce35f0db774c1e8bb8c40a900f53f6bf182fb564fb4d2fdf365576aea0a4296ea72d1317ca44444883623ea42da8a5fddd550857ecb2be3345e70bf5e61d6b361fd6901eefd152289f01ef1df44bb6ec0d9439bedaab053f83938508d2cdcb3c8a8a6bc212a94bbc9af4e6d01aa3505ffbd170f4a5be0e452692a9ea1d73ca7aae8fbe62c48034902f0c0cf11d9ae4ddd2ae12c97cdba161e9bba4d45fb2cb4e634518a491eaa8788fa2adb112f90392d1ba6aa1ff9d08d8a0a89450ca59e3d7fe931dc19e4a1ed5f1e126ec438021cbbf497a8fbc520833fca3a52d4395b60325177c32df74901f6af945300d333bef98fb0875f7b95de39e35d300ed0856c212f8436dac0a3e00210b1c729c4efb9fa7f57741b0eea1c8390bdfb1ad2eefc3ceaf1f309a364fa06499d0c0d101772d467aac9044aa4d88d6f9dbf22341a46c06bf6c44b97281f7146c9d9097d6f8ee52f7e353c3e1abca8ed6c51afa39adb4f0a079c6cfd63acbd3bcef5461c063ef524c31939493b3562da0685ff321d9224e717502a100deeebdf782b457fb506015f57b6d9ee013f61201bb72a66d8e5327ef0c36a472cd114455f27b61d43f4f5e0481b0706114607da59f5b0b78b9d48ca29f95c4c4569cdbcb25e1746ec5128bf486526425b4f9cbc33ed8e9ebf93daeb78ff06891b19719b13fe5fe77f628b6a380d3dd7a0def7ac2cfd81433ac564e1663f09d5234448b4b3f64abed60db5f5d3c1168999c49cd5d3d88e11db8522094c16312563d57ed420c493aa3b8b7c4a426c7da098532bd17e13a21c1a86af3a9ae641437884030e7aebc22a59bd48c8e3811bb1a4f3a94aa0ee0403074620cf853205862af56e2b094025f806e7d65da900d02ecea618a5ac8f2b8fe8e18719f2740ca86d90db403f62054511b0d7085341dde13fc40882950f3a8055cc325ebfbf594b8bada73f96569e4c781d6263c9c2c7773a9a967470950fbe2ccc531ec23520666b50440db695b38cb053047cf39816fa8df74b4c6b07692d684f769c8b2a245d65c8627ccd3a157bc89a2f5767d48eea19876425479f89786dffba62eaea6b3dda21753a35264681b9d31a59d35f8225785830d626cb00b642a33e251c7a6bc1a69851b4cda56ddbc6f9970319680f8e0396767d8b3dde97249f29e5b7ddab6a6b46391a6459876a11fcafdeb770c126520ba185e7e21a90ad323e8e37a50fdcb77748fd37af3ea75cbeb0399a8abde7c798ae5b61cb84078dcdef76cc36c8cf7037c6a8034f57d2fe6d367c2fb25aeb45ae256f0451b086eda0d21e8f4140186ec0d3b4cb13e3444facc4cf1dc906b622950cca0c5320eaaff075e0b6a79bf6c07fda46230bf9711ed7d7a2a9bc62ac879579a5e8eb55ee17587e467e8f1ad1fd6871787a561960c99bef87f65fde1d0c41d01b5ee11394c018d63c6c72036b309b54f064675ae51a718509099e853fee9ed16859c80fd8da5866fbe1067f3c1a259741bac5c3b2ead2f7c523d051f4b015d67643c64ec85d3500b82c363a260620fb0bedd052b5734e89321b829b269965531570207998532cb5168633511801b7e479dd01b200602374cea53f853b9476f20836d04ff5fe4e545d6565278c4c5fa94d36efef44620e7d5ccbc4ad194e61ce5375d79c56d3d77db75d4e33704e6504c5ed7613bd104b88111c93d8280a1cb853bf868a4dd3318c31b549f66c2f6913d184e00a6eafbc6509a2686e74f3e61326593f439075e229964f1e04cc5b4f1692ed9bfec057b012859af015d76ce88d0d0b2cfc1ba22183341841ecb310b9b596080bff12d3a646faf5fa013a4417a37f5fbecaebd55096452fe154ca08b82aa8d42a3ddffa314b651bd1e25642ac8b0e380150ae7204c188bd4c0a9626e490a0ec165d698022c323dab05d9c5de0611b249eef65995a4080a6c418500ed9f4f4e185384846bdb1378487e448e19b3cfb7b21e4f1ca69df9c3ac0b07831c7525acfc8385a07f8d88bb43fa59fcb4679a7170dc6944f73e040a77e7993cfaafddcf9878412860f400652245fc57255768d4e169717b4fca965c5b7f6bd1643b02e9fe5f1114a932002b9738dac5097476e3501634a0de6d069a9856127e85870c90afc074f2231e6f51c4b2648f1e665f6b74a1391d2aacaa3b93f70b8b9e6b4999fe0e107b4f8bd9a0637e0f2bf4447a7f6389e7b2309297817a37646a7d2e43be87ba724515fdd6fa56095d4b5f036b960a71df05773ec7a33ccee18c4f1e18a0096b6cb43e468ba2484d7ace863c24dbcff55766c1780f0958f4abae8d8afedc59d043a6b5c076a1c345a5d2f57d70ef57b49b6c3ab914bc5166b3ecb5e3935cfddb126e007f361d13e906937efd1a22bf6d7ef9707da8938be4958db7bd6a93ffa47224af28aa9d3aad0e56c1178b6f5f7a7e1b1af63900002cce538fa8f622d13bf907b9407a9b0a88213163467bbd6e54462059d92d00d376863b266286a32a99664bcca909f69d4ee8a72e50da342c86adc63127b5b35cb401fbd5ea32358e09acdc023e4c42796e7144d589438725955300ad45e1f37fe5bbd73f147d48ac7a1ffbc480ec0f1ea8746bae195b90e05ce04de652a58e7d60b90fe4470f85f7212ae80643a6aec0b9f50232a31a3109b35a38f8fd866786b6434be3869e7ba178be778684b64972ba444a78e4e28535283870785b6da207c10cf44d063e48bc810d4f2a6d0763033b0c9b504ff00b074135bbcfb4ebc6b8872cbcf5d374345e3019a55db0fcbd89a23ecbcbda261529452b0cfebd22dda8c5bd4025e4be3356f9840d4071d98fe280f049fe444af749696e33e0fa8a63dec7c5170e288459d11d10e7eca7a97c777d3d406fca52c503796eae395ee6b252adc4d92b7481cd48182fff13f8827bd82d6c891387462f07bbf065d182418e1dd07c02e93e2d63f05b8442eae89de46c2b2d6adcfb486a9a2aacffa89f2dbfccbcb219bae7d0d4d9894b3b41f5336a73c594e5a20893eaa3803dbe1d95ad8766237c325143ebf90402458d622e5f6d08dec29bbb8704f85314a3042b773b5fcaeadf02d3dc116bf0e6eefad2762b115cc3e9e663b635a8f6bae4f8388a83417e2223f98350f5995840c123b4ab9bf6cfe893861eb81cc29e463ecdf3a29be656dbd7476bc10d77fd3f795a55d79df6534a30eb26c16306cb96324bd902a95307d3e2d4bbfcf6581d1fea6763002aed2a1194aa56c3a6b633bf424a544070650f918d1e046e4852c586bc9287af3aa51daab37fbd62d643b6d02678049bf5a492f14315ec5e445e29186dc258b2001e8ae37b99fb78ef80cf846503f7a20f90b199bf9d4aa11144162153cab369bdc224c3380e7db3bbba9bd12f60769d4e357f26d94360dd115b5dbceb1a0435cdd07ca34af080e314240b4f6c106e785506dc65b4d4cda56d19c9ce5e183ebbcca05f4a6554b8e632e3a37b89199bfce1dc0f627a1dcf0193ecb245723f7fbcd6b28d606ffb405e2d777eef4b4e4680ae47d4f4de6261c26465a086fc0e1d800e659b36edb0ae327a55b2ffea68c68600bdf2445492583d05c68b2d8fe8dbc1de99480598069b0ec019c7d931c4e707b3cada313fe111576445cb2695e8a68abeb6f2939efcdda856532092bd4702aab92dc860451b421b994c20e21c6e7eb91e981c410e6a06934ccb97e91d2785fda6eba33b1c37facd66a06f9bb88e071d5cd22d1acd4bd6e793f53fc1045ef865605b359135706b3b510e52f8ebe239a3a397688a963944381698ed7b6eee232045f72b2e924cb00241463490695b8ae4c82cd2c5fbfcc383fad839ebfbf8372f7defa7a2ba0241dad3a10a4ebaca495e28721f7264133c8a15285e1a06b77b27e935064b76bf4f5b26d28fa7ed746f3656f93a108c7c3b787ba0a34201c3d60831f24a00f975a6e90f987a255bf48921e7e5f4be23947e6ffbee0f967dcb3b014b2fe2858b8ba41d9ccc1240287fae595c16d823d7c973a2b7a9d40262990e9f126805b015159105f9cfec082666928a6e779fffe6c454a238f8dfcda48e51f7fadd92b4024948b3963b4338bd034abee661a8c58fc08a4f9fcee94ac127f41eef46eb57e54418b06483ee85a3f4879c4067c5d6e09b2555e40823a76960c03ae68922621be9a6e762634196e5558b0969e835739550fb931df7b040cdb06b786c380320036bfeaf484876281743cf1f15db1f5c3b57d5e4cabbefe1389ad35b2d62b68d98f5ef604409e37f1028cf0601653790286ce7bb18c933922d043c865f4277e1dfb085513d6c16f71860f540ab77bdfd04f9546516f22af0ef46b55bfd3c98faf6abdb84f548573d0854e2f039f0a5666129b6ed0eb11e925d90d3fa9f1185d64482ebf9fc662801d1e8da09bd33a7c1b5d182dbe6d074d3e75753043473fdbda168c7cb7cf72e26a28130f684b60bc673ff372b6a143e96ee8872cf4533af77efa63128a004637054b0e8df7bf8311248368b2f021c6902f50480104d42041ad5139558c94404c137a5a2a58a63487b139375175a2b17a25d6151f16ac7fed64d53ee509635f4b2fb22ddc587d2f213446549b6a3e83c0e7cbbce76c79a74ca9bfbef9aabbd20ffe5c5dc2464e21909b8bd8a1fce3eb3b7a31d9b9003339016728338c3bc581606cb684a4f85b1dd4d5405e7472e97c0567cac48839bbcd0ddfc43a246a2cc49b9b500802d81aeabdeb6cbe50fafb52ef350e6b22717611aa1c0cdf670fc45a71341f53b41c979d1a92cdd9890f5a918f3d3be59868077a954c58e2fcfda2ed9fa765b2ee6398341bc2992e8681b82aece9691c33d5407ed613a7a1ad12c62cf099ba8864ba0780a59394f02da482c4f043e8106831e7417023cffa86dae108856074d73977715bed62510293014c9d5d021cab99e1ef394c1b99d512358af054a252845bd1ff392f8ab2c17fa900823f38acab875f541fd0162bcb92ac426c5b1bc8ad72a84f298aff61d0cd2957063bdc713c84cd18781d30fa27f65d065e98e0ac715ed5818e88bc71139bd3c49cf0376281f3b2405183b7c1dff19ac06df48c43573fda919f3eb96e605d1fd636c4aa070804cc904f9d0833f3b5b397746917d35c4c0ecb05669b9648c8fc457aa87d39481a8274a837f80f78f6a4621a5ae6ff4a86dfd78672770233185a6c9ed472780e2af757a96a13da53744af29d0e72808c5d6725ce9656e9d49161b29889dcdc90a8aeda337060a1a41bc2c004f9f36b1bd5bd7cfa42c8830922c98b83bb56196fe3d931d7bc6443c3d095fdba6a7f31af9c051426bdaa91e0eba048055b31f7f3e49462fe10b158f46a560119f1ad49856e8df712836901c4ad0f5d3eb1779ee9786fd18e45716591db2bfb76e65d2e6705250f615e568f7701bd9529022d8ebd744eb2f6d41b5cc3abadf9c58c5cdf0ea65c0998c13272107c116f22bc0219d3859b5099da3f05ac4f04744d5ed36a352430a5bc5de79f6161d14eb0aa67cab06f1e3e3fa1971915e997ddc6e696ae3f72c9a5c8ed62cd1a0ee6fb67545f80c8db335a10763f6e401958623310017c0a8cc5b48a5b3275c484ecef63c9c253480949ba25656033f05696c04d89c30807ca1bbcc1714230cf9e5810fd226f3b39b20f948bdc6b3cd01a69852b380c375b2e89b0b7635c0184e4df5cde711499fa437ad65ec9006dab000a898d373a9e9b2ea31e658ddc1e803d4a1fdd0555c8f7810ff912af6d3a053b5f1e78a8dc9b334bbb865d2b7b96d7f4f9b5085b7438dc120f77d93b310d1cff1fe89702423c13d0b243119ef2519157971f9617edd0b5333f918caf73af46fac9cc8650cd91d9df6cd6954457e7dae84153c019f2279c92ff95f67e2a9f0f247b52bdeb113b3b141ce8f51752022dd3d05ea97f1ab075942577738524b0068b3ed5ffb65926069e9dc9019d1c356d4806cb5f5202b9a4795c025bf9da02e3545072b9ff0127ed7cd75c743dcd1b70001bea0ae26cda5a781d41aa8572947fce6b0673d02c82ac32f9988a7cf078156ecd1bcd01d503736ba89291c9cc80a177e7c941b0658122b4b6ee5a6766bf3dde2ddca8e0785ec7e13e6aa3755c1418fd8642519ee1f8bbe93b34e14d6304918532c99eba90d3ef257fa3e5400ba1e9714dab656128692fdb4b9086ad202992218f0568d90378f44d651de76653eaf6e6a93cf84e1467136714c92d4282aa8d6ee3644343d83dd79adfbc21cacc88ffa52e4e826c75a3b96cf84f7d0753a1d0a805c17c2716c307897485fdb33816e02fd849185128a97034c73a1f3e27fd06563bfb877ec8a7a190f5c3a8a8bb79540ccfd62ef16725b2e3c453a0c36bcfc54b5155255247997dd470075c2a5f9700394944b0b4706896814cb5cc1d648c9392321ad25b942984cdebfac42bf03f11592dca990f15dabb891e1d763e04ba281e16673715dd6baecab3c8e7b914945f1369ef3644e6972e6500b572a2f2c6f44fe9738fa22d5c9f5502758b133146254a636ce2a2f7dc706dec4c5bd8ff47775d7a2ff59dfffbc5bad75e47f4650e3f817336fec65226a7f0a23166304ee7acfd05b17c067624ffbfc6079ac2e5dbacba4e613e03f6cc91804c30aeebe6121b336d339f946e335d006dabb1154e8b1ab1cb5eda3db07b21e410e09406435594716f3a65a056d54ad6b956346e234ac99e8512beaa74672f65c237066d3f6d5c0415b5c53615dd6563e52f473e3be035575bdfa5d97b004b0ccec3c39754f6457311d3c6bbbda7c255b93129af023c6274316403692922393d8af5086432ef522c4e77fad600c3ef7b5dc9cdc75e341601fd6016aac6116a2e274333f57cad8f94e2b5e40b1000f34fc88b28f151276d0c322de67f153d2c21cee82f9140d786842359f528457489ec9ea7a8aa4169d1332d20dd2b6eff8c04a96ba9f10313fdd67f3cfabd6ab12297941cc0be377b55bc479921f140326c9080f25147fd299e82fb21e513eb19dca219791f44317c4e6ccf7e7c02853eebf8eabbd585d0d5649abca733c1e73b4774cc6bea6ee6e95d1a8df9fef358a7969a6557ec8ea7aa0215344f88812fd6c01a31bde9913255580457c163693e82eca2ff05d52049e108310685c72ca74d58929ec78f0b2f9f320574b830a49f5efbb620ea980a0fe64be053ac7539e9c70c5c904582ab654382bf8a36052025fd710a1a78efa7f3d810a9dcd8faa8e954809733c62ae4edaea3971a07c0c71dff9a21870c8c88442e5a62ccf1b8fd911cc9a6abcc90180926d4c85e8ef5ef366c1e790d3f4cf56ed8b4426a4748cd865b0a6ad49b2d1689c3984aabbaab99086a85b1b24ad3f5d9b71f1e823dd2e8a73112743e6a2dc23fd7b7caa3106d67a5af17c53c6dfcd425894a763313a1cb1a85e13578892717bd400715e50278ab640999d6b6966f1891f385352ab8cd6daee50cdf196c0aeb1c6f3db365c97b14ddce579a9c896453f1e9515eee9dcbf7331b61e07ab6267d8033076bdf2feb82534b954577137657378217d06e6c95ba58ae2ba18a8f9e6f0b39fdded614c98ba13a237bad7e427c7c0757fd4785292bb57161f385525807ccb2be5c974815fd563edd3dcb5c5d24c1f8dcef4c1df7e9022a91cb97577e0b1b573e5d123695c19beadf7a0e41fb4b1238379075c29f28eb0acee0a211d80be93ac7f4202fc300df60115e06528ad559c734ea157bfab3e8ef23330bb28f1f635e1bd891f8cfd899932cab48f8c127e1ed2a36ae7b24ad8bf914b17753d6f14537bf412f5287237706d8778de2dd1238f3d7f97579fbbee870f64fba0fc5e01735f4a2bc66416a26bab36723626996bdb07c3fd574ec989fbf83a879921cfb6abaa271209cbf21f27181fde8ecfcd692c874ee83af7a67e0f0aeae4aabf6ad7ab4fb02eae2f8952e6b2ea1d9b39d7f9d3e172ece773e9a4c21ef7e2456cea630075db7a3b3b761030a8debb64eae627e85b632af88f37bc6018987f4e3918d50325583483c218a449204cd4cadaf94622459f702883c1330d4a52775bbc5e5653c2fd675bae11c82ba522280b97e5e20902bf517b3e5e4dca9f5504e40031b1ea459ddd5351f8fdd1e684319aa5db728c829dbbb47688dff454ece5572f0c1c56808d4727230881e4f7cbf4950e6f00a5b93fcbb0403bbc424ecc1084eb2232e6ede01ef5c774acd70e0390c0db2ece920818cc3c68560504cc5fe50e7fdd02228c79108f5f853e4c24a4a9044c4bfd5bdc2c74f89d2bfc8bec72c3437c6c168b5030e6351b94ac743e9c92807d1556bc87fb77ff7787c200a5b0147ac48c688beffaaf6a802bf6d3daedfaef96f67892148f66d6f7ef1e360c959ea9596500cb3d40239c18028e73544a3c59d59c12c9e597cb3a1364ca8ecfb10fa71b8ceb7c64d94369e98b0a4475890c46212edc21275b5c0f0af849972e87023d7434b6b97ebed4d926d2745a4771a2a0fb76f084907f8438a028a67b2d8cc42fbe191e3b50f86e505dac6275aaf9be6ac0efda70345ef4474c24f4f2f224824bd08f91bce44ac3fca9912c0c811cc3d7ee9c5a2f461842990cd2e4814f855ba86365a394312ee00acd8651ba3bc463ef44343d831489ec28c4ea1be212071bdf345633546ebbd4ae6cd7254d91f336a3a9b7f8da872ff3c1aa774c804cc67e7acca4a3058156fb5d3e3cf8301c54b778768402fa88d6c4662699bf11972170ab4fd977529393215f1d1941813e82b61e033141db70bad1c2e14db7328afa4534d52f62fe12e2e7f69731e84b56d0b50b149a1285dead1b641ff79f38a8e4939620dd1609191d73856c2ec5edcc8ec53637983084ab07bb43340fd1076b7013ce2ec5d1849466f5e4cf98de9c0db4cce831a40d0fc6635b7c32f9774c69fef67b523636d8004a034bd8bbafa8ab8fc5eeb09ebf64a835a0b9a8d63ed660cc427b9fa2c38558f1668a8a6f6008f234363a82c44f93a6755c33e53df35965bd92f6ca03c7df0e7f1a54ba2ca5c57b8e8999feec96e8e58492eaabd5b5971a0e2fa9a05b633b857214b5a1141e56ad2387bb6431334bd58b0413799bb9444e902469bcfb96ddb189315dc1629ba6ed983a57e1084322a5b0bc341d30fc0ec90ffebe47376ac43aaeeef71516eda230569c0cdc30175e39b5d8db848c5638990934286c954436cf12e633f3ea7560c824dc0bcf3f86ca8418cfb27f9c6ad097a21dc761869f3f0ce2c2dd7cba5c50a0c216f8eda17697213f90c7234ceef3a3e5c8bd433822bb4e62639f9665b33af633fc3226b48a4e3d81444da677d7860c7eccedec0e2fdecaef19f5e11cc7094e3fa4be8ba4489909ec03681fcb187528e9e80caf20340e9dca162f05c43064d37e5b4b6a16269f229e92b70b9f3a03e1e30acb11b17185601bec860ed13ee6754e4a5576de7489570a7b708162ceaa9be1d8dd00b78119f062a8b3ae7e3d4f5220c3b1561bdb73f5dba588ced3d5292442e1f7b0234c74e01f2bd6953425c7fee4e554ea023903312c5a6e30cebddb055c0c7b5d92d75181b5fdc1538a991ff55ffbe4219459a8f35b6a40d35e6515035d653b88a531bc55e798351396d6c0d2317ab50bac37f5d7cfa98c0422f77d1ddb17176b9d8029442f0f62d7926434a59b5aef8b16dbfab9dc0e6deb7f550951fdd6aee68fe1b6180c46e9a7b73e65e226521a6ee8a046c5c50cc2bc2e4dd270d012e11b43e52af257bc9af4df424cb408649759842b14c01e8310632d0214ddb4c41839702005e803ae8ea227ebfb19f0aefa9efa4e954271f1de661402a83367b6e3b55c778de4393cacd69d0ee880b0c566d611c9a8127ae986aeaab8d30113814de3a84668f6976541efbcce01f8ac7f69d2a9b8765ff6c4e7a8c54d3628b606068a5c32891de2a8849291c521fa7751ce003faa58fc2e44abed794373cc17b4cb4c6ed1dcb7f4296d956945340d1f3f631be27f873ab6c339818a0dfc868c23ac0255a325f8a54de45697ec0c9a2f65e262046cdf388f962848db4b5b883dc431b1450758777a3573ff785898eb364e5aac3ab8b95524d061897ee740ac265519e3c8f0d19b4681866a7c649c1ba21ef1b9ef117e543bbeebffbad5400c94074d2965a06ecf3398ef50d8655b4e873646404b603ebe70128fbd3baf733d293a8179b0e55914a00f231c898ab176e78ca90224c454bebc5721db9b0e8267a4b2a35c3b1d8ee8f18f765d2bd6c9f7047969fdb6dd7525aeec3e6fc8aa29669ea02ccbb114d98c57aa40e64efed517a6fe12700a0420feb41a66f82742115c7be50ff48ee8d46d26cd1da5cd5d8b01c6de50d4c408f6c9358a72ba6ab65cee514b53cd8cc7e1475aaa3729473d07cf9037c33c3c21d6f2be8a19d101f59289262c5d108f547794ef5ff9681ec251f03c79fd74fc201c9178899682982838ec026d0a4b85a7f84e7932f65072d221d0838f8ad03715ef7463cea5f052ad00a3e2edcf12c87bbcc252290652338baf60f33667739736bc8ebe66006a9386f6fc47a63b2328a5a88bdf282d92e69f6d8e52a8e3d9369de3d6d13d5adaf7220286133e6adb8acd52c0616b5b5d77abe3b0064bcb0de43177e0ec5905f6610586a14912563159275c0ba12ccf1fb142de425f2dfa8eb6dbf5fa44caed5fca3390379d90b0e331613c179e4af277ff4c82fbdc74c90d402b3cffb55e2143eca02926d41c6ba3b4423a7ff444de9039012c8596036c087970891710573b267f7aa9cc48196b358a5bc5910ac761a933579fa9f9a5a71fb63d6592a75ad9c89b8cb945e17096f84bb27838fbe0af72a38e27384976ba86f66609c3bcec49394a39abfe58d502ec9962dc07f86bdcb0c28928c7c5bec6e4aa4cfbb6f10e9f1dd5df12d97464523816913e2ffaac13fb8b80cffac2726c89c8b8fdf38131b093e60025b5fc659ec80b7a45d7b1fd1028ec974122cc69b7b91529c864aedd97e32e410374a9497d1bf3e11280ac1d68ad7f2a4eae0ed5686a388fb2f78923544f9d5064aa92060a195142d4f1568d0543c117ffa691a8996ff5a1571911c60902c102c58cde3012d704647b03330aefaf555d10932ce20e9a4e22666be0fcd84cc00362f90e88833999b6a26a241f6fec47e5b3b3945d183579c1840f6398eb6966d5895185b95eee528500be072b6a5172cc8f2f97382c6dbadde3a48f709f1a1756dc631fd601524a95bd9bd921b8e7983444fd040f497a3fa1d3fbcb5ada20931fc7be61ad9d0458453f01ead065592a526b4fb08a9df721baee7c4afdd962fd59267e7dcec91e1af826a1d925c558c26e16f0774fb94dd087b66e82bbce88a0d0e3046bf1f9efc2d2de5379ea5db0ab749a711769f52430b2f729c9120ec252872095d523a77bc0241e1c14ca4bfa0865ce368c84934012bacc28896e09cad5776967958544b59cd6994d91b0b1ccb3b41b97ccc88551b72c485cea8e447d2551d7c6a8166889409986a907efafe74a949f0784ed0841bccc0627925ad081df499471bd102924cab171edadc256b6958e3101a8a8506e732346b7d360be425d745284cce2a3a6b4c1c810277473b8558130f1b88ea9ee9cf6feeb6b33686ca846548263fa82817cb70e6ff23c289a689667223b6009cde7facbb3c3e5d2cbde35d9684dbd0527bdff6484f88b82ca74ec78b8b90f6503a1839b47362637f805b9439a42de9d8f1bc333933dbc763c613333dfb3d5d608bc2ab7b03b80c9c15ca3b7cbc0417835461a964d3ca4a6f8cfa13c61d6f33a0fb182d05f1c962db6d71f0995edc05e142c82d7c8b85dee69efef3196546b3942babb13b05072c4c95a662c01bac60654b4a1523ee826469adf0bfcf654465ebd0ec7caf8f99d17313bbd770ae886a7fabc129659da5f7688cf9fe49e49ae6083a5c49a954b991db06d5896285d38a69eb698524357c637b552b3ee8db6c331b5f92085182b06a2dd195f594e674c1cef18413af1a44baf953464e54ddf7bd15f8347fb98dee7e773bff2bedf4997cfeb2e863cd1a04e002980065338129dae7ae7b4659da53b04d50b97b78d0fa013afa5872cd2e873d437f9d2db6170a11539d5f6d6a16f28901ca9db19cdf378f8b9538fd8eae13e467a707373da6f2aa9d8bc690f66840c11d0400950eb485ebfeaa931ae17314b64f495262cabc9e4f111504fde27517c966bc5afbe4a955178b675f2f2a014aea9866e9ca65271768a9508c493429732eb5990076c2173864cbdadc7b9e91def30f18ee5604a3754634e4150d2e6318dcdade37f01bdbd36314ffe99f8a69686ce021798e8f64ac06251024520a5b4c4c333d2bd3371f26156c79ff11699b559f2d1e21874168fa93b369487e5d65417bfdc738dc71a8749f3e2df1e2d4f191c0cd35fa2d4cc40424bd69702910b845b6d0e42a66a4f0b0d340e35f30e7d0a99cea57b5e53cf9c67ee264342fc3224a71e0cb8dc236ac08f9a610c66ca3c47c82d11a7e5cb57fca0772bd845ed0c892df004b54041e7bcced4c03a656bb7736e8675ffc4ed62f27313b640d2457db686c173e11a6dc88666c2cb3dd629d8d1bfc11d7524509a83f23bda42aec9e789816651f9fcb5d458b178a3f1f29b682c964e835d24781faf4b185dc219c9f1763e8b1a5e97483ffc5d3feeab516e4fd61595c86ce87eeae31a658e1399a90df1e2f9020808fa6eb3cec46bd6358b5c6d3077274b593d89c16130bfd7379d28fbc6c965d8c5098fe45321e2aed45e6aa537d1963924f689566e66a1acd34dbebe7cdad831b23ba710380484a2e1af578fe0590ecc8cbf8dae4752875cbbac51196339ca5e341f189ccc1d94458f318ab336b0e7357c84242df4b13ade6367ac77672ca5a9b7bc4871da2549a8a58fcb7de595b47e3bfcee86921e924967cea3a5cb28e229d99dc1ca0f3e8312ca45666800d887b3483f076b6aad86755cc081e2ab52f5ffbc04063d6a5e9d91c3ade7a7d273027448e58b4a5cfe38e0ad347027d975e099416230ad8941295bfe0a7c3704686e65fb49ee983bea85fd17e92fb80a6939e2a6c4658bcd1fe5db45266dda25bd2e6906da59a5524d647da69cc5dd886a6a95581d1d0757943e9946e5370c029dc48036e896b2384f3d9e21c67e647b8670aed544f22d8daac578f3d56a38b51dac7440192cabb0ffb8015ce11e44e357639d45dea7663729edf46ea8d8cd37e5778e4a1706d4f77c4b41c94f940420ee7fe432b66a76153150c3529d996598caa9af3b70d842ccb80be19b3b1789252c80f47ec5ec321efac31256e47a7a47cddab1572937d440f74d7a842553090fc19dadae8c35996f88a4c2e434ad8a1decca6ffbe3d98faccc0db1b328d9c46947e2fac265abb3c55cf70f3a13b1ee3ffe4cded068d43f9d50770c8f6488e3fb65e48101dd3bffaea44b9ae63838cd898ba98f3b037ca5596ea125414ebe167810d6a31f0243b5350464d7f4e1c25f610cc5717b94ea4b7c23787499ec0419b035586c137d3e54863d5e717180fb64d2d1c4c9d0f623fc7b93a7b0d6005b4826dc3fd829b1b665f7051404337659477169df13a9fcf9e2aabdd4ca46e91a95b15142a85d979e70b7dbd0f0371afb71f4a7f7b20fd3f77ee8f2d678dfddf49715028ec4d0b41cd1ba9ca89340cc9478dfa062aead699ccde7b273e0281c409e92c3fe76a8fd9e295df10395f11e03b435a63e454028e4a2689a0c0b1b8eff9b7d47ea63576fcd172ebe2d7282c1a3a8a5e92395842374e8017726a4c329480e042cdf10342049ef9439b27c31436e22f753f4cf37d62739ba15a8681e4e1f7cad502ac866c70369b8a97a12748cab2b8e3fa0035823160ab3388fe766502c679d8535abd1de8790e4b715a4c07970f45ae52b22c747faeaa56cb4996596fd0dd8245240a01150d08463a13042f191e6f25255f4c0e4637eb4e9d02fb6f45aa699b3789d0add26b899852c379028224ba05423b3c2a033c911ae786d9f9ff964777b6b95e395acc75897663fb23122bb231d37d9fc73630ccc65f9ad44ad0a8a925a2f68b118352705a67d5d95711f22958f58ed88765b0d7b6c77768e8d073d06278a19df462a364deaf13abb177f14bc686ef0b7c4b5fae6e069bbc8cac70250e8040f41f73f34f11b197971c818c80520d02d98e2de1bf2ca04e3400027b3b0ed64732ffb00ad80597927b8d0aaf47c9d8d1c55d1e44a03bf42a67dc995f0eb17275c4d34d201e2fa45cd376c112ebf7f5fe05a8910f8dfee1f552f59459babd219d60a124d2fcda511a94169aab2847662afd5332c773e482c16821b0165c74a195a0b116edcd9844acd55a5fe69eafcce39038a5ca6a27f0c4369c180e23bb099a8a28c55472a02501ab530b1367a527c0f8f7b2011de4d2e99e848e69244c0d6b2968f8418d8300723413f027f245349c5afea54ee041bbc95eb4b6e95fbe5b0f438e5115080e57b8420217ed2bdbbb77ab23fd574b54ea7e03cc30b9d649d2701b45496bd90f33fdc277c703bba3d0f7fc301feab6269bde20cdc5bbda829cd6bbcc99cd4d7a0d7e02fed2f52ac4f071bf0a8489b0f93873af21a23c175559448baca37517e1bedf696ecdf9e335bfd20fcb42b14fdc005876f2af191a91ca95ebd2503b5b688357ab7b806a0049f722f65c6650cba93b023fc475986138ad9373b16fe164ac6f87fa05aa2a13ca3dca2392a2717193628f7ff4195428558427e5c89b0bdae30379ca7aba526bad54eb7613252fb8eda0bfaa6ebe458be899ef86c6ba45ec88ffa02c69c2c4bafb0359c2e6d11b4bcca23f06253aa3875b2b5398f6f7c3dcaa6c3f28b6fe2a339b42f92c40e4d1cfd1300f8414c7a9fa6dbdce12146ecc21c6f20b3e0e846b02485e1c8e4ca49a855b38b87e00e1bfde947afdc5fee21d8b73ae5663bcb5ca140fd65e92cce054609441cae5f9f4367f999319c3a81ad07f86fc17071a965e29a1f1e26df7552090dabc5d7af94b3c23be55b6751d4a726c23592099bd61944f5ad21a7fff687780d349caee80f32b06e993c8a14bdc3ffb558e7e643c0919890c4062777974a0b4b20e9328b3f3bd4a43cee7fbd965b87e84f4fb6763b2f2715c46b5276b481caee15d71b427bb2dfa36d7f646bc99ccf1a7fbade803b2fde7a30c8dc992bf0fd7d022cf06acd83261b8b53318fb0a14fe41bbbe54d504bf39ef9a0b0bb39027e98006080febfa75a87b2c24b209fa8a932db096db196831566f52d9ddd5728c40c9e3497d333be7a5310f9386fe210de8e5455173b353d3939be331ecc9096cd93d4c9f229158ff4b8c840e50d71d56643c19fdc827bec0d37641b68db0d554d8886e41374b58ad58da43d3249b1fd2b2fca4c894953504a5e6051e8239b27432ab5d2b5efcbb460518f232408f114b37d081e3104012265e61ea042b74d539a0dec822712ceb4df2286e1dba81f223372ea1f944e82d3c5f189a1c6d4ce9ec8ccd80fef8e059ef03ffb3d0cd7407cdaafad44f0c3efab418fab385eb471525573831231078cbfc3a45108601e06f146362ed73753fa977d2893d11b196a658e6ec67bff72da35208b4610ddeda52094458af171679d420758c0bf47fe88a6f97e680ce213a7ea95a3bd5ce116eb35d1e24f30650ecee81060ce5bfdace3168c09fbb5443a64404b3d4226789ac00bee43c9d03e66a265f9df0445f620dbfabd567e65838380fe50cf45d4c8b3c44845798fb4f0f1321679eb190e42db4195cf335ef7c0866f334ca4754adbfddbe03c87176f0091a111c25345826e8dad823188368a270e2fd9e1e0646d7e42be18070811680e99ea5cb7b219693633c584d3fe168eb03b2f2bb18ce1a53240d17ccdaaea51ab43c8f8f465c1f9f4027f7c1bb3abba94624cb35f365acb917343b2c53c0053dada0e95e9b1faa8bf69b24279bfab14a004e51c6068eb217632a772014aade1a5f130638b3098913e7bd53d20eb7883354f6bdefac0ccc46002d8d48a1c7f6e59d036393ea498fd046bfb5d5e1dca50bd0b1007e62758613d24f96ff6f2304ba4159fa41d65e75eb4a62cde0fbb42d0a3d76eca899a5ab136ba0a10dd2668ccf34d70a76e8cd874669d5500038450975b018e01597c4ffaeca2583ccb889e7458228677361a77d5dae4d1549cb7248a6d61fbf8663c7ef2541da7e0160d78e416372c23b4915d5f0c768d28de4cd49f6262c6c0a70e8d6847f1d066705bded301f435ceede666c9d2a304b8c2f5fb93e26f13fba30d7c2743e696829d9d86dc7fcc4ae115ce6fdb73762cfc7850ae31278c5f49c8ce0fe20154f9a98d89db341011314a87fb5d4c34b5f55049ec36e8c6bb9e3db6b97e3053ad351c6dbce03838294dbb2b1c60e5f79ee3159387c770cb31b043b54e99d6f595d5c1414c471de116b4682ab4c8c2744feeffd8fc61139ed64b4ed03da234914fe161990716cb1b2079fd7fdf96f4727aff4e91671ca9add8befb1b8683402a7b83109bbd769dbed31fee8d8dd9dccf38b7d103589f07ee7134a2498bd5f81e466bdd5a036905401246dd14eda8b7d991ed187bde536b64cf772e327829deba78910a302ce6c6b6733dc140b4b4b9e600504c410fb2daed6228019296554d0de825735e351b47589b2520fbdc14760ea3326f8a149833a169f4076f3fdf72711fd064335e12bd64f62baf7c149c543b8244a035e422b9bf56fd4dc9155916f7545d081bbc705c36bfaa668d031e561c5ab210208db9c965d871e9d4e1ea82df4f02b76e3ab00de0b2118852257d3f246b59be3a09edcd39170d9a20c50fba080892d46ee9f7ed663837a3ca69348e209ed18c025b2c9aa29c05e7c400b18c024b1a110a21e719bd7423479e6d89548d947ce0f0b8659c06b70b8cff25cd9f6fdf6d13c7291a4eb78b998c8702c54854984364aaf05a30605f08423074339d4c2f9388b9acd815d2c51cbf9e3a31a8d035c8b1455a50dd8f8787ddf1a47b23de1e557c379951b6a72bfa0c8cbc50e680a045b1f6cda04b042a8ff19a80c09bcc7bd481059b1fd9ee41f6bb27de03ce8d29afbd9b491e7ca277f34e20a0bffe5c0f73a607d2eb44420142f29999bb67a7b155e29455bf68bb740a5fe909ce36b1aba2422c17ab5e65b41100842efb3fc42ed06f49870f19ddb3e85cff983c48196ca3fa6e0c4b1f64a12834195c24eeb9036a435532f6f771267476a5f280399dbe5d4ee1ffe68eeda65c6d9ed7c6ae4adc40a08a0621544e42bd359a157189f667b93074fe910986abf43385f527407144cf55bab686e888fad28ec443d9185bee9d31e7e954b00f271d5691c3062f0ba287d6375284cea0a7d587abc88f331c8e69805aa0843ae50abdcf466f8f87951250e53ef7bfdf2fea5711e0ddd5deedd68ea8d8ab8ad9dd122f6bb83d9e57cbc5cb11fab2ee77e833cacbc1b15c4167670d166dd500ef074e4f1b0428047c6e94d3de60f2a1ae276c4e7c878daa713afc6dafe0192a990943e1aa1a5236dd3be8f51d0e381c4c0f5aa5d5286b2b90af595be4d3eacc59d19afd90ef397f00a82a1cd3c83965416fc26239967a45469692c97424acf6777651af6968af9dd7f8ab1b2839e33e96255d904549b558c05ef3a1f458775503e9bd0cb3bcb25f53fe40df8f43f168efe7abac28e1a2e122c3ff9e93b4df7082fb00d230ef91fd058062cf1e253e753ddd9b5913633abaed29f0ce3ef83cb6c68c41e718ce800258f7f3fbdcfe8a20343358dc67fdad5ff6ca454bf7768d2bd4e9763048a6335bb39a75db8b1d577d86894d5b0884b1ab5d42067f44a13962c746ce69cf6b7f2ead0560b547c2e1c3093292f1d698d4b9aca78d103dd206665c30654acde6ba2e44ae6ca831481a967bc22a680ea1bf7d7bae4dbab9d8fe9b42621f258ba371b89f376b62f1e2a4baf2b3f548e4540871393fb9b3247bcb9bb087e36dc2909f2ab324ec8f423f072a2328792b5c635b13e49e59242a220eb63d993b4d176bdb053731532d939ea3d75cd910a12ecfd0f7c2563531c22c90fc955790925639e3353bc6d974e8fcf1ac9d1cc907e23efa91bd524722d90247191d6f79bdfbd9a6e4c54f3f288761c1b0f4b596dd38a8642b55b3d8b1a092d3f991be93deb7ef894d7c958e0f08f3b92086d3f492d8f89f6dee8b6700ae93a34f0adef38c472a43a5247104569374a5c37f812c34a826b1d272f0b7035fa43f2cbc42070b036df7d264c8b200d09e7ecdf8ad1343a1848a343f2ce930e700394ee4a765d4ba4d0a427ad134ff260e9dff3b33906cd60fc307a43ca27491522e0dbbcd02ec50a66ee16041d5d61e5b8008eb7426a5d07fc0f3ba7e66bfe81623b3d3760bb64826d7a61869ada76c9b97eb7de8aa148d22431b6803a52db17875c7f493c453a9f12b1c3af34043dbf27c346f9baf7a6578436437a3d1c4e44fd3af48591b9532c833e1893399013e101000f80a9b7d95cb7f25caa833e7bda39c8310e753b83d85b4d34c78a21fb4321c216ecb8292e15afe3bbcd2c2253ad21036c4c546d0147e26130f2cc1f4486bdef387549abc7324f1151814dd6a6b9f6582368491fd0b1926a2583de144d8ead188eec67db5a5841fdad721ed15ef61b1bb8f9be399f48722dfbe69af9e1005cc4f940e62b2a91f744c93d319b7c9159a2f999685b4c9b3a1d53ebbea3ac1c8693644c40c3d23744a269201033aadaec4cc012d16576740c23081dd4a55f46e3d10ec4e58ecab0c09e8a1ddfdb1e0f8aa43746c841684c25358ef792e00542b11e6c5cd02194c1c7f491c38299643f15003fc7934da6d326a294849eb8ac97490ee63046ac1d0df612e04e0a958a3f04fff03be929eb1102d822ee7968bb7768507fed68d1a8f72e18589c11c7bfe0f726b00b619fadfce183c3bcc091f7804da95ae28bbc64a1727bf573a297ab048d8ab48a97035d1541492755b5217f3e58ab43b0b6ca954362e01b345c1f2005e114029fea5976e7fcd7a249aa3e7ddeb4756d4780628a49333168517655a4c08bff7f811e6cf1ceef8acb82a62d2c0c409782729368ebe2bff16879ff831fb5bfc7819bfe5d0fc836a9e6a890e02d3663e81726fa686e6291d4ca9bbb690e4453c2d4085f4acf75068be63a878a7623472d4e08bc7c51fb05eafe7667cf3badf7270061b937c8c2a7742b70ba41ae55ec1682a2e97fbba1932e032b967f6b4769c85d754a18a5e5059217fa56b8c34cd0d1ccc26dd7e1d729e97c796b9dba6bc7d04d027defa517f917deae2c434ddbdecfe09b12f4d5e403c414478c1feef52a15e7472845f21410906cb996e4f6ae1209333832d9cc6cc8147485f00b79ffb6c9022a1c6091635ee07c2e8669251e76f598790eeb1f03587042b95f4a924f3c8e8d36c870df0498256d810168420190320e249cc958471b53ddc2c06df9523cfa92c184e93635ee924378f328e32a6ec628c63c03f09c9541ee2fd3d44a36b716847c0941643d84fcfd5c70330e929f2c542b36561326aabffcd080239c2fe26e2856946e8caf92a5efc03eb6fbb3da8e79f51e30f5ea5673c62e14a4cb01a69de87c17028519ddb9faffd7507b6748b5412202f8ee903e9c9e0dddc2f973f3200f1c5606785982e13c5e6883622613964d4539a799b160409c0ec0f0020b4af79e6f5e62e6eb4a2ff719b3277280635016a41c204e6d50d0a8a14f86191405002086e22b9b1360a877d48b92eb67af12c939c8cd8bf7665d7a40fdc7b6880d27ff4e26036e07ccbef3ee00c673df2a7c5105faa324151e972ef1490bbf0e454f29d91921a11b1cba41fa35a4d7eda4d00da8134daa7499e28d2417d17821a3008426504c52b9cce4d96d13086f173a1eeeb715e7d263d210b2167f272a7f2058fc7cb260b885d037bb68703632eac66b0852b515fabce28751b006659e3d09a56abf0c2e6ba16662b09997a525a311247559720ae35f5e43893c351a6b0b822be0a75b3e07b919fd85e16363dd0122f687ed2e40959c54e3efd543fa84f6551908970f5d525f8ae74295245cb61096c227a5c543f7baf7109588857b5a22debacec1f47471b229222118657f70e1fa39c722453d3f8a0201bbb4fa261f6974428f5fb72ba5d80d46e38a8b7665a89d277d83efee8d78fc28f2df54b60fa57747f92cf22ee4b9f55af5f934d78932621bff57513527947898999010f8e68854d65fa6e86f5146c27ed0878baf131c886a0aa0f587ba65a5eafc652c3158ba25bbf985fbb411734a1023e27a160556b4029df7832a695abbac28d24e298f0658055fe18b0b2f7c75fc20cc5c0f034611958a162a2c995f0875d4d484031ddfabd7bf473c1f6f99b8eb588ae738f2395c6628b1a146ac5c5b061414f192e70cf4705424cc019541c5584db4f941e86f09ef4ce9a603fb71120f1cb587021d54109bbfb880d3b2fbbfa69ad03425d0806d3213a36eb0e986c14577584e389fcf980ffd840022f12c8efc6e3f17fe505e02357db5509038f9e4684ef529b9d03907093860fd5e5e882b73642771df620ac995facc9862f0a8bf6988ac633acced7d641ae34a5a00e57768c33511340d5a7a847b347a100787ff9c085249438f3aca6e3521b8ca2371f87b3f47fb731fba7acca0a8bf313e17ea276ade08b41af97f17fb913e3b433086e083c85d0d5f9e54f0b8de5aae98dee6fa82c0c4de22464a577f21dde1e5686bc2fc9f93ed36ca6d063fc8547f6c4cad4c3d5cd2fd3de3e283d0c13468e7f267a5af2a40475580d414323bcf5f2605302693a026d0aac0bcdf3b905e95a8ddd77b636603ff45f0531455983e49b97f9afcb47e58751cfea4b972d9980d0733ffddaebaa4d10c9fee9e25e3a512beb962398fba3a0fc46ff93a0feadfb0b636fadbcdd4fdd4a0e3870f52cc3021c04612e8411912d12e3a10f6ce41d451b44347f220d79820f2f55827150ea7b92076c30c862cc6d20dca2ab14e50dbc46ba7d085895f06f54225d853c086d6e5dc28417ec50b8698e595d072f30b292710e3620980a3398dea29bed57c36b5398bd9c96b3d70ec5c1886a98026a2171afc94dee8b71472c7fed71956b35b4146cc5c22afea2452ec3334528eabd16b49a91a21b4f5ba342f7bebacc4de5847d1836460ae060586ddf66a8ee012bd5fab6469ce6ec7a15980f7fbe9a8bafc806607cb389cee1b2cbc0b678f3227b9ad63db6f0026e467783eb105f68e649030df736db5ba02bc25b9436ad6206af68277cd666561c93529f0f17758e9e53e8e77f12be6a7ec30c6b3d4d747b5b89a8415b64972677196b0c43388d9b89fadcfb853d3d46bf99f231caa8ea0ceba27bbc883b988a4fc542adc457a8bdfa3bd9116a11656a3eecd3c007433995e47ad38645d6431dbb94b57b83df2a2be2ef84e9d7200573d7a692547c9c373c582d1cbbc3e3b0d1d2dae7d902d56276391ab02cab07b86a4349153e733e166d0241c0e65f4753cec2f079cdb83c9bf44b6aa628e691f61bdca10f8213ccef1a28e81dca5c1dafc6585b0c71d65800edee22f1b4c678d0ee831ae39b2fcd8532d411e542f84aaba4118688376c630e274fbfa3ffa89fbc3711777bcf40d1e124b390ce2fb740dfc21db3a64f1ed424559c4732a54152d12b9c73309597244df15e9481466120cfa697dbb531031bbc77058d0996437112fd9c21d38ff42829be5ae0e4581a083a413d46eea32982d670d7fd184e84fbedfed56087ffc8cc4999ab24f7b7a7b03e6d695a14747599ba7e8124ef3b0eb01677cdc3699e92eb8e51bd2dc5ff956ff0cceb8f00a602d000040ffbf8e6b2f0b86f080b7001e50f6752b5e56fde5ad7fb1f8594b54bf766cd12afba846dde6035dcc343263395d2f9c8bf423bc29eed211c7a3320fa8807266d5329149d4ea773ae366fe8f581329b536440b19d928b804d2191e1caff7c7348464898346c2eb36fa271e3d6ba46c4bee3cd71aca18c518e5d7f0d3b1b068dac48947d79e9cdd0b7c16e9194980400f238fbd3ff669bd19e2629a8dc40492f88bcd7173f9cb9ade59de61f4f8e8aac6a1f10ab1625addd4724536e0903c32516e8c5c1a7271bf511280b7b3c69b6503ee5be3637ebd86be62447077319207235bdca3fc1793dfaaca61ba57b75275cd53c19b392e8d5b5977cbc2ea15fe0e3cf860c18fc3a9ba8789fed784fa8ba37c44d894e1d08b6289af306ac814ce54d17b84e55ee05cad244c00ef65f1f175b9d17dcc3d30df3ad7f2fa9525f828059e3cb92f109dfab8f2bb7b2bb019f12bb4697f1ed16baaaf44cf2fa95b086e132909bf714ba06f58344525be1a2b2d31dbe261f18bbe15200ef30b2199593e4128bbe3020ced4bb15c4e96587d5d5970985606e1546ab098bafa28d3d41b684acddcbf163bd8e2842ca7a84d82ddafd8f24c27f9172b0146a0499afe59d13cf6b868f08e2549398ef63ad1e2b284a79e26bc48600b5fe23abc86581ee44605d6a9235a59dd88ef2ba6b6a39670ab6be4b69d69aa863b68e63b292b814d82b3362faa48547f886ecc3911e17919c751dcf561ad58f10d955d1fb32cf377a224f01e02dae1882526b2c9a82ac26ce36d31b06ebba38b5e41cbf6163b70e7b4199a41173e207b442453e3407aff6f9a9a64116ce9324a14cd3f225a6729dda726f4bad42a979dc4d4dafa6a44501c527e40966986590f8d208b8063173cf0ba19bd16b60ffb4df0196387fa8132f7d1854d1d2c830b3c341ec51ab22326e1eb293240f8b9055d906fb7e890621ea87adcd921890ea5bdd341b9884de35ea22af7af7504f101016c29323ec6cd10e9f4f78ff277ad0b97981a328da663df282e8d0f9a71435d7a06ab074f4ff3a87459c503a96acc219f7e83450de5e1ee3a98452a86bd01e6efde8383c67c171a020e11ea05ed12105ab6880aa20460d7f34a6931a799e0b05866f9aac1b438b442a526fc37d6ae4844392fd011754d4c09ec40e484480e83432f26d027ea648c54a67571df8a4934d3b14707c2273b59f680768915bb8f397b8c1a7a80b565655cdfd10590e26d45f8b2de7779a8225d874e17985a96bc0fa406ad569e762de4d805a60ffdcf51b638ae807e665c27cee1bef651440b4e98c8d250074823259e3ed0d9a97c5fb1f77a6a490ef1e323e48a3c7c602e5f3a7e519ca824cb7ce2c28780fab59173304400cef55a30a07a1b9a19767f86e8770b6738855c61d9b45b57a611b247101a42babbec5d58bea60eaace20780073a4d26e1ef01fd0225c527cc14869946df28c7ce79cc97ad73c7fceeb8c70c7c3d8c0caef6ef274adf69acc55c50094d345548c357687117dcfaf604a278ffa956c3c4ade35b9942aaf93e7c36bbc59c767b55cc4a9901e423a457a95b5c0f362613944c9ac9389039a1d59648c1d9129ba75f43f406deb268e2dcdb57822fd343a44e4ff68ab18fc22882acc78e8f74f3b8367c291c5107b1f07e889a9e1964f9db9754f822d26d64ba2ead8527e8c62cd9ea8b096d33f3560f8a93fc671d7bc9f02e6800db8af3f582deeed6f3678ddcea7e69fb6adf314615ed67c2aa09d58635e356f3bc3ca297a114c603c6152dd3d0bc3e7a1920cb7204d742db7875481e476aacd13a7e3964d46f1120c84bd1778bdab6e0b9e6bcb3092ff3c74c4c35a19990f9f1317601c0b634be43477aaa613b3763489ed997194764ce11ce7500628f325e3cb1255453fc5b3e028caf4f5bc8b954e295631f36757671e7210558c7d83efabaabef91f5b72c21d4acab260dcaad4b0aa6d8f150e220fa36bc6e7e51d67090fc932acd885cd30a9abee9399aaaa6e5beda086aa43c894b12d848527d072e145fea7748f9d1a1b590052580f30f773291919faf1c3665b86fc91f5c1b3456ef3e8baad71c669a849f440f8395965e1d82b15cd960810ca470fa5a832116a4b0a9ef983a6e41e6529f995f202c6c8db2d795712095ce611d2a9b6f83684e219f466eb72faf736119aeacceb0f086597b13767c558014bee19523716060ceaa970c43ca12cf0f3ca95958cbee729750103018b296c927a070226c0f9913684330e87e8b3a2104987c22aad6eab9f9db5cdda408cff24d53125ad8a00d8a7f0e85cfb402de0fa73784c7957172216e499f00d3e9d966a68891f72ec7932cd46c0492e8c06f58081881da537f5c61eb82d4ad90b4d815aacfdd39260b697f44de71e9359d34271224d15368adbb8b756ce68171ea5e6195e405d0c7506580c7ed0bade4d28f72f5daf3e5dbe6fc96fef18b39cae61ea142b0304914e0081681862818d459b6292c85c83fa4a62fecf721e69a5713bb8a592382300906db08e473459b4b29e692ef04afb2aaae8dee9c9e56e9880f43d67efb6c3d833e5c936cffde16726a86412fc4a1b36d5d17afa0ff131b729c4edd66fe1b3c1d64f739924d417f000b6d4eb94121f915cd9770bf805b116b8f67b0d768699b8962fe5667398b6790c50f02592bd98cfb21ec313225ca0f47df6eebb54df4ce9835256c1814a45b2f147bc239c5bbe77e45b695784ff2b1695fd535531debfe2201a28720dd09a6c919afdb040dda634ab1571562135cbbf856705b75bb275b019ef84c218e31ee5f627dbf9d07573dbeb8bfc55644d5599b4d3804738438fadab7f57aad4da3ea351d3aab5b2d29e6825be35cf1ad8421b808caa1f212455fca8969f3d6437787b42574591ccb98636855f5ec7fb9373e1174a270af05a4d159f5dd4e4d43eb09412c5a096f9ebbe41211271bb36ffac3b2b55b39dd2c5999dd2e92b57b8035ee290fa6186d78a4f1cfa1048ad0cbcfafe4433a938494f1b4606d4c5574101414bca8910547dff07d6e28314b7128836e6ce7b2372dd52da3095c9c7bc298d4e59f67f16b8b590811a53a40191b2bff941b9dbb739f145c0b0493b8d1b9f287e9c35dadce7a553b0ac4ec641902107f9e4b610cab32dbdb5b23be67ade76e1d24e44bc4865eab6f697ab61e97c0a99b381f6bdcd1a42991e700ef5bef071e039f3a103bf7db53cd2e6c49f3d22de60c33b27f7086c61e419606ff1c26aef825843522c1e76f2a0c8cbcd725d24cb401d096fc9fc010d76af3abfdd65b8aafdc7ee82efae76b7eeeef85fd101d521db925b1da8825153b3bb1212067a00216e5f9d001fe5cd0eaef0d3c55b4f34bb9269b87297d8710fc9d602ed6c4ab27dcd410f162df7908afefdcc3605b0a289319dde4a8ea638908bd019a9e1b4699ea9deca45f719c72a1213624665cfedb800107659c291744e95c9cabed1ba8e804e1046138601c09dbe04bea1b4ae68db0d571337b3844e383641a3a00e1a0493bae7817bf7de7cc5d7e2df2ebae7462c6786e123a5c43aef6f5e78d063f39815a086984eebdf2e1c4d70e548b483e7ff432578108d46129ef3d964206530b77f280bf2af341c4fa59d9b28e8e6981fd130f56752e5cb33e24c7294c18c1658df09cd7b30a3eafed81977b67454b770f7a1271ed72e9d1cc7da5acb086afb4c3ea465711a688604521e78c793d113f4a0598b14440bcb6b4aed3e8e5814e79beda3a2ebbb30fc04d13cb3f5a2a2891ddcd4ce71e0221e51fc3a45126f66805ae897210f9cbb55966e52646c5db06ccd6a92075de73d577f7476c950a41f9dc1e7ac27aa6b40f883795208f98012e59ffe4dfd02ed8c04c390e0cf34ac520063455513e24fd5d47b034b47437e2a3ca56a5db10d445bdc2b7fbabcd8251a04b33f074d785285ef776ac581831fc43d82ca6c27a13925b50bc8aeda17bc91077bebd917aa14a9aab56e5e251640e6f703023298fea0934836853e49bbc53b7d8c417c3ebe865908ae66b446ae7a74cfcf56ce22e242ad1af7458ecd788ee5a5ff4aba5a7428dfcae89f9c5e5205c9545988be94b73326c60e38c329020d83860a47ededbb59b98a6518b8ae04bd2171cd7cfacaa159e83d5e1cb3293f9b5da888c9043e435519de5415e32b0ac4635ec3c606a0cffbcaea344cf87e074b03bd3b40c16f2d06a295a975ec7dde46ca41ea07dd6e2c1856207802ad7373c320f00a2f373279e783f8ff4fa05482cf5dc9068968d2d24fa6267d7f0796a19aed2c2c687583cab9fd39bbbc50583c843d01f7c1834be010019896d83d9e29518e1ee28468e46277a712ce50b8cdf90a5bbec26104324bfaf92414e29971fea97baed19564098d32062b0e36d52bb15a19f527043b1723275f38e2d7da1b906d6e61e6ae17d6824bbf7884327432d4a0027966a494eb6d2a268f0c38921867b590306a14e31b7e165b95a7569c2542e1d7658c10cd79556dde716f27431a5dfab6401abfc09fd11dbcb04c0a2a92f58f7435617f81e9f840bea94915df352eb5f34114e31bce1dc14fc68fc85020e616568d96e57c15adc45ed4ad9af4b848f7cc5aad42b843d32248f4414f6b4763a8f2aa00b60f513315179704d0dea64a372268e85c10f1fbf7a3a706e4adb6829fa129f72e9aed5c9f9f923ca5c53cb42ddba611ac18e4c8902bf0b0d51a312eeb46bc92eb9deb72cb6eb12cfd625a09e5ff54de514768f061e35a1be253f4d495a320e710394fbc5ee3e8f7ed1e20888fd44dc53ac0acfce1609f6580f8f413c429befc078620bf0375862695ad47ec4f19020b5795c9423b004c1916c33b10ba3540b66a06f350191088d14a01fdf8ac5ce3f9858c1cd2f5f9a2cb6978758cf5fa590d3a7b68de1c6cbb6f714f6b17ee1d47d594b3a0f8611cbad219f4fccaf0a89cba785e437872b3108139e0db1275003a3ec83d41a9f06bc815796c22a45820401e8a78dd06bd3f72682f51843b23642b141d71414e54c07e8e691525af2f99217e621429980696bffca2f74fce10c1fd507b368a7b8dbec7a8b7264b979748bcbe5303ef68f9a4e001e61b0aceb27968f799288157a1a6ef319deed382987cf104af4a49615d12be97fccb2b739650b444c3166314b4d6fe45a0902c4f850caecf2202a26409ef603edda0a0deeec012c331e42ac86f82d8a241a0c9ef405ee99c92dbb7092f918b4fd1e4c2e1130fff30ede19958eedf0f40c619dbd392fc33ab382fdf73b3a826138f0d982d4573b8f48dff0143ac47f5e015df5711790cbcd06178fbdb51e2cbb0a5257d38e6e88c53a9a10912b70d9fd5c9a1eaa2ff7f8a4d2676570ab38ab77464738989968e30d7d9f8fc7cb1ea23bffbfc3d386ef7a814c91c3c619946ca4ae38b58ab881891a9571bb0202ec535c62aa9ec43b479ec36e2642965dea589223e61f0dabf9e0424b410a2d5ed14ddb09a4177e7d45d42145a10687e5cc321a389d0a9ffeb2ab8ae97b35f3645c4953e7f8766c6abd934881f69f9b5d48df9423b5a0c328765cf1607c2ff2cf00f875714a3e85f4dbe9ac71b4b08dddf872e4ece4e3ef238a491e34e559c1d97c9bdef3166ee2bde542010df3f1146d7d53c1d88c56bfc1eb44787f8df6cd32196deddfeb2664734a7ba4b98a00eab392d08957576cde90633e4841a473cc797aeb36db4a6082b397f6d3cefcb33e9c70a5a7bcb0a65fab1aab72219e82da52e40734cebe7cfdffe96a117d90c9bed925a6356714f0052db4e5bc5d93050c12a207c5a5a5d1bad410dedb1d5c41ec4216e0830c3f485f12e49031271d3a6688bddc762e443d15391bb8c775895df4f463ab700fed8f3c1b8e129a5ebd8007368dc957df753ae48385b849d595d3ab2bc42721a0a0488dafba598a56ae05fe8748e6e219e572ac2b770e2b7dde2fc1b10e794adecc569a21277903599b10747939ee13a4d8bd4c281da365791a06a8c6faa4a53ab0211853f1a0d61d87575a2b1b872205378696c99d166fefd7a7022fa9adb749eeb1bcbc633171f11b7f64be66fe27d6d13bc2cfdcc3f20ab307ab62f46fe70770178de8cb0e55b62749cbe046e17f04198388cc2d38f14abe907b07c36551474ef0cf66dd0b5781f2ef49fea183db393f7f9ca2775bb1c7fcfef8a6befc039677e7e6cfe2cf70cda1a5b6c62e7a314b35478f980ff2600d13f4d10be0b5a64bb59439dd0d1f55570f061efc4a8b6ce843b45f5bcdb5c9708d869f21dab103ad5ab038ec68f50c25be4aa04a9b79823ed63bb810f8fc431c3c677c39e0d14a630f29f89eb5ae840983feab4cfa3b02ee3346af3252b6fdef48987dab9d88a0dfb93fb8283da3505c1277b2c3371ecacd09e8cc542c1fb35c0db3c07c53fdab371fcbdf8867358565ba16984435fbb85bb9d2ad600464f7d7bf6f39fcd72a264e85ebc3616c834c17150b4bd43cd9d2251a2bc83b1df8279a77df9edcf16c42e60a36d2e5566c54fe4d660ac03ab4f87852a4bfb684fa34b9907f625209abb36b605748055dcb57b47ff8da909b7048df9dd2bd34dbbb81fb076915dfc1f259281e0327961e1e70bb5d45161ac11a8e2544508ac54b6507d970ca9209d177861aaaca38eb9eb31d3c9d08f278750d076c6e30c38aed56d23c4105e4f945809516bec148ea635a5a65abd6eb1ef14b12446e0f80015e1c13afa6431d24db8637a6de0dc6f32df78e9822ee144250302ba6051293ab8d42ed24ff1053ed927db5b0d15ec34967bbb080f714f9dae7a48288f76bb2d252866a4f06077ead2a92741215038f0128c79abf739bb2cba98485355007a9359d2012fde3097867250781fe1a9fecc35910dffbae7d6a96d85b240993c4f001a8c061b84b47e6375c8b6505485e6cc5a6c25b2efc5b86f9375d379bf2ee35b6012f124c045dadf93b8603947be3516519b8ce8f4f5b46f3bcd738b79f792aa0fd3de2bcc358775e8e8962eaa53ccee165cd6426ad28b93b8c186c9669f774bfdb497249a3953099286a1a1b09d0b8dd8fcf25d4550442aad01efd351f09bde8ffdbb34d412da84c4c7240c1a811d2eaa868b2d9accc32c6fdc7ab82eea75ce0050788ea4cf41e1d41a9f696ae59adebfbc6100342197eb6651ad795f5d6818caa6b0e605d665b0a0cb32459e31cd083630e935b7415110721cc67a2e09ea6064fa245c0a628b20a5921e80805bb56c70a0d45cf8aeebe9ba0305cdecd45c41cca78fcaa717f41c27a0954d6581c8464e48775f1590b442b0dadcb288bc11e80322102a2e1c009ad496a1e3f6bea4085b46e3ce1d77689778e235e51a2e7e53f0b33f25f0669d33ae08da290c218af5782c44fc3ff1f97dc8b12170bbd43bd051b29ac9bba0cf838aa76b6e5c19ef5091c3808ced6ba8361c4c93cc47fbd00eba026362869ced2ffb099b7097a2ee279d7fbba92b43d120c83dcc0036f666e49becffab4449ed06474dcbf3edf53bae9da25ad34daba48382db06bcacf47ba1507dde04517c2ee24d14b85da9d77540270c04bba3feaaec45d259055aea1e07b4117572cd13e044277f843d54a090629f10507868d8b5128bdadbf0080c9eb9293f9fe63c05ba0182dacc55f538a26ccdc98b0e8cff787208447834fc3588ddd3f971886d731e4350e1a223a8aedd79d791af13971e17ea9f7f93a7d3aec463fd5e4a6544123260a06acb1d317886be47db3de5d57d77ee4bb30ef7d6558f7fa70e7d9dab1caa267186e7a673e59a213602de265da00ab2a4e4ec6d8a99b3b98cb132351e77ea4904f3145377058870c4d717250eaffe28afb151aaa25894e381d79960034034bd1cc5765f38eb0bcb2e823b53b3ea53378e98412d197261f7f41b45cf874a0a95f1418e76cec525eb002fcd45429ef298866d188b216aff90a48a407942324fa0bd7a079dbcd6583737931bac94565d578739ade1360afc83cbea1c177d527a9555ee0f3fe7e821ba01605ace9eb39712300ca52d4acdbb440c5b78ebc1990f2545de50f27bdd2f71d2d4fe501009ea905710b550f6c85524d3b48c82096a92db24ab8d4a974719e5d1ae2731f208aa2fb04413267a80c39c0fb47f29c67077a37d9208d1bc6c7563104b9ee93d1658c238d2fdb3de2b0b113a3626aaff90729724d7032e92b1d78665d3d8ee54ffce767f9bf3d813f1c38d5091a4b40b04ea42f407443846cbffb3c5d62821574a699f4d135fdda848e0c44d132aabf6726693aee4947c924dec872e87888d7014bd6f818902b519333b44c150fd503180800143326b94fca684e14b4249e432234ce8f2a6270b08d9a896029303849b35d1ee7889c7739d09cfc5149d356d91f55bd26d599724d22d1ec139ca43f2343ab0753901a0c0c45f423913d8b26ec8cdc52b6712d6fd9b8666d7111384c9ca3ff20490a3086fb9c14d40791e89f93d7be686774a808fc92325b22e83749afd4a32e96620c6d84279bf90d6cdb2135ece5e307b43a061608a7c516da80eeddfaac41c9e047d2709575fdf69bb79fc7616ecd1486a529a0926ad640d472fc55c3d85d0789e1fd09f2424ce0ffdd441faf4dcb73c82a9234c2690f5e5a5940ef01ef5bd3d836c167b514f98fd39ae0dc117f8567a421b62c5012bf2e5d1b92eb5d012aee941785d15241899a9cfced703d43450dde8c77d5537a0cbd8d39ecb82b54b390577f50d4c141cb9813acf02f5f4d4ee74e928a754abecabdb2a6dc01d86b674fa1042afdc088b57ca1a2f76b345071be101f0d941fb76303a4f5a7cbb1ee5841946ef6443fe9ee4dccd92fec88b7dbce49bb77843c3542b7814324bb4a81e665bba0f5c600b0efbce2ddf643844f207835c7830f8fe6a4396a1f2690d67439e5725744415672ad256f14b7aa8e971e784ffc7a7723e789b4b4245d9ca8da53b713d9fe3fa683949382b6ba5ba9cfb9897e78bbd9b1180ae1c29f5754e1a80ddda9bd956c635bcc2c5d7b8d1652e2da64edd24cb81c1806a7362a16ed5aa6d96502d89e7fa38295d948dce4c4b7dfe90a0d49f296df2318a22a8b85c00e82102b1ad884e870400e2d9ad07d4175335cc68ad2b358e073ec19fbc33358e3298af02de96be07ebc8b0110122f7ae45b86a332e6ec414aa1c7ed192b4b6270355ed7ea0cc28c7039c8a7c1dcb20f8b851ecf2b4829e5eaa6bc0f8a96cca47be03fb80215e2344b0330cd273cddd913c188f479181a4a1040f2bf148147624c7a1dced8a69788c0c8ece464c61e62cab60633cc76055e84034213e380359aa81872f95728a38d8694bf9feb329d7fb5c5e04820fd5fbbec9288f7c58da19f2408e7aa6bcea06fa882e23af4e9f2f54eb45082a12c2a89198e68b1bb60567faa933cc6c17d045754279136a1ac057fa5765f549f39029137204096c195c9b5c779fd6e6b2006675ff75664a10df77f2783be29d70adeeb27cfdef3188f3989420f13b9dc968d0ab33d75d9bafbb6bc615c7962360a926f42bd1e61df31033c1209501c062a91070f2921dfd4ba063825d4ce43242c851d6234342db049e703828bf32cf32f94bcd938a6351b9a60bb4d95557f9fd7a0534cc7a353b9464efb1e441308bf21c02242564911663d80fdb9e9480c3287f74941ca02884629d3cbc8b43c0c8c696c7251f2e5aed2f6f8142e053144ca586cbff365973b902b0f257d335c0a30cca2ab762094ef43fab224183cca7ccbd628fcb478ff242c4cdca42b29386299c0445283e4694e919bae15d3300b9ebc116766cf7ecba24cd0d4f18fc8647c14fdd79e52369e5d6c8cacaa31ff6c153ad2be3a67324a573272c7a9425589102883c6f54492c5e3cfedc19611d51ac9af446a04da0db8b2f6b70bfae8b519dcc35826e2ab421c41944ee3bea5d163851e6280ea5bc1f9efa6400806d79bbc75afb8241fd3a9d6e4923e60fa59feee4d0f74e44bfe6bf6126d98a53ecd15e0f0ee15a5b86368a578f58da0ea825ec9cadd5f27c165d476ce5910be3bceec4b13f1de8fe9b3fe3867d359f087f594bf913324c892bbdf273bd3b57ff9a6edabcaf8bee28c9bf2b21e11172e4b85c5a099140e41419ad28d4ca738369c04734927457c8ab1d20cf7f11f8ca883472a208e9bb0eb1576c25bff62c7ffeba98e08abd49475030e6763e9551760e1f917513e996400549bbf37ec2f3ba31f452d789147d523c6f3f686945cecc036cf5d32fd84cd63262eb6645a8222d8809c9f677bc467642cf95844958e4910ff848cf4278d880cb644511d1192e38c54f4bded966a50f2ae62372af821cdb6a78c1c84db3c384e4d0cd609163dda7e8fa259699b1d8e6776af296cd28b590129914b6d7256979c2b73f0789be7e54f67d3a27469f132784dce6e8ae40b6ef46bdb9fc1db4a6f33d216c0e24434a0b8c19853245257401592f1811da08409c648f778e6962d8f09c08984d55648b4944d3f9d28e696701ba1d52cc3759e77c1ca4e1478317e4b7d6c4d8ca46cb772d0cc4b3e8732348d427c39eed5d33b82afe30a3f7d991332bb8ed9517fa655f725333b308bb3efad83d3c292eb8b43b66674e0a61cc5a03c72f5b38f17bb6f981eb1e0d865b17f5c5094f14b73cb1a10c770b991e5f71a140b37c14e2512d364e08cbdda56156cd1a353c94dac5350813691762adb9cfa0dbdcccc9fc7bf4b7bcf641dbf9c3e09370507eb3d56a3c355c9500554401433011a64e9c4f49c56bf6e217afbab0e7767a846308d0d1a1d59b75e5d85994330f7c63a1a2e13ff201ed3dbda371debf800b6787bedfffab4c3ad041738bffaa24fb47b21562ad9ef0e315a2ff5a058b133c1c3a64250ef0d80273df3bf0501192278614d907166286e1aedf0ecdeae8d27a52ced671c6ea300af5b7dfeaf3e1e7f15e06b8ee0199e2808722a4d698c13c4e313fed7d041a23d74c4187fad9a6b15eeb89a47db378a50faeda259dfe2784691c14186436655579e5c5637a0a6578966ec3490e1025d9c066bdd79f8ac74232d07c4622f49366f648e3a1a7199d74392c8a3d9a8d8d8e65e2750c1baf67307d65162160f53be49b95dae937c185fa902068b2a32b7640bbe5001f067306e2ccd5c610c0a1dedfd708dbb4790abc5fea51d0ae0a420aa4811a2b811dc323e8d83ef9d83030ec8a43e0d87a556821cd1d049a465abdf13f3618bfc8e3165c4544583306cc394e5c852d3e754855ef0552c749692462238555a64e82d99d84411c4f7485a15f6605ab9705b28dce7c4807e305ff1bdd4eb4e631a080ba575e7693d1f8738c4a3d98ffe1a81540aeb24cd2fadcfc986ff0a35304e40cdf5ee4a7f7705509dd4c0717c583208fd24cc602914794d3643b2166457e88d1ecc4d06cf867e156b99efab726f79d271d8406e2d70f4c906e3895daeb22875e88c06f83c953b5311e583275b20fc1e04203a7e6866a321eb6fd0863274ac9aa617e58a30df7d031b66e995c79f3d5fee04b234de86d3c75336b2ea2be7127ec8dac1070eb3b4b1b1b4ce7c1cbc3c5cd10d4fec327d8dabf89b0223724a2349fa1fb728e6cbba5ecc0377ac4658cfe77182854874c8e5c91802acc47a85f229061c2ee96ff4f09d59235b2f1b2aefabb8d43b6763a0e15d2c4a1b856045103f040c59e8a29646a1c594c478931e44acfb7067128a4e8dc2f8a058e3abfad70a5e5082e8d4f5dd84ffdcd5608b62e37168ede6eb262b01e50e5a6ae7fc20caea8467b581835c2e490f433f9c6161e07e06a8c8dbb134f137782f5727d96059f716641ef30a42fcd13958e7b29efcac828026f05183df9deef3ce971863e2e8c32a3fcb1bf9a173ac38dd8fd2e8ebec8883007b6576485f4738e81b79bf74aa51dca62f5799c49b480830b1e6b6160ee836cb3ce2b671c46dc64c6466633f00e37abe6c3fb7ef5c4cc8522a36a0af758a0113ccb4b70ae701ea3ef761ab22f2e6887074fc36a70b38273004bef5a757902b4b8098eb634fe90d81483d62f3b84f62a373f410b33205375c0fe8ddfab4d3528a6652896ae94ff14614e13e5b5ad1d2c5671cac817212d8be4b468df37822f3b4ae1ceb9a7eb10dcba076e6f6158d24cfc50130067b0788c8da805976557629a59e373761a001438313f8bb486378d903e53784f1a0b6fc83960afd657e2aa00b3e0bf0333ffa1adedc1ba67552600e843a61abfcb88cba9107057d64a2834216e147d00a83828cec7c1121ac42704598c517b874bc07b3f53640737353a49b7ffbcdfa730554ca7d2772d686b876234c55fc8d555c9ecf5935b80c44cd4b514b5149f589ad707139286c704f5bc7df7c0cdd5b90bfa69dd0c38bc95157add498ae8df28632da58de0b217e1fc9afedc9d5b9c4ffb8029c516f7fae658777037250d0baf003feadd5c7b627e446e293f217961fd2fd344412bb393f0b67c0ec5634016612844eb40708684ecba00b3070f76104ce665deb73c03bd95ab16d4cd117f846be82b62dad97efd8745b8de8564081bd31e8a65104437709fd49e5e561185ff8a7d70d29eba521718bd7c16da35c748ced583d795b08db038a29a10fbb542be38c7bc9dd381fc0ee54d6dcd88c82baf884ca6252a443f1d461526b70a44be8739419e7d0ee8b25fd2bcfaa5e428ceb5e7f558cef05e98f0ca3f11da79d6d2472f49564b82ab3416bfbbba40fbab1c5729762a3c8907de13fb3b59f4fbd08f42624373faad19a331fdfa6845352f60dc390b2d8fd581083892c3f64ab35fa4282f9b98dd8a6df38e2bce1469fe90ce365140ce4f93d145c36c562ca0982a18fe9d8a5e4c91b5a4b1a9b1a97cb9edb88bb41255cb4958b37e9907eff8500306f14b9a8ff9abd8d9158a88f4eadef1dea296586520da7ea4aca9fb4a1716883af583cab34a3f1021adf5d2f8e6f00c19acf72eeaaed774cb5358445c1e9ca87eac6a46ff2e1de8beb92d903dc243ee0b45826dccf0046dcbc55cf599638571a34e0ebfd89caca6a690766d6fedcad93144d3eb34c200aea7ed6e263d1d7aaa0700272facc50feb5ddd4bea189b60a542a5167e52fe8c221cfe42389388c3090f964bd28a77a240c8ba0290caed51b802c6438468d2e0dd49e2fcd31f7d744273287b8219924c458fc01492ee55e944167d6a1bd2f02666fd41b137e480393fc2cae902ce60684a351a6deb8b81ab9174b6811e7c42e1cd5fbca52e6d5146a56248fee4aa048e34d754f0c75eb1390254684069a5adb5cac4ed1ea9fc91f4cddbf203341650758bef1f0ebba2e7b652c2bf8b57f5688cee0c1ad86b19503905055ca5d7db6404fe0a36345c75f613cf56983957a436e082e1e76e434e56760cef3ac7f59408e35dd0838be1814144cfe1b3c22bb8bca48beda987c39d66450aada707b46f46a2cd2dfb80f533dd4eb30c39a55b9ea3d073ca18d24a5508421b59e7cdf274f7ae9f0960b7b65635418500482ed191e970cf2395461a063455b564a7cb05214c04c000cc3a352727f28dc420892c771f6bddf1f9f7e2cbd1f00206d6e54c1bb44fa71c17c003a66e26258c4ac65624dbb4601e831afeac59c5cb0b74694588ea3893df6ffc385ac20051167bfc7078f7d50f5874a2717dc617e2ce6d7bb3fee78ac47133da881cc712fa24191d39ffadb72121e9e4641d6b2ec4563ffa3c97f97a8a451e7f681434f4d04054fb22b0c9aac77ace57dcc69b5ebc7c8c7da627816f80881e8ca96ada2dbf09d5f3f2dc70dd4a69e9ab900187ecb14ed91e0fe1699b3e55908439f040e415e2f04066c66aba74e9dc67cf9cba8194ad7239d6f5bd4e83caf2cf8e9415b172183e79a092bf726809de35b582afd345c4d19587330629b0c3b41f4e8ba8f85eab56b33290135030ff635bc070f8a190d2e816031a31e96fe4d0a959f7d830ca2b37e0a9b94330ea308483fa576dd533e2203034c57aeff98affe3865327e45d85849d1e30c2e613520fb1723e659245f93238a58bd62c0216daa2a351dabca5718872d7161ce89b7df85a255d8730b69dbbf7af856735598d24fdc39bcde2926f0ce8118b89db16a6de4d8bfb4b04110b839ae943773707692c8b70d9b293020c24db394d746bd1c3b96a56fb9e712b1ee45b3a38cb999194f556a5e0690e09bb3c931087224826f0ca4ed1eaa2da8823ee055b6b635c9f105cecfd3c6fff38d16a047c6703d9fe409ce3f778494db7ca797eeb944d0d6b82317467bb4441cb8b696d2b0def24a1ace3a1e2ee71381dcf76f07007c241a82694fbb011d8ef708a6ae81e2a9c084f380d9aa34f2a1d6f0f2d900fa0c863850bf43369ba7167f0326db7115f6d7e80d3075949deb84a9a8c48a69e4e1bc3fb5e1d71bafc31e6ec11f0b6de556e4ccea3f557b987216b15b50f8cf2de3750a8f60c5d573c08b11437099b0080ab51a4789e6278e79ff621289cb9efd8e7a084e0f77fa85ee6c229c32eabac1dca1c6d028ae41095eca43052f60232da69fa5d73b8b753aeb0d3b4a2a9020136c3f392eae507498668fd7d8e664eb8a644811912388d14bcea731f0a02d0dd41b5e4496221b6019592dc6c78828c82ccf271e4351d7051f776e54ba5faac32cc78ee1ef018a6628da01a5af581bc224f6f05d15dd264b80fd827aeba2b5fd256a8574b9e3b9de46f92f05ec1611bdb8fba360f045f064a9c9cfa0d1bf2b2601642fe6394c1870eab8e666d493492e2c5a57b16ed810c74ff2a0c5f4a8932c64b6897b89d0875626a46756438971e88b1010b22c7be13e2d16f98b4c65876099b4b85ed38784fd343c564c73034293b0b609e2174462014c4890637a18f6c54de46988864e2eb1cfe02c6eb946094a6d37bb5fa4943719f3ee3a4360b5afd3bab888a5be8a3db6219a00b41cb572ea3ec2ea8b4366bd2f1f2cd47f6327ebc421ca009c88402d835b33823914274511eebbd246458993f84e1cf7f9f082423251c5b03a454b61fa7bcb9e58f0e7407a17c397f81ed85a009c8e5d96683cb5abe69204af217222400b8e2ef2a605a9f721d8fd453a16ac49948d3f620b84b387c1e053f0a153486f251414d88bcc3efa40a51914d50eefd1ecc336e8f620f905d0bc211401d77a0de54c4a249339fd75d0c9c48ccd2e8d9311fa2f01dd0353c701033a681f0f9a12c9132b3cdb3cf699b0f7353d4041ed1e7b31073d0a5794979e4531e5521c348b28cd66ceebd5d8f7b473026484eaea125f5d485d2d97fba46184109b7400ee3908bdfeb0a85bccf67893dfd111a8aa853f1b6203191b2b3b67e2aad038c334c57df24199158a593b86876bd9d453af2c04d84541b1c532d29ae80ec8bd5a2de93f3cf08ab69270bb59f8d201ced3cd8bf3fc94bf86acac4930b4bb0c338282161a8ca88620f422969e5beaf8401d619bfdb07f7c824339b360f63adca5dfe33da9ed951ea0e18dd0cacddd9024a96ae164cbce1cf7ec4c95d469e128bd4aa07e635af8bb80e58eb5cd3feee54268d31a869aa0bf48ae46300b5c37908d1da2d14b006458577c7823c9ae88461a101aaba743728ae74d0dac90184b0cba298c3b05c251761900add0b64dc8c7e6e0852360d32abaf9d89bb0da2be58a7989a0a7c482956b3ca422e35ccd8cf4404eabce4ced764621a757be9b0ca10ae81612701c6277bf490869727047e1cd3a6f5258de02538b4d047e14fcb283a812ed73ef86dc7380e430c09c632b45bbfb1fb317ca4a7813593b85aab63a1c82333c0896ce0fb24560510dac9b3916525f82f44b48527db3f11cdb849cc8157e402c7fe0e3c1f2f671a2e7092b90a519a92cd93f591033abac542aaf88ae4744c3b9df942479dd88d5468f2f6a1c460686adc89e8efcefcceccc02537f3e5afe3c9d4ac37d91455b0dad2f0bf35d82b09a4b977f5342dfb194c56dc96d5934631c6c0339351bbf2ea0192b039508f08bad917dac2b298728bae06e9342af7e44aa7b9720edafc14d72b115df2f7d4aa64cf0fbf22fbb32300f229c5fe322474c7e1143aed1db824bd494fdc82a2815aa591232cb253de964d88db1f7bef6e4daa64e07637991bd882e4fb122f6299de064b3446467f46cf1b087a9bc2b8fbe279e432f0975fde86a4f727fbc5680915a01931b491acbee57f5b87cc398f790696d75599dda9b188be8d6a77d2fdf3ca5e946c2ef4928483950eb4a2742841fe4462a7b25eeaf86e369a47e22da5aa2411ea3ae65adcb9202e110a741b78cda73ef18d73123ea8e4ee2356e8b16abfeece094406f8f4692057d5baa41840a6bb82e43dabd8ffbf579975114f0721e210849a63a830c9650a893f9d7304f035e914e3ff767a33896690c311e4051e227e0d2ef0b8f350e2fa4aa4b18aedb347be351af85f8d2433af6efad6e63ce6c29dbd246d29b539f1e40a2f5fa776976e6c3787b5f91d52961beeee8622510a4a84a73160e8db8c7d16417908080447a2143c071c282eab20841d02d61cdbd6d9db7372303d9aec6a6f57aa6c9b1f23664dc4104d0ca764ad7939553c484e07ddbf61a5fbae76c1ce551df24811215eb86dfd9beb2d3888e565b2fc0bd70601b523f0eb6cf3a14aa50c0e8493855c38d61f70d51ca7d7150ab10bc38aa685cc00faeb4d92ec0f2c991449578f431763f53b3749a273d03fb2f6a262d738cd111e1b80fbbc27ea06545e8c796b98608efe850e6a311b7c22429351c45f5d664f95d83a84ef9446ce284a76fced3091d3ea7714905f7b3856aefac390463583ccb56341761df1374d1c7c5397791c1ae33dd69ada2985ed0e09f8118a8076d07e0dc49b50386f7d1c8e65fe4eb7a7e41d87915c421ab95382dc09b81c29e4fd91245b0c5f8f83eb258a755b35f9df5adb5eb4d96abd6db930c8523ce93901d4a3a6aa4963e91749e16004c51b024b6e7384f5645f8b2373a8daefce9ea9c8c6007095f936d7f648ffec4180e547115c4b82bde78a72c7bdc650b706e90066c074a0ec9aa21b6660fec094254946b5faafc82a4e9603f3dab88801f71ae4d5fc35cc53ed6fc3679ddff244016eaaf809ea9d50514169d917f70d353899cb0e098b977f10ff094b157166b9e880d87a59db3ab35cb96505da8f8e975515103e94ebef08d8609ce76a9bbfbb3fdf49c706697cd743e2dd915b5847d717a7990a91d16aa0e15c0739bed0dc9ffe91298b59bd23107126aa151ca71e914321168422a3fc571f40a49d2252c945c1708d280cc5518b2619a3027fafc4660b203da643f5c3842a1b7bcf6a4e197c103605f3f0bbe10dbc482577360830874861cfc3e1359f3abf417aacca391386183b1e5705027219b751cfb8d82edddcbc97fc7f82699132789a4d44e8805de7c277ccc12da4f5dfc1c94a14b51bb919a76c8fb13cdd9540a2ec1b98cbcf83c352bea8962b01e9f85d09cc671846d2e9385a70e264eb38f57fda9346bf4a1258fb33bc488e5a124526071ed1d7d043412c206a86fca53b42e3419b64ecb098fdd5ba50e062345d9301a135d07922632d71940f4bc90c9f59d87bc4d3bcd762c7bb01bb5b892b272a96ddb7dfe8ba7a710411914e6165f7987a8d64c57c12ac5f48b0795808de989c851fc00efde3998f8d2572028faa9f494eef63c40586288b1960617327088ce257706b178f21d70d544053c471b0e737324324123a8777d01d183ffc4087c71bf9e15665125dfbaab3dab6773070b4ce0a5b53bd0fcb0dbb0830af667c16ff8f725e8bb9e648cec5ab74e4b1b33a6d7c17a779462308838f21db9914407e5b3b0446dfe6709d685060266093a1cf355ca25a3fde596594a7d76ace2d0ec33152b460045c402a9f5a8eb84761c32638b73a6d039c871a8bd78c2092bd1a55e999fc4db50bb519f32c594cd72f09253ced04ad08f3b04742a3c94eff5ce3d7cb2c8df4451f264b854a4104069b49a295313473ef50b4eb00ebbb89fa9b5670d8707feaeb676f4ffc2ad63f3bc33b4b3394c58a1626eac75bcab17327d53dc919db90f82d969a6884772ee163c61f45525292de2184d2301bdc152df1311286358e158555843de67adfea0d8c910a20ea64a146a15728dfa832eeebd1b74cf7e70a1b0068cb72289fa19d06d810a403bbf7c9b2db371c0a208f701891f7321247d89118649385ab51782a86bc8d56609b514f91be45d0db53601db1a0a12bf89a85e948085f31b8c37df54f75e4e8de8dd8a4cfd0f7b28c90102e337be77fb9f5f3a0a92806a1ad6e3ccd15c46b5c631b95f76062e795c5a6b9fc38b28178473fa303ba1c93cad3abe80d616ae8c5a4ddf422e8693019d4e34c449e1da46d78fe8f458d282bce84229ca68d857d4504764ceca5647d7ef7fea55e2bd8e82500f3caa3eaeeec5cc5aca905f13ee60ed188e109ed757d8b0863c774796aa634f161a4e5e52c1203a53e1fbe34535e60f4c0ca70c269d2948d3d38affede15cbec72c2dde30c9b5009c25929844f7d20e7389e429d92f0332c62ad219da254107b27c6380d8f5d4a23e5c5d6001129646d6cd6be8aeec0435469cbe569be87dd944adccc3cf35e1cefca74b2d953976a863a56aab0129ec3a9196d12fcbe7626218e455cd1183034c5b87eeb37e2fba937ab37c36ae1d6c59124563611201477ca3c9478d53a9f50b3501d84f59666ab8b40230f42638037be5a52f486f3d000bde24173f877b1cd7f6421e1923f21254fa6d6de8ffdad421c71afa1ef12c2c6921ac190157638ff7dee0a1ceadee3af3aeb1bf9931599c2e80abf96a6a9f93223dcaabf2e1239a3d2250f8e69edc352f51297439f2e64cdb38dafcef1a5674bcff13d15338d7aeae3316e6080ea120add2e6aae3f81cf860678cacfff2bd15aea8a0a17c1e79080ece003aa5d7bdc435bb69202e63117ca486c46af613e71e85241ff4d3dc2e3259a8906e08c2dee61e671cfafb61e18d0edf9781d6e6b8210903adc4480ff05304549a5966781865a6a3dedc4d6476e14151f13dfc61169ad604b64526d2717e40e3569864895bd981b80f653f80f9611e453861947cd20550ac9d89a47d69fa7efa681aaf52da40a74c71f7b73e19ce15812cb2d98bbbbc3179993e4fe0f98de5495b49aa88e960d4d2c6e3c85e768aa8243d78a13b6cd151c690eb96f164314cc1fa769b8feb7d96ecf08cb03ccc8214b1d242c118c500a2df3ca30a333ca6cd298c6b164628bf357289ee75aa41bcbcdd02ad010ddd55a41ad74b7c97488b621a9995a6f3700a950874f2d92d80840a8140399fe3d9ea73cbd831987aca3529a4b0f64b4ce929147fd0602209dcd6e748a7e00f7277f48837cc7817268d7a94069327a1be8770371c56924ac4bc81a099d853acca5f99206a02c178623356c1a537907852b9c6cc22d34da1e5477fa3abb77469ce3674c0a7e6bfa68345b74de10838ed007aef04b54f95b5115cd89cf6426977b585565f61ccd16308be1c110f745c8143b957818cc9214d9e8088491305efb77e6ec42924330f74e22883f1e1bc512a4867a04c2900e4053c9ecea47b46a9396bbf15ce566481987260391bdda4bcf02b28a48723823035c2aff5f80311cba83d31021f520f21a3fb6fa8a5bdf211e1b82c0d93903455133a743dd5a9cf5a74d05034504896d5e237417ab0f7e6f2dd1751a87c202cdff85bf87fc822475bd68ccc204d748e84e123f5be181aa69af70da2e3b8245dddd93be0aec60f8532a1f870fc97312095c02adac1096b355bd9f52654b351592a20cfa68fe1bbf9666c4350e29184affe441a0e1cd72e02900ee4d0d12e32afc6e711c2708bceb4bd9270b7d83b30e1e17306c29a1dac89e71750a10033522632bffe77d0f58fbe2c73fecdf5bdacef1a333a1b36b92ce17b027f81e41b03a010579494c110a0dd8b074679b61f26aa514a01684e9dc6889f379f2d6d114902f3bc3c157d9a3c7d4ac86d5ef2e91e501e766bdea94261686ea098700f6a3ee9fe6299ebdd6927e29f8d711ca6c467deb4e924596cc5a36c42c64d51b27bb5929dbf67a433148f104d0767e3443f98c8737efe5a18bb8b13689c1d84163808aa5910deb7611a6ff3f22d14f666f3a98a6d9faa56c2d559271bf81f43f41e4e53fa1a94dcb28f645f6d6756bc41feb7d854c54dfe0344c053e69a08e9455c5f08dbf854c8705648364e125c9f70215ba94e503d4c8aaa161db2ad7df7440498652692c7560f0b6d05bf6f5b0f165c03258f011304f960f79334faad07af2e52cfa2179a62497cf7212bd987fd2339e47cb876d5316a62c936321435a1a46b2a006eeac374510a56777cec57e917d62418a505332c565d07636fa7826539c0bb934b2ad209faf366f917f2f24b03bfe7c418ebab3b697dc16f3a166dbf5a96222cf766156da9244fb76a00ebc131e32e8ca6179d34bf51a6706be92566c2c10c766640b0ee48f60db35ff96e1b696a98a3868315771c861d7f9899da5a07f3b2b3b2492a9aa32cac0db17b2ec4c59bdf568185119e6302dad42c99eb2c8f000ca45453e0a74f34d6069b06ff13020f6d11392cda20c4b8cb14a670685c3375e7598f75c3051de3ae6c284ec212d20ed4cf0d734f4e5b612054c7edac2f604589450f16c55bba2a302306e862d123a9de00ada7a805337cb32b1d1311368fbc929c396ace74f213dd0345f8fa8297a59b2ff3c8f768b4803be45f7355d03901ad3d1381a991665826e8c631e0e642a8aa0c2d8b46a088d0ba5407588529085e9914af212776f81038cae6e7e871e0f257e4ecfbb2715881d59b68f186834840565015715117726688ab50ac2097f6b01231fd49cf5b2d4c8c20a489178b3823305a4b2187ba44cefecdea2adf65822e7362b7f60dff2cbaaae3ced7b34947604828bca144e440276ca78837d6682a6d682644acc844cf701459a261fb7115e01fb44ed395e7e71e7e30de8d9fd437ff17e78c0529ea9c3d0004b682ec3a6bc34c478d4f516f7763e06eaa109f3fbc4da33d27c8c7174c52e596f7e6394e208b614973ddd7560c6a7b1877438aa410a262f297e64ec26bb64a04ac9c8d98fcdf58a1c18e6ff26192e22bdb50240be545cfa447df1294ce62c020d7344d3fcc24f683207b2d40cb3e4dabbc9dd36f052b87d8732072b8a5a40b59ccb6280e3744e3bc95c0379878d99e629497240d863666d9b43ea41204f85ec8b342e1d53753e60531155e2b080bebfbaabacd972189ba4b73254fef657a29ca307aacdefd4e8102b61eb9d7089a49a31d03499ab00a1a60f1b479ad4168e02295c32ca70bc05b93a51184b4b2f5038369b86704a0a57a266258b6b05e25ae537020b5b07f1c129b4c2cd03c94fed97bd145797778cf4d70f4486737d9a9883d7608d6941666c8fc41315dc6b8e549637ee3d8d171a6e7012371c57b6b4aaec60740bd41931c03c6622f96647b031f7893a0fd9e4c1ae4473bfd5e52c43ed84a28e0eed03ac084dae07cee96c4da50eca68b5b51779ac6380ddaa9fc3ea8527a0fabddff2ef0331763eb5be335c59562fe4b0931c81843e31386ef709b8276ad0f27a965cd389745f8d76ee4efea56f644afab6b2493caf4f60df97b3eb22ce569c75c9354e3af813fc482a980b367ff00698fa8e16c606c60b88ecacfcf39b547b8715237da33b356ae0762b0b3f28c9b1946af229010160f2bd608679490c666efd5c9f23a7e83d4f9070a487142d434dfad45bc98c915bcd58cd3e43ba44ef77767fd0286c2b943f85c62131622aa70b319c6b50764494c7d25292b5cf6f35189d8e6af8c6b68f1e5767b0729933a8787ae07d394623508b2207a002165388370f0071e371683a279895041340b1c242e807c763f93c0d10fdda9b661b5f8566f79e173ce605f747c208e670462bc13cf7b204932a107abc600f7dcc5971c3401c2b832679dbce0c17d4d2047d44fd830c813e72b607ca793f2349deda2eb2c62b596b499baa6cef7fe3789082cd5d69ef70db66b29ee138b9110e8aab5737735be9f09bf2c781ef47c37615d68f30eca8b4c52b6d70d8fb14dd068c0f3d0fdb3239bc225f5059681fccbb5d219e847e431af72fcac0d3811c33721296cb03660adedac36f9cc8a599c740d4c030ed8edb96372fff776d2a7f5cb0047076fc98d90a854d8174b8678991c4e6eaf01c1f4f046d73cf73d405a076b06765bfe3ae4e3262c93671d4ffa27a7c6056c096545f33f5222806574087ad274bcf3b64fd70ef370e4137284ae16d16cde802b238ae1fae0cab037c2d1e2fafff1375f092c7c8c35989dc4f883120fda976dabc6eabe8ff4cbc61e0bb6b68f655a8d5b862a600521a76fda481fed05c9a5e9dc907df953f89a580040e4fbd810100f75063fc46717295dc6eefbeb3a5d80082f0a7f8fe00655d4dcccf68137648c88e3d32fa722950c97110badcedbb67886dc0476cd535c5c9343e9f500a4ed2f2ac7e11520013b48dca4a53e5d61142290f9336771fdd0c1b83fce3971911a292cb84c2f4adb24f26a3fecb4aa5244f8e305dbd2931933760dbc967a19cfccb4cef5d60e5ca2895c2f510520686c1b23320d8b5223ec14ed37259b2e3e69f83c7a18dbbff71280c7761ad2c1215343a7f015a7606a7001abb8622e58ee6a7a2ff754ee95f41f96819e9a8450fa43056c656642e25551031c9c88330b2f6138632ba8c76895c11f5438e44adacfaa1b175216ed014abf9c85e5032fec9684f37fb57e3265a138ca5360a5973b18356f6ba0ed8784e4c0340d701004a68cc0032bdaff6aaaa70507332892de54ed864e588458768cc294ff1c3d27ec4a19c71468ca77479d9120d565a4f9be9bbe1c27c404208df6f886649d5d92ca305b64a639f24e57619d8f099a1c52468da9644ba60cf9955db52f0b8b3ae07da438be020f8077a294f58cbf68ca2d2adfa0548f883e974ae95bf6a284782f716c4589393a2e574c96a8e822372c8edc1dbab23fd01fcf4347932b0db84c5b6e2e056530058781d56ff6050842b78b453aef0ec0fb7e9da7f1fbe701b014a079dc11c0ca823ac1f68e624dbb1e9f74fb12b9e95653ddfc97210c9816477efedf1e7011fd880bf1acdc90e5f8794a8856c8fc0341f6ed604d23ba4b211abefdbebd01a2c247c71081a2c34c3f38696b9caa1b33b7379a4ae4189d81385ee057191eb3d97793f000040ffbf226cd74b71af3f73077a8fba4dee48a5a8b6f972f86370b42e0b0fc4606a5b6e53e7b20dac28d44ccaa2b1cc25a8ba81e703583eea0d0ac072ec621a3d3b887eb8ef4adb1c832827691b2831497084caa562e503007974d789f9e5aa63a0e5eb63a6db1d57af3be14267ac544d4a50d7a122e3537b4813cf2ea02462339ee547fc01b7c01f8e0cfd78dd62d73ed6dd3869711391378c167509763fc726de86b14f40c360ca5779e561756ceee0dca982c23d2fd6e7d275d5b9c73648ae4796231b5dd4e108ac9452e74b312b737be105198e33a2df4940d31250221672c1c78b90a1e527da3506af20267b78cfdd8b91e1ad97de7dd60465d8f7dd60ec7943e44fe170ee02a4a6b02099c98fe28559bcb14f38259e4b6f9588f81ddc042accd5f210bd6726459632a26aa6bfe0516672927e258bd0cb11e3706816715856fb0778aa96fca115ff131a73a51d291ac45ff745d243eee06f99a3aad6677416976f71c4dadc400db35ad7d5a271a70fbec278cbd41605ac22eba710e8ee3be4c9b0b6a4c398b973553b6879505ab8d76db22a5be0d9d36f70e5d65e9a4e8931cc758517ba32ff4655463a5b5314152006d173e6a81f04f835201d4d1f5bcc16ecc0f6edfd076b41978b33a05f5e004eb9efc9e9e1255c26061fdedd02118f40d5cddef9763e15b0f5b773af4cadca9fc5313ed7ca981ea3dc5976108c85adc81773262da406fd7cb7215a4e20b5b9a09e80c4a163740df63113ac22d99ea43667282f4421118bb5cbad9748568d3f9d1c21261aaa3cb59003cafc11cb786a74c648f8e4ae9c8cde811e82facaccfd4bdf36ce50a66228123adc28e00e6204445c3d928f794a97e6aa7f406f0a47582481853ef6ce0123111c6960960e4edb80f53b0a5f2104d23540191f083a7b7ebc86b05c060b460a9e17ad8fc27c57b92982912ce089aaccc4b467d73773fefe87930d232cb590bea7c9bd0737e238f263e7ad42ef48ecc794a2cbcca8e6ef5252d15708c01261efa305a8ca979b3e1e4defe64e9252d7080550916e8e72a51323f5e9b8c3e20bf71432f5615809ada92e7dc3ab142b8a06d5919deb1f4856272760f5b7396ef9930353c1dc3a9308232a1ddd03d251f7a016bb9fa57a50b98c1f2982608026666b937a17a329a4eb0a4e1f24763872c24f55d3c04bbd3b5f80146c2eda65de170d8b51a645d203962df0f79df2f388ec3eaf2019f7b95c0ed1103876881a94a4e9b323c4fe1f0caacddd56905f2ba3371d12e1f23e2812a1ef0db0dda2414962a63828d93b322cf04b29da40b38162fd065f10a17689f65d30ce1462395124eb5bc020c5f4725eb938aacd26ce771b83815a76f68aeb7e1b1d946b7b48e381c580a5720ff73a3f47b361e28c74e17d4eaf6f770ec7da8001c2b09ec99f8d21e87f5d631af5298cbd1575d57ad6bc13ac69e956d19aa41aca8502a07e4f869e305d7816dab9b3833cdce5c2b319120cf35e1e10948ef1048681e921e7ef959f99d84607da4e4daf49ae72944238e1d9118aa49937b83fa7bfe9c82d5be9699233c4434f2f28adb5440a749451c83c37f864c680002a722b07583bc20881ce339d6077926cc679743aaf048a7a837f5a9f8489771237be8dcf37c23947eb017aa13d42003450ed6fb25b713dc2e80859dec84c172ddc6fab980b016e3f775ad6864c0c14e15e42995cb47172224be14579ebca8ea1792f5fdbe73ec10273179620f9aa41146238ba101ff93f50aaad18990125b65b3db8cfdff76ab7d908d5257bc1541913c38e1d0606dbeca5566afbc8dab6a6050f8f51125aab1056d6a564da66595493a90c51dd2265574ad658a5a08706dbded6c435b47a9ca29c0edcce717a81d4d69eca9970a8eb9d7fccbabaf9d3b60ba4c26c612d0a5a3473aa05c55897da72b88e649bf545b0953d27b3f10cae1b29143a4b99b54777b96764a8c5d718f99830500824eb82809f2f41818df25332f7e0927d782f39f89efa02bca2cd80c978fde57458a17ee57ad8cd41028f1be11759bd3f0f2d431f90a2d136f09536296424862652bce090b21e9c3c6a197cf0860b857eaf42f2b773159626c0d9a10b19bc1f6550a0f4192465673ac1a8361a79abd336af43081dbf5dff2316564d5774466d978cd06dbf84674ce888dfd984f4e7c11b3d7d27a957e36bc98a09aabf1b45044516dce7a76a3802a7e3cacdac1ad5fbdd10fdd2d1563d361340b3ad9342ba274b58e6faaad8994be3522aedc803bc2cba4e11966b36b8995be45001c53c04bb73c2c337ccc00ece4e42537ee4bbccb60026fc6695414ad31ad3a11b0942f56f6fab9e89820ea952c1330c23142a8f80351972db58d198fe61a96707171fea45d6be36dc09a418b06c15b70b599739bc835a36b128d51a318c2aee961e17ec2824cd84e6362f4d0ce93b2080655721619529361437947af87297c97e9779758ace745dda01b82171f9be9897fc31e0097d85b872d1b7e63f1247001d5ee50e7221d211f9aea614ac70d0877e73d9c4b8156054fbe37d1bbd70e179a5fe569810bbcc22f2d9a60e09096dd2a8f529d0020418b5c03df009e424ebd8640361109147c8e2135841078f5fd9e08a8d1805355edb59f34b4980659afafb3e3976f108572b3819697096b3fa74256edab82286c23f2f170708b2fe2e7b6bff68cab3b742e8674e22ae34d4fd793d0b3af84eaee1dc06574f587684cf8d8a34ac364e7a726cc6a7bd549c88db227de8208c00eb31ce4f849c928ea044e665af7e11bd2320235cf1703a151250b91c89b4e2f3ebec5aa13fa58366101a66f80a522f39f64eafd61202cce6734019dacad776c6ccf86afb3669d0674c0cbcd782b189b655ece6ae44efa07a4ef89d2d8afc5cfe07bf3df4f0d93d7d1089fc6366aa300ad5accf851be927c95c5e41cf4155cb8bc2877433c729b954ba26567df2b3358529920b9c82b587c7acf0e3c9a14df72b268fde37e4a3ae5b5976e59cd03028aca06f7c466c9736444488f402d793a738dfc9869a25cd339676ffe140b143a0d36c2c9601cd4ef0b13d83f5284ae4a55f077dfd4d272c4a05a9c432997aae79c01cc735a2c14476e5ea57f83a43f45a38e175cbfa58006b184308e2fc1decd1ca5d63088cf74417f49d33794ec5f9a4ee6796e99bae3673f1d682a4be49760c5df6c337fdc417c3a79639310174bd51b1cab44899b4043a8c51d95f499b2697bd085766824e97bfbf2dae6c01ed15d1283f0dec36aafe01c6e981f7e639ed6f46744964b8ccc604a9a5a3dea6262959437df337c70cbaf8bed4e77ccef38b127ca73b536849e9a525aefa4c67a700c4e555a7cfdc0d2a446cbcc4d340672d3fee006f1c4c85315892d9301aceeee9d1792f42987258db35e4ed51dfcb99982002f093310738420bf1d644d1ce6bcd65771516e9c5e190684dba68733af376351d5f78b4dde423928f4259ccdd2046e69636ab06c8a6ffb00556ced7a3a680a81503780211c5c3916c32015bb73d3aad869261831676c90990ae0aa57657657b409bd5b299f4cc9af0b84dbe173879e41aec24e5faa8567149bd7f7902bebbf6fb7f0e6106eb340f60cee6730968d53b738316f917271879b24d8772c5b04c5eee2e7c02b1bc9ab34b8e4c74c030d9da5f41b093307b3db4f49f084eba204808dd9a241be1972873cab2877c7cc6f0a0eb4212a340b8b98ad541424373ec1b33a4a44ebb48689ec8025aefc319e01baa2e7abf6a1df00c8f30748d242764dd9a4077374ce8993caf617aef5cab6dc991a811023a7912c9557a69d45403026876187280b5340ced1062b16d81d0a8656d2fe307ece56921e0e45d404bb7ce3c3e3e351fbc13001e95453e272045b65318c7708aec2d0ae732e6cd6fba9937ae499239569d9268d67e2a5e08c00c4201ec5b8183aa90dc4f0a225c1e93fc5ed626dddc336f320877fa9ebd3bcea27658fac2491e5478ff07c4b266d51ddbed03357534fd3b2dab1ca137114970b430d24ed7f5e6784797bc1671a66a90c1ad8a7e9305629b98f7464c1c1e7e335c3403fca149a09e7dfa323d9fabab9d87b6ab695218684223df4eb1f759654619bae050c167a1894cf315332de1aca5e1f41659f7c72760444b90de37256228ac7c1aee22ffae005ed27ce06a4b413163cb54029ac7e6922965239d28633a02e433807dd44d29bd920d78481a70dab8846ef670b9d1f20e1ff9609ceb0b610115e6d33c1312486a79cb856b7187a824a29e543b10be989b6ba500f34a96976a36847c265de62204192384a4aa68ef98477126b8fa868015376afc486208b3beab6fccca1108a8010797757c96c358bd1c31cb4e2477f4a32133a04e95f4bd6c42c2fab9125912a4ecc3160c9eeead7c301041609a933521257b19c2ff81852d72bb2062a30b097d8d8f3feb99e27fce49d1811d0995eb52dd02f597200091d9dc2f61666c3b775a41b2a9028824707eaee15ee4d128e9a8e094341fbe73fc43281105cb106ac2388b1aebf99ebb5f55e5a79b2baaf8e41e8115d6dd9ddd39691eb6c87687d0550af0678c9049d919116d1639b2c956c7453e8072584be9b391a0bb3dca39953f5535a82781e8d6f26cc6a7a77f25d732ebc22dbe37161a16b999165e7ee1284e5786d1b7c9f96273c5d07c851ecb40d42e3ed79b9a63c948143fd39355ff49092f589ea1ac51ccb578929be55d49413b726d80d6e4a54cd369661084dac80e1597c88120ae6506b0f05f90d5c3fa6147e6bbaf58d283e143728560cb6416dd5373e75f8809a6edbf04d1a24121ad85f5b49e62c8fa351cbaea8c062e9b867dade38a7501b7222eb49e9f10a202f01d0bc3652da4bb9412ed15eeb1bd756f28c6dbb016184531cb513c48b5b23740659460adb6a70f46146004fdc414b7c7da71a48cb1434f3f4a4e9cdb01c7130a0bab20d02361ba2a4b251d142b69120a100eda44c941916634cd7f02f360561b84383c233d5d0690e5f472dcaf565fc594bab3f4fe6f7af839fcd1c83874d15166bcf9a9ee8a7393d35d6181597372a5049283eede2c7ea79ed3955f1d142dc98b5bb39a89b386ef40f93cfdd15140dd0243f546e5089b33011fada08820f8495cf505ab2e7fcac3c772b0b6e2b7a7581a6528e4d16f833d53e96c3c99395f639474ff5113e2f15884e31b017d2ebfe901d81487b9235448a3e252e84e3a8878347f93472f910fa99eb4dba231604e591f9a628f7f949da7c68d2b92bbe95a9d4b0b460a4ba2ca166df37dbcfec671b0c57fc1a5e43f0f05eb3d7fac36ec3d68f6e30e493d6540d262492a109dac1870fa61fc4595059660a83c67c3c4aa51d90f03c8772d3429ed1ee1450f297f879fa51e7e09af2a5b637f98d52f18b45796c7b27edc710c1e4c3dd6d57088232f656b1b319caef6f0745a21913fec7a28a20af5519e29b232fff286af2bf6159738bd69505dd1467e8f4f0aeec3f417619468862180325fdb6ca68d91848701749063285ba0751a35b4138ca9feb18b0d102317e4651ee77ec755ea4bb4cdb285394ed4ba641bde932a1797dc364caaec91aa453ba04b9aef3374c6b934156b0a57fb1ff65a83b27162fc4e88e1bfb3253739a3821f62a833e3a5f48dbbd347303a1e5e632992d5a0cf4fbc13049f0abc0d2ba16c5eb8b97e9c66bedc8f69e71f1226461c587a981a30c24358bc92637ce91ae3b456788236cb4937d570e63190d5812165686ee0fac9e94707a16c31571512e80b7c3d027d5c0151ed985b1bc81181e5d8075d680f79129e926568ed50b66b7a22306896ee3ca91895f11c2d3b65d15e5c130e01a2f166d4689c8e154493df572e8737d56659da062b6db526e243eb4bb18c51fdd89a813890424ee151ef85dcfdb2bc3c831c53bf797e2c8bffbacbedcf8f727dc25570da41a70f9b4058da3eb2708f0ced2fd85a17cbbe19e85a6f73008b8f310a271bba66ce156873d6abcff41d8be162c415322c0b24fcd6fe4ef39c5ea2cb9cd3ae63ad17507b07e7a888f9a43b456ccce988542258a785f7045eebabc156f49f6b6e3fed35120bff3f0e5432086262fe4e99f1513fde63191a8a45fca775ed7bc3cb331946d5304b92abfd8293b99ea5cfce6b2013781c3f9d2ba8ab612cb81c7bce27399b6026f30951e9988926cc314662f56bbb152da206b301167ed8e3b9452d5fbdcca0a2eb46f82b32dd9f334ee519180586daf37578912f2d4e004b490a0b9048d30f4bcf2197e9ae71fbb75e7905582a8f2144ffafed9ba129f0640feb208c8f927dace876b67c08d7cdbd346d8bc339dd68cfba092f0cf5e440bc24d554fd606c0645d0c5f98f733b68f4ccac8579c3855ed70186e5d969b1bc11e7817fd348f86b2fb8b1ba03e884996de51b161b6987a554ddee9953c6d459e375f8340c0fe81414205946c4988a70bdfb9ac30c674a47dd6a6a1b87184eccc25a11fdee13db2d92edd9820a0c7cc957efaa0889a4ee8effdd117829994562977f3d2d1c07e938255ee45f195c4b6973b756dd6d8e998f0b40d81f7dc634221b3f6c61b415292c73cd9668a4fa809f21ec43aa8178708c1b2e33fffef8bb003cafe4117c0d1e3033c9763a417ee0175334552747f44f823ec60e0471befaa2cbbdb364abe75526b60d2fd9e13a5b02c8378844cfdb806baf9806a3086d0c17fff7edecf60a28547084ba2fcbf3aeebba530e2737f6649c165a575765c226604862f6e65d222e6c14e713828658624552d489eb405043e5c11243510350ba539fb34d37955e66c045a0cc0c9bc5922251594798331905fc7094e29ac481b3d1ab0ea0f1633ed99df8d9907b014fb8b8d0caee59f82ae06887a6bcb8030c9d356428be526ce3e229479893240eb94450848b37f9b366d57427c3bc07d9b8bc689b981f9803fb5ba95aac89b15e872fe3f229cbccb3eb86fee3b49436932927e44f4b2565960273142e58b50d6f99847094d81d737ee05727dabeec954a80415e0f4275e0e5aa2ab883abddc07d540627bf882331b2819660c067c63045be355fb59f9efc4b2648627e34345ac0c254ceaa034c70179c9a99407027ff255d855a1d3dc87067464b50ae312c14e71b6cc6ab151864959493921e5f7a2d12757b917c317bc99853ad1e5965a7256700da1abf08eef4cf205ced5e9e76f3d640f28bd5190a64c849052f046192fd73f545115b74d7a178aa960012b06a5c6c5c7be91b8ed375efe477cd6fa5a57d02ff48d5ca5146c0130dca222b2bce9062db488edb664933c72a1d49b60222ef49460106039518d4d24e59134969f5a9fcabfc6c37fbb8e8ecf8aaf25d5808bc76e9f57aed0547422e544a1248b79033872cb1a11ee17f546f327666687fac62ac0bf740e72a81edd7fc543f35a7d54cde68a9dc23e008750df4c203271260e1a645cd6a7702322bbd9e448ac9977d3064de91712b9986dfcf399b7fa3d290787cbad92a9ed643b0822aa140e2c349387c1d6a9564d459d63839f4ec46c3bc675a38f8effdce1c57856ae992d71556efa9c21b2c319a87d923041038f7c14220cd92f68b1a6ae02726d7a3998d8befaf6b0496e8d5446940703c482d6f5f5bb27f536a00973106ec14ae2be11f03b8a77b869b21f4b876b5fe0c65a0ff26ccc8c6c2306a7a0e734920904c3b4faebf905f67a31137d4b8d2c5e5f8af07ff42511630af876dce4321aa5ebf0b2a998f2d24a1851ec68527eb620fa6638f46d4393e6973cd1627416c5c5b26bf34ab720d15b0799b6ecf30df9904d4e31282289f063177e99963bc8bf57c6bbee06722ea23eb6fdbbd3148355a9f01ba6712e8550d964aae822e39b0b0966cab4d691017828353906d6735b900162c2609cda9a2cf806b9ab9e70da6a00e3e84ae35b342ca3c8b489c0e4c8540a3a96ef7280cbda68f441a4a63baa7ee05c2c6239435702372a61f9ee90b3ca2ea13df42cb9f1fd4539f8e6cd84b34335ed0d8ad769ff3a469410116294b38cbecaed0ff0a32f6e1c280550ef02a2dc93daf95a39ea5f0b88938c0f8e2afff73827d543a19c5cb36edce0d17b8678f944d2fb679bef5660e92b3a7d0d78869041baf27855902e1199710b670856cb95c237e5f52ea8ad5591d45d1bf594c692e7fb5be01cccd1971398c5fd208b3511070a7ddb41b7081898bffba31d993edce673a837f67e33c5e1e10b2ff3bb7212e999fb27853644e29cd6da6e4f77768f0e582c25d166ba02c1e02a6e75798962b6801fa6abca418cee0016bb6a0cbeb5bba0b8df8356324f332dc358ed44e1ecd5bb6c0430f1f4595f97e4bae2b60c7f9806b9b0ac168058bbcb9699d3a8766b699ffcbdb26e0ae593fd267c1c5c7f4e5cc5023467fad258e701f19500f8fb772793591af92bdf6b91138d63d682a1a4f615a4a3c4bb0eab33722caa4b170dc86330d06f2bff23197f89310bb7ea47087bb159f03acfbff9b053f2cf00f0749aca8534a2b879aa4dea4e7749865cfc840380b435f6e1076cfc160f18b65b9d00e7f898e968f56ce70c620a1b7715edbf1ef58395e3f6a3cb449d8a17987052228e4b0835275141c071c3294b9a7dda2e165e92a1020ee8937e6401963a694115fb10fb26d5066faa02615099d92f398ff1bdbcc6211eb0da83623830672072de459a261e33064556734c13eb6946b09324a5d5e63a53d43e5417a3cdc3f11ff91a14de50ff55b017bff28adaf23cfc68c244ac9e55d6551c595e9cb907382dff2e341753e4200f997b0695158109234b5dfdc29173cdf2cdef90190720c99a14419991e2fbc7a3e9b205efd32bf6999a0a0367fc795f3b5c140b40b1af6ea8e9b179880eede62a190a3717a2f7665234d3f621d228d43b9b37e1fc9fd6f4eee6e4a2050ac41c851ec2d05d92b74fddd1480f664c330cf54e84b4c6468bbba59baf922f78cc146fa94e059c7c8c944df2956872d5c876f03a1be125012ec773069750b30a6fc8a86b22ae0710a77a283adac97cdab9ae7abf7471a12f720a272f526b3892447a9141845ccd3717cc24616ece47b1543fe83910686be4d30840f39bb30ba6ae4f5fb4f2e48f97057e47d411b494f607c33c6c9df2aa916543bab78b91db98cfbab4f8a6b65f48a45b0499ff270a00cc96e0335126a1f421370917f5ae30a0e88f7e111d8eca7c81a258735606f2e42c51d020869c68dbda0c44988f77d104f54f99fa3216c0f4a7cc34153814d140b936f79c55c690ea0e90220418b94b76aad0838ddf764bae8d624c3167761d653b68414772f23ddd9676e549e7f516864d9057c8ac2950c6001cfe2dd4b366a2363fdb0021c53173d451abaa361a64eb257e1d0d879d1307800792c6a96e686e7a3826586480b9327b86f068e3ed2866f08181f765383a90de5abd28b4fe979223b45af18942cd7b63ff8235f114fc65fa5c59aaa59dc69a0684cf5175f5a8cea269057dfd9efd1cc47057ccbfc172356e26cb3b3f84f201fdebb12459edaba6503041537f46efd0f34693acaae7d4eeda8a4b5333992d2fc46c5568a34f8d2da76abd40539f2f91fa977ac55dec81f4cf4b2056f3b779dd9756418963cbd8249eb37d739f6e50404f92330903a7e366912f981bbb119069a4a9d236c808670465ebaa22b6fce03c5c4a38d03b8e35d6421653512401ea488d76ce7a5dd01b134e4b3547e1fe86dbaec962962ec23320f962ecc2f8e2bb6ce69120007db7c4e70c01454efe874fd9c4a66b5ea826cfe71a48aa1bd264828b21180b44a1142386e8e7374d3cf8289bd564a4562973721cd3317b9437d8792de9d91c057852e65487a9a1cae9365533e53a8404ffbcaa1229a6e4569fbc577e9a2dfeaded7bf295a39d33ba5c75a6feb9d3b1693160d7484ef3917b73d2fb7312ec6379eef435c3b5feb2edb994662b03d19bca2de70822f07b41e07403538109a4484e52725e62aced104844087d925d8579d37dac67a302c094bf942ea5b69d91a0ed502938a40b75566129f3d254a59b328ab72bbd3be32ca56e1b43236d04c79aeb7e444720610e17f4e7625b1ace77c781130b7f934d5e620198310014d96fb0a6a1ec1a56f5a7c56637168a0d187969fb92673658d70b4e10bb2ed9f36c443cf8bbe1baf1d923494c0fd46496213eb03ce4739673a79b8daee3afdd67d026c2e7554d8cc33693158205459692f2880c978b14cebace880446fe9e30237648bc5a8a4524c283e291541e00f4cd703e6b62d260f8eb4e56a85e38f1fbb8d2518717df20e86de2e034baf4c2719c7e007e59b12bd0aaa416e95060016d3195d14e910e2e5225cfcc71c91940ccb491ca4c66b8ece6305f9c741de779040d26ae5f680bcab7124a8ada881c279898cf88e84a5b5c82dcb6cbef4eaa0261ef92bf6a8b05093616884bde92dc3389328c2cc505d38bf3f8aa9e5a7463bcbe06fde0bf5718f1fbcd452d3dc5ff24926b5e7b56445bb4fabd487b7aaa9d3b9717d21952992c4ab384be045b8cc58ade26c0ee331c51ed119276b426f8fdf848485a039c3f88e1a69d5e935b758eb673aa7b3ea6805a0667f2ed804f58367bb0adb78a954efa531a60236f6a920634971ae94540728db7a6621c492dd541b9cd3d09cfcc538fe38279319dd3b2efe2008c19084839b1ede6bc42b8b88e28acc49557823b6fbd0caba45b4f455eec01308cefe96ca0eef2e3f0d8d6b65ff42b8da8586567378505afd5ebc4583f07e71ead02bdf9b1b606e714c3321fdc71eca3b527badd5246aeada67902dd6ee2bad07204d29c8da6361364942c87ce8aff13fedadf07f0e3f6d7fc584206cc558ae18dcb00672ef22d7104315b69acf82eae1a7370e9a2de411ed7307e373ec74c699a7eb1d59ed36e111047c7388b330ea25591aed40982288f55fc2e8e101c6b4dd56130ffd84a40bf5c2ca8ca27e1718ab8b6b49d71d963d4b086006562a7b1ae9e5337721611f2cf5e77ed4302b8be2ac88cb6e9f4934b9a29ab7c01b5593534df844b4aa04b44f59e225667b369c9bb8db1b152cfcdf5bf21e122fa5060ffa684a65da4f6c5559cd1c0d28db364b83c4c1c0643355bac9723ad6f5ff72342a3277b11a84136943fb36ea77685020ccee1d0f5d8f2276d1952d6b6b0af2c6c9c3a16b4bfc41b0b979d87e9751a3e2720e993ab460c27eb06539901e872cee676a9306f119eade660052f5b65ff80b29e9e0bf96b761fa3d5c2f43aa4c3d98f002e913c7b7460ac75c66c01196d5e16571bd9beb5bb9f673d59bb93761d2ef41941ecff5b3ca1a5087ad03ad68941a3111e318d3e3e9560f7860c2e7b2cd435999a920f34f606ff96efc9732c071a54c33c56749bbf70c791944bdf6fe94a2abc330df28e9b1b7c231938677da26001a2d8ce1a53eda00f480448909e4aeef9d0563f79702dc6b040918e1ec66f6f350b551641c418c83e66bcc03a8a1a8a1674638f7d27508cb6c752d0e724f3fa0c15fa3e6c17f3212b52458bdadc0a716abe17ce853e76a01e34ad2c789b9628b645793e40604290d2ddcc1e43c1fc105dc5af8671f451bcd148630fcff9f1734ca74a69ec4a87b8cba96657e748662783687a62360096ac53d63c770f8231ab5204b756b8bb41d7639c530bded1104239668872c2d06474f63a166ca65442241caa3267ac9deb3823ec82d6da3664d4c77e5fd35267f21b3d8098173811a69d3c411a3f9cec25ece9eaf6605883a94a8d8fc5357dac01d4a05a32de66001b48461f3cbb366162df98ca96fe2d9c3922085d08ef0cce5fa2fab81ac2be42cf273d5840b7b11b8af2a41586e8ea874943cfee8390e107715faec01698bd66ba851f5286acd80d1c9b8b2f40cb58b7fb1efca5031e72435a8a6e70d706433fe1217b0f1430190e39422539eea0b32650a0448c9845ec858eb299ac839ced2e45160606ccffc19e979e3846f205e759ac0c01f0002b27fab7784008c7bc45781eb56cadbfb8757c54ef41c4b222a8c54758e3b964f7bea466390efa240272949040e16aab7954fa3478016662d48a2ff47f1ddff1cd05c0193259b44775bbcf66c947a17371b81567b366c37150f33c61450bbb010657c3d98e5b97a8b70ad205ad93383e2c8755d657c7f53753679316a79a13bb7782ded4b363c82857e08387080f6f40e5f20cd5784cf7fda05f3d53364982745bace135f61424a059134dc55d2a206bebdf355f17c829065451b2d0b3fb6c5d048fae49b51a7195a9441e00a00256d14f2422313f007cd0a81d0e1da46d375cac17f60f8ec54a7d5f176b456c561eeba0b365f2c0a0c6a2c7926a86acfad2d3bb8dc9d54201a44411d7af025b6dd3b479cdc59ab47dcd692b92e66321b5017701c720767221f7f8f46edad79a3a64245cd7063b97e9fa303d393abb8f9e5b167281617019bf0c172301431022f30cd3c580ecb3e9ea0c5deb0c2e9b71f68704d150ea878ac868fed69c74c19174297a77046f6fdcc2bf0438348da1bde4a20ca5285cf736be20c3548388f4fc52004788ea91fae8044f32339bffe6cb7591b65d03ad18e71868a47c136b383e2480408b2e2b5cd38490d0ebd6ff51fc02b859c6ea4dd57f4530db0e15761066d8618c6d70b8b61cb0ed55744937e1d5c00143ab57fba58c4c9622ec09d30506000921874164adbd5cf6b6bcb2acf500de67cfe3fc4939074713b11a87914d6783e22d97d696452371ce8c2296cc9117b432f0e797174f59f7fdbad8fc0ff8a62e3587d44fcae42623739624ce117c685393ddec638220d21cd36d2b844e6ca8b184d7dba953b01138e95960d90be536b45373ccef497a4dbe07d48ff38c1ca17b89e8a6f287c35119f540fcec762cc1d7d86090626f73f960152a540f8984759e6a0cf0b1c7de6aac51e34d3e64d576c5327c021e6215dd507428528eb0adec4c4819588af47fff9ba924fa510e6fc0c1d7fec51a68ef08b35ead7a76152b8ac1793f4c4211b01e097fc4515051b6945c1d5ef53821a6e1145b6fe59b396e4d1459a6163750f5cddd9ca6379452f339b91a60166ed2755bf8b8141c764ad7d5c217946126912d205ca7f8bfcacfa188a2793d1f5d838816170cc752724a1304863d255b414193397961a24429340c4e8c5960a3b359f5921fdc786095da9110a40de9e44ede471006e40b02d43f55f3779b6c00711b9ca0ceccd2e36413697d101f8dfc3ab4f427be22a46cace15588ef21906fd917266b4f2c20857113d1025f060852528091e674ebfd5938f8b83ac6b1534108453666fda0540d6d414ed3e27c6c49bc6493f2efcfc6d2b02e9230afb3e29b8d52831cfcb82f2663877feb8189ec407f70638b439ed2b31b8752b1992e62d1c4f9899730f64698c25e74fa866abe0dc57ff8adac4590531f001d7f4dde9321acfeb6f58124291ff4b366de8c5a1f898033860df2ba25c696e5e2cf602777ebc8229be3ca41f32d7cab37fd669389866852fc914b6f6704d67a002617e54f4236c78d36acec0eccdcb5431feddcbda7fb43f7f6151f313bb0ada5046f5607c85c46afbf79924f79d31dd09432812ccc22c456d2bcf46236aa67466b7d7a71923368554fdfe190d3b3860b1baea084cbc19142c6196382b3156f25d2c5cb438e7676fbe2880aba84f6f72810e513f76fa8cae2303aab009077ec21f6f99bbee4c4b7095c5f3b872c97f89fee338c5b6cf9dc6e2a7a7d90ebbc9104d9f4a4f7bc31b13de00f8f4ea5bebd9515df66b104b72b98f9d9a7e2c36b5052721dacba333f0f44b70994e5ebb0d08821f9fcc323a11a0af5f5e5535e078fbdd1325eb5e87fb6ad1c29a8d3c35b1dd10bd7e5d3857bd911cf7af1963afd34a77b634aa8a2d277df169fa582e3285787a1c138f9aceba27ba28c16813b8656b2729cdcabe76569b50e462e22ba6cdc039905f94c2e1beb737c56b684b5d845669847e7daf8cf1482ce4b6ad125cfbdf773a409e20b0f781ae242dc08da6af00b593c9ca22b47afda40d542ca76a2a5b8e49f17a150b9daa31b6f76946445a4e8d100529a44c4be2d1fd1bd96bb969aaa129eec5490ab14163d648cffaf6b4a65d4a481fd3a3bbb2ecbd377bf07c8ec9cabc1670d46640cc85f9354c1c928fa5c5f1332cc5a4324b3ebb1fced2d766af5449e6cac66b4525a5151d2fb0d2a97d98caa5ee0c469a802cfca5d270ee4eab2ab5f366a8c5b786ef486e9a12623ed98b93d50675a3ba3b1b25bd6c7d3ccaf52d971e39e5009c716c985d3512f635f260c8ab58cd4b5268a8e307c9a3db3f3cfc448481b970f71468f89395091f9f7583b7f45081800d2a1b1cafceb8480b9dc111c8a399379c0a3043e89cf08c74880a4ed3afc06694f8faeb8050d85e11c7b7b89c6c0f0e48f2a61d254710764ffe2b459996f2d95d47af28ea9a1f1a1fc2827d373f40cd7af65a4d4ef0ac2b12bebe7908d23f39dc49a95aa288cc7899b8cd807091771eb1d4c1611433597e1ec196a7d3b64ef697c900f210edfb90f3f08851e8fc4c82755c8258f710a07ef35d055f2655e9f40c88cf794c00cdb6d78562a74cb82af2c3e58962aea66c0eec2246d11a737c39f881b770d71ebba4c900286ef3632683fc89fabcdcb2cf773ac68114737ab6dffb038e99e0d6cb7dd6e2974f56a30d6369125676674233a30b42d62be816ef32eeba7b88468bb3f6436e6fff2691c6f8737ef28ba20f24e9404c063a8302a827ba345adf4a9dfd2a5416bbf861beb19d45fc185c0db17a09df8073585e90883451f1abfa602d18f9cb6ed219b11054eb3f6860bae7bac01b1180fc97d5dcf234ba8992edcfed131a9b08e3b8e7d900263730f99eb045586b65db0df4f392a9a2d848a13531922dc2548b5d643bfadf2d5063a245180ae69607b8476a29d0ab1ce5e8ccaf93653c263b4c1c630b20f5c25b3f4ee201682da8b151bc1210087b4b42213773f3a47210ada55a9d00523fa08a12250130f4100792b73f5d6f64adec2b9edf7d3b3139c04547fb3ab1741f88106261431db5358eb905b2fa30d5120642c71a5dfe06c864c9b819f4665422ea7c82939b82b4d4aa51f41c2092052e258374cb1a5c813574a84c8bc6df2e39d71982aea271fb1fb35f5794f818ff1fa5b552f25df36811f4ccca31f7ee713d9f8355fb65a579f0b86a3ef2d28cfa04e1e7c23ab7bb4f09afeaff090f27870cc89cbd8469d9db6d640f6c09c830e733d4932e0be1ac1c0333850a0cade32dd514f11354e76bc7b013d3356870c22f0d45ecf1e8056d0c6ca9174a96f0308100e499c0c89881f921d5323567bb3ccf773dd122481daac69fda804ce97c879a11231341087052c5b25c50322e066bc589058d8ede568d4dce0652abba310b8a8b6d1a7d981be8260bb02089d78ee88493c18284d562400782ab46b1901a3c84fff716fee1ef386186b25e723e811382f97a6366a0f6a6de2bde5b24a876554acf206290b19d55bf0686802e6d228cc16986d14d7d2a4f900aed1c08f47e79aaf7cb6b9befd6a4368d6700da5e14131be78045a3b3b02e13930bbe8a9eabbf8afd96f7760f13f8aa3d5db7fff6a87183485df884311ebb392ee68f133444e777e3ab6f870ef36dabb2a75447274a02ecf08a76bbfbf4f9e88303be759a3629ecdb33c5db04d5a6c23f4260c0430802a132a93aaf2f34a60bc14bdaa2509156254fa8071dbac495bdaba8eaa611ed0e6ec4d010c2ea4817227239983fa1dadd548107d6e3408af2d44a70999d7f7c40d84b4da95675bd4ca4de0036f68b123c8c499c1cdf69e6aa72c324b73ba4095300a0c9320748175d793a4eb5916c31dd5244a24941ebfb83e5027112f88e829ba5202b9dd3771dbb0aa5a6ef02e018ada38224babbf89cb1dab957e6c50464ea86a63c6dfdd3bb477024a936e3ccf711879c6732467c7f3518225ed29a533bc085b10ddf76134372aeab9c1a66a1387c7287003517d05780a2f4b5aed5ea6b0337422a0c43d4d3a760bc7e3a794043b3ac808026a392e5fdec68e68cfcb10361995c3dd503496f1036e3adba2a26ea392cec2aab81c6945280e49a575ad5664dbfb763d185a5cd56646eb53c16987d3d9f3a042dcba8b79c97218b71637937258f3a79e8929790ff489220250bbabe4da83debca15bb24621dfe18d746fde0c19c57182435cf17d69944028007260e698062685bacb4f71388620c37420a0ada6bb9565767c4f58e0e5aee858729647cc6aac08bb1a9efceff7af8f5cbc10f43d26c264720538d6b863c29f83f474e70f167ee8fbc2734e55361d2726521a4854469539c22b7ad1ef335d4d614115af087359bb1da4fe6338a45d696c1e44b781187409d1c141ea499bd273f9f105c020f1653ac6ea5683a855310eb37130d919b5a9c81d4e925e9b46608b80e340f0eb2d83df19a8990b1edc8a0a014a0d16e7ccde4946d56da757cb3545279d81cca95c33e77305d9785a84d587f2d118f9749ca7aa8d4363b802c3a51ba92bbd4e8bb7ea8baa45e7153381f68e9ec347e3207c25b590e485e27f3eb606b5a038921e0a233a444268e430e36d5ff3a349cd9ad1bb20fcdf7807dae9d23f60775b1206256eb8e8d4de1066c2d051709a0d17562b8fbf79e80364365dcfe3c15ded4e2573d258b39be6b2f585049644cd24204d380971ef864157dadd68aaae30b84cbacfe3097a605e3b911e4b2eaab4324412679a633504eb4d6209684e513470e37afde0ca8892a8605f530a26cb8141aa5e0e7c003b390efdc15d4aa1958e3635c74e3c4c578f7f8bcbe6e17b77f5b032af2e3e7f8c819815b16cd89ab5cbc35fdd7ae5e379d0693ab29696025018f568b2bf61316c4b35451432d77b55199956bbe38706bf9d4782e1bb76853e8152da187c0ff59cd9b2646acbe841062a43e252ffaaff19622ad5b1939434f218040e42da22f319d904465146b744838fdf85ab337be6fcdc2b7de2b8956c6ce419b8ce8f0b5a3ad55b5f25c1755ec2f1066c177e0039bc518ff1e525f628eac76f6b3efa3fd9ce8e4dbb6aa684c720603a443671df766f36bcab9892239df46562b3288c07b9e13aec8f65ffb2da74a49f0e103703a8d366c1c7230469a7f1c6b332a0ced96f4d9b4c54d3742fcc9801a68928682f9814e0b7a8cd5b812df11788f43fb215121b452836d5865b8dce404d1f488b975ac0e964d0c9f62060d8ce04d935c6fcd111c782d407c16c6b0d3274628dc74ba098a0c9e978c1ce0a020c7d6374ab231187acee79079ec1adc3279962c8ddf50bb510913266d6655e8adf8ee8ea4f92afa0cdb114d8f376327c9baaa6116b5f977b73ed5c5667f7816ae50e8bad9ccd6239129b276d39f5c94fba0a047d43d4ef1b691614f09714a47ad49c28f7d1b5705ac3e8f1d589797d85c0fa40c87854ff38e1b768c8761b84809a499458f7c75f14ceed17459967cde17f17cfd2eafa8e80c04d99c86488c9018cf8d46b602d7117281433433ae2c2654c05e1be6a5a08cd8e056b36cae7be903802e8c9008ff75428c9a42d0b29a77a4ae160bd4c21480cd9182c8634a069767b8fe245a2a2d7b473d675da328208d76c3337d39507edd65edce3a20db8fb1b33a3e55ed39a735b1b4801c9c3949672b420cab8ad3ff1642364da5160723d3232f97611ce4f6e65ad0a9ea74fb6896a6971e3caf2061f198320dd8479e26b944f145461962c0be395d9d3bf5c817970d19e13278ec268b3ef65ddf302b5971fdb8ea75cd520c1925ce28625c098950576742b6a23ab9d9f5e34f771c13d095c9e4722ddeb5428e31a48e0c4b8fc594d5141b30c9db45bac624fc9f0d1599b116cf32948adbb9c179a6d21a0157f669624982f954b4d6a4b566c5748d1fb7e03210a4719ee589a149ff751f4f166c6cd6de103fcb53ede3c6be0a8aa25c24c3d7cf6ef1a101945496e974cb2abb94d9b20cdf4b68d3f5f84e887fb20fab22b1c1983cbf38d86904c7abb6d495df20f523b08e2bfe55474ac9ea2860a613b3f14dc34d9e43a8af049f6a4adb31593d6c279f141eb95eb29afa1c7b84f77450dabed689e5dfea5f8acdf96b661c722703e493e717fda59bc529596de1705bff6e7ff4db5fda4004a94f698ad4083c9e5c242673fccfa0fd787cba02ceec0a52b5bf7a2ddb3da459e95352459115c1895494f64248894ed317ab0b49aac6651d45e1694ac1031d5c4694061cd1ce779935efaa4b3aa661a32d1eaa7d4b6647996a0e4debdbff072e0d55706293987dcc469f23110d677c5b70fb3520294171bcbbdd0d2f332539c555b6c38b9289dde51456a94529f0136c6ab3feee8ed46e39386e639dd4677ac0266956052508745d5df204d5f23c81bf3cd51e0a153105dc0eaffe6445be535b18cce377899e3087a2f1f051f69174e6d042f2fefa662b0e3269660c709b3be172a5a9bc6f6f7e109563cd414ecc6761f28826deea97373da8f149a53da9ecda5282f80ee17854e0e315162171a6426590c3c5792fb125949178c7f753f6c9815fbfb527d8455b211e2679b69be5cd672109d12c918d09c8bca9c5d29deabc6ac7d53561bd4ea2dd91401720c51cdad3e1e97ecfd442790b8282f9b45c0fa10055caa36a00c4067d35c0b791b4741db87ed43ed0d3281d40730971c94c2940f1552515605bde43cb005205eafcf68057c4435b01c1247f16aac75d1b2b6c0bc2440b43e7a92794f6631bcc2f088a2ab2c9e4674993ce442f202a9a830c9890ee9e5d428e00fea155722d71500834b75f6345f24e2214795b41b43e7ffde22ed9a9ca98de69642095d2c6a10bb9fe7b4abdf17f03de5477af2c1a81fc9384aa73f0ca167d893717c0076395098ad9a92d097122392a67607453a934160120f0a200cca9db099f3623dc51cfbc5d3441bd75bc71cd897909be70e0af490773862d528565e14975a170321d126fd91041544af1631d18d8b1d7f9d3fe2a2272c5d9d8240782d2c3a35d151839f490bb4e8936839098db8b21149df38c860504fa3892828cf8c8cff7f5b726d5a986b95bae4113f1eec457741dd8e8cb712712e192e63f0fbd8b6afac25f612f242bda36eced54bb1b91c53f2f97ff61fca951f468a05f151e921f8ba3b70d366a4013235c99e76b467af25b9a87783b213fa25a75a43f8f4dc22cf388742d98583bf2c205ed9aa846731b1fd682210da32de215fb7f60f2464baba71d46ab3990fe57fe9e8bff6ceec31941632a225cdfec24ba05f90e2468cc8a3fa291a14561c819dbee2f89fbc79ee8248ed85ffd6765257dafc5dad5f05e5d78e482b78a15e30d72f786927d5440213e5e6a3afc52d7766ed9eb500fe601330df25f5fb3ca583f0d13eb0a0c3d8f3534e187d32108ad8c54f59d2d45ffc0832fc8206e0072660f744bb565dd2da49873212428e150b5fef1035b43c04f2d00df2d4296f53bbdb2eaba817be7239d0703464bb01f229dd56eb540b58f7168269b98df9a81d7a10ac385d6711f15c6c453879c2f41f61e3cc009c96c2ecf67a2b9ab02e2adb749707f4bb21c6740771664bced16d810b5c6afde205957ace2315c6759744b50b3a817b812d730ba78530f0b4273c03e6cfaafdafd15c4f9d6ec6db695ffb7614c4c49ad4e91978390fc34d2cdcd151a3fc3abe9656f808fb81b1b05031ab8ae1079e51c582753ca12dd69197f5be7017f5b4beb5a61e68bcb06a7318e58c917c4eb2b000244381fccc422c137f81a9f2d5d1e3cc82c04616c4c43d1b4c033b2710a8d65f0d117f858ae1b7a843ff37542881831c0394a6f3739dd3ef7e433962b398f9f203069c214884dc18cfb48026c0943825e2e214ebdb8f3f6a080d4a500403cb91f81cf2c877976cb6b70299ddc260647869ae5ed1a01807897ed43a192211ccc4a80f2c9b2854e5e7ce864a63711637d314536c6bbfe6a11e50deb3739e001bc96aa34e3e63549acbb54e4ad1c6aad13986a38a2d293770e2427990e07226b906ae17fb4d9ff4ee154377fe5794273b645ef4153e9e748967d14ede1d72abe6d430e707cd413a648537911adef45e16516b2c6bdb7b51ed491a141a0e577ca636da7665fd4667e326af585b3db21eac16155a360be688af26112d229fb1c8b415b7afa3b1e383c6484f4d71c2a0f7573b4afd545261a637b92576118a40c43b84cdd181cff87038019880f0f87613a7791640b9d2d80fdd282adfb59759c1819d3ce9b43525b9e6a57c788da1c0cf588c5fa5aa608edc7db507539ebf3b1ea2eb1b7b8e8e50d2a9284cb10f9f1affda90eb33b1141361ef492659e6d80b4a067d7bc36e6c99596ec7ed1edbcb3bb25fec9fd8c1615eab7b54ea692aacee2ca3d717e408f3c5658eeee5aeac25c00716f0f7100e036b226eafce7fcd38900e44d313ff9e4aefc1f7a2e924533e75892ba12832e561770fda1a3f1437ec054e97eca08cdeac9fc68601cf747d50f287d1677e8d0c1d5d4d8e8df28e8c9d7b839148c60d3ab8aca04e796b1218f4b9b31ee267c269e9d2e91a747d289a2d1e7cd46369942d51c1f5c6d33b48ba7aa0a0d22d38d6b9f6fec3464b91681d65a40f39f3e5a3cd6b634de2df3108376d473496cc0e69f070e599c8adbe8f10718172c7cc84e24a9df3c3343ebc783b1d64036d21f500d42f6018e17fbbc45927cd3b6836d27b67feb711746bdf0ba6ca413c00ba15de9d556e593a5d6bf08138e73ab831ae72c42bf9873a3ef67d581fc3907a7ab34e6d3d99c55f9c1cc79a89b89e703efe17c44606e482de98015ed072cbdacfd99e49efb66dda0f330a4797ea3a0046534ff1fd50326e2e0fa0ab616259149b154a6c8f56f8e3299d7dda1c228c92dfc8b271de2ac60d21c9f3b5e53dd85d63a26229e64debcaf692e44fbab9c6b3bf7fa9ecbcd9124bb5fd1f21c84ea90734bb4098d57448339a8df249e74c6e4b28755df07e67e246fe392e802128f7664d1e589a32cf9ca11087cbb045b85f0d1c9524f899f715c6d002433d6e05d368a7b6e7b90dfff48975678cd4edce6fa17b3a14b6ae2b9b3a9a71fefe81e220adc21b9dc42822247eb9374ef980c77fc4869edda3593577d09dfccfa8b0da07b1e6cd9d6d2e4edaa73792ddedf047aeaacfdec1fd127ddef4ea1d48dbfe2c7c4eb19d56bfe408daa19e05af11fe4a68dd07ce8d24a8eb5094e62c519f3e0b5e6e215bc38509f691f14dbf4d735910eca9554075b8240b9334be27b4122f9178d70fce35985c1b1c4be35a85e30617911d190ab8332c999654edbcb73d68b918a107b68dae581976ecec30e977f3d080a0ca0c168d20468921a3440b9e43993947c9f83863a68ffd15494463e80ac1c06c65ab3440cae3d3a0d2f461866fb6e8c188df3d307cc806ee21f9ad8440fd161075992f3f15d73a0952e95d78a0c7769bd7259df4d557773bcd70c02d2a36c7726374f18eb3f244aef6ff6e6fdacd9b977e56d2147fb4ab8456d4a2cda2a0f9ff220651168c6aa3f9be16ea2cebe75204763b286725fd87ad79c067280895cfbceb225ece30a101ed86233be93bf827e9a266cecd3d6ae19a6de0ac45571dd4635207bc2557acae46cbdfe4b074b01a7f9727bc066dbb2459749b26f4a8857486ca8548a0edebd16ee1f022739633475c340f0be879d38ed21b297365a2c21732cee05c2e34204e4ca9cfa33aebc1811c9aa546bbbba286aa61fb5d3812bbc3b82beade3c09e4d60abefec9160e424b86e4cac87f91c4a91a0265c17fb72bccd01b591cd8558f13fe8fa1504029e50bf04a43ae3601a2f7c0a35ff4f1a502c7b06140ffec0e2d07f1d248b22f4bcc0d99f2239f1c7110c0438c3ba0fd7d4bccf50d7f2bad679c425d7f444095f573d25cb8c129893d2f7dd8f699af0a07c8a49a985c74bec4cb24633b5af13c71b5a8e69d4e087ef72d017d4d424f41e1c13ae97d65372f6cfcf846814428e4c677bde10ae0d66d8251fb56ffa93ac4d3d08345d3f03bd07774f4cdb78324a35ca222fd9ae8c50bbe05fa7c0274ce823bb916010d8e41a51bcd69d076908e7bf29be26221ec8263c8f8db11c56235f3cbf47b28661d78f352ffb56417f208081d8fd4dc50c872ade2d52430e9fd30bc64f68e509c2098ad77696da10a90db26be9e56cc7483e175b1e626d87f1cb9127229917880b208daf33423caf58e786c79788bdf7909530c288d163cb7fa7807386e9a7433d8be5c84167a4a66f5c71736fb498fe68c3f59b3082e130f9d31f13f8e0610979234a385e117b4832002c2776cc82f30374e793c4d1b132a953a970bd8adf99404ef7534ad59d3b3da040b6c09d0ce15101e728fcf8f2b9b541787a5183582634300929158e63611aea4b30b19b5e33ebb2a76020f3bcd6f4710f8995c463d20b16b6b2abbf4fd71e12947abe8fa4310ea2518155ea332ced6693727a3f1a2130f3e9bbaa360d25cf82901f557bb066f9b4a7c7947ae635836cc6c2ce4c14012b4110b8a95a8f32328a6cfb9925e8da354b190d097b9074180ccc33c48324ea4d28984ca3827434cca143f91ea69078c59702e6baf44091f0fcd3346d06df0c38e67009a098cfed03be87d78255e76a74ba36ca6aa9e5bd493c3a5abd6bdf3bcd554abbf50508d66465ca836a1cc3d24a6f0bea73b89fd86a16dfc66a4c0197b8ebc20e6f2ab13c280cba01272a069ab65e3ba27cd3adeae128bcdf0c05f04e2e98df39e43133294dbfb7273103e31b1372cce7e75dc71e01e9464cfaa173e480fb5d9048e148f094c827916647007c596ee74b2f8fcc6cfb8359cf22a1af1b5474a1ccc8d81618701fd457fe1d2a9fafc2773472dd3c990aec7bc8e9c6383df85f4af8534fcd18f81ef724dc33286729481e9dcb92ad0bd7e07ce8641a346c13d836bd9d170ed9c30b061ac2edce5ba685b4e5073fc47e59225ea556a0211fdc938cd0fa7d36f1c331f9d7ccf7333e1fc88ae42db124544a95cfa1b9930f2a100c6e91f55d5cb1b6e345d0fbc7541e8f219eae8803fafb210d6a52db2ac7452fce1bd7b73d229ca83b194d72fe2fc3fac93c020e2ba614eed2cbbd5c6a0844dd0bd73f91e41de8217612d5e2d67b0d30b9f1ad7e719bc2f5b537204d3feac7f38acb59ef94275fdba29ed79ac324038c944390384c90ecc7b66ddbb66ddbb66ddbb66ddbb66df33ff6cc8bb7d7fd0edbc75f747457566565cbec07044a448f7d241176f8c64bd1db821d63f3292ea24c7039e0aa0d079126b9af1efb2b8e0743613539829548ca00aebf46211f2c49aac2478b1ad0012f5cf60b50f89ec5a8890d6d96f945561b23af3adde21839c50b606f2cc8eccb7e6a89cec28454ff58899f358740befb1a517b9172770177f9092d653c2f472ed057454bff8c0c0e31518b471b3114248495fa01029a2b0b8bda7aee9083bd19c611d873d3344c6463e98bffa6d70110b2643db7c9a624e7fc63c65b731fa9d548169f11eb0aa9d27a3a9ce9b2eb0fe883f11851c0a5f08d618a51080453ae6f48c4f563d4e2ed90bee90d9d27a626eb215114b577b845466b5abf3f05ad0fa100df927f478dde00ac419ad55fd71338fa68282cedb18095262333e9c2b46926af44da49f5f530ed0ba16603de1a12e4604a3e57ce7c9ec1e90de318ac2d86976f8fc6a13f2d7b80d70845a9c3850de06455b220ea0bbd94bf6511beea02342c6d93bb42b7f5954f9b2838fb979b831ebbdee9cefea3c56476459a9dd44b6852140d08a588c5d19041723c58ddc740005a8120d6710c4225b546402135182b086cdbaa122733d0162db0c0817b7e0acd688014a7c0d2ac126e6e9402829fb3244a858582bbd4120671ff663d8ae2fad2c5a715169b8aa2bb5138af9cec5eb6cf60f76251ed6b7d5b8c0b51b28fa4660417629fd9a4355f6fc2bd4c7647171dea19106686bf9211b7dc3f0070c05efb73bbef1c8225bea162b182375b80ad13f2dfb4612ece9f35b34508277b441a4738de249ae526a17a2f019d9ae62585607916823f6bce6362e7bdbc75a7d5663215729afa81385f00097ff866f58a24824217227da9480694c8362abbcd8094a09ab327d74e46eefd87c7d65693f745332088947e3d1febd9e4219a4fe008eaa22c238f649e08175b30261377dbdcc35925056997119cf7e320237bf04d7f4653aac52988214566f869f9db9b1c8e9109d44309c06342e798cacd8132e1c70004286f46ed9b2253a603a504a3be1a3cc137b5f0eaec80ac383f9f3d52298a74087fff8c965a6bf8f60a1a16e04c224916718438a268dce79135593249e85f0b96473beaa5c0ac4528279daffdeb7bb7a67708697c6ba9dc40e15238de4bb85cdda974595d2c5e39f302a43719906f19ba8450d5a8db903a3c820f61a0d3d4db0f981d74ec41f19f8c20916031941746f8e2ec66b90aa943e9a2ed9391b87642972fab2bfbb7b7dc43d06be043d9728cccb6b0118e02d9cf6125684008c64b1340f288834ff43632cd17ecadb9e69f70374dec30d24818ff5012dfcdf30854a2c25056a2e5f890c97d7b895fb5cb1788627ce885653daa64d125e6f68ca73fd3d92b8003f0d3aae7e666e5fb6e041f25643d9acbc0698b3b977aaecfa612c6e12681e83639578f83792d85543d621613f3072d285781d5eea3e4e3343e55adf47752e408df35fe6304783a8c4bea1f6a20bbbfc00b7804523cc961c75677b8d3d198a7d5ba76c4edee066736a04643a9e679acc3f5553baf78ef263a808345f9a78366fa0badbf900e11d5a966a5008a2dc44492a0994d75d4d1e640eeb08ada1770b16cfd02f91fac1e9b7c70f8d4d7797b98ee35a2da020017900ca1821bc85394f5b2515b905b954cee448e24f98b415f83dd2db07ce3a753f3677c2808606ce38dfc6a653bf723cb8b85675ceec6efe3d6d90d89e1cdb553e106d0f5bd4854de2a72a2f87b3574182bb217218c32801fa623b3cf9d05f80047173ffe0aca87fa0e1391657706dd6d8c6dea2ce1645c831dc3c64762b799b452c802ff55be4c8acfbfb79acd6ae6101e08492159a8e24feef27b3afd7e89ecace9ed1597c95f3a168a4cbea6a00d45e08a59d1b8de00ee077744ed9ddb7283bebc7d64d6bf0dbcbf32c53496c9c65018440f1e64f6605e1dea51842d77db10ba8c00e3ed35ec00c5259850ef295e1238bae2ce2350c4d7fed438d1d00d1651a1edb0925b2969b0cee53c653b6fe34178d4bf51967eee4bb09b0139654710765e94582f7fcd59bba1a9c9341351327480c406af63ad238efc93596ae150c9ef62d961a3a8e67a55204ca9b2ddbafc42fc6cc6e5ba1c2b9c146a3dc5050c9c70015f408a523f74ac16bcb8e40371171134720ab27630de776e1cd9a7d8b146e57da37b4a161e8a3300a0e9d3fac5305228f3a5fb0478e5ad91208f221d9eaa50505837d290b28a9ef126d368875f01a1531327d82f5aca4f45d409f0e70dcc663d5e98bda6ef83d157e3c0af1c889f30631a108fd5f44def3b2beb1075b8b740af60dd0dc9dc922a30ba7c346bb47a7ff3b8ce9bd8e4e8f39434b4069b5a529c9c3681f035262c926076174c4c0b1176d69941e54117b13175e6d5a4df240be119f5a71b830a1b5865980d757bc31ccaf45f72e735f97f7bb55fbd0c38905de379e7094bbd52e37332d855a12f968e83d0cefbf16ab6259b93ede6cdda3e241ab179da751cc5e48015f088944fc08b552062038de0d4faafac80dac350d77fcb051c78ae8576fbd523116659dd7785df1219d20efc17e578c8f4c1d6cb651f68f3566dc458255d9002e5566bc8c5b6f5eb2b2a204e888bf93dda1ee055b71a045412827a5111f42b22042802b7eb17dc5c539f2248716721b94ab57632a34e0a2b699c01589f34fc1fce9983586c519cb8eed3beda3c24b0713ef5606e70b72df633babbe584d5f4eebde2fd63ab728380bb5143a9bcff147e6632b4f60c78c6d702201fa0f6b8be68c1d885bbf332f26acf5ae6d8aecca8a34a3e37871d518182a2233653df4dc1809b1c46296658dff0515dcc1ee4db3e978291857da561559adc17ef7010485838edd5c72edcff5a6bc9338f6c816d6e0a1104ba1ed2d366cad2951c4b836ee4bdc07056139707199dacb8bd14ab3c6fa5ae384bb1a35fc3b40cbd1d2b47b5eb9a3a396e73de58701f660ad4714e9c35baea069c30f523b15dbf4f107da87436dff112a647d9ce9bbd90e41b32902bd639607c4fdd17c36cd1c46c7abd8f9cfb0230f4ecba9f2c07da70937a4d9061f4f662bfb95173ca87b7b7d5dda9d3a50bafd62161b583e58634bbb52277b2a376c94f1c4e0ed0e6f0b402ee02242d0e88ccc664e16d79d7aa56cc4d69c891eb220d0f1bf4b28bc3c10fe0b272cce20b2c6735595dd9cf16937cc61ac68ff08b9dfa7f688546e0cc1f3604a4775544c81868ed4762035f286c580d53fae20d60f1476f73be8606cfb0bf86e8c3ca2eb9a6a3accb9f30aae438d87256163f4e0ce7a5883226b7be58442ea4b2ea3a9c64a89d949a0cd63f2218f8d363f7996525e21c6d55109c6860531e0fe51750d84f9f7df7d036327b84d988f093762d0dea617c62f02c44c430814e6243369443f8f8060f275886ef2e826f74ff1e7e793fa7863987fe513ad0de2752b91faf9f138edc24b4c7131da7f298bd895d66bed91894ad9a93775060afe19af5b53234562172c2f39d1f5877c837c54a32735857219a88cb5c5e0acbe5cf1894f65427b7c453398df7993c61ad914b02e8699b87e1d873296de1cbca89dd098b0489a345b273e207446e1fb072aedb6555df6cbe5785de18c172686ed306f91d73cf2b6fb27b76ad545fe9fcb3c01f663be75babc7bc78cffb73286a2d595a5fbc0f05831b9732ab477e10caaf7b18783e9a415ec247628e6e2f0ebf8b14932c8b6add584f0e51f8f2a9f0de5507a064e4d1c8873c2b4e37a21318d23f0f2f1ffea6c9fe5e7498774e69c2a665df35e23201df83cc0040f9ce3d89cd1228ea1151c4851c41828425a2590d873ca6457aa9ee9824e100ba828860eb6e6c32bee85cf5df000eba8dfd743b6da71a158fe162ece667cc84f0f7f8b653fd84d98a8e98d30764f92ec8509f6c5efa9ed06a99c6f235215ae7ac61c2380e907e29c6a18d2617a453049e1fb556969fcdc769075e1aef91977dd95bba12a5b0444c8b43eded14f5dfd5b01cde08065d7ee18858d4527339a12bb8f41e4efe585924e1b9b7dfad1aaabbd9e384ba89e9530c09d7be29a0536708a9836a43f80c11cc3262d001ec20f351aa81510978d16551ae68126899a8979d13d9f6cfd20976cda8d7ffde1a42843519a25f847ad6624844e251e70aac51b08b7b14b69602267e2ebe60b322cb963c1510c37b3b4fb627af937579e14156644a456a179be2217f966dc6be3cfa7217cc277f6de59006393446120554e898a42c69fe92aef5206b83303c6b91069440ab56c16b7cf0cd16c566e2fef0752e129a84bf971cd4acb4d6cda63cf63669cbdffb18038f3e0d8c100fda095d62af7fd58c3a7962f3f0897bb07e6f39641c04238a5dc4ed3feb671a04d3a7d258e8acb3f7cd4193d1b4233f0f0345618d7b589b927f864172035802b034e045aba98a2adf1e78de3f336a2e967edf4b6d56431383893ecfafc9a25ed8785a5328efe0f84de5eb5d190814b735291ebcac92261f3e1b3e657bd4a4e41b539b9b0abb93d60aeaea85e12f6773a2ed4c124f37a20d7733d9f698b300a4c2affa90baef2ac55c6a18ad67af54677e1d18246049f2774e0138b5a15a9c0f7e43881f406283d789fc604a0fb5b5a0dcf8f9d08e2acc0583d5f1c118c1a1b9737b4b2ae1f1594c9b0bf6079e469df6e35de49cb66d7c42b5d4cbeeb344926327d5f8c8d7b9f439018d9e228d597f169401ed092bb803d472fdb6071db835f6dd3369b85ed650197edf7de8aa12d6f79f8dd42ab3d7fdd0998126602ae055ad242214fa2ad86e93fd8d57f36c2cde4ca94313da059cd944ad72afcf754f579e2f8ca0c2751efb968e14d3ce3d18d892be991429ffda0e4f30b890ae1b272e18448b54e950f88e52547a6b2137f457b9fe3b43608afcbd610a39faa09e191617a6aca084eced1e5685401884811e625e5fa097181e7447ca965ce587c867999da988b3de4335ed777389612f4892e2cd03742212a447107279cde867a3f0a4a21b6753993460cd4e0df3d81cb17b9ac128a5e5a0bb5609c70eaaf64756e70b07f7c6e85f9d27e0e4d5a82997d1c2b271966a2c4266f3cd51cb8f5f6c4f1235917843a390ee1527cb89ed3e5fa6226619a6809da0154fb6a4f025aa5155c1019bfc99cf466679a646d2756d1534123bb60f8d0a9ed3083af2469bcc0e13c84673f62ee639f7d0c0756fe29ee8fd454124658f697613bd1fc89ae529694f9f837e1b20e4df0be9edb9775562df9eb66adc62e2002397b8f721d314aedd23b5e1da8b8a3df2f9d50788d3a407f398e288faa39422701323dfad647b5ba013de329dad1c71766246b493d9e7c7faebcb286b0574c193471d08e123d9e0ca901462c2d9ba80334c7f59b0f36ab58ab9630157ca472d90da14f94baf9a55e5b83d69899da941f118c2c6e003d71b95223b11ae3ebd02b7b1b4c48f3df432df3e465050e89a5ec583473b1eb3f27929b8b057167743a6259552ac3f848576d4e194395e9d68466200156b35c10f95ef6266e7b36dd3348c4fb7d672bc4927327bf6d522311ae14a118654b96c4221a976b12bfde57d4f82196170d973407255241fbcb782a3905f7f577a44986f43ffeb58d30574f7ea9d48f4bc0150f75e6f1d26b0af3eeb257d2a7fda8f9f9a355e12dd7a7c79a7ccc46a7a17a5dbfa9127234a2db2d978f598ea56890b37fbcbfff70de870986efaf7d96167d47f640db0034b09af5fef35a208e3b150e6acdb6f9eeed348f793f926756a408fe738181c7ebc03ef4b5d26774230c4013c1ed0764312eb0678a99f4e452d017482515648f092465596e993cfddbddd17ee575c98fd7296f650c267291775a39a321b4cf3e6223feea12280d91da065622d50f9173e7357967a07406adc84e5065b2bc5a1bb706b7457af477bd071a31e6ca150b29815c060ce5edba2e8e16f9a4eba032e47ad5e41b4f1a4db5c7441a0717a66de9806f5b5be58a6c960b08e3ff0b33906d76a0df40e4f55b8647f3ef8d588077d5200d54b90bafde19a549e9267c942273bf1d55e1f518f6a5ae27aeabc9ce28256ca4597dcacc508f3cb7811ca79e0d0037505007bdff7f8b8fcfa6857d2eaa920ee05bdb07c850e1ce2108748d71acdf47aebc687a8f39673095cd86e6fb61ab69bee6022564635a7456d60728c9fffd8b104c15218e2c6468fa1723f5edf8381e2237ac8664137a703ec4ee73cd03d2300d714f1dc3793c869be4be4d69e06684b49616e607c706ecc72f1d95846105b4ca077bdf2e66328d23ff6ed356910a6dca1ba8a587bce22c22a767bc23474f1a7a1eef0e377e99dfc4201242342b485dcc34ccfa3d6a7b12d11e0b1dacd1330c1058feeb085ffa19f8b626e5d39d928a33d3da5c510b67c1cb66046692903b2560b62311525549f8e6eadb5971222d3ec1ca4ad3f2c965d0389111473980b51b4624a543ab9bd79108411f76bfdd3680a7ba4e7f14ed3cb34dad1f88258d53814ae9bd3031ff86c190936da1cce17ecaaf0d7f761a58c2df39a4e7fb9a8897cd1a40274f502231e4018557bc78debb2ef021c1e9c8c2c6a8e0c1eb147f113ba3e920ed76024014eecd9c25d9dc0de2f33d32c9c5bbcc8fb394fb117b71e19c3e9867016a44861a0732c1686bbc8f90ef447dc7541951639d2ab07d102ed95ea2ed907b673a21267a4c660774f63f31b1f84f2b4be6b4114b48620a4a1d2667bb8af11e1888f331699cd51dfdb1161883702609573d9c8b44dd722507e3f06cea4595aecc7743487617e8bd94371ff1bb6a8b50abedf118432cc700179776ba98b4a495fcc62f9a32a5f54717ce5299881cb5c90a179a2c526e5a4a4c5e196de5463ab626184e90c255b1b6b0192bf1b427266072c4d1fb79459313e0a6cfab4ae64344a4bc4dbc65aadcc994a04f576da7621348e67c2e5cffdd4261e565f761a046b4304d81f694c11ff43530d215c045256b5e79268e6bc55613058d5970b4226dd0960f6570c164d4df98c5dd930a327f7827e486db1b3d247dbe8d5e396c41035e1249f7e4d14a7f1d85c47d69a0634ddf942d2aeb778fb7ee2482f5a5811cf6197a407cdb4a318d6632b05b5b351e2e183a6e03335ea5d0ec6a075b2c620527aeea1c8825e8a7e477250ba9ad057b352ea7307b30c9bf668ce694aa8cdd0a294953bcc395b2d090c4c3870e2ef52e5ddd03b3ae807b38d02fea3334b2ae751fc3c85136ef56d02a85ec5634ca181b4fcabdab79e1fe6cbfe6a6251aed29f4a6adef920e7e05b5f5bf857c05d7dd78d1fd90ed5f9f09d7dd182fa728c3d9f549600ae650c2cbbecbf062f6c048ae36189e972526f7f70da9048a44ea9cb020b24cc02a9163374040ee74d100771f8d85ae145d5d6f8a5e4db45fbca5adaf260a1e3497df5b4a823eaf6d6760d893e5e27f41d892030179e4bece2422ed67f0c0bcad351c0cb14c06f8a73d4a50891a4d107ae78618c7cc31203ef91fb3be7bd70c239030bc2c39fa5f30f5481781ef2069af86ad96271c6414b1e611a422b09793c9461e327f642d060aa1c1bf36b84a70969d03aef3abf0b2976045a2e0db8cdec0074511de0266ea973997c3ba7c6c3c65f0474ed5dc576351fe6c74b2e976a5fe90ccdcd9c934f6cab767421afab928876df58b381e3272302453a11960af53715cc108c6eeb57632adff3d0b736f4f962cb9025e9e3bef3dfcadfc2b5203cf71b60bbed0d762fe60d4815cc3824c9980c6d3c9b1229ba100c25702b3d1e85fa5fa13737d3953e5bdac2de233b94819524d795310dbb0768c5d8a085b92182bbe998a0a3636580690418b0f223884420c36d976980931e4733332c4829e53fb41dc827c66616e86dd17bd0d913dfeb43595683911ff055676a65d6e92d2d7c9a5c16345b361aafabd09d34937934e23dcfdc9f63bb484ee4f947cd1b7bbd043afd7f6de317d8b2d2c87244e2d0cfbb2fb9e9f4ec4062fe8972776cbd2479bee664fd06cf198dc3db083c0d3c15ab597f8cb4b53642a190619042c51ff122e51171a45d9c9dbe6bc17906568eb76eef55c04eb657df63f7baa7682d1e184d7acc29a20585c10d9baf9735cb75b706dd04a32edcd3069ba1fa1a7dee97fcce3311597aed0f41a750a9a6b976326d8e1fcac8d730bdd933ae4138075313b0246eaac4afb63efb6e952788c362f45dbc9e488971f3fd318a029073ae4abd52924da29ad8c13ed0d5fb24249af363a313ad9e0ceb410ac87dd8075ca9113e382f12facecd48ed3a8ec55e44ac4053950c824b0fa99d539718e751be3dc55395b7dbc57ae2ab2fde39281b4b68a0de47428cd6e6ba13bced2cbcf1db06ac7c8dafd4a74bce121515d586d25dffdcfd65cd72f2b9affe68351041412812c01bc58b4f3fdf516dd79a6542a3250147ef6afa15ef928673ad54a38d8b81eb4fd7ac8fdcc374a0df6349a1054fcf7368446c9071ced1d4577dd1ad9db4fe88da93d660e01a8bfb3cc33269904a9cce84b70744c6a1af0f58ac36e4b05f1adf92e4083fd111c9a9e839b57dca6a572c2f262066b3177b7b2665bff407a088b1f15bed4905a9969d87141d72a8473652a7e304f7fefa61bd4a6c954a0abd0aeb61b070ff89091b2b8b14bfdbd9e7d8a854061bc875aed18945f6087b01e0e97b4f1bcf51ed2e5b4044eabb5f13dda04188c5d800f108a9b808db467f2dc449db65c1550e18639d168971744de94c3338b4671bf8b6df820ac9c5e45f2124c6e96906f8db74ce940479932f143070227b5216d415d01c66850d2079535b1423529ee3011169885d1de23ef26e4667cdc00e5f223251d306b701f3973dd15eb2c24f61e9531934904392ef0395620bf215843b1cb73d55f9642abfd863fded7b77617667551097bdc89c760c6089c311deff164035f9ea4a8eb7947640bf00c5ea1d7480f232367c82a3c4d8a29fa0182b2ce3363bf66866b74d22eaf3af580c8e97fc4dd219650cd80c31de848db94f48941434c5d2988436f183ff98eccad65eef517881f95a613ad78dfbae80409ef9a5fb5bcd71275a84b560ebea349b75b0c4d882ec5b55e274d22921638c09e1db846d06c1406886cf1180b999125ab28cf9f0224949f0ac0d21d409b09ebdeb1afdad36d9bfaea26d8061068ab5e4482a1b0987c200acebc6c7dca609332cd6725b7945ca90f069cfe5bf49da7df79cc3f7e9997ebb9b4f9bd689d586f3ebeed65f383e6e5a8b30d7c898d474f8b1e44f813d76c4789c94f57f4d7c58aefdf11522f1ce15809edb807c9ce8c41616cb539409c770d5f0bf867e55710d62ba12bbdc5560bdc1be399f27c1cdf6f3256bf720c2f13b519bb7004488b8de081f27478bbd12aef526754913d2deb231bc2a399a3d3911b2c4c7efe2786bf6545f5efc95f7afea55fbdec269f7f09009afa5b7862483a71a2b399bae41081c66471df7f3c91486b445b8c82b84498d2d1468059645f9ea5eb7eb8e7faab10a398fe78734f84f46a241bc5313a55375543b2f919326f1d409967c605e2f3e0e0260425553b7572a3e910633614f0e4ecf27c59caa9c34cc4e32f84a195db1d321b0db173080bab5b5ad624b679e6e3b7d19d5d1a6aca5b161d2c72debd175224180eff4a5d983c84f47e94960cdd55efb94892312c314aa9c7df07d517cd88dd74fc78e120e1cfd63a43fbe79b013011e5a47f5d6c993e1779485a8d82464c6d843062822782d2d029bf7854e4ca796612b9fcb6203b0306c116562bad04dda32bb1041ce439047ae77eda4afabbfdd5621be4f6974a19c10bbcdc94ea332ba72f000b1f15eee66592edd67920ab6a733faed3f8eb9975e11c0335d508914e7b035191900dfa8446a72c81c71cc37ff9e8bfde547b49463f1a2708e3c8d1e334e3ebe87004c164aafb4e1f5190bc95dfed3c7eba84e2eea9fb84b2e96e78fdef9ca4d84d705e4700bb9c50338f57edc7d59056c38e96329103cb5f054b2e55cee0607f820f48a8a0fba697b8fa0543c03bbedbdfb7333f15431d79c03cdec5aa82510b69583bb15817e9d0e7cb1f6b7a8156b632ab220c9c50a8b6f58d4783e68f3344b322757bfe65a92e5db3393c48d0e1df8512a82a12956ab58542179bf78cb4db07dd5c1e17b52c1bd43f4a24b6cbea926d5b82d8fa17eb7a3f2e6f2ff37eeeb908d5a022e33d4f313069cbd9bf01401241a63b4dcc1aca2be1c303cd0dc38f143042a9b63b6c34527dabfb7dcffc6504d31fc47a9aaafe6b8e96bc9871a39f267a25815d0e81714cacd4b46e9aff138adc8869db27ef564740641a094b04b54db667d85c5ffcde564f49b5c301af11432cac022d7eceb9e60072842302790f05902bb6ead2d655448c2303793a65a72a8c737b909544896f2090da8eab61947ff883ee6c720d66c3790700f2d171bf2401c944d6c4afc20eb5f09015f57a9438b9c85ba168ee76e99b3c91455973625a81f6574b6e42e8192a07cc5fb46a595d1ce507e88d9b797fef786f0191f549493f04b4d3700c8a179d537136a9a937f62494e84b9af52f2aa2325f001128dcc1c185ebc8f785952ecf1d3b68edc36ce29bc0d77682bd412aef7fa227dbdd86326a1d7a594ef836e0fb25c9f993a0cd2bd1f7751a6a05e317d2badd1a1f439c28ae65759e5e955ab136e19e6b3aa8b97397d90c8c89f53c166ae296efcd7e07f9774282056c722e2bd8b5baeee34faad7e80611ac1b36a095219c288c9572b416ef482e0180d34bac7e72cea7a8a7d265551d598db1f1f899dc047465338a4fdb107cafe00c759c6e33de5cebf4de8b0f2aca49cd9585901a9c251bbb56a5f410a5d55f7887e1daa51d73ba948fac174940335f23205d669452390434fb2ab1be6b2f9ed9a49cccfbef285159d1df8b3c910f268095e71941eceac26393ac651cddb170ceaadb27a312e004ff62d300f3b52dcd2557c0ed71808169e0f56b2e4f8ef12862349a787e64d146cfcf2622508cc4c32c81355ea22963f63d3326e7638298d381bad8d73ba6b639b4bf97a59f11e2fa8461a94b29eebd3fa1df96cdb09671d36ac40d402f2785db8fcf36a08dd2e69d6f408ce63586b963cef7307916351c48402218fb43da096684ff4ea986e0d0aa46e19c2bbeac671d91756aa476350710bf5d1b3cd1cedcfa1e3a6877665a91869920dcdd08456eb467722082157c1959a2c7c3d355fd42b62e2e92cabfd7bf8a107421ef5e1b5d18639cfe9e462420e539bd9a9c17204d62a3ae0c2a1c4b023adeb118a093dc89a78650618dec8970ec43364df3a605b426425bd70a4742a498146255ed7c3ef8fa542bd7ef1fbb17a472f4dcf7659690cb534f6f3e96ff6e7e19e5fb9750039225fc3c099fdf4bbf6b5567f517aeaa7f9a88d66e2911d3402a6cc3f50e0ddef6036bb49202266ba34c8028b4a647033eea928a9c3a31ed7ceff3919bb85b800e490f92a9ea09496234fc6ebe8206ac4b0e784622fba26caa6f073f61accbf6b74929b70a3115e785d5ca98517f3381286a466a76ee61a63eb6f476bc712e70200137d2077cc391312cb0b6a7f1e384378b3fcc365ebdab31cc9df7e3c9c4d380f35e01e4ba837f6e4d71a85605bcb1ff9f7021c7cb0efa85ed40ed6cdd2f2b4a1d7879a84348cbfba6c1d0ba2385194fd0a1769f08c9492fae3fc8b2f1d36ee25d4c92d48b2404508d0ead9a79f91cf153214b6e9227277044965fcbea0c6e00b057d4a50f1a94eb3021de08d0892976102e6c4bd10459ecfd17cc1b5add4701b0653a8e3dac1841ba5f1de0e33c54bf077aa183445e957a99dd30670b5c81d744163c876c2fb0474ba7b102b32ca723544005b957742eec1041891d1bdb987143294f4d24f3ddddc2d55c3863fd2945ebbc41b50cf8efc9c7c40f56d6bc728ce0a50e621f70570f585f2c474a5202647fdcd014bef25cc7b694664ded28f095424b35c9b8c846bfef539cbd30477f43d68830c974666c4fd209b91ed36995eda133950361cb2f2dfa937dd8f785fcac4c833e1105eab42721a218c93f092fb942da8bad5c11a02045996ad2f3d7e51f7859821706d8cebbeeb387ebfd3c8947c1e1a4ad135e85c9971bb5e2cc4574db48b79621b622f33639020e8b165b3084b0a5ea4ef9140c6a119d081e5904831bb0eeed899272b4092c2323f39ca58f3d9ceabe93f0b941da24af640732a9c309aa8a6a7200107ad84e52799bbdaf14fe415ab7b71185d90a2f80f2575519577900f999bc2e86f61316863376f4cf7a38f92e8cc5b6ff17f4ed63a1d48cc5b3cebdbcf3785530f2b93232947a349f23d946dde1780583aee6b88f2a17c49dc812f4a397a6d91ee4af05fc9d8476e01991da5c548cae574d1e0256748011b484bd26677dc33d88c53ee16ad7ed81392a113b5ae76c87a7e6bf9c3a9fcc87b584c9f323bb2a985326e655110a350a6c9be2a233a4466b0459d43fabfaccaae0fcfa1768db6358d116d5b3643a26e5aaaa0dce95efcc826ae62460ccb11dc6e2c3f003ef11e98f257aa85536e31771c71905e2f870cf95a1f70e9b35472331362ff20280d7155e2aaaa7f06f94c1f6cf4c9ef687b8e2de42d884a479f6f796cda37a0bb518f2f13860539bdfc18b7996f368f14ea97b432fb5265cd96b21923caf5730d1ba0db64fcf9d926eba3e2339b33df3fc2753182f6be9de6695d549d60be7f2d157fb4dcf4d605e89baf0141475547111050f34a73bc90faecc1f76aed4d78d536b105f4567ae960229053a3c53b741bf27bc90e9b80387734af5424c68ddec2dec47baeb5c86ae3930c456c987cbe5032a9872dfbf7280fa71ef544430eec1f57fdea3eb64cb62365d2145d1cf9cb0f52087314173aea6f34dfccbf624d96fa870028296295a6ecfd2ea2e7606a726bd4d0f25fae093ac690c1f32b46f9c747efbfe712e7cd62804f0c576339dbe0f8045ec3ca9f58d08c6d6714967a8599742af10c6006d216dfcf38ce6c1d1a648a484f69135a39616cdfb9e56fffd5a6be687c1a38d83475a64ce0ca4a20fcbb379b065604853b57095e3252535d90b1a00ec1500f300aab98bee6bbf29c2fee6a768b10fdc1574e3137ae12297ad2885d1d8440c797b525b5ab443e5d973d6e0997b3b1fb12b1fab6c4b1149fa6eae73b009e5bd564b03807230b7e2d8e08c7cb4d0f940e3c6592e4a57de1b848fcaebc8e2b85f406baccbae006bc67235c530db84b420677b0b41c68b5b02eacbf5d401e40d6126054556bf4c46e149ff490e1de14202dc2e79f52adc38216c20ce35b42b56a6796988901fc7d991fd440a501e131254b1b81ac9078ad17f00d7e5bbf1c0860848fd9947cec646af03a0b96f14498bd76d491e53d9ad32e58057ffa086cbe35447c0a97ac110c4d6cfa88e8b4d3b21a36965b40a8703a80bd8ea103e95ec5910ddd14704eb83e940264c2bc08f3b971ad3f2996f4ac08f97e6f194844b6ad11a5220e1c969b0fd28e42983f3285bd1da3bc8b550e8b8d317d72c894ea843648094b77dbbbcaeda8fc253b79a84722d5eb2d68e10c488c0f7ec56778934570bed037d2cdbbfbe831c5288fcec468ddd7b16e7fdffe63d9463aaa9a7dcb90c196167074943b0207f6aa7a88d10696d94e836b7ff9767acde64acf6616b47adec586a7aa48db848aae8cc46dc4fa0659e27fdde14182c9c1b527663976ffaec3366609be71f89ba0abd10cb0d183335cb3ebc40b95147f51d0b567bcf4f69a81ebf75ef4e7cc59699db6b1f9dec2fd095a6e32a4350cc90aa02da3e774fb2ed235eaae81a7ffc055677452b6239ecc5f2e26e9a9e5dc6468f5d758619e05146ffafb520ce8948a369a940378c8ec240e95401194cb7b9bf6926d2b437974b3fccc8295919e1881c7774f3c113ad6524475b06039c188090246ef03e12233d368d2fa12ddb75f20c6c9a52fba4e0d3d91d52dcf67dfb9c5b4f0a6ad2c15ce025a69ce9374439c583d281830e1378653153e6f89798d28accb7f637ee27ab7dd5654961fe3f475d82091254bc632c9bc0d8bcce8b3e5ff615536b03f1d61931ed1372dbac2b1ce763144c275cac1b57310a4de0e391207947f0007f82670845545ee9f15de651f4a86132469c7292ee0ede05f2652b9e9d78863ac012018fe36fb9b054eaab11248693a87ea39e7dda15140ace99f51d447ba83cc10eb1610b7c0154181e398166807d11bc46b4ec68ac370846b34ceb6bc59e71cf0fcecb47f58ae3f47164d8eae4f9fcc9058a6255b16fda7ce9eed69fecbf4d6f4584cc701598429a74b3f58207f5c0cf65c8b0fd271d7cab1460f03ed4ee71880f7fb87763e19caca7f2d47701a0b26bb88a7409d41aae036e24f860b9afe70a054ad2ff95476db8dda3fa48927d58ec2c51bf24f5ebc69524614fb2f1f1f290da3fbb6869edc67c706cfd9a9846e584183d885dceb927f0dd48ff72b5cfe6abc0a87774eb3516bc34f990c4a9af6a799ec78de52d1543839c431df7eb644498e24bd20f4805f98d85e3a696182b52b88dd34e9674cec21fb80e8cb9ace3740b6ffab7fae93a3e7ca5c503ccc49f9aa5ed2a30b2ae29f0e9b2ad7f3214432e529c800598e366c585caa19ca08f851f4ce121fbef7d591f5491329eeee9811ba86a7000eb8a7b22b5b7c555bb59c64d64b57008afe8cdf25dfb4423898668864f735bea00704ea7d1efcd69dc7318c9087220c84715903209ff945a5e7eb30f38e9224a22bd809744c918d6f2b8861205e2b1b79de32b8219b0145054fcdbae63f66f6e96a505ebacffa8ab0bf04c2f6e2d01306f811538b09b1ce452ab6d5fe39f74bd70065cbd7038e76f701a448358cfcccd88e6433979423501e9a6c29d88b15573a6b126976f0a0acc4d5738b37791c59a3a4a66773d845b689dec7f2644f0c4f54cae3b793d27ee009057b8ca73ad6f43a77506e60fa3f69f953b26508ca14cdf7939df051cc574a88eb989e5274ac86056290a95bcd24ed49d9f63c8c1d68b1ab7ee13cbf603db4751017abba95fac53302999343363eff659ba1489641e8b654cefbfcb80c600c9f060398fb82bb47b18bb74f69c11a6f19b3ccc1dd17f9e7e9e194970433b64e6ab237a751cd4feada42108a47657fdf901bf37dc516e1c673b37b472e79efa79f0f6ccd55bb5f095f8a12c99a67c8485ba932ee9fe3d1f3c2dbd59c858821df30f10d795679da84aad922d1aef78123c7c6171c36e5e7a6ce8fb3a439c9003cbe52bcf56b674b2123260e5b2fd984216267737ae87a6273585f3ccba6e5f7defbf65d2d7c6dcba063a8eacbdf7759d6c4b4491449d8e1983b3b81bb7a130ed1e355ff04ba393589351927fcccc5336b5e4219dc29c0cf93b1e71d7e05cdbae96d253055ae29c890c6c1679c43c7c58daca8fb2c5f3f510dbdcb9b6798a5748ecfd7eaea698f28bfcc4ba64f5a82f9f3f408c9df22a0087ad50689387dfaa6dbb156e3bcfe53f73ded9ec9d3aff56f4d954a7e5e9dbe037b0270f651070eb4a82426791f49e77bf6117991b66ccf09ea484e0f5e76608ba4556f1f8bc14aa20a258a00c780433b2c04fab54c75630287369e312587988c571a3e821b06ed54400a385d0267f73060df0c508bac9671ab1e328b632f76b800a78a1a004a9cd52ad216a087ca6d1760b0692b163cb9aa3cfab4c539f66dda383043bebdccbd091548eea78105818328b2396d42a4efe6e88d26cb05b67fb7dd8764e7456fd59cc947c39602e7a09416d0aef8b1124a5b800d6a1033506057e3b10496f4d8dbc7195e2f1fadfdb53ccf09e21354bae6be78bce407b0aef1823825f275f735e988e09a8b46cdaec9ceb0258ed65855332cedb4f8926a3e9fc287176adaa6a655408389ba3eb67e54dbb5c2ae53b4534eb5810c5020d5d7f38093c3d97722e03e142e4c6541f7e79bd4fb115aad4cd3866fbae4ec292bac2ed680cab6ee522d4a568a2825400e6746f2bc56095d1bf099c1647dbd0fceaa02aaff1880394823695db859e096aa80889bbba75ea5206eba03e0d9d09229dcfae4e3a6d36db899d7e39681f9b5f146c7f3b25ab2833969e6a43501179db64205a1f4f94d22a204050c69d350c8dc543c3eaa86224eda5fd4718e35bdc1b2799d4555cb4231e53c15ae685602871b79cb940932ec303f6099bf57f18468237f98275954143902df3ab911e4fd02ce4300c16f5a9f3d02dd47e6872468ca034417fa6413da2f8fee4a0f38b5bf892eb7a2a4091586ef9e89c81cee9e8fa1c27dcf4e4802bf1834131ca3496fd62e6661219f8ba88fcba8a8e6c01ca0650f5565761fd880bcda12c4d2bbc4eff62e77a63af8f112c67ff5fe390bcb9a1a2794e32789c5c382ac4b62044fd8f54f4cefd59ba94597898ccf2ccc395033cdc563c036e7054c248cadaf7eac3d1a819d9dc6e7099ade0fd6dab62a431ccbec77484aabfa6d88802df8302797404e36f29c383b42fe2d55a7621facd74b47df62333b41c7c8cc31ba93d57795911e1f95aa32f7ef585eccff993ff1d52f581e38e090d4d1e1f0b0399281ebc2d8d38e8425b5285a72607c5ffcd11c30927751d94c0116cb827d951013faf4bc49cb8208fbab88bd869ad668d371b43cc06b0665356a30d26ffb1c29430e429714f5ef31124fdd05941cd2f3d39dc67ea217643fc4395f8de3982e9ce66d4a70a560b644371dd5b5bb7edf52652ac019e94ea9647a7085c629d467cfc9857b61f1053ff67e862983b8672ce61f53da1ff592c5671db84ca597e969add64dac64132372c56aa0abb6203b6cda261ad64014e9bd5a3a1f651f889353fbbc860c18b8494d911dee076b14ee69e340e272f5ac1d5dc6d0a0061ce7f1010f9f3de11cec9431a5aff2b5d832e733a469d8c8087fd163e34e69ecd10b4e0431eb7d3865395edf9bacfdfc832b4097852395a737601d5d7bc037793ed6025cf153db49e34aa5bc43d4246047b630cffb8bed49bfefb77ae3f6e088ac54e4efda31363462b00c5b520ab73c9dbf969be085bb0a251dadc6f08447dde0980536eaa2e59694e971cb0fb62f36162633e57cf42f200bc57a096930dc6f8cf3e024647e809521a74cfe11452f036f394ee083bbb3e597daf842ad8f43efe6a3903010077fffe07d3c499f5a0924a22efd9153e49cc5da2b0fcaff776051af2fad7f665c5cad94e44ba031fdcafc8eef78c92a31d1ecad7ff1ad7313864d08a35e2e9faf0174e21814b0450ba47391ca536074ad97104e242d1341f5cb1419872f8ca60f125582f38f38a5168b77435635581eac77bc62679e54cb88d972d03b5f5a5afed85686c29dacb2573e7e4fc203d82ef9a4d0acee9cf64f5ab01b7ce49388dc43be83b397e1ea732635c35bb1a385de369c5b3b5e9700347e093541a803b42f5966b34b068e20da0f9bcc9493d57b7d3c26d7598e50d32c4b6265ad64e8c495950d7ddb25ec3598fe7e7ac16b4533b047fd03a3d164e80626dafceb51fe8d4667e237b01ab8ba9faef584062ec9d0029f44c530e9d1c57c61a235b17d80b1826cb48f1e6d0c8769a67628c44c3d73bc8c29a6161130cf6a0fcc86300188c3be75e3f4eb2edd45c24c9fa15281c3785c767642a6608a41f9bebb4b82ed0ab8aaebc38299cea17be49cffdf642d4f05e755ca903eb80a50048d9d7f16fc481a6ce6e2a95682565b21f6ba12c4a5e2c2777319eb62b7c5fea8c507b0e11c4fd192751eacea4144b05eee09a9a8ef214e3502815210b98433d5b7b8ffd86dfaa3e50a31fd09b68d5fcff60a6039fc6ad3b7202297b0fc2403fe766d1c291e11f2d29a103d846b9bf2c264d928afacaba2db086aa98b3f78a4002285e5ad88a51c200e9e3e90edf208d69f63807807a712f7c6ca8ce0e8e9c902ebba3ccf79d758e0e6ebfd6fbefe040bbeb55798fe1c6c557f254643315d2682b0867ef0e89cdcbb7b2cc061238a5c57acdec60aae4c3cfd34f4037dd6d82a411d5b2f530162eea0290fff6af7ef238f9a3a6309857a12ef43d674d5497083c2af2fb364041a803dd84b5c68c3e043b2a0f94cc1bcdc81da9ad0144f30240314e24edadebf30684301c2bc9c47de6a47f0a33bfbadd9a9b2e138b58218b3ace37138f55c0446e301a08e50ce6a8698d5ef930948a34e80f10f958ecef21c7a4aa363559a92a455ce75127b2247c12a3bf1010bb5099142222394f582a1a3c2019b4fcefcbb00418fc86bf0009218a26da566fceb411c9152f5eb865e1e3f4a89f1d444e3ca4201e2cbf5699f8b2259c6d2e67dff5fb42b3a170c70ef2916559cbd5c7bcc1c4b4bf1344641dbcabe9a8f188061952bce99fd9b4a1a73f92142fe30b5d148e79ca3d77380e37277c10d8641e84c3e16103fef7bca85d5465bc9c48e5c1df48d575165e9d992025ea76f6f116ed64771c7dd79e2ed32e5d2b24d0341c98da389314af2d1251258e2bab9c37920a16da432d983c6f145182f989176493d2921c2b148032062c21a5689402439dba5cfdda2578834e2102c82f17f02cc6e5930172a3e4cbaef7851fca5c70c2e474890de3786c78854699dbb063110be84b2170a225dea878586fbc1250cd1b7c1cb83455ecde7b9ff5c90c76df9d8c13b6477cb5f9f40b644727c12797b85ea4376c2e07506c0dcc078997bbb7c79b050513755056dde56f6ff7e424a8472780f895447468bee73e99beca9c11eb1e391aca98fabf07adb4bd6c6273a514d9c8f98e398745507b4a2e3da32225304aaf0c9b698d53982103d98dd870fff4c4a2293f90b88f4e2d275caa97fa5465cf9b84c81c321bb25336182af057b4ea7fc53e74124a820b356a080d8ae52a40fec9da5a6e13c21274953dfbc17c226ee6062fec1d1d55e9d4cbb4aeff294d4b3577eb721792cc83f894ed10de6f4833dc4a56541deb0d616619a03eee5aa39a4a03c81a81eed7c9d4bdc01039d6ac50cfcb25b421269a280f17dc7b745a298ba0a39b85296f3b601c8195a65c2f5777aea9869a7123f9e2e269640c1944b94db4112790f12787f7d44a13c9f20b38459e22c5d4da5f82ac63b5e4903e41a537d007029823abbba95cf4693f500d7ccf9686701d3e8ace744aa0d43d2617829869dc42103efd9caf2f6b85d7427120ce19b6b2b4f5e639924855c0c880d2de75749012d7bba0487b0ab0be5858743e18b6905279519a51c63cfaa08850daaa83befb63a61ae87fe529368b7aec47126924acd97aa27fefc6e698e14009ea23cf867d365acde8b4b23272a116d1ecd41d9844116270cba7e2d8f88a06c8655655e4e01e32a97566ca7939bb668be25fe5d76e09e47486cf471d7bd84749188552b06dd482cc9677a7e5fa9606bb06d99756ea7dbab75073bcb7e84ee38fccd1b3e9d040a786af27cf6c97a46fe0d8e528076b8714cc8d865d691b8cd055fc0907900dde0f52c96322ddba51e4a6578e975a150cb9fa45f6e89982b3315816ab9015a60e3892fe2ab844633a969f06559cbc569b86ff68bae15e3cd10bd47b7234751bc16a11c282f8e29cfd287300daa4751eb00a8cf40222efaadef378a597f98b2ff09454ec1a0c03a339ef1fe49ae4c6fed64d13bd5ec6fdc160b4f6f9375a977b71cc5e51e73b9be2e6cd02f4e01bd2b70cca08d1e0282250ba1f4d234078cc1f9f24af044c5a92399cc036028e98596ee6d8d490aee30978b08ff4282a6cc5cd587ab7cce570054bc60996a8ba39b08ae7cb2af300894982bae98118c8112e3f7588207f8984f63340a80f9a00d99fb16d44bd4867cc260dc5c69c4e73c7b88da72951ede9fd78b328b39f1c2906ea81581c4f00a5136f2c907f6946d9d78715828a09e015b7d00ea5399eff9e6916117635cb25b87ac6a599f1ca52c3c58eb5c0c96453ea5388cb7038b6c0149308afabb01f6484c8012f629eb6704ae13b39d30d81f6b3343493471dadab29e1f9a7dcd37cbddeb55b7ce15fd892e86b8d90ca4c705622bb9fcf9c807a7b97884a34d09b431dc1d4a3b02579cc380f2bd60b10b05efc663a169ba088f023ebe3b95c80e6eae718744f9c1bfd7007e31fbbf8ce810fb74debeb59b178a4d1d6dbfeb7c47ee1d26e269c93b54bd273c287fde7b32e25f0ec9fea17d226e42178f60ec9d0801c1228e382325ad8c57cc6fc9d416d762c16ba39f33891c1c32028bb11d5cf227fee6a99c28582102eda348bdd0e0e0091ed650c66b07b91b1f758d85540818454d6a9131aa7118a496ea070bcbb1d042cf556477e175a6a0b03b6ec035e57b7c54b4e6263d4c495632a7e9a335a179e474d37cf54863dfa31a7bc314d846812516d4c20349889da8047cd69ea3d19ea1c8267727d1e0889bd008fd49b8b07d007f7e6216900f195146e8a2ea76c0012ff370eea7f1a2b52a1f7f364fa8884b629c273d31a8650896034c2bdd02f26b40ca4e55f8460351ed5631c426cfa59b059298fe254dafb74f5f8d4f50db7202dfe19791476ec269b82f06cc8a2629699510f856b5c4e127a7e9cb8293fd4d0913df50be66b74d5e0d5a01e6f38c741cc3f9d9853ff2f43562d188d11e152bca5680fe32d9fae82d8550bc08ae3c91b2508b1377c5e817013c33f39fe87ad0d5a70364cc6b044eff0860efa7bda9fc94a729612c63155ca56e79987ae0395ea3bfaaf26853ccb07b783ddc6ec298afa70a1066b2549d17d809a2f774050aca7916e323dfba3aad8605f9a04b3385ffe36685fd579cbd131d43996a743354b74d07d3a913c9f1c076a716426f7b1b20e3605aceb3aa9615a9bb06077adb7d9111173162b3c0cc96614b0e1b2ba48a4bd9810d4c9a0514943c82f8533f3a22eea335ce8c1dd7d3f35da50f31b7a9243d2682c015808405ab925365b248b810059f9a91db33491ef41dab0287b5b60cf98dbb8896fa435941ca61d8ebb7a3ba8de23adb8e6dba7b3bda933827177cbd5598b82dcf496a0480e79bdc80aea57f457482554c771e9fc554f2928ff216be7663482dd0ceee74a4babd64aa020feb720b74f2ceafbbefcee3b6eecd2c39975a095a19c96f29dbd8a74801f636abe65dd64aa052750d90f70f828d9d067e43689de4c6473f00e4f3316f740b2a46eb5757770734580779669bca7c7ac89c24cd105ea10681ff87d261346923468bdd8940f465060bce56df10b4aa4e04c0547381e79e9dff832fab2ce8dec05a1a49429e888f408eba7b4a2bd9552b9635a19870ed1f9418953d25aa8631aeff5a315ab4d29e2394fe75868d081534f5812a95bf032d6a2ea38d302bff1210a95b6d550f4d09faccf81af1a6bd7e947530dd7cfcb1250a732f2f79cfb6ccce40f25824ef9fe98932771f6162e6673275b094de6097e099d623c92236b662f4c43078e69a9054a7ab8833b7c59ff7722a9d88868138d9b265015f4f87faf9f011683c69a6e93bd24565b94ab386e258244022d5f584b7ba6e4a034415807a0e3278c33d8d64d66654cc699cc67fb195c104edc625eb49de9a443ee4a919f2989d09d81dfb6759dd649a5ef7563a35a42e50ca1d23b2077bb27ec8c6f1a463a23815f300af0428051ea0c0eb3bb7fcd70a3dee22e790d12e58fcd99747741b000000806f90498482d6df626ee00311af8586aaf2d2e51a5db5e3cb0684147a4b4885c962e85162c4d479d3776e2e758ba791b4e502ad35411a93f66936e199a094264647720a5171de1b49e75ce9622180ff75845474868fdfc4c48a966016482054bc2d64e01f580bb7fa9eb8b983c603a56d83caf3149e6b4410a89118f7da5e5cf25ee72c858b5c0b97cf25ece5eafe8bfdc71f60409616ad730b9e37b336e3f1aca5fbbb83fc59ef100e00c5007b4e001cd4aa2b00a2000000a76b70ffa5e3dff7e329706ccb625528e5c85a2b4d585755050bf49469bfb44c04f0d2bf9664e55bf5a97a5eb31d681ae31c650ee4e7aa5c0ff6a95732e585f4bf504444442415c229db11a5a54846a18082f7102a532a666ea4be9fc57e75c3ee77e87f5c53c0ce7861ffbf484727df233abddce87ab8adb127df02a607a6de4e750ef359a7470d1de6e824fe74be0ef4ffd2fb5f3afe8b69ef8712da0d1ec79a684c386452591ec701bfddff9737cc78510f2fe088bc50accad032adaca352bb2d88feab2f6dc8b574cc90872eed85bd3898fc33e5701238d882c992934c85602a0c10ead056c53f0916779cbb160f01df73166c16710921ff5819f228fba10852e9440e5dde56f9819082b8661007b0105763b4a721c879184cb86c0a79f68942229fd81b151e56967936df12a9ad41da8188e9bbf9913e04453b0a4ed85afdef3389e1f4ba730b291903e44b8b959ddd03a84d5b3701cd08cf0c80eb161acd5021f999a2ff4b5b0933e81ca0c986af37b1c50716ee8c5c6fb922f97ff9d49e9b73b21f5593a169f81fc98fa398c1b374f7f005b17021da127a3ea623e2267db6b221b6f0008ea7a0479a0069995f1557e46a0d9cad9debbd8cea7fbdf7eba97093f51ba41c7335a9cefab0f58441b960a6d580cfc40f1baf6213738b4d09ca2adfbe9b92f75f35ff17e322fe10c64081b81a2141b47a6bf78449994e2fdd4d3edd9db94f8549ef4d8705f894ea9f0db983b29b1e23a39e56d9519041b175b9f0a6a6c6dbd21fa260d1c0eb0925df74792f9cf4c3be92fb40e80100633bd832222f9cdff28d7e47735b4e2e4ae5feaf19a589d567f4871a91a5657dca7c2fce78e95109befed7bdb913b72b3fe89d8a17efa808a21d3eb99ac8a53efbeecabc5ddacd8f3d295b6f5744572a267160a2ffca30000000e66080ca0a0080098b1280d046dd058085ff62ffd597553bb96ae2e9c067468b8f6725c46b20efacf119c527b325b68fc3c8c30e3e45088ad45332bda8dc37d84a0fa4e9b49afd4070a42a35da8efde2ed7bf2682d114e9fdc382517fedb1b321e71831d2c2ce551d762b471bf59e0b7f8a5f7cdac51ffcd949110b0d57c368ef5adc99979b19c1ab4de4da31cf2fff2df7f65ecbd7c93d442a2113a29be2a0f68dafbed18c3b5194a8fe31a6259b4e083ec79b88449af30ffc4c5b7a794216bac85a8a67cca0e6e031529b5b6044aed7fedfe7fede5638f27acf65aa958fb2c5f6ce91be60cdd649e2a395bfd716a050195cf17e980d4991761736301ae34ac8aa9d52e84a67c83169fc03886ef3bfb2aecff314f85fe27d7ee9a54dcdbe23bf43629dfb8d24083fe6b1e7368ff8fbdb780caba5bf745e99212490129e9ee069194922ee9461a040444ba04444a69e94e0109e996924e49e9960611ee386bef7307efd9737dbe7bfbad75d73deb7bc7703898cf3f663cf97b9e39ff27312e2a686577dc063c438eebc265cec6cd417d06bd778d9b6cc221d3c9af96144f64f5cd476ef7f1596fd8b3ab5546f80b8744b80adec26c59ec139e9f6ca92d6d5f4f2c0ee00d2afb592a19f0cc7faa0d578364015a3d091a07884f03b32276c2b9275923e90314a9679573d1a83e0b3bb3db9db21f7651c4fa399afb359bcc34e0c31d80e4c3a88ca3acc6e84a997746858a7c77eb81de3426b916ccfdb714d32a68f35fd36d33305e19349fe14efa743ee330b3d5c695df3b375ebf388b364ba83f5bef3ff0e877cee21a37ea9609a63070895f1b1c01e91c68fd03908d0ffa64821b41302ab9a31abf475380d8e43afb44dbaf88695984d35248ab51feeb8377face054a47daa0b504b5816406a40f406d7fa7cf50e9884f7a57777c388576e559b626c8997ce628358a8f403a02b44672488174c4758c995f9bd104a736f276d9ddc63040638b0d377910644e3cfda61fefbe5dd6557c19375b46e4b3e8393f36eaca2f05e523f25a6f45198ee7a62ff8df6610b37025f9053eaa49712425b3f19630596810e63b54c17b49d96d507c3dff8242d379a6d2772d7a251f46a8a5bb49dd2a0d8beba39e4b19014897641f9b99a1bcf991eea7b87070ef5393391a4102976acfd7beb726676af47ab78bd160a53fe38a84c828881d30e960ea3e43454958c9fba1ed11f2b0623d65daee708ae8737d48e098066a42a62902fbfa6a3903f1da83da94009d739ff8b73aa7468dd613578ee22c9bb133b95f4928b7f55c0c7d3a9261600f40f23102e30bdfb665a48aee6ffd5aae91244f5972d2045a9fd57a60bf25116187e5e849cfb84968b6cc5b6d8dc4a770932211e29c089f7215bfb7d346f16b113a3e04cdcbd2a52faf06ca684f6e42d7a707fdafc20fac3d9140fcecadeb2a5f8f7b6e9e14e2ff22fd117a0f411b3e326ccd1c7a9cdab0c4b71f367e960b7dec8e5c1a4da0eb407df6896580313f65b93e7e837e864ea53742fda85a2f4315b378e6ed4cbda075b5c5d0386571db44431be85e104f9ef65dec5af5bfba2560dc9a766810d7f752cbd71fe4c7827cefe2e7e6b4b8fd300f2b3be1f216233065a9bfb84f807ceade3bead728857c9f060c6f93117fa80e68a49e5887d60706d900d0bdcc4ccd5ba8fae2e48ae8282f9167ceb551cd50f955d057a6e0f3863f4590e3dc09443abb326fc6f6f73d236daab44e8c57ad3f9d16bf7597b4e9fe57432ebc8ff949eff082a39de0097ad273a3345fd48d4f0c981cc2e2e6fa06afa2d46151aa8f1a34086b9e54121a7c797510d5fc2ddabbb1d041aaf224af671a1b26b9410f79167dbbd3c8e83266e2330c463ad06e01c606f2c741be3c486640d7119d306ef768d43f0f91f5593c54b122da38ade937d6471c9c9cd1535539bde39e8cea9f36d245b103b279a9945bdb015dcbed0db6b26bd7f60ae2b6b6fe6f40ba1d5abd5b4a68fac01f8fe4d83b4abeaf55e8617a2813c56bb9d85ba50bac052e9eb50ccc0d14440ff6ac4db9407c0ad22fcc7e73054496eb7d281edbea242cf53fb908c9c2417c0a9241506c00f29f41e308b7a73bf89ed9ed8d1513f676d8857ccae55ef71488fffef676b7bbee8397de82de4b0e5b12f107b7f876f6d34618755f53d129e47921fb18679e3ae582c696c27f69ec619bf23edc9f043fe109799c4c1f172c282eb314927183c3c68bb06616d4d03dd2594cc71479200bafed834b442feefe85e495a3ef61ae36ad8337681ca058085abf04da3650cc045a5f902f0ab2b5428f8caf8db5be5e960e0bf83ee4ad1edf72243701c5a6a0f19afab2e2e09cd01a5aa639d94bc5e59878652e9f8164a6cdf46165960a4bf09d847e478f8649e9c2db5721a0b59443a0b9da6f8e28118918c0f46cc16f1b3c4690f90f96639cf9dec4e11ecf03bf4d9bdc391fab2be123129df94897a3310e5b258745327bca1463824777bb50ad3fe3196228df04060efcb1011fb4be947974ac3fcb744379b5ad4eafa66108bc59f1ec34681cdfd81a5b860d0a4db27d2a5aee52770da57467736e69bcebbccb12e1cff8eafbdbd7c1f7c71a1fab551dd23d22953176c0629df254aee5684934de30ff79a8693354d614474b0c5b44e995182dabd9f0201be41f80ec87cec2f276e3d0f55954a5f85839f6737b21f1fc6ad09a836203906f019a53906d04e9d87bc20f8d46727ff0886cab6c3c8cbd155188a411c3a0abd6494d89fe01df26763686be912c38f09d17885f40eb01f2af40fe9af8fd6e615c770a7c9c37813fe145834df1df55cbff9df8f2bff0d5926bc36c5686ed7024fdaba66656ca4c8697f8fea07961c8a35c323deb63cfa86d65e55f51c711db50f8c1f8307e7b1daed2fc3197026f28f779b4c3f9713748fffdad4d23f6c54b47ffb31e3963fef082f95b4eca578d8565f73ecfa96eafe5db93c49185d80e83f8ea6f7d9649091dfec039c2ab8ba0f713f19901c87e80fc4ed0f36cf2b911560ce2dbbe2a3658075c7ac7b3d6489081649f9925cdca7366e7db3e9eea70895c04ba8cb5b91d685efede3c03e7e0ff6803f50f88a54169f352a479b60d1df51d4bd7e89d4f43f47a5befe52781aeab4c56b4847df68dbd17cdfbb050abf4b373844118489f36ef8cbea173cf5d7f22373cbf1433b3ade01fce0cad2e06dd0bedfc817c29d075eb537db01a09064b6e94fdd356fc3ddf91ecdfb195c185edb18e23a67dd161d77932e5127da1f1c31af3db4f7852c986c1f53dad3165452a9ce35c592c100f81fa577c8fb6b4966392fb3a9ebe015f55519f49d67e87f25d49031f6f70bae12cc67b47255f19d79887dfa0f5c76be1350f6a6c9bc692b9e128f41e189e929ef56082d61ce46783e60014939061a952774e644c4e087123380d671bc5348a8682623ff2f01a9747cf6e2956908b0f4adfc51319ea904001f9d9d0c633d0c6b520cc08dab8767236318ca12764355caaf40d0a750a66b39c9b32e65c454d140af7adf70b460a553fcf5a0b0e9c9840b6e2fb6565ef98a7c5b8ff3b06ed8fb9e54beb4db4ab20dd0ee283f0fb4a1bbe65c8826a5d2d2cfbde73570767bcbca05803c457209ded617855e8696c81fa7469fe9d08f35a7d5e4488df53be612f180a7906545f0b1c42e371c41feccf3841e3f0836d6696fd4021e1b8de566cead25dd6ff9e19e9ae3e1c8f05fe655ecc29a52321864f4fc68b1123105605f223a0d52fa0780184a9827c1a908e058d1764f7eb48e2a818dc9fab783deaa1a08d19c3e0389811d9eb5bec4afdd490df5d2e2dc0755c76648e1bb170babb957dc1fbe124a80863906c15db8e9f41bd756cab63e5309c393caa9d9d321d91e305199c61e5dc77c72b992fb078e593b742bebe2ed426b0dfd5cc9ba5fef02e58c5ebc09f82e47e9153b9a352cedb16ecb58b7c0d9fae04debea2cca19eea378c4bfa205ffe87a2ccedaebcdb0bf1d91a0dee1fe5c92bdd97d93756c8298e16493727cd0971d272ba8d3dea18d440720ef25941f81f48ce41eb9bd6c7396b2f4334ea4c69ace9ce704585d9d6e9fd4949576de25eaa76728df893c7e8bbcd065d240da0f863aea9ac9eb9cbd9c9d381218243ef3c35aede13e8ab80d617880f41a937d0e912e50a5e9d27bbe5f7f72db47e4a66299543326b1c12e865898215d28f5daacbe934c51f984003e120205f3e67587fab24398044a218f633819946562b618211d9d1863cf945c0bae0d39928c6363b73dfb31f9c203902d93290fc82fc1268c70b6a03f1c6efdcfb673f2f3206ee8017a3f89be75ec50a7729bbdb4313920d68632150dc087a8793cffc9245bc3f4eeb8b80219c23a550318c4d5c906c81627bdfa6579567541161d7d446ba4348b68862c32e6ba0b181f29220be028d0de4f383f22920f9056105d0fa52a0b97a8231a02c27bb93c0ab3cfcc5d9bd5aef3c081ea79eb8c243856586a40059a0b562aeae5f3f749214a4c3d286d91d459ff394b5f8bb3fe15d37633439b0f4eeb9ddb0a0ed566172db022ec4b8918180dfb5e7364f5777119d542c86639b77810457b41686d0a47ffa59e0f77bc66a308ea47c3671b852d5145e42caa09c034866407635202d51fc05bf629e8dd0ca2b5af932279b9e9c3ba075abef9f5aadf75e5d49aacf999456773a186c0fa601612320fe7ba131628695d89fc02890c3dd2ccb4ea2564c230fad9d065d078ae99639f0d007a25a0442bcbdfcada8c6b43d665fb3f795e15ea5787997ad600aacef37517fb0337bdbf9b795671d2ba9081cb95fe57399dbcd4795068e1714bb60e032efc39c95c0e8d0a0609b1963908bcf31bb35f9d02ee93510243dba17617ae81f5fc0706b192f887952434df2e593635c57b62b9def9ef60e1c6aa8143d839be839fe6bab4a07fb6dda8d20fb0bf259f9759cc9f028b4ec555787ae5751064ad00dd6fa41f8692562ef5a4852cb33a59ac7b95accc1dfaec4160788666bd78b8b88af079d90632a6a3e6c87145f7c01f9b1b37309db4dcc69699533dd49cdf509295178318691076cfcadb2859fd104c57ae819cbe173bbfc564076461c9d31aff489e9ae606832fa4294d5566274b20b176f8e51ed33ac436e373b91c6ba07394ded4f7440f17446526aeb5949afedcc36c7530d24c778ee284f555e8ab2454ad7f09df98ab96eec55799edb6e6734201f1894cf03b5c162cabdae718fea4ff9863f6bf4806a28d3f243c40f9222c20a14bc6d4d0f539ab6bcf633c127b6f7417c4af465407a472d2f08d734a2ed7ba2b60faa03f22cb47615e453b35a070827bd3699e6b02df5e4e7d1fc28c74978067a1e0843fe1ddf16a43b41ba29cdb8e455b2feed82349a4b1a87b271ddbe303a2d10065565ec86542df57ca53f2b61d78a2d0386c7c4fae477726deee7516edd0d811eee5c1cdeb4010f1f90ee88da82daa08d49407e18c8b700c69c807a0b90be07e5d0a0ada7e97da5bb40bd15a38de4f01d274876be5229e8d81e681b01738a921c578adf3f17d2ba57345b22c86a7f11d37905ac6770dd238d77e3650fed5474b0c11c15dbee589aaeeb342a0b50f8515cdcf939c0e271a9a2e9fd44fbde517db70aab8d2161fb6f1bc2d45a7ba5a7e59620de85364604c5833f3e3e20ebb1e3bbe2b576da3b577e53d83e1dc66959ad56bd733c7170c54e1f16a96b4e45172e15ddf116e67220366ded92e59e77cdfd73d1f7612f0c407532209f1f5ab904f1c6e6fd8edc684f75393364b9855002af17b2912cd5205d62cde7b4163443f8821dd77d4718ef4e86c437624bc16749ab3d9c072fe978efbc9eb33aebec108d4606d501c40b142b9ca14d765984bd2c6da53814217fbae9fd3bfe69fa4cf06b23dedeaac90a9718fb0065143c91fbe520df478912e934103955ff71babd7d7d007e11d95e601c28c707caf9ffa75f64f91d3f2f365e91b91c47a422eeb1edcf9f0a96276134d16bef5363ddedbb61351d4072048ae3a1cd65816221900eb32836d9132bf5a4f89857238a9eecbe225a61b10aad0fc7ad1050651958b0dd4fde378cb6089bd28b6be609d29d20cc17188b037c10d01c18c9be2e6f21bf1fd4fc41ba5240aa06fed3f7e84810be066a03d950d03b1433059f543c6179ad621363d5f551b087c214fdb6a75ba65c98eb47bb77d925b0c588948863bd4b9a205f1eb446d0e6a2cdb3e7c4d7d7e244ef163785c336deabf1c59ae700d9f334c7d7af3e1ebe27baf58369d322429815494a8510c4bb20ff1ee49b2dba3ecc3eea6c798de93f9e94f67806012ef2f414dadc13b4396650bc0f6dbc056a03d95fd03c0ff590297c32d5c43e1ecb8ff2c80fc872b97f94089a03909d06c92528e7009a7b103e096a0361782039527dbcd2e5ab2c515fc2e3faf66b1a1f23fb569b1c28f7096d6c0a9a2bd03c07c34c6f7397469e85e337ec8b053feb157649a82c9a925c3b5e0b25f3385ba6e871efd0256873a8b6ac90d2e2fce1948bd97f14b8b0dcd51064762909aab983563f836415b4e6d0d6d281785262284dff644556a84207f71d9b6086d37703c99d8a108b7b932d160eca9cadda06a40c68c5b987cfa0cd87160b5ad4e7c5e85661770f858d1adc8a3d967d1b09b2fb42139954291f74c7bd1bbd780287171de275b27c40ef60e7e8d360e2ff9158b7795b8c28189e4459a25b0f5afd0cc2144073206e359424724dc7c1d96563c7cc191912c28273098a6bfd4feecb4dba1adf8e783b9533fa1875fe1ab702585f028ac1a0ad3307b5adc505f419086d734ed77587368e522e865a77b98278f77c5c416d8465a961a0a029075d5efc35a504ed20b473d5f68dcc37238127806e489dae10e79bdb9a32da0e45aa161ead728043c4fa6b71e12c0993732c372f50ec07d207ccd12e652924bb16546ca74c58bb2b68048f8880fa6f664caca617ed9823a7004142af72d1e2b99b1f02504f02740468aebc8ac995661d16aad85d5e8bd379e8b5be18775a035dd730342a7e3b50edaa5cf981396566555002e3a311507c04c2784075c3a0f500e95390fc5e8a16d8b40a1818b17e6c395fb6637caa6f986f1027f3c9484ddda2982dcf5b36e0d67b49a27d56e0de87c8c22d9eb8c541caec2ced0c97eaddbb5fb624de1b88a0bcdbb1e291bf28a8dacc796b3e6eb97df8e6ca0c793f164fb7b0f2b9047cff7379e67d31cad9dc2ac2a49e807e368540d5772f5e1889d3bb602480703390cea69e257ad45f4366d01622965778f8b07d4f66e627689ee196295f0ec3bbcfd02728e3a3d62c7d8874de5f00c53820dc0794d304611e20db03e271d0d8407100b4f829c82e40ab77413203eddc837c5b903de70f51e68da9fff61ed60249f6bd779b9177297721884f41f5ed72aaed7daaad1d19c3dd362126740c7c8747f108d0d6cd81f6162c33f159e1c42d3dfa86dbf9f3e49c70adb0a08cf1961163aca7f8717c9ae89da9ef3b6b989eaf307341f307ca6982780384df83fc8821638e6243c2304be4a754ba66e3d1a8e885e9ea203c0c5a9febc1ab81c363b6ef5e0294c9eda2979f9e8b776c2c82f814da5c07a8760bb8cf01800bc06b8b3e3274e431d8095597ebb5172c4312e29b01f9b1d0da9e8ebbb82e16f9896cde9ee96cb973eb1cfe385db741f201baeecf6ecbab9c08e4a5209edaa8cf93786569c8497b6af119da3d26afce15c516063745c763611e85530c4ac5bd552207d58281e6006367135d48516a5f0e55b1b570de486145c0741ae47381e23c10bf80de91b882d43f4b3bb7486b3074654ebbf2d58cdac108c4bb66ae56b60822fa0bde308ef75ee67f87dd0baae074959f7a33a129ab00cb78d87f6bbe7c88f091f8f4efe4a3a09d536798dbfd5f2f077fae473d2238adce40657f2cfca3c926719d8b12dfdb5e868e933576b23bafa63a168483807c7410360cf23141fabe2a8ebe723e4c909cf7e950ac20a70c03c9bcd0c3e59d0f6ecba4dd8965aee9fb8f8e32f77261ead77f072f01cd4100d12942bf19f952db9ef4cbf3a64cbcd4f4a2dc65fe3b31e44f8ccdb45494d6d268b00530eee14780f64880b07f7f952e29ffb709e5972664b3cd6d6918b25d7709a0f5e1a0adad85767da16d03e19d7fb3473606b0976c7844097c9eb589ef65924036aa1393b31cf7cebd9431b77aebad836489eeb62098291b1172da377612483fcc876beeb44a527d2e75532cedf6509094ea7dfa707dab599a7701a52bf72b688d24b7161b86c685e2ae72c59c31169d85bf1c58bf01e96268f79682f6c580e61ee4f383e40d74afc2666a54b8716836d3a8554fa78a93a7a718d33028c7f73b7c7a5702d95009478bfa0da6b38d125960b2d29553b8aed48e1dff980829a3f96cfab33344cbb12b8dbd0bbf0774c7fb83bd9bfcfb6d2cfe2c1ccfd3c65e80780814cf2424449954151edc73b673bdfc84445a819fe0223d28852049c006cb92fdb92f2e42b945c70aafe93e2897fa3b72f94f69f3596d788fb72f01aba38c4fda5afac18af8720a749d4ce02c3df2f3f34f7a93593f60b6eb2b1dee530c94113e7da465a48d7c4732a3a810750205e116870eb4b205adec83629752b2b305d97c2f263c83ba78fa5e3c0aaba6c99f209b02d21ba0384f7f46765c7e01c148f55b4ed76382af8f3b11b622d85d7a2a8da4351bd442285f1b064e5318c19e51b9e83734b276179d3a289db13f1dc2e841af76defb1e7f864abce087c1155688486a60b59fa2da79e892f4f23d6bfee343cca15bcf9d474e1ad953c46098f20c69b4bec00bdb852e6b2eeda9ba20d9e992b8513a88b3b16532d60c5cd49d174c7290edeec583e6aa84308c446053beb5984cb7513d72b8f9eaa76112e83a90ef086d6d14e83a10060ab265a03668ed7458445f5bdda97e99eae33be20732acd518f118baef23a5dc8efc6a3314a8ef7a294b27a210188dee7c79f6d0812df18538364cc6f310dc97cee7bebc922b5fd8ef3d10ba680fc2924a0b5dc53520cb9e2a05bdb7292a77542eb9692d9571835d7dee45525ab6092368bca09809a4db998b05eeeca19b985362c54769c2a42add73e73b8316ab02f50f84e7b4b75bd0c19107c3ca287d132b21c5b49ccfd7865193b91f2aa77ef581d414d92c6c90e6eedd0c8e4f6fa416429f3e7d85a1f82a8e2be267e1e4c3d954944b0fbe67a23c9482eca6af7a59a2c9f4af6cc35c23aae065b37c556b9dc512fdf02e49e4248fdda941f116a8160ce4eb418bcb43dbf63b3aacea42e0871d7eb387de70bacd49e5e8b6e293c0cddfe98b3a5cb04f943fdb4f984c59a3a9dcbd9123a13c49101f401b47415b370cadee04d5f982741da86e1dda3a694a430b98f7d32bbcaac57c7c6faf65aa974b0a32879a1118635a1d095d455e38904cd1328c5ea575d044c5384fde6682eb277b825aed104b5def5e13ac92b99e192fa8f5aece2af29bf6e6e2fd5456f37b9ddbf71de3983bac9f52ba3d19b8cc544a432a02d6bc577c49b7eb5addebcfcc709b1bf6ccf6cd9b4ada00f50f94478436aff1962342ee9e8af1bca21aa375ad654b4396200f3f92dce9edd7b95d383a1673a74f4f765fa4e9f099a848d29d7e56ffc444f146d86e28b6d2ef18de5d64369727d96167f04113e7acad38e99bd1c8a2b266829ccaae81404133d8d2130cd18bfa1afebce1dd26bb244b6dd77b3f9fddf9107161ec656d716f3d73249ff0156599c18fb6ab917b5fe79b13680da78a41b9b185c6f6239d3dd381cf5f6b6131e4f67edcd9c03507e962a0ce01d48c81f854e93ae2ea4489f13b06c66a2f6d57ab7bd4e4d13b601cf5036152f3259a894038120d67d5698ffdaecc28b47249753bbf987caa9a7ea7a4e35444f033feeb3a49761466ca92e8657abf922cb2db553ecf92263d2b807180b50029e1dbf8272f7124a2d82b5c98b60f9fe8cd435b935f811f7bcbd2ab20989f427369fbaaa576d1efa130b0061cf0bc3f5baffdd5f657dbff6a03d50b80e40d24d3204c01749dba9ba2437b0686eff1d385a84f029c942ded39f8d0c65b6a2911cf92de88859b862d33549761880e37b0dc05e526c28369692da8aba76fffcc5932b566be2fb352d1016a43a2c30d137a5855c2e2b048d8700f262ea9f35900686f15c86f07e92b10be0b1a1b280683b63e0754ff023c87e0e101ae94b7a9f76af0cf44c6c3bad90be76766a038995e67aa4f60df9371f169c5bc6cf556b97e369d88d4bb902bbbd4b7be9d5b874dcfd77ed25cc2aa4981305a509c92d4daee1b7acd6716ac2f1b58ba656b9d27e39c046dfe0d84198172f4a0be80307d90ee0462af803905dd2bff51f9c9e847c2c7bb47b745e73dfc39125694ad413e17687d413122d0671d0dfa7ee1fa13872ebf9bb043a5339ff54e802a0807f15ccf329fc62c39c0269bcb99ef1edce0ced95c02c54cf93af68f2adda60a9df6babfc34e4a37b95e506a836221108f83fae73a028f4cb7c2bc1522336624503717224152af0df2ebfed2a77fb5fd77da40fe15c806407bcecedfe4923a18dd6efc63d73259d9f36a337bab4021ab09b7863e0ceb06de537b25537292d91faf7e76b1d3ed108ba82a3b2bc31d3345c93e946ab604f13328ee06e50340fb11e3044edf4fc43a5c23ebbd6b64ddf51bf14bbd25fb676350a0fa5810c60d8aa7df886aeddada08316598d2769f139c94286bd196836c37b41837a8a61274ce1db4f80bc88682defb971c81e508dafa0390be07e175203ec87977d6365bf1de64c881694b7ba0f411abf2f75568cf11f96bddfefc3668f50b688da0cdcf80da900290edbe69ab50049128f5d7f69a3f606921a905f95220fd02c24640397a505d10b0d61eca3a946ba615f468c727628e0f45240e6da3f1909ef1cd819e073a430d14f780ee05ed197d614c1182466aac647d1765baa2c94ae751d12c25686f3db4b81e28070eaaedf9b37340d062a5ff4af23177a25413f28d574400cd9ac977f5b8c77767d513745622a84e01144781ea7340fa94b5936083339bf6cefd174b9a6c2756dae4158d57a0da993cee133d6929b3e4f444549eefdb4eeb4793162bfff279ba3f995fa03d4b4ae113b35ba0d983671d423a51ca3423bc01520d4620dd04c29541b129b47e4e462bfcc4def31d1f5f8cbc070fde9cd38cbf988cfeb3c7fbcf90b77f253ef8b3fbfccfd04dff4a730fb2dda01a11d0be2c109606b2dd202c0364a380e7ea02ce8f009ecd02b06539fd8a1f1e5a1765b931ca04bcecac11e499713c81367f09da1f056d8e19e447ec10c79633d624b4128970e69717e3f3e65b5a61411bab958dc16df0ed18281cacfa5ed989bd3f78cb3ece089a6768e350d0de87df39af00d456b7ee1841162a5a23bbb922ebf9fcc80fe10c15a82741b52ed0d6cc82d61cd446da50c7bb1bda20b49f9dbcf4ec91f0ed1e6b84c8dfd9ebf167b781ce4707b5fd2be9d37f46dbff1ffbfcefd606c281a13dff1494ff00f9c0207bf4afee836c2eab6c1d6c35b848e6da6a0e71b555ec5c60bd84765f1bb4e79780e200686b35406da0f782ce8802e58f40f5eda0fdd3a03668ebdb41df1e00d94150fd2e083f00ed1183f65c6268db8e235a76c7c6184e24691448287654b394db3380fb8effd5e51cc41bff5ff50574560ec88f05f129681cd09e2f0abaee6bc6464900d544d9b982f7fbe654ab579287ddca91247a0611861c84b673492f7e3adad64aac1ebd9140203790d2f53811ac7b12716826a46158aeb75fc40e4371bae6ca5fe5a6be872bf795ac9219591059a8286ec6609118466390db9b887f972c4c357db0aaad826b63702c1b35257bcb9b55f232a538fe2fdb036e03e958908e006173d0ee9982f6ec63107608aab587763f2c28e70f6d4e04941707e953500e08b4a74bdf2731b0809021816267084dcce4d9938a6fc601a03650eefdda5990815b3377c7fc1d8c47d510b1fbcfa16a6bd01c80cec501ed7f9b6acae1fb26a23d33fbc5dc6bd157d02ef1185e0ab4966bad8a8fd6a504de3a995e7cd4165c4ae3adde0fadec963e55f5f777c74c478eaf30350b63363b1d079d97f167b78178c3e8f19c869868abeb9b75ccbae718a2841dcaf8c7862a1dcd2e14acb04281d6d7b05a4e518a238f184078e2dfddfb0f857f003c0703c057a07140fb6d2cd0fe05d4b8b7058c9331936291a9a6c9c1bab94293e621207c0dc47f204c61ac47f26879dc6853c4e03a4f00ed2e7261a4943c88ff407300c214285f65eb5a99ed27d40659e9b7488a9097d21ff0433b2f206c04841f80f60f82f66680f6b581f044506c0fdc87fb1bbc0b3ae31c641b413536d0e612a0c55441b52ea033c441fbed4175a5d09e277dae3175416466fb5a85e8a77d5e9d4d793caa18dfef7c6700c4a7a05c11488f4fdd97dd09a54d1e97488c9fa4c47cebe49b4aebf03b318eafbdd3fdfb95c9df6975bb6df303b01a748c2c36407300d24dd0ea0368f71680cebc88335ca72de5f9acc0d026c05fbf78f5dc043b6406743e3aa80e19b4870d544706b2a1a03384fef2b9a0cf2783d617886302640184c782cea484b67f77a4523651d4dbeda77591de5f616bc8d3b5ee8783f6aa82e403dab36840b53da07801daba7f90ec83da40e300e1f7a05a0d68cfe301e9713f6d99f761fb02071de82433db158fdeb631eff8be902da00bafa4d5158e0d28081ae59cde60205187367686f64c0ed01a399aaabe4b3978ae4dde9cee5fca8bd519a59c3b06bc1780179f7fa9c961555a449b5d29080993f802db11e7a00ef2a586cd1d9c323231c3292e68ada51f0f4a171aec9f82ea0fa0ad6700cd33683d40be32c8ee43abef3fb39b72971cbab2520f0f66defb6cb0bceaee1308f23141fb3540be23a80d54cf00e2a127b04a64aa6d17f6bdb0c285a2ceab4432bb68f8b748f6e40b957169ca608fd86a0661c6f2ba0506a1cd5780fc49e037aa00bc06b215203e00ad07c89f2c8968cf150f81dd56fc7a0b5ebfafefd0c75bbe05a423803522005f0524bf72e59ced43850fb59e1132af31b5493c0b9c283100c592d0f2c6bd80564621f98929fc7c7a93be81cec1ba2b86e644f72f75363d17af075a09ee6edc6a8bf40f108c46f4191d26aa26ad7d794210f06e50fb2bfbc10a1eb467bc81cea102c5a6d07ec303c45720df1bda7325a1f555a0fd7e23484740fb3d5790ed59bdea71957391c171756a091d81df8ab311cc7402cdc1ecb350b7baaae734d4a5b8e6471dfb2cb5ac03ecc0fd7e8847361c41bc8c41b3c9f0ab9e4eeb4f5cd20841cf03e953502d27c89681780374160d687da13dfb0e246f0e0332308f0a53466f7149b6e4c6b3978d638812a1c692db5e45ac6e5b915f2bb82561c42121fe50f5f03d380bc3fc86b910fde2bdc5fd60cb64db8981356c33cfad99098386a2342fcdd6ec1204147564683146d0fe5ad05aae60e55774d8ffc04b2ebbcc21d34d71c1f4a5eb06cda9e996be7904f196ecc8f1daf7476afb9d346c9e567454e3da2aeff24ea2717b5e6c19570a166f485e30be7be04ddd2eed5c2b58cf28d0ad94cb3c5af810e74893fd09cbddf7519c1c8ccf778f29251be73091c2ddde20d75546ebb78f92274d1538b832e200bf7b03c2ef4175325c7e634c0d530143e1d7b463c801156fc48e05faa0ddeb068c49007809b47e04287f0e6d1c0a7a1ef01b5000dc6c9690718c9d594a2dfcc5ada7031306affb2cbd36e0ac6d0c5afa99f5dc8b994aefb4b73adee3369602bd03c4bba0fd83a0fd151e76b0214831a802757d470f2f1ba716c40db0d940f206fad62a085b07f138a83e1b84d380fc080af507df048d48e7c312068a15ae0247a515f2e47ee7bb73201c04849582ce5a03c588f46149710ebe2ad75696053f4392cd8236f0df9382fc0890ce81f6bb41a0353762afba584ccd3a9f32dd6c7ef05a02e5a3096fd52bb99fa7793adab8e7435acf52fabf4bf0f453a3816ada403e0da866f101b758237ca827dbac600fd987aa8cec34cb0224902cfcce5e3268cf6700d95fd01942a0fd47d09e510b6dffa0b5bfd09e1907edf7997e671f1f88ffa03db315740636483f03bf45079019d03e4890bf9697a5e6b9b4bd3abb341b55995d7c258b2048d105ba0ec4e320bf09f41d0ed039f4a0f3e541dfe7fc29a09e78f458909061ca598c5c49e35055fc2bfc56588977df67383f2b02ef8faf6d75e3c2b3133be4bd1b781639abc5875eb37b857421a28e470f7080f805343610bf806c0a513997945e540d1b3c6b9d6a71dc8a4baab961d5dda7426e19e86751b9cc54d7c9b893f8d1bb9d3620de00d90a68717e90ad05b581706a505f7e47b76b840d3245ea47529209e2c70c2c3fff81ef7a0907fa5e3ee83b99a0b807a4eb40e7b98178c3d62c4b6efccb62de8775fccf84b3fe3965f211bbff8c36500de473f8f34601f1302203be49e199282d973a45d74868b16b68f76883ce6c00f9f22ac2619631c383038c326b6f25380688323ac50e407c00b2bfd0e6c6403a4c543d8abff5d2f35db1fd46e5d0a9f2a363e51942100f81e40db4e620ac19daba2568e71e741635c8e681e6efefe63afe0f3c0254db08d24de97186e1544819f64e5eca0963997e594afe28d3a0b902b53db8be8573f6e4fbc4f7967ab3b6b8071e3d4e63fd201d0bb26fa0ebaca4101d3e8ba6ac2cedd4b3a93461e318a0673083eabe40df2201cd3de8dbf3b0f3f61f335e858e37650b328a929bca3689b93ecfe8db5e3cef9c212cec1740e788a272641bebb7471e59ec4ade2218d198a05485f5df68b6bc7a42033ad3138449435b970bf28b41e300d57d817c4c10664eb5c45dc26b8f92dfba3af82aec2292e00e523a0edc58b0ec80f09db34b980037ee58984038b55bcb303ecd7067aa3e01b7b65a61026fcd8f8bc0d448e423267fbaf9d2d3f6d9e9337575f8e28ad507e668fc22f507e722307ff0f36fde7df6eba72e256bcb5331cc97507aea1dbbe88f26efbeaac4fea3a7d27cda7ed37c0e17e113808435c0540b17f0827f880de68f7f0ffe980c6bf88bfb57616060b0fff3fffffdef7ffffe57fbf8cd8b89bef2959418c3fb88a393a3d92560e81ccbc012a85a84d24c21df4662207ef3b3312cc183d726ecf6fc7323c3598cdda4337e2d0c18c445bdf853c7e5a6dbaa81f4287a831b1b99dfd771e2e211e1bc69d929773fd1903ab33c204922ef481ae5e374cafeb959182b39ecf404e7636a62edf995804cba41be16f1a4400fccf48a059e7861475d855acf97a65a110dac5bd1c3452f6dceee98e1dc21e3a14c1071c7930b99f1f726f57d2618be827b9b99d2db4047d0a5504e0a9e8618e67ffadb81ff5f038081a140bcd9aa4670659deb88545fb577dbf9e71bec774d28075c30303068505cf3473ff2fff88f02b128cd3f82db6bb54528013dc6f0235b289eb2b7222c0c2cccef31030cf96b53ebe811e6056299fee235566eea6d718c57bd6173e4f1dfb08d8bd052557901f7c09d39f8f8e37f33f4d74dcdae31a5aa719abd4975ad257e512f70ab76800c7fd735d2b1c56662271d4293c26606e16d7b9169ce1b89afe5eef8cd7abc5efe7bbda3cfa0f57a314cdfd37c0ebbede34fd599550bbbccc2eb8bf79b122071f32fc3953519e3af291cb4de020e39e76108a7384f69fe3bf45ffd7436355de2ded7536bf60587d6139fa351a52446fdd7d3b60185aea01f535b0609a3cfdf7f1953cbf37e3643a4ffcef8ffabce2191fb23f2f5f5f535cc6ffd607d7e7505dc397cb98f3f9eb8a13f0332712d7cd6671f17fe5fdcf28b67c2fd42ebc1ffad63bf31a6e61b7f38c307be61fb6ce3d611447f18c3c58405031b5e8e8008830a8f8482800c0bf73f7ac1afb436da1f5229f2fafe985e52f7c7f4caf53fa6d746fc823ef09bf4c9dfa3d707ff31bdc1e017f38b70af0595ac8ffdc8d7a9e63ae42146bf0d2f9ac74b1c95a96fe29f894fb83195f075623fb448f118f0bd8b9a36aa1ea8d1c99c3893254154e054525cdaaebb3c9c401b167dccb8a3e506bb66831d032f1f04d3ae891b230bcb7681b6f2d352902155432fe0c37a3d9e686dfa0792693f54fdcacd3ef41dce12780cc92431f92f1ebfb25708fafbb33bce05dc82b9de0f9ff53368263f5eb69ee65154c145ce8d53467e61d9ac98cee3ad2644707b7c9884b3a454d9c2ee144d0cf32a0195766976625ef1fb12d78f94cf07cb081bd2259de85daa5eb3780977b3df0693fbb7b23fbde38c896a49b0b84197c0ccc9abfee6fb29cdd19974fbd75ea32f74027558ca01d7d1f9d999cd70e708cf83e16f874768389afadf8974f6a7226dae4530cd924f5efb3ddefea5c702f19bb0e942798c382687ae436f49ccaa93766b4dadecffbce6b2cc276bbc2d31466d3ab5b61e5b7ee28f9e676ec3c4f67bba0746ac2138e24eabc23291b9dc81a7fd832b96c74da84fce3f9e315d2df7b9be1747a0795c73fa73bee4b183d9bbcb914a25970c49fdf6c903ffb15947d62692522c3936e2df54b8d03919ffe3fb9b7feffe3f26ff7afee17ee9e4f0cda621c9078448ed5799cf207367aa6d5577d0e8f7472763a77461f3378eedfea27fe4bf377f5410f303b7e78e319120c0fd33e005a2c86c492c06a2fadbdfe12f28f853cc405d421c6d6c417ced987c0a16abe013b5cab37b78061a811fd4a398850dfa8c6e352485663ea98bd195fe583b6c1484a2ac707782febb35a10eb667b1fc30162bc9eff19f58ca4fcb282b3dabfef1db4544fecaef828a4d44859fc60e958a449f3cccc585dd832f7c90f8366e6b6b6c4086d3dfb7703cc76042bd4966c06cbe41acdedaf26aec57ef6f3e47eff3f1270f42ad456f97203b8fff4d7e37bdf947dcfb585f856033c169aaf3e11341ceab5ae368087d34dbbc29c650c8a7db8cade57da8be9f6ac71f0ee11fc58f77ee3a0e9316f82ecdc6d0c9bac2ea63cc4138d53d78e4eb776508f92acd66d650fb827bb0532d2f6fd2ef9cb41a8d49acc145be90a5e84da64f3e13672ab849cf47617de3a911302af3ccef32085919edf9fbaa909b744411388fc37b917ba2ada1645b7ef0f561c5cdde37e9562f9be10987325fef1b187c156bfe88f3ad9b94e8269dd53ec652f77042902689a9e13df6d587c94376889015bde629f91efa2db3ccd70afe9b4a1f82a52556bd2098d77141fef398d7b785f87e162979cfe7ba4c7bc837e975e3ce399395f39a5f92df08e30d50e1eaa9206fdea427e23c5b59e8691abe0cd235cae2b88b335586e07c93fe8a9ce8224d217d393d98220bde1b0bf9acc029eb26dd6ecdc3147e789b2509893639f30c26baf77042e7267d4e3f7d71cb284644b0a7fe2d6fa03f9d019a812844c8eebed1a66368f184c99b5ab4bcbeaefef92751c69b741ebb89c50c7521328adc67d394227c31ade335104041a34fe8f64997f158f919291cdbbdc4be332558087d957c7bbff969fb8989775ad11381d8366c19a3f8999bf4cf3104a286348ff930329fc3863ab18c12310c94dfa40f3b2b347fd1b3831facfaa9576f19903528fca5e8265df9d4262d408e7f8fabb11537446c45c0eabeebc79bf4c7305f6011e7abab2af468393fe34e97e31b6f41f44fdedf6cdbdd466000f7e484ddd6f3dbd5e8ac6be24dfa3b9ea54e94efdf82a4684eabcef8452d92afc9ad6fd289e1ce70ac7d91ba751bde6966a1b28fd2a75441d85b77fc9e6fa14f28de62a0ce3af6e667cbf85cb769dea4731f0b977ebdf7dc6c9a59c7678327006b2f8c19627e7a82053a9cd87a732ab45b7ae02b90fa9d08ee35dea4bfcfe87ac245457f45afc4f0a6128f50f9ad14d2fd9bf46cac4337264ff1f2f5fbe696f40454159c9f4e21e46f1f71f4c3570ea5d2d445be4a64e5e99c30f40188e77bd08bbc58b9370acf850b3340315dd333ac825b7b93de143ff5f3dd58fb709b8994c0c16c15715bef88d54dba64fa9ac10987148798d0531f1759c188ccd8560688fec35f6849c7869c1a9baf739c3f836d2941e982e03f0f9eaf48a7f479df92cca9f962ac79cbc3d8062162010fd3251941de17b49e7cdd54d88e6cfeb4acd210f15f2c077c656272388d94fe806fbb565afea1e1c84dc40766a4deaaa435e73d734d4bd7c32e55f24f06b09e3f6fd22d57da2c87670c5cd9646a8c33558517c616e06e41b8d6b4366f6e453ffef2b576b94377d3342adcb4920c62fe376534edbe713e807da7ae1aed67819a637bae7b937e9bb4f7c44ac02ace8d9a1ddbf5d6f7795dee4108f99faa330cc5a827213c71e7f6eb51fa1cb1a4f3d1f1265da687ab1d435c8950fb438373087f1e99bae6e0d94d7a58800b9dce59de9d7eecd7d1b38f1766e4f927852002abec2ff51accd9aa1ae67af719c91892c32a3e4344704b21d26a5f318b3f2465f8fb7d60cb897fcd371f7493be413d7530c0b32460adcebd23abcf726ff5f5ca6d0850a851e60e427cf8a7a6d2366fec57ef44ec8948e16fd2693713ab9c50759607aebd449c58703562eb10f321ecd34c494f4dd8f2b33ed6ae30f2c70a79837a0b10b18e71d6804c790fc6f7c7c776454b83301f1e4b638edea47f0c8ae772c98a0e7eb965deaa5f0b1fb80c4b0711eb3d4db52c65e3a2cd94f5a978f10c7fdee223626515c4fcfd6814ff72c11471c7e8c32afd1575c054af3f84fc8a68f4d493594b5df820d69d1dec36b95316e012dea47b21aa3c4b1cf98ac1d14e4efe78d365342d19ef18c258a3a4f435226cadca236e49aa2c231b456ebca5bf493fde33d949e5e2ed413879941c7da96e2c53ec5973939e11f365e3fc076d4218c3bd506f2c2dfbe0ddf40488f14d3910af6317ba918c60325133048bed977adeb9497729153a6c1a274f3736dea4f5bcc7733fc8f09bc34d7a04e9e320eff3777c3b56f0f859ee3be24b6e74e637e9039ec7b7f54643f1420c74df0637d7614aede3bdb9498fc27038376facf5e5253059ecbd83f2fe43d187a69bf497082faeb77cd8d24eaebfac1bb051e16afa15aadfa47330dba43faad7b9638253eb8847451df63941fbf4265d6b47ba358ac234256917666991155e7915f9551a04d04d3e5ddc80554a3e2ca84343b9a37bd65570ce72932efcacc2d6449bbf5142c611db2ecde30d2a499e2a847dc94523bc3bb8585dccf90413ad2158fe9e49931184ffd1e63c845c8c9d727d68ce6e3abc76c872670d02f4bc63401c539dedc6edb147e0e895fb96e6780b65fe26bdcb00c691afb1cb29d8dc83ef950ba55f0aee3308ff0e1669d6c3ef29c98e0009cbd866545a74bc6f0f047faf9a15d468a298274e9fd61f383ba26ad6c22a2e423cdf68cf6f88f09a3b8eab0afe6b4b963a3bb931847f17707e35691a905a9effa48673aae10a4f4d4b630f02df2b21f718beb738ed985df2ad32ab3557fba230ee26fde8fe9ca45cc4a7722c0ee35849864f76ef6dbcdfdda4afbc374ab42c8ea6f45b881189db4ce24f110d80909ff6830c0b7db358d92f65831cd2fbc617c832a310f1c246c0199570f2244ec4308ed4f9c1def637c7ade29b742177b2e527c66ff1d7098b95d7f8be2d6d730542c41385fd733bef6015cc7da413eff3851c34b3a2c857dea40b48b3522e9379afb1cfb07f1c7ffba3b0d82e1142fe3e76b3d0c9130738380cec9e47c5ba9314abc440e0a33f63f284ac964fe0185e1e9d94948e45333e5f80c03fe7542be29a1f627ea3b2ced9625608a0e51c647a0c01e60ab4c1920933868f297486c6674ff79de2725643f80f0c79f887b04af1700608bddf60edbc2a3a9be92062fdabf58d987ab1edab8c250389631477f98d915808f969a3b4ba7f476f2168b1f4633ece3d51ae616908fd6b6abbba2062e3d0c8f4126e798e477ead43c115c2bf1e11947ba3859dd07b7a45d511b16fe39422420da1ffa7e62d7699b1d4026e59eb479015770fdae77942d8dfaf9694056c5caf199e9d2d2a475aa2f62299fbde3d47380892e0917f5206e34fba9e558bb03c445539f97ba037c27f01512c694c1b30a7147d9290ea49738e2d85e2f24f444224a346881f3f10b84f6e9095443286174dd04c9a811e89715c528cf9f79ffd1f5d85701a8d74a671605614f3eeca061e987cff2cb7265cfc8b201ee6f7827cb8b45fd07f015220c0fc8bffe0ffc1cf47573132367627d734b2b1317bf63fb89fdbcac1dc59809555d956eda1a985b8d9731d1d69096e6b6d6b770b4e5e23710f453e7e7989a73c9a9a8a1e8e462eaa562a728e5c4fc5ffbcfeff2ec8f5e03741ae876ccf4db8f9d98d388df9f94db879d9f8f9b9cd79798cb88d79f8d88dcdcd78d8b8cdcd8d38ccb9cdcdb84db8d88d78cc8d8df9b84c39d8397839cc3838b8f8d9798d7e6bfc4866aeb602ec6cff30fe1063a829e8a8dca5725c4c604349447c1d94b42a952b574ddbcec0b69d122f9a7baebb935bcee31ac1128e7262482454211d305a503b8f751a35cf1d898ccb602fff2b10f31c563518514df894bef93fb41eac84ba127dce6faedaafa059583171b5478f150d54d59594e4b50d14c5d41e69481aa83d969354fca7701d845606d5ab41848a6fedbf3bb59e4edaafbb3b3b54e63da8f260fdfb02f49f537926e7e34f92465e2381aa9f5bf1ab975dffe2f737988dc4c6ec1f00b329c7d6b6532849907b8a6d05a3d1d010d3f71e438449560cd9304752f04aae0f1d6b5c7ae91c1a5b26256fd2dd266551bb3def45365d0d29539852981e72b89442c040198e3edfb2b107b47395f993d2d5ef3e1f1ce9ba499fe1b3777dfec201cff19a09b985d6792ecbd812c24dc43fd577f2bdbf961fd87ef9cabad72d3b58ce0122ed9db56675fb09e546d13be48393024901ef798acc4108f52a664c4392241a69edf4feee5505bc6ad2e7a1540818f7ac8418a66c8eae3e4aa701cb76246d44c54806623923c4d930c83a90f8ec0da78f9bb455724a82a46fd29795103922673288ade79997794cdbfced03493c206032cd3d36a2a4dbd430554a1b1bf88ca35d2fbb4921c2ecd66e74fa49374ba43a943ec72ea7d78d61d4d137e9e5f51b1532ddb3bb43170d22fe0c9132f63aba0b10ea87d3f707dc3a81b65103d371765abce28c5c18849bdb62165c50f7088f076f309275407f9997252e1362fda82fa9ecb38edf575626b1b0bdafbedcf5c0e0808001479352d9d86d47f997d679ee33054fbd6173f4dcbe490f62994020fcc85e59157d0183360783d3dd4d0201234cbfb44130c828996b23537dc0a4e2c74482a9e577938ec3fb632d6dbbdcf30b56c6c86bcaa3afeba1141030a9e4f61be19832bb260a69cec5ed9813ce26ad0608fecce30dfad9ff086121aac8872f276459b9d668d216a2ffd39ead4e2930f5124e8d638804dd788f4c183a21dc50ff8ba596fc985b58d272444ff059a345f771b56ed2f5e42d35050c96ed8b8cce22ca0c9e8a8bdb9740c050195f9c843f87f2c3d1937c9ca676d120687fea0ce18686d6926c7d70e0946fcad5a03f5b79e262ae150311c69a8b8ce47f1d4188377e6a8e534ceac3c64bbe3a7d93eeed4a925db2e00bcbac359419fd78d7873733f225040c3276f2291ceb9178042241d1e1524c583fb22ec4f8325bf8b5c5d4705657b1aee23564c9e5671515576ed26df05ff854c05df355c9b7cbd525cddc9159ba8480696a1c08975cdef256c35521f3cac7e3ac7eb27381d08d7e9ea91fefca3ec3e5fca9a6bdf206150159107b0302666ece2e37138f11a8167868d44cc1f3fe432de7370845ccd7d6cd4a4a5f7fd132aa5fc429cd6a17a8ec060173f687a2fd2425d94b304d28493cf49c98d2e381809936cbb2f3b81233ee615b79658ba7dd33a0365f86d01f443aa85e34b81d485a36f7ca7b87e1909c295a2060f4d52269f90387228aed2bc300a94fe82e8d6d2910b6cb4def790d79045bc20a2a626f85b1814ecccb2c88306d10334d8dde43c6d0839ea2d0c0d30f8fc42a8ee926bd63b52ee4a18fded4886b6dfa8baacec7fc4f312052f6685ffb6c4594753d7ae4364f088ec2e55e8b1b41c00c0cad1bae1de6ef241fd7f190706f7f127b785c830ec13fec99bab6185bddc6022d360758f4241dd7d6e137e926122b5fdf7c3177eb2837de7b55bf1ddad7410e0133c429054416debe55caa54dde60a55f8d138b3a1676932ea572e971a7c38ef6c45ba083bac4dd402f49e6fb4d3a5605af140ecfd00ce54b41597c7bfcd77dfc0510f3e31379ea00e3c59e178de945f478b2a64c76ee89fc4d3a9dfc4be70d47d6a9f2049afb85dcf50d349aaa1030e093657ccdc97d1adfa4637dd62444777111ae792508fbc2fb7688e57955f5305367511dbf11d542f6c2fb9b74f1d2ca6a6a9fe3d3301146fd820b61da2f5f3e43c0c4f95d51cb3fbe2c68956a5c0a7f79639a73af0b8ee226fd1bc103292e59bf979b2d5ff1f6047fc0e55ed042d87e065446e9e4c0af926fa72c47bb98301e656a7ec482e06f8c3187f44f4a4ce117cdfebcbb54c50ae38e10308206bd12a971be3e958f88b6698e368cbfad18a46b712b20635f99140d3d76c957bafaa83233f21b650f843be1286244a03054d7cd120b5b37e03564465107313ebab47ac260854cc97e9edad6859ee0d0b91e1a08fda5a6bcbdcc64bdd5fb50d2c7753371d6675fea39040ce2d9804e5f6e7afffc56ccd467c14537ef325c6f88cacaf7b37ca1df15ec2dd89265761fe590e0a38c11419431fd68dda86e1fc0df3952a5bf4b10ee6698168e0d01931dc55fb9cb5a60796522ab335be19fe6e033c3b442c0843654130dc30d2a7409337e5ee3b65d6bc39710f3bb5a983c6e3238c1b1e38ed08ecb3faa3df37506a27fc469398c76d7978b9b5ee85564cf13b71bf15f41e847cf6a6491d79904f629c9d113e9c2fa0ce626ebb837e928fbd6ab5c9e8ca93a6e24c69358d1a8973a022937e9aada38b21425b70d346d28a55399f649a89c308e20e4fb730b1d71f32baf5b1def8e24491f092ed5265d40e8cf2eb8678d7ea466ef75c77af024b5466bc45a682160e4cd71b236e5e847b7cc188e0a1ae72e6c8b4a4f6ed2313417f78b460efafa04273c5a9ad71e59b7cf4144918ad5d52d39389458b45b19454ab58b391d19388737e9ebb82c23e942dc8fdb90bc245eca73bf74432c780a9126d3e71e8debe21a473df56c39f7811727c8782f080123d972db146ab8c88feb7b09f737795bb1643e6d8748d3c62f8bb4c7de2ffc98af5e3aa8c2e57f41740702065ab7e8cc37b24b564e7adba49b5be5a5b684190c91667d97ee9d545527dcf833a217461855d16cc03d0ba2ff72b5c4acc3db5ff2bfb438b6106be45f15207640b8db21cce2d2d6cea1b8af51fbd6d5b17f4417fc7482483305a7f7cf23d3b1ab9e0f953eed715bf1b2f92c507293ae2df24af18bbc05d3265efc290b81bfcc835b561030a9b879a68b0cd61666877038bb03859e820efa26441a076506a3f5db28017baa38e683de3befa8553e694394e57475127f5b8147fbbcd5589a13b731d652751f5d01c23fa9b88fd7ede9cd6252467734d14482cec59a09a15f1b4b90ac832a24fc3e3bce2d28900921f77bd040f8772fe1d38be807443f92065eb0981d34f512eda00b40c01b087cd55c2ecca9649f1a9fc938ec3f5cbf7b0de19f115b96a95320a1229a6d0e9b578973f4c7d7bd87b0af0e74c4e4fcd8820f4e299e250b5eb87e95952fb2b949bfebde4ebfbd94b380c1a05b9f23a4fe9d3cf0bc17ee9c88ddc71febe1037ffa5721b544f7237caed17e2fa621ec8679539e63a519a5f8692d6f9e5a1efd9ea4b6136ff2ef45de70ed4f5d7120f0b1fc739c9619e280c75346d145828fec5222259e7cfe4df4e401ccffd53fd85fc13712e3567a107919a42734774a5e1352e01c2827697d0aa00bd5b6870818b58285b3f569844318a7be0fb6e50e06917a4e3fff8d0eb68ddf2af54a2250cd44e9eff665af2f8d593140f276581565bcd29cca97cd127971ff444e1516db923452c8564461e1c3ed5e7aeec97c43bb9aab15b3ca8018e714625b1d1aaa9e396d981a8cd1f28bbb9f6e53a7be27c194754d51c0f7720ab745b7b74b87b5c22aa4d0e19e23b4770b092ff7cd96bd972419d3a2ffad2b3440b7d18a58e5b159684085a596464dfab011b9833f87cebbb6818f467eb6b16749da0b83ca8838c7f25cb7a4ec32d14882fbeb7c2bf05235e97dae760b8ee09f277984678c93fdf108ff128cff3b04c34078ea2bd5e26ead67e407ce377cdf4328d9a474fe91824191abff522357affa442dac44f33b1cd1ebc7d91fa222d0b73b095c4504ef5a3211e69d447062a8d9eccee6efe7916a78157ae523904833ef9fd3367b55113d3d2229ba7337b52ea8cc0e379c3dbc3b3dc329cb0af9f3d3c1e091ab8c99455fbc0609caae950446e696d73e9ae1ed1f365b1ff813657e7f22252b158803cfddc0d879489246c729c2d278b69fd9ee1abfd94d87233f971e7024c6edf6dd67f987c6bbb423aad0207114aea73e294fabdd163ecee8f8ef04a37e0974b1bf582467930bd85423ff4b30fe1d0463e39385d4704bdee8867863a0b842a8b2fd0164e5e79f2d1833e58efe6f54842e1132bc746cc48895cd8b9a4a4f11b634aa4ea6db6d2eb20df475fb2a83b045d2aad9aff13ffbfb330776700ae1454c2920156909207e916f21e69625e03e6e6ee1694af6180bb0fe885c709253ed5a44f479a0f31d851c02753e6a98421eefa5db5804bac8a80c99995c26af45eca8a92936ebbcba8d8822cf6392b9a3798b29ff4fe3c80ff58b3655e0cb1e29ac3d40e62d26b94fe1f35a3c7d6e520db6c0ecfebd4adf931d6a8266c3dd54f95779814ea6a6f28275d4634ddb58347f09c6bf8560041bc15ec257784f74522cc494244a9f64a83846fd230543acf34b544115bb27b6fa43cba93c4c23d183cb87b0d1da1694578eadcf67d228b034d1902e76098e904af2ef5468956bb639d66122d2a0440d04325d1eae31899014dd19b9bc66fa2c4bbb7f1bc15f5a26b6f976cb689c6baa5212bf144eff732431637ba10dd241914f9df90a076843df4f4be1a2910927de10602691ca888a78b7905072165e9f327cb269c50cee46691ec1fcb1b61f6046ad2741923832f669a1d2052ff2eb69c9db0845f51218397ad120448b8726a93f03062ffc768d8bedbc351dfe128c7f0bc1e853ed477bdd8d336a8fc9f4a2a3603628c91e2efa1f2918cc9ef0167a3f448965ef28e5277e7d4b83521cab30d29ed86d9056b4dcca26b26d7147e360bf28a3a988d144ae5180a75d26905ed7eb65e24e6e24d62306b4414e12ac055ad22267abc0c17b7a097d6f5f4a4f146a967e7a559368cf7254ba7b88f2e185f60611ccdb57ba1a4eb397a238f81592b7e148650839516e8be3eb11c55a8c90088cd70822bc3f51e3e52082a17fd8c6b14250aeb92ae315c4aff9ee43a07c831b5953ab690a83ad97953c7ed0744133e15efc69b50bcfbeee6dda2cb5bbebc4cc7f09c6bf8560f05811f36f47e6e391cba52f66656a5bc52a6bf0ff230583a60fe585960eaebe8f6cd4d3cd57dadf939f22bebf7b98f32a097f909d01639e55aa183562bad77f4a1a0f55b153c4ae8dc30d937b134101e125eb9ebb18cfa02971f057b25cd3f3603167e37bb7522df03d6d180eb50886239a55de188f317d6023223a7f1b30e6aed232ff529b37da97a56e563ef072d2c8d28e6983d262d3032e8cc4f7f59499eb728ce3f907698a0633f61f76723346e45b6c57960fdd07d9ccd5e1e0bccfd25f6422ba9dbf4dd04fc358af553e89b3ec7c51d46cb615c1029f8afef9afe0fbdf42300288f45693c2ee904e67353e763111edf716bff5e21f291868a65ad3d256b592675cb2ead7aa626bc95b331b98ccaed2c2446774e18bf98c52a76fd8c734e81eb4f33d2a5d7e3e7526af3f11a092fa3367e733f6571e1d6a4d92c4c367a6117de37c47a5eac8863d63669bccf58357f29db7a69faa644bd4787c855b100be9b3955d4817f1be9d4bc037d04e3597b875387de9e3a4c7e8499446415258a99ceb91cfd2617f1e77cdf4ee383787fdd1c1d178dec07b6b467e989870234acf859523918cc78b41848526ac5d97cc2191610f2c9782918ea794530cbbf7ffb218ff168281c5e66865b86f14ea5ff4b091deef7a9bf0ee81d93f5230aa373444b51fa520bd422c888fc6c2104e566a23cadeedcaa28ca6659c7bdce12b97f120098918df7677a30b95d649daf7ddb91dfa3b92979b0ce6936106dcc586c42f9eb8270cb5fa217788aabaa2ae64a36b4ae7e413d533b008c5952c24a5641fc9bf5c9647a30ee4222c9bf6dc27b49421cca327b22aadf4edd4716745f0c45b2616bc1e0c49cbf67afd35fc0ae7876dc9ba4ef4f8e4fb29e98be76dfd743f2592d7e9f3316a8f72d9d85ebfb19ced917de721d993db4b975b77372678b437b6b9ca83ec7f34f066b873b8d16094a0cf61ae30307fcb9d909ffbd7c235934dc2a2ffe2d65f55f0c2f40ea1f18b60d3c2f2e818ba0e1eedf7fe0828104ae7906be6c29411d9e639b9f83d99f8f5b113ff9d5f2ab14946d940ea55ccfafc081372d61b07e11cacbf7bf17f4ed72ffa27f10f162a242313277b67e7fff1fdb79fbad83a9839b1983d37b134b2b33063b185f64eec1dfe7ca457db7f7489b90d3dd62f9ee2f3abf2c45f756310e36a67aeb8712f94638baa6eaa6146f70c97e6d4cf3d5bc846d0b119ee1c5ed9e7ff6568780929b6b4c968fb536e53d7ef0d8dc45d0561d7e9c2f255922847c3ea39f1e31adf11eb4fb17f252bbf587088bffa3c5b85556e05a821b47e713a3cc8729cd6f87fd8fb0fa8a892ed7d186e9a9ca32888a0345924e79c73ce20201924e71c5a7292a880240501413218c89224484691202020088a92440141fcd6ccdc3b9733239c8badffdffde6b5d69a592e9eae734e55edaadab5f7b377b11d4e41e565ba77ff5e15084d1b346f02fe5f06f4b802c1c4e6cdf65f96efdb054070c0d6fead84f1c7c16dd6e17bfd363c367109a16bdc6cd98c8fb0ce5e476c0f13fd02f28b1904db1f1902c78a43a80d477de3bf648cefc09fc2daa83ef3a20df5be19a1bd854f11a0c048e8a1c4746d88e4f0cd62151e82d74710c2e3f22e8f853c40f9d0b57ff2cbdb12b6c60bcdd06db53f6bd448e532df038c112544de5b14120d092313814084217d5a102117504d02ba83a30e0fc1da4b08a1159fa9c5910a2ed5e9398e607da380058b80ec34c82013130d6427c3047c5f37f166f698c568c0008c1d799ab045bbed0d1f39828205f27d50a9bf6ab8dfba09e097868b9886fbd7dc81c1c50ccbaf7878635d58e44a54e3381a90deda871f5659da3c31586a13a10f143fe77edbd99e3128df5235f193a9b0674d63ddd2a0ee89c2860ef2ba6c2ce6a26c212a5339af118ed482bb2d57f8a19d4674eb4b6935902523c1953ae15a1695748a1fd453f79c28ca6f7a52142d65361507ef6d35ae787961ff92ef7f9e7c5b360d09f4b0242209195f9bab2be8b020ed1f3d948dc5984b43fe891931f9ce2b710d4a7eeff6e9997edef64bbae7feca45126dee691ef58c8c903ad9331ca1a40b50c1b732b1744a048c46e5c8e67c9ed62b248d3e1c57d0c9b7a33e99cd89ff28f9d6d445d6949f0bdf3ef19442e9c216a1d87bb257a8bfe4fb9f27dfd7831614c748387d09fac45bbcc5cdcaae6f5f3f54cf60bc1dbfc8770231f936eb894a7b196376ed5c2cf779d6347b141e9193e3374d1909e669db35e946621ff2123fa1af7f2dd92a2651e91c4055665de580864ba2a4463322d91e1b4ea6a57dfa07f514e985708cebb297d956fdd6d345c705ba4a4d9bf77fc9f73f4fbe1b1d982edc369a153523865904caf87f8a96bcd4795865ad9b2fb2e772889e2122dfafdb2d364f151b9f62147e737e29e94b7557f7256c89204a836db6086dd7d5cf5eba5c586f3ac6a373f5f9a7636e079f7a36a724a0596bff245ad503e5c9aea6432a22eb7733740765ebdf21fad00ff3214cd6b43f294c3fb59bf06ebe8e5c760891973cf7e43563123e22e31a23bb52efd74bbd05416b5ad2bd8e72e4f671099ccc25ac1b66aecf8f18b1bf85e927340adf97e051649d20c611520daed9e4d435660633d281e0bfc2f47f6ec19077b4b53477b7b4f8cefaff09d3b77014b7e5b051e617f7f43593b2f252d611bf6caae6ee60e9e421eba8acaf2563e0e96b67ade4eacba77e59ed07ae2b081ebd7f40983e0f8fa5252faf85a5190f3f07bb99398f1917273f0f2737a739a7391f3797158fa5293f0f1717270f1b872987398f95a515073b37af15a739278f053f171b871962e367e9e920c0c7c5cdf993e4435c52c32d34f1542f2bc505db78d4bee4e7d1ed78130b190572289ec9f510124b25f5010d63f3f93723986c9402106312bb15fd92692381445867f963b95beef639a0b9163b9042a8a430e0431199eaed1a94104808e5427be80b3b0d996d45f86ffffe31a1e6dbf1f0108a38869abfd9b8be7539c361df8afe4c6e50bfc128f2706964d267ad297879d0c6b547c591221d9fc1447fabf6cca80d85d8d9992c542627e98d3f62df2d967e42ec3bbb27fb3b6f664f8d01f35335843255d3fa489f01b1d358a2325e51d708861f6d54ddb94fcee02d12cf77e720de37995c0e23632023d2a3faaaf350e6a236871bc0e01a3379c2a836c919373d2b56c2552e2c4f29640690223cf6090556d6d4f3536549755ef763e97b62ec13480fe21bee43acbe679fc6ed98c35bf226d1096f8bd0036cf9bad39ef910a10d3cc2cdd03912884969e2c420c0295d4898f1567550a1e4a2a5282ed4e3bcddeb87fb00df5bfe3ddc106bcda61b0faf5c85ec2b38399a05100162f35673a618a59b0bf95c67d374da76329164d59800b16f441ac6c5824b863489ba2cf3d9436d2bbb1a568580efbff4492c8dd4ae27a6326b7bb6e5e5eb1b67630129129394f562589bb43502f16629700678861b8dda00b18badeb8f7b1bf8ce24d88df5968df05c7aabfb3abeeb205ee482e49aff05ade5d572cbb351ab2b8dadae7c80d868e684fb55e7d7c854ce699c976e6af146b12df90a305b727f1252c335a5f1e87734e7c9d91e505d6d6906c4a639cad15f21547ee523015d0cc7652db2edd2790f60dca7ea92cf63e57e7ed248e6be6ee0422ce6127be3e241bcce82acc79ce04e09bff9a2431a4373738cb41720f63a949399ffca99ab950930ae2e54bde9cad00986b583b89db7d858dc452fbecf4979ab24261fdf34af0f01f00b54309c378ca2012c585c615ed71e93e654dd02b88652e713e37677eaf79b4a9454a21254698bd013310fe2019ca46e26ea961f8a4a8544ec31ee5a929928111dc4bdf43646924743de8818bd18bb3d77675a02290c90828c2de1be5290ad9078f095791208166179aa24372076b4f6e2dafcd3f7b0bc3d2ff2a11dbb7681ba891ec0fccd54ea3ce39c2dcf5f2a9439ea237dabdfa402064871e9af48fe59f6462076c55defb588a5192ba19c7180591a77a54c0563dc79dd9c6f6f6c7b69e3a51a2dd07071ff0a1a1bdc53b8eb2be975e2523ea50e88da2940ecdb5279bbaffdb546ce30c5b676ef6974c69c0972003fb66ffcca66020b8e7bdc190bf105293ff86ea7222ba0fd06c3788d011fa52847c473e931dc5f2ee33c06f45f5d3857440ea68fd39925e51097530e17fd139700f527b9f07b28ddecf9cee954aba1e44a553e8b430168ee1c611f2fe7a09f3dd93d104165c3fc4e3d07f2109062eb21d6a37bfc4305b7ceacacea54710d5c67941502d0182d7bc514e221975887d325bdce5e6d2c248b1302a43094d1cae350ba227dd5b40153a37f4a5750883603105ba9ee5332e1302659ce7a653a7fd0b131e07db10c80a3816d2c1e15eb2cb56fd6dc273ef52ec76f218203303ece775f33d08507d075dc5b1387f8ef7eb4a0eb3e7910df3ed93acf9777b210eb5d4b41479af05335c350801a1f3e3f55171e77b9f3bdfff26d4ea71022f89807607eb7e4c58c3eb116556a63ea668dcd7d7c9aa5fe0920459a1b332dac165544f014ed7cb9045dc8e4f9200b40ece2380d7ab6a95b619b22ef1dce8f2a170af4d6e66d0ee2a2e9db42b4f0beb775992e3045e477a8af3d370029427bef5f9039e5cfde9221fe4aa1fd76ca6da4225fa0a7e585d419925468f7473f61a299261588c5d62ce0e446cc0fbfb7cef091d8cbb9bfba5014267cbf6016b0bf4cf72be9567fb9b1f8d646f811212e933892e613408ac390cf120ec9e22797775a91c59218dd1cf9cb8c01297e91d86d4feff95128368bdddc332c69894fe835133e886bb00c6bdfd128a74219d5eb54797af6e27e9b92cf419c33008f3629f8263db27eb1e360426946a6411920c5634e9335dfb589c7afd7ed3eaa70bd5b2c9f740907e47ee01ef3574fb9bf6299a3871b83cabd7a9d97a30790a22e21242baf7a3feeccfd57bbafc915479de3d7f500798712922e5341d79f982aa76cd8494e076aa98f9a025224eba0880e0ff388d5dc65eedd3e955112a2d72a0868dfd5ccc49c3474dc972bec1d45da5b7138ca1f3c006c80c9edd73184c535a9513157138a68b8b93412f50029626f445da28e7e418caa5ec41f7c4b5782e05a1a1dc0f55c66423fa35f87be1638251449a3c8909bfa5806c0962cf5c0c620933e33692d1494f594b211eba9337e1860fd3a615c567fdac7a65efb4d2d356547854e6b1b408d36114d5c527a78836e7cda751a4ef9b542f4963d2045e57becb99525637cd1c9a4f6cb8285f39a65d6e600359fc79e236feb0632ff265b8f76546c8f44c2876c806dcb104fa12f043ec409adec9e56cf7ed0c8cd7a05901b42422d9c2efb1da561165fb48ee284096fdc3a392036fd8dfafbe4e2a4706fa7f14b510928e336c16f3f008c573eaeb60c5d195979d1c341f43bb657f70d384300b925fa8588543cc20bf25facbcccfd52d86ad41b70660070a633f4fba2b22de351b7645d6eebfdd56832860aa0ff4486a7bdc5bfe3fd62df15decb2889c5369c8a2173105760dae95a5e50daa1ff54e5308d12f3bc4ea975f8205e80becdd16223d9f080954c2ef8ab79e7b9a47040fbb0847c0c2e7a97e6615e95c1c2574edb29234307787e2684a10aad43b274ef1c24539639dc18f6bfd2de3e88cbf9bec4b73a53d81736bfe576c656ce523b9354ff201eb15f64363fb835df0f6799c445316881e20401f6cf9467011b67883419d9a6d39e09d3129e16d52f04a4f0c498b78509d175e42c8f40efce6cb43ff7e6ede206ac8f64d872170c9ff5d546dc0a8887197fe95bcf048c6fad2cea6586bbe16c1e49d94f4e0592564f24fa03f6af89010909338cb8f660a3eb9ac9da735ffcaabc01b96904821c3c6a9af5bd1f90f26e4fc3ba4b1dedbf00f4c339572b1d8e1a4db7921146a1f90b8b9dc8439b80dc1f96d412e21ef58272d2b0beaff3fca9058124aa80e4174f958a4bd74d8977cf56db2455e2b2d477bec69f3b88f3db9cc37e9e1a8fdc480309fdaa37c3fa428a0070e4e07e9f91a97763b88f6fd682748300e3f5288d09ee411c7f4e36455b8855bcaed9f9718a8c37eb807127e03a16e4cdd6a88b6b0144935f89da6fd899b7979c7e0de89f328a8db834a59b4441b5020f46ebb8aa8a25a301d49062cefc74e90ecede0226957bb0789eeed6e7fbecd01d54c23fe82e94936eb5a8289ec6c141c7397a4bf22245ca0af0875c9baf53f0f7af6736b39c63fa91477f5ba65296b3ab66f7ef3baa9c37e662d15c09baa50a819c74c9fc1bd9e0cf3c1060d74c9c3d9645174a6700849f1d6d5a805e02522908ba80f48e731b812096a3b33fd7f48594f7a6cc501309b187882168153ffb2365c4caa6c1f1ad9b98a272be97a1976a79d78907763c087692c971addeed4b37580de284b9ae4684c89abd45c11812443dea3a0f24cb5b0d3fcb34783f0b72aef0f2fa7b012baafdd80cf17766527467fe62a750893e4e7f5bd93383dd4f070998d3ed9ce145be8fc92d09556db728a04d126c28f2dbbbdbe3b58f4dfdc22993523095e26c6aba4aeec787fbf7e8bedc53a177bab09a754d7c6ec78f8fa8533ee9185e2689afa2b810725a139e956d8b9ce7f9f868aad74af95622d008c91e703d55c61b43d9159f39752e35608936afa71fed7eea82ad348e2a342de97621f9ed6401b994a5cb14a0567d685a040ab6469ba9122404efa2450877c6702d349426ccae0d41b32418f7148a8dd8d84311fcbe5bc7a9bfced158abcb67dde7baa167c81993249a2a5273f2f065eb8fee3cceda6c42386a8a9b15a81fa66b632a123778c27163b90ea1e70b797022d87e50d9fcf6357e16130490cc4623f4299c771da6a67bc9a3dd10dc1c0804e95b371283ca267a25fc4fa144cf9361bb3576bd4414fa744f60c0c3f114664f21d312dbd70ba29f2b9447bc4dee49ad30c1a811b39503d7060b538d136f1c29d25e5b33baec17cfeb08bc1b39dc7d8704a53f77170bec1e42b0c516cdded4c7d2f5fb2de54c6cde9cfcfce656ec92e2fc667c16e6e69c3cfc96969c12121ce23cdcec3cec5c5c52d25c9c96dc6c3c52ff17930e09a02034dafbe1b2dab43e841b45f285900d3c78771fafe838f8b72e693e0efeb7820feaf64194bf1aba32a5338bd037424e820471a000ee7a71a5ad1622ddcd9dac8ec1eae9d561725dd5b5b5fb6f071b517ef1f1db0652f095ab111c1f30572603fa7b18c010f340a0cc9e9b87e7b9c3ba60549d14edc0ac3041dd71dab8f28d729ce77febd2f6efeea33fd6285404956ab043c5c4b7fed80041ca8520c54290d4d4e97238261e10bd30acdd4b6fa7bbd57bce800bb6d3fdcd7e44d93c69358680dff505266e78cb71b641b0d1f89bbae8daa2719c15014c5a8e7533eebfdd9f48dfac857214f9fccfedd0f49b73f8ac9de24cede1b3fb68fefb0e76e27fb8e7d8a13f807b8e8cd80906ec14890e1081697201f6163cda74eab14defd8be35cea70de4ee082efa6017f1a2fc95bb757f77bf2e4dce4a75282fab35576dfcfe10a32a138293560cc136fc8f1728c8690309455cf14314c0020dc28f3bfee6454f80881c59d9f3d61c8675571707e7dc385a3741a60e52fd891d2ceea1ec4d231d26387cd2983c0f7dfac647bc2f41e71e2e5275589468a89187952aba13ecdf44bdf88ef96aa5018b67c0a2cce9f62cd6acb8cafdd3d68f7d3927f0897e1477b88f8ee3fd54bc812c6912532ee9f5db0e16338f6efc9a9fbfe6e771e7e771e5c8ca9ed1f9308c01dff0a54015c810de3453fb99f353322a953c6c79ec52b3883c5a7734b36df9a3e6409676781f84f88327b483e4aa8a6edce88ac679812bdd3acb3c94fc9de141c6d5c20f4cd2cb3473d5eeadcdd6fea8f999e33fbabb53510259a356d35131d6873660e0f3fd9a9fbfe6e771e7e771e5c8ca9ef7503333031149ba6ef8c0eed1f3d3e1dccf9c9f811932cdb129594f0d1f7d6d56e0b60a3df7b89e09a5eadaaaf3488314e9ac738d11dbd6ed6882a6747f7e1f947811f9959c87fd56d6cf867d742f54b4f1dcdd5b4390bb8d7f202014ff7f20201405e4f9e820963a6cc0a230e2aab8fa167abbdea0451fedc38df7efe6244b247f2d3affbc45072cdafbf88b0ed22622726465cfdb74f4f3c940eca555177fe6a2f31ea5b26bc773e3ebc782b405ffca0e1432c347f8a7cfc576c6a2c2e6a8f8eaeb75948bd36bbaeeb4e56cb9053c092597372b7fbb5d3afaf02acb0a8db74266dfd51fa514584d694b9ce5a57f6ef489bd02c3614550d8d6f9d4aff9f9cf9b9f60f3eff8416940a5e0b8726465cf7ba41b13e9cde343ada99ea29f3b60520b153f737e660ff18e918d15278a24d071bb94b2dc6d20fe540af120df146d33de6c097c55ed566a357ec3753f5f178ef114c6f3e8bd678f24fbdced3d0b41b398550155a1d91f15b03810b47055be5c3ea6523ea8f1cef9ac8f12229f917fcdcf5f4afb7195f6e3ca91953d6fd76118a335213aca18881a9857dbfb33e767ffcb9d8e92d6b2e829a3fd50328a2fc5d998fa59680c6c4361db77771d9ef14599c87e788c2751d9d56e35c6d4c0c62f327ca74853e962912632f62e4e8bcda2d78fda3f2750d76f97f18d8b719fa6287c935175ee6a8de9f55ff3f3d7fc3ceefc3cae1c59d9f30e1e86694a9f64978b7a7fe9c8162a2a4dfeccf969bcdcd7f351a351585ec47ee145a7cadd9a15171dcc9635ff91eb3b838fcce842d97189ed6d3fc0e3de46872c633071f6b843f26ba8089bb405d3c97c4d6d1884103a540f44e22419d60a90f04342cff45942e0d06d677808e92b9310c3ecfc1a0b588d2be0b6d4aa977e5be626d5ca1f0a506ef6cead1b50b09e07041e4b20dd0e3ff12e804af7a5a9e4e29df7c153be71f3872e8db97423eea7d97e3bd72357fef1cef3efab6b91f3bae11efc889dcba160eb0282d31210647fa930f70cb11fcfc0044b0215d146fc0b089437078a8e0441c5c0444186a27dd70bc01836475e77c57ef552ca9178ea29f8d17819f4ff14bfc16c7e249ea9127a34ded179249e250a765d184ac21dae572a1fce04f662d2d911cd8611975f92bc5b9fd73ece6e664b8eb572a5492c65c63ada32639f408d9ad01435489596e4cae32a5a3277d38b7e0c8bfdb550de9859ccf73c9416e7af2ebd48c1c4f6fd629bc7ade179222c5075c78c8aa2b85c27eca11483dfc8ee99abeca7d11fb5dc533f297b8b2cfd265a3481b459fbf50830bd1585135e9f541f1bf170dfec04565c3ca6542e0a23ca69ce04a50029bc515be796798216c6b24fe69b649e24036e9dd674c19d9b8357de49e7121832da19cb94ed199a319271d5ccddcb9362ba997459eb8c15938f5e78ec721634c05947af3ffb6b27a17073b6758f6c78d9cd3bdb14664ecbd75508c270e0a79bde68e50f543eeda16bde41d9f8771e85334b793f298782f596ba4acdb0dcb57c85052c6d83d58587098dd3aa5d512f67bae50a2e8860bd8136aed119a7ccae8b5be75c1594f63d42eaff9e438187644cc88347373a4029392ff132e916baf3ba3fc8aa02b653fecaa1f0730b8e86a99999cf595d537b7b4bf7efa8ff9f1c0a6abee64e32da5c3a2a964e3a66ee767a76d22e5e9e16ec1c0eca8ee61e72e22e8e120a5a3ada924ed2be2e3f30e0ff7f20878295a539172fbf25bfb91937173b2fbf152717371f2727a72987192f079ba9251ba7b9390f0f1b073f1f271b0fbf29bba50597252f3b0f3f2f17273b3b87150f629d8166e9e920c0cef6f334a9ce9773bcca1c8c976a6573bb25468ae5b6bf766c6bd2c72eca442bc756b6677159e857c76fefd299f875e61732f5f4cf043deb23511590f4167fc0fd6abd4e8a10cc12b143d40b0fa1b83a5f4bd44ea4ae5887e0700238fe60cce26b15187721ba99c4e815abccf2e9e3d1b10b717ac7c1117dfecffe3e4471b0f41260f50b1414d5d3de163c592abeee2de95959a6c22efbf63878c962d2400ce10ad3133bd567a3f69f16f71e335e394efda054a4a2458755f2c76749c2558bafa7b95ec3473b0e0ef67eb0faffd7edfbd9fd83e8f822fa7d38a72f7c42738cc4edaf7a9f91bbb63a9f688677faff65ff81e16045cd415c7b3fce9cadff46d8394682aa5b24a8ed803cfe025f32fcb050754baa5dde13b3e6a6b09b586c028c6272faca57fb573c64e67cbcf5fa2a281af8f29f00623d579e398dadde75707f5899b275aa5e983cfcf102c069b51f8f1328f3e553e67d0bab5207adb1ee8c6b96807bc2b90b77896f9787941240cb36ee7c96e5dc6a8800e4e2f03d6762fe52bf2e1dade99414dbe5b55a9293c939077165f4d5a44da8a24ea4fd3e6ed3f446b068da36a07f39d7942d9c2ef47967a76a4560bfd6eeaeadef02c442630ac67fc6ddc8c97c2a6d2c3f91adfd3849240f704ff857caea165ce32d35c524640bffc49b8377dfbb3d3c88cf64ec0a0e47466c33a3bc2f1ede5565a7d2587f751037271112bf10c55dce6f16d3c5ed88447d29741190cba3cee151fe2d36459aecf56b495fabeada269185468f337e9412c97a2ff8e55bf19fa543893ef677de61cf87fc2f8d6f2e06218a5b7ef9de16cddd655c4d2ace1056bbb2e38c7fd36dcafdb2fb42397303c45afc437bf1d3295dd5c7918f06c1f0da55e4b8243f3a58e8fb48871cffbc71961f293fbb450d98a5502e2f1757d62d9c31666c34f7d380fe7d19722766523fd4a2cd77ea8e93df4ed607217a4088c4cb9dd9f2f5264f4bdea74fb75d1b20e19137f1387ea47c7a94c804ced94ecde6ee61e5f8a5c75ef0ee91e9388efcee25d286e92f0ab7d467620d66866e71f4132c0d1dc499e54e7ed24481ddef9cf0dfcb452e234fd12e3c968a4fe53f6e482e51691be9c712fafa460e89e20c0920ccc9d6e22ca993dbd31272aa04a576d8d6d7c0e05a1fe0f879cb2ca1a977588870c09d667166342e02c78fe88cd503b7c20ea255e58b4c724bc161a68da2d8c0f18de0266c2587bf5250582d428d145b3f0f1c5ff20b03d769ec7c844482d1ba521a9e2ac555f200728d78510f306d94052aa5040553602a74f59e365805e8875d8bd8c203f4488fcf2d3a465d1ad4db24d49e07c439f3c018c8e75ba99a0233e6a34eaa9d637861ac00b8873da36cd72b5af6d9e20e143a815ddaeea1e8f46afb20fe68aa69e27445d583186f196bf7c1bb351abdda80fd41db6089e19e5e2af6b3e6d79503ecceb8d57ebc805c1d13145bdec953d806e42b31359257d219875eb401821616af37516c909f21d8aaf313d0912f5af9c2dde9799cf1eb4d72538d7370f854d6352c4ebeee57c42836aafdbf34bec3a5283b3df5aa5d1dc318cd6db1c8562fc333248f33fe0f666f4a9f4b65c41ac4d2c337d432e3bbd08ab7761cf9609023aae8490db9516bc22bf425d0274c5d2f39e647cacf936e1cac58ccf8d39ba2f7632ad45e52790b869502d6c7c5a21ec6c7f1eb03122ab28c0517f764e31a00f1d6d3952b29ac83c4738333be4fd41fcb616028e09afe48f9bc242c8ae7a36828f9822844fd5631cc5c1449d8e938f27b6dbf14afecd1353393647c97908fcc5f7d16d300b1b1f4183e05f32236b4b4d5652da6f3f261f457393e417e95ffff2cecc2693529475853b855c93802c79c8fb2d5b9a8bd7d7d28b84749f95ce1681f8560fa93a36d355fee1a1e694b59c83c9234a2ef59ba76184692f9a16d4df068f3041adec0a14e3962f2e462f9b2b5a3eb93581ede01c26f4b3c688fb6662105788b7d6ffb20ccc4f71565130e0fa37e663adfa0379074e8f885ec2bc1ea778f3c1fc11ed11fc684472227154b37e34c38bcb26f88daee00ec486b13b29402c3a1df17b114eb5771c4f8d95e1b5595049f067fa4c634f1f809a93195bbebbb052cf85c772c3a7b6894442ea58d7300cc458ec2695fc2ee94ce7e9450c1e19fa669a4129a303a88974d8a243cbb4c6f6c6312f5f8d9ad9c70e70bfc008e21771d55d95d55e9de5cfa38f39e771bba4c2e15370fe2a1b35aa40d507f8f3a1392ec5be3e64a9bca5e80e9b45586dc5ba6bf1ce992b345e4beb167d3250c03a476bc2877858564c8c6ef14124f8954e23dc29b2ab47d07f18a59cd5c82730adc464acec29994c5c558e23a2f01dbc94c5b73761f0bff4c592c46b0234b3d99f2474044297ecd98010da3390b1edd42207b7123f713af930071347bf1e52c8be604df1669b745c3195edda8a44540ffb64e8bd83ac9d96fec4ed6ec697d14d29d0d9dd93988cf2e5de4c1f15aacc0844f4ff0a845f8606d3f046463bda377133b20d87ba6c711fdf572e31dc8fc8540f583f82001958afc9ccedee0f0bcf76dd56d3287a7ed80d471d96726e8bebec21f24d008607119be398d594f0548ad74527b6ff1ce8b87ace4e6ba37b7eb985f3d646605388a3f3de90cbba895b53c95a8c1d2b46575bffe4b19c0c7be52d9efbb03130d89a5da0ae016459abe33ace10b30a744155bec97b7a8164ecb34c17a2a32fbcb1f00d4c5c9de8f466e7e9944ed97a9e4afb838b0e351e9005243068c99a956c1ccb1502d549ecb2f95dd36d9f20304aea545428c2d50570a7633d85de0f2efaf95d0c1001c0413c17936e5b1f47668526b06831a8efb58a22e60b9aa39c13b50d1f3b9ae1b9dcbba7d6e2039a5ea3620b566d195c49d32e7ab53c35d9615adf6016a22164c80e32abad8655e9c740ec752469f18ba874d990dd40280f9815bcfc2d0441c3d7aabb8fac383988e332afe0380942feea8b4e5194218576a78e3bcf1d8677809c26f028e8b2a6e75d1f4b83928410150b9287db8d809c322406ab385b077c29a2e26981c6bcf4230a969976bfdd6850ee296013914307baffb211f5eead9ce12612c48f1013202b8f39cd5d0bb6793a220e97031c452064f50f82ac01ca0195b41db9ce32d66136eb021d8b12421debd0688e837e6276b83084b999285e3c6aa6b9c30fd1cc40b481174d7aed681d268a9a173e319af1faae1fb7c8d4f80f9b9c13ed0b116728b948028cab7d0b55ee7d6e407406ade7b89215bcd850e6efda4f1814f618cd006b104406ab8af9e5c719f5bb517917d5e6f9cdce0c0bbf1bc1930fe511b17c9989b7920265f689eaf57acaf289cc601d030f808a15962142cb4d83554781ced25396897e401ebebbb28c22fc881fc1f02065ff463f26ae89e821702cc3583867ab9925755b2f0c5d2140731f338eabfba3001d5f9479e298bb1599158556f49648cfaea31e00075b466ad0dcd8cd5a93fb7028b7a88cab6753bf4ea5dc0f8246c131b5cbbf491240bf5267ca610cd79c20fc0a1794b48a9947ce3f51e36abf1cd7cf7a986e1f45c2f803928ac77343dca23b2b3873e263e9cbdd9d163dc18705c9b217d21b297dd9b5e4b81d681ff2c75a6ad0ca0eeaf98281a45e429f31be1f7ed06ac27178ff43301bca3edf118666f1291626067e6cf4a576709de9c1f01a83b4f74a8a1fe0924e9be762c73e799cf5990de790de08ce0183dc58121eff376483cc33c6da3d3e9486fb1771067fb22c695da6f941a3da8c9578cb2716a602106701cf4e2785b41f7e0e93399e99c895c64eae8856a1a71c0fa2c15888ef2b4f8bc9dcf8a5913d7852dfc4926c076ee5ae1e96979f7d2641b232b9fa57fb6eec7fb3a80d4cce18a74c1aa3424aa549fabe44d0507f92b563300eb97077eb2ec6db8adf5bb7b987a6486f87771910801f2b1a9e371dd924da062e6ecd7c7a713439a153a9b040fe27eb5cf2252c30ded761370d905da57b9c80d3e01d4154d5e56d6ce65b78639cee0963e22ca938aacc480f56ba160c06f2948afa9fc757de0abb3ef75eb2e5ee43d884b3ec2b4b88d973d8d7fd5bbb7dc4f29751be60b5097da0766a7458d85c7c3a6de7c288cd91bdb5ace04ac3fbb1e69459ff0569ba294214b8384c239c101ad2207f1015bda97bc86fe3cda1ee64daeb40be2112572007350460c86f547d3f27d65f36229ed312864cd9dc8fe207e623d910ffb3151a9ccc7aebe74e68b68c9d3bb007decf6bdbcde1afb8c910b3c237eac14a8410ebefed607f12c6e3dbb2dc8d5f78d7d556fb5de8c4e2b3754d91ec4dfb0a83dd3254655eb1e79f126b70f2350de781fb07e786784d5c1703b3e727fb94f34563f50f91e17bbea209e34b5c9b7f1b29c496094e95473ee5bba4e643e40eac7f9730ff65eb07590f8a492de945951421b745700e037535ade0df86391f95c1efa001fbd9587d38c06384f38173b86cb3fdcc9650a6a6b3bc963172031c20920725596e66f277d490d2279fece25b021af5530c11970dcde76554f70a845bf723ed0cb51289e70a175e81de0b86cb4c51221a37b2b35a187e87abb4459f69be54f7807f1e0e7b22f2c7a93676fa23e3897b039c47b5e5d0e90a6f0ec3bf4851cda9a4bea74318e63d5f794e90c6200d7885e98489a8205d9ce57c5f131b964c4db19af9a02f63754bb0cfd2de20ed97b15bae502c5cfb8d3b3e800f37ba65e05ed8c6a3efd72a62ee3288a6440c3683c40feea546d3b3f610bcfed32a4bd92bebfb17b37ec0540ffa1ea66e57eb60d19907e74db82ed1c6d55ae724f00e084b1defb6572bd8fa8e3d375f9bb69d3320529fe007a86d68765adcf8a688d2ae512b16fee9b8e8c873c00a45e955163c8b280aebedc7c4e936ad1a51a9a91f51e9015e84dd053a4223daf137b31a97e6cce16276f08b5028e63aae20eaf53933a434f870acf32e5a61bdacb7b2d024e00ce393c6b389c9e6f539cee88e98ccc87384100edeb45cacb3a15ac7a16cf9a66346ab13fb1919c16a0ffa6f73ccbd51018ce596252bd578fdce014512008601710114d3e4ee1be847e4a37abe5e4ade5b4e738520f0027acde3db972c6d06b0da7efbf9c6775c194b8160a383f9c7a1a7a7224b769f8ebc206d1f218f2fa90c949c0fe601dc10521bf9b1f93e9f3beef46346439b6e101c01ca54b36ea2d788fafcfa938d9a58dbbc6e073617c23f00ecabf5e6670ace467bf178ca07f5d6bf947eeae1aa90ffb1f6e1fc727feb772f12abc193a1cc16b4f891504f933531dbcf9093c84e0546ae897fbb744a0231148d30ca6eb359050ece5564818f6cbe722107833747b181e8afa7116128a86b670aea6fd166aea616c22cc376d6343751b1bdf3a7e333abfd25849a09f5812e1d5193c8efb1cac7cebf685a37e6f65cf74adf968aee93bbceb900c9b6d51eaabc1fb2e56c20553e46f0022fa5d5cd3297808ccd726b5150a319f0f2151d6ba038114df29e05362a3b7b2679cdd41d208c73ca5e27aebf7cb316a91a4b4d5180b1024e5806510441297d4925755b9a4a9ada6a6a47f49455c4b5e47fa9296aaa2b4caff1352118074f7ad1b3f006a5f1a418cc12c7177bf93845054b5bfd209eb92231279fdd1953b50e6dfef17a985d24451a94808a8d03840ce4babe6443350a67a0c171dd37477283316698f1f53c8068c0205c1ddeb9ef4608f93a567d2f3be1ffebc7b8b4277c8aea6452b4bff69da93f9eea744f912f23d1a44adc33c176c7b7342752175025ba2f6a126175add6dbc8b53f0de9ee9707a58394959d009b71f919d5d8573299f51a3eef8bc42652ae527075724be493bc279624f3f2f3857b1ef6e6297a86b895359b74792914d9ae0a3e18ef933255c0ba26e8b58ce94d9030cfbb5fd95e5c95f1e39336a4710a458b43e937b724fba684ccf09dd1f6374a38ea3fa5ab4b9bcbe4612ee9d6e85aca1be946215f5acd34fd1cae320d170de859412c96411859ab82b6e2d4f3822cd3b3e6a429f126b37d9e1a56a4a412958327bb77c071c515a3365b6a42f359913e66ba611a4bd83b8cd6b138e5b6f9066d2540dfb7aa5530964f1f365e1d0a6ed3ea1e4e9bae11f53695f67cf20f2e84ef5e3e718bee7ccbba33d56e33d90259db82877746d99de945dce24eb712deffc2c5ca4c3805a9578728ef3914e53410296772c1c6bdb2f245ac8e7abf7352bbad3269fbc7b774dc65b0b1429c61e76d45caad687599d7b98d090c6c032596fed97bae1fe4c77320b43a9208196446099e8916e1134d89e3e040fba83c30e0f39b3a4f5db7fb538349ba72f3c406c0a82f12ec1924882cd30603e40e9c964ac13f2973734327ca52e2eccae2debbd0d380e0ef60111908977dce5f1dbb1a48d6be211ee3dc21e37ee1d5af16bad6a76c26825c8f301bccdaed0229d9c68794d6ebb88966e1bf14c8ecf0a875e3d0f3d112ad247cce3fdbddff74dc337823bd14f282608d64730df2430b59729fbfdcfb3d9793be3166f9bc5e2a4301e98f3de47f0fb40b628b0748768527354836f8eb30d114e8d6472760a49b9098ac30a1df3fb753f2d6921d806b0390c96c901ecf9cd48bfcd42c6df8672a4ea03e16b96f2bbc3955714d08250ea945f73e9fd67c6790db865489d43c2fad63cf8966c2fc1247348874fde2473211c67cdd0343d2dd94c01d494587cff0b35e360a1310912fad31a033d4fe8e0393ff4ee3873f4f873102c272cd2ade3aca1df984200f901ebb3e3ac8150da50779ca64a72c4e60858c411700dff0ef9473abc7d7f48dc71ea873127885cec16d5b0add27ab48273e62b6c20b818413513d17c9c5b7ed49a4e1eaee696d402d4669696563ed4ccd4e20e4e1e8eeef28eda9a52d402d46c2c9cecfcbc3c9cecbcec3c3c1c7c5c9c7c7ffe44d5c3fd3fbfe1e3e5e066e3e6e3e5ffed9fec9cd4ccd41a965696aeaea6f6d402d4d4ccd432f6a6d66ed4029cccd4f28eee96d6ae97dd7de41dad9ca805fca8152d7de47f7b0c3b3533b5e6656b4753770fd7df3e48ca97fdb283a9ba962a3fbf879383858c299793b9ad8eadab8ab30adb79354d2b2976690507270d2e655d59272e251e2b0f5d736d0b292775eff306ee52126c9e1e0ee7e51c78789cec35d95435edadbcad9d7438dd3c742cb554ad1cb538b9790cce5b49ab58eb594bc859724b6859b0399b5a3899aab8c83ab2caa8d99b5b6a72f0b968397b2b2adad92aa8fad89abbfbfa702ab1b39ae92b58eaf071bb68bbd95aeb3bd99d57543463e5f3e573715431d5d0e1d672e4d0b2b4b76667d59390779267f770b47376f176b1b1f4d0e4d2f134f765b7b4d69257723160b37713e7f4e49676f552d0b7d671d4f0b6e2d6e567d757e41597d0b7d7b3f476b66117e7e4beecc8ad22672a65cd6fe3a66ce960a165a577dececb4555cfc5d94e49555749d35cc9859f8bdddadbda59c39a5b414b91efb2ae8ca7abafbaa2a2a68a87b5139b8dad9ebaa782b6953597a584afba8f8d97a3a7a5b8b0307540c08fd89f41f523b04b0bbf57fff931fb2b12b867b56c35fabb950f90f50d4abe380691a56afe51facd77647f454150ffa369fec393ad4ff5133cd9d12ae8183a61174e567c4ceb3951f01435ee5e38c0d3a9ce73cf8fdee39c04c77b35ea71dc53063294803bb8204d6f98b3d1970bde9c6858aefb8a756a6c968f0b70899e859dbd6ad1d07b692bda017e252393ea98db1701668c4f5ec2f2c322fcafa88db60dc29a49bfe045ac00d2f1446abf19db8e4a53cf7243e5e017b32b9e512f00e8e36c4545c43b9fa9ec337ad1f8a3d576bb14a9ab01e7a9b7066e1ea7d7b8d567c4980dd04f3d48887c0f913f887be2c42af82bdd3f11425d00b7bf56c14d2f9b01d0150c50f978da15b2af6727326ca773fa93edce3602f6a2afe21d814ed7f694769ed463d4525130f23df001b4bfb26ff24d8551c2cad68d14efb69b7ad5af4eea033cf9eb71de49897eaa451923457a814df8befdd2a27207f11ef2243c7a3b1ece3bea686fc37389bc193c0dcf1fc435982a129d510619c6ccdea8547e6a3d11a0c60f20f6d2ea3d424b6de90dc71419d5b8730f7bdd13ff2e205a1d57f2e219cbebe4e7d1ab5f662de6d64e3313eb013c6d230f53a978e46791ddbdaf6db944ae7eedc00e0678aaf476ea9fd9c899b1cd918b48bcb13ab9f2f82e11208a0aa7b8b4fffc684818d3e7b9d9940baf34d4564e032eb1fc0cf5197e796576d686d9575f281943e0138ac8f4413cbedc8f3e7ada478f3c4d61be646ded09b5b03bc012cf1387b2233762b86abee7fe08d38d38a03e5c3819709e88b8699642429da5c4856c70ae19a7c14b7d1ee0a92a92f5e8e1e1aabac63d69c73ebabb6cb7b91d04f0b4f2157cc1f256204c5dd4a84e96f34e7662a81503ac970fc48d8bdfd4142371a9274248dc032fed3422013c758fbb3f8a350c10a3b15e6e1efbea6d945a70da0f7049d2bde2cf2121e554aeeef2bcf03201910d727d5d005322e40c2d74fd036bc66bf67e2ec6f7c6d30521db004f4f7ca8d15af989708ea1a5264ba7ed8cabe6d4d4809b8063e565fa68da2a4fed6ff62fe7d85e47abedde04506744ebb991d9b5382c9d6ea9708891d18436b3a5b81ec487306b8745382f2f312a5ec29e181fcd2830ee01e08ff3499b34531a3ee63b8a3c0cbce13b66bf2bda0d98bf7dcc689d148ee74bfae4d04e16cba9bf9062032c204ffa08d5c6871dbadd2ad491f2d16476433de4019efa95c40676fdb37341b272da50833cc309c3c57d804d4dab84416dadcc073736946f2c3887e782d25b4fc0fa15d6d5290a75ce83d3ae5fc9ec0abc644d19e30cc88b8d7d2ddcf8f4ecf8e944c6567f9eabace27a358d002682adad3fda86f1e76af1dd074b0f5e0caf5fbcd106585f8b92a15bf9f0cc77c13b7831cd7b53d4e34d9900f9a2c5507e6c7e9defe4958b71e31f2c2f909357105202995e3cefcf24fbb859afe4e8a74d77dd0ac89103eaaae19478da5c9ee13dbd4dbe453b57e685f11901c45c63a2fa18d66629c8c525d275fcd025a4a1374c00a6821a974b60b17fec85a2b5a78bcba5ccf496183e00e298e5a92fcf0ca950ab6589876e2563765d8f1af0009884d3981904e4848b4e500e67a55b0ee8d492482a0088c32842e7ce556287441ae0e97bf5d4254adfd91496051c5c575c884ed6400bcb4ba6f0cbed3bce9896d10288fb5fc7a434d35e272541a65ecf85043ed163f631008c0f1709ae43a56983d823f740be4221a2802102519d83f8fd49f2b48cf8ec213c34f850ec07f277cb9be500264dec46de0b213cda62e18cde88b96c7e829cf460c0fd0701262ebeba8fa5518c448b2acd3bde846ac4ddee07c86fac0f5c38d6d9e89dea5eaae512f669920237c0fa824243bbdf76cde284f599af7d44125bce39530a00264e0b466cf515ae4761b7379767172e3136c1488700c4cafb6aa1e104a6f53d6f298889b83e19dd73d3260530759e735bc6c5d485a6a0373c4a24ba8854694d370cb824f559357a66d58ecd6268676db2eab0bdd9e69205c641bc4ae70992c2f2fc359fcde03db5a68de71fb3db1f03f4c1a9e6ab11edb7dee989e1572af75a9f14ca53013031ce5093df5ed555eb5dfbfcceea9463a13f76933780c9233d65a9505375be83ed425b54159aae81305b3880983c67dea39b8fa7dac82ae91a96631760f184a41460cf39e789799b59823b9fc08cde72eabccd3d198c45809bc4f876e1298b172f6fb4242be91be45b77d6a19a00f4a5715625be22d59633b679d91fc96f73c69e62b100c847fe39379abef80bc4b269c6fafad1fe6635e55a80feb9a769c639d581a385ac9af976fc1cd3edb4549c580093ede36a228de727f34148bc073e65e42aa9490e60fd266db2cd9a202ba5a014bd5774e9c18b71bc4a5780a7aff94187b89fb0549fcc7a2e71797ae285463b2600715c3f9559e55a4a936c6561745d20357a3619dd6b001329988f09767e4b5c5b9fc576259ec39ea87ed01f307fce33d69aa832aa5ede9d5871d5230f1e74529b07d80a18130999461a8c33f4a438db149de23bec1a6e0188f1e352d1cd04d919c81eea0d3cef35f15e3a4ca57a1cc49dd0e9e0c6225939e466f0f343dc70776d03a5a700fdafe846d6c4b99636df32a3e5d1e79221cb9b1480f62b341923e3b96cbdbcd56ff4f4b14f548c68b1ed6780f23c7367b1e4f23dc863760c854749927383745d5407f10ef230ca4fb3a2a70daeee8fde17e86eecda8e02cc4fc5c5a16c453edd5acf48c9e8fdc1a0db84d1db004ffbbce61a3d927fc5830c71f7127765b7d2a6b53240608f08fa231f99650eaa85642d73fcb353de5b6a3c803c0b951a7ba85c55a5989d2a780f263ab5ea3d4edf03c8f7c0cca30dee90a9501fb8828760fd17a7b0112a807ed774b3e55cc145640bb1132dd8163c9158fa972be20fe20f3fef0f8ba6e89c74ca30ec68ab5a2af6315805300122026a241f53344dbbcce27a51115388d8d8a500024fc6e7df949b2bbf8c5c9ed05ccdeb5b9b2109f70530d12e949c9afc50f0a4139b2854f3cb6dce74633a313f0053e5c1845f11214d10b554779f32d79548a6791b40e0eb8ca0e55ac53d1dffb3cbed1f9b0bd0a2df447b2d03982a1ffcedab39f134d6f03a1fcba53641916a7af200efbf6292a91edf16986f1ab058aa3fcadadc6ede05dd41d20d37f840e02bf69febcc7e3923ffea8c444d557338d902657e9faecb375aa336cb726338e9f047ffd19d7ff1e8ff952410ef9e341dcc467b6fe06ee55325bd1451a68fd31387b454d45d54069701df1fd20c1d89e057a1931338fb5797fdbfbddbcffdffeedd1610be5e13be4c3276beb8abf94baf20a3ff48e96d84bddbdb8af0106aa28a9abfdd76885e7fb1ff948bf30d27d5e07adf8ae8a91b7955fa87f61456a1df8a261cb203e58cf81791fc5fee5b0804291386bc407f68cdff8e777eac3bed8e6f4e0075d781986a31568e63ee107732118a6b7e98de6690bffdc9953b65ca927ff938f8b1dc45b6579fe0ed24822c02e75011714759d933e21f8d079c18c4dd7f3f5ddab4eaa9abd914e46b31a276c3bde2b9f7ec2b920ce8e60e126704b4b5d4e64bc21f89819068b86f1203dec8e31dce8ac288a763f88465ddbc6e39a9ded30707c165050931e7d0f12e44cba155128cd992c97e9091638f2f8cdeb9d2f4e2ec7196bd6fd5876e5f0f3b4d422483f2fbcdbe7ccd6f6b425fe5cb3022b89a023e9b81aef8e51a5fd68ca941dd8bcf33628962920e47d9c97eff9a66e80ef45fb70d334624d442514823d700c689ddd35fbb47f96b46ce4e7c08f86435a164e392f81641672a88b314c9e281081c1731714034252330e52392b359ce34ebebda8c57e34fbdc5f7876eeaf2fd5f676a02b48f1cf629abe5eb752a9d5327f031ba661a30f3a71f20f2f066e850f8ab4b45ecb720ff26dbc17f344f4e7968a6193a124ec496b9f01f0d28b419dafefc8f376115b2867040fb6ba40851521600c6401ec9dee06b55ec2ef40cd2b2c2efe4ddb2146f83f5d5efec3b1aaa3fd877ebf050c2db26cdbfff2f8fbb9b90f35fb9f6d8abe2705957ff6d7844b7e65bf73a75a2f9b7ded856828722274740febee9aac7156408d454d9a5e5a7697893e4bef3af0e1501f1a44b42b72fc1437f93af50743608a446ddca9e8918cab24bacfa2db33816cdf51b4ddfda20392190dfa06f91f29abff906d681aa9bdf7ec3962fc9616fd8f225a134a1672e1db8ef41e36b654ff8e22d89d98ddc7610c5e7b3327d49b16a9ad205df4b2571276ee58bcacb6d22acf8ec4063e121e7364c6aa1a19592288a3f72ddfc8efa80f38fb25be9c3f36d533c5156ef07d7aa48429172bce710d9aeacec032511537020a1ce92a40b20bfa9518fe07d795e8803c2e45e34a13fb49bbd63d4b66a14e44188dccafed9eb525439487d2c04eb1f55826b6ba5ce30695b5f7731d08711f46cccaedd8937a6b65bc860f72453dbdd7bb82aa880bc38e975edf95ef2503321b43fae066bc73b35b33981361a3a9c199f6847b1261b46a669dc3fceb943872f4744eb65b75ec4c816edc1ee609979ddae4d81df36af95655d2847a7b3d0b53ee6628c04f39d1761e1e79d6c1f7335aeacca587180a45c853e8bc0408bb58f65c30a2152858761e709ba43e0cd4311c8cbae38d70a212114710ca148ea0bddd0edb53f76578aacbe9a79fbc705004fe49201b207775d40c5f2c8e9fa25984c5645d4f5a7dfdb65fa3c324f762d0fbf4692d5537656f1f9110f90c59d7f190cfc13584aadb4e717869cc76b5233a66aa35fd73b798a15323d05529de1a144b51df5bfff2f12821d4c9ca4eb4a0aa03a77581d4975668ceee5eebaa2fef49b546757952d0e8aaa93f2b9b976c6083a1801e53ba8cee9cd3b48efe021d4bd2f6a91e65978af9c40ec84b40830d04d53eac75c31437793968eac31b2335279c7a10bd8663e6b8de0854a5aa5be703b29f37e74da4e39ebedabe3b47761b4354ff5cb4b3b4683132d72a40177cb0d3fa87ef7098d449e67e7ee11ea3689ecab89780f85e3b4ff6f857825e628f8af0effef50afc0cc28901d14e20889e54ba9b5e25821545c25b52828b56cbc60dbca112e78a404d29064a7a3ab2383480dd106487db0a4c9f0efee3051a3998d8d16acefdeb644664e2c7282dd61910742730552d04e5d9c70b85747ac54c9f6ec5aeacb95b4b949145a90fac7a2a87d0307a329836ddb24089e0a488edfe61f5aff7fbd88232121412073ebdcde437f4dfd51c1b978624604d11774208550d179c1a1abf01022acb721d4b941793ac2cf89bf79fc45ba1afbcad2bb4ab01d82d1bc83acf4470405b2940cdbad318be717eeb097419ebfec40db15737e8bd43e2be159457bc5f4eb65699ee16b4501dfbfccfc5df9b51575509edd4e5cf42cc1a2d47d66577fbbe2c9c4e10b2ff5a34bced73f21a89d12da7a38385bbab2587a9bdb983a5a5bb2381caf9799d8bcd9fecb82f8e11aec7480e8edf33f920d0fbf9a904e5c00dae81904db1f1902c78afbde36807ee3bf648c0f44213bea766fe80ecad6bf7397ffcb82f193f2979f4db03e158723239edd366326f1096f283568595668eca2e586ef450849b302e1c8cb283f143e1345936ad3100278c1e6e1cffe7bfef28f0f1253c9ee9e3935ad67f4e8b10669d9698a337711b2acfdca5ffeb3f39763285bba9b2a9bbad97d67fdffe42f37f5d27416b7e0e231f0e09176d4726277d4e0f4e4e270d274d07550f47465375592f2f4e1b490d275f27413ff4966c5efb08bfc80fce5ecdcfcfc66e69cfc56fc569cdc7ca6661c7c66fc6c3cdc56bcfc9cec9c6cec9c669c7c9c965c665c5c7ce6e65ce65c969cfcfcbc6cec3c5cfc7cfc3c5c5c567c08b5ffa7e72fbf81b4917c115753f9e35b33a6500a3e0dc5576706d9f1df52cbde901918ec9a4dd6155f83ae54b75745a6a3cfb4253297389be315229f2dac4ecd334b761f4e1da7f86f4cb77226753507628fa14f2371953032235f34da1ff8e37004f2d5c0bc0ef8c11fee409f45c47141d1ee3540fe6d81ad8536538d21e12036ee50480bb5833b0b3dc5d6c9d633e37c149ecf7b532f630ac6ba4d32bab4049bd042ec8e638eff9ee3da710abf72b8b8147625a7482fb674e823eeacc4eef1d5437ffcafee42ece08270413335777572fb7e163bfe5f34b26354dd34217e5de677b479e46887e36f4ac171f8fbdf2aff765846712cc3eac61b5f186e93d06d05fbe40bd90bbafc6ec0174c7ff20d03be3e83610951547b1bdbe72925cd87ede30d64b70ef326b0ee8c661096dc834076a0bdf010d6fe8d5a683bd15d218095f38140993d370fcf738775c1a83a29da81596182bae3e849608616b0e78b727110aec905de13d8985974ab433a7be2b5e56151015caf54ce9e67cb3ebc47d167921c66ea84c13a1ee57f69e67ee3f944470b2fe456f7a2e19f6c736a82238e761088f9e1fa113470d1a92c30df7fc5bc6501fe2d9fc9b786fef7a3c14a9596d783f81ae371c62d8114d4e0c35f81aabc7247844d6ee5f7577c4b23fe6f5ff1972f3fd297e391e8cdffdc93202a95d8f992d82c5da4691a1a804e733c5f0e2dca8d77ae8dcdd01ddc447808d1fa64c8d904865adcd0f922f75e1b82db33455feeb28ef4509192f5ab1045dabece8e7d500cbfe9eff588d5803aed471e798f7f64c53d5a8ccdb9d6405e80eee4e6e0e476f9bb5768d1dfeab373db7af17a9b9b7b9b7adb71b2d95b71795b9b5af359f19a5a5a73733a5bdbd9f0fea60eb97ab2b1795b39fa38d85bd89a9aba7158d9f170f0823c1ebbf9381b7ce7ece3d5c5bdcbade8aac651769c91f58aed675bc0da8fd89114cc320766d9444530001a1344f1c582ff1f1f3c6e21a878438ea3f82fcb984bb43f936fb8a9596839d293a0e724a671e238078befa88f60fb4103f0fec613b4a996d1e3dc75bd83d7b7193633dfd9186eb927fd13c74fec47f62f18fe13da06d6ff60f2378f326f507e9c3e38fe15a3c0a0bfb7ee379fd7c95f682069555dba3d1871993a23ca1ea135deecb3f8ffb07ca01c473ebea36fff3f353fbfa37ffe71f313ac0fbe83d5bb89d8fcbc70b8cf18cd6b725ddf78fc7f583e8e353fbf23001b51f900ab4fc3421ea0fcffaa7f7f42fba108eab7ffd7fa19584137f5b6d7d6943a9c57d6bc83caf16f9709e5427b2d2acd0f7097fcadd53ae5ad91057272438d6697d75f8405cf0c7cfe72ba1f626464b3acaf8457c4c0daba877545c6948b1ba57642186d6bfd884bb9ffee2ee11e2d1774bc6b7c368ab12e89aab55a53d6b30aed1883fe2dfc97bbe4e7961f77ddabb9a79e9a17afafba9ca48399929aad9487acb5059f829da9b6acb7a6be35bfb5b226bbb517bf83b3bab39bda0fb42e1de7d474fcf9f3dfb84bccadd8d94df92d39cc78d8b92d39b8d978794dadb8b9b82c7879d92c38784c39ccb8f979b9f879d9f8acb8b84c794dd92d4cd9b83939acccadcc78382cd8f8cc106a3fa6a5a7830017073b0ffb0fe8bf1b62fea1b30b897c84f5417b3528fba28a7a56aa20e2330ff27cf1d753d52e433b8db9c41f535baf73d5542f6b98d0b03e2d756b142ee4f84aca657fde8f422592afae5837357258754353138d65f3ca86fb89296ab1f2920d85253fb0eb62a13b50fe48e4984d77d7f87cffdf799e542aaf6ba13465d88960f45b108b05fdfe5128e99550d4e70c2c32e74a8b62f0f9759e3d6560982563686aa40bee5b746c148cc865f771fc5f32cc5ed57c4834acec3726f84876da0a0e3511d035e93f74d1f957771efd441086e00fd831cdcd5c2f5b1ccb150228a02e9566e8f6c33f7d70bf07e0700cd6e45d34b0703c8ee4580ac8e1cc47fa265ce1f76df27af289fab4b0aa12a2abdaedfe176f3e2ccfa396ab5671c45d9a3c59e3f1c9e3cb33acf7af262b186fc2e6f19aa13b48ebff0a1a0abc5b8b346feb4904d84bc94b63b41552876bd94d2e24edcfb92b3cbf519373f826e3ca5deb08a61f218941fed10509d10c94e2dfddffffe55ed60cdd39a5fe67f6edda5352ad7a182888ad7120d9b921a7c0be1924ed280c64f2c2ac40706b10dc0604bf0c82db82e0206e71983d080ec2ab8381ac3430103e32cc19047701c15d41701077090c4467847980e09e20b81708ee0d82fb80e0be20b81f080eb247c24088a2b040103c080407d111605740f060103c04040f05c1c340f070103c02048f04c1a3407090947fb018103c1604bf0a82c781e0f1207802089e08822781e0c920f83510fc3a089e0282a782e0205e69d80d103c1d04cf00c13341f02c103c1b04bf098283686d3010a503960b828344f6c2f240f07c101c24cf02ec0e085e0882835c7d0003a1f3c24052f6c24a40f05210bc0c040709e1848104ecc040686fb02a10bc1a0407d3ff40d2a6c340a2e4610f41f01a10bc1604af03c1eb417010de21ac11046f02c11f81e020f6291888d51cd60a82b781e0ed20f86310bc0304ef04c1bb40f06e10fc0908de0382f782e07d20783f083e00820f82e04320f830080e120c0d7b06828f80e0cf41f051101cc43f0203f138c22640f01720f824083e05824f83e02f41701056166c1604074993000389628681d87f61203658d86b107c11045f02c141aecc808164d28181e49182bd03c1df83e02079b060ab203808eb11b60e8283c42bc33e80e09b20f847101c249011b605826f83e03b20f867107c1704df03c1bf80e0fb203808ed9e06028283d81469401cb03420fe471a10fb230d489e341a10ff300d086b8306c4074183098283c4cbd36083e0206139342059b468f0407090b00d1a02109c10042702c18941709078791a10561c0d29080e62a0a6013350b7b7a32403f6e83eb1f64f27af27f3cd07490b5899273d2228bf782ce6f5378a94cd094900ada33c00f353c43de37434883dfff9005961bbf57eeaa31f8104b2ce7c7741fa2f9c0a48427f6613fc33112ac33f2da3206d2fbde1be50ff49c812e90caed2d0270c5764b0eb3e00dc0101d3354db7155ae3d2cf8d8921195c8fd72934be40ceed8a364377f0d5e12198735e21b0a7e8b5f852c1a53a3dc779f0370a2a621d039644040dc429840922f03880ef1f7f1eb72d1b90abc342be9afc91485e1d5f7fee3982030ff27d50a9bf7a3b279d36958d92991bbdeeb3e810d5a1324e9ad1ad23386ffee9de4eb045ef6f817aac76a1c21971e6131c0ee57efc3cba0f14394f1daa7c31e6d2907f6246e803c50d9730e81395bf6e95f268186c6e96a2c4ccb0a2449bf7c33fcd568aad1193177bb4c47cbedb2461a8399eef8bf73ae5d2171d07d319febec573cb19e7a5539ae1a77f504f510bf6f4456348ad52dcb4a7ab083db5a922607cf3977cfff3e41b6f347aee161cd7fcfa40363ba93e87e04630b3cecf94efedde3aa774878128577b73cfb43b91642229f3ef9156dab2d973161bd6f50b9b255b79f39d1fbcbf66fa1452bbecb8567ab1b68062003bc26354c4e4319b61960dc50feaa98d751486a2320afab3a3d9f867d40bbb32f35d61bfe4fb9f27df43f71499926cfcb475871f4a49ee78b7a730aa1f4a3d64462dd1f14742280983785551fc07f2accb694f4c5172ee0b370fc63d24694969d28c1186ee84ba91afa9a14699f244bf9963d91b346c1ca44a7d31376b48a617789f833f916454a7d5b3f347addf19ce53ee7b4d220661226a1b746e2f7c4dba2e73fd92ef7f9e7cef3e10a37ae2c8b7cf6be7babaa39e50dc3e11c37958e582b832cb54ebbd3144e4fbfea8c1043f5fc030db5dcf5a6ec23a9a1de377d6946f17097be1020d24cb7948f85cb3fa312766f3cd0a161854de47beccbe899455db01e7a7438b75a9a3205cfa51eb37ef3a13dfe476852cefbb1aa39bf47d27b0cf87e2fcbf96ef1f1faef64bbeff2adff98c547cfa1b97c518c995b50be158fc55e7cf1d4d3ebba38ec8022e7eaf5a400a39619c6e83e86cf91b1face44f948ad93a9e575c78870c5d22829289ceab8e5e8e54c91f5010c8f6afff385a37173998264c97dfdb215458b344101d8ec8fadd0cddc13c70a8c6fc11876a64c4841e2cf3271ae0fd5d4b84366b0b94274eb8cd2e6029f2befbbac025f66bd3f9e74d4a53a255dfedbb187dc90b272d02382e598cb02d1cea11647c4c4992bf8750ec8bb88a731ea561fa629d4f34fd0deed7b21f7b38bd6d2e79cec52d3cf960f9362a3658aaad598f1877da6e23ca95b224dbfb3c13e7bad2bb87325535e96de57ab3516e3f4aa9e2887146379ced341565f830e389e4f6d8dd3b14fbd7a6f3cf936f8207055ac1de5a45955ed1da12f1bb8ed0269c23195b68f8984f11916fd90cb2d1e0d7273096188561fd74a263a333158c4d1d5b7cf022bc9b24a85a3d241b57bb485495210e199fb106db12a24f85e1a2f65c4566cdc89cf40e994be6fd514a95b049c5dc72648c7102ecf33eeecbd29e1c519d9d5febf73f4fbec3f9e5f74308b695479160d31f42e95819569a0ef5686a4ac971bfd343cce8338ea7b678aed20abfadc636336625608aba589ff09c5577fcce9dbe4a549f5a244948a671481a37272d6317e526f45e6ac3d4fd21ac93f8776b6f918fe3664d5c434ca9da568787409f1a8462635ec3f97b263646dcc60fa38cadfa7ca7f39f9f5eba38463b37a975d8b3f01a762fc57bfdf9cce1c2506cb4a9b37f7f26cb2441d1de17862c4afc2bd705f324b2ce07f01fc93886a214dd6a86ee2057c24348be0685303e5eaf45ceeb867bf083b40dc4e30196f61bf9b7e98bf483040f3d565feaff47dc5b05f51574df82b806d7e0102cb805f7e016dcdd83bb3b2438c15d834bd0e0eeee92e0ee1adc99aa79fafa3e7cffaab977669ebb7e55bf734e77efbdd75e6bed7ccd1d137e5d0b759346ed230838a46e284878445804286898ff872af9ff499bf45fdbd8c2b68bff952627ece81efbdfd72b85fffbfad57fa5d90b3b3dfcd7ef23ecdedff6dfd7ff3bcb47d897f5bfd274846364a4feebfa8f6ab1ffe1fdc230e313a1169b34bc339e76f4ab366f674ece54476ded3138a3816f2d964e796fa279f2c2d0805222cfc78d8cf91385165ed17bfbed71a9b027f5cda28d2df5467d7c42aad02170c97352dc88d3cbb716ff2ecaed40e239da6aa8bb13bf6eb6b11b33c9e82b7b4ea24018aa3f6a1a1799f2afcf87d4f15972d050636f23acff9493ffa7ff872eab7b334c8e902ad570154e2aff19fe81dcf90c030952f43e42bfb487617e04c3877d067911228de70ddb747944066d9a08962fe35702d5494cf1ef982f0bfd44292b5cc982c1f42755c548278b65823d1dcfb18c8fb2ecc2dfe0a214af785375f869ced66ad18cbfc5e719e85dbe558d29b70ce9e55834e0a073090564ea6cfcff7368ffcf458b60950eeeea6a666bf1adbd0e34ba506f0828ae3268080458784428186838c8ffef0f6d71de74c0ffd67a8b77cbffabebad19ff957b52dc21f95fb98dc53d0bffb57f5f7ca1f75fb569c5373bff93e31f4c7cfb8a29e43ca294f5e4cac5e19e3f2955353a23395b58083712e155cf409f57b16c1e2315ce21d38f15b773b41f7a29829906152fce8ab71738f8cc23d27abb17e9b7fb4e87b30b29568b3330dcac362c96b9a56373df2f2f5ccb68bce9db4e6d3ff3b44522a60836c019cbe7c1c27a95185acd79f8854738f18b9cc0b4d0f5fc4fff8fadfcc43624a17cade709fe5fb6b8b56cdfd7a39694707a55eeb7405e9819e32ef6181e56b898618f5717b339e80fd88bab5eb9818fc7ab10c8f3ee9d0de1b32cb8e46846894e52f9efe7a07fabb46afea0624859d2f1bf4c1ae7425740d9407f6315314feceaca8685931ef7eb42719e825f294a181e1a5157ade559b1b7690cfdbf6799927254fc9f99f1f6905711104ca4d1d29c572046fa90fabf07d7c002e5e4a3e17c12e6b418728bdde1bf12185bfc331e3d607ef4a99b278769e2e00b020b45db68ad790646592aa0c67da6f7eac31c229fedba487d4966ad85bac5ee07840998cdf8f6534f5ed09ed890103418890755344900f1e98846a5662deaee7d3902c2dc8962d3e4fb6c3ee07520ee7c9f8c159c115dbd35f264377a21a563e7e7fbcf75fd3b74ec3124cdc2fdd05ae8f3b9bf1a33aa8a8030d2b501e3f34943e2faed132e69df9705ef21575ec0451b8f79caded313829b4a83ff3e85a695e420481120debb8fa06b69b1c4cae8c6b8424dc6fa593b85e203c60d23b0a8bb3b9838be7fea4911f710b3b1c43fd603a91d3646e410cb60f36b8ead5107e6e98d22da7b6180b8e688ea5e400bebc9f350b9f58d6ad76006c31b17107e39339fdca8fb5f2f64f7ebe28b301ad1d823be01d8a3436dc40192ef5ffe0cec1064aabaa212ed3807e0fdb041ec2dace1ba667ab622442068f30c5dfbb000f3e5fe91d5453c9379940a0e717e0ed78427ffa26e05086bfc07142b9b03cc30790632fbb0933b3faf3d8403c43f83638228ef8dcc9bc791dc6b71788f65c61c08daff5c4f8908cac0fd2331e7430f67e42cf0799066d51518dd6b2f4753f1f7c067ceff23f1f994839f563c17387f5b043eb7a010a9a76f2c4a9b47b50df20d4b1c0e10d634f57d963b194820ff89ac6230267ebbaa6cfe008c5f1a4c14c3ccf8f95ba08a6b51757378bfb580f31e10d628c30946e36a7e32b0c171d956b17991dc11a305844957108a727cd8f5f0c9ad0a94cb5b3f7f44ee3f01f3cb578813acd24ec9f8cfccbfc0a3706217c7e4e102fb8f3a79f2a565126977a0fb7dd45416e2aa75d68dd27faeb3ab619bb3be9c7b8b330b19a5f41c5d6345cf02c225db7fbf2d6f82adbe9af533de30351d3faeb19501a52cf176c22433a5a0200e4ef6e3d42f2e593aed24e0ff17ef96707133c8d249c9ce54fd1aaa2fbcfda300083fe975b5092f2caea5be7db1b298931969ab6e3704610831629bb8bc2315e619431fc45673288c7a7c405889d011de6d9572957c10f87e22767ea26688a210b0735b3224eed3832025a27a1d984b371fa0811aeed4fdcf754b9a40abc9a7181d58590de6113ee6d84f10edc0fefaca6e1b8cfcf52c3db5544898357b0b8d9cad1f101e9af9c22ff3dbacea89cdbc31d4ad50e686451a02aed0854cf48b56386a744792474315e453bf177ee600e7cfc641b8094568b6e852483be46ee97c29a35e18e8d1b3c16b674125ddc4042110bb7a73377dd3dddf02822fb5edceaed15774b811012e5e1b8935b53baee1ff0529a94b647547b92a8725300c5965a76bca54008427eefaa2fe7561a483b06dcac86e23778e5b2d05c0b8b7bbfc595e68b39daef370582d9b450ba25f487380703ba043c5f2f200cafeecbcc9713bbe25a5c35f1110d6f753547dbd0ab74a4028b9c8d038e5377addd70238700b440a7ef4474d56055ff46c50912c635ffed602c286d56c2ded0aa7577898d478bad13cf4cd598d05809bf4f7bdb268c30f995fdf0a04df7654e757c2291a8cfe73dde2b6f4a83b67b2ff137ebd65dbb14a83a947b60a10a9a37579942def02ac235abbdb3388d5a826a981f39ba594eb4ac5dd1dca238f10adf1c18c7ba1b816102ed68410c787ce28dab25dce2a2c382de97dfe310e10cfb1179692ac2cbdb3936a27c59135a726c2159d01627891bb3f2cb644ede8693ff28fb76e83328af81940b8dafce8929684a631e4cacafdb0c2d374ebf9771e80318408ea0f51c41ec55ee45f369b965568191c6900e19d4af131951974cb5632fb0b3626a2f8dabcc27720f9d343fbfe84859aff2cff47fef7197ff8a76ecb43c6ff5c673ccb32925c9f17487a87c56170bf8a9ec6170eec9f3571cb7fb8d0523073b82ef2718810ae38bd5b80f0a0d876c1e24af663af1f2a6e75e898aab5a83c0e10bf63a47c9259be1564631064e86654457a87ce210370dc6bbffa30f65ac09155c6863fe93bc275bacd2e40b8f34b0f7e7f7007d28b2c768071a783a48a3595121016e4d0a445d03364d077b35ef63f0752e47fad3b00f217a50795d55c8daf5f1419bef62ddf0a73144bde01c2f82d91cd548b837a7385b3076524bdd47b6fe66a207ef76bef3afee85d6a252c10d9c0897d79f0e570072a6a33ef932ca4592d236266f7f91a4dbd4399a1b7f7ffb9de3293fb3b2c558a95cc2cacf5408736d6af2515b87fd162641eb7fc75219d82161b1e175814843b3e02fb3fe5644f374e96eecbd0901d396aa326fb9cc108f07e8d425a9897120229146842bf3a27de61b0e56200c61b5dd5d13b1b7f399ccd0a38a9fa6c4bfdaf1feb01e12ff521abe23b57a80ccb72dc8d8122d1f2f3bc12207f3b5186ec11d7ebfc8c9997d92df3f5e97d191c2c208c8616c551490d7747f7f259156383b68fbab65e048c01f6c8ff9c3037bb119ee9d896f666c71830b53002c4f8edf397ee67c95b8668c8a3081ebddc1341cd5980bb75d872593169efd25cefaa29df8b8be7644b440e7040b275567e2463d1267baed8562a29043fa89fb500c47c47862f2c904f16c3d63e8ddc6a04c18a7c13e380b00c77aba99997d118fdedda3559b8bdbe7345ce9fee3fd7cb73f3da8d7a17e56274e1c25f2cecbd16aac900e2bae04bc05ffb9c1a84607f212846d54a4a99862000ea0dc862bf92ce4de4686d4ac85c7cce907675fc0354bf2818782586b3f91110219a663f2be94ddd9d6c01e164ddb87e8ea8bc8bc6771534ee6dcd9d6a162f57207ff92d28c4b6dde92861cd2f385a80e26a626a130208473f54da289d4d9e851d96af93b5889e9ba1b2d900c249cf6b3a5ffab7029859c7e45fee363688774bf30a00b4e8d6501cc2b72c2bc5ac23a139e223d348580708876a2554a4d04a4e9c56cb3f6546fc18ee1074370584893d542eccd665f6df17bb69e727111e875fb541e1def34c3c938ff2cc34de6f563bb1a7633242ef0c62a09c3922eaf3b3c5ccb932900b087b42c4ad4ec5078a577f284e348cbba6b95783c57ba9be010de9765100a84bf10ec2c4c392c6fe03abd944625a60c72d7b0d0ce5b308e96dc7c9e3e46363e72910617a0920def391fecf7596c0180657b7efc56cfd52f724b082da07e79b801f3ce79a481a0c958e5369aab9354d895ebebbd400202ccf7fdbff77f2b74b5fd163687b15797f14ce04021076f45227a3b94d5a372e425699376fb1a7efa09103df376577c2386b4c20f345fd0ce6bb7834dbc2ea853290dff5bc348ae67e0c870b24cdb44a19569efb93085835cf90d139acef76d3091812edcace41ddf247c500c6051e908e15d464ccc5c347833a779b8e21f2fd9e40716ea6e3a9f7bc3ce158375d95b2eefffca5b4840c10c68e64d76d27ad8d4d7234322288e0c177566fa503dfe7b6c438b53026f6b306990bf6e65ed110a13331c03117fe77d99a2e39b26eacbdb69bed08698db47006080b37a9fa68c5f26ad6e7509675a176c65ca6790680fc29d72dd2e21452afd30af6ae4f063519e2c6121510264325216f4a24b6fc1a82dd4d920f281e201c6301f237ad79addf478fa6c8c228a42f5482352def8da180753a2e77c5fb4f487572704a52e37c3f285367cf01e16f6b5cf8280f25af06f6ee3f57b40d8a79524a7c20ff26b3e58e28613f7049815bd3442e5ff6c27b7905deaf4171ce603e953b1b971cb550d231a3716ce61e20dc536d2d1f3db128b2e9fca7b502e5dd13ad502807184320856592265e896d451c4f555af4b73b9a631c00740bab645c25c6cfdf4db1ea3f678b132b0b4805db02f7736a371c36c260ca4e2c26775ff4d328bc952309509f7e527ba12454c3a6afab98872711eb9378d279028c49b671381fd875dbf56b1889216f51c6a33aeabd80fdb15fa03cb09449f12f34e955344a0a7e66908b1f005718fc4d9856dc609c1009285ce5a23255e739ab00e389c323ba3befedbfd25e6a222f84873faf4f7934006ed8bcc33a4ca9b4629599aa1fc17d7c4c9e28920ac97fae6bb640f43a6c95ef40ded897981fb99019f73802f1f35cab300b45fbdb36be5a4d9c824f9a9a46270d3d707ebcc6624e508dbf0dbd77aa3f1b8ffe2952d006e00f9d1c1d422582b1080c07ea90e28c79c1b39c9280718c985d302b6771b297cd84bcd3aa15310af6e003209cfca0b8181e153c6bb1625a543acffca52ae73331607c919eebcc33a6425c2112b03027f9ae12a2f4341a10967e46a27efd41fad23216de457474468f3bf5fd1c787e9e066b07baef6711c75f18dc3e268a6eef1e6403f98bd1670e24c83603b486592117d162f73443c143c07829101a0635c2dcb0ccb8679e0c17f71cfd7bd32490dfc67fcf88dd42e62cfb59d58452885e72c6378d04182740a1cb85bba16b4193b207ab44056ce663b42203c63c3724c3822a7f9bdbbadc653deabd58be42cc1e01c6195ff3df7ae7ef8d96c972798429091f4273558d81f8f86ff62a5c39a47fb0aad62be35bad457113773490bfa8487cb23d2baf41d147e280c8829af9d4a53302b0bf6b35c9bf530ba61bb1acdae445e77cf358cfe5078c55ac2acd7d3beeaa2f88ffde8fce1d54e979518503c2f9d665e643ad9d7baa4e998c2a9f04d4b5991563e0fca88ab91a26b5d1d9314e4af2e5187be32818c500e7b7a8794db4c86fe0ef33e6acf13bbffa372f8d74005f59f3b51b370bdeea60d93aaf9cd09a9d868018048c4f8a7fdb77ff5c46085e4b7feb8bde46d1de0e9202feff3bb67aaf0b22667ec254c59a112a99e596535ec0386afec72ab5bfc5fc003363a454cc26528105043e20fcc5baea6052d3ab2cf6afd6ed2b62737d9afcc406d4dfcec3ab8bfb82b629c6c24789bd7ea803523f81f4012298a3f8dcb8139679bad3610b9dbf60c8afed2f205cbf64a2549f1635ec2abc6ab1fa499c084dcf210808f27c42d37ff426d37c2c1421e2ab518689f0cdb0043a4e541836998fcb64c1276f3de2831d7bda622b5c807117b515cf84c11fd40e8c36630eccb44ff035b9fe40fe066f39c0e6a3139ef28e98654f8ac7a18e16aa0a302e19bc78c9a50c85bd443684b2f372c984e5189f048c3d3e23d2206e34accf62342317edbb250591ea5400f59b4cd97e02437de4069e9f173a990f0d1136560d503fdb28fcccd49fa05b26c41d5d979aecbbed877806f23302b28cf8a4846bbf7726b3e7e8bb1e2632d6b1c0fea2d7e6f44f86abe94464d791a7630d96aac6b83701f67fb2300322bbc477c1dbb727a57f2ebdd92fc3c0fddf4f125bb22f26569a715c2425de2c79fe7eea3d60cc351e935fe2148ed220738ee79d1dc0d7693ddf0ec4df8538fd375a19767b2e1f46c77ab27fc225ef4f00fc869af7e32c049b0d8faafc3b9e3a01dfc8af011e00779728f8616eec5446b00b52f42f46dfbbb78d45798003fb296e1bbac76c2e6b723f3724662828815e1834c6f9a5cca2abd9188c3944e2c483198e1935dc8ba506dc0fd88252b30b9f6a3de88e4739bffce0a7a78707344056f053d16548bc4942f89e5584db9124c8ea0b40f30bf5e9a3bbb4d024c62a251e8a654648f13783cf407c657dc45b4f0f78f41b5959b99afbf2a63f34e40918233ab9961628b59eb99f7d99dbe22ededba648ed04cedfdd6d22ffb9594583809ccd908999752baf1b06b0bf752b88e7bbb5798aedcbf79a5cc6b8bf9c86b3011d519c0e935946b9d7ea56a98c4ccad79aaf9b2b3f00e344129249ec89a1d58907642608629ab642865662c0f844601d66c5ff119a41855edaaeab4f838c9fcd1678bebaf9e8e4bb6c8f63e3a32d224ffa6d7ad8d70620bfccda0f28602fe6a06064eded5f70178fe34a3706eed7c46f630bdd950ed40c0f64418d2bf363a459b14047bb616d5603bb155bf18abdf41d57d965ae4c3715505f16e9bfc5bc50ffb9a75b243028fdf5ac1167a101d4c7a235aa57bd1f24784f2d3131797c58e8cb337d80f367c03efdd1412fb9fe2fd7efea6c0335c4dfca1580b1c3deb9c53127055957646dd3911d850f83bb110e18df67717704791e1ea679f46d429ebd0bc87ec203c68fe66a731eb93bf1f603a256c1ea318e8346a3288071dfe4cc2b194db57cd91d5c3e3b5f4f3b152e3f3b70ffc829ad57efe79947265b17bddf99089d2fd90347917121114b1bf2e090e3264b7758e29d9d1d352102f8e56eb0f4c4b687171633821db4c090efe907c97fc0fd42f41187d550b191e2875a3b7f197719bdd48b23604c723ff29cff8355b66f0dc905c559ad5cb7ab191d303ef0791aa43ceb784bfc1dc6408fe8afb9391e0a0dd4cf3aa2d239c7f8782dc8a83b6e6d0836934869d18071e687111f2fe37a651f82159292cf192bb3a9c8c8007e1c0fa9a59862858418ef17b6dd7cf19baaf08e0ea85f3bb9fef172ae9a60a9f49bf7beefffa82a7b5607e0c7737f60208a552c918a30dd631963b5f0d453f8013fed16c149d1ccbf66c985e13b3f02520a729c297380fad5a75686052bbd38a7c5c4a4f3e53303bb46a90df0ff23bea13e498e145b53dc4c73fe4a3cad600a1f04f08b9eab490de1c043f1a57ea63393de833323832f8031e6f4f30ba2b23dbc594fc4ef269ac3cfde6c5ba9407cd5a4a650c24cf223cae4d3aad295e73deb7e1b05f04781e3d23faeea7ccda6f399fafcf3c19d54b95dc0a075d72d7ee8e3b6b951cb0f5c767c67e73202bf4581f35d647d0455b9ec5422de98f7f7a8282d69267003c057d6f6e5e6c376052f73dbf046cf045291ec50240171b5d90917f683414a07336f41a111f563b4f5e92d600c70e3f04038501383fb5cf5da1084a52bc449bcf011885f19a3b5582928c48df92c32a50e84cd4f5c4240fdb74c88da23fda65a93fa655d567ab06aef1f2509703eccb9322d3f455a55d11bfad392e4ad78dae86e01f505ba26c5ce7eac5f20c2b2f8303685b6033e692110df3e1773729267b165d954905dfdb6cb167dedef05e277cee1f1bccd5f4304833e17623805b11c570923009fcc2ecb0fcfa698fee8d0fce562fd953bebf7b23fa0bd1ab44825aa282b8ac60e23354f9b89d7fce0e601e4a7e1c73f8b8de9a820f8681d22681f3c08e43c71007c7cf30186954d955b354738ed7d434df70aeb001c6028603be7aeca5fafadc3879179c7dc8195f538c10ef40758f33cb0555dec6fca6636f2688d16b89266030c00fc955116f9faef3c255101c9276c0ff3f6e0e740e0fdd3a7bc698e5086a90c35289e71b41ec1b313f900df778b9a89f7a113897307d3315fd3d307c5338408c8af68ac123a8fa4950398c5929f613f099d12e2fd03f2c3d8e4bbfc13fc69698a7bcd7764d44f98ad7a1300fe8243f53eca19660d2ed5da3c8fb5c3ab4499861e30ae74c8c6ff407f4b5d44cfc67fca94558b50f2900c2498ca69bb51634dbf3f4572fd91e3606337fb655b0be03752a74273e7506a61752f91f058261ba5262dbf81fb1bdfe186fd38d43a3d4c9691ec482420c1a4ee04308e44f9ac898db5b45e5c9bd9b5334aeb80d0684108e45fd3a686dd2fcb7dac77d7a2f66a9b337565106a008dd66492df393a5d13bbc94710717afa0b0e6f290b80ff468b5f86fb04e1a7ff54fff1ccde3ed22c9b7508186fc166d2f8d6938e2ee88449b345d5865829d1a701f8fb5290af53f51253e14e79a3eb1c41f3afa23b3aa0fef425fd6b90c12d48fbaaf9857aac6acb3b11ad1ab8ffeafe04114f6cdfaefc4d646b113e64c01a702607eacbbee17da29c1121ce138285b93494376e8c7f2bc0ef7be653d6980b11bf4ab3125ece6cfb9bc8f32e01f717d766a13fe52d84ac38b79d990f8ddf4da77b08600c42c0dacd1199071fe41d0775d49963d34295420ec437e7ef965941a89ff36ceac636d41bd06b8e4a4b3400fcd60199548bc56bcb7c05dd002502f2bdb89a03f07d6ca711c9b1cc2e50b98ab325f7a82a115de87c0046d679f76729e15338a9c5538d2fc20c0bd808d89b407f28ee67949bcab96ff661702759da9dcdeadee20b608cf714493c1f50813a92a3f1b9c635fdd1a9ccbd1ae8afa865edfa2645fff8508324af81ee9d737334b200f4d75f923ac38aed6ffb32ef17c9d5582ffe66c8f102f85cafc2a72cc99ab5040aedbe105e9577ed03043f80fd094d555478719fff79643b577af364da0977d61288ef6287a950f19479cd4addb074ce1641fe8fbf0681fd9138385c64d243ff145e87e3ef52d9747a9e8602187f53ed880f8ff3ab04fcf8a471ad1863d7581191039cbf95b6a021042cb73246742fc8c387eb278d0a088091e8cf2bab5744a8a1a484c03ff3d8b713d3834af409b85fbfe1b123baf5974f2b0f3f949f70cab774fd05f08761742d3af9118cd5e3ab279f42bcf2d37402f07e40442e29b623ee8b7cc1e9eee544331d6c332607f2678ea9a80a337609c4bdb78d9fd349237129b7c30063308869486d2c47a43ede981c61ef4a5d3fc59405c00fc3582fb70869bf19f97998141f097c52fb2bf60cf4c7604a706b3efbd31223bbb0522e53f9a731406c01f80bd66e972906eaf8219924c90baf656d6ef3b805a02399c69f80fba9f67eb8b8723919c7592a1541cc17c04786ce3d70cad2d531dae9846486371c9449b18c016369c19b8986a783dd24fbf79b9fa2afabf2844ad581fbddc090a074ab6d9979f6306f8c279c7da5428b1ed81f76c7daf6f04e5fb60476763e30e67a69159f8febfce77ac5cfd95134f6bc7604fd50171e683e5fb29273009faeda80d9244c1d0892a265d24b748cbb3ff6bb05f1e57ee38257b4ef3b052cc17de51246b89866f6c0fbbbcfddbdadb597db52dea465552092a4dd642902fabbfce1817953bb262a5fe1ce68964ad2f5cd58e980fe8f5acc8f683efafb4297daaa58c81fed9b7d50f700e5b4ccc6434fc0fd6f957c992e0dfcf244419c5d0c703e2f6d955d3f617ad09ab61e542f7c085419c96305ea8780d76f9e30aa1001a64542d4c9da9754269a5180f1527f8cc1a7b591b7f2860f78e792ecef9ccb7d0780fd6786e6446dd693c24659ca90a17039f3fae1380a3046bc8835d6e4da284a435e88df66c8376ce8d74307eaf7cd967d4f650a7f4b61bf8fec031da148b44d3f81f3a16bd162e271bcd448537b94274ac5d96794ae0af4871f0e75232e74a11f5cc9193ef78cbde77d11c505f809a429b8f9f282a55f674cfa3c1a93f4e53a501881fe4ca0babac12fa46fddcf08f8d0c38165161108e3407d8055e532f5e4eac5b6d9f874a9fb988cfe7b7e1430f63aa973f9a8e01e3ce0f85742ce578b882274d81130561c6c74e63cbe88f0e4ae39665a43e3a1a3b2dc01f8619caf7f9249fc115fe7ca379a6be1f60c86edf380fdf398a91422911fa743767e1d6b833156f5fde321f57faec30d8b04d4abf31f1be768d5f09e9bd64b493402fca92784bb869ea85d8a6198dceaacdadfe3470a6b007f40566295d4bfd14b26cfafaafcedb35be3310c1550df1adc9bffe2523914ecb8bd9968928b9524d3ec07ea5ba35ad692127ab2b4ac6b1c026c793a06668925e0fe52488ee968e5f41cdb80ca38aaac0b0b140ab205eabf762beac195001e77ebee63e20c970296641d1fa07e85ce4d0f74ddc5ea52616f88dfd619a29c97fe03280dd8fcaac2b42ede4f95cfb40f55f7cc9bb68fa802f8a594a702d52b06adf293ae96614b49a83ac9351e805ff40944e1f541334c395e59e4596e2f38cd791d01320086cd0898e53ce2202eaa8e3d42dba33d871812c0989f963fa63b464a505921cece7449cbd2ee8f0513608cfef9bdc70f5c24fcc5c4eb005decd8befdf9245aa0ff33838722c57cb5afaeb4f482672e15668330f90d30160fb97dffad595aa4de8c58c4a7a8ff82a50b2d08e8bf3b068ae2917fce26fb38608b526529f970e7a40cf4870d43c5b4f5663fc85e926f6c8d8cbb48af380602fdcb4573d452666eacb6f46a9ab028381a31b58e3ea0fe55b93e2f67835dd486d550803dca61d7eb8b7607eacfd51a7bee84cdd6d28e62de6ef2e5a83314df7f40febfb61cc3de5222e5b61b96307dab4d558a3d6405dc4fe995fd4a3bc8b7f7fa981a873ee53b38a20eb6c0fb29bff8eb96aefbfdaac7bbe02f6f5edbccf42916c01f5c8fac7b4c1fd8d085aa9b82b849116f27bc60009e6fae76be8b5dff43088da22cb1d27c489d24da67a03f69e1b93eaba5c57bd776cb252e50ef30dada4508f033e65a496c10f8481762c9087d57a824f47f14ca01f9b7eb06672217be63c18232a471d7a04e208cf539d01f244fdb883a7eb9474e63367cd6b69d1cb3e83d00f07b3d7dfee89285995d5a16194b9a6d232e3596efcc407f7a238911365c94e4e223a4307a4d2d1a9c5e37801f9956d6903f2b8b59a9de4c9ce8567fac1ee04e000643dc3474f355a12bce53607f897fa1160a5cff9d040ca68897094a1aa4f8fb4bbd026fbe7c2101222ad01fc04727b21a46919514664ad9a046f0e3fb4ad8e679017c5474c380116617736f5eeff76fd3847c0c0ba116e0fe7cc9dda061f345b27c2f5ebfb979ecbb9ec7a106f0e7fc45fae61f67c2a71bc34a4ae5b93703c474dc01e7c8959636537b58fb49fdbef3a4cabae29b1ef933c0b83f3b5fe528409c156a7f959fbb5be025e7a74ba804b03fa8d98f9a76be92f4ab551f6b6f0942b7f18a03f98965d6f4f5dc8f7b1271686cb90b6bba5b0d3723a03e6476e8cb6c0d3286237b105f83ae921fdf458f02f2abb79075aedb4601af3f442d41a234766aca863140ff0f32390a8561f944c65d0b8f064502f2bbd8551510df96257962cebb9b3525a9d6e560dbd7681713d1017c4d85762126b0212142d1af359806f9701ce68721c06f79d5f2a3bb3cf5e6de1465b49add72386849f204ee4f3fb7f2ce44d184b7d2d2d9eecd1fec8359b6a2407f91f7d1d4cb80760a66dae37953babf332c7f290ec0d7a8f9140ba74977f8df717c6868a1563a52cad007f8a7135612961fb8a3a1c882aa457233dafd67621881fcea589d84863faf3823cb45c958bc23a0f283292a10df10cb1eaaf7fd6fb6fa3854c8ee143f3c917d6903f00f13b45a5fa5008bc6c38d9093063909fa18174a203ffa37e9762cf99133bf385dc7ebc8099a4c7e5b10e8df5e3b761b9286791caad0c193e9d4d2d95cff5202f0f3a304b60657c3528eb969d8d0a8387942087f75805f69fff173c818dde6345fcf77e9d7c6079ac4252c20ff4216f119536e9c7c795b82a35dd50f2c695f3004e2b3f5aad12793dcd2fc782a22a369d6fa044c7704a0fef8b62d8b653dea7c9922fd6e2b173d5f7b2e241c883fdd553bb444ef51da67537d778c0dff297e0b5824077e1fc083e548f72fdb8f02cae4bb43dfca978324607010ac7a28a3ed9b0fdd979c6de7929a3b2a283207600458f1379ae6f5d4686ee7930b25685528f7fdc15da03f876042424bcb84c6f2777133639e4749b124440fc8bfcc02bf3997dfaa2fe575358cfe151d1e8351cf07ceaf1cccc603be74f7ee7ab19fb34c547a4d4f490d20598912793f803f90905c2cd3f36732684c8331530dc8dfe6b209fcae8e4370695d28095a55ac1fb878dc01feee94f1be6f72a4e10cda281e3d23fe4d7e63b92fd03f649f825e6b1cc8a6a13b4f4d10f639bdb86db806f0adef37454610818f91497c58dd1e19e46152d52300bf66d056804350bfd8a928e071f09c556624cc9411c00fbc130d0dd69fe8d189037dc217d81872b566a601e373b64fdf2d8f52b0aa24f72f1a9d1613cc981a0a01fc637a1605c1cc8d7a7c77c5d9038ebefa46efb51bd85fc79ff496040ed93e4fca58347c4365e6d1f06807c439b05375d1bb98263edacd171bc5f24c9a6cf8e05c1353328fe71e4896a3f0b43faf6a9ce1142fdc2900ffe39a00425b487bb09354923c4c9530486619aa04b8ffa8f5bfdec8b188f07c2949bef1fc74c133e1530e787b9055a518991844074ecd42b9780a84e129476502efe78375f603cdab9834c7dec6f0771a435c1f2a36603049956bbda92e0cc5a7296aa24933ed985c83660fe0fc3740251f1d69cee90a34620c297591f9b8c90d03fcf1a4b2be7785a5363d82c8a80c7b588933e70bd880d4d6603fa89a8c4dc6463b723cdc9dffe403dac21d909fd6056d7166b27dc514804971f52184d1365e9d00f24b64f9dbb285fcfabe963ce5ece022a2b10ec42b009f728d64e31cf9fb57e81f5592c9bde68651d9080570bffdfc4460ec422d32930dc57a59331e168e523a050cae42716718822c2af0b736bf8051e3cefa1a901006f0e7cc37a02f3dafa689d0f2b3b60b8f94ae24c76f80ef8766d81554a07395ba006f8aaf4d24838cb72a08d467699b419fabbbd3484f37509f59d3718f177a28017cabb5a8fb036965a3dd0d37e512b10ed674c5870720be6da8768c7b76f7447d7d9512a67f7f6ae0ae6f06e8077c0ddfe3ff38f8f0c0a2a525b679540631b7b80af00bdb613389e859b8dbb90b088e654ca5a49da00c81fac8bf9185cfc03d9e22fdea41684e287ea6481711e89fb8a246255e76f22ad2f4bef324e17cb393a28106ea4b01cff9e94dc5e098e507e1f206e11dde873c7920be072cce0c50fe93dc17a771b1e5f85a4e40b8cb04e06b9679ac9c5a2e1352f533efc243a8debf95b7b802e7f3cf6dce2f28484967677f970f0f118478b7e73f017cdac29baccab75c698dc52b5cac040b6911b90819c8ffa270b346838e8bad3b262c21c747a7eed0782981fa4b2a924e1e7a6631439571f281ecdedbf0fdc741403c87199c7518bd4f65dc9a919da46a68a0afc0880ff44724c48254f13e669e0f6486458fbec32bf663bed003f447f8bf5f702093d642f52699ed545bd36671a3017c90964a8008cfdf93f8ea3623eeeff2729f34ca24c0df211945bcbeaf8d3fc7bcb45f55972344988fb105f0b92a6b53631ada2d752675cfd02557efd6c97811a03fa287c156962873f1d12119268ac5bb9e5cb65601e87fb418253c20d1e97af1360e11beaf5961bbca4d07f2530486ddf16388145d54fe9126f60741731c3455c0d87eb47c2be6676ed477dad99c863beb375391992a805fa0f7370893131a5d5c7c6aff091f630473d28e10f0a8454927bc510ba93fff36973592f155f226fd1736d01f8649ed5290cb52c9e35420f09dd395293df95806dc1fc2bfec857de44a875c6243b6177cade9b3bcfc01fd8a72f41f22833ca827cc7ddf212e2a362aad4e2920be29347ec6c2749b0be9857aaf9288f8878ec04418e0b7340edb655987712ffcbd5b784b6c6b46a7f86206ec1fb6dd28214c3ac1283d127fd65ec3a83a2ca361a0fe2dce090d70c511a2c98b24fa90167da3a5bd4703d40f3e7e843b3684cbd17a6b48690dc733ca5bf2cbc060be8fd1ccf4ceaabd67ade24e7fe027137aa80298012b13b75de42f72f85cf54e550a58e25ac86f3e9f5e01fc3f13d9190d6584adce07a6dc77b1cdfb98a32811e83f37ec841e7e8033382e8eacf94a366983e6722bcd04e037b7c18e34c13e4f220c3e3b15e45b6c9e1edbc0609da94737ff0024c313fd37b1c797c805a7d6b52f803e8b37153d527b136b78c25e943fbcce470ec7a202d85fa87fed795c6dc41e0b690b02676fa130c728be01eba1d2415fb1e1e69569ff95763a183d2443198801fcfcdd1c8f16331fafd2046bf3aba6971005945b2ae0fef29c821094b70ad0a73eb6375fed955978543d04e2ebea0cc3a57aa49c077c443649d77593fc5b7901c00f322d320fd4ec8858780ec37d37ab852e1a046b04f02b3d99f5633dbc958dd1246b39e3e12eb022ed3981c13835474adfd9f2c3aaa69c56a6e433962cc7ce3980fcf302692136616d21e51e36e4298b3bab66ed8510e07746a6f646c5e17084422b352a5cc01cb56025ca03bfbf0adabdac2530686a22f00c13a94b52d2cdd203f0919e2a23c93ecc08abc0f75bfda5b40cb0789cacc0fe15a637d13236222156ccd5bb2f0eb76814e3e705061790369ede08c10721890593eb8f661b4a6ad36c02830d51c51111191423944d4a2cf4e5fb9ae6b3be1581dea9d669c2f0677f02b36d5df5b0992f0e66ddfe01fc47f4635fb7161735cf1794aa4bd48ddef8b8de1800df1013cdaf7d6442548be26d908629930ad44ff9067c9f7b1534878732a6cf15428bb8e22b5b17163d0900bfb146c4a06334f90e9646b947f3b2dd028e7200803f20e00284577592ae0fcd5f1ff05fa336e676d40380fe41c6877066f4aba1efa124f73c889ca8abe7530c003f041a6e1cd212912d0aafb968bcca6e81f7336e196004822901bde7961efc1db2db87e6a2bae31d74ff2d507f73c29441d542b19d39afc69beb56df2cd504210083894afdf924e7952af6ab95ff6d081838eb8fd02200fcc8bd96c169272f991b09e5a9467e85c122b6c97980df7bb0f34ba41f9f25c7564040544775aa7a393613c0a7d2a99e2a866626de25fcb9a6f9f855bc765bb10da80f14a4bca052e577e0938f5deffda90a056c16f8007e94e058c19018ebb3ae16058d6f17f6ae7e260f0670bf1e1859b48659ad2a49d0d0e28cafe97fe4bc2806f45f4924ca11d2a9de23841dda02ab3130983c3e7a407f3a602b9a26e1c3b8e8c0e5d0a21deab995290538f8c4fea85da0ba88325f2873ff7e1abe7eeea45913182cb966f1ad5a4341d382d3361b8fedc1f268a88e1dc8cfbcd4c91b50f0941c7ca9f6211fbb1b2443a5c581ef33bd6a4408e5fb8ae5d8f435ef5d44a5218fa124505f71738bfa211d4ac8850fa6e7bab3a988deff6201f6672aecd099a4429ed9174d84f9485fbaa39e9b8fa0feea33ae71a995dedbe0d111ce523f0574411e35703e895cf4f30d5ec5be35a5d9b0fefe3cb279486306e05fd8c56d07db1709096f730c81d6e8cbd4e90948f2007f0a5f2e7d2aa9b48be487769b3f160a8eed1b2fd09f19d07324a095fb219edea6a6d7a9f46d20766f1cb85f7a1dc21772e6a27b59063b54e4054c655ee139017e47b886e91ba571a207d77c76cfc33f84b2fe244e00df1fb16023d70de8e8d1b2c16842aff52c6ea86100fa9f27f897216131d5ddc2eabeb19eee53cc75694300bf42ec64d7bf4a852811a7b598f66366e7a1886e17585f8878b4cf582b1fd88414e15e26f54c68e29202f773bb6ef535db67795be8679b61bebe2bc70cc15ec09124c480b3d8926fb86378c2e4a7245dafc3b6eb17805fc026295b29fcc72da9bf39dbb0ba7d7ee9fe6e0c180c74c78172e311bc036f4968a382334546669ae60cf0bf1bbe7df85768953c1b99ddd72a560a89d82ea600e8c3a9b9e38a6abd529d7147e2cbf0a31a02b1cfd880fbf5a8f53dc5d69e0b14d94c482eb7cbbdc3e3bb3b203f18f3ee1150460e5185e99974baba2c705c526701faa79613ad0733c305aac4c1365f0deef5e8edce3e01fd53546791360bd4a8d9378dfa5f7ab0b6dd051a1b407f302f3b35de1b2557f18a5d49e0cbaf1d9aa17787003f039d0cc7e793b2842f8500fdcf441a5c79334f76c02e2070ce8bf118973573fe9626bc23fb9b5773b714804fc18e4a3e39601aa3683035928c7ea43c247f8a9707bf7f071b9cff5a0eb2ebd3aec39c21e7d5ec1c90ff37c1db952221883aab902cd2ec45f422fc7a5602f451f65e3bd38d75aa0c10b2463bac9718a38104db00be12e15be379d5a7d475e8d8cde59077514033450f0cb68afef81549d919b16a1da27c815bb53236546106a8cf64cb7477c26e45548ef5acae066b8f15205dee81f850acc992658d3b04fdfdb9bf4ca060b69dd7b607b8bfdba5eb2eb0ce2b050278d755f73116c427783680fe7ff2e17da8b76dd994300aee013ede06eb4cc602c06f892f4658b717ad3e602f2e562b1c773582f7d50006ef5297e8d148c82d04702232ca263f6ecd5ec84a03f8b4b9f9525a50a79e8d1ef3dfc248c4d0c7d5aa4c805f740629dcf82a37f4512e56a6bd152ff7f55a8505c00fb0367a2a776c9d5d58735e59370e1a84c954b000fe133449bf4cdeee015980129222d3499b3691b401707ee5f8e2ae832462aac324ec48f8509ec363f7b1007da5a42dc5a2fc0ffce4fa4e213452b7d65449930a60604a4addf169f093277a0ce23b495caf586f425642009f2cd66b6cca1e818295bccc5d8e89b4388cdd6300fa73872b572610bbc76d2fab46852aaeb0dea4fd8b007eb579e0416645d997e60e334df1be133d45102e14e03f3d7c15ed9cab4129fe52ab5ceacd8869d0649f04dcafc567d14504fa04da517035344e11910d0cead6c0fb0d852e5d15ee71edcb27ebfce1fd1b29e42e1d0fa85f64a708bdb814dc141efe40606faee3127f7a5605ea6bb9c200b82a46a8b66719628dbe37f8aee43e5880ff5e8bc218082dc4b19b2587af6476e343a7b8862004e0f75a10790f95eeeb58fd684ed198460e010f7f007c3b7d652b9ae091a022668bbac0efdf390c16770c10bf1bb889180413029a9f2f8e972148cc03d1f49181fe8dbcf0a5ce98ede95f9ac054661b0b8113fb5a5c4e403fd2b3a65c6ace3ec71832d004dbb74c692130c9f69feb78b28732918d3331185ff9f3fea8a8750c7f4302069b87f054726016c51ae5969cdc2738f8dbc4045203dfbf8d5744ad92949bbbdffcdd69b2029662c6a022d0ffb4711b6d55dc566abfb88c2fcd1154f22084f900e207ed7505a78b48fd2fb54c2ca8fd142b3abd8800bfe58e8e7e56928f4bfb6a464d630c1e9f10f76507d06f2d2fe5da4238dcbc4de45e0a3962451fb74e7603faa346d225449ee0dcde0c25f82f6cf7a7f3ac5f61017d9f4a4783cb2d7a11d15a4897235bb9f78dd9c314802fe7335249cbef0d8be1e2234cd4478b2f4369da0000e4ce12f4c7d362d15ecec2a1239db339d33d385db03fc7ab21ced91857c78f971ce747fb44a27ec10ae8c75de3de9f184a9b7c5c3f39d73555d9a9fabd960be81728726d877d19dc826ebae53ae3a11c717f5d2302f5c7bb1649f42019b808ecf3a22c2958e6eb9e977ce0f7b6d042bd4436f614b04cd48fded481aab96943407d77e71aded588cb75aaad14143726b99cb5ff1913e8cfa9b6439c65c1b4dad10ec671089466b7b78aff01fa0788b841bc8f2766ce92739ed738d79377c9e47880791073b6de47925f26cdf56e05957c747472adc9c6003f07f592ecbb02a925d57cf8748de06755192a9238409f792458c7e0ac56fae172be8e0fc5b8ac53a49802a8eff1a5d97e04f45d6a219c3ea5e85e2d0eea12e001f5fbe85ed19faf8edd78d8da213f606b389ccca2b581fa5aa7724d437cdf56658fe00f43ce9cb14f2eba06c08f1f8347723816eec943d59e279850d6a9d0133100f2338a589aa530175fffd7cc739c931dc5faf3296620bff0a3343513f691cdc249fb08b594475775db1408f4a7b0057564f4614ed302aba47ee770a9cccf7d79e5ffcf750d9c08daa3dfbaa49f4659aa105d5653ec8be5017e2f2b760d51e68cb2b017ac608953a19c5a0de23980dfdc492517c49a3454fc7eb4ec4d5225226fdd5e04f879049dae236224d2e49ed171638aac9285018e3a007fdae9fd01eadbe3c6a255ef08d4f38a59e109d60fa0bf2c975674e5ac51da1a76ca976ce88d894738f708e427f60f2f7e0daa1e842fc1b3f7eceea8a7082c9d408198bc4b6b511996399d97795e5b80624ee4859208dc7fd0d2f1bc5b7506d71788a95f2b8fd2a6af5e3081fab381f78959193e1c39eea20361080a8bfb22dc12887fddfa0170ae73eb037a1c160e44c49c4ae7ec0140fd90bd8c95c5e07b11f3b68c3e9f7a641b8ce7ee04f477cd999004bb91bd8f0dba26264c8259ef937ad380febdf73a11d16a24ec40beebb78d7e3b68c6083b1de0fe238b97363c1469a580293b0932a1977093654604facb4eaa0a08bdf94878a2291b12cac75faaf342b880c199860e9cc4c385125b7af7dd03bfe32fc51ebae580c1d0bec7b86d113fe1b94574a3c3644e8f638f4a2e007e1393cdc25c238635d64f932f1098266a5fd7d531017cf2a7eb4fc20a07321a6eb5773ffab47d963eac7f02f23fd814455bbc6e2886d3740dee3f4d8a9b4c693340ffeea0aa7c491e0fedea9feee584137ee0f1b0041580ef1bbb79153df74d87e5b70b2c198c338a13089202fd95c6a26031c49b93a89e4b79597e4cfe5961f775207e7146f0e3147446d3264a3d90a71a3fe547a97500fb83c043cbebeb127d224a39b4c199e59950dcea1d802fe7e7d0a33b685e4550a3d6c6adc74ff83fa59803faa6d71663cdc9d83c5d76f6115945b49dcdded31cc05f222c1623009f96a668495f423bb4c39155bb701de8bff34d7dcefd21c64252ecb64314a66bd6d7b1fd13180c5e12b5115e853da8fa3ca92f20a828bf6d78040de437704fe76607b41b57a66dd36d1bf843cc3bd8d6c0fd2c56b94fbe58f2fc50617f2ffb61aee3de8de5a506ecbfd39466aa41652ab2df7cf790d9a2c822a700de4fc76f550984ae435b94e7890cef9998daf8f3bf80bf48971a8ccdba7e72b20aea9b6b5704227a5aa00db0ff475cbfc7c861fa345cafc945beba26cc0c307e079e3f5bf7be52958a2f215c0e6a938e0d77e128241de03712f6f66a6b339cc01748760a043a9467c4438f03f89455bf34dc9fa3bd8efec1a799af616bb2c70be400ff384f5ee7b26ad4a13d70313c9ce3b2ad4558d202c01f693618be7858e0e83f5a185af5dd42cb77cad900f5e3b266986a21756415ea1cd2239511ca4d10f930d03f205d3ae1cdd5ab0a280fec9bbefdc0fd7b3b3d0ae83fb7d6c71fe45b714356d135430b10e8683a788f03f92115b6464ffd461a6ff18397cf69c8653dcdde23103ff464d1e6610a12318e034c11cf6cb0d80e83d681fcb34dfc22e05ffe0c0399d15f0cbb4f6396a55f31017cdc7c7ecf6220ae519bcd88e46aead7eed4bdaf2b70ff6f50852827ce708d1cd8bbb2254040c507690d01e67e36c20d45fe4ae971bfabd864dd92f76eeb233800fe8476d559776d84eb4d7238ebec1291dd4e8e2b34c07f0d90ea52ff8692a6c07728dbac7f3c69d37f150ec4ff378a075a2e72358cb40da1c43bab32eaef7f6c81f325440e69c43fba4b23cfae8854ed2c270eef6606e8e708f99337f78e6dd92b27dc513ac2a789e01b6781f713e64b644952e562c9b15d8c26d227c40d916e0bd4970c1304d92166cd155f4775745f7872e0d470e05dfe73bd99928c92e3785c6edb504c3907d5b308a3fc08189c7931b5e73015345a6e8056b00bdbff8254988703f0cfb96c08b3c6a6d8501d5bef9962b343a64f61bcca407d9e15b5ec51efa1b5828d31d35bb122f1a920509f6e09c422addb91615c5e6aad4d5fe51ef436f901fe49a182e417592af49aa57f2ab1f7f2bb82255a3a01fcbccc744922f45ddd1815e223dbd212bf8caa4b26105f0ce59ab249affdec6c6617b810f70b6cfb323b01fe9c21224db5364e865d9f07f3a3f1c6f7f31e84df96c0fdd031fc066d48b028ebd22d2c14ec9a82c74d08c49faf3a14b0f34f9adbc17c712ded755b049fd4a700fd33325f6f348735fe3a49c0c6247fedb9c7f6ea3da09ff08c4d749d15d955b83e2ee1f0505bf467ab2a04ee67a367cc189a31a26751ad1fbf31f2df4a982a0280fd1da25c99d91c2886cf8a847c8bb465a03f0a7906e08bbed6fc5fefb6286246e5dd68f67fc8c3f2f2ec03fcc4949464740664f1d10979ec1a26b9e66a035367c07fee2ea107b3dbb991e5c3130734b9a720ac223378fe62b4ec864c1eb9cbe5b3356dc462e7936fdb3f02f533749237fd276c0b735add5f472767df7b6555c681fca2c3d7d1c722f4dead5a8185df725fc69ea2de1218bc6a2520684af9b66d8bf4b7332c6f79b44d0a8a09f007eb63417f3f48b83a95a6a10dd91bef42b32f2505f053376b435a7173be276a8897c6f069bcee3d0f4e02fe415a3c0c4ada258357d01501cb5f59e331a5047580fa20333d2fec95a19242a6f62db4a55cdbeffa3c14c08f58a18e521cbd9b7da2cedb535c6e7e911d1fb502f8c0f30893fe9ca00c99f89d4b30d7e676d53fa46300bfc0ed879c78accc54a58d90ad429ca00e92b32203f0311f4883bf9dada378537ed962673aff9494fe2900f16ba33659ee26616744e6c52fcd51bdf0e5a4dd0e787e91eeb5b0b63e55280ffcd79a6b0f8ccd63ae48207e27e94a0a7cacdf4164903ed178dd680ba8c3bd06f21bbf9f5c69a66b8731750b3e0f30b41f961e4e7000ff8347cfa0505d575cac9c016572e2e4948e7fb1eb00ff0a156621c4ef8a87651cfd8c90e36bd500e7071ea0bfecdb058f5b349718dbdd5cc7edc6c1e5e2cadc0ae48f0915ccfce1f3bf45fd7598deedf0775e7124c902fe976664c906fff01c88a37793d091a698cd43a6e601fd646cbb40bd28a72cf312d63bfe2f414dd7ec1afa00bf3917eecbaf59e8cbd1489fb8e4541a694716886520fe7dbf0e5d2b5ac1aebd189fe53be7db1ace506b03deaf4a523ed1bbd7cfa1aefa8deb034b5a95ff7acf01fecf6cbe517be09f6226a4f39a1b16050affb61a0dc01ff24e66aee6e78ec5b8e341aff868a953b694ce30103fc6d06f243a2592762717e7e90f75b0298f3f3203f891fef29ecd331211eb08fed245f921f94f3d066f20bf9c53a7fcd410114c8571b721ad1dbdde794eda07e0c37f17f3058be32b58ff1d3429455bcc668e607103f589aac2e48172682cac50a983f52fe8683ecc386e00ff6f3d8af6b6e848ebb54e2465520ff950f0a12717c0df59e01cc7e443a6ce2f91169eef39b06f74288f81c1f9f1d1cb6ac5dcb38510e571a3a34d4156e5b86e40fe3049398771d6578fb0387be44ff35b2121d3391f787ea3ca2941a97753459397e6f21f7fa334f284a602fa4636c476cc5f01541b892369e97fd423a731503701fdfb29abf5fa7dab3a6e3896c0fcfe7b6b2f5f4d70f0b8d2ab9cc60b73879eca598f9f03efba397a0f1bb0ff3b64a1e86891aa6c9825bd72e984d22a6d5688c0fe8593b20bbc198254d30cc137150e9cdb702b45e0fbce7e343185e5198a93ea52f0bd3edede6d68d506e2f72be1f8974a5f529862b132a599498517657737007fb063c28162f226d0e595c9f8e9c7eb22d2396406ee7f6c0bc81fd69dfc885d7482ea861c8999cb5980ffdbeb7a4cb25629b57ba64f11acb2c129d5e3a91dd0ffa2c3663a95cec6faaef38a8d1c7a45f52e5a238be47fd1173c2868757d9c28f0cb47a30c62f861ec0cc467e86583322bf111238d5af74ff3eff1112b056080f88ef8768d42b19946d832a5dbaafef39ea1b4070788cfe1ee79b722c726ce768a8f214ba6b277a5a91180fec7ba0e57c095ec7c60b66c5329109f75c4fcbb33e07fd0ba5a0823176101d1218e5db3198824fba03001c4a719c79f0a010221b142f0b89175ccf2bf5ed484017ed63302546a79974567a82abb3152b2c870c7bf34207ffc7e1a8c93ae317d39f060a60b2d7e406606930af8cf6296beaf50bfa9371a70f330ba703963ecba3505f08f4e169b877b3f66c55f66229b79e81a9c3502d280be39df59582ae6f7373808e66f30da3bdb99891470d260fca5da51d08f8526299ec1b9afd355fa49bdd401dc1f71496f0e96e246ab7be5d9a254f9829cb77280bfe3c827e9b11fa1cb05075b055c3f294220064c09017fb2a82951426537a112e9047e68ad0f13d1c2f28140fcb1ce6da50c679991d89671df8e185ed9a00eb106fa3fbd61e77a399305e1268ceedb2d9ff214ad49a080fd8181473b5f1cde873fbdf833a813b245f677e830802faa5c1852183caf56624b9336ffc0d3558447c200f0c9a1f5830ba3c4ddf19fb658c9b92a11014e1df0007f7d3f34c4a18bb87ffb398c4e4d6ed2a3b6e8197507a82fb13c96e3a093358b1d908a5a1e2cabd945be01fad9b1c6e937fd44aeec32883bfea891c7d0db280d401f2f41f23b8beb8f7bc5adbcdd9f9406e1aff13f3f02feaada275326d16509c11ab0e2ef4544ea4ad0bd9f017ecc1d9a611bc6324b36b6b982910ac999f06b6312c06fd249e648c7d9cb33c2fa5433fca9e7831b42e026c03ffcdd13e64a6b23694931fff00055f391b8731813d09faeda872ba0bc56b190d772331970f2204eaea800f9ddcc455e048b9e58a3c2a02823e6dea93b464f1e802fe3ac07b9a243e91c42720ba622fdf9b4a7134502f427a35b5be9f6aecf8b3363beb046ffbc59b5861305ee87fd11436737b882483f5e4ea193ca3cfaf14a12c01f25d7ad40a416691b86f13304b3322f032b568c1c70fed08b9aa8161ebf6cd1a85dbd0dac24ac224cf602fc0ef2a9422c3e65c24eee3f4621e684f7e9e7160a40fd0de5325e96ced2a2d8f7cf0135c022724be8e70c809fee4b59cdc44e429dbe05b6751c2464a48b4c3801fcfda969ce433b910aa6ec4cfeee2576cc4411ac0500ff234f6d615d9f5d4462c023ff0913a057ee5cca0ae867a237ec19b78d3ac6dd97170ac9a90fe9ea137c80f31fd065c2bc363893301f19947d16bf755ba8b10a7c3fb403089ef6ed7ae6c5744a6804db0b83699d5ba0fe1e67e216fef07bacfc712999ee88c3f40beff726801f9898865816b80e5943f3e9be2bccf7aa6f9d971de8bffc4bb51034ab7a900ed7fbd623c43f37f7a72615d027de59d3c017f91d2b769f300866251bb4a938720163c4c29519beaf1e20496f96cd268df2a1bdabdaa402f88556bc7612ad1a780d1d157ea2f44f7d7038bf7681fe9c7f7cb1cf34d1e5e1c626ed6e9ddcc6ccbeb30ca0ef59a6589e1af39f18e49b3ff9354c51fb8da7e218e097c8c3abb811a98473ae733867ced152f40c375f9700fc8a810da6fdd393695b9ee8159b2b9410893d4c20bfce2fb38c90887a8b8220f3b9e0fb98fb546efe08c45fa12d6bbcd098f56e5f1c2f376d04bc15fc6e52a07e4fc1e2fc1ec5405c6eec91b129dfbe72361a1804f4bf66b5af5450c210f54c7be8bb52c5bed48e502300f5773cf473a6703ea33f3fd7df412b4cac0f8b35fb407ed698bc3f71fe45703f12dbfdebd31329f7076d5a80df75445aaa21505fc0072bd57b93b9fa1dbdfa0b0a509fadbc9fe0fb1319aa9cb79ec0bcc84bc31897d007f01b667c9af768977336f86927f7d4a20755c4691281efdf8a451f3198ed2a417569e0e748faf7e373f618c0ff15cc7aabbb176b30aa08ae5de28d6ccbdf608604f8c5dbec7978ac6db2e893190a7112e76ca6f2aa2c6f407d96ac50e2b90be1344bf75c999dcfee6521ec06c4a7e6ca19ad7cb22d3c4f3ae6623437ad54d15131e0f9ffacb00591e4958fadceaa48946b95f4ad22c501fbe7179d747154a0e2689cad537a89f1ae96e58e0b90ff433be61785102c65db415bf041985f7dcca45203e2834286be69214f62f600513865d745df7d5d991a80ff7194dbd307aac307aadc93a70ba2f7558cba6102f1416080075ef09b0107ad2c7d06ecb325416eda3570ff45117c5fcf1adc4af610bf563b9d8463e0396d06decf8d4bc1e95caf0e33122143d82d4aa0b4f5d131105fa9bc3ba1fc35e673021933d32c9b3a0f294b3d80fc8100295f524e776e50e5330ce1c0bc50990de53c10dfe2b25a4fd9d1c6625f2d91a84ef22ee9d9133c81fc9595e0d248320d597c4354793ecc4e528a149b03b8dfd77ef4115f4d6bb185e72f94df2140d6dd20bb00f1ef9e9ba4d68c428bb0a67d6589a1216e2140b704d8ff6f97a18a6acacff224a52db444c5a20319bd8a407e86f46d5588f5f31f5683bde6b613abb17df1b41c80bfe05b486d9d18652b01d9f4896368f2a9a111ca06e07fde66d42a95ab0c22d2b9c76cec9d5044e37f4506fa53d0476f0a2be8329177d35271bd156342e5e7dac0f9edd688d1a65052e17676b15713ef9f0a22222006f8fde684f8abb8e63f840a65312ab131fc21ecedcc007c2e098e9f58d5bf29e3da5d91517474d43c943601e82f5c6084dcdf3b4f78dabeffbfd8fb0ba0be966c7b1cc73d38040beec1dddd1234b83b0408eeeeee160224b813dcdd2db83b0408920482438020ff9a796fbe6ffacddc9bb99399fa4ffdea9daa24756f7f8ef5e9debdf6da6bef4632d9c84ca0cf4b7103e21fca6299eeb67d5a3ad50f6b1c842a3705ae3ea8407c6096ce2af8eacd2bf51cd8fae886acb7870e829e407c2f5ea4fd47b92fdd32a7ffb690984e0b96cf5302203e3de4477c4edc79378904f516fed4aa0987c6fe01e0e71ea2ec3c937c2e6ff719a5426fdae422661e6d4703fee12ba9ade3478bebd7aa83c6019cdf7c66f25701fd37a472ced4fa77913ca3f98f346ddb0234e19d2ac0fcd188fb8411bbc32dc174152eef16088d6d41c80fd48f4898081e8f8e38343fb92b1dd02be3e7bc883a04f4ddbdeebcf7349b23eb7c715470e5366106c11a3080ff57fb8e3e6762382414633b6417d9567e7a8e9c05d037c27e19943396af7a2af7e9cd4b46925043bf9713807fefc07f57d22c1f5e15efd840953fc67a97fc4d13581f4c053894c3f7a82abee15b0d995d1c84bb9e0700e3732148171abe282b8535e6f5d4c4871b4ca46406c0bf31feb6d56348fb54928396ce2bdfa3f05925791a609f4cd44c1b6bd54d3e06def91bdedfb2d572a02702f5070abe7c56cb86406855fab623263fd652921df312b0ef3c1b4e751b9695cbe18dcd76d7af160ffb59a700fb19ae7f3b1cf4984d193bb3b23b3b5cbe27158a1a882f197179b88b52fc987580f7e5c27cfa51e8e8a510d07f5c780403f266a8f86db77e108ea9c925ef3e6402f1554f07fd2c5d233f6f19ea1df6c4215ba4976a5b003e837e329eed77211b19af5c8f1a5dce21c2b4a70ce4f7e0fa5a2428a584f6124fa3d11c7571f55e331a02fa954f26f019f4b852770564087c8793e189fe29f580be747849c91087f3d3651c95e7bdaf9b0423cede11a0cf52f2deac98b02327c3b50b274b390a899f5e6c00be3fe7f3e980108ea8c068f2e98bb5e716bd13282ac0fc30a97b5623fed143b0bc5844dc1e8b5c18dd3715d07f7ad73d866af06750119746279fbae4107b8c590cf8d77d387ea8f56d59c42ac13bb14ce8b47142f3e740fcb0e2fe936ef6828d54afcd858d40a9e37741dd22207e33f95cab4c5c06176ed68f794a73a969324c6a09d0af951b5e3c6ec6bef3f7fe30da761b9bdb455e6500e80feb1bc9e8379037875371166d2dbd298a9df8a781fe693b71efbe5274fa12870ca116883fa14cddb802f0b31514a5cd497007a626992c159af9f1aef26e6580ff1b183efd4a4a2b8d22cba7e830df586cb06b6e01c0bf07ee262b8f8ad2cf028b7ba71b356bab33f189017d588b791a07f532e93475cec6ac84d0a1866ae6d234a8af1adf3992436e72810cb9e65da973fe98f10d58df5cdcaa555a2bfd0aa46e4611a38cad7558233f01fac1b408f7b247cc1b8705c339f147bebdcb933042407ee4e8aa5052f305699734271fcff5e118d231033910df6f51161b6436a0e4c4e6f65884ad69afd9a31304f85db488f07948f20883cfa89dd76903a1d7957c1600be7fb45f7712d8f763df46c6880cd5aa3f717b5e17c83ff928b7ec21283e17b8e271b526bf232e7883be0fc487209eb6714b5da58596b45d37ba698d668c07c702ebf74ea1a8b9413b3196bfc5f0adcfecce406c6907c03fb92daedfe8c1d6d6b0431115b4d72ced4b3af702fe7ff6cc8b1105e9e3cecacf1ff2dfdd6f3788042b03fc991541b3de0061ff784cd1144efefdc2ce3bf32e9abf6ea77c5dcec6e51d5f3440d35b8a35c7e9f40c6a15d07714665258a38ae5a5efe71a32979b267c5e938504fc83bb099641ada577ed1baccf8d531f75a77bf75301fae8606fa81fef291912c68ac6df2b93567c3e60fc00d87f732d583129d640c11fee43d2a44a35b1e628c13420ff488d8522d0437091f486e016db4b34b50e17e06745ad0202064fb7bf27bf88303878c3f781d8e708d04fcbe03308e86790ea7fd0e8f908954f1dc50a3b0df0eb5da76d5f3567b773a4f02a4edd58e5da3bd943007d3ac7e612cf71c767bf69ed6ec3c16d2f960a487860fe5486b912f7a50d31eb16f0b3681eac412246dd02fae5d72c1196a890456e7bc345f81d50166e17637b80fe631b815e53a26bb84bd6abf38546552a41b6ac14a0dfe4185b3ce1d7531f9ac794d526ab24890bd53d01ea93924073e23368dbbb4a76a0a38aa9f006d3622302feb3e8992fcb1c0e15c5676c957a430ba454adc256007fbaeb0d922a30165bcac1e81ba9197f912a5db906e28bda30a5d2c3325362b2462b17c6c7edc8a5f6db407e23c3645ab0818689f4aed8fa70010e72b56ad72ef0fed45659c7f963f86acc4a7ceb29e20979ac511a407eec8fd9ef855b82eb98bdabac73d64b45c44a15f0007f522584a860caa7b429f919a65623ac273071b712c0ffdb0262f5eba7a5576a6f6ca9d9461146b7ab70007e2237bbceb7e0c93996ce6b3e9e45d9d557da45d0807e6873beec154cd63117c44a42c9b32a8e83f5897b805f285bcf50cfd85954ef5a5a4f9c4978bdf6bd590f78ff7ce538d379fc2c06c23348afb9575c181ebd0180fe9757f6f876d753248cf3808dd5747bce7b520208af41e4f6c12e41352ad88dfbae867410da4aefd0f7033f90cd3ce3b0e416375f7aeb47238b79be9f41df09e46f5ea484c06f0dc591cf3cf13f350b77cb6e887305f48fbbf04b58b43af4e39fd2bfd0a0cb3e5879abd800f9039cb25f76a52d4b9f5aefaa3954736e6b09b7b901fc4d2c8e34eae4a66cec7b0248defc71d3982ddd4f53c0f847beb09e769630917d08a493785f56d1e9ee087c9f51967cdce59252b5cfee30b2e6e15e76a2d7e4d2c0fcbe76acdc2d7b453cc6fda1e97b26a634b97701601f844cd41a1d5eaed524a3fb0f5d9e525cf1f1ae00e31f0e718214f7e64d90e4aa43d81992ffb3f8b27e60fd89ade8271bc3b9af2c83c6f8feadfaea4cce6e06d0ffc712d7bfab22c42453905788de1d73f8b2568d0de813a146b9fa2d7d44b0d8ae223beab2e93f58cf7f01f4d3f686ebed19c10cb2debe4a4662d2b69ce6b35240fec9b38b08c62f4feabce11ce9eb8d989507ed7b9981f777aaec210ebea2b058c292e32750ed094c154406f4e52a29779ab64975be178c6c28cae1885230033b003f2814765a84fac1e013a26ffe0d99d3c918b5991480afd6a7598a0ebb23b122662994ad20df7f96a7cb05eaf35a4f8bc4ca9c6f2c6098f00f962f0f3df778d109f063df6366b13c44c37806bfe5c4b9b4dd22a4a12703f62f8b6fc898beee7d777a73268ea52a1e3b478d3f10ffd038b412417fcfe322e298d2413a228f49c9ed07f0abfda14bfed7873346d57bf8deb353792c0a6f0201fee6cb8218d24219bf2d670ba15ef80a6b43f56b1fc03e9f63b52108b36569887b340c5f4de8e3e82bc603fae40a4efcb888a84c9b12bf0b6de3af44636b0b8900ff71c6c252dea024a899772d55ad2640170253310be8631159840faec52a759eb13ceb6b9bef39da6f1e00e6c7f910169ef1d3e13975bf8f5506c925e74f5f5a01f1c9fb03866428d627b7bbe2234f7c7737bfcef983f52b4bb6b0c3f8bc12605cf26d4bea86c923ee857000fde45baf3bd69da20a758cc4d3681f014141d31d3980bf636d7ce5ed6269ee512d928437fdbd020ff15009d02f6def874b8bfd38ca85f7fb6c10f1958a84a8d212a88ff572c1f27065491ef6dd5118a25ab71051ea613d80ef1472ca792265e8d4f50b053eb55d0bb222ff3000e26bcd821e75694a042877a1997bb81d7a2148d05f80cd4b6f0da2101fa22c3ee8bab79aaa0f6e24f3856801f567651852aae0bf4a0ad46b0b7613ed87b36cc6c101f865937ea63626fad98f27ba7021842c651cd29f6d01ff2e95f5407776c9bcf909b46f67b657bb91e40521107f41e44563657923ff79c54b4d1a6970e5f0cb672240df8bc9583e8975bc46315ccf994cd9cb73e4df9d01f8573ecc2d8d6ff67b0b7cbbd34714fa934292483580f929df17425714653e94ffc9819a4be8a321b1670de03fd267b75ac553397efa66d026b14c9d7671251405e84bbe719bd75ca822506d3b20b3f86a615f9aa50703f80f529ad980e5fc62e707db84c523df8ea9ecfe0e407fec71874c23d704bbad92b5cfbbbb141ba2604f09f4ffee4e8157c0f265920d3d4f9b431aeca6ea5a29d0ff7934089a9e07aad7cf44dcf38ffa3c8a1e132803f8e335fe777b35e69ce64876f41f9f30e524be70f603d71790b716a0b83ed640188c9ac5d53f22339cf500f479e121228581a5a2623f86f3f22eaf48b4318aee80f81c24a2d9a4bbc81beb7853e11e127222c343e160405fcba5910ea144fedc9e795db52fdcb76492253102e03f15a0f1709206211bbed09dd940aaf55810e43e03e2ab8ff80775d3be9eb0bd7ae231ff55cf8796331f05883ffaef1f3a05e6ae7f357fb5c9a677d7ff968a7618587fe35615dfcabec16e12a8b624200df5ff36a2170fd437d8281c901bb38c8922fcbc85f188ea3b04d5e915907f858497707d5c4af154a072a1bff61c01ead18e1fa0bf7899a4b0c743ad2506e18661d9f9e5ee33fe8803c0cf4c43ed58966fab5ad4afa566fa2f34e22b06f302fcddce210f14abbca6675b26eaa79373faccc00f9b807f18ba88541732d7dfc6ec57a1b755734d85f0c519f05fbd1437c56272ba7493565fbdaa2f8c24444c8804d6377d5d6bcf451a842b16b5bad50bf6829843453cc07f1a098f6f71b054f3a3ca205ddae15d8fd6723700eab310ee444f07672870dbfa0c3c22cdd2ad1e918404e257fc92a9934c6e29d94d4194e6ae9e879ad33233c0fa3b95b0763bc196b5f1f9d13d671e861389376c30101f4028df742d559a7acbad72f7f4cb98db1276f110801f3c2937216c5791146d7dae91e4820fa999b8d080fc3a28d44241a16f8382fd90d9fa3b6de109b03790c0fe4610e3a6e5f29c2f24f165d4dd55f0d268855d3201ff3825e0f4d883db9613d6f1adb0d080213dbba422e0bf9cb48c8eb37c8a965bec688d48d2821c0ad0cc05f8592b69bb71d5aff1015cbe8344d329e95f113d5481e76f17770e7e1ff396b886946e1f47f6b56ebcc23ea00f23bece8d66666fa2f3d4b6f07d76765f893d940fcc0f79772c32ab98d390cb8b3ae8a4a120cbf3d076403f85adb47732287b1714066b157995e4f6d1e6811218bfd032ebf8a23e31b3149701ad71f22759e1fa58407eca275db87c7282343baf5a5e81b1cb5899d2e44d607cd0bbdd6853e14abc3885828b2b6845a1fe7c5c0dac7f787c0dcdd73c92b62d9530761851e70fd22c39007f3589a1e8e27cc098240f13dfaa48a87e3c9da107e09b2388ac3b02445e0eecc4fa0c7b5a766847fe6040df3705811bf29ae032e322fb5313372b455e4cd921c01f5da746a2cfa17b9172f52a463e625648449707ebf7231d9eeee6a52d6a9fc12d16f53f157b9c736a05e4df0d573d1e6aa318baaea928fbacf5a1a0613c5706880f0db32b3b31cbbe7ae36b658cf0ed8346e642523dd0dec510b913437466a08127cd7f9d8e6b7e74bb01e0a7391215cce4a261af99a433f7638f256651ca78e0fdb812cc28cdded76af4234176f285b6eac3907f02f0255e8de0fdcd4e434516e9b2d2586b8973e24223503fec5a6b4d5ffe4921ddf106fc2bc83308fc42bd4c203f4961497c288599b5e33bf9e915ae7b63d39df219507f60631c5bc971017b5ce48b8b246bd9e33c21d353409fbba02bfce95c4348c5039d252ea5e778a9e84da004a8dfd287a7866aec81665035e0b9ae7df9a39b04e0ffd3b278e636375f55a4dbc11adbfad2dfb3150f00f65d41ba5503ed73f3cd8a763d53cac2fd4dc55333a03e0c6e08b2b077d4d3d900f480673de95312846817203f30c031128cda8012778fab5688e9fe8d77290ce0af517605a31efb27488a7c383baaeb3c1cc67f3d08e8ef18f17e0cd0e84c9d6419d9d149722c3b140ebb01fc162fcb65975658b4332b3c09cc3d4c47fe09ad07107fb0db0e1c5f467b86766c568d59aa92a1ad2f1403e8eb82d1cc6a6cde74fb971c47634ae96f87f0b94f03fafa979f91dd764a4ba9f68ca907570d67987cf4a701fb8565e8b5dd25c913b2de86348b53fa636e495b1fd83f9bceedb9e5a2c1ec8fb0f7afbb0f73e2ac199daa007d4ab9170cd57873a49c19d4f675078f0014dd512910ffe28d687f25dfb5cbb5a6c8ee96d4caa45e2e750fe82bdf724d4498c4de20d94567cecf5ea4401d86ce00e3bb2d7dd02366d2bf97435029e0d98b9bdb76113ba0ffd1963fc7ba3b49219fe757a6e6cb11bd0cfef408e007bed34165ae89b8fa06d2ae69866fb1664d697b00f76f257f4750d733fa35b6f5d18cb21b554a1c2226806ffc9ff4cce05c1b45efac1d59a7237b208f702301fa3606f71dabfb3831447fa6d7175007136f0fbbb9017cb37fd8bd57d6f6188a374d907f534a639e497a0dc0cf6337061faee970635a5b767bdaeae4c6e838dd00ff99b87b04853d81d6539d69ea4d78b4d5298b262ae0bfb7ec9ebe0a4e4daf53b775540db37960e785df01ea4fd415fa7dd6585836f74db856c9fb64687bb0940df0fbed27669eb91421c65429f23be4db2d978487cac0f812c12b334fe87513cde0a3b4aed4e73bad999807e29fb304df3060e4a0085a5075b7b5157dd02df77f00f12d0c2d6feee4edfea8bdaa04ecd0bbfb0cdaba0ac03e2357e0ee515df5c2d98d07ce74e0a4b5b763f800f16325e8960fb6d41992674fe7c5557297d4337d417e3d8d532096ca6874d9c18e7e2dfdccee2b3d7537501feefd171c0eb965a27187e6553c260b5be58dc96d001fc847ae455cbf57d86aeb1c2d2e4ef29f93de9103f61f6e7a8bd74c67521ce1f586e6b97db60662c26005804fa3e58defdd4928785b33d9319f2fe7459df60801ebabc115e9467ecaba9497618d2f0b2b9f4c7b9331d0ff97029fa107d19ce7c2d66b2508c3ba662fc8dd00fcd30dffa0f6a2e50627000b3932c3ddf1b6257d41f0afdb69b38b6ff09d3a05cca91f87bc853b657f523f0df0fb89fd7daa49843d34a6546e4fd7a5833fce1f2900f81ebf31b727946f26ea4162c290ad98823f73c301b0dfcb323a11a8d6129fea977dae5546d07414778a00fc4bf69a7218f744a5fa52e78bdc77e219a570f944807f78b5503f2ba6cf68d216b3b035199da8df39be05e0631e64c96916e11a9eec13c7fe4fc99a09d11cf400bedb9ad7e0e531b7a563a21651604b9d4fc14bca05ea0fbd1f58a9b08d14c8558a8365635b1e6cf4f45002f88ba935092a091848bd7e1d127627ff3892b7572380ffbb75a7413582b02fc77a42ca41b91384ffc58108e0af9f7f7de332b1f1edfa80ac41a73337798eb39917882f87e73f4887f9209cbaa32ba174d6a5b3dc8c4800f56720c6a06bc4610f5227a82ea74f437516be34a503f6d182058afb1b023af3a0431c72abe941d95d5e33b03e053639b4ce0470c8076a779fc7bf60cf76f93101e8779a3990efb330e11c57b96179bad2e85e99a77f07c61f54c571bb0ca66548e705878d34b3de17ad230b407ff390ec13baf6b6fa6cf03bc5164ec66d2cedfe0a101f81b80ba23bca9bb062a7dd8cc073f1d4406db503faf75464fa917de8bb901321c615e754c526acfa36407fbcd589c2a36c694535264d28889dd8fbf913bd2030fe3af84db586a3abb13b0d215455266461dd688d00fcd387f16c6658d46ec9478b49c5e000f5b0da971ac88f2ddbfaf6de9688c985df573b30c8a8a421e24d30c02fea37e14e9653882788dcbae01c3bef289deafb01fa6e31d60e550f985ac56a98ea47b97186f95888b240feb92b61c8dcdb5df9b9acf490265703451ab5ef91807e6062b5ad47ca85989a9b709267f5d1fcb7976ee580be11eb619a7217fdf92ea3891a8cf9b3c7e81e4378a0feb6ffd5a60045779485e9a4f136d6b2802f9a33105f3456df9936b668741a2edf3779111256e5f42907f0ef4ee0738594f027ea25615806de08c53099a9ec01fa047adfbd3cf97001a445da322d9dec5d98aafe4500dfb41e4a3f8cbc5075671b3b2d8e1d55e5256e5e04f2630642c69926ae86645a1e8dd05276e936d63b5403faceeef1941a894d9c98802786f20dd38bf9f6c59d40bb38b96e9bf8750a8572271126e21159ac37592750ff864152fbe55cdd812ea52a6c55e79ced93ecd066c0be68e826b47e2f81f57ad7ad4c72575a0399c1520af47f356293dc8f7285c9d5447c1df77c6b977a4c0520beac97f944ce3872030792e8f01d9c50887d099c1aa0af9aabd8245beb7e58e6256c16e54739f1b0905a04eaf7d22c322e4b9b28d5460a23672131c8945e9100f21488592f88cae42214e1b1ef0295e850b68949543980ff5765fb437ce3d3c936eb722d7fce2abf00e3dd1d303fb6c93635391126d4fd89c59b0f07752e5e435002fb33c119bce92558389171d7a5e13e96670edc747a0be813e088d9fdbc19de89578c6b7b5684b5077e8d2600f20b75b86917ca6e88a12dcba2cdda7ce45f789d9b02f951ec0292770411fb130abd0613d3777b49272733807d4de93297a2a6b2e8e3cd3c66642b16791ab2d80deccfa9fac155e88d15b631897fff56b5e9d0103a443b50ff4d31bc798919d7a9eb74417023959e84f866b802058c0f2ae1531078209d0ec9aaa86c1c3775feaff8e05093c251e971c69b667551b237162d7a932c65cd807e0353a9b12d2b7efff57bb6821a42b7cfcfd7b480f793203f7ff65cb5a43e5cd459a6ac7256a4577506f05f9feeff907e7ad9571e350f6f0c036b35a57bbc04e803f8e3a821cccc387fbc3ece16a3957b822b237f00f07f02728df5f6722b763dc5085e52d1f85835f56480be3a29d3709501e71d8fae102951dfc8d16607e43e60ff86eedcc765e430b14d60ef743d056ec7e1d4cb81f9a7cc5c43483d922bd9d012caf5b1ee31b6ac241c808f28a7d1144405556f0ddc6b030e117f1ce66ca603f5c91b7864ecf01d213d0fbba156f3ca5ec58ed04303ebeb31ba15393ab6e674b2f06378fac711ed25fe1380ff645cb0d448de52d242ded6d4f580cfbaedd9b402ac5f285c2c01d93c62eec413299c4801cb03dd6b0c80bea59d0b29ccf4ecc7914f32d1da22fbf719ddd374407facd1e0af78acecf3e34af54cc8876f9a4203d20ae02fb5e368fc48b80aa6dc18505e492960eb3c8e6d07f8e5b55a3c64cfb89b8949090d9da6b4f5139f785b00ff2cc9de193cc0af699323230e9aea4bcaa990b503fccba2e859113692de65f0ab4b044a290546c52873805fc478727b9a0239fec3759cd79c9ab00486311b06d8ff62c6d4c5ccf161f015ea680cf40b2bbef2fd4b5ac0bfc0a487b0348af06c6b1de2326bfea210e032d600e0df47d56b67bee8efac32c43a388442779cb448b981fc0a02ab3a6ca62ca6a32093cb4d0dd5d6fd032c0f60fcd72829419e9e4ad0391e5194daad97c8eb59ad03df2f7dfdcdc1e991e3a8297fa4762886355bc1eb5be0fd7d2a839c4a13d44531147065fbac3f137c249007d607073d1fcffb91778410db5b668834d59a09a1c7407c6c16a6b696a597b7c49c4782ca35a3e41b5a9537d03f706381f32507284d524f2af96d66b49b64950780fc683a63bf02d4f2ec93a2683621aeeb9a2c7ef169403f46e900b5e20e691f340d3b387c2aacb4ae1b5704f0ffc3b942ef106f0961b856c6befa08122c4d88d003f931bdb8cf915fbb701bed6e9666fae4601584214702f71ff62ab57ce0b52b3cef2d9985aa6b0c64931600f477f48f03cd09c73d9e056ba6f718d42a2b5ef8f380f911478778674c9b41c8ab4c549e838892e27c6640fc8591e204a2ecaab54383c791fac9683e42064b2410ffa3aaf85c6f37977793491b424ef903c76519b38c0f888f4ae19c48b5ed323f73ba23c0cc979067fa2a01ac7fe57e153e776cd08cce3731cdc6ad3241874e16407c5369e0a59730d25bd3e1a37a78dc81052bda7106203e85e9e61f3aa7f775a5e8457ef231e1bbeb1e4d16a0fea85ab96a123b1356f2c9ccbdf8ec7b11a5effef940fee6d53ee93d4119bc68287b1574a68a4ae186e4281d98bfe55c45fc41ec533e99e2b7d961aac56d613ba07f85a39b37d5ed5c2ddaed4b950eefb05afb36bd80f85e3f8daec6e6bb30e467588acca20ed902c856ca80feedd2259f8ed6491e65b44c47b199c367d9942b0de08fd4290922e371f6b2a4f8bae04824d20707268481fc7ab7cbddab350bbddd1efeac269c9b9d74bd5d39407f3032d6a0d61fc9efe281c07a8e366dbb077be8043c1fa53087a5db1c43ce1df316fa28b5601ecda90580df0acc9033e6069d035718cd7bd95ac41e2547bc02f493825e443b9c1c27ae59cab53cf346855f8d100300ff0b7583b72786885620567ef0f6c9d67ef48a5e2f109f8015e7b45b6e6899a519a7e89121311698aef702e29f21583b911de7f82802f1f80886af1b3a9a4ccb80f909b9e5c51a55c66ff52cc580092935aa9520400cb0eff579faaa1918cf288a065056edc7b3663761a201fd13facd54401904fe7735d60b7f488e555907da49203ec97abe2a7f56d5dcc87b7c8514b5716e60bc8506e82fe812a53a6363ab5430de57f32e1b4df550e48903f8c312edbc5fdc3fe7db156347c7d77d6439278662203f3ac695cf4693a78526aa24ff4b03b730ce408f28c02f0770ac17558d35179a8cd17cdf89fb826262ff03a84f3aaea43db40617d8f8d846742052b22fcf67880fe85fcfd11926e81f6182a204ae07342408083a4ee3407d8b0339dc7367c15868ae59fb37f20893532dd85a407e63c8f4ebed3a1ae691830b78738a38075ad18a2c809f88ea5384fe3ee340c043ddb7c3418e37ff348d04a87f2221e3669d6b2b79b4077b1079f68de2bc149206c8bf7d8d58958d733c69c8838fed6fc3889277b93006ec8f50936564225fe4b2b75806d9743111aa39738c0ee4f79706969da5ab11291e1c241fbe4ed82a64d66c07eaf7f9e60c97af3fa87a8a12a88d143b223143478b00f9a11121116b3b64439a862c06d3d3d0fcdbcf0b9780f881e181ab0d7b4c5873c537dec3f73ba78d6f5ac681fc284bebae5963649898b86f1f38680bbb3ddfb94800fce2915df4d842c5ea394118a5555f287b904e4737e0ffe678f4cb8bc7550edc0c1e7ca07e26797bee1302e8074a82e05b6cead58aa3f073dc3ce8670b525f6b03f88c8b798a2bf5c44c4e3237ffc7fdbbf76693fd77003f20b8e2e792320291f531eabda1a3cd737bdec66ae0fde99da0a5424e5bcad535730cb624df4b3cc70903f81d53ebe2ab41be32cc2bd97b7a91f4510968075920becc38ea4a062962bbddcb235703a94a3a1a3b8205f0bfaefe3c9863aeca75dc7d302f101ca45f938ea602f6a9e225cdcb3ae319f4fbca8caaf16f101376c56a00bffbddb4b2f1582588bbd4228266db7b4768e65012981f4c9bccfe6fd4124bec158be689c4dd7964197380f8b9fc01652d418fa5d883c09bedb3b8c6da7ae31f807d1dae44c49ed9134594ce7fa2a5dd975d40943100c4bf3d5df8d05f5d33d50c44992cb7f43b7e180cf703f82119e40cbf0f5e5fdd8d1e448afa2363f90bcd6500fde3ed3909b5c50c4903dc0646cfeceee0dcd7fc28a03ec47bf8e1b705419065504e7ef036dbc3b50859acc0fe1dd7e11bef978ac430f8564cbd6e889e4d04bbab01fcae208665628318ae7af908e4a34ae6e89aa20b7dc0fec52f5363705050f37e4b62835c54636fe1c48c06f85989034c948019a764aabd97ef4264fcb0d19d1e00ffabaef1361d958b6bd3eb5db1aac454dee5392b13c0bf149b0ca88691452ffaf2c593f3d061fbd2a54203f83123439fa4cef2f9f41cd71643ffd2685fc2960fc05f3873d0396c84cb075c8f3c6939213bb0ab2a9307f00397b6dd6b558a8d04c6a761fe4266fa4dca5f5e02e313eb3629d4a3d47e84adf68bca5290d8419fb936a03f088ccd7c3eed02671114f47d6e44ca19cef7e40b509f1887c406eef85bc0f817456cc27d38bae8053d47401f3ee71a66ebd78f767f475559b5079df6d448f803c0dfacc02c7e09a3d4711f2df4b323da151926d16e01f8135badad6b2aedd4a7021b3aabf891ec058c6b18c0fc10ad3c2e29780f3b33361bfed45d4aef89025f05e07fdc6fb97fac7a9f78feb238ac19e10e625fe61e03d0e76e309786cf69f4e347191fafb4c95d464df7f700f6339974e4d3dc4af7f5213d43f3c48d5e24de5e31b0ff339589cc9be84248286a114ed8211b02c7cd0d2660ffc1bc8c19111412d4cf5645e4b7d0cdef1d7883de01eff7858ff91ef676f1f4f42632947df4c081c17e07b0cfd3f10e07223a0afbb6771aa6320217ad1474e2c0fe62f516428c3af20df13b756f5eda04209770c89303f81ea26a6975add96ec7f3b8a4247e342de8b44e0cd06765c350a19ade5b277832a834f9137d74c19faa01f8d9086fa9a6339a6769d34fb1975f88c2676fa98801e3b7f301b381713096db65fceac75e973cc4a3164d40df8e1981ff3dc59bc22201b6bd7d444b0649ebf51260fffa0d42dec7e3544e2ca59746cc48b446335a5f03f1476634ec86262dd441461ada18988ca8157347e15d10bf5e86a7c95a8a20b376bc4f5fdf905e3b8e00fc475aeb4db36e6cfad18bf7bd639c1b4f0d644474007e0ad7a31b036bac6df65470db81e84ee9c827fa3310df2126c018aff4781293d2202525a8162fb7b68202ecffa42190cbb45e8e86df66d9413efb55a33e9f3a00889fd97c1f4a13c6b3db9437c2b457ed71ec7bff2209d08f9232e3853fb4b222b606281c72b6ac3bd3843b00f9a17c299cf2766687fafa84affc433b9507e1e6b981fd0767b1489796f70a28776659a9c2accd1975c94a81fb1fb9b49e1ddc08b13d3e23c0c673d3cb8ab14304f025553b662aeaee138531d44204a708854cac2853a07efe1e6c65c6652a0b9b47ff97aa4e610ea86e7c268000b321b8d7c026ae144a9bcccf343311795b792a02d8a71b62738960d2e40c8fafd174f5dbe50702a9c240fdb0d72a8dac48142883541c99ae5c38ab75d830f6407caf3e1c27d172ea4afd7e86452a82493261b9d31da87f87a4847bd13ebfd174202b95c42893c811f24e18c88f65c14a4be79d42d8084882f557fdfa61858c2f04c017ce620cf69ef08f1a25610266ddcad7f60850a101fc13d41bcba7822ac6a9daf8a655742572efb9dd26a0bf608dcacbddc2827d78e4a3c38f62a451dca899058cbfade77bf3b082c5179230a4c283c14eaa5e691b00fe581b7f392310ce2f226b2830548778fd95523016e85fc7cf46bbeda45a8d0e3a742f47876fc713aa8e80f3f52fcb95f5b05f4333bf0d932d3869f8fc05a70bd87f481a5df8998475cb32760d962adc02a302ef0710bfb8a14046dd72db33770beaa536d0714f183e2501eaaf358ca8ab79ff2899562824b5d50e2e63b1f4ff0ac4974ba5a5df2f751ba612a91bc5dfb56b41bc0bd104f483cc22888f162b07482e42f82f3443762f6c477280f5df69663b6479d7bfc5dac0e0391577516ef1a63f30bee349fb9f330e6d07f575b724ec27f51be3a991c800fc0d89c0f726424cccd5a2a8befaba8801c4afce00ff07df3810ac99f94d84051ff76b5473a3aaafa829105f1bb8f3a04c99c1be4d463e8d51c8c166c523031e1f421b3344e02870536646aeb02eba58cd9246e51eb0af0fb00ce6b8b9676ad73c492dd42a501c174ba4003e7a0ce7e6d48168151e60f46324c0acd0c83ee529a0efaad099b31dca6a13d648f73149a92d51dc4e2b01f4f517c7b31b94bd984f2a7b8abcd9b25ca5ce676981fc3d3de67acd5349d5dd7502e22d3ef363ed596854801f5ab6198c917ea79593d8cdd34dbdf891673f1702e87f6bcec30ae8503d2d392db22449158f1adacf6f41fe9e39324016e7a3df95b4c24cd8d788ca6f258d407d5d52d9d7cc5bfe0d5bf14c55d464650129b60fdc40fcb9e6fae154f8cd8c8f4ff450e76e73ee84c02e39101f7f65199e22630eeb21a26beac23c89cfddfe5507882f535ee7ba2e65880b90fb0cd9d2aa5a72347ac800fc07ce7d9206c275d6eb61c48b3e8e51c1430b340b809f3025dc4b5faceec2cd23585f7ce8d5b4bef6c406ea6f257e33fbf818afe182ca9155d745f856c7f3bb6b1dc0bf3064b98f27a9e56ed847acd2e0bcce76407302f853a1da2e3e8408e9761e04873cba53e16119bc0e80dfafe71166ff1670b86032b455e7cbc2aa3e0f2301e8a3bce73b4782c8983363c34d550a22ac6b1b2172007d0da7fd775417ba51c48877c2f57e7c6590aae708003eb094c14fcdbb857bc77050e7823f32f5b48cfc0e583ffd8f6d504cc85cd1b45f35bb109fe9fde8304305e20b2a89cf6157f3bd7425205fce987b7e98ac121403fc77199ba011f57dd7872c6e87fe08d5ed4b19c96c607d14ee1da4e8c3c3e6b4c7bf4624efa3bbbfc2ac02ec73e387399c07ac671de74c42477e7d223b6184ea80ffeed7d2c5d7998d1079de70e2f62c1626adb89613d89f9016e72454fa25943e0a33f4f46cf757f9e8a95220bfb0adc1db414621d09732b3f79332f687cf4c5f0b007e28475fb311ca933db18b6e80b39be59da771f929c07f7520eaf4467f326459717dcb3f3924ea994f7803d87fb840ef3c34a69cb194afdeda89c7153696e4bc80be4187c17320e436396245828af2ee872e04f19c2ef07d71b43af8c60a4c8aae5ce5b34c23cd2fed9aad80fce84c42e8bea6faf282c7c9d1ad8f383f44fb744e00e3273c936ea4e4d31dec3bba11762e7b7f1753b405607ca3d57be9a97e4cddb7c0115c23a455622c2878028c6f088847ba114710ec9fbd8f64c8a15ac6de758402fc547eaa016c716a9595077ff9129cf41e635fe03190ff1830d684a1856536bd00ab8a63c385cfd84ade0cc47f55086529eb3ceb3433a05496552362be8bf24801fc5fc1e42795715c5dc7898b16d394a30ff91fbef800fe49bb12d973fa1f577b6604df041f2fe6ed17469a00fcd26262b263894f4deacb7b5d3bbc4406ca8b744f207ef33e8e59ca77b9ccb63b48517126e69cdb9f9712c8aff3d36caec25e873aca79b37bac059f1e2f51c30fbcdf55619df1ccce86c0882ee43cd9ac32dd980b3e809f48cc26075e3226120b3872069e64f81eaf933200fcf76b8de3ba63c2b689e751e77a7de69ea7aadba0beecb18f794ff013fe126d38d2c6092a51e248ca3000ff275ce9bc4f3278a9959e3279ff21659806b3df1d58bff3fcc7a509e3459f62beda26d52c2b0bd0a96e01dedf4241c7996e34eb7153eb47f562381c415c5f5320fe3bef67ad382711998d161b3e57601dcb419a1f0bf8e7f71502a4cfdce1c75b3ec80ebc7b207e5f33b006f00b04b98d4635c85f5ab02d5bbea17d3af673112f05aedfba344fb9fc623a672ab15dd42dc43966e41eebe3d58b503e4e3a09088820a2673f9a24764c8dca217ef7c8072087548b79f7bc7c06fa920c396eb91dcca1f40b4f37a8e930b3b7d1164979fc41a83aa6419c6fa7fdbba0ae21afc298bded555b0553fff27f9b21b707072f6a7fff7610febfd902c56294b5a8970af1078e0eeb25e18ffd9c222e2ebb92f9c7f0cb2c6c4ca2bf7f86cbeb7ffa7c4639d4baad8ddfbe34232656d710e5cf1e79e3f79b217f723a2429c4bff780fcd913fce9db6b8422e2293866430421153307b1418d37434aa829d115fee44c917f7a68fcf7a3898aab3e5354305051535292d3325010557da62e69a0aa282ba9f08fbdda4fda7fd6b540720593c4a6f3f7396128cc40be518c2e028cb9681d80dc3883903fcd210c2a8f6c320ad2c6528394d6da8ffeed4bff577776415dbdf00f829a2e0e46865b236d9228817d072c987341b20c54106485519a616db177a4142a44ba8bbff7c0b0671da45dd75063fec1706853f1cd507ddebc532cbff69120b37fd2fe27848efe5ffffee5cf9ffffbf1dffbb5b3bf4865d3dbb895e6448f54043255fc192b44969d24d1400efb46e38f6b52bc3f9ebdc193a08c92ddea4a3c2d7a7d91cbc26c1102812489197695697fb08772feaa2d96f6ce695eadddb54c75465f133b3a622558e5ebdb2146d8fbee49141a6f465ad549c78c00b2fe4c2a8ac06626bd6ae4d354816ee94f84da0467f6121e5c1d268f36be9c41b41ac4618ef20abcbb91c0188c1a4fb3c3e0154496ca98a5dc3a87519996145870de52a4dfe21d193bdd2b64d0ac60a94d2c266adbb8f3aeece3f42f7ebbd6a0a7610c4df481d2c064bb9b63788703ae8893fdd0e253e6e12e3be9ea7151b4c8a57b9e7ce887492c34f66f9f220e47d49294c96688fed9f9f90d1a024912fb7fdb6dd5c7f7af8a1ce0daea8f309ceee2d1df74229c72fcc943ff077ef30fcc88fcea2d1e015a03df17cc01fa8cd9ba8152cb866a49db903f9d503f9df1a495508dfd3ebbe5933438890cdcc3cd2c9c33ae50c97bce6131fd6e5b01ebf8ee7f6f99b886710f83c6888e5177300d42131309229e4e698631cd977bb7f747e6efdf19ba867fe43b489745097c68eb8d57c3dc72e48a0c9af6b259d0f9dfbfb9adf2cf9fef4d4f525dce6c6e43975bf8bdeb995b3388fea27d1485773f38768da54285c5c25046187abf8703633fed54e02b61a6e74cfca23787d4323481350b8a175a631a669301172a9b555f568168b10e46ad367ec17c6a8a9bf017978e5fb4af3f5d797eedfcdf6ffe79ff43fd743473f0f8f0680f5d2425ff09dc3984293c4afb6cd35ac87274ca0e53d381d0b6f68b4bfb4fda2980fe813af278b490c6c77917ec0d2bb45691fc08562de557c6d73f303e45db3b28cb23ba4c3f06f9430faa19b3b1a5ed2c31cdeadeb7186db446a2bd5437a5ff644f44801dcbb94c554aa2e8dad32fd684a0c310a23c5389f9d24e467120eb17c79f287b0171cd66f653548b262d7ada6d48115873fbf6259901da37b0a1d7c90d92b88c12c2a507d73a21c4cb34a46cf04539d503a3637cc21b46a5ae4dfcb0637eb784ff00f4d10cd3b03d8ce5114afcb3ed218acd68869418a66895bb486e2efe36c41828f87e2fbeea7eb5402d2c046ed62860fb2ce11c3f454dfaf2372faa8194d9f7000ead24f6d3a550622e9f4e43c7b63915aa512c019bd05f84753f59b6218c21a07eff0734e83fbbc53fdd03ffaf6f31fcffc7978069a86e00d884d0930d3114ea62cd5b523e2ec93502aed038bc9d7f1b0aaece6d63eaffcdd6c6d2e0c803b9cdb10f0f3f397ef531baa0ae61b94325b8e474aafe076bc3524e51d42dfedaca0bf337e341bdb227bc504666aaddd8f26425247043a2f308734c4cb358a63d8a98b444e44bae2785e18c4e9afafd96c95b780ea2afbf7dedff7a5c20d08161fdee668534e8e0a14772b063575b6295dbe727b616e2d76c31d44fc63bd44fd6121888fff003fadf7c7d04793367237923a757ffe4f99c96f6e64e7ccccc2f6ccc396c2c9c9ecb9aa8b37948a94a59c88b6bc9297888ab739a72bcb4b470e05533b17a25cba1aeed6224256dfc2f74637f118b88fc2216116371e760e332e5e6e53135e135e1e0e4656337e36637e536663167653131e6e0e1e6e06431e36535e6e6e43062e160316767e36131e73637e2366231e264e3e03132fab5ef67e66ac3c7c3c1c9feafe83f86c165fdab3bc8f3a231e8bd906c7cd1d3b8cfcbbf7f3acaf6cfd66a7ef4c54b34ad5778d8ab37f9fe380a0beec6e46b5c032c5bb23933521671180d48ba07afc41a2ba6a618e49cd54de79bfa05c9d8c8e55befb83394e831def0c3fe036b35f4c2ff369dd07dbf6e3aa1ffa69dd5cfc4e8e6a364737d8de8f3029d96b3a8218d6f81e5a19cbc73ef5056470ed3a16ce9a5dd4267536ae3867ea4e47143fe11d3b9bb4ceb73c9d6804dc9d0d8bbfd84aa597af4a6fa3fdb74426dfc9fe9fc17994e631e1b17570537393b53690b7b5b63294d71730d537915773b2ba9174e56e21c469c262edc3c761cac0e2f4cff73fa57f4be9721a5c0b8cfc950aa630a8e3d43d4442dd1e8fc43be83f956b3dd318be71a7a22bdee9c3a9583ee418f8f5f45f4adaeff5b23f81c45262d464416e82eb8b73f9fda73fec182ee92017ffe2b1c0239102b29c80ab70beac43f18477866facf7fe5737ec060ff6f068447cb8eb40df62fb94108a6dc04540102fe7fbed29f4fa91bfef35f7f7d0a6b13e1d70bb9bf8493e15ffa10d23d36fdd3ccf90b9f7698fd7f7cdafff169ff5fe0d3ae0cfc832145202082e15920209a5e985bd31341316d516a8bfd1d548344195a77f5771ae0d82120fed4744bc2962219f7969e26bbf9c982059108e946066cd7dfbd03f3a27bd4dfbd0395ffb1c06fdce14f4dc486340ce513f52e949ee6d6182b5fb18dd372fbfee43b26f8075153950671f675364306d743f800916a640693772a3c58e7a519eedcc397d42a936f1e80486418c4f2016765dc55346efbb16898f388a04b5add4fba7ae8f7fd87c7c5bfdf8e67ff7b2b080ab6d7c06f2c5e26bae9ab7a5fffc526dbdbd83ef74c52b2462a7e09231abe66e9e4d62ef7f7c9207a959ffaaed750d1fe41642752cd50c1d5e230b2ff6672f067e703ca5079a7f2c6a7bd6b5c11e6df268f6bb0832173dcb77e854031b7f695fd457239d85e1cf7677c866da0948b0af2c0ae4406f476701abbfdaa7d31fce7a927736b495b9bdc5601129f7f723ef62f9eff7b47e0eabc6a66bd6ab37c59d693e31db8c8d70639a866b139117095d7a9e7564f424e9d514f5fe15abda3cb25d9c4ddac4fded1722447d6b1e933ad29e54cab9c48d523e44e580fb890ef4498e622d43c7ede8c4b8c60b6c4cca5dae7c440b69c29b3dc6b83d0c8591456809317cf8dabb04a2f28f21ece78f0d1a4556988e553ecc359cd9fc0862b59ff20128eb22689d3e63b609be6bf67857ef3339a48baeb4542408c8642ea097d4f85082292c9f487ba867bfc174fe32ffc151cccaf7b1ab07f43c87a98e2f2f1b9cbcc662e7ccff50e9850ba655a79d03e95daa148417c4b71743ca93285c8b31d8e7f239d7eb5a352cf2efe473c8d97f171ba8ed6b5e79aea9d9a45df95f7091e2c0efeb33d0d58d2fff334fe559e86b3a5a79b03b78d92e44b564d75f5973ce6e2a62fb438cc6cec94ac9c35c56ce4ac5eb24bf23839d958a9bcf8d73d3fd42f6267917f3f89c36a626e6ccac9696a62cac169c6cd6bccc36a6ec4c1c6c9c363c66ac6c965ccc366666ac6c2c569c4636ec26166ca62c2666ac469cc65646464ccce69cc62c2f14bfd0367e66ac3c7caf24f9fff2f787f362e1313132323337616734e364e767653535353336e0e362e632e567623230e16167316367363234e2e230e0e5e0e3663232e0e2e130e6e1e560e734e561ef6ffbfbeff4f3dd55a9208a1a0fca74ef0aacaf9dac8a77be5b24b5e70e7b2d53a3ac1ea5dbb3b71e26caf6b184a724511603ef1e454bcd0723f5bfdac3a164662d7878cb63bfc0281e8e72414a47b18a4a9b89ca796d7ffc4364cc38693c67f86678c0f76a5d6759f11e1a78e996d9acf7a87aa5475f47c0c2d2b39ddaefbac700904133d506ab9e5d672e83c114f07dccb47b54e8fdefeacd7fc7f716cc0b199a979f0b2fdf37d9f5ec1d8db179e55f118cd615655ed2a0ee159dd9e314efa19e20bc5ade47ca168ba06b48026f38079f624717c84cbfb958de0623708aa1a4ef263e362d6f4c9d17fa4ef79c21e15afd1a3c025ff4fdf534696e1b6fcee8966a08a5db253c0c957a34aab2044d97f9860253605ae0c68bff7503a48bf0f5489fefa4183f364f90c8f6ed2fcafdbc7e7fadb6566331bb5b1242f7a827c681c04c90d7ec39e093b0b4b3da245f3fe191466f8c90f36fe0856fe7bcfd7057555e51f8454cf1aa45c10d794cf5fe99cf08bf05b0502027265a5f3e1fcfffd8be2d7e47ee00c81626e4d0f0d4178686e4d3708013116062f40fb172007694be94a376701799ddbe67ef2370c33e7fffedc477ff200b38338cbf5f21bea0da87eff7956afbaaea10efc83c8f6f39ba1b6a998bd7ee65f00903152c8d4266bb9601c9a46fc767b91d0a0d891fff7fd07989eae1761dc1f9f0ab041d03b972c6b4dfdc8bcd6eb3dd2f373c180ee61bd713388a8fc59fbafcd577067319e75074c454f094fc8790fc902e2d52f0fcf30ff7b59a7e58268fabb6f67f787dff9ff0d0a32d8b2eca0184e9fdd6e81349424c30696089c177e0abf4e7740fe6a1027fb17f109e42f9eff139f50baf32ffdf7a77bed5eed26cdb17fe0de7710a92068151f527c6facf89b0ff6448b924ebd16d88de41af2c03f8884a8a219729b893b00e7d7ba0e64cb3059d71b2e9c332fc72dc5735d7266f664ec14804470df2e9d1faf2cc8a72e5ab5c288acefd52a535e7ff943be65e533099f48f84fedafdb1fa99f298b2a0bb754ffc45d83fbcdae698ad098a4fe9d6fdae4feb8882614e38fbcffdf1c4fbeefff5ef3af4b03207fb67c434c843c953d2aa70f7af2393f18f2c5aef468a82f362abed4781091a18b3fd43514ab7f109184fe9ffe3443516a1cdcd1fee25cfc19bf71f5571fe6bfe605d439e65fff0037b0c2c4a56fbbc4575693bf3779c828df260288073f15b17ce1a1e1a4b3817288565e119e6b62a61cf747bb2d8833e276fe5bc3469c9c37dbec7742d5f66c3a6047f48c9d7136e41f6f0b675408d7300553101df69cd47eef7a840adf019e8c8fe25a1c0f1be16b1f2586d5f7460f94e4913705ffe81847cd5eb06bc028f3e7daa4a82e20a960cc229a94cfc36011a7bf6356cd968f2d7172f7c8c9be2e52b12e4385e57dda4cd8db099bfeed75bd87410c9b11bbe0f80d5176cfeb4831a2e17bc58fe28f579133af35de2f0cc4f4c210df979a3cb731ea39758c2fb143d7a66a8379b576bc731a98162802292cbf4490f13dca4efbd673288af0a7780ab125cc2a7941511c02e2cfe227d2eba066c442030d35ba5fc49abf7dfcf7ed7e11ab62d9d99bd92a9a9819d94a99994978d81ad9589afc1137bbeb8f3cf2cfb866e0b02cc49780c2fb7da08d9cf0f89f5f070f3b9045405a4f7252ba73045dad794d85f8da503f0ef9514fc8d9af1923f8e0785102c013f0c5af9a9e9e29195a831da41f472de08d469c09f8b5b51012f806877fe7f823dfe067cf8744abfaa3fdadd0a5dde359c7e0bdabb466a524967fe73784e3b7b9fb233d7ea492df8e8a3bfa01e3a57d3e91ec4c398674bbe22f7225ff6eae06e29f5db07f7afc03fdff0fd82098c94f8faafe83b9c49f3c3ff4f68b2271dc3fd8c7907f10cffeec19fcffcd63c8f03ffb1b40984e99a3a5fd916ff02d5338b5f59b3aaa79537a592213ddac96558ad82ffa1cbf72ed7f77fff4f9f44007fe91fef9436be91fec9f7fc4ce730a90200fc6b5f648d67e10faeef56454c76d3afbdf6843257eb1fd7798fe2ea82b25ffbfe8e09a2410f58b8044cd38e7c4f54016aaba89d2ea1939cd1461fa8bf5df54f819d813199e31f6977641cd84a17030c6911e400441cda20723a3e835f9ff770adb4c5e3032525dd3dfa6b0b531542939daa1fc200ff3570ed533c17a21fc95f837bb4b686c43950a1da26b321c12faac858d9b142204921ef614c21fea4ac53f1879bf072204f9e3bc10c4dfde26c8dfc51bcd565051e89c189a50bbf0b5d6d2ede4ef2ec0f46f73ba7e7ad5668262024515da6b244978a58645e44d4997a927bf77551466df98ae6b9f68ffa0a791ebcd3ec15c508f0c7f0d068107b5d766f77cf0f7e8bacb1cd7be04426762c5bb9f405568000b22ba5f303a0ee1c0da5bf0d438e39f54379bdc00050c7db77805d93f08c50fc19b2a3ee5ca1714c50237f0a253eea1328917bd2675645e7180912d0d9423070aec78870a7aee1640235176a5870e78aa943689ed0009ec6168ebf02a0a9539a86aa1097478b6a5c57c73400163254e13ab18fdc79e1390e4d70f8896cd6130be006f50654a1a981f74d2388315267f63cdd671c59e0b14f07ae0c136c3cd3ec69b72d17741c70b7adc4c220f28785089fcda9e216bb036eb5a4a1f5cd059ba47d801393278de68ad18bb347b7a05c74eef8ec4dae482018f1542dd1013f7b08c36ac584ee8446bead25dbf4032fcafdb6db82858a6527de8ba644ffad4f8116f79114401137099c6d9f6b8e4a292ed07258f16541fb30464385000df9a128fe949043cd3954a7f75e29dfef14cb50e50404c2f87bde9a9be64baa3aad6cb1757c88669c528400e2ad6e8949d9587c98b9587300c846537f26767064001894854a456ef320a4323ce4025fff0cc10fd294ea080f7050b7cd2034607d7cdbdc754bdd6e72cd7fb2520d448eb0d0f6705c342cee709356f6900e77381d7026ce0b00473749c1297a03be337e328c3a7bd5f22f618904928bc8c36c5c5aa19bfcdc0c0eded0f66f9e62df6f6afdb9d77990c3c359b08d1835e1cca6c44d39cd3b103b2990ffdba4c3bc244cc07419f11f7dad1f1d4e48f802c15d57bc8ec842833ed5041138fdcaef48f74e807802ef98a51f163c8f5b20bee71d226dbc4fd6e86105860e7473a4b610332c7511acc914e4b392c65eea83d40add7563d720e9c46edc69a11928f6c46b55d3e7c093c1f7ef5009721cc89535759b6c1eee1f3a2fd27aa408188cb80acba74df459457cd283ba66ec1cfc5925181ef2f6f98204ad7efb6fb24a669779dd7cf35b7370fe0c1bddccce3e0458f2fb7b9054ca0bb26f5a6441d800d2ada9b87fdcb17c44d179a049e6fe61938066fbb03bc5ff9f6862ead0b11f9683af95bed27d45f5c2e8c800271fb9629b7018c1f67bb093b78b25c92b712d733800d1656283d5b050bfa1630ccd1bc16a77dd8cb9c87811cc8c8acc6f4c612c41d62afd159b5153a0d4eb828a080ab7f77caebf0f6744887f8078579cbeded46543720b432323809fffa2cfb234661d23d6f298740889e1de0ea2e0fdd31e1bf9c1ffaeeda3ab0461897227ed30fc8b2369e9f0f5f067d321662dd910adb99d261103f0320a658b028e1160f5b0347c12539c654bc98f4481550e0cce8e15d07a3f59738b9d8091e78f664e7f3ae14001e10ce985ad7a77fa92f6a0eea7f8b81e1b191920a6c302859623f54124b93dc0d33676bcea4db26bc6604847e5e8d1773d9af2ecac87dfbdefe72c8ca518ffb1228d0fe59d84337c0610c0df2b1fb949d46d0f2bbf60da040a6c7abc2077fa1849810e8af7a6c020fdeb247c240812686164bce294667b87ea86a41970f0754fcd8754081555519d704e9fd253d3d688c4ad7a4a915a1926ac07ed45391f85fe5c3f53fa4c76c55b9799fb3f15b020594d9d95e29f6270874ccc59797ab737a33f5ec1f02eb97a890b782c6c3d8fb07bc49149931d82da75519a0c05a80b5ef816899022d3222a37c4c40936e3dfc1db0018496a6f092ff5ec743d1a367764114af726198137dffbabd1507eb62b7f105b3dfdafd13ea84d8fad8865aa01d6570304ddd2b9461cac73b50c5401c6f3b9e022870bbfba14e9997f0055f1d1dba34a39370b5bffd1e4082c18a78141fdbf5e5d220d64674f0f27147ea4500344355459ec3b0f9a44b092ca2a2b044e8d70ad766e0fb27e6447c26d941f19a11170e88ed0c2b628e6605646808794491f6d84a424b55c9a195b2a86b48c80340812be448f8e0a92f466b3ebd65fb39907925853261c006631c22fc9568b8e526730cacc9164f1277b7b241fb1fad5fda1cc76e69ffb04d19c93b3f6f3d478a0bc0e766993d1d5d61f7ba70e9dc2296d9d1b0d50c1c007ecfc1486422d40e4f5d95c1c50473f0916d5ac003f357a5fb0422a000423995d11eef34dab564912110c8f2f34b692383cbc66d83d5c58ef3a9c56ceaa74e0442a30f969bfb7012f244a1459b529f702487e185fd002a719ddb6632800d3278ef85a41f8a94e3bc944d0050a06bdbd637d8743784ac83cb1116863a37a7205c1e28504e9c3d6cab10ce2dde1229a6d31bd22688f78d13c02fadd1f89b4adab5e6bd777c078d04b8f098591640016ebf991c9b3742485ea90273361fa7561648489c01fbb1211488536d9cd347fb28b92e52ba829b5b530728a0148117b3f975bb6de2cd1b21d8f5a5e424e34fae4001cb0dad2d0cdea55b99c151c9daa8749eca1cf2e70015a7b64ef314fa9d26232b24dbca2e6a0263e86a20b0413acf89f5b397c3385b230fcc1e7dc577974210e6405c02a7c629e41d433565086a10097b288c94d37e005040f5cd10ca347c5a6781d8f382daa144b1f860af156003eee6681391fc77438eeae27a7eed1e7aaf874ef580b80814fe6bbfca41e6246f13f3103d6417e38917acc0faa5f77565b48c3b1df1fd0ecdf286ae1d11f4f900707ffe819b2b578404dd6a9586853c4779a21de356607e8e55ca2de35f3cdfb5cc3b4462531bb18e571a94f823d797c143d0ae38585a751564bac1c09dbb85097c0aac6f75a6773be275f08473249e91234c9f6131c94380f129848ccc1abfec40e2834bbb80f649da00653b07c03733f6537e9907edc34c9405c48d1f89229bce99800d86a53771493fb6e13c89cfb0dc8e6a4ba9f5b0cf043c5e98e710eaab739d3c02ace2bdebc9af9263d4218002b6569a14d94dc4fe91fa755d30192d8295331e6e40012fb4cc6a6174eec2eb9a68ffbc5ad9220c13da25206b0c667fbdf7fee36c0ad463728edec247ccb668cf0059722bd23eb43ec995cec4c396b86c6ed474076420b0fe1e29a7b373ce77962a0e18b70e286a4546165403f643d066ee6d3eaeafb245d4737936971e56cdaf6dc0067035a6937ea51885d87a7bea7a429773241ea735f37fdd4e80431d871055b41a529b491b853918f318f72b50208d9cc1a44529bc84b6b660e145b9746f2634522570fdd03e3f415bd41bba11d2db75fc6477974f08d6408156cb33e2c216b286b6ba4fb5b16f73348bed5853005980d754523b6b47cc94b7e7b3008cf4be5c29265820dc83dd513b98f9754da0445857ed9c94a5569cce0028601956d35cf47e22a1ceea10c6a626142d7dd45e1e90e584eda3c31409c2da426a04e0452ba816f0d6bf00542bfd765749c847391f0d939e992fbb303f4e7925077c5fadc526a421d2c218dd9a6dee036b3c7f236d1900df7ef1e3f0795a8f27a2840ab570a057fafdd12b556083466316a4dc3c8640c7a9fe5694114c0b84b69631c07e1be2d9c17144c99e3d6a8c9449388040ffeec2086e10fc717b9bd607e97b5f604f93dec7b1d80d7356a0007197f0e55554a9adaddb1c75158950b0eaf3533fe7bf6e1f50d350a4ad9ae53bbb88cf6b149ba462cf7e06484a37058a36f3a510236af8b1be4ca191c9b7bf79066c30d2bd7968dcab4f9af0454dfba9fadc699fccfb7da000b114bb6cc7465490127a8942bf9d8b88d7972b148046274b75b852bcbaebc5468c753337c855bb462f01fca7b9f9a30b92047c815cac8cfe4e4d25023fdd66c07f332ed71e407527d547674d1d8daf99ab2c7e39051498ffffb1f65751757d4fd3308a13dc5d834b709720c1dd5d83bb057777f760091e5c82bb070deeee16344070ceed7fde9c67fcc6f75eaf016cf69ab3bbbaaaba9b93bd215d29c9e14896b0953a9fa6e3ef95ef0bf0f936544d19b1d7b2de072e213789b5b1bf8b844d0358159ab214fcb63a143cb97c0f071e3a27ca415d7140ec1acd9fb3b6ede058d38e2b837716579ec67a6f06f87056f33859f4b61357271a33c7fe8a3cc2c8dd8a02f5b58860ecceda787c07e79e272cd54a125d52b726606361cfc9a30f3adc96fc4419a88fadb47938c8b60650c8432ec60a5e71b19dc8d7e3e6225fef19a768a480050efaa6ef5ea497429b832f11ca4409c45789c87a017c94f7c06d1a21efb5af76d3c513568e4f227ea50ae0b3e6ef1bc810b568146f7ff0f6066ae2c5203fc1010be26f71150cfdf3d23a2963b3ea157fbd7640251301d6274a7348767f27dce482a0944fac3548b6479b1c6affe5e79de469b792e571c47f8a36f3a9eb7d37eb13ee06e62cfcdd74b7b51268116a8b53c2ca5c502b3ee8f6073a68e2386bd7027fc53a88245c8dec63d24eda7cfa01bc7fef8295a0581143d3b99769ebf88d14334dc43d60c18c4f205be9df00256cfd9542744f1afa2fbca6307dff253ef5e6527be8ace13215e232b38888bfb27736d06aff177ca45d468d641eb647fb202cf32b655cab30493e03b0ac7f2a97e223ecf13058f03b87bd824530ed8bb003f8957bdd09a333c378c4a8707df96e854bfd379b13a0fe4a3dbd6d5954aa7553d6aaac963a8a25d238dd0006f0a96cdd4f78551505f2584aea864a7f6ab4c2b501f2df84549408826f4414e347a3ae6b932e09e1a521a07e28b10bfaac6b7275adc391b0bdcde7b9f521a604201379e8e392bacf39b86c30e0696b5364ab04b41881fae4ab517ef193286cd61fed9d8be1ac1bf242a2e9d1ff52fffd5ff59da5c60ee5278495f21ad46535e6520f6beaf748407c3eff2226acd5969c8e37fa703991e1cdba9fca1e013e9f9ce3943a323325f1a2fbbdc63afbca2e03c47faeb50aa614163d25fd0cf708f4147993c62c1120be4d2a1960ec32848e333914e231255ea1f474c903f875172b831d59f628f8b2c4d26f0a2b69b2f75f3db020f5b8b075873ada20fbd04900935cbcd8c11af61170ce98f21abdac1cc439a3ad3f9fd19ca1d9d7c9750e00f9f312a93e24520ef1ca20703caf8b8db8c76003c0276cf53f7e6143497aee3515cb4b98b3cd8cb73901de470f73346963d9032c9accb1d77ca6d3ac7fa65cc08ca77ce70e5a52d3c75252a2eab5359af86e45a91e20ff53f6789e5d5f2c078521a0cccbf9a3372bad5d01f95bb429ec99561c89c308235922466e099111fe37805fb5a39b9fafa5334d89bb7a7c763dd78b1b6abfd803f1b57427264be96771a4b38679c76682a56eca29b0604a9a6af082c02c5473181fffc22b8d0b520b8b55fe7f9f63f8299f93b21c75dbba6cfd2635a9c8138a51b704f2efe7a2891867459b9b4aa29249fd46f6e9317920bff56442f679fb2fb012b32e9d36cb87d3434ce803f149c3a7903750f9ea3221753cca3a2df943917c076057c966f2f62bd26ef1fbc8e058978b90ecb423bc0458fa7617d0759421847026872f2219df8504c1503500df4ff4816f9c0e3bc102f308d3fb6fd6d81c5e9392407ebaf6d864a4b5d5af89a24d254360d4a16f96499ef8dfe735a9fcab9476c50843bf24381a60aa1dd02e21007e415af799fe7875e00052378329321d620117210458c0e7d783c2c8561bd90123de6c63291c59b21baf0cd457129869b8af596d58b8cd7572e16222b670843695ff257e427a7ed230d1f193adf7dfd4d5b9fa5e4b9acb0fd417b0490b5906ee45751ff41085191a461c3f5a6f012d1fb95b43f091743437b123b0814e5a2b44250970c0006ca69eb081fa73037cb1fb91da32d11a4e21de4de0f3b5af077eb12972c421548ce0bb622ccb679c4a00f86f496a1c86b52ff67dbc1fc6283fbe28e1e552110afdeff33456a2dd2b3afe9f3d5cae38a6973d4a1768ddc0005d2bad00bb518e38f31fee9e7ca56e4f43922e3c40fed4943eca1679ec483c450c989c2132f9feee341918609a8b5d312999066fbf337347b5632dc3c8d17b08d84c39fd9b8cd50faf4bd009e6074df3aa851832118101ae764a8cc1db7c3617181f13eb9fee7a3736c86280057d70a4c44994368e438393750879073dd0e7c84ac0f9a93dfa92868266c222d0c487acb9dd7f13f7e40df003f45f0ddc1b44e98cf3c57056d69bfefad72d920303dce54e3c6f9bdb7d46952d07a88d08acd9fdf7b0003bd820ce31f1042edc711945cb68bc6c1ae67b277520fe8cfc79c0d2a3fa545e7d4fcd72e1337e4719b007d4ef47c97e16df0838855ccb215c85750946613feb0213cef3c408627da08fd8eb3fb13c63974b0acb6a8803fc2ca75a97780e8f1892a986e0bcf3cda70c827223e07e4c9172f6b726449fc4f5f37c51dc6d222dd18101164c3c17526ea947667aa8c83c0f200d524f619f2802f2e8ae06027fa1c17d9e2911dc4112b56dab98bfca26808f28befb6dddc1a040bd5d5393888eb6befcd505162036141e59b3d6a613d6d07bfdaeefa4852113990216c061ef557c31105ccc311974adf50e4e6ad1fe480a0bf0a7226e63f0e123753ef11ae89f3715955bf0c800fe3f00a5601407d6543c3052f4cf49cb8c796e6619c03fcce484f389ea9ce10a72b1ed073698a8be372701f06fc62409615365c7fbb6cce1a2dda4fe679f6d603f01c4afbcf446516bd23e3961ee28bac64fb051a459fafffb9c4cb430f09fcdcc786aef72d4e43ea32c438f12b0a0f0b80555e283f29886aae68dce3f4ddaf8c61fb9c08060f1319add2083b7277bef74b244025153241a0880ff3748369d269c20c2aa0ca2c070d23d140c802905cec7c270b55ebec6c528c672ce2fd6623bd4e17848c0063e6a73a5abd9af2ce048bba40693966894d8d806d46f661c02d331d25267df421d4703239711cdd2a681ce612c54538c418641ffdbbf472366683a99fbb381c082b8e63bd9759ebfdf6998252506a622f02d9cf991cbfef7794a67015dbe6aec7112599b0f6e5cf53df32f5e405fa92ba458ef7babf4ebb48188f8576b08639446099cdf5d182c85c59dcb5db4d950f6670663d1264739203e7653db886ee41f8f62f8e3df7470ea5543f63502f9eb1f049ec1235f9f7deb7d4b80dc8d593f2eb500505f21d05c3eed2ea4727d13d36579a5592f5e74bb02eab7862bac0eaac1cf7ffd617d04d1ee1dfee6257f070e889dab6e50f53f9ea90a336de4c4befb8c8efb6c20febbf61a1f36bf41ee5278bcb5e0324d1c70959203f8a670aa1fb7a2b0f8581e9fd6961c9ee1cc694213f87ea71275d9747619c92ff608022bb0db11f247378049b6c95e5f8da3d0b0287f2eadc323db271c3a39c901733e7802a4526d8dfcda1ce5c8f706cb76c90a82e701fe4cd829b6bbeb2573dfb68312137be5938c7a223580df34edbce620aa1220ff880c7ca9a9d3593b725f04f0eb14f32069ca8fe97906211ddc0ee7669e1dc86260810c74265fed601c81e81fe15f8eb2841856255e07c0f9f23b69e3923039868ffbc80f37275cba05e9e309f0e36dd0b31676c5b0d5acb595d053e5530eee26f8405787ab8b6c656a971bd589f3f645697a5176bd3d1e501f4f42df15a435d5fb701ffd2586202a5e32b70c02f8fb064245287adb1312c306be37245a7fefe1b22a40df73aa1afff6bb0a4a3cdecf1a2799e22aa509f71a88bfeea1192e5bc9f309ff5ede2143c629351a456101f52b995cd3a0ca239b56b54455adf5d36b4f712f81c2ff3e0f46a4b5df63f175f1f14ed6e43a64848cd7dc02ea4f49443b35334d9db87e78b7880d6672ebae161fe07ca7aae1bc9473b5af744decee7d12eaa31eaf1407bebf83fa926eaeb3c0584f8cb8edcc84083f277470c15905d572dddf205925084f68772cb3a91a0ac63c207e5afb88ebfc20fd6b82f91832e932aec51c3e8800d4aff0e48cb78722c72c47f2f3d04fbcde34d4fbd380458efcc03a7edb416994bfc73c63b4902caee3e31fa07ee77528be647d88d51d9ce70b3f41bf8d4930d3031664283b5fc91a65dc63bbdde805555970df64cb0600f83c816c4ff12d39092a19f54dd24d0cf290e1ee1a5890968f6387e5553fd574ad1d30c8fa8dc405ea331c804fb4038a49ab4df0913b46bf2b197ca643826d7403be7fccfe0bf9bf9d1988ad1c779f7ece18d7f2926402f753f6e03b810f521febd9ea8d4e1da5b6a73dba1380efaf657edc6c0f613c4229e695153a56f5ef48300016a82243f98e1e989bf0ee18d539669a542da5665ae07c7f45fa80da44716fc39ed39f25cd82f54b79190ba8ef4b7e55054cddf943d6c749278c862d666fb26e03f5c7e0c8dd7a492845a62c5c89cd47c2d3c565af7c00bf4a0c85bf1d1b4fa27132d0e7a07fe80c4df55b04eaeb295adf6d792adb795bcf637a7bf216bb4a327580bfd3cf084c898d1735458fa078ccf7f6567add32a604e25bfc3863af3883407b0f64f4dde104479be920c02f9c38bf94f4fa3d71f3ec1162faa6afcda6d4d1000bf0be992eb36a77fc88881b746ecfe310d855e9b202f01d7573eb034dabd0cf94fab92ea5f6d78fadaf7b003f456827f0ebcbdc58e9db9a7ad23cb710dab98c14c0ef7e69c8658176fba05111b4a54fe257f18ec4170178bfc5550146c27f887e9e7e689dc5c8e2d25017c406f41fe836b9af5fa0731d028e0539d5f46b24704fcb81fca02620288648ea9a2aa808b129b0e2bc5dd8be02b451886c581158734a376b9c6e43cddee2a887b2b000633de63f73e4cb6a95285e888a447114dad9e12e9603fc9ccc3a1bfbd081717f88911b394cf013e55233130b903f857052f9ea1c94cde8841cd9bfe0b38906fa00fe039ac9521201be7d7f1b891fb8826398489b28274d00bf72fd9154715b70cae8da054504ea7368ad601ab0a062c2dcb22bc005697d58727196d1536ba38c25e913c85f0fdf5eb6f6861d9a947e73d0ac36dbb3bf04df2fe42d9adaed68fc41cf9736cef822b2931faac0021553d10cbbdb74bcf02b91afd4be33678e819bb080fece71ae4f6ffa33df352ce95ab1c961f8496d5d03389fdf12e74b6f1e23a2177d34a558998721958a0d017ce426ac78de50bf32bbbf2465402b2d1304af1b0af85fe44ebbe303e57c118d98a46abee6357d8e1e6704d60314ac87365d7c57af61e09b4ca71c6dfc023dcd02e86f3a901f2cb92625c8b848480bfe0974318756746401fab9f2d364b5c2572c539ef3c2cad112e8f8903880df4679cee06e8a40c88898777874781a3e1ff7e6021648be9ae62670ef9fe05d4c2d51981d55e0d7622c030b824857490acf0ac5149b6e1d06080689d687d60b817613344acecc7d87bbe8ea1a5c45ec5a5dcdc441166041f94840e5fe99576ef8ebcf6f2eecd2eeaeaec69f017eb723af32e8c3c90445ec8025912b8f323fefb932c0ef4afd659b1abffff4b106ee9d7b45c47628950d3f707f7a6cd22bd4fc630d4774e0a91f17d8905550cc80cf47d022eff568dd3fbe84f3ad8e46fff2358ef216a8bfee315e3b9f5190bbd6d8937f39e657c41fa1526a01fad26ac05b13c98e8bd893359d721463f7285b12707f5aecdbdfc1278e7e13b54467fbc0f6aff3481b1d5820a44c979f5e160aa11ad91cd76a8893ff821e4800e85b7cd8b476bf63bf57fd583f5fb8f0adabda687401f08358c0b7a848a211428a421ffc6e4647b1ce741f203e5f09fbe57131fa3a5b7c5bbf99738a5df2534101f4af8d7e68ca9f885cb383966daa9362b5e7b1b64b1b003fb36d43627f80add613d9a0a4a99190c591120fb48d5b9e7a2c1a195e37580fdf2bcbc0e4c82bd89501fe18dbd6c4fe4efb05a7c9a71118feca40175d611d203ea4063e33a8fad45eee1f144ed253744c53366301fe34ddceabce6bd3746ec8d971ad69294a49836f8340fdf6c552ad17119d3ad3cef5e0f9d21cffbaf0b21258f02db8c92be27ba06301abbb7edf3d58e77f099d01f0df334ec524b6072f107418a6b04d47abcf93a95740fc576e7587bd8fc3a222fde9bd5de835a7f5125604f823321408ee52c3d4bcc348030a7e0a3e3eccf614006d28abe3ef32ca9bffa63850740bee376cd0e7fa14026d1e7d119dfa49a48e55d662362742ed1d11622d96beffe5f7fb624ee8f9b588fe6bcb169e6e2e99fa8005c30bbcffd608ab6fe27070837861f4e678d05f3997ece0017fa1341a9ab1696fcf77bc8380e265686e7f6bea53c05e8cb84c5010b1409393fe4f2ef8dd764bff34cb20e05f8877f948433c3edff3b48c3afbe9c418f917cb0250bf520ddc14b318afa7294cbd6457f47f0ae4a26306fa32b7d7448b12edf1eb7ecc66b2708fbf137b84c003eceb5d6bb81c4c5288615e2e36fbfaf309345a96df81f8dc9fc43ae359694ad3167b20fdefa6e4f4d2da0c58c093caf738a2659444ac4ecccb909cb5f363a12109f087e06c680eb5508e4947c474b3b397e090e8135d0183778759513fdcba550cd804fe443dd18bbecb66a407f86b4a7485af3359489efa235a2a0c89585eba4bb3807ee172956ff8c76049565ae17b988a493a41eeb339a0ef293992f8600c8b9c6ee6cd2973b5c7be6d63f000f88e9f71bba69e21386a141ef33e014b45044ee312a8ef7a06b103f8d02417d8998da0a38211d834df76017f2d5d1cbba067af617552f7be1b52c4f69f6fdb60fdb019f86de9f237349c2f8eb15361b6f223135410c00fc4943f669abf68d35bf016abdd06a3df9c598500f8428fefe70bc5ce4c9f101ce6a457a753adda8407503f2fffc0aef952e472a1d7b709e1311ee2fba79810f02fd21609b2aaff581088ef45655f51aad0edee3b03f405b9ad6f138aa20d7d9c3b12bbeaa4f9b9171d5b40fe28265d7a570b75f0e5c12aa1e6ea2a1bb64fbf0ef87e5d906df931db95523f45c108d7d11c875405fb03f95bedb940a6a6ca2513aeaf7eeb2d8982d1d64919a86f4ba33f7ed4e3a464808ea48fde36513ff875400ee8e77175f31a38e4052a107884e19bd11322f54e0540fe690f6bc958cf1060725debfcac89bfdbb8fae31c886f7e98ed7303a13a5d96ca740c8f7f923812068e80fb83f41276cfd2405466c0cb95ca2e55ec111d770bdc0f56be4531fa05c8dc5e776dff02e607c28e7f42805f1c52e3edfcae89a7bcaffa3dc291f33fe65b4922207eb8c0126221ceb1405e1e09b9f35167be90ceec9f01fe014e2b85d3170fd92fd0ef3eda1f19a0eb3d5902fabd7299ea603e4b11bc0ed1ee8c55a0e09fab8c6500dfb5daaf3ccffed631a3f15de0566de4340c98e501468cbd339a27bad678b0e79c452c2cee3f3c117ea7078cbdd3131235cd68c6b00f11cc3943ca824dc6a64501fc839df3b263886988ad64e806cfb397deb83f5361801d4086fa102e0e980ecfa8850e7dfcddfc7050e3cac0fdf941c97dfffebb5e43d81112a2ca430ad2e9d74da02f3369db8967c32bd32f477b4ce83898da7f873b1b883f33e3d237d8123ebf1056c4cb0f72cc29556fa981f311cbdd75bd7faa684faf59bf8b79e07ba43f6808e813143839a366c917f3ac54040c4dc1dcc7033e56405f36c61f29949989a2ccaf1fb2e8b8fa9fa8e27fef020bd4a64f919c2b6c57669a3affec88be3b19fb519708e86fe2b6559da25590d6ed677c7f7f28ad125b5f9c03f5679119cb11f77a6eefe61aa34ca9b0664b135729e07f17aab198ae9611e19419939a08c9be90cd9b9b02fc9fa681878f10a1972b4acf3f42fc3f0eed1d61b201fedcab22f1c607890ccde0ad9219c7bb56019dfb17c07f683eb05782c0b245babefb587139fdc7c75eea3da06f8a323a3993d7d1d8e9932b7a2bc10592e9778903e7c3f852b7fc8ab7e1fda74304aadd183ea92d8e30e0ff63bd3197b52f8fcb2284eafe94d87dad660805cea32e7a71ebe63659204d1e10f72a128018731b7102e25bd9e3bf6aa3ec96686d379b1668c1095bf9f477e0580fd38f9027ef9b022e6cb19c8b3eca43672ae200fea6b43694c79cee484c83bc827b856ada2b054538a0eb653abda5c4aab0b6f00dfbe94b553c248de3da57e07e6b1da092898b10d9c046421e76119a6d27ac8502f9fb57533277e345283f7fcfea76b5df0bcf3cd412505f1b7411d73eefef40745291ec3eb0de7fddad2205f8357e0721d3f65f5bba51ac8fb3ecc898731ab2760e40fe6fb7a9cb9c6fbd67b45346921990d2d1bcce03f2e70ca4f0fcc98bfc89e8f045cd3844c1bfa4cb5200ffb164eebb796ba4d807ac3d0418bb3d99eb545802fa5e9606cce2d57b5ea97d787f6a9b7416bd7bd246c05fbaf8d23b44a466191c136ea182db83c7693c0905e4974697a53731e3774b1ece6724359f799043147f02fa929aa531ebe8e5111e84afcc2736267e04a86aef9fff05ffff5ffe9e39783fe947d6271765f3d71f01e107a4ea8dccc002c96895c92442e2397b2442739fc3405a362a23696081f0b05448d91e3e1b8c567ff383bd7dc43f0fba65807fad10362f56caa9ac83bd14302664ab9b19145801fc9d4d5f9db8c2cca5426bd8ade1929af90596eec6017d347b4d5fdb13f36bc2d36bae536c930e1a7ec01970fe3f4da88a9ed39b231c7faa4795c009e5ca391d02fc97899ed59ef9a13557c7053841b50ba881441462803fef36b024324e66a50973fc93c9d2c83a593b572da0ffe1607c5079af9f1dea0c0961e9a208599bb46202f0f3f2650b0321e51daad503c6149ab0d17f94b5d781fb137bbb4fd48299929d7a06c99a3095c03571381905f8f3af634770537c3d7e931abf7f5ecc3772632f01f013749ccfd4dcd48767b50b14f2135f6fed211e64005f3f487370d3142a45c4d6fc6d6dc23e8f30500217d4be5759ee6c1add12f9f1278c297b275758aa240de0172c4e4d89127fbba75b35869ba20cbf1251d67403f5013e3289f3cd65b1bce4736398225dd55b86001d804f278fedafe5caa25eab12bcc97156f06983646a00fd0cd5e4e3c017eb059ebf96e2d4e8fcc27862d31600ff914aa495881ecd952c3afa86e9d38d84bbd3052ef0e4d4a3b42b3c42e546cdf4d811875a64b560fa06e8433f930635c69138420d974676f02f705a5ebcc0fda159ac591e97a6e8ef8cf77490f1eb30c71ff8be03fd01b5599c87f0d5d2f670cbb4ca0d3ca47b0c5ae4c0f713fee91a828434787eb9550c06a6cc9467c34605c80f2dcf3dc25fb3ba1a242e9870dac43e94acbb2202e7f3cf85f2ea3866250d23c4827d328a19ac42c01760acee5b5a35a94f1c66411e832566cd67320e4f186b80ff7694baf488515427ba347d50a22bcaf1c5a7dd06f86b3c384882cadcac2c8e522f8622248ba958ac6340fffd9006af911a37b08213fec4db4e3cf3e2179c04e8bb7d8e7f15e827ff61157454387c2588d9e9823600da4baf56b593cacee578d960f70f305b7e69714f2800f99d0f8e25acc1240fd1fe345a04a9e49a0735a109389f9dcf9f15c72673791dab98ee70a61452b0cbe601fe9c7906835e80a4f170da63c799e824c40955a20ee0972e83f1ce975fbb62749bc230dd3b46abb1f75780fb87b5f5c59cb1d444b8db16e56f8f188656e80401a0ef9f2f8d33140855b472cf7a964c645668f1ab6502fed7d96f5fe215d68e8548619f771316ec0ae279d981fac849358ecde8893f0de97bea432b46a7f2771d2d207f0ad477790c5c66efab7b0bd2c0b4de3df40d1000fa2e1fb2a74c2aed8e82efcbe9df119db4b711f560c0bfc943e0f7846885a25d7df1646faf9d8be292ea0cf8bff2622a936d1e06649aa9ee67c778f079e0945e80faf66d2576e16be48f004659f855ed3a5cda0814b003fc39cb68109ba31c85d07a3349286fb1014e5a61eabfe4870c36244a89769fe0bb0cd70286e562ffd58d14608795e1b783bf2682f508b1f162ffe487fa8f177d32017eb702f75c5820cfa0256c5a462655d254ba901be40f7265be7ecff1e19ef6eb63a947ada274e0be7702fc3b54acd294fc764810c14d84594e5f5b7f5e6052cafeef73125b52772aaf008430aa12048a183eea1c9545602f8e0d0256f30874ce7304634db1f8da1a1a425934507fdd15b739188ab593cb42e44f1e1d12780a274703fc0aee6f229c334ccf6412df6ace6ee31f98b4937d00bf69a8f221020a2fc397877bbc86b0e5a62591211dd067c7935a9fcccee2f5c7d95ffa1ff106c895838581fae1c0a924d0120d3d3a9b7c8ca9a5f7f9afc9bd0950bf2d8732eaceeb4e4d14f90f48dc69ec6ae98519f1fe177ef6ff8a1fffd7f9fcff8adffeaffc84bda6cebc4a163b53068f7cbf6f3ddcea43df08dcafb4aa224501aeccdae29a7d293b5e2a8d5b9426604cc7be09bcc21708d78790e28277186368b779890552803ee458a3c4c0225c3c0a7db926d6e4f42a9b4600f817f95b8877f9f738a5bc26c2af1687fa6b5ea44a01fcb22c34170f95a0251318ab3807cfd481eeff2a0cfc7c2b13ee436140439db39157cc242a018cd0bc0e30520993ed83ddba2931f1d44b205cb037a73ecc2404105f079ded7cc52a60c8b534d22773b56bb60fb34e80fa3ab18c652682dabad5c65a76ed47e718945fb235105f8fb3866b6b0375bbe3bf695e12141763cf3e8902f561f8c965bed5a663bfc109d9f5e1e2f18167c71ea01fb8d1a89309bf86c4d5e870a3aeb47754bfa77802ce97670d9463174e3c2c7e71604f42ad92946edcc3dd7fe1ff5a7737065a8da35bea287f99f4c9e287cea08e03fedc5e9c279fafe51e1c4226ceb97168780a381bcc803ec9279fa9be6e8fed9e3d2fe80e7fec1b116462150ae0271cfda55802b3dc9971213a29b47391ee7326a0be22820d3f84566c2c1a1df1f4dcaa6e0f2557ec03f899b497abf34d5e733ab19330cacd7b065a1d665b60413f7b99b8b5cf3b4abac4ed19c6743be53fe64bfec0fb15d4a28b19eae6e232a7ed69458ae437cc7b7101f8b379bd418c393c94172e4dd6bbe6beaa9877a4ca003e38ccfbebf8747ba842c1327e2049fa9c45df960de80777dfafa61d94fe14b4a976b5c4fb6b6fa1941103fed220fa11051646fc4739d34e4401b3b3bd600e57809f1c2d2bd2e4f68db7618fb3b37334fecd17bf1e0de88bf9fa632abe9f25d0207634bd3a88e63c614a99f300fc5de5e2d79b6b8681bd423f64408052c2b9a807e82792866717ea0d76ad14b65a975c4244cafd3b0c40fdf43917aa8c997807f15f38eac60f8b577c35b75020fefd7e8cf6a8867b4fb748d7027fe2bc4e769ac30cf437bfa3ed8f38202a6d15dfda2381816ebc72e19107f0f375efea5e871b8aec0e229368be10674dfbc911a09fc7a148c77dbdf0ea70a7c7ff80eadf62936bde0af8871d1b370a9353c268078a78a48752891134b22f00fd808f33894433ba37829bc1e454e6acfff63d7303507fe2366f44064623f856efc15b7bb3f43cec71a302f9f14393d617594684c9903d4c06749524f1e1db1b20be6013f12342d8615b3b86aa19923ac8cd3e78bd07f80bfca5ecdf1a2b8a1a4135d561ca2897dfbc1e5919007f6fa407fbe8de721391e14b6026114632274e2d70fe2b3c0486d4e725f759ad2f83699347d0237309eafff7b9fa5e7cca59f79de73f3753413aaea94bec5703207fdcfaeaea63ca1e9179e2309a1f43d8513abd2300fcc9dcde6c9ad513913a0702e4be8f18dc228dfe70f7803f25446e8405ab7407a5208790313a4a4de11516f02f287856e7995af094e646b5c76b6d074eeca77100facc6031c51dd67ec804365cf49aa0de649d527a30c05f990ee89a971a0e11a852880c1f887df490fa310de0071cd2f150ca8bbe95e8fe73a78a37aab8cffe007c87c0f5d5b3bada372bd9a37cc4bf70679ab6861204ee9789d65b03daceefdd799518124684be5f3618ce407f651c9c792593d69bf8a76c710d2ec6041b63de16c2ff7d4ebceab6a94e4798d39d3a92e487fcdb426b05871bd03f07b1a7966b532c715c225208cd6ed47d1e7d017f0ef6d84a675eea878ef1208426f6e87ba13ce74920bf5176d60444e67bfaf58fc8f620e9202ac040ff06f413013606ec747616646e0e2a92b0e2acadc8ec20e0fdd6eb2f8b67bdce3cc5cc6b6a35c3206fab9b1f00fee0e0513ff116d772ca4f813fb1694566721be1be01f983315143a263204188b1fde132c6f8feb72b76d29ffff2bc353b4141a5b507c1f27e6daa1e49c9849b2d16f8fc6c6cd90681a994748c9a8d543b12fcb349fe7e003f61c514fd4cdb16bb4c3c23c5cfdc5e4bdbc1f319f87eee24722c66d8ff605c06bff19a75f15b4a12b601fcb15c601fddcaaae5dbd49f233b6b2da213b9f448807f55eb6576f24575f67c475ea941537fe353f41200f007218c97bb768daba4ed9ccc558363370c87384e803f57b0696b195f31311f12c137912633cb8ef2ce1318c99849519e214cbf02e5b714ac633b325bf50dc56d1cf04799b4bd337611369b3d1029789cb71166b18c01f8453e7aff77d91433245e019028c7736ad7e74abc80bf084be43a88f35a437ea3058a77f6bd331e9e7f1f707f59bea91dac3d440e3118d1bce1ba5d8c12fde802f01b3c06d78c657bb685a0bceacbfb90720d31e26460a9e7518a5568b8225cb58dfc490b25ba15b6184b01e09fbb1e6a534dfc0d810967a72fb2d76d94908819a5fbbfcfd762ec2da95dc7de37717e33c62446df51d24206fad7892c76e8b418fbba05f4a19d7b102b833ac7e981fedc70c3822cb3e0cdd55742cde7c8bedce19b941f68007f5a951dcbe545a480e36d2daa9abef2cbbb4b1bf0876ab17f4be7577a5f1e5c77bd3ca89dc6d36d6a01f06be5872d6310d9e1467ed52999b7f41d77c55223c0fb619198de3970b44af6dd965ad45eb1d0d9b30d01fce7d2fa11bd543c580eee5401bbdc432dcbcc017a80bf48127dd546f13404d6b81393c0db9771ea512115a81f25968b6ba7c6c32ce1efa5e31e4ce627f766d1007efc2887d63bcd33adccc656e2584a2b44d4656319e0b77b479c71c6fa8ba7ef3bcc3e6e60d54e9056af02f50be4fa99674f490e79a5a8d2e85fd10f7e6e9d20ff7194e838fe0bde5550930197e33c43f91aae0408af1055a36a052f0d0f125cb8861cccf126f0fac559003f9aa8fff8380d9156f75df58b83d7dbfd1cac3537a0af5834249733e362fcc45f399312172f8aec5aeb02fc456b1ddf6f3765dc7dc3b03be1e5523d71a7b9cd80fc0c2520b992e0b3efa599611e1787d8efe763e10bf0a70a90b324a6345a0e460495641bde4dcd49c888c0bca1b3cbb18671eae0a694beeebfaed0f16bbbb952007ee12596daf211c3d8624d3a3331c651db2bb57500f0cf4100a5141c2dbbc0e82aedf7eb5cccb70e511aa0bffa9dd6c9d29c7b598b8f5c252b35a2f15e779e04707fc6594b3ff920602891fa621c8d65c1175dfc6a05fa9fbb63fd3ef3ea53ebe41cfab3269e8609a9388401fe80fc7d975e11eb63e468627c9ae3daf677d1585f81f915b24563153ec9cb321f9f10852a1a203efac1ce01fedee040454319be686233959119dd89d319eb5105e0fef8bdca7b1fa71cbe7dca36fc46cdb16473c4d901e0c7212bdcc7931bd769eb92b1bea9546c0b8eca6c207fa1c0485a5796376f97ad3be97673058b13208f00efaf48c2cafd5de5ec81347bb381e890067a403214d0bfb54a7dfea7eab1b7da1bfde47afa225375e2d33810ff91d652db2325bf39e259ac12e0f3dfc6dd5a2602fde9a6d98b5744b17cd6250a1b74f518736dff3ebb7a01ef87ece1746ae3b474501d1f37709a3dd4ff2dd81af47f40546b903de53e106f693e1c2a154186585701fad262cf934609bee1759c62f668996382001322d01f8011a4276dd595524ac2d0f766219857c80c9503be1f1f88d9bbda308acd78e9d3bdad89760fd32dc03fca2e924f76d2f0fa1c6473102a03877c1c85dd0ecc0fba7a3ed383712be36ac84ef3b4595d6112f15200fcf5640d6d3741efe553472cbbccee90257373ba17007dc8b04362b7c4f99e2b9d665947ca3ffbc47bdb18e0f75db933dc7c5d8ded84c8dcd77cae9b43e61abe00fdfb2345f6cb08dd97239b14d88de4f45c0f4d450ec0fc8bbf4c93b42ec63d4214cdf8338efd36181ed1b1807e6ff0ec87fe0a977254c02774faf17abbdc42e212f0dfd2f0b60e4c17ffe0515053c88fd6c0edf287292dfcdfe70382158da12851e38d3596bc8304847e4f4fb440ff835c425946a6f28f84c1de8cd57e04fca18c797940dfd4c1be70973dc8f04a7b9641d81f08205fd6ad019e9f87931cbfd9e8d2115fdda274e64ee534b42d03fed3e7eaf4a8ee9cfe5efc2d27c1eecb374685de3a401f456c0b3663c7359ee975eaf0088c23c3a60b6a03faab263fa40a610517e521f791d266194acbc6fc3903fc8d9607de0c3876e93a68145736900f66374d0486007f493a57deba802c6ca2724403f956e4cc626c0e01f88b98fecc383156d0a9fdf9115bad2d9d54bc899005e0977e9d8f3319440b8c1f28eac31728f278e6712f00fcda5cc704535e3f966379a8c09c98f15573755b1ce0af8f8d46db55f89cec3c7f66a6a8c3befdbb578603fcfd1590acb3fad579b39c31a192fda503300ef3d7c04a7227f2a6f7f3db0bd1db5dba8cbf83d243633f0a00f1a5d90c7f47ab08ad06b567a6b8aa83224b58ef01f097cf2c38716f20b9d234c3c3eafa7afcf355ed8601f417a1042d7abdb5352fe9eb23b80cb8906f48dc2a80fe3041c656cea151b7416d758482a0fdb9886a901de85ffe6ed7eadc88899b905d1d2d27a0d792ae025903f037938591df5c503f9cfce8c2a18d6d699ecc334105fc79293905966fa34d2857b61c6881732a2159a73a2340fea0ff2979b4928351a0f04f73e7578c05d49c32501fb9dc666142126acc68f0da59870a29fe693e3c01f8af69e9d2be572e2ceb83e0222943ac57344fcd3dc0ff30b6f4ef65568de83381396ff95d685e606f6d3a30bfede62462ddc6cb30f683160522ca8f01b792e76b20ff15d97d0fe6edcf887fa9ae3df53170b3eb12f902fa03e6c4890d9de07eb84775052e0df191d3c57003fc180f462f5f505c4a668832b4375ef1556fa916b813fe3061e82e88e0822f83f5e3a4358f83ffa05d29a08f7e808c105954a094af62a22e4f32b7ba5a2f9205f24b122273779bea5f32ab02dcc128557c3efc9bef007ff891456eadbeb6226cda2a4bcba5155a2811861df07f241635a86c418abbdfae6bdb3a16758410cc1702f903a514891aa2c7f40c9999f99cc7e0fc265e5d1de0773746ec72187429d44a07fd16cd8364f4f5c2d496ffcbcf079d1e7afd0a35d4e9fc58169deb00658dfb5005e0978303f89bd4298668594abb4c333d1ad81aa26c80ffdede3377480909d2f985a716f537c656fc9f0c21c01fe76e15561a248aa67bb218cae8761f7de229a304fc554be4a1d325eabb23100b4c7979154d1c53069f007fd10ebc8980404ae6d5edaa66f53e8c286c5eef23d0df3d05f17643b31631d1297f53b85784a07c600a943f1093ad300dd733a538da9eb86879c7278705fe2a40fcd70c1839c80c9e97608b27bddff17e334bfdf80dc83fd839a39ffde955338675247c821028328eda5962417ee59d75e9f2bfef26bbcaccfe234c916bf6f120fe7e3d58fd24571726c8e315871d9c48d6679a07c4077cea4a45d2c792c317ed5d3363869c20f7ab6e20ff3c3a236424f72a7419713ee68962c9ee7c758cfeffbf7700491fd06ff59c4c9aa2a28eaec59c27b68f7696893d3f01f232442d9a76f5b761c628b399365a9f8b52cd84f012e0fc13e9dd93e37f95aa41259b6878652f1dcc842707fc21ee0b6ac4887ed3057ce589f26f1d3e8471d09c80ff2a85e16e02eef001f3574197b91bd24cb2501be2f47ff9f9d2bfcdb6e1e7424acf37e80912362422ae1334007fc11825e73248956b4718a7b44e363d77752e3c0ddcbfd10968c887dd09be72aa3458973912ae5f2f21c0dfcf427b5f8b2211667be7b3cf14cc5238771f19079ccf81bced9d56c69533fe449a1b5ddc0896b04b29403f4a1c0ce9681c6f883c38458035b5f067795af604f839e90bff430a294c3e5b660f6a9282e42a0fdb35003f3d4fffcde8dfc272996056e825238866ffeda50ce85f9915b6449f5c4c7d2a93047f15dd24c83f4a3800f87bc3096e74c71b79984f5c512de4db278e4bf411a0fee66fe598270b20c7978a3a7c4467d764b6628104f4f1bfb079302cea8937074f2c75b99e63dace5ca3b340fe9684c468d87d13ba0914299bd20f3286176001fa67ad246c3121926fc51487b6f25e36e43db90e2a80fc0fc34437802bec71dd1fd5ee40bd85cecebd9c0be8ebe610ae2bfd5d595b57f05e1672137931a1c8c8803e5b8485f9fd71b71223301fcf31235cd43bf51f09a09fcffaa1b5e2676beb66ccf830ade28b298bb2ff02e213f592200edd66a03a2fb7d54752774e6691c319a0be78b28c1ef88669fe3ab7f84e7c6e7ec22581d50de07fbbe3e3330afde3424f8ff1c68bd1a1fd85cfae017e5582c1368d24d9d6dc9b73b237268493e0b3892de0cfa6cc24a6cb8caccf88dea25798a09e69a5bda403f2dbf2ba5c05046fd21b3494cfa4f6929d568a9fa501a86f338c0ecf38c82fe4f928da0de29277bfbf03f80ba55694bbd6518c5259f83c5183e768f20fe6f200bf9bcaff93a600e7a8ee21efc37714837184838040807f5a56fc82a5f49e765c63776728258ca1e22b292f1020ec2abfbc93f7bc7cd5a14ab21aacaeee08d31c06dedf5745d40e590415f143d45813dea1553ea1ac4ba4ffd21ff6b796a5dd5a4d7ce291387196ee3e4cf06bf977a0bf7348ae2f838070cb272af771831b05edf6aa0b11e8cf6f592919e25c33bbfb12a177ffea1f4b736c3f00e01f84b0f33b9a144b38a65fb32d5628bb745e3ce2003e912741c32ff1de13ae594f5bcea73da24c0f2e01f0e15d69a5969b81c70392456987afd66bdb4ac435e08fe8466b6fc9ea1cb0772424f02996623580962607f0d553989fe9548222079f8dcc25c6fa87e7f50026207e0d19f8d811d07912fd7097d4f9b4174fe3ef04fa8f56636c6409ff5de972190bd8c13eb066b36c0c03f83f68cf6bfa81b221eab132cd82d18e799598b301985f65a82072ff0b3e7a36403daf688baaa8bc1ca919c0df5507e2842ccc197951e73a6608c8b6f4e9679200ff89e39e18cec2e6419ffd6a5f19556ec7ffc01803f81fec6bbd4244bc577e6e3a89cafc8249d873702d0406ecb6ae9d91f6fc6446b4bc15e19814d9244cbda706fa8f77ea44a9288d366da73d19ba153c7e55733772020b17bf70a4853cedbc56a70e17311aba19bd14e77b02f999b03285620389431381ea3e497723b6ca406b1fa85f46e6b698c9f1da3776ba953388abba8759fa0580f367b51c28dff8d8764b6f62ca58b87911f5f42006cc3f85b45bbf38de1b4af4d4e3be70c7f2cd0d9c7103eaf3ef7fb74c59a12a664e8f84439203fb7f92dcb403f1338cf5be21ec825c8ed4d979c728ccae9b66ab03b87ff30c4e550618f346c608de67b0b915b817abfb807e29f153c89957e3b599459518e7c28061aa35b709c01fb43bafdd0c54fbb614630570ae1db415fb8bf5003f112fd0d8e27d7e22f62a45d942104913bdb17102e0cfd6dbafbf3f2b9d321375695f05fe759ad2e3e304e207b41332e2c69f6e6252b18a0989ceaf33e4e4ce00be24691ee8e7fbfd5704b78c5c741ba52a463e361cc87f85e13b43acf927385cf4dec69688c446b3a77f80fa34bc257ec14bac79b1e6b541e0493ed253e4c89f0ac85fc9e31b309ef8694f776461c3aee16303d31a003f5e1f1d10653052529ab7634d87f18817fd46790ff42fffb6422d1a260d8c8a12e996f4cb48f1fc131909f8db19a85b66b498761b7f0bef23da228a4a9c19be03e207a922097a1b31e7668cf47c47d8bb053371b64a403f3eb9876b4ec22da14dccb0582c43f76d8086ef00f09db02fdd3983ec772f55bf0a41b13f7ec4e52f63807ea47fa52bb4fbf9a431108ec800de88b97b8c0606e80f31c41841fd79f3f6fb26f3abd84fa490c45dfb06e0f71385b219594d27f39ba57b73a41dc15172f0f901e73ff8aab54c1d2b08b78bd49416c581c8554d6b02f0277166482134c37fcedb23fc3220ad1a74f3fb6b20c02f43f7fa1093fa1030b56960bf88547b895ae03203fa54a7c524974e4117c1fecb4aed15cc1f5dee3529c000b7b02ce104b3a6fbeabd2f75d5de3caf0945c92608e89f7267481f0528daccfbfd103d731864303cd780fe48c3ab1f346c3ad14f7cea1f745e181b7756ad7a81fa0257e4bd5a1fb7dc406f59198d95c73b8518491ab2ff823f5bd704d670b3c7dff6df95149fabf997ced11f01f14b9da80a267880ad97ee9b6d87b5846ef8aad03ac04f7fa70ffb299ce92dda74b3138740c28664a5b909f85bac1c7efd0d11a0eed2afe1ad08e3fdb446a35801f40743d9f2da58ed11637ac03f0eebfbae2294682901fc826a8f0949e5fbcabe0ea2c58061a1a35a829f6840ffd9d8d7207e0b528645caf76937d671720fb3e4c1c0deb2b63ba673aed6ccaccaa7dd0399fd6e3ab6591720fe056d7764342be1c4f2bfffc5c5525f88b2424400e04ff97f5230a648b4f851abcbac36eceaabe7944b40fdeffe7dbfdfb86357526def7ec9da42689c067d1f985f3f4a7394b7cca6bbdd477526d556e350fb1d1107a81f026110d5f1e333c445998465b14633bd207197017f4ffe7dd815a9893a843309af5d1ab664f37b3f41607bb164ce0c83279dc8ac9276819a069df71241923ff0fefd9e39fafb39ca7982dbde1e85ffca48b3379201f5d5d2f19e52f11b8caf83483323377eb380c5e30ca06f38c26498d5419ba7ec9194fc49ff8beac234720ad49f3ce4710a24ea1fbc4298051b7ed6e9b4c8ab8901f94bfef339d48950a79d0ea182e67ec2697c9e8b1a806f24d8996d273408ee783dac39b4e7d920a4d43778ff8bbef07ff18bff17bff27fe1dfce8e16bd00ddc77fab3885fcac57765846b89f01ff0b2d85ae1efbc62d91b37b61182a8393e6ce3e3e509f1484857c76964ac4d3380eebb8f341fc98699409f8afe9a7777684e184217cfc71bc1a3ec77156dae000f8c4fdec21b473e756fa00f26b0734e9e2b03f8d753250ff6949284b4e36a0da7b409ab361d47d23f03105f83b262ff632f7a12e97c52bae3efe2bafa9cbaa10c05f7260e2a561934f2af0813d2e387f90548079f51ce8ff7d645894fd212178a7e17ebdcdbfd8ed2c5d3006f8fb6b63935f1d66c21ccffd6c32dae3a80c7666fa80fb63721c79bfd1a1722bd11926ffd89eef3b973f76fd5ff25b2fcad3afe0ac4708bb275eb2179cfc64d7080ee0fb21c2c150641b98f8a9ee57ee79523ed42ff7c513f06ffdd3485fb923b8caeec24a9d9f75be7259a06a01f8c77e980ab5da8d05449abd7ffc2fdfffc47e666004f0a9db19ee8dbe5dc9991ad4064f3355444d4923aefcff4b7e01a9687332486e3f9250ab449c42d02f2016e51ac81f3690ebf1a6a1af9e5c63fa5754e4449dd19610807f86d9d8cebaddd3ef74ccf869fef34d695359003a800fe3dcb34f1c3dbe2fb1ccde63b13b58e577201703fa2ba3ccf67ef8ac6ed051ff04879943d3eaaa212ba09fdc6d758cb9540ae1c7c10a2bc59adeba906ec203f33d8e663a5d503cf529be6051bddfe1c67ed876c807fceba773b8c439d447f222353578944c9cb644c39200fe7552b0ea7b5d19aaef976ef8f881f6c0de17a118d0ef48aa974aeaba987f618f26c2b66224798cf57800fe9ce59d34895355e873ff08a7a809423312dcdfc9807e2de1ac91235fd4512a934e26fee1c4374da3bb0ea85f5dacfa338702728f5b1b4c7fe5b5bd53425ae206f0e148585f12f40dce9dc74778a98be3e69b1dec56207faf4cad9fc02abe5321eedac2e74cc56ba80d6906f2ab36232e34dae414d76f23dd92a54d612ef737c84440bffefeb8486de941954fa5d94c4a0795839a2e0adc0f353cf26445051448138a27020ee787a163d131c05f7b307e27eb12fdf846e57c6bf52a98a38c3d38073cf7dcb6e887ae79e7ce1fa74c7fd8ecc48192f300f0172e9717e4761c0ee3c5c1c8368deb88ebd93707407deb1dcdcf67eb41cef69ef20b9564fe3c774c661be0bf930ef77eb2be3f88867f6718d6b0df73c6f13806e0e79c8568566b5571e7719fb07b98f031b6639360801f64524fa118a97c69dc9bff109bfe6ffec3647f19707f0bb55ba66683cff1f44d8f8b1df5f0e15a36ff00f5a9ab88dd3faa3c8b77f772c8e27b3742eae9118d80beef0891a7ccabbc715b44f192a2fe52d666713402d4e7aa663b81d321367b33a9aafbbaec76762b98bb803e44f9741c97eb13de30a0a2836d8c5e83181e8c00f03b0deac2369abb44843aed07ebbd02bdf0aab0fb807fa3fee6263dc03e47382225b52d77752e28bd340c883f9b5cbd036a47140d5ea82b25d775a129aa6a3ec0f9dd944d281daa75abe1df62eb29cb64e05bd1fb0c10f89e739f32f24d0eb1fc594d9674a091080377ed2400fcd7d3cef7a72552d5a0817e25696a4307018d13b8df2a71ed67668b032a37c6f6c1a1d4b8aee4d7d3c0fdd44d15f2f69bbd20b9cc8adfcacb30668933c005f2477e8dfc7c1083f5127b46922e45adc77ac0d83c80df0b7c790ab9721799f2946761bd47fd6eb1eb407f1ab20d44d5aa130143641e32397c3066228fc02330ff86e7be87c1f8e03756b65b9e4ffbb1d567422c6aa07f42c92dd293cac4254ffff43d16d791de6fe4420a20ffe1d350d3e6d6d7b64bb7e88c5db95b58986a7d07e65738e9d5bd76ef04330ea061be906d1f29cc55b402fc9b63908d362fb49a08cef079c07255038c5add25703eabda788dcb8cbc9bd98d61fabad84e78066204007cd2ef83a17903f97ed7219f70e3aa95ca8e03a51ee83f8517667120278f64f59786806c9f67386b3f2706fa0bde0be311fd963254ec83f763d0938520e55e3104fca55bb0d5a46df127b01d105a27769306d64779592eff2feb5767093ea4899e2bd3ee1b8979676a743fce527d4060e0609e5a7593a0b9f35726829046141d1c09a504f8a1a779e4e013d8a9a5a69f506c9e817048fdefac2cfecb7375899e1202ec9efaf2521d73db959db1e1f315603ee1d6da0e93516e3625014114e27aa0009b608910707e02222908be6ccfd8733e52e741079e387cbdf80cdc2f78e4ed4d130222dcee4746dd5fde546865baf580fe523e4a77ff0b922aa90842d8b59b94ccac466314c0a7fbffa65434eb0db20facc5563a527e44b5593103fe813ff12267fd8e3532263655b66eb32e38aaa77640fcd80f679428d927187c3650a59b9dd88c6b0c8804f4cd0d83206a5d0628b387b245036a48e818e10c01a07f845c4612a18acbb946a1f81971131a99e0bbb22910ffbefa3ed6b31d579c9b892fa662897ba81f322a02faaa700baffdc2e762675ef5dac1f4c1b2320d973bc0bf2456969816674088e650f18c2cad149b5393b004cc5f4bbe858cd7266657e1d9be88c4be8f8269e46b077e7ff2e67dca96f495371357ed47498f6414d44e22a03f2ad09641a5be4a704eb7ba94cf61b5baeceda60af0c7f8982425dc14ba28c98fb5797285ffd54b0e9a03f2175c78369b95a065f5e9b4fccf58c376bdf9b40820ff183739f2975c2eeda57453e7a96377ea544e4902ef1f757a218aa5eee70bbda2a317f48f8d16abaf7f00ff2e86fd41a089ccfb89267a434a16fc650423b21a809f82e29bc7e1870a316f96f9386c4a9411ea1a1f01f0637237bd97cf87e6bb8429cd26e39ab1b6dc4342c0fd92bc0c7c70e92b13656d55bb8af55779aa62899c01f495af08fd52a4559c6a87393562c7a986f4a5a2c07e94efcbef46157918bec28ef979ba3511ee72c09c02feaee05aa83f5f8c5f47a6e9be37f527dec82822d302f331fb86cde3d1a9fecd47882e91ba51fd49cf632d06f815e5672d957f0e9a61a4304a7d78fce743dced0a003e6a89600ef7fa206fbbcedd15c8818783870ba104c4c79fe42490414212d73e29a7874f6a1c2476662540fc58c22c5c6b31af6b481998459917a8393bd38705f8a57fae84d77d4593f64cdcb581ffc21d45fd595580fedeebf59c4afb8587d16d6e05a62ec9d3a86f8478807ed6e6c9fcadadef5777ef6f916c3e04846c87053260be191c4bfceb3fb583e26b57f76f9d777e7da9f83680bf317b59fd2eb7534ff56c2a9bbca50989da467f0de08792565cd575d7edd6fc264cde37caee68b673d603efd79180cee5d29c3b7989c00cc63628ddce528404a84fab6a500a6e186f153f4c4a1879e9c2fc5914c702f84ba7f728057b1f271d71b762a0babb082dcf501100fc99f920dff3614b377964226b67e66a8727d18b1e982f348fddbc4d15e452623e393e9f1132a9a97cf61bc04f4411c7c86d7abafa79ebab316228c3d87f53ee80fa7d6f5b0857397b2e5fc2c922e96433e1b4af8f10c01f518c482981a4cbe54f671a6777146690b5d31f81fe39143cb730553a81915b923c2612b29f689501f1c0ffd783ce19c488723096a56912142aa02de76c32017c7f3fd1978dfc495afbf85d0838d47773f60a6eda80fe76dc8ff4132141dcfc1b0a331f6effae5cce88a102f559621bdeecc7a6a1458fefb3128eec7da529dff9007df4b339539f237fbed5f16e1eca7440e447a20e28607e4bdb15dfdb93d96d8413bbb1b593da97e397ce3440df82d8cfa5f8f65aa57bc06bd834f469325666005cd2f8aa3e11e6c94a58ed70b4fd1d1e13d3b8024717f047158b24a9be0668bcb8a28543af7041ad0fdf8f03fc157b55f5a1f08a44eff0472e7fe5bd24c323e167801f4bc642620ed3aa09a143eb8e91a72dcf4f16fc02c4a7e483fc87b493298cd6e51f9fce8f4862488f5c9f01ff238adefd9f6b4479bae36b48086e0d9f8eda6400ffbd462ec6b44c1d2f5474560590223263e06e3202f541306b5e0977802a1bb90e1469fda6989333b22d60f0977836acd6ad79b16e186d37f963fbb1c14efa08c87f1ac1129f5e345ce3d32fa435cf282cd6fdd9db81f9f77b83591b74c872d78ca80822e8ce33471671e4003ff3f73c98ef9fdd9f0801b542b836a62c1aab2a5ae0ef53926c074ff322ae526752f71ed8120466903101fa692aaa9c46d56fee42afc4a0cc335e39f273943ec03f4351b8265ca2ba802950239e01396250800bf502fcfd6f731fd3155e9df38e86f5585762a2882dd972007ddc594a3fb65f5caac9cccdccf90e328f7c7a0bdc5a1a61d1f2a9367a1b76ce69ebc615e10bad2abb0cf0f731b499b87b3409f626feee46172566c9584c4700f7f3f3795a554d47f5a41c114486f772fa22c57a31e06f5940a70bf80af96f7eb39134ab11b600375ec01de0df46ad4c24179ce2f916bf06676c9d391ca98df902f115e129487e799b87c1275ac368b8aa23a33c9014a85ffcc3c7cfe75bdfff40a87030471a3357fa59dc0ad4bfa30e5e7dfe33354934f8a1fed9643dc1e46694003faf1948da11c27ebde53f54566662d8cdf0cfb704983f43a914f1bd2624ded87d80826bdfa9c07f31480a988f573d899d3be515c1862677a9a679f5ebb70d5c0ce0bf7cb320976262a55d90b4452d50b08a82f4c9d906f2c3d32ca9b58fa57c39fa31bc0b820a412b8bc404901fd7fa20b7e09913095fd16fbf52c76d561b09b303efafd412c39bbc42c05a224da9db199579905d2900e0a78d1c5554b4069a8c97082a590432dd8644b1e4007d05fd01414d00c6e4b8debcf51667fcc6197b7101e82f9a96ea5e9a8dcfc03062744db80826f4354f1c01fcd9becdd7efa72b5187ede08e201ffa18de7f701207f8b36d75fcf59c00a234f28ec2b3e066853fdf1a6e007edaab33df6e285c8ef43a2d182155fd94dd8aa817a84f987b085d33c8933ee9a0be09bbae649a3ef93a00f57b0adeccb7d487bf5fcf6861a2b1dbab83165fbe00f317cf7bae886d3bde20457d8ecb28a6cdbfbd21ca00f8663b2c1fe3f5237399376de9440dc6a9766e322c30ff8ebed3777fdc96b5c9c231edee188642c1445115e09f147eb77492b638d25e21b6b062feb82358969807f405af771643063f857306c3ba5a8c30157f18f87700f9f7fa0d067640c2cd5941032d997c2730a1366911e0d7e6ac7fbb8fd2d8f7b5c8d106e747e439c08bc900e74b722d7f4c6cc5f76d42ecaf0a3e36ace6d9d8a324109f38fb6ebf0ef71cc2d62f509e6c04c19e520501fd7792be7244124beb9feee50bfd8d907fbf15a33d00f513f506e91765832fb6d67038b4f9bd651f30fa4b007f2fa6f39df18e64367ab41acc39fd49b3bdb1af3e80af634ed2f6a16ef0d490a07b5427091cc213827b80ff3fd1376cf27d78523bfc4c85b9213236afb2fe03e0cf1fde798ffa80e1d01d19ae13fe5032d91cf0131de88fec373da75339cda5ae567c69a8d8fef410fbfd2bb0bfc1cfa48d322158fe96acf45d726bd4fadc587424e04f99a59bc7c5612958f1a5ac3b990d99aea2254605e6a3f28f042dd2af6f8a5945072ce673948cefc40d00fc6f6932dde34f4931dd9faecc4d8baa5bc641975cc07c236dac230c5a4cbae034e6354146031113d1e551703fd56f69df4e4d92ddc777e7ef12faca259b3bdd017cf5cc7c37dbb58965728bf38337170ff7670d921690bf27b65a4a030af93776cb963a282908358f50bd01fcd8db4686e414741d2bebbcc6efc5f64cb4ed5f00f09b2d7e370423c65d9a59e91f69ba04d1dfadc72102feca69f4f45d8e475b52b832d96693b12782efdaab407f8f4f09cee8e6ae8bb533b6005f856097b79a8512d0ff4f99f0232033ead6f233cf9bb8dbbcbcc3b60116901ffba38330dbc3f0314229fa99c48c839548a4f281fc7c6984cb4a81d9765ead5dbafa0d36db2581e007b03f6741a3935baf84a84d0ac96859d7f184e004031b98ff82479f3c277afaff63efadc3badab6bd71babba5a4a4bbbe7483200d8294744b77b7740908484ba9202125298d92d225a07477a7bfe79e73effbdbf39eb337c7cbd9efbdcf7dcf7a1eff61b8e677aeb5e61c738ccff88c31bc2707973f51fdf84999566bdb02ac0f6f49bf8b08ff459737738d612bdd1d81af50a0fd7f45bfdf667fdf66dfa9616475a556da575d1d329e9cb92f774b2957b7ff0a3ee6aadbe0bdf75d0f566437a8d3c2ad61309aa51888cf14c540d123af8a1265e59998a28bc730557d4b00fc9b639f9efb51fc6f58c9bf3e7ea978d1701edfd604e4279f62c047a4926a5dbdf5a0ca796d52a8cb2451018cefcf753e416291576d9bd549443b148d7e24f3ec23e01f760df57df92c603a3a8cde7e1a150bdbc7f629ee57debf2fba7b396fa81b6d6c04d94718e3d2e8673ecbebbfa23f373ca33772be0926add3517dc594bc6af15f8603e21b1530a7abf9a39b4f325467e37854105f9e551301f9edb87c733adb76f782dbcb9211bc200a7e1e7e8e003e62ed3617aee0717ffa0d69734364799001f65b67c0ffe69b189c61a535be9a167ef35d3e5d075a6f6002c07fa565cebb62f26b96b45e169ee5c41e5364db0b02fc8cf1dcef616f09cf20deb3c41db2bdef20525287c0fa5544547723558fe0fdceed9c314a4fd5d65d7704f8cf2bf59f879cbc1e1dcba80d7e1452fa5cc8f9750cc0af0ea9d503092cd0e408bfc8c35e99b8ca890771f5dc36fe39cc564030e5667e1dccd207493879a83fbe286e9143dff17ea01c9b909cf14f63ad99abb261814009c8c7f14d470a935beecff923a1b98d9fea2dd3bfa55d3cd4120cb5badd2dffc7b03e8b3e1bfdeca197b80e827fcb74d757b11d9e0171d54b6f0de76feb5456ad1ab7dc8f728b5cf5df5f32ec3014942b805fd14e6bd1471c6715ad4fdf4710fc8187362562ad0c05e59a7dfbfff9675d41544f8473d88c4c45905c906fa49fc5ebc538151515d5af3a7c5652e86c3bc6969444cc71cd2f3085167aa056ab4f615e859dd97bd0c30e9b713a688269bd9ed640da74bd28b59f185751bde0acfcd43eea2017c5a03e9657711b7e4da6e2759fd58ad474ec4777d1b9c582c55246526b61251ea618dbfd4d427331e8f10846a6a03a923f9ee06f96fb0336efdb962bf0a923454c6db3a70b0660e924af9626490cde3a092efce1dd706d2d4132aeeaa85d2b5299b04b21afb81c661cde22ae0d918dcd262ecc43ac03a5d66e93df32bf5b962b34d07d48d1b9e42353fb2c6f84f9f6e05e055e08f46b8f7f9fbfec2694dddf7d3afb5f7ee6ffb3c5f33f2cf009d11bf8a9b205eab3e4e805c94c1b6a242e41dfaa21a0c46e79268abbddffc72a020a0a0ae68e2aeeb6fb0dff58fcb0e53fdedfbffd96985cb60606cb55c628f1dcd1b5469d5a6836d2efab0f34e4588fbd0aa062c9395a5f4030a9947e1d5a87d4fdf3d43b6a67802cd648cf5f0a6d82d58d77cd988248ea4527f0930420cb9b64f4c9e22119d7eff92a5f4007405d8c4faf00c97edac63c11fc34b5fb38849a8bef7afda99fff6805c026a5cc93b8494a02fa0f4f7a8690e1294cd7a44c006371487aa942ca1353cefb64a9243e634dc21d720d181386aaa6e61bd96ff7272f18f42e34eea76cae940364d4c9cbfa03cdcd753853bf9dec7bac24c183e66c0098df12ccd64977d4ba421c70bc0f33d9be954b6905180b53e7a5cc062d297b62df33f0c84fd60452e9f0013241561a0d32ebaaed45e5019e01228b8a06ffd5150096597d6dd216ee261459b4ae22b84f792eecfec619202ba3ec22477ccd9d74f13eecf3f942ac778c2e1805042319b149e128141e53e2cacaa3618c9ff53d9efd09385b4d9ebe4e122959d42c27c1d426835627d28b064032cb8ace6ba940bd11ddc32e9689d31f6c16309f48816099c6627cd6a71c117dd5c2b19facbe395930688d80b13b6ff8b68ee4681987b1d29784c2b95b0c63f23b500cdfc859c087e1694dc78cf2c3399a914a33f77c39c05948aa6dded54528fc9a8ad6a86a044f6885832a0324e3da6a6270876fb2e496ba6a5322f984a67cf29c0182c510a3fe2082c103a71a2324698f53a2370edf8881933333121e279d236111ae6f67f954234a7883840c60d35c612c5a08423d6d23347d498a69aca585faf827b075d4bf9b997da841133e9c6437b71b385cc310f402c09009e5469937cd3b5aac1a121eaa57d48f8f2d29003065cdd11623c23e2decdc58674c8ab173a12db40b501d9b5557e422bb63320a35f26f34f41bc3659251816424388c07c39a0537300f4306668ed14ffdbe59694300b0469d607d3bfb3da761e5021f65a09d6439f33540c6bff2e1c1a08b8fceb7ee5dd238fa46e55eaf5b099079b34c8df0e3e24622e8e4e9a2dc8921faa11edccec0fd1fe29dca77a211a8beb39ff1c4ee40e59b5401c18a42943697175ae21a2e88349735cb8228fd22294031edb917d3cb7e8f3012db1378e57f9af0d84a628d01c1346205f193fda7557ced5a52849acdc9c1a5e873c0fecc3bc07e4ea518ed73880e89f07eab6e1c33c109384b1cb874a474f50cf1b543648e3e18d41b08e8c700d83628f8209adce471337bf8836ab9ce4a2dba07f4005804e75f7998450821b5550f575cb3c5cd330af7048ac5850fd3db67332fcaccc9d3a3feecd3f550aecb04f45bb003addb9bae72f9ccda41ed7a0dca43257d37800c1a4918c163469fa51a972a7c05f195966e6c4e00c8522e9ec4305f708ce46a5e5e11b4bb31ff2ca9dd00f67fc798be1f99df4d510fdf69c996c8817121cd7700ac6174e3dec6bae75298a2d6c7a7e7ccacb555d2051463db2c65298b1275f7a611989749a8dd5e44fcd408348bf5345ead7938fbe90371de8d1ba3653faa7bee4bc0dc78cd8119f49258a39ac4648f18d6227ae764d20e70e620945e498a5f595ef04e8a61a266fc8451922b04c8a6695086d07eba55cad473b4cde36abb18aa5dc8403463446df4e5d32f4b2f74bc1bf649f7b7cc6c8a4201b099d51922928da124f094986add8dd02497b1f8000816b869cfcd85fb4644d13e221a3a3ad62c3d4e3802c8b0087dd8d0757aefe94796ea77964fd0da0753df00c18ef05a71b899ed77ad57acc7175fde7da29cff100724030cd84ecec49787c48b33d4a604203a5b38798702c9326fcc951b5c385ee06461bf1208178477268c1205c002e9134a6fb517d5f8639b7a17abf8f3722a615c80775587d5c31a5bce7f4283e9176aa395144b77180e1463dddc5f55cbd91d61ce8fe449231f37587f18b301147397483af991444262b442bbd779c853274b33a40c907585e0db3dc7bef58cc49066d5fbe07d9bc8b8c004cea71a0949b377d42b90f71c7dc3fbe622316ae51ec0fa904fca08234bdaafd738ff0cf3bd926208baed0a30374d5fbd24cabf6a3e86fb8832b1477225567b6c0738123fa1f909aa2e529fff6c1c737e32f98eb6b0350c086628bf4c342827ff212055222b49de153b32b6bf057c1f5f5fe10d128b1c338750fb825a15eed234ec63804cde99b3e96a276da74cbef0fab07e23cd26d8840970d60b464aae21d87145a70fb63ca85cf8e709932144bf95bbfb5623d0a751d79f5a477a49623abd5371a901ed34ea090cd997bc474109be9322925c2c3bdf5601323a07966ee9ecd150e4eee235cda5e441d565153ce0cc872cce66aabc0fac876b84527daede6fbcfc4d09b06f74ed73609fbf666a105c9a197380df2daf407a0fec5f95a7757616fd591a1c464a861382b2b83059ea005819e545238386cb5f758a937652ff5ca5d1118a07d45f8c5df31532d09d21596a1f3d254b9e065fdf00c112d835e77a5f12be1d9a43f63c8cf2d97359a45740321427a36cf7b356d2b5b02668d3f00a8428931d31607edf69b43748856d62423607928d1f083099cbf701fa3dfd09719ddd7c2853336f33354d6a04e552cf1b2058f492e0e0fd600e3b6aaaa381af87456aea0a8b2f40d650c5b7127c2420fe5025914f3eb1e064e85ae710f87e58a926aaa42567f866d5334fe5c632c3f53b5e01f65501feb78577b2eec65aebc9731eb08d24e72a4b00d8a53aeb3b5689ce2b86b64833e9937c79142a5e00041bb01f85701722330c6c5b0c6d624742cf137d540182892eaf3211276c4f0eefb9e9a6d1d107210c502402643991f2366f05be5669ba4e42426607eaf9c2631db0582c527ca06f442a1a4d11bd77b1f51ae1248c08703e635bcf5932701b575e7f3825938cfe4469648b0038f242394bcb2fd83cc6d830bec9be522312b5ec0591033fc53aa6a2a30fc6b3349f23b5a61731c80844816050c5bba1cff0bdf2aa5425160fb51fd1764f7c3f01c0e2d7b544380d868493e999559619c472696607f78060f9ccf965f50bc7982ca50fc81c5c2d5b6c0446b240312af6e46fdb9f75fb2e266cac2b4660b77e18bb4b02f6fb616cd623eb87eb289f74ebfcb2128478c82e4881644c22a3a3a4f2a3e89286b6059f7117a96efec5f9089873388f3058ece8184d47d3604c09b160f2a9e23a38d37c858cd5bb795dd086bf82720406895e6ad0e7f07be979f8235168b335a39989fce7ff73551e903fde9e96f8783aabae114b61e28f812966a55b7e32e016b97838ae1d349e6a9847d3c27a1a7da55f06a11d9fd6c2d96b78db496de8d78b8d14e4f2d72f9fdc37da2f59080aafa84d27192e28a449247674a43773e2645fbbb90d3781fa93813fe8bb211db7ddffc7e2dbdf3fcc6da8020585817073bb4d9e0afbcf43c4f19c40d4c4109a8b8622cda716817b2a471feadbee886adc22a706de0fccae27fac42b019eeb101f7891d9d22474788de4bbacaf7f607d8a6f8b87fb4c3f172437acad59801a8e0f92af5451bfe12ddc901fa9e6b409cbda531f52f53c729ddc350d0fb21d3ebdb4c776b475b4fa44643a153bfc88908043ef8eeb4f5cf61dfc84de77cd23d6480b5f1bb6387f3a15ecfa6e562a389b8bd32d83e198018527d4c78a8fd93cdbcd206e2b1f9dccd4dd70c688ba4bc91e2fbddf0b36573320bd6d01c29ca904fc45e7509c07d74a21eb83d40b1c8eb99a6397ac93012bc95cd7d723abb2f64aa57f381e5b8f480bcc4838345a73b3d2a845763086ae69304ffa70c05f7f07e52d5b3027ccc0dffe4ebba94455be1a6b18eeab7e47afa6c987c5d837e17ff83b884b05b70c79efebc0c3edc76f43f14c63daf7d3b403901d1067ff6844f884a6b6969e80602c34ef90ebea1c1198d1701855419b1fa96250c130237921a82855b5fff618ea0121a89b6d50cf51bf8f8b40d54abd83cf00c8186212b6192fe43135d2586143897d31a22b5ae0bcfee867f178172d5a60cee138028259ba7a82856997ebe068de27ab4cde51f3c0df0d2f840eb89be680f9f1c772d8db347f3e9a5cdfa73ffe2f3367bf27a18d8d6389b1e66280fa132f731b46ad3bdc1e7265ef61f65f7dbedfbb7efea7eb57e5bf70997e67ac06163e2ab349863a1fee5151a607a4e784567d30e5e7f35f19d0c7d821f7505aba42267e0a3b1ab1626affca3ef79f39bf30a8e92d9eb2b8b36882a63df130975e61d75755ff8df3a39148b6b1bccbf76f39eb0d0866c228aeedf84e7eb0ff5b89e089a5140dbdc23ddf7dc4d52d4d5b88d8cf010002bd924c92ac64649396cbc0d43d1029ee78ca6e0ac4f3a7abe35dcd8d82c6e3eddb9b271977567cc8512d2b35f3f78d2bd48ce1b3f22e7977d7e1e3d4939d48c727d3a6aecfddc562ae826e332d657c76924787f868a4fa5b98619ac50c2df1496ea82f7c642fc7578ea379642fffa2cb09160d83e94646fe5697636363e33ca6ecb1ed501b8ee09008c8390b61fcfdbde3dad93caef803aae5af410231fb3f2148201fddf4cc52254b6c4311615567a8b76e842603a8d85e828887183d4e5d48661b832edfcb976e9d020580ac6d3921163ca34191a3b9c37bbb53a474b379cc0048d3dc9cf21581d86af3aafe07fef7fcb41798fb2500a326fdf124f57078361a218141e34d399cb1963e2ef07c17ea3faa68e13e25d571a11b9e381e2554d322be0619219998b993b5f0b5057189161f1e8d4c5a8501204f7a8ac1c638075a022413b622685914dfb31c0a18ff007b4fb191cc55e7fea3b37b1f62d3cf985cca81f793c05a5d8b430d27bb37f8f3e28103e5888fc33b00e437f47090b22fd25eff162a4bb48a767815129a0764e4d83f27945f092abef0513bc56e8b62e570ae5a039cd4a761c179a1d8aa3f4bc9ee0d6b4347c9f9fb98031571b2c7fb45329bbfc432b9462f424be3d20f79e8038c0b4b0b76e17b04cbbbbc1c946ab044aeec3cd05a40c525de280f1de19b5743728e4d8cc29fded860bd83001d813d12286926fcaee06eb0dbce5cc5057196a84a004fcb83c38f2d181ef5dd8ea026b3be41f86c74601c00e2219394c545ceefe3cacc95b8b63eb45bb55c41029cd87018b9b68985768da9d6fcccb21b9543f581874045b246273d7f31a9530b9eeb07188d2efb29de613a00636a6f56ffadd45aa7576ddcfbbce918f7a6e3b11e8091d8178082c0f6560ae724db34307fa3c133a3cb0aa8a85dce590d63813421bc918563786ce8294cbbff12c8785e13aabaced473e5e8c63f50c3e6628f38e64d032a029e96140b15957b67627c583864d8dfc9edb97f0280843afc395322d30e7e0fb43f6accf6f1606c7871008c97f568186a8c872bd6267992d18d3e997afe949f818ce9e524da84450ecc351e024f962024844586023d607defb828bdafbf2144b938911c7f37279c54466a0930aef06d1fd5d66b5ec9f56a957e7a548053def69116c8786b7ec58e344a9f88f41003a372af6f900553cf0d0089cdf63ed271519839efbe9e8f7eb3309c2d173100643c5df559d8d455d31216592e8b1390297befd57400fa0516bb216882b37b3f8b98ed98fc24a741865503a8e84705d3bd72441aae5c58f8aa809f5ffa710f11020002b513abc895be9486a5298f0fdd93d63dc3611802188328d7a4ea3c9b5e8a9c1d5913cc35e7c751a2eb40102aa0d43cfc071c49e2cdd6bd8aed88da7c74f10f80bd395937e5e44e10842f60beb289b33c707071fe0d086268a40c4854e21f644920925193f5436b4df37c042a7eb866bd96a3d020fd8a443686d45b80aceff61215a8c82e5161be9d2d198418719d28547e0af31c455c10e808c25a322486d26e9b44357940a4da87ef5a6fb50c646c7fe73efada4cc4304f11827ac08787ee0a9bc707fcbe35eeac6c144b71a26739059473e6178a7e454a80b1647ff9ecd384dc7b9a998f99cd15b1513d540e39c0fa95ecc5862aafeedd1d66b869fc86fbfadc035d1c08527cf3d69a910848b74fa3a9e2b749cef289129804ec6d83ea778ff73b0625685579c97d5dbdb76bb022818cb7b866ace469a6683751c8f5ccc1bc7327fb640950713810378e5110c7bb162fa8a8413ed696b9952f1fd07f07092898619cf78989c9de673dcf97be5142890182d04e294dc5d8822d1841fa5e45575435b930fb6f01902bd7e3fb773e8d727aa82dd489837752922b2c5dc0fe54dd4e568f6f2911c8e5b84ee50ff0e148efaa0718939eb583f79a105f120557713039ea4a172e04e20219230b58828ecaf5e4290b0130d710686a5a579766c09d6bce88d8cd5cbf886ff8f88c39e9a70d59d6650910e4e3b65a7e7588971043fd95707149b6ffc590530890b15dd0113efcb6d1dc248fafa8317b2f41243e581f4002b60a2352d9e9e35c84a5b86b053ff57cbab4140740ba025d1caaa832f46255bf323ff2056c292f476520485e562b21f912835d19eb92123dd0f266daac9b0ba8684128beb54174c2ee6dd6afd81f27262a3da8100580e4767e4996a79a823338c2b1636e7d34cf1ff13401ebd73c013d8e277c7fe8b13b69c266cf475ce3543023603784daf6041963322ca3fb78eeebcbce11de36e0fe20e588600cbcc04347b1723dbc47acc8ab17ea40467cdbf93796efb554b5dfe9cfa729449318a8b679005b1526ccdf0eeabda1b7ba8b1dfffba90d421f8133a0a203e6968b6443e5189db1107b666bf8eb990dbd30a0e2f5b2440f8917c6c3bcf298b5056f3deb984f4a42c0fa0c4c9a55229d626afb2c527fff93ff7dfddcaf9f81f52dd7bdebf54c8a5258228a6957e51b072445a117c8381852717adc98909a1af8938ffa8060e21eebea11a0df2629e1fd4ba22edf3dabca5570cf992439ce37058218eb3b2f5718bc046d1f16a8df8fed86bb864b960518e7b16d266444b215958b8b6fdbd4f8f35d589f0d0041a285eb05731cc457a3bd568c040289dd8ea2c5be4045c49079ded3e6bd2c4a5184304f8297ce12d35c4b00a31cae0763b21642918607596d22a1daa43d1381072a528be26fac6719a4a0da3590d1b72a7ea6fa101100740cdc3642b3f82247b9e3d9ad7edc2d96f21e01d502a898c2f90c1e010eab2989e591a1fda725c3a30e950020e3b47cbdb0e7c012b1cef0754d7bffd64b6d75e2d780bfed1266f6ca6a45ad3112bfbc0755c2c010cf590708c2c3bed2696c2d1941c7ee67a5791238ba67e2b6056444ef7d3caa499194794f1813a719e2b544e933250b20c1f226704144099373177d8c489d87417d1f6c9900c678785344eb531b1d525fe50016d550cf593d447b008f782596ab7f266839b2d4df86906b8d2394e2e80c0409dfc9b2310e8c593f4eecab720b52bea41b27b001f49354fb7b3bd67c0625b99ba70894d8d7a8f7aed901c6bb76fd9187c0f1cd49eaf3c1cc981546690e546e607da5702d9d8da53fb00d5fddcae9a6ff8412428b05f88a0aae5e89f771c8aac99060cfe7232b93c8edaf81206a9175d4c263463ff914ac2449d6edf0fd9406522048d63f278df56aadafa3e8b06200eac92bdf88d7794090714b963da516499b7768b07273d3e175d9fe1781a8aebf3833017ff56a20903fc1ab615169bfe0668566e5e22c9a59bf9f973aa75c05f479d0639e7ba6b3b367163ffea0106a7f723d86a05f1878eb430a906f288de4ec756f8e5873221d65dc6a00abe283b904f6bb62c7edf5a76e887b6e58d5c1b59280d5af11321d6e5723bd3b96ee6cb8c57c24f2a3d81fc07f79da9158d723aeaba90bb6b05b87d5a4efd37000bfcfafa2cf1dd719222fb223386e60f8750b4bf40b802011d8e8915b4363e08868d1f1357fd52f7fd25f0820383f5091b249adc9346790e5436962dea4162ec203f3d7288b1b2a9bb9ca732b5d2cb9148dea938609044253ef61d8d4d6ab040dde1bb11dd6cec9cfb7a77601a17fb9c170b5346ca962ed540705aafb4cc48ae6ac409f8bd688ee5da631b75a4be84797e79721a3566e7e401dfe65f4d982a9dd265ad5c9e6eb3aa367b4af67e7c13a44b630ce374e7a513b924fda36d1d3184d492f81f851137e734b65f30feeb765d32957b668dfbfc85c023c6be1cdd525c2d3cb2ec28244bd9c0eccac4e2a7700e5dbf47bad2ec4cc5d87598c6011d731b37fc4350de461c233dd57a1cb3de988ea2225924aa4b9f254d002a837654f13cd0d82b028cd87dd250b51382d5dc98e80d0b6d0a8933d45a529ca0af5c327d396172cdf18be0208c94ab289b0dc9c058d7c72f4b08e4827b903871790c7db2c4873f966e77290b0f10412159c36459c3c06f4d921a142d4fd7cba12e9bb9311889fef24a040620ee451edccc260fcf05fe63af7adba7e85e88826653a0a9c2aba236116cbd9d6d18e93f033abe2ab9d643dad80571769f286ba76e43ce8426a0dba1fa7a675971705f03ade40e908343e0e4ec856745cfdae2b08e5670c05e4c1cc6fcf13d40f2ee5e49f43da3ee292adbf74eb00709b4611d3e016286a2d7c062a2ca2adb4b91f2f2201ad9679a65c3d63ca91668a1d08295551fb7836830850db3a5760f69e78ad57e9b6e491bbca13b13f2df703f208de3a89648896711d5229b028696bcdf29b61720256c11ae55ed5bb2b3f95c22f14d3ea4fd5435f655202a7f6e0230e6dbdb7b819e484a96c1b0fa97ada036c01ea8d47af98822f1ac2d6c5375c5c0ae6a5509c590d801aa19254596dff63661a2593b7e3cba327c1a9ccc8401e9272360b397e122b5432675ee3d52303a176811be0d40cab547ecd2ebc5bd761da6f18167f22f12ce60d702a3e74960e516e2116a1e9f8862aa198ecbbcdb30f206cfd907783e9aafd698f903f671377bebdb6e37f0f583549a3cf6297f729e74e1eb47fe0b5fea2339ec205ecdf51923ceb57440f69592e73886de44413bfa344035a7d9b10869db42c063387f56069bf4e88523d3f14a8d312205a025f49ea83188a687a72512f96f7d06d1aa0967409f8e4c908ed768a3afc409bbe27f740c99e19f09a021625943fb7a513a17f984fa16cc73a54c6e006bcda082be2386fd78e81332d5142f7af3e4f5d79a8d47f2b27f5d86e4b6b781a9286a9f7815cc4843552a31fa036065b0d8f7160156226f2dc27f91498a0ea4dc00078dd6a1bddb6d9f9b925116fa8097b577908e6dc5f01753a9db021fe922e4682c77a443f9b420e60ecf9d500aba1e55bec4753da71bff55c9d16b6adae82ebfba1408093e43e5242c1f8d3f01ba77b4bb4fe8c5fc2f228803c1e3e55fa001a5617065a58799b1f26e902a36c6540c44726fec967ca9d0f5e26fb4f65d84c54b25999e0813ca26044ac40831d06cc0b652d3f6b15b1cc7cf526e0fdbc4c218379f88e345846f185fe3c73c363affa3da04e03f1390ed78d415939a1e887c7dd96fc4c9ab0df00ead3ca3dd896c5e31aaabcab5a12e81bad17c873c380d59fc4c6f24cca062d759f23d8eec68fead17a7d18804a75783ea9cb5cbe78f89a7686f2a9415f7af1b630707eb784dd43ee99be897eb3ba736275f5bc409ed40e38bf4fd0092825151ee8882b8e5591bdf2e38beacd01bcd6a9d64bacb1a865aa167359aeb9c8ae32976016005542930db73154ba37dacbf4e35dd785c36298562840ddb087ad66465a969c67fb185dec70611b446182035047315efb7416b00d0a7d0916ea88cdbc90a92b8002ea7876974ceb050f04c9dd246aaf4630d02ee046f200fa8fd8bb3cb9bf4be1f9cfd5dae8558d081dcf8c09607f0fcb9e567aaec64b0f430d41c75675d3c0de2705f2e06865595fedcf900a0e1b0b98896f0b9f05e8a1817d80a4ba88f715f6043638de0a0de67d993e497401a895b302d45707727d3848cbc25106825a9f316c62c13ceb2115c2984b9ff2ef6972cf3edd3f2fade66b04f43bb7a16c74f01357cd339d38640cf3d2dae88a75a00f64faab8f825c73c9d69ed1ce315398727a36c8b6409d2ca6826a837b9d3d2486d56f092d03960ba7ce30006a5467152982929e0f3dad833d0db582948582421d50a7cba79ed9db887fc5871ad5a33446b79580a50c19c893dca554996fd4a1eef72c165154afda9a52d0790458bd3424be24334413a7915bc62eb47438a7886102006a15197b9ed7dc89dcb1914c60c0e9914ea4dfed0a9c2f8c468b5983e5e8956a2f116733eda7ee3bb19800fc92a7a5b13c13254e44dd2a39a4bcbb7a708c95acfdbf953394c6d64fed2586b5b062cac76008e6f3475b01a8af165afd41aec34336b4de23090d7e812dc49b4ca08f921cc298ba171a9918b2e2441127cfb558fd6b5d60fdf290f23857a66a29e1ba6c8fbf66ff32b95a740da0ee92811f10a5a4e1316162a68686ceca399e7e2e00bceeb5dc4e9365441fa1e29b65e90c28cf72ea8f8e80d729dc7a3ad9fbce50debc9ee1d14cc0764965cd37805a35684c35929e18accae97b7fb4f9753eae1d9437605fb1ad7ba1ad3fd78d13fec13676a86142562bb007a00eee645a138ed3ec6ee581aa1a3ffcb2ef37735901df17df984ecd512b40a5705223fbc1c39dbc924d39a04e777cf148587b337ce31722e7a76fd451588795d9813e7eb4b60cc9e953ed2311942b072a5e71bba52f9500797f8fa411f42ae90f75221c25f48d55ce3c674fa08efe3e31bd5993feb61aa5c5498fffb9d8c413a8feb47398c4302ef35174cb077ac1a4ec057530213c899a0a77633ddd962ec7987fcbfd30a8770b0ddf962b73d75c1eb1ff326be8df5ff5dd484b771b1fe66723c51d9fffb6093249fceddf10a0506bfdf5dc141af1cecdd3c3837d5ecba1f510b61320fefd399e230e0504a3b17f0da6256eaa43ec082ad1ecbda3a77dc735010bf0295412b10527dec25c6d6753a9a5792f3abdd431bf77c77776cb9e8285b376c3019ea1faf2a63e55d65c79283fb32d5765aa7a884199f16edb065aec8ecff03ffc82b985f3020d272e7f08748e9eb13f527c9ac8dce45ecdaa89530fcf30634c0b44faffe105fd7f2e3aacbbac23731bc8d7df9369e77b4809a2ff31a111daf44de49ff882c5f7149d9acbac55cbfaddc97730054617ccbf50ea2339d6f188eaca9ab83db408e44f28eae6240a48268f7f15896c7cb0004fe3c2d060feb0acde4a6da36e2e309af49f34914032d92736f03efdd34727ad6982cf39a696968ffeb53ffff7edcfdbf6df6dfbf7b6fdf9abebc8dc0632f7874f600431f83d99c6c30a58ce2555ef3f737fb2371bceb1dbcebef270cee2c5b460ef79929140e0fea6c6117ac0b64ed951944ec865217910b95b73729ddd2d2b000539960669d767e4d14b5baa9d276ac21d0feec2a66d8139873b0d95e255d02d87fa0fd6681ddcd21075d5e4dd8e66b8bfd99a2972da67039d8fc4d3204e6cd2b98a038d5e78a393d905a35ce2650447eae910c51b58ca7c146e167df855fcf23cf767bf3ff65fa70b20f6466af8eb76a4a92b160c8e37c54b9a025b63a377b33c6ea572de628ec1dc426287fb9fbeef61ffe4f191e4ecaccd4c5ccc4cff8bf7f35839983b0bb0b1a9da6acb486859d8a9ab39d9caab4b2a72a9b89b7bba7aba3fb4b5b676f7e055739230b53772763697349170e47ef6cf9bff5d93206e3b366e4b829060f73033378770989ab1b31b73f29af071428c4d398d4cb83938b9b88db9cd20fc3cc6267c3c4646dcdca6c6fcbcc6fcc6fcbc3cdcc6a67cfcc6bca6a6dcc6e610ae3b3d3f82999bad0007fb9fa716dbe37f86589d9c43711e19341a143079f27c655b20e8f1280bb6fbe2b538bed144c9f61d737911c34c279cf12d8bfc00c9fd8446d50109c67a1efea88fc17191dd24b7aabe3f64cab34acdbb9c8e89c2e00409f461b710638f45ebfeaeb783a05e8a4afd8307b705e61c5335201879c13d987a04b10e53ea9fe022dda6286e399ee16e191ff19675860ae871d83a07348c84349331a22e3986f0a9fee53c4cc47f9970ff72b17ed5c5fad575646e03f9dda43a34b8747ffb965b3e21dbe0873fd3847b8ac8dde517331573733d2383b6dfaf21f945e573be28952671a3c8344ecd19a2d2031861788ba7586d1c0fad02fbdf7e94806fc6e9a77c4cfae8b852ee59cd90db3fcbc51aa147821665d17be426a67c153ba99b43295b92f3affdf9affdf9abfbf357d791b90de4dbefc9d07fd454beb9cdca83f01efc99fb135ffec48f7dcac2f4c3071a549772c4b209f21f816cc263cfe52349cdc9875fea522447dbc6694de3c83e892c8f65854fca7a4cdb5cc43ecee7e3ff9c75dcf09926c93f692211095831714668fbcdafac7872c9c33a528d63d6ffb53fffb53f7f757ffeea3a32b781ccffdee429ecfc110d38e2ffd88af48ee7ff33f7e7e915db19c1c0900d16d384d0936416c176fad39156f35264dab51cec72a9c0763846a3d9b31e02fad55a0a02a973f9959810bae73608f13b9ee87350bb1093f27fd6f9f984a447adaaafb797f41031e2fa44a837f5e466e85ffbf35f10e5af4294bfba8ecc6d207f9c1acc226bfd7b22949d6fd2621135f7feccfd29749349b7f27cd3e7a0f91ebc735bb9ace62bf4735e2b95d2483c7969e6675de7622f9c1ac35e200737ac2550bdac6180e32260fe5c0fc32f51a45da53ab6e546477a2788722c1c5d6e650dfe7c10ea2f49e621d8f37950012d306361f0367da6090e50bf493dffcb9fb1a568b6d35bfee6cf7cc28a04bd867ff367688ece770983ffe9cf672a01c1e4217ff977f7fc7ff86cfbca7fde90d03fdf1db52918f6d6b69cc9070493ade5ffed68fb9c4d754ff82cfa9d0eb4f4b8a2124453446a097f6f7244519a273dbceb68bf058231e5928319b4d1fe2420d892d6b409634a29201da191acf0d852c821b96e7a33dcee5401f1d41b09ad6ac5795ea76d7ad3a30ec7e6d96074de84f3af00c1d2c9f6fb4e6da793f66b9ece0e556fc5aabdd824ff0504ff8f06821174cc9cacecedfecbf7ffff40f0134d05090e1708970397bc393f9faa1697b3a284a2b8ad24971984c35a53c74ac2d35942c6dacdcec382d7f27f1510cc6706e13032353333e687f098b2f3191b9bf21a9b40ccf8f939cc79b8f939b8b88cf8f821bc3c1c9c269c9c46662626fcbce646a69cdc5ca6fc5c7c1c7cbcffc3816009d65e11f167ac0ad56afc58481a3f0eee35f23cc4ada1ac092ce40df240fa3ea3b8e3e9a7ad9ff571cb0cbdce29ddeee63e54e20cc95a4f94bf2b8eca1642fff35b81e0bf668f08e0fd09d9236bfb4db2ce227b3223a391793e2adc1da33631404de54dd834da3e7d7879e6d5d757d4cf2c08779b7c00439e71dcc32c6c7f7eff5b1d4f9afbac4397dba501905d31a91e5943e18735e935ad8f44d9e43dffbcb803c8ee784eae68ace0386a6529c1a791f58883fbac3406e822ec57f9be0ab728c5494e68b56fb57b5817169e1fe09931640d04bea314ce8fe9d2fa3a6416c86f17430d14066d77bab994179262b61891fa78f8485c73ebf53d20d67adca4dbec9b8726f6f8117429fcb1d5cae4ec3ec09e2eca8ba5798eeb2f023d69a9f380fa9030a9351728cca6698228fc886dd62c5efc2df61ab93cf2b86d0ed0a5f17948a6b014354eab0aaf95eec328afe89df92980fdadf7c26b9aee8a0f4d6bb3704f834ebf3ac68e07080c465e9d895ace7a2c1c6cd2e1940507f73ce0c001baa8e0755e7eb2747b27949698d9a8a10ba979a75806e4b42962349c4d10dcdc24eb1f984f4e976dd09e760207520395c87c913422af6b89f941d68367d65619f100bb72686ce34152b519e6cfd88165dc6002c50ff97240f6cf6732cdcb18280735fe0a898367515debe248ce40613b75d9c71a6d55ab09e9ca4d81586fa2e37c5bad01762aa12afabc4184f5d562759bbffe204ddca05f229033bd9ef9cebb834928b3fd98f85a0d935f977efd05b03e67ae0a36bb86047ee215be2ed999f36c8af48905ba5caeaf5636750e069a7f4839f775a79dad4b5310006a56fbd74be2c6a08867699ca3b7ddd0ba24956f0b005ddea4cd9e08684127374ab31253a1e937ad2b65f1a801e1891a734df8ec471336b07279bcdb1232eb2773c0f88821c76a789923efdbe561254c3249d85d49ca812eb216cf4aa366c78be9dec3a7489fa8aeb1d3745f00858bdb58c9188d3339ebcd1259686a6864cab745db80e7470f0b5dd46ff08eb5979685ceffc6afb3cda80178cafe698c0b593588d734cabeb47e9875b58e44be4076c140c8e27a1ad59ba43c891759419ff52a3ea4eb038e3816bbf87d5b591fbbdd551b846876a970fe19816ec0c2493df9b076bacf1459cf4558a2779450b57b093812eb7e706778c6b1638f1929728e7a6b9e25d7bf04d8ade62f389f17d5a946adf2bc3a9868eda83b88ba0f74996051357061703945813d7e28f37dbae545819232901d6021e8133d8aaed7c190bbb67c48f98cf63b6e16b0fe3a56b6a4bcf6b2a60b1b1f440ffae957d660c802d5cfb8d2f866b46d50f1e55f37d591a92c8505721502d9336ea1fa074363b31c0d1216cae8edec3a5897a1407da27c019823612cb133f887b939475331acf09f2580f1a55f6d7b6039d93512e4cc3d209f459f3863a004d8bfaf559d2213d98d91d26af10af31e508eb2324280c294c545cd9f0b1b39cdcd8e3f30f26cab956e2da901f5795e62113662f3b8d2c409b0983616754ac74b33bb03fbb340279e4bafc4bf97ee2b6b4623fc5b2acf10a0ad054eba7e221a7b2d3c344da2792fc721d1ab530980466bc3c9553771e5c48ed33336ca88da3d6926670764ffa83df2fb94c8eee071c204434018f821626c130b70f46a5167ec87b19fa4d292c9ecbf8e31e4b7897a03b0ef0942247946c5b80caca0f247cc14d694be7ff904c85f5410be3f2126a3ba2879b994925a42f379cb17282c2b251ff39e5755fda70cf997a22bd203d6a783ab40174f4ebf12b7c52436abc304c5c8e7ef76df7cd9b90714366e2b74f7e3c608bd5859dac15462688729a3b004b83623e93110f716b317d6bacc619f0e2f8c14fa5b81ec451fad0831cbeb33a699e0eeaaab78ae3794c95282bf954f99fb71bdc5d5e1a8b6115ef63117f94cf9b318e8c2cc5636d2449c58f86e23be8d416fa29c4503d20954f589ecf1328a57d1bedfe5a291dedc5aa8a321a407b0d3bf2f05aaa4b02b7e43e8e1b5b7efc2afcfb8cf0b144efd095d2f095dd8c81561f0de0ce27f3d67996e049cff9e6b19cf2d371f0ffbe3ae7f2d1a65e5dcc55e00da924c7a2ccd0abf92cf19ec6563ff2ad2b888e75b0c6407c01a136b20f0e87cecf53dc60dad38aacf4930d4fead5c7f31d41ca747f75ba5adf250c7d95bb9e52515a04be1d7b0b9d8d59f9eb621681c853132d35adc7ed240f623437fa87a46cc21f3f7c4b06b9f20e6fd98561da0264f04df60c39e7cacb5272d2583d9d18755be9e71a0f0bca326d6543f034a083496764057255181de7914f56fe51f24ca4df885da957de25faea921ef42e4717a81ec3291b228b661a4d4fba52742b527703602ab1831b6bf95abe6cc2e5be29d2d867d7c617b6eb35db6f60c01c8d997f7cbfa2839481da87889633e37a67bbdbf560f9c6fd72653ba06e7d08e08a422b9b9f7eb7a9dbbfc007ff0394bdf86ad82281259315db3de841a3ee71b4a80ebc51f4bffd2af3b269b417c7b22ae3a287c88d26ae6b7720ffaaf3eeda329c76a71349a29dcb6261f1d8e0700f90f0b6698969a6967e58509d764cd17f8e1b340f691517b2659e0f306f96f61509ab5cf3a90ace92e802ea1a61981b97c6e9966a754b01e62339e4acdbef6404ebad4b0a706fd202f7a6483ce2a357b7461900a29b03e082094c96d0829ba9d36f2c39e2c7a4ce5175ee2bf954f303e333377885230d6ef511dbcdc29312b0c0068e2467826aaed7988325cbcd07d055f14da0f87a980f58bd2afbf4b313a579d5fcdac7bc1be8ec938e2037c9f8437e86f8def23aea5544e90bf937beacd85a80bcc5f69fa312bc5b3b435e9ec0964fbeaedd4a9c0321540ced8c53b8c14aa2eb5c56884eb6a4659f47317f056a41abf9ca2717889b56ef2add6c76e669aef6f1aff563e36170dad918af67251b48781e4fdc210ea7c2d50789752713b75644ec23a10ba714f9979c33d5daa13e00899e34d95a4096bfd587a248d6ec78bb8d4f18c10a879b0effc9dbfbfba43afdf793b1bafe507d6debb15e07c77183d7d6fd6ecf2928e9ba2dd355154a5381a8ffeb7f2583c0be2ddfba2d1a6565215db6cd52664342740cd1579b523fdb67b9f84eec7dc7fd390715f957b55cd0ed41fd988cfadacc40406b9b0f3156daf1c63e785feca6be9500e8e3930fa5b6c2873daee70ea5e7763f8f3314de45779a6701c6b2b3f6fb95a60cee10fffcf9875f02b064f346e2b230860b2bf5a060fbabb3de08e6e31aebd83999db28999919d8c999994a79d91ad95c92f789d60a5c8dbe6ffab78f11f3d9f9d36bf740cfcb75be241b716910561b1c187cdbd581a75b3eae4e786fa71a8e86dcf0fef880a75c48b137302aee4bdf2e1e191775f66e13f330e6014f047238f04de2d360276e8baeb37b86d7e28f48f2f9bd2454eec09479d4256cf5ed5a924b2ff59dfd0e155137a80009f0ad42f6093bbeaf94d18047dddd8160ef9a4f22325d80f9b94ef18f7f9b3a1ad3ffbba35eeb35cee03f01262357e5a8b4adbbc9f503f2d778ae55077c94bc5b8cb3bf8cf6be057d7d03f90d6c5fa77d73de2e0df5feeb4ffc4b75bf5ef0cc950dd6072aea2bf2dea1ba5619e75422c5bb9a859151d45b7a1aae8783df247e361cba105b40c84c1505618cf06408590f59b4105f43e678ddf1e840a669cf30c8039873b0b4340a1212217830abe6f3ef66fffeae09664d87326159dd1871195af89bdab04b4a31c7f16ffc4f721881edac651a9ac6e884acf21bedb59017e62e650aa9aee1e84e4643873258fb8145162d3a4983fbc5de723e496f16f4bcdc4b476b57530736235f330b134b2b330fbd52fc5c8eec1fe0f5e7fbe1eb8ad2ce92f952dfd3bd72fd912ffbe9c6eb97edcedf961e811b7a981a4ea1a81521b1e5ede71db7dc1887aa9075fe785b1807e1a3b7fe7fa9567bc6dfc5f8a50fe23ef48e723ef1d8f92dbce8aeedff97b3114942f1414b198f8df4b5f1d5c442fbf2500a72311bc63d2ba1cc029ee1c2857a1f0c2741aef82420c3ffbca7618f2f786fcbbcb9362bca9d9e3ef7d945f58cc8c494378bfaba0ced4c260e07fd6222289fe35a26ba7217adba6007022af3e652f9db68067656d5e9b2b894ed64cc3fd5ae7d06aa13a87585e6250ff36661db494860a43e11d8351b759e5d0e2928fe594950cd435545414b40d94c41fcb694a1b3c56969756fabf6271009af6360a3fd1d0e7108b07780ba72f6a74f8178657ad0794fec062fdebab84398787fc4798fc2f65fced47ebe069fe0961f29cdbc2e455434d6d0a99485b84535f63b5e8656074df245d52b42514785e434ed1a4ecaa7f254cfe480cda416b17a19641c16a61e85d57bf0a74f9e6bfc2e4ffa3c3e4488a662e468a46cecfee1c26d7d0b455e7703795e77868ffcc560ba2a3c465efcaa5e5e26065e9ce612d2963afc3e32ac5ff585bed993cafdbffaa30b9b939afb1093f3b8f392704c2cbc7cbc363cecb6e64c6c565c26e6a6e6aca6fc26fccc76fc269c2c50ee1e433e6e6e0e6e03782f0189bf171f1f27340203c1cffad6172e0fdf556a26264f5c8ea32122f99e31c8c9a2fcaa2dfe2c7a02ddd1666270c68f3f75967b52f908c3a142a32b015416c121de1203a3125cc366bdde29b17fb562cfb0075efd22de17530464646368be8226efabd25d9908db6ec15e3ca9edbf3ad7ec330fa0fd5f97f8761447bfa5c641bfdc5bb8a2106528ffc03a545e2c4203d82b27d5e0303668c8a79a95f519d02a958513af3b8dd03f6124211953e0af816ef3dfea53aff1f519d4f8c3d6d5d8d55c5dd9fd8a98a736a4aba99dbbb3b73f2f23b79886b703aaad96b38e888abd83aa93de49772f95fa53a4db9b978b820dce67cec5cfc9c3c665c9c5c9c2626bc4666a6fcdc7cbca65ca69ca6105e230e764e1333763308bf89b10984dd989f17c2c9c1cdc56f626c6efedfaa3a6fbbc4c5f01b3cf95f5e0b3b530fd1580ed77358b24d28caabf75b1e5b20733ac7271ef29a7dff62a7ac47e43c6b125b51cee2283fe0ec08f7c58a0b792754183f8e2de91fe827a51880188c81635b2bb5b59b007d4758ebb2a53f943726041b27e0af20ca99424008ea1541e6df42349871ddc773bd95b30e947091cbd00911f723d9ff581b3c5d9a849908451658bd789b16029dcbd512025dfee24908b4eaf2594b5f28cce30fdcc893c14c18c50183a106fe90caf8bfb816217f71c3fea65d5f77d67f43bbbe235359189bca1e4cdefdd2424c5bb9a1bed3a6bff17c7fb15d9ff21d7d22f1eba0bcd7c268ba233791541b33a9136b1341236a23e3d0158e974c5732e5506ddacb1b78bbde19a3ae8d0be1f85570e9190958f803dddf0412a8bf381b139d3cfe7fbc5d9ff2dd8e4f280a754a5ba337e230d3cab0eafe383a428e9c7697b5859431b63fd413d97c32a812ee885cfc77b7ebbb6d7d8affdca4c94e24797b9faa4f146696e46b7c28d68b2fe5c5c4ef55cbbf6e3f86ce27857e216226e5883258a73834a78b8071554c73d258584b222bdcd3b890e4def0e68ed935e2e381b3a882b41dd54b8d0d4191d211231b618a02f9d8dd978427c75376d6b06a0fc318e97a42821b725f623d85ffbce267dff749eda3fea0c445da0fbbd9ee96905bd5eb5f099c5c987f0281f38df176763b1507e15b86cc831ac6864f9a1a5f01b4f9e762245be716be3ce92e51ccc9077d5413197b8020d9ad6d89ca99ea541ac748ce437e46b3dd301805143d370c718879ff68946c2c7166a4f33a61ebbcd90a684aa1a65e24e8e19713c561be9d8ce6186383a4b509dc9f4277cda0961af32cd36b38f51e8cb28cf5f547603ff44c2a96b585bc96f45eac5392aab8d7f0e107684e7238ba2548618e30c670e7f276f67e1631155c02b28d8e513c7c5ee01e9306cc8651956033b44e6a42039d5b556f1ed1c716f39e1b1b98bc7b0057819f8be60964dac0249912d47bddbc6af98e9caeff52a18161e61e50fe75c06d245455ea30d284f6301825eb3b8b5eb608d0548419cb758ce2d15624f6ee9cd1779da78787751500462364e974e1d393f75d5052d563d050eab37c4b1c407038e8dac89d0f9cafec368cccf77e3c756daac0061088ae84cd261b779eea196d942304a0b51f58f18d01e59515d142fb24ded442d3c5ee5ee677aaccb307e003fa027e922abc00ae72d83d8beb2217ad005f991e1e20a80667ed1812bed6142af1afec634259975e0965039a4690ecf9c4413c73e12e09ec0899b842d307d50480f368e6d933cfa245cc33e1d58876398e47159d571b005a4d7d2fbd38ff8849900dbd7c4184e9ade8072d16a0a9915decc5c2440857f3c70878bf52a3f6bd6c030580c0c3ff7e837822d6f5b5ba78c7099df9bc5bc8040f60b996af86777eaac011f4247f0fe11e3b5d929bc4068a2d0da8b5d4fdec3754603b140b4071b262dc8b1e0398016e629bfe546dfc5f1a86b1e144ef09b9409a36008226bf244a8b27e1a754d355aa3a93b38924c72334607f9e61ba1da4d29c44882f91b86a29b94fa1049601049f000faad1979a2d48dc41d9fd41db2d3d3e2f6181a643643e8ae4ed76d1b357b0a439dc3f0e9f1e0f8f01e5ed735f3f08c0a85ae8fb798e302fe133a3e9a77f0510802ba37408d53bb2a78c21e7872daa1034763b12a0a90b7f2aec3a2103852795e4fab5017a0ed296bf1048900c8ec21ef8d8d98263ab984f2ed4b91fae130534fdf93037a01330a38745889fbf6e2137bfd5d55d0720f96916974479d0e759ef4f65b53366cbe01d84ed01fd90a839f036624eecc90473053ab7ed836e833063405d96493a3df99261d54e88facc745c835c97b65c122856f7912906a53a9836c8b9d290b99fe2a209030901284f8a9f9196883bfde1fc0761b6fc1cb534bbaf8902b07f5a2c2eee496b3c6f4c7e594c1c1eff963b54a400688ae1972262e6b20ee9eb8c484ebf5a4f4da3083c03f44b87ed3ba61d0f2692980d973294f705cc0612ef80a65eeefb9a188732df3293df9ba6609bab65bdcf878dfaaddcb949f284f0404234e2c8361a8f4f99456ac52ff4b7728cf3f18fe504589bdc99314b529f3327cfbee001041d15e69fad35029a59b855a839d05dad1b36e55280fe5a7f069f5db4ed5a1cee8498fa6ee919b7ddf516d0148c23a9ae0133365f9d79babcbafe19665931ba1d50deded1a21c53697a847dc602ad14f5791e5afd67328060d8955fa3443a355284b9423df6ae94cf6738ad13c0a87d8798eb74a8d032ccce4cb5a3a6b9d570e1c580f2be047b123dedb1a86ecd02f58ead71b834b26f0e4f7f2b97d353287c6b9cb7e8947093412dd93af5e02800802be2f7f4386af1481b4b25719fa38f29c8d70d1e0204618b088ce59876ea0dcaacf1719ff702d551d5ca006be1495dacd213f14dace0e09874a49a6e3507faaf40d33e7f4ad740e1b3a80bf313bdae808e7c6cc7ec67c0fba96023e0e6cfb27cb4bef8424492214b576dca0f282f3f901b4c0c45c0c1c2ac4ff9c5604dafb7bc9613f070f65dfa764f3f555d1ccd44de93542ebf4c86b4029138ecc7ca7ac38e704a32d74efd022129f9fc22171580bc5f288267b141b1aebe160517078bbbcc500820589bc5b893fb3b9e3888df87b06836fb1d79212901fbf3d367d688b4f99e32c1a9f24d767b63a752c543407f344970ebb369b1d4f2b5260b1dec375c2be2ba0309166f8a3e7d4e871be8f7fef0d8bc2299f7d31e7311505e399a1e77f49b6afa42947b43ae64f431bbb5cf4fc0fe78445dfd6005e67573bec4b06d0f14ed9c139f1240908b5c2b4528fc5605fda940ecda32d73dba849e1cb037bb78ef9d2a8dcad394fbad2c98867d9e246a1801ce2f64dcc28d5ac4d43eeb920f1d196e6e0ad6bc69008171e78976ab4ae895e5e6def59e8f41094e72cf7d803a05a3247c148ba9be4ec39899db9f550041601c00f0b451e9466b7d533c1eb73d6f38ab88a4deae685e80809c6dbca4a5517fffe5bca2889af5fb951c44cc368000fd3a6ece5e3b80d13ce085ee7562f902918a781c600f3f318a76932690a1d9474f9c353ea9595d3e5e05ec8b59d769ff17adaa23ebae128f56e6e29c3c089a80f2e2568ca173c9bced4dd29f56e1e9f452d7a2f69280f38750abb6dc4285557be2a4d2d3efcb821c010604185fc8aa34814a4e1bad5aac5cc632ddff2d745e0cd814119f9e9f5af1c3a864f9ab12078e7e417f6142a0291b9f8749b10622d6677fdba230f607a9c65897ad40fb93525dcbbd5c912fc42470f473f91c58c7a6ed0ec0fcb99e3ee1b4155cff61ae4d2bd8ea39af114dc10b346d445ce7909951cc35ad45f0fc4098d1879753d60d34d57284bbcfa1e12c7ebdd0cd4b3cc5490b993a3704ced7024746872b9e19e7abcf9ef5ca7b981a6c2aed00015e02df80d8d4121ba980b3978ad59f9524e928a307b09fbf96aedc848de65fd1178e1395c97cee9850039a8aad9219b7497c969fe02978fb7c8afed4b5332b11682f7260c3307526875f5caa2d1beb18ec09418d4305f6cf31beff4124c40c69edfe316d7b40f327b32f0636bf958f47c6e4776b3b4eef8d1d7cd49d10ebea74d803de6fe0698106efb9b0108fe00e4ed12af5c31a8d4b80a0cc4f86f456f80ddad60b68b3d8c575f6c19fec6e807d1c9b1dc13124f5dea91b9a48e52bbc1ef17b6b0a20c129681a8678124bd991316d16e55ef3186d14c1ab8e3fd19dc1909e1ce936136db5860da6f160db24592fcb02cc191942de8dadc006223ec44fbb84f8851a5bbd94c071136157b80ba5b563942d25f3d972d09c2723170630e72654e3eb5ca065d0f4f64b48e1b5238eaa39b4806e3c5fc73ede78f1e83062ec7ca13b32dbe8eaab4106fa2d772453e361566439910de4ed23b5ae1508784c00ee0edcb229cde1444fd70a14dcebb360fbf2834026e0f9c3ce9478a21d8b6416517788f68d7d3011266780c232546517c468509ad314f0ec28afa8e562578672811e9a7cfc2c07032a9def76d5767cbdc3a838683752806e2ad8abde92698f0503866929f7e7aae4a6f7ba2881edd69cf9c3fc541169e31a064def92398351a13316c02d498b42722d8d92bdb3d09f7f2f33fb2a0b9ded01f0b5d3d104e16934b4f3f7c37c96eb33b57a2620bd0089d05b98e635e15047bcf2789e1c49307aee0ca77cc16fe522d0f23a44aaca70afea36125418691f7ac607007ce594c703aa2936b2a1ac1f8eba4325d1717e18cf01a8788f76a0ff73c49a26a82c21524914e8d00d982f83bf95d70d07c508d189178b37b0045ac268085c1f220180e25eac7092b995a4087c3a341fd76cc361eb3d19c0dd646c5df68e2e23cf9ab57da0022931db0a6df502882fdfeb5e8457356064364c6f147f8f76e0737d1d031cc712297e356cde4a9c3d87eb3c84dd8859ebd22f80e3500af553430c65a3324a67a5d9c5594a64940929a02e6ef2d12ee44ecc1f4c7056b63c6ddf8f4f9f1e007a2ca276f2dff011419f66c1f2e53186c2448affac07ccd14d5d93e7210f6ab713a5ab284d0ee3213933e98039f382218b6ede0029d953dedf8e235d929501b610c8f7391db53b526156cc58f6ad961cee814ac0f05005be5facfab2381fe71389401ff8524adf05ea97cb1e40e107c4d6f1f2a026fec7b51679db63fd78f0ea71a848bf9517b9b28c1e92dea4f716e90764d66230470d9203e4358461cf2b86e66d7157df6993aed074d2e4124da0dbd4b40d191fcb08baa1d0de8bc430430938a89b696c50db30b596c4f3dfd43096f261cd845405ba5702f92672b6339cd24c8931cb97e85f73b3d96d9ff4db03dd3644b5e10c194bef993e44ed819951c8404c750e02d6076b9892c903ba9863b8d9539b2986f394e96431a0367674b65d5b0cdb8b31e39a09f101b82c62c7a20719001cb1279c9f3e2dd1f03ae9d5792cbe12d7165e2ed06394089d094a6da9e168ad643f8e8c85d5b39773a618d83f7d89c6645589a47d09c53bd6217aee15bb84e8bf95670d90db38b8de571348c70bc4fbeeaf12b5dd00c00d6f7e2a577ffcb6c734420efdc5c4d5d46066de18304737095a874353ddf0c3b1ccbe359db7bdf934a105e8af6b679a08353b629d831cc84828d5c50f84efe4403ec32b35f175e6b20b91ad16317349a3872763137e803931e131e5bbad39f51c75e7b3c1b860dc0fc90d59408e3ee0754a63d98a98eb6d60aaf0cdc28efee01e107d95747216b774b3fb713e7744d2bfb69385825e0ce463d94d29e8438bf2d69a665e13f30f3ae8a23b2703fa7d64b4ef5e0b06a19e6f4f3264438d8be7ac9210e82676e6bf9e572b73d4451d8afb688f2e0e39dda500e8b13ad960d755fee96bbcd8ab95f9d224d22712dd18807efee8b640eb3ac5a4ed829d40c2c3b1129793900df0a0cc3c67f0b10b91a10cb199bbc5e9bedc906a6501fa29a039c717ee29d6c8d0ca565e3acc162f81f022dd6fe50a5167181a979ee93f8adf9b3e56b8f726c5e9334094b79d57cd8bf8a95da8f871c22d8b935302ee1b0b90af1b2f50843630aa15d6f103b95e330ff9a095ce0470872ed02fbe4f24a5aa4af1e407a4706a4f23f7968d03fbbb01233da1d8ea99a83cd721a9e83825bdc50e900f8bb741193010802d81a4492fa6688b571d8ad4400098ebb67882ab6f614e584709d431081d1bbcbc3e01ee6e7ec41bd7b4b4b546dd17f328c6d89c2f8a1cd701fd88b25394bf57ccc73f4cfec4bf21803968e4a90de08eb651e1ab0ee3c036a31195708456982f31cd5300e64e529f8a6950626d4123aac83aa988f8f9e72579a047729818c972c5b87868fa7398455d3cc7599be35d403f3d4586cbf0a13cb63eb0e4b316917de268b85401e85f9b4943c6bad288a10015af9c356a789c07304e803b8e8a4754a1841be437ce556f12f905f188fe412f90cf2b900d0f739fbc51f2c8bd8faee141e9411e9e3c607f883f3687775f78bcc37bf3b89a516c1b6e2a3101c827871542b113fd5a18fe54571deebbc5d8fa34ae10d06353868ab9e41b931b1facef557e16fb99ba50da14e0ae3c2be60b84a25d8c33ea461f27c1147516778407e04c862e0c83e8b9fa7b84ed8b2f339f363e958aa704f4532f33c1a3802b470c61cde8b7a3ac3d3951fb0b009c12d31e86e06265dee09b8f1fcb69d3f05585fe35c0c94eafc55a371bcacb18ce7d6bb2b9d162734d2f08e86f9e77e36531b3b4aa711bd9495dd298efbeb32203ee54d9b7ff8fbde7006b2a5b3a84de8b34692202d2252181a0d27b07910e4a20a1f7264524127a11900e52549a62a5481114010129824813a5a8205d440569c2ffedee7bbb645dc8b2c1ffeddbe7f9d6f58b73cfb9f79e3b3367fa8c9e956a242b3b0eccd7a6939aac770a61c730c73c0c196cf840167a04efab82b2cde0a70f538c361881a320c64382f463b6af51a26babbc25c6d63a469318e6463d87039c37e7fb7a29cfc7379faaf66d3f1ed48261ceb290884e7dc2e6ae609c007997e4f564b0d4f302c6f3c5b1d746313ea1cb6531f0117beb9238a82c4f84c1ff5d06bfaede0830de7870e3e37ecdc5c7cbf3637730f85f3f9909db6cc879a1cce375e3eb1502f0c7e7aa30e44fe168e9bcb5509f4cbc5b026c9a53d70e4f93716374c36ab579a117136b76ce21f9eafeafc2ae3441ac6f30e8b78bf605e77ea5020130b0d17feaa6e58b53e6ce18e666f6068e7911125d68faf0a9a9f637e5ee945cd618f994f736a3517ce4308f2022b13a73d9b54364373330e2445b9146eb0414ac1bf7bccc852f35461e70f0afc3e00ff5fb4c086d965925e5786fcc919e17cc1eafa5b0c550e797ae91f76d2450f21b406a5fdca65bd3b8bc89a11f5c18a0b1affccca9f9f0daeb55edf3836373880318f99c0fea1cab46898db240ae808553453debe00f1715fe73713fbe514f0c0b923ff80b1516b1bd657ed67a950e9e1c014915f56beb904b9bdaa966d3b7713f84a9bace4c8f8042731946b0fe4addd722e9dd177fc4fdfc8fc4fde8c355c012b68636270c4d8cec3ded208a1eba8e9a67dcf41c8c35348d0c35dcb424dc14957c54bc14157d34fe51713f300988040826098742c56c10e2109095350c690db311878821a1564804481222662d0a03db4045456110099804142e29262e66230e8640ac454170d1bf77dccf550a8615c1fbb2f7e9cd028fcee5bfaf7a47da9aedd351c73c64232fdf4221cfe2e6b87432479eaba4e2e0f5aca463a39feb65e44e28674c313ad8522ea0c42b02ff5c89f97ff13cdceac8097c12913f9e0dadac03ae50e8a1d0945562e8c3e19255147f8312f3f858027488b0e02129c6f3f58fc9f208b6119e6bb3b81a0f86dda2b6ac64b1c1914eb03c1f50f1f72538775ffe112b29cb02fed1030f1b2ff92634adf57eb596bd2b79a40739b3a4c4014bba1105fb6dd3f8f8af70b32c09e146eb705d59d290b39af3bef4b057731dcee57c9eefcc6fb1f602d70e11bbc665e48e1d970de23fee1fbb69175cc5fc3094d22df19c2268605f8a63bc54c227afcec8d8bd2a315bb289ff50ae0c3ec0d5da2481c83673bd2ef350ec077efff3f01beffd354ff1d097f87c41c767579e3757b95f771adf6ef15c037bcaac4adcf0bb14465ec7aaf82097bd2ffed47c6b0374913de6e5f8e34b51e39d4ba492de71ea073b1e9991a4fb1c4c61021b3a29e6d03d4e62356f7a102270c5b4378235dba66daf4a9c9f2964191ebfb0ea5dcfa51409523a22222f43b8f203bfff79f82dea7c6bd4ac2a7d2364e9a313255233c0e3823dc57693797c5f422fe7eae284df45cc7eecb1c7d495e8ac2f2b7e8eedc0bbcdfeba32c19341ac2c91b651c618ff39494e8c5319aa972b9893686c1db5da37d555c0ac9f4731dcc046057270aab2da2bfe3d7c8737cdf989b421aa5fd9f53d397d388f5a94dc0ffcfee7e137557fd49b5c14a57572673688d1047cec63b090e1f7944ff24406e378d33f6a9a843ea565e47a0c9a281ca860ad3951ca5ee3d32bc658d77f56eac0a30c637b56f865d3fbdde3f932163390ce5b0f5c1e340d673b01eea1cee1d60570f90e0a8da7b186d6d490adcc732e667fbc1b949dbfd6ead8a6eb8997b262d1dd5dc161a26f666682657e0b6e2423ad5ab75c87425341ef57561d42c967cd8caecf47115c0d8ebc10756443f3b1abbdab8ea50af4512748f2928b3bbb895549e92897e9f33f9d032e9bd68088e20c1f11601330b3b798227998bcaa23f947e9df94c7a4c343348f71253bee2729d4a6921b55b367343b6b012a9fe4e07d404d2dc7398e60718a475e689d587ac84b8c1f8b5aee7e5c488c1f3bb4522c330dec0debcb2311a803a0f999d27ffa1382a737c150075c214f40a1e9cabea285d0f855e4211dcf4a2c7053d0b045b06353d048b0d547f022a3ff235d968efe1c6fd6e447e8464b3e632c29b33edfa7cc61332116fa3fda722e1282aefcd3c79b3d82f9ba9e728a921de53cfd7580f2c0926acb72199e095ef7a96cf97c3932d0c58d39d5de363211558afedd188270e3cf78281c0d7d28dcec60f828dcf47702ac192ad76a35446e1bcfe6d2a47f71b899afe59dcae39217153fe2a2701c18f2362395564554d27ca972c98269c9c72be2cdeb4c429ddb322f5547cf9403cd90e15281f2b86608a8b62edfd3b7aa2a83092a8f7d8a5582ccc840f34ebdbe27385f8c0c887eaf922077bf434a7fc69fb332d423210229bc7ec37df968bc48dee6053f3ccb52f6c0051c8557b979c8419b3a5f51984bfdcc47640deae3b1c1fd4edc8195ca32ba699ac4da63a301a3c976babc7403afa4c947733e36e4e3391511b592668fb97f6d9e7ffd61ca1ad7f727f143c49acedcc09be22b8b149c6b7d71cc07fe0a8a0a0bec1f97a6513e513d77c2d3dc468fc8d6e94c9c62f2d2a57455afa1f26c8ee3a5e6abf26278e100e7295cef8f4ef14f8800127732c95e46f397efff605a4ba773ead69a230d15a3e0fb8cecfb4c4f1712babf8e4988451f177c37fb80fd81f2cbd75527133a9aa564a88d6f3008e27aff443102e60d18cdf009c93cd54f1afb535afbf893b86b8bd9f73bd6e69f7b1aa47f2cfe9e7e6c9aab23cfc8fcfbe0f834c10c81543d77d8a95259e7b3707a233e015cbfff04cc4265f1eb7000914ef89b02e8606eaa4254378ddc73aab30956a2a1d611af44c94a483e78e838527774087fbabeaa924b438936b91e50c2f2b2cd449993df00d7fbcbf3df60d44a3913f4c542e8ac463685f38116ff7899f534d60de99cfe03f8a73c447507120da29ef6dc233ea6925b207f9cf902c408dffc32ac65488aed44b1b00b8efb8f5197849b85065fecfdaac83a2cbb45bc60139ef3a8092341fab028f435350d227dfec6dbd097b9eed089f60b1825750d3e5f7ed864d06930073e7d0876797852c75b0b2304cbe8890360653380a7cd7c05b17039e141e2c14a0c69209c6279c8e2d285bbb2525f5d1c54f43ff731e563441c26e3b988df2c1b6560823e7d9e2e1094afcd588bc1df93d55f79a93242caba7d1b4d428f693b3df29fc470d1a54d754778c124a71c508405e56c67c81f9e89c0100e336c10b2213451acf5472f75e8f6f549eb93c6dec380c7ac76eac994f6becd3139d1627921cd4caa691e03fef870fcbeb98d664d737e9f75344bd7092f20c6f979a917e0c2bae96b71d3d87e7e804548fea4791646c462e131b14b32b70e3f18395b219c052027598f4cc108317a7865297ae5a6e5abe36b6cf3049e5415ab3cf4dc5be1cdeaad957557651743bb48da4eb023ba0de4dc3122ba3be98620fd2681449b69cb168249e817bd5f5e60b8289f51a06514085f131e45d478c618e09fff7c9e002362f33303c7cc2aeba5f0a18b2ddc4799ef56cfc730efdb0a5fbc1924113c5910b07925d54efb70674ebb2f2b4608c2d2947449043276fccd54f04a96a04434577216c36ef0f3d77144f15c2647d4aba2916d6619b0da9ee5ddf7f898df36b2fff148253998cb1383c46de6eb5356b02c0f926fd46e03d77b273238489ab3b89df4b43ffdbe860e448072bbf942241f9a2dd9df486fd7d967182de50fd263f4a9df462a017a8d8a40dbe7ada99bfe585ad7d67713fbbcf420b87d9bf797d4707dae57231adfbdcdfaf8b9a274fc3335e5db3560319171a666f196889bdea677d8637e4a4b3fbc4741f9c57f0c4f306e15b94117685bbdcdfac7226f5458979e992ed9067e568d137dab9a37f4da3670cd57f7ca45e61539b6732081ea4edf12ffa0ac57b60d9c52fef1dcdaf4054ba19f6b5710d2a27e4d6b2624f039151c84056931d04241021009a47dd4ecd390d043dff915363fc44c8ba3d50043a87710b829c2396f555eeea22d780a22a2ff3e285707006072bff40de964ccfd39a9119bd2f03b8b0190d71413dcb3b3c60d94c56cd04ed382a9641c1c6ac6221473e2261463b518e44ddd32d7c7b1339a2c8e5617cebdc4111bbb1a97694f590dadfc33e667746eb730dc73c4b1a81b36cbd5b75615041d58092d870726cee69cff784fd9f92b23e34ead41f10408bf5b5fd0f22cc0c122fb85b9a33607366232e566ad1479d97f176baf1db59bfdb67112d2c78a55b61ac13282d71ac5fc7b6eec3fa102ec6056862d3e4c1f5c3c9590d14df0f4b55bf123aaf1fe75e2026dd2b74a31595fceba68dce293528b2d1db18ea63a3107da85a029bf29364a78ab2b2d80e9dd63caa9cdcfb6c6139ac0637d96b3958169a9644cb9b30f0f4c1c647cc8eed39520cd3533917d7bbf8e1dc7c3b7356678024fcc33e170bf14366c56a3153cdf703ceb6a97b065d15f237bf010e1ad894fb1e165d65bab17c2d7be4a8a7129af01a521b7a39b51135de8c85ea99115dea77ae318292011c6ba514ed3ceaf840ac3626f98a4f44a25bfc3d1208235f281d814a4efa02f01fbcb027d80de2633a1cd97dc05023536ea4861ceabeb4c5c97eff64f8add0e422cb08c9d35d3a57b53368a82c9717e563b02897533f43dd8edcdd052ea1359fb24369e431c7b4591e711f81dee31e6de805f83aa5075c015fcfedf475ae137e21e6985ff0ddc3ace5ae629932a7b75c086de34d14130fea3d25ef1ba63ac5147faa33e150a37bc3c2231f5b68a119a1a1cd67c8e9773073ef66da455faf8a61e6f5370124decf2ca91c483b9fd0559da381a60be73a41570f46f6efefeef89b432f581b8ea59a96a9d7056f28219aaaa6ba91b185bbb9d70f7f3345136f1f6f143a8fb231cc4f4e48dce48b8ff7df6578ece3eb37aea7910ef7ac4d5eb13f27475c64c0ccfc3e591336dc4a21d6807032382b25a3e37537f5b048c2c2cd1ded9a58584ad256bdaa82eaa54fb4ec5a2eea13f5141e8df914499ddb8357cfcd7170baa03ae506f8924a2de8b4822422cf05cdc0c9d4458e40d522c748e595ac05cf12a1432305134d277a1baeaed5c469eb3d8776e96ffc39387fbd8bd274ff8a2f79d2cb6f7b68744bf0851bf1f276352db2fbfdde49385b1b7d4dee0c60b646c9cd17749139d32cba88c135c10c4b24c771283f91894fd7cdb0d94af7304c25ae603d68da634541be75a92175ab99e7cd6fffcfc6e90bf87db92f9a11ab7aebd8ac428edbcecd2fc6ebee3ea9533c3dd01f9e78b5e644efdc0ef7f1e7e0bc49d4bd792f561c9aeca89ea7e681c377058ad757bb5f8c63867184e5b283727d9c1e3115f177de8d4fbcd6248e7cd7d2607735e9bca529656913e21d2945e669222ba94d9f95efbfcb9ac8ee50f819b26ca2b80683f958656ead5cd67035ad57b85df2f14e52a5e9f23208d083dda6c663ad75afce1f9c20ffcfee7e1775cf18c78f2eb2eaefc3c932bde15ef593a671473b69bbc1791181a3e5d7e26323c32ba0664b91feb085f0a6b48c3220bfa32475634e3594ca23215e3dee7b5b6463a73ca293f7fc53e60764436dbecea48a5f686649b2411e10b8abd8a343a9f13afdba157a86d3a5eb5626d6ab23296c399f603bfff79f8ad1cb9284325cc24e35d53654c7d28d43ff7fcf4b675b73b78d33e0d9ef980137ebb2132921b7c5d7b845822a89ef7f348d8aa33ddbb3ddf5cf546582c71c5a1e099153eb19ffcddb09737c0973e65342fd2aaf317bb5dccd0bcf79c488e5c69d6a76caff0bbab593f512f3f629513117d0ead7b128da06dfcf203bfff79f87d30a267e8ae5c6e6c4ebd4fd9072b2149a9638e74db4faff65015eec209bf439c04fc1ec8f8149cc87ffa12e6684bf0fa167188fc50932e5f6c583114af6204e434550a60bdacf4a6ca50695fe386edd869fbab45eac1ba3d8a80ae9895f66adc22e95688f450680a70129a171e5245b4174a351047e31dc6fa99ed4b6489fd2caf8e3305bf3a5c071a708cdc4cfaff26ba20731fcd1afa159bcc08f4d9cb6a14ad4c0d8cc43f886e6f892ec7eb457f9a2b1361fe39d16261f0d53c9dd07d58bcecc4b8f07539abe197d3c99ad387f3f7bd4913be6c37171360afcf949c40f2f46551bd49c62541f176f974ef894bb2a985b3fd9c3c4332f2f5941fdf6538749a9396a38b94d347f6ea5039109b5f5cf2ac31ed7209c1fe5eb9ae6141b1fc941ff8fdcfc3ef186a7c16aedc93c3e6a1f7d7fca8ef2b7d82d329edb8c20d0b015cf0bb415e7e286c9d8a5b23c27145868b5feebea28adaac5a9667ae827a519c48a0cfe1ebcf06824079fc41b5ba4f5a3a6011ade40475e494e51d2b5a019f8a1d792fffa30f958ceb4ff9c7a65a7bfa1d280ba5b5e72575ba8a9ffc90e4fe7944f7b257aeb28d6c115c709d40d1a2ecb5adef99e06dc32cf8f54493012dceb8b408957bab674535ec3ca1dee19b7a763237eaa2f48980690aa716ad28425237492462cd089d6a584555ce973b77a17a72bf6a1ca5b53c30204ad3960729e8b60237b9b257874aabefeae520cf4eb80425286703f9dacc36af5ff6077efff3f03b6a455b6eb46b5aa62f09a01673b04b393945775bfcd5575485ce1ae36669123be750f144ee2bf903235af38bc059d33a1932e527a387d88b499bdad7a9c7990f8260a4e447bc9ff53694ca549f0365b3851b875e79ab4553bcd1344d32474cc986dba142bae55021dd8b43051f37a4c7c782904418f75f2c37790c27f76450b621bc6010d697dd5bbe34fa8328ff7944392886d48a264ae332c99b9c2e64e40c1db2ecd92e2214403a3958727c1137f3810832e06476b42cdecbf1db5fa801774a5e462f7a68af89049b0a586a397604fa065d38d75807a33cf40ea19d1f6ccaa07d9b202cd14095fe8270f5dd88333d3ed47be5de703d7bd48c67bf1ce082549b502f5ca6cdea6d4eec0f4de69f87df6c32067ab1de9a7a792f1b7d732b46dfd0ca1048eeb842a1ce3c2ef88d5f4859320931b276ffd0cfffc56a042f2661c95a2bd1bd28c6042902e5e730778b31304bcade9c8c0b8bd02910a3fe7ac8bcb47a1fea3d19cfbb4d7761176e93bd12aa06ebc70114d7e265c63694d3da64393401ce7cb77ff0ef7f1e7e031f12dd6a1a89984ed25265cb73e7b7bb3a93d2fe3d85aab5972a93d27d6594a2bc1089a1993b53450f867ab4e88ad5f9f18ba744bb38a1003e6b37afc5c6a7aa317807ce290746cdbccc9ef0f289c4bf1250ad806a8fbf8193a6de8a42d3509f0af95a9e2b0d5cd6478590cfd40342c947faa401df3667e38aca37b7477e48af0ab33ff548499af336ffc71d099f1ae578b50eb842c012ce60d55536e43f164274e10ceae7ff55111054894ae0d0a80b2f9e11dd6d8f9b78866d60ab13852dfa6b8741db7b828671154b8bd31d1ac9d15a1d2b07a663b9c9412cd16d7818d163d99e5904e5acb50e8c9d7ea4e4e70e68546ba47763998f457c056211bfb146b9628bd6a7c79137d0effe9df774fedf7dc8e1e1e10100a7486ec4bbbafe0e940e2c3f66458beb0dea568073a81022eadb4455c0b1b392cf447133f2614338bc6b000080e697bffffde7d7f1d3bf6350147d56c5edbe248fc32af816a6b36ec4edf79617e8a612f0b502a57bfce90c2948a5de868dde4ad3914bf748ed31163c81e46410fd00c09b79f208ddb3da38ae1b5b58b2e12442e0453df1d038f21518c096edd5246fa949ed73043cc55148c17020d078bfd9d4a0ddd1c29c2c52184fb26aa88b4bc8d17721b7591f24a72489d7d1e8f768b3551f7cb028372ab184e41ceb5768bec6e445f2e9a1b5a75cdcb849568214de83e947de67a98ce498266f397431b8a3094e02725f85f7b355f7b17df95c52707d53e7c9a6dde9d346ba73179887724812590181c6f584fe0794330d93132ae255ec2bf958a95f7d9563841ce12aed486c565b141aff105a86669b75a60cb084f53b86123c9fd01acb5220059c6fa18f62385afbecfe315e325db5230d712c1789bad88e13c1245dbc3933ba56098ded8ef1391ca5ab859a310e79b25df21ebf942ff7e26094da809d46b0a496d0673ddb061bc8b5c70e395ab4b915595a3d35ac24dd24bac40edde5ba03a7c24263cf25b59aacd10a99c9ab1dc45fdefc8b675a2cfe4f1f00000060a4d8790f16cc48a8361c3b5dea8e400ebe40468f09fca498906dbde611cdc4ea35435473ba44fb8dabcf5a2be205df9cda0a9f2944bb4c29bc88d5e9376f6a1f6c0a10ae5c53d8f6316a2acd5affb8c7f2721b0acd45eb5bd9489234874d7093adcee6cba15c56f19733250aaa1b6cea947d0f7d2aa7b71660e0f9728acbfe910196f97aff3a5ef1bb01006f8c9ea6bc83c67c918bd9d7a7060f101d1ba5a77821eda0030078e760bf0663ac009d5068b60b3155400477a863036eb48b35050608a0c089d7e0e840c0d6d5155396d94d7c3b73b4e152abf8d4f66f879f6d13e7a96885e5f9710d0517fc8318637900a0e58f8a9ffc95fdc7d8df40add78f83e8dff8664a58f914a00b45f48adc91d8de1fb84228f1ef64216a7959349b73461521f71e9465fe66e75a142f70e3b7c8b164a6e62b075de5b5c895ccbae240678ea61e2b85570c959ae43f7fbb2c384580c87124a9491c783ab98372f14db250c2ac5d5b2e1b67bd8a966b05f9d1377963049a777043dc1f6599bff3fad40aaef62e56704f24a711dcc909b9eb46e9bf250bc14f22bdb441be500f5bb08aae9bb5a73bd20ee92f09b5021b3afb6b211d209edeca503738c2d4cdc14b6b0f55601cade47b5096190e16b31613b78683a108840458120a124382c00898244414640d855a212048180c22612d09028b414148b80d14048259498a2324c5c5c14810540ca7f7c7b52c33c6fe95e9707c56e92c5c3f14d777e15d29282dd770ae70e7e91463d8c46cad04c0b9a659cf47d1f4cfacbc4f773d5ffb9adba2bd7fa8ede2c8f259df69bb774003210ea9a4eb40a4856923d9705efc88bc50d9ca35addecbdaab1569f7b39cb097755ec1a345fd96f14a6016e8158ce347c776dcc882241030a404140417434a5a8124ac6c405071a4350c02978042a1a2a250b0b58d185c544c140e41c0c1566010142981044b826162e26248513808db71272b0ab316b5918443ade11030020e41c04411080844dc0a02b5b24188436056602b1b711b711b18120497808b5a43ac21703002060641ad61927004fc970c35b2a2236830f0e9b7196a228aafbdbef4ca00e9828fb6d3d6b1d0f6c698b56d621975c015e099b07dc375545f06d064eed968902279151071234577004712c45578c1a2a9e321224008c3df9dfc3b9efabb37b4e37dde71efbe124760815fda7eed5fb67ce7db87e5eee599002b6f88f3625af80444e7eebb85ee1c5ccb8d22dbe97a1b27c18338dc8e80ee2055d4ce24fedf561151ad0ed817be0f914a3a320d43d3c657a239816121787aef24eb80cb77c381f537edbec603fe0da8ccb6191dcc0570a0001c750076a0258003efa7ffc88712c787dae79329526b5329018055f5871d21efc97e067202d8f1b8a139fb80007271dfb02304fc5486e6ad220bd6d53317effbf851fc6b018e5c00c7e8cf2ba541486fe0df2b1fe9391d69e0c7c3724b82fcf2f39f56213ff42cbf8531ea5c55cc0497fe3ad4ffe4fb99b619c0513c00f3bf1e1180a7f7ae6a4bbd950304b02a42824a7ec636dc141e026c622b36b10d57e7f29e8ebb8b144e90e643ca77b9b2cd51c68194c864c3af3b5d6fa76571fe3b3b06703426e12c56a1709c2f20ea2b0186c1254441623692207151081c6c652306b3b1b616b3b2864a226c203650b0b8b815180afa4bfb83fafe62a1045812212e2e8a04598b82911024042429018122c54112087188980d4c52020e1583894b88595bdb8044a120a4a8a49895950d1c0ab3128583c0a2563821c08ed6713bd5aea1adbf3da2dcf6ad30125c79085b6fa83119d1396153a2b7d37c62b30e0641a3cf60619d1c6deacd3ee7ba0a05e24656d537b3ea1750f956e683992d3016fcd5c923b087782ffcb28cbe0e22567b6e288ed5d72f38992dbf0e972293de9f887dffc05650091b8818188c80d848da4090a236363608717184b5a89504444c54c21a02b282c09060a435040e43c244e1507130d41a0197b4b28689c140f0ef4b61754f23f8732bb5cb140000349bf5180ab882b71c8e0f234cb5e293fc4d181d6b6e5e2cfdeba442e88fcf3029b99bc7bacf7c3df3d3d874509b5ec5b5c922fb77199fb2b1a8b8ded97f793e3e383a4973074ac117f1ec47fa627be451dc38111ee777e6ee78787f4203310a2365d6f6c8fdadd20b9ea2812e7fc177361be2c9299c54d3d13ead6fa0abab69725a5beea49aa1d2e9933a1a4adaff2fcab32536a5622b7cac423f4644b0097d7552bbc07fb87b64a9e5e9e2f64bffb29dffd2637ed136bed5631a10f265792744c2f7a577b8fbd70ea814d36e4460d763765c12db5b7c33b49693eb80bde174e2b12a4949802d3e66541df059189de8a57159003a2d05852e5a30fed3ff348f42f3f25c478b53f7e489b004623197bc5aae03ae10807e9d5245c0bd075a198ed543b09669c626a160610b58fdd379146aed0fb06ddc7610de0b71c2b10e62fcdf93afd83809d8e2303d64ddd517f957df6fbbf17b62d92d7c17033122508e1181f14725dd77b3e0592bb72b9f94944a94e35fd0c61097bc585877bdb297cf170e189c85de8e5b8e61acfd2017eed526e59d5ef61f7c3e6ef914273b5cbe7f1df0795884535a8525104da7830a25cf3be6f513cffa893bd2812fa0850049b83549fb69a4dd5e50fc89c1bd552f6bba04f837d745fd74627e4285309a92d7859878abc857e18d89489c67c0f1b4c4b0514038928872f40fe9bc1b7cf085f8a6bcaae809238cf5b359adafdc799abd913839f25c88382fde4daa807a37db6f3a6de49d9c53c363d41e1e59c3ba4276282b23613736268cc13f4b9a66b20343e39f1a0f58388f2522044f7147302f9b0536cac7514ec21a3154075cbef3abf1838ea407cde9a65399f5c7c68f0d06f888244ffc9d36e9bac40a5a225bafd6948ebf64fb28d67e3b958732aa593b5213e1f1eac3e1e3b4a7d47eb67dd0c04077acef8b2122841ebd7b41ece85a24ddfdf677b68f09f9ba650d149aed02dfb7a4b0a6ad4adb5c443b9a966f58eb774f93b3cc6f0c844df6d876b1bf62905dc19b45a1d9dc4e57e18dfd3e92f284b0b586e36699e2d79a6113b06764725ee7b3ed1df2aa7e770f3ae26a50f86f1fd83ce05887dc5fdeff3f298b03978dc2f1a81ec315c179ff36c5552a2e361837ed3c6de73839920f4bc3755b5b02ffb432e70afa3bb504267cfeb0b59d4c3e3c39824ed50ca55152749d4a29e0b0c681439422531db66127193b98c8a6087a4bf023635a3a10bd3ba88edffa9ee7efc175a53a941615e51ebd5307e6369242cdd1b8f91d7ef89ebff3fa643a1ac67fd1edfcf3f8cdf76c25e1a3083fa9680fb3b2b2b5d2d47031d676849cb1f2f573b3b333d19640784b8869b8ba1ba9c3f534f4edf69069e0a862ed819151122c8114838211e210511b1ba8a4b5b5a4a898a80ddc5a140e87436010092b88b8b89818088e84db80c1082b712b51105c146e039690149394004b40707a7f12a48ff3511804470ff60e3cf5c8e5277706c68e2ffb188eca24143e1dada3b5f21336f4b02fe265e91499cde041180f341e389059c67170fc8b5e446cff199e73a3ea12121d9e7456d94b33c1f03fe33b5e89a0e439c9c9d39e07df3bb39de2479d905dd91f8fbe676dcd4e025bd19e3c31dd2a5518b4723daa038bd96ef2afcea7e4d17e2916b83d6e52d20728d014bffa9f30dbfd5631fbdf1eed3f5731db6af69df2b0b91adbfeb40ee46b9b9eb361fa771ed48f84ddb8f671ac6c527b09c3bafedf58313bb54dc0af435d91b2fbc4da9d68a19cf512a8e9572d1bb702f0c0f98f77e35c8c10ad19accbb60d059dacaf927a6ce84e2ab5cfc8df3fe34932b23c2bfd5acee1f49faba0fbab11ee63d5d74a6ca600ec76bde7e14a8788076ed203d0c0be891072264fd84feaac1e0a0dec1d0c21df67daf26dd6c7607ef2d29ba716c6a877a4154a8758633b1a46763c1f8848ddbafefbbcd1eabfb731aecba3ea7e561f0e889aef89ee8da97bfed0bdff7eba37700508fac587cfefd15805e41e5db917ef916543c5061c1bdbef750a1e889089f4d70ff36c5f20e4203df6303f9a8c8e7cebfcddfa087fef63fca381cd8788e309948b8b85fbbfc0472a270bbdc5827fedf2c813437ff2321b0d03f9eb16e2969c82f9d705f5bf2a2b7aba28699cfbf8c0a2372e497f555f62d8117951876a15fda58a61ad2c6de1ea429306762efde167940939034d02e4ed673408c7b02efe41bae64fbaa46f383e6d4caca13be2970e3dcb8b5504883ccd4b13bb6192b82ae809d7dea7bfc36714ce692054a62b3a35e07cb5f01b6d6ffd0e2aafaf2123f1e46076550d8d66ffceb67a215ccf6cb9fa10e6e39bd377af5766bdfe2251e15f6ff44af68bb162d7451ad6e997b0cf850f0c854e5d32bef4a0a5fed68d406f8a059828e943aec6bed8cd42a169567b214639b6ffacb28287a34c82870b18fbfe03b176efa145b55f38a838dd367cf37eba577915930ecff5a019ed8d8c8833f37a667a61bd38caab58e08730f60738ef47d99f7e14fa35e42ca1f4d0ad244a4283145cf0eb4fe0a79c04e957ed60a0d7fe7349066e7aba2177abd087afb0c5441f5996195d323395fde4dc9c725d11ca89d233b79038e841f676027977fe19fd79a61b625f5833590d78706d2ba93e4088acc960632f8e4ca554db6446887e9a86795a4a3c8f66bd77bb9d4a51ce3d3f603fd72cb36c6a53975bb937dbcbf97717e2d701e10faf1f1f3afd0ec955839d5bad001750bf321de0582ee1738c844553b18de2098635b3d0500478c03cc0cef3ae94198e488f6318dace4865e324741a0bd263638a9caf181af86e32f345cdd73c6845e83384f28a2918568430b965a0a5435fa945367d5fa467fab4f5574191a72bf327f442bb7062a84fa2f4a9c211bccbb820bd9d96450036a40b15b9f6aed8eaf6d04ded7195e952aec3cf8615b54daedc84be508867617b8b27a5562e055271efa84a1f337c5f609cd8af18841c946f3cd1a2b424f1b006cc538b2bd233b9be5efc0a3e70ce6c7022fb62cb75ee47f58fbae7f50ff58ed21e6e21ba50af6c757aff11c52eb4971145e9d5e6f658fd23c24ed679814d80d0c7ada3424cf3e47f02e9f19fa1d0e43efd68e6148d2afc463ca7ead338caf2b2389aad7035a5e354f4049f56ad0c9b94b2dd8b939ad41ca87d230ddccb9354a8e25aa7ff6bbef8d77c73ad91aa52b975475eddc2f1fb34c6acb136e0b8c79638e2c0aebe51a4116293db2ac957a237bb7e6581e4fae364f1c11dbf218d99f16ebee12ed6c7233d9413794e5bd819a7fd21673cb20349d2cbac05c8e182435842947e1eaa5991dd2562cf25cc092cbe127aed48f2844b6ad43f1d8ec6e1784d0f4f58cb027e4bdf50ace73bafb2f5da9329f5fceaeef80e40c9470541e2ca99bc16cec7767e93bcbb5b7fbdff83b1fd26fcf238b8099678bb2a54b1cbe7bb1d594df408cb198d2d55794f533376f7fc8fd1f902cd843b3fbf9514aecfffaffc807f23152181b268ee0063047983ed87fe56f7e7977a3c240106b70741af0f05facaac0eca1f3e4af9851ec78f8ef1ab4965915157c152fa9c8b2ee368fecae5688d9eedadf7f8f4a0c79cdc9e386a9b444e703fa4c75f777c0888fa8a494a5adb8014e424ad60086b6b31714924524c5e1e2c270e058983201045258818122a2aae88fb19fa17ce180cc1b6d62980f2885d7d05ca220286dedf796fb69ceada6ee031ae7c1f17aeb604512746a7747b73bef0e66879b11b38c6c67d12913f9e0dadfcbe823920e4fd90e1ebbffa8c0039c7975f38027ab63f3fa2cf0b667ea9c1b02ff1941e675cbbf2aa349aacaddd50c063dec8e14ff7efa3a5a5a53b79b0d5b9f1447724481e95bb1c2280dc0d7c57ef860db10bc4c7f7bfade7c3f1fb601b116814d985bf8c43d89ef1171ee1b1d57f2282ea2d2c6dda3770f2bd02b7cc8734129a50f88c40d233fa1dcef03bbf9de179f7e0b19bce199faf4af7e0ab209102773ecb13260b36bd3898f67cde7743a6e48e6f57d2b6a2fe693736cb4fc2f5b81958ec3fed10458c17cf585d168bcd59021d35f7345d923812969ee0a1f2becf883f53b47ca75b92f52abca803f645e027f457c701b9013f9b11438127c9c700a8ba15a0302a84887a2da30ac88d7b711cacfcfadd2f45707efefbdd96df7d7f74317cffdb803e04d1e5975e17c99a2f31ac85d056fb50818368384cdf3edf58583d7c938ee4523292a672c252d462498b3a20e6938e3700dfefb9c29322d2dcc079aa03750b8fdb32862e86bed94741be4a70c4e7294bda8070e9aabb8d34b149e99c6c546b30f7e20d820557322f7ae78b0334affc5bb3f40cb4bfb041546ed10f33902a24c577f87d10e74226803227f1b96e0448f8453ccba2330e17e8d5f7718d6e94ab16ec68aef53d72f28cb42f578066c300ec46c197f48e1eb7bfaeadc6e2fff4020092c3ba1848bcfa907ef9667cb4f053cda82b138f0a6a89d28d765b5cc6c460597f21d876ea102b3be17aa5d8ec9abafdf68793abbdb46eb96ac61f4056807461a69f68fc6501682ee09d2a2041187735314e423cf6a4d95d515cdf137cfad3a7574180c2f5e1a52cc2a99288c3fbb79ffacbab60f888b0d7a4f9ddf833756f7654b3bad74de4e504435a8797ee3de8e9ae253a372c38a5febad569e319fba2ebea0be8046e672836eb512709aed2f0cf3e4cf6abf03df161622a3296e313aa56afb2c087838eba15ac44137ca173e0dd0d1cdb08beada6181845fcb636b996d2f0d309b91332d518f7df950f93e0de59ad2ff30edbc3cbfbdfcb9206eee6fdbf19ec5f667702ff7ff8307f099f8df9f664f2408dbcb14d43d3d59f0d7946f75937528e727a270503bf674832fc5b4fd54ce77fc053c5dec676bbb0fd336ddf79a9b545415186bbfef4d6bfbf66979eaa53b818557f925e33389b23172c75bf5a3ff90a54b6cf7459646d7f54ab7291e9cb6d85ce7eed01b0432f77908b28b16dc1d457887f605caad4f381e759c5e7f759f4643cf1750d3df73feea9c2b2ffd83d5517be96081732a6307e3df5d1f65d0a1782d1f0c5208be09d83e0e49273eba0c7dedfd768ffdd3d55d8f0532ef7cd6906f7b00819a2922c248888a1d85710ffecbd542d5e9338cfcaeb282a168ba6fbd7ea83bca6d5081217db75133343dceeae3491a17241828ae801b1e3833816fd96cb6812d0994fa4307194a7be10344c3c9ece3cb8291238f5d2754461614acdc5d9d566d92cb551ed7cd239bfca36da41791b8ec1762f5d3f60525f40859cc7d94f7fc268ff7bdeb3e8f61fe03da2d78b860defd2f976bd9a8652f81ac96b5f73d4c291f758e2ca7b863b7a030b1e16915815668e5da2e2951697748abc07b51bb6bbb0f95ef80c2d19acce8361fc81da5af6758f61e9dbe3050c1721b32c39f5bab3b68233f6424d6cacffdbbcc71257dea39ef490c15a7788f39a7d33847d5fc750786452b5a9e7abb65b7dc7027c25265affcb790f36fc948b35564c31518728492e6bdc3a6d1d2e63937e7a450d5a8a368b99d79c998c3966e334fd6a6344189d529a57d4269f3141de7678cd07f0b6656db9fa4ebc3cfa33ae0e4308440b84125f542c2b497a350317f6bb74e5eee36ad7e5357c4a42186dfdd16902c5960ded85d9c1ec330c8c62f206dec7af9c432c877487a9ca3ff65a1f6184ef32f2f20f7d0b5c3d9ef8d4b4cec3534b65479ba3affe7f67546b6ac8d60157f0fbff9d93f26fc3327e23ee3929df96cd866dae5657d9caeba6a414aa9a279d599bce7cf02c5f755686a62116e07812d06804fbe89bf5e589a7bc4e86235b36a7f4f66b7f9b931253b119baafc1b730c4f17dd5666fa6c6544f131037bbedf7ce49018e02fedee37be7a4906821bde05a704fc7bf38ffb79c146d9b13da726276a093922724ad5435e41de17a7a0ebafa1e2eee8ab606506d7d391379534f2d6390a72fdcf8efb3bf72e9cec172cd132254e50176c79be5ab2fe7842b6737bd7f83c86d3b3a6f73f4b80e77ea180ffb7846cf7da142bd377df2161caf9216bb244612a63b62d6bc2ee794fe899c8bdfd2cdfe4ddadf27ddcc8e17514bf5421b954954c35eb06877fc56bbabed50c6f29714f5000935caeb993921579ad30eb55693e1b13987f18c66ef86b4ef273ca5526d6e733e49904fb6c1839a575ce66eff7b93f6ff7cbad9de9136d2c459de45c2d15dc1531e0e5137f6004b98f83b9a7a391ac929e94a38eb9dd0f331343452d436f5d47677dcbbe7ff1ba49b8160201004094388894ad858c1c01262201b71301824292629018582ac452520100931184452126e850021a1566008c24a540c692509065b83ac2570ab69856ba953acac4fe601054f339192d3e46259d2f591311009b15b80ab46a9c07bb280a3f4f4e28e6cc247eedfc75ba7167c4373695e2a41a337f5738d9c5825a7203bad57d8250b12ecb2d7fcaf72529e440af1e2ce97d33df97f1483f4eff7991b2b2b5ae795529ea6118965da50bc89bc3a9197d3affe64bd67d93361e8be9d740381e120c79b69feb98fefd277c32bdf0df2052e81efd1730b55348cb1f354a9b4afdefd2106fd8ff04ab8b2bdb589a4fa4915450852d3d758c9c55a5203222667aca0e0eeac25e665257e42dd475dddca58c34742f16f24068d048e98485b54bc80c4be7c4c53d5a960545c133af16184fb8ab980b570a66f2f4be7f933ec1296b2eaafc8bc451e4733a5b76a12f01c6803c95e98b1d4f12afc885d0ceafb7d3856c85cba3e711d70853c018526cda4411f7a7caf8a3ce43fdf09129be0408c416cab058f26fc0d4b05eec42b9b57aeac4982d5a915703cd8b0b52a22f87dfd8bf2b58dea34551b9d677959f557745f943fe3d711c0f1ecc5b5c5ecdf7c00b118e6f008e4343e456efd176c4199bb2fa27c9806173cb2719298d90e46397aafb41081650b24c43f7e4f8662e711ca7bf1d9c1dc543a7acdac3b17a532ca14f9825e312f0cdf020977729fe737b9089b6d796f454ad9c2d61962677cdc36a6df41a74949ea55ca656bbdeb4bfbf7aad39f80655a30e9149ce8a17d47ea1d9a80e33eac96ea3fe8f39f479fd8e86ff7dd1631e973b77864e324b1b3bc3b10b9add00651791647fe58edf0f7a4cf97dc2581490dab6f61799e8ef7de8f0114eb544653bb7da687a6e45935f203aaa936390664d8f01fad2203c503eb5ee35f137bf44a27eb62cf3cdd15c14b6f08ddf68a3eb54fbf51ffb456636989949c61bb10f1b145c814f1833e7fd0e76ee973b77864e324b1bee3f210bc6d034c282e9c6eefc9a5f99e4a931c65136f62afec42ab0b198f579e1a01b1a2e665f87e639f1063cd97c4c86b3219161be531b557aeae94a7107eea99d2baa3cd106f6155cac5fba9d68d789f7059378eedd9f1687f13caf1083af5b4fa71b4b6600b3ec320c87d972812f90f3f364a4cef09a8a466c98a3bb98c517422e1c5c2232e4449a5318db504aff3aae4729f4c27003715fbcf6c0a85de6f9b42b1173debb1a91a58a80c5b5353222c1f8514e3f97c2551e65e511d822e8163143584b4ae2e9fbb34bf2f27fed113790faca4bbee893c1f450ea23a9610cad05edf7461df701ad9a0e3b96d1194df7c22e33e0d4e9c0c791a9ec7bc288928e64beda1869e81758c33d13fadbba88780b5a86e5cf8fa22f0c8edc9e81bfdfb9e7cee5e232b98bda88612bfdfd595acd0ff30b871832e9774af7ada3f9aec38d12bfde5e375ebbb3992d74bee56742ca5fcc0ef7f1e7e0b72be9ec19f307138c4cc1645abeed70b585e79b6dd64fe2b42eaae2bf138e177d1502f708220fd625d40f2a28d43974a651bf0b15e821a6b4adc3591e1a92fdd9c8a2362cafcb32a979486f5fd1bd24f9195bb295ad5898ff3398a50bac506b1ed95245dcf273ca87d6a0dd98c549464e3f3092a6f44b4fdc0ef7f1e7e0fb9a169e97df1da27fd38602a779edb7f91f6d9ae79335e4822f144c9219cb650eecbd40058a2e4345f5c558a7030cdcd2737948ea13e0730faf18cf9c70565b937ea1607a268dfdeefcb094b4dbedd5964fa76a8d6df2d894ea694dcb5f32e1f0fc35ee1f74ac8477b63c156b65efb4a12c7f036c51963ddd33ff0fb9f87dfcd1ccc37aff7f8aeceaa74df519b28bd514f2a2cbedd64beb65bde7cda38f948e54883b8c00d4cb6e5c09726ec9e7d9f55d32e21d4d5020e2f32ac0dc9b924ba94cb77dd4088d81d68b8e9e24f3d5aa630e3d7d2a040d0691976e3d1337e652534252b6e4a05f516a5827a2f940a1c3b5960eb344184856848b1203405c6f3c7821487756bed407a4667ab6f0732abf53aad73fd20ea7f1e515fcb6cfd2cca7d0efcf870fd23f2a0fb15f7e23c98b717cab859968470339fdc7500bd392964e09cacee0ef76a9207db18e61c67233796f39129ecee78eac1afca5cc92c6cb5f6e43c9912a8617cc93b8fa7237c68613483e83a6938dbeb6cd3bd3ab4601f7c148499aff6a65fe1d199632beb752d04adfcc0ef7f1e7e3f74ca9884703106b9aaf2891d491a6829aaac48fa9ef84d783442e110b1eef2adde0e80707bacab3521aafc6a9297d4655bb4231c35b571863483fea114a0b88f777cb3883f032c6a4b77d5a2f525f873e5529ccc090eddbd52aaa3849f5d3b1ef845134ad4722904585036f7a22be3077efff3f0db62f91e5f7b8db30c7171cf3845dc9d3c42b22eceef89df478fde21963a17232dc4749ba320da8d79f3b8f8dce599c0ab8bac11c096679a574ee2f91f39a57dedd2a18b9a1f7d99ed9a55cda07761d4eb81c79e5724b7cdc0eeed15ff3e141ff4e84c8e49c3787ea8878d6e584adbe2d28bff6ffcde7d68c10ffcde2d7e0b915d97532f2418baa5b728980b0d1ad65d633ebee30aae31af71c1efe903fe3cc0686520bf6b9468671a33fcf1b3d99881a114526115bf837ac665a74529404da7f1e908ee20b957d1791003b2dbd7de6d2c3aeef7643e7b57bc442f7daff83763a2dcc7872ce741579a12eb8fb40f9613b75c8efac1bfff79f8fdbc86252fd66cea9a67596c7743828a3a529467fb0aaa0cddec5d509cbc9b722397ec4f3c98e39e6c0ab2a2aaba7074f694ab4129f0a4b9d8d08928e3f7ee66def80de7cd46959e2ebf795159bf3149c9e9ee7240e238538e4e369f2efb09636b7bdc94eae52e149a94e32cfad0b1eb958d7be0bc44ed86246e7b3c8a2f580eb8c5733a8f1c5f8b44e7bec5b3f33892d4cf5924cf7fed20a7a85f45d0b8073d2f71ac9a85ad1cebde77b97b9bda1821dd92ddb39ec51307688a748b3e9cfe642bbcdcbbf39a8bfdfe61f69a2209314ec359fbeb16bb0ae4a813b82e59726823968cca5ce7f0f40b92e3de4ef55be1bbaa52b1848e5733dda127c5d2d9d2d2e0e5e738a10603ef8e2565ff3faa54e0b0f69ee3c78f2a26bf1b7f832a265838b507eae71ec2211407c3cd2b11872a3d30da109048d6a578ae26b8939dbf758351bda732e7ca538c12561e4ccb05a9ee32ae81a34eeb03e43e57daae7a7fd8e986e453399c75c0de08fc0e6565156fbb5ffa1787d0bebefa732b507dd4967ec6df965db927c5a876547ce484bd8b5a44523ad22a73eefd8e7515f19afde2eb80bde1d0f70705efd4edd82b795dfe5fad480f9cfcf8d39f4a4526b4c56f3a0c611b27001f7101003475070074c600d0e201fc4d82a49d6e6ee324405e075cc1d7fbb5bd6915bea2b268eec0fe2a22bbc48377451a1bc0d1f22ce556c10332de141c0d25d39ba56fe7ea44b0f5d0dd55a5cacf332376a6fb4e6baeb1063c883e69551c4933b89d6417c54ff00988bea14e85657d6c19edd40edece6e480f11a4afb51ddcc516b95b941410f515fd93e3af11cc6ece6c5c2b1d26dd21b90e30bab48ff8cebc905ac68ba898f10b18b9b78334804bb516c44314b34d70f87a62ff1300e58ee749daed056c492bb856a2fca6d221b677d8c533fe8a637c5bb816fa5097f5dd6702ed635e4738a45fd428f71f90e7dab1d2e12f9d8484b9aa7ec837ff0bf20d7005ff2e0a4ddb8742f3b6f956e1e73d41794be2a62e60939bf1ff4c3bb73fab1d2e1f672de75144429e26372ad39e33270200250a80003c422262021220e95fcbabb3dc0d4eff7e5ca173d8b122d2150698d777851f4fddf10cbba2e0c48a13bc966ec7fdb9b248b263acead583b9d8e29108de54b448cdd26ff0410f72c4b9dce4461ac8d38dca5258faa82e43edd68105ca9e1fcdf93cba46c4fb58ce2e1fbb7e8ced69d8a78ed8e0a6f203c96f916254a3a34d8d7847a8889cc8659af6abdbd5ebbd5450cafc22ed64fbf48cb489d4dde1cdda35fa6b65c1cb4571601719a54558d607d8bdc38e65639e8c5177200daf2a65b055bb25a0b72d5279256fd6c1968c5e50acbc454d32433ce03eb3fc0aea4558ace7ad4d69389afbc810c07165a3d55db326b29157844699a3b9147a2047b26f99cf6564d9e8f4beda779e4180d1634dc91f2f9b3d58a6bf1b794383642467a6de0befa63aeaccc7d1bbe52e542445bc5f9c37cf53d8e029d0b34c7f6037bccbd8a931e97894fe9736d63935df5667f9a313136be7698a76149a4db7bf8aa251f1c04a1a8e4a374654fa40ec59c6130edec143be2f973942abda5b8ee163b401a30ff576aa35a19abd6a1c2cc9e62a728afafc0b0c9154e022ff237ef3b87c1a36f0c841c801feb68b53184afd7473c1c1f2ec7e70d1212296c7059ab38f2a09be6c857f8ce9fb90a0b3e63349267850952897592ba5b002437ff81a7c0b4fa42a8fb5b4fdc075ead1a670ce4a8cfae944c10f8493d8669dd2e31e5b15371e67a0cf3b88811c1c2c5db29b515def5837672ea5e13d09a5f952721de3402b3479fe3469aaa2b77ba823d5e3e5240d7baaf056b89e77500a7dff6510c774983c81e3e5891a238bb9ad7043920b5686567c6ed71ce679c2071273984cbd319a76e9244de8f7fb68c40576912de839c372d832601831cd49877b422d33fb243de36c6da302eae3b29dfba63104d8f7455502f61f34a956f368ca52a5066e9d3a8871e036116b94d5c4bb2e1293aea85470c4569b9cd4c4e85eb8ffc0bc2dfe8869775aced10bee0ff3ee33d77dd0c16426bd341fc20b3aa273e9c849f5dfd31f217be0b4153e7e92358e3b364b3b3486fe337564e42177f33318efcf6c6c8e2f4ec7f3a552e0d303121db3f9ceaa7c8ead70b32fde0f7814a9ca799b6e5b84b42be97d90490cda0a87bc947125cd273d16884f7996ed0defe9519e4bc35be1c3a02a6b5fbd371405c3a78795079a9d8cacc633b7c261e8db11c5d1483ff127e94a498767cc62085f62e0e7e64144ba9f3328fd686d7a5796e9e1cb8573cd183d88246ff18e7b0ebbdf141e84de929bb986fa601482e138222cf0bb495a5d8e0fba1895e7f4ecb28e9c8c290639c3e2b32aba43697baa42973d4de978dbeb081b30627a1fb1f6a70622bd791415c68f1872b75eca5d6f08c5d83f0274901c372a69b1e3eb574986c186f4c792555be1a7143b348126169c0165f44af3c63ce73f1d4ffeba158e46b470f372d445bea7b966ead01057925012791143a0b3a46151af232a7039d09e7ef833e4b4921c3546f8cd4399267dd0f53b25907acbb70bb724334e9cba5bbc157ed80640eedabb2f350070d849ce07c41252508fd1b093c2652278be37037dbfa3e3e6d0089ecbb5e213f35be10ea513d617798b3e79679c3ddbff15a6d2ac740aa3e49d4904da471b7968a13e90a15657b9e88576d28191ad703f8dc44c016ed289068dda6b1d0ad5907344ef30b26694c6ee082cccd9eebb796dce3e561cb170f7628bcc56785893f658fe958fef0d5475e20c53adb2cfc6a963e8f77530c5092ab7170e533d6afc1a83a15710f10e18596f51e7e2dba99ea91f183868af5c9b61dcbfbee88321b23806aa675fa26cbecc1ed36bca9214751b5273f2e056b835e884c88bf3a784f7893e05acf2fa1124bcc954da0ad7cf151d3e39795a3181c8036fc8db236821750aa324e17b30738b707e8ca06a95f983b5c7f9116bf75b31de8ff15eed99974f6f3f851d278b7fb9b9b169786399712b5cca82f1d0871b9b93bd0acdda752949c9d5d30f305ab17c98ef65397d2c6eaaa6fa65b351fd496fcef0098c984698aad97ecb76d288793cdb79e734e68192805a0c852336736ae02de71b5705d23a52fc21a330df6c3e8c9c92bece77e4d0322201737e7a5062d446dc4cad2c06fd9190da1bba2506d174df1c3826f0908a8c70a51a033fdf6476a53d3f7f940af8458a247289b0239fca0e637fd304eb48cac01c96f762db8c1925f20452f47dd730e8eba385d30693d0c818fb0986976c6c4507f45731aa20735acd370583731aaf6e2adab0244b478aade16374c1973d91fd4ecf83fef163074b424a61a30226c0438c8add06a22b05f3a4e40061163d2bb1ee0c7652257a8cfe2d27337c1a256f6464beeaa4f531ef15097b48ce1bbd15aea2a20ae5c557d0e18baff53f0c7a7207fdf21346f842e1fdcf29a2de0246da105ff8ab5bb7e6039b82315a319cd7fb34157c89a11cb6a2bc6f3ff2f9f533e6c518c2a1ad8d5e53e2e8c6aca3b42120f0d6e7f7797c72182526bc144d2eab4e74c934701fbf37ed798afc35c9d1aeadf0049e55d231a3f5b79afc226f47ec52a1f2d74edede0a5fd53f2ea6cb5ff04c581a10f56280fa69f5abfd18f44ddb2f883a089461b4bba6751ee0c9c6756de63c462c652f575103a1e287abc727ee748dbbaabf7892d58bb1bf4a97a21e3fcd22383a352d79b8216fe8a9a6df5b0ce1dda8d5dffdd688a77756d19cf840b949070f933f86fdcd4384ef75bd9b361997dbe4e5fb9b1ccb778a6a49b7c237623e93166ab16668e4b49e5bec195e79f86e14c300340da5e86f11f1cd6cb39435bbe1ba71f2626b14a601c965e3d159d569b1a471b7c43a57a6f1d6bb1118ad7193e559e2bc4fa11a5015671206d19c2137ed64315a255ded6cc8b34f3f2ec3707ef856a8e5699e6c3321a3adf0d4485acf02e6f2792fe528b3a6c64efc4d680006ff2f391a8806d9ee7bd7144c1ef9f85d9dead354160cfc193f9351acf05eabb855da4d9fa81e0f5404b1c358bf7b2111a6be22e9c97e5b9645feb92e3ed2fd0a462d19f3928b57fa339be246fadaa9490d4295031c5230221b783d920f07dd14e3485f4dd4ddace7a0ae28907db815be768353f52987461e85989e65465d074a5e9407033ff4df3b3b7882c90bc9c9a845fcb49ce5b2189231b6f7e6e0bb7ba75eade3970065dc8f7735ad9f558e8edb0a9f7b3b9d27973e44a54a236811cc6459b8c1ffe8c456f845bb53e3b66c86054d63895c79d9177db9480f61d86f81a0debaea97cc15c3862ac7e68c4203d2981e61f41fe2aa11d9fc50a33900d7f74ab618db8777888e1983ff5ea9fbda1e7f877021e0709ac08573a1c625678e61844618fb5dfca45eaceaef72d85dda2f817306c8acafb1153e0bbd8fe8b97a98a29345d79f1b951444c9d58ed11813ff2e1fef5bd43e4973c70bdc2d7ca3e593890d18e1b943b2da739fa73d2c73eb98ae8bb954eb04ebd06128977ccf2f78be577ed44114904be6c9a982b68b782588b1c1056106796f6e9cff9cd4c1728459eaa59db923c6f3df2af5cc341e0e240670531851ca1f8a7a1316f51243be933bc4b43af7a437aeb3ed74b51218e633dc751683feaf7c989626aeb497e739517098745271e89d72d3b7d578e7ccfe03d57833edfbb28bc8cf2fdd9466e8fe92c9db9998de54fdfb6b76598d178ea31b574e113cd79d543f7cebfcdb2f11ec098a563c9cf26cd2f41357efb2eee34b4fcdb6f7a549eb138af9b20a7fbb31394b6bc169405e05882532e8601416aa88e83215fe1faf048e65ffb157e33d41329b743c85f2911ef20e4794b94cebc098afc382c48b4b825587ababde4a6169a0f0b7afc68b0d3fe5b8678faeb58e2004c793a31f7c7d9b347a410a905b777a72a8be3e304b4a9b07c57c9c47ad75d5e2e4846a1bdb4cb49034798b6f0847975ab33a83322bd57b334d5c2b81cf87759417862262f065056c1e9cb33a28499864e3cbcda4a3ca9da0277e28554a5c319f1aec0c565637d6182e52b65e73faba76f3d2f49939d288c7a9354da8e03fd3b3362d9cb6c14f6214b5a52d5fc8f1b7c73671a4ddedc7bf6e87236d503ad9dbd8cb21e06e5e480f43f0ee491b43ac765966617e3d06ae8f7c45c9624bee3b797fe53a7437f0bf40fb7f7dd066de1a2dc696a9be53c4d5e1562328c6a904707788ceea3224340fa9b8524d866799ae6caa178ee5068a8cace3f2dcee3c99ec73a7ec721c8361cb5797e84fdced367ad8a845331760a189b10704cf336944467e13f528ee259f69f523c7e28bde6da5e89d1d91dc0e75ad28340d4f70c8d7f25c69acdee558ae28d2cb749ab2e8bbee563209bd8ef44bde3b7e5302fee7f175cb9aa810c280ce3f582ebb6f92fb0643c72792780abe0ffdd1c8593571d3ed96fa77c0ee1f38a557803128f4c1d5cc2a60c85d05028def7c40619befbaf58796e7cd0ac18621f1489bb9ae0f25f42178977ddf60999fbb33833e678fa38013e2a6c0388ee51a9b260d451d3cc8a8f5ed30c27dc51cb3a09b794f643f18e179ce53dc8e3ebaf2e10c96f9f438cedf6904df977ffbe5ab8c4daeac895fd13eb814f71b2e698afb6cc58a5a619d56edb56ac2b643d9a7c28d0c21f00056517078baee292a0ffba3e75dc6fd5b08b36a44b39bd82aee6a5387eb179f9be1285c985beac8a4787f9af7e315101331bebc6585a9d18ae2edb390e70c3af4de0a534bfe015ac397424b8e95a4a4d84f2a95eb20e2991758b139e3089efd6c8bffd91e4fd0b89a7615431f1065ed467e287de460dcf632c261c057feede5ba3c5c7002fbc1802d868e00d77ea87f8266c82be2f67556ad10e383e2f0aba8d8f52339f4aa351ffc4b03ec8e0e3fd6eb847614552c993a351faf12f9be549aa262e87e6b8d4156b305af13e5d6c5480b26fce80a6ad68f3e1ca9942bd247dd17884ec555a85e0e17a4ad546ac886deeb27e8fc70d3fad35164648b15fc4de68bd31fe24c2fb94868e3b9df3b77e32ab953c493c852b8bf26fa75ef739980492f92e1faf507d7716d6f826b6b504becfbb3f3fe637bc0dd7c9fbf80bf7285329b4ccdb19d7955ad7d54c1e6d36eefba6a99ca54f4c2061c2c2f371f7ed4687053c055144efd7c382185d9151249b72f7f3446017243839931a727f1409bf36ef6ffbf717ff058b584d61b591bce54ae285070e72742e64ce475fdad175bbc5ec79c9f3bef4c7ec7e0262bd1d711d3f3e55f2e0fdb09f9cf470b779b9843ab91b73b0921f449b8a69ad4fde25fd4e6f90efe455a9661b897e67206951c91779256467b49a219c6e794217645d84c5cb9e1f2aeca5aba68523c372808c33fe591af924597277e7366184df6e8da93bb0c9f5431ecdb0837309ff6ecc6296626818fa827ddf3ecb775415be19a36ba7df88a7625d268879b0ffad4c58dc5b55f61d82fc6fc22cc5e3f0a3171582f10cb5cb875f3a632469bd8c4157bb920d323f7399f3c9cd1bcc5502721e7a48c71607970cebca66a76276e2fa60069ba945f1a7b81e15f7c1686ef58da3cf946ae9628132290b87fcc5c0dc3bf366764f0982ae261df24097d689c1abdfb1919518cf539d3c65e8ef08dd657f109cb7ba7148f94c96862d8df19ec239b82112fed2ce6a32fbf8c1a3bac4aad89111686ecce7c9d183109d0b4fa383c9831f1e53e7b6800c6fe52c52c9d1626634895679790f187296be8336304bc741fdab878d46efeca0db9229d25aeb324acf6c318c101a5a78c54d3651e10cbf35d3cb89030bc10ade98e51aa9ac7406c28cafe93cb976883def86b5ff8ebe68730ecc72b2f4e8a51d3879c9bc925d957132ef341e4d35b8cef73476ad2f361d5e3dbacdc5ef71b675d288c44fa3082ce6734e3c3a178f419ef4c38bb45ecfc9ec6bec8c2a8b5226e5292714e53fd6a17d94585e02319b46fc35230ec87bc66c7c4d995357a74ecf15e9fa90a972f1cc8c3a057f17b3e9914da05d4be35c7b422b5f45ec624fc1f7b6f0196d596fe0d3f94b4d2dd024ac3439752d22d2ddddd8d94740812d248a8a08488744a4a09487783282a12d208dff53f33f3fe597394673c9cf3cd7cdf3bfbbad44b6ed6de7badbdee5877fc6e93a767e996a279e2fdafcc8471b7d3955d8814d99c43f081f86b3fa6a5ae26e3aa71e9914d86d34da1b42ef9e12220fed185531f29be13722f58546532bff7c323ba0ac03f2ca53618dc9523733bc8e591ab1e1a827eaabb0e909cd6c1875c37eae83cb31331d59158883f11bdc70ac4c7d5b396556782c766a45d8c96d1e04b07de08ed03584b0648c677ea738b54e784d01c71c8d7c6321cfbbe9da54f725c3b6c6a587b4e282b6f22b9dd5082add90b30fbb6a6adb9581b0452aa7f0f7f92c10c1db2d501f8dfa9ac94c7314ae84a1e1e48bbe87ec73462d214713c4b7f1a0ad96093ab3b6dbf87f0688eb9f9a3da8c4ee359ba4f5f1e4ebfd48d4e4e763d11698729a287b687409be7d7c85d45c93a947d6d3cc8f2b51489b9715ffb1481f8915b5d58ed0b8d8fef4b89778c955fe2b5642302e5cf8ea141b9f1e3e2ecb851d1a61a4dcec84f4442818e3378ea16fbc34edbae35ee1e9a51c2137918927e9467e958def9d7275ed1f03d604b5be3bf7e14e7fcb91770b2e0d2b979f25eb1ba05c5c0ae66597c665b2d900dc40f0207941dc39082ea0ce204a4f20bf6aff8581101fbdf7583b507cda3d15bbb9df85b22055460a903cc4fc6e85d66bd6c2fc3e89dac95cc4550687b7d4101c80fc83be9f04f9ed3dad8479441c21cd3d57d28760cc84787b837b8ea2af398cc7e73a99f254b9d242b23eccfd2172ce673b5a4195b9377a711c25d8dd35fd0a201f1715903c3c76f48bc172691efdcf89a41cb1997d402a00693cf5f6ac417d82416b86eb1b34c8f944af8b6c5e22cbdfd13c1406a47a848cb002641ee88a43db99a1ae0df9ef860a5ea269145576657df234f613eafa76601d87bf5b489e8bba5c5f2bbda7531b84223781a813cf700fe735c3174bbfb6c0bd3e3d3abe9a8bbbcbe26778104d139aee16217f8ea428527e6a64c5e4c114cbe6d80bd25c33df3cd9dc816e58a17e61893f29a66f6d0fd8fa0fc1a3dfd72c7b4b3fada1bddcdb908b145eb5a607dfd146beba137325f1f05458617f0bee509cf1607e24fc7587eaac7d9c73b3d2494776ab5c632b0993f01fad73c2346f4f61217d9d3196217a2e908d2798fd2f767e90f52ef5e853fc2f52f74fac0c7a9a661b1cd781d7012d27e837833d23c9e9016370c544e3533e5615006e247725cfd0315af0ab8233ea849a50609e4fa5c6a03beaf512e63e4b088799c598576d969fba436fc5e221770cadfd46a0e80977f77f2e12ebd24ae009b3645521fb03f072263f8fdef5bedf5a7573ec83089a3c0d407ce4b1085536a284e9f66e3a838c5a1f37ccdc7a9500055bac203a96b456b2f1eff465de13389d6e963d329a0a080cc725e0da5c5c1dde7dd07494e06229fe8d413e0f9584c4585acd4eed52db73ebe0e2931cd6b221a6e05e637ebaa84745f24f631b64d2ddc8320deab2afa9567e9fe89f70ce8c3917ab25275d2b1a23c38d2d8d0ee03fac737edd5c194f1cb0df1984bf0fd32b4ea4f0e01fa98859875462572e95b176ce88bc41495c7f047407cb126b1f9f1a9a3d11caa281c91d7abe226b626108e2bf0d86552cfe24b6ee6ba2994e9a9c0b5052165c02be422984a1f543ead67ebffbe150edb0fab5c608f1cc83f6a27bc62a9b67733f7595a490883d8cee8fa1ce084729c96b1c8a87a5b9cc7918bc3fdbac51609fd11107fed7550c560b3d6900d6e7b73e2d9206dd3922304c066bc2432940f5cd4d6d9b88b8181a72db739ca3f08e46f6144052cd31dd24e0992350e79a8b92c047f4f8d06ec2f332acf9278fdb8f199db9cb9a64cc77ed31640fcb0d8489df495a1ba66a9108eb603969174de2a1ca0fff3665b8c5dfc8586d3d89087b0b6995b56f25a00fb87cd0ee53e330de70acec34b66c6c50f7915b42099805735d1202672522864a49f6c8cd271a00e9f420c48bc6609ca0a4e566b92dbcb8949c6b1bc3da8eec40bc4770bc63f8b719e14312893eb6abe8beded856beb02e27750ec34f8ac24b9f705a6e2d1ba7cc94d2f69a38bcfd2df2632d6d0265adabadf9df7da0ee31ba8f10f03f2bf5c63eea3d3d9bc6baeaf11b26fb091f2386cdf0412a61b18bc973d14a6d990f0ae16e906e8ef8ce77a018e4cadba71a44ebf5e143bee9be6eb1bbc88d5776400fdce8d8131cee03e472be7c2b38db7a73ebd24c004802d1299b60a9f22dd635e0deddcd4477cc7e119fe0278bfdd3a0c02c4e419df1bbdb490a9b156490531d793b374664ee449ddd29e126b2a9fabdc8f42ba62e46f02fc9db1f53a5d4d3622d378b8c875764b499ef74624603f8c551a85d838619442cacc48868ffa9e2a4a5a02f1470f862da6f6b741b35d5a27e4e58f2f3f307a1a6cf2b7b6fac34bc1e857128e7fef98146b8f150c699ff76f653a3a6056915266ddab673bf74093e492dd04e39ed221330cc89e07757ae34f8e209febcb1d69a9fbce75ca88968f37c16f0604e365f9acfef6d713ee2e6ccebf672ef3dd792a2f55f40f3d806232915af0e2ef2ee2ff1972974be8b7bfce0eb9f11d1175a7e91faa01d5d077396a572ee06f5ee5c18060a49d0548f0a54b2d66d56dd948c93f3b916224c9bcf12e4f63fa914785c1f54531e76106ef613551d93ab09cbfff5db15f397d1e5342936ec5a6335ecbae211fb32413a19acf443adf4bcad8da04bf6f10100c2702810423b34320d52ae6b68c42f0ac9fcc9ffee86dd068a359d07f5459c00981fc0fe9476ff0e327b017a995fdf00974bd42ef7ef284ff2151185e632eeeaf70a3f536b7c59e5ac3334ecd6d6bda8f0a08ca1f6faa0ebe235a83f217055730e10ba56dbffcdc834852ab837c4dbccb39fa47e5600770710141f4740541dc25933570c115105fc0f4a58496b670a9f335bdd9b977fc92c625439658b1ee821ba3f3fc45b87e7e6d0f1cc3b39ffa2c884a74a2bbdffc8cdc502f3acf15f52777baf2bad3ee00fd4e41bc1afb2ca7aa24c7dc383a4d07c6c67681e9aad91f087d21810cefd41144bd6750dd66ba5c0403a4dffdd2c5dc8f100d70f33d3da276bf8db7ffb67f82b6c32639136d867afb378707dd3b8f120804f08561a7a2d5df2ada8bc3792fe17d1c288e16aba6f977db82fcec43f7e502822833127f1ff1da8436d468f259f43a6f69e97246c5df4cbe514d78febb5ec3faad368a42c6e04fa98d026b5b7138662b775c1fedf65989e7bae50cad4a3b28be00567a85282cee53199f4fee1d9f555c2a1f2e8ccc5faa0dfa536b7b518dea63f03e3cfc39dd70683701d9fd57e6ffbb8b7c6ffd3cf2ff2bb551a3615479c4f0e4fb7241579e9f06919f3806c3a9acdafddda329c3f917783477129b103a0cacf76e53de3fb2f424b36e79730258fc85d916115be6c5e65cb9064835de8b8ab6cf429f9fa51398eaa82a4651f05d9b88db54bd12808249950978e45e7d9f8d0c4a9e556e29ef8cbcf7cdf474b7699013b0b8a3e8aaac9bd4d71f69f74f257cf0116ee9f30532a2972db7f3f1cc837233876454e347474c17b5b2018fc7b273c76170d4d7da8db77c9c6187f2465559dd80c735e7a450bd34cab8bd178d47a1a5f335d9d502ec0a60e71c2972cef466dcf7791f194e9ff162eb9459f00a60b1797b5651e657ab31cd575b973d6f332697ee07327e9b27b12df98cbb14c890b15cb1f70326db55ed8088e515f7d4a2101eeb654890b4154b0d5aee44a1a6c9597ac9a0741866e373e77585f71845479e2b94663140c6f25c1f7e102fe4154bb2f1ae01c731ea0af9dbdc7c40d61bbb15178af14eba20d06f5a635c1ab8cb6c02342295ddb686ba7d881278d65da477f59d5684ff7b44e044e4418135996f94bf887ce8c353b9d1b7f5e27417d8a61ee3466d572f5506a676cc13065f0a59161d13024ee4c2326fafa3926dbd9f255339f46d35d36545cc064af0df623ed4916eb7a0d9ad77486176cea22e6a5d03da386694d085290b6f96d0def44c42bdd5fd0c7e1511c86d90ac48b16b167db243d13f33f2bc6f9f58ef9135e051b4d37fd5759cc6da5b276a1d8737dbfe9801c2029ce8ee0d69ac883c6f52fabe2e121dc56e1176e5e02390519d34ffce47987abe8cae0107f1908d79e021f9e42ec0b457dd6d565d5c3f4edee17cf2a9d7eb1693d765e0c41f0751c369df2440c6467b24b9928d5b8663ef447b96ae5afad6db5253c77b3bab2a6491ab4a14dd7556e02cbd6d3c39697abe8f9e2ee701bf8dfb9bf44f3eef0031a7dc249b8c4a1ddea5a8ac363d7fcde4433af20b807f6d0aeaeed8ba1bbd5fe2aa0fbf02a569e68c4d064cf57ba4a5a7a761988542081cb597d339b78375ea018f3286dca043349644f2c7b147708c32daa782a42480d69b0a1c089459d5110d98ec0e6199393ec2c07b0096db4999976004dfed0a36e40f576d90f24c6abeca7096bcfe6932135d7a476ac95edc1fc5cb2eafd0fa5a03e0b1b68cbcf339497245b32efbf87eb95187b2719134f07ded4f8ab810099bd15d783af9460dc74d384500f9e7b0c3528f5f5f2a45244365fbbcccd6e81ddc3890f199359d99908aa995afaf481a36808595e2d45bc07a969e3fd8bb30fff985969cb74ccb5dbd2d393214579cb3744ffb0f13dd778f042edd92c51fc45c5f7deeb5049c18d525b9aeed155fe78306a454f3bca389cabdf900408f3e390afbec982fcb711f5f362092dacf31d0261788f8d06bced159180eb93a6a4a5120fbd95c31c87b037c9f24d2e7f22b044f43bb4de93a2bd357173fb2525c3d4ba7384a201ac21127bc97ef4f7d5b91b122081205787c5aaebfbfd37a97d9c65b6723dd68c73b8352520403a06f2e7e9370a15f8e7d452005c752f5f9c0231e481bdd78b29a3eb42fb7f94c4d405cc12e7746db0d09f0e894f5e1b1aa7ffa4679acf7b448248fab0acee83d60fd8b464460ead6d3d1ca611ab2c5055e33c80f5200f8535c2efa3b9fadc568e132b64bc2155a23537c4da0622a394a70fd6087a2d297b8b7a257055b88a9a0d81bb0e77cad620e86c6db3a755a63bc5eeed660c70d001ecd91b783342ded8ee18ddf6ee3789b93a865ce0f007ddac8ac956be4bfa5d08710fb204345103eee77368356013df65da774e1ca5a49dc200aea4a3654ec69407e3e229fff4c8beccc670f2fcd653778796dd69e23e82c1d6d28974394f6732c553b660224f1a50d59ce4810285f5e52c0d5a0c0bfb6a4cdcd66e5107e5fcce77c963efb0aa2d0c97eba959c29f898b069a8faa51d1e507198ac84a92019aec3b580f5e1a1478be177ce01f95c40ff1953a0afdc0c7c7a9da896bb8a4ba80537664214d04fdc885870340e7a2544292dc8b6ef77cda40d803c2c45feade6248497bacba75ad39f8f0d3f8b3df502f69fdca77ce343eecbfcac3d53bd47397104ce335bc0fe294024cc7e62f1a9f14e92a41e362dc3e9bba5d7afced2a9a51a7b0ce63305278aec2e777d9e4bb8fbbe0e684de3e5d74ce6894ff674a3d679928d529e52c99814a8f8f820adf992be1273f8c6cb5b66d9c375f6161bea007fd234350de74f08dc26ec11a396ff64114f6d2e03c8cfd189dab1d5b8e7af5f1386682a1bf0b45da3f20080bcfa519f2f06b42fc7c6894187936ce5c7b03e6500eb531df9e2c43446478dfb9e64bfd5bd4f6feacb4b818acb179f4df7ae4bce5ccb26d21a9b645ff7de595403e46bd283f80765f3a7d4e4e19c2d3c445c52971aa7008fb8ccb6768a01beef61a0f6064bb79943b59671feedb374461108fcfa159b35b71bf85c8548dab5e1ba5b80474ae2ba6bdba3cb57b5096fd5066724901d6ecedc063cb6961569348e0b285c72f3f43767de99467a8ca52e9da5672649cfb2065e628c569c0c7e11e4d2a392f611f83e6ece1c90afeac4576b6fd2df9777cfb3a94ebd0fd82fc394454c42c31421e80f10dcbd6e25e404a3e7021153845a4b7572aa4286e7113aaa696c7efc039da3807efcc2c6985bc5a0dcd4671a7cb3c6e35d8a53491f5071244a31ef404ca375bba72b2ebb062321417b6a00d01fc7cf7c1a9f87de266d5ebdfd468c555d8894e213d0472bca0d9b0e5f2b9ca4e7692ee7ab68ff8576622d00f223fb14756ec065099b0e836279a56ec5df45a600b0afe233f82c7094492d257605df33e978264814ad02193e05a2abdb4ae858594ff9b2fcadaa5ec95a2fe802fae7b34f4efe7b2bb365e5038e6e6ad23c4f2d8aef00ff268d947f2217b52f3870d6d6e8dd84fb622faf0d54ec953767c80467dd3f3c7a9c7c9c0fe53184e307d81392c298ef6fd9ffe2b583582a0b4ff0933603fa8184b374fe8dcb91d2b72dd07aba056cf5b598dccd10120074b55e85db6b62e6df969103173c950f558f0cea9e0215bf07d7a41f20eb5f43dab83e4ba1bd8833f5bdb805e0bf515aafd6fbde138d5d98bcaa9e28b8238594b732e00f904b037e2b95a03a08aa417e22c99e3dce90d71474f591c709c997e70a6e1c2fde4ecf772a91f93a48926c2fcb616edc48bb585619e87012264dd0145aaa298834bf9c12c0d25354ec99e0f6d3a108540e521fde412e98c978c9d6c8cbcc99f30f9f1d19d93d79a1506e634e4e130e1e736351a8a98918279f18bba88998b809370fd488176a26c9cdcb23ca23f66fc9ea025c590db63e986c962d55017ae17c41c4fd959f2b2e3fff153a2c80d65f067065ebbe01637e1785d9095e9fd158b8d03b42a4bbcfa76300912267ba322182a3dce9b228b49eb71a8cce5f35adffe566db178541fad30174d95cd32ff87d605c08d791bf5cf53afb934a8117b6dc3c3ca3769b8211b51274fd0bc258b5bf32c7f51f5cbf727f58eeb75f5aa37f4946c1744fc1f01bc14dfee887f510b85c085c34044e59853e073a598933a55b739cd6469ffd965a87ebea41d70fd711f11ba1f9f81fff98645609ce091d307e2bfb57bec6ef9cd7cecd11bf221160ed965f7276fec3b281fbe1284456125f05988e75a31ff230958ddc7ccdcfb9fb5c982eb09c63abe63b50b88dce6c92719b0ff75b41a6276ff72efded77c9a721b070609afac2e0ca632afc209060243dd980800304ce5064a3c3479905c1080961901a04da4f5eab93bff299ffc0a686311e1ec6787858e9b080b14679572e072d4d21fbe9229509d1eb05e4a7d27840fad791c3ddf4c4c0a7b9f005ae0db6531f1719cdf801c5f52c96d0173dfc5e1f023f9b88f2b7fbaedc4ebc15bff2fc7f22d1d535c79f53c303ff30f44141c3396b97572d6fb8f82bcfffd12e394b87854df7abd875ffd9ebff5be49c18069d08c6fee3863f804b0808860b80a005c3c5da39fc20b2096bd5c320939fb94b62f7a3091a3644c35c7b84dd52cb2f14d944247c0663528ee7505130f07cda7fb29826ba69d37a6b7f6e601372d7d83177fbd6ad57920f26b0a3915f4d6c1e3be4c2886cbac314c0f0fb35e1700685ad3579afc583529202823db81321d54f4c3da2297fca4f7fff754e28d71f9f0ce29d1bbf62a608a089db24c4d68eab90a9157b9d2e8db23b4d0a37c17f0d08a2a7cb0ee24e407a5259614077fe13a70f9a0ee0df0604b18d1cd6c0b7e1140895fc8ae685155687a53961ddff2617147b43daaf5c606b7ed5a5168e0affbd19c94f26c2f5fd8d997c4ccccf678abc746b60c7a20e56692b8cda4e5860fa7f2544f46ff7c73997fc0d92ddb5aafb7f7c2834e734ab7d038198fc5c01c2fbcf216bf2b346ad9b34af04fc2829e5479ffe37e364fd959a47656cb5fe04c39e4012d2bd9f3f0249a9de83ca5644e2b747fc282be55f7dc4efc2b0f4abb35a5776ec83089dbe04d1d5fa05c3a9ac66371d2041432578e4efbc8404516624d620d10e5c2d1fbfd8c1f9f7e56c96f4a60d97271403d22fd593e7ed580abd79664174f0302e54a753be86fcc032c8f34074e59536a5c6d297d582bbe91de43fbff7df5e15085604d3d27fdeafd7f9c0ae7e39a333c16eba874ee8627e01581804b0ec23f8a60bf1d2bfff42f88bef8fa16a646cec45a569646b6be6fa07c6735b399abb08b0b1a9d8697142dd39eca13abc1caad6ca2eb7a4e54555b4352cd5b5f85d399d39ecddc5548cbdf8dca49c957534d44dfe446973c16405918b39fe2062ec9ebc505e63532e5e533313536e6e3e0e23633e731e1e4e734e28948bdf98cf88879f879d1f6a62c26d62c6cfcf6b6e626a62cc69ca6d6ec20be5e285f2f3995f6c312e99b9db0970b0ff39eb67569b9ec167ddeb514461cec230cbf3e4b1512a8c96ad18cbb04e9955ac4721c3716841a4ddd7adb1f4cc4fd3a55e2cedbea276d84ee2c96a90f79ea0b9fc1ac7fadbd855e2bc8477860f4b6c3d6929085d3f482e915a44bfb2931e1e8455ae7780b8755668222eff0942f377de5631c66db4ba2f4add79148ff0cc39d863240f229c1fa3b29bda2f5e877fda47cac26b21a13391c6d5f9cc4579eca595d1db5f119a48dca41a9f239f53e6a0f6477e851e72a35b04a2fe5768fe5f2234d545a146de76ce72923afca2e25e3c1696665ea61c729e2adc8e7676e2dcced6ee1a50157b2b730f6f39059bff5f094d1e4e5e7e76336e7373131e7e0e4e4e4e332887113f2737b799199f31b7198f318789312f948bd39c8793dd98db989f87d78c97d388cf84838f9ddfd8dc8483fb42f347f91fa1c9c7c5cdf917ed0f510ea3bc873e52dfe38c76a2dd1df9b07990578999d85eddd7f1395089a6355be2907828f796257e6be55bea1c7ab519e7f0464ea691e1e5a800b18d8e5823db99389842efaf4bd8332cef7c40c4b7f37892615c24ebf981ac897239705abf4e6f6dc53dd7e8e9fb59518d08a92104feb222909016dc224336a494c68c9955824af72d70a01a111980c06d5313c611f9e8476515cf181cd61a39d2cf980574106e771eeb72bcae47afc34507b922f912c15b2502d84ef336e652cd6cad3108f7ab047325a6a4bf46e5699ca5f30a0686a8bc329dfe1a62e17d188320da09a10312b20a4edbe81d043af183f3be192712a31db04e10032518bd46641b9a52b4baa33426a118ac9c1aabbbed00a6ff4d69454206d1e0345d3a994207b6fac852456920a1e21e9108c6c613e1a6beafde19f6578916c21b15801293a2aa7c398b075c6148ce9eb7ba378354781aa6b3ced2831e117ca1ea9d903cb88c613ca3d0f092503613681c226a395a3e56d85b2ffcba54e1caed1792d998fa4009f4d84bbfa2da985ae2619b5a4c4a23c100c1767c202f95d9b9fccac0712734df0c79ea6af5cd774f7a0780124322ead587d632e92f493a0d071997d3ae160d7e02f2d4b52b7475c4d4aab544b3f49e3a44e379a950b40110b448f202755f045f78f32ea074ef274eefdfdae203123663ab65db8ddc4c18a32d50aba22912bab9b70c0008e403c2dd649d89505c9ec6295e8e651abb7a5d36c069ccea36d47ce9cbd8eae88c9c470c954f3169801e006178a418481180239772185347e1f8acb636932d10f83edda277ef35716fa0b4b10c3fa274788eb1932d0e249ce50c60d4d4af2c2d1bbc3fc580e4a47b1d2ca101c22615dd5341b0e0b6da4d7e23013daba2c3d6e67de0fd5583238f76c82b948a4395ea9903999d92d39b80028d93b9a1dc2e7ae201fd6f1f474a25e4a4170aad01989ea0b79774f1ecf74535a6d0b1f8dd8f8d14492d00770d9159cff46b75a89b29945dbabc7e31c83754067029b40ef0f07d781925c6b8f49e9d3c0aa5e82d8221102a68ddd0ab8d2c89d765f59f8184d4378cf667eb03fd11bcaf670cb609c8c747044863a11d8e68d1b7c4000983c55952ec90db4f0291f5dfd3a39e2451475e59044ad46ee607930ddc764d7e7779f314034a1e87e1f004e07fbe85c62b93d33813280b5e180a8ada5f0b084e803c7ff805d63b7e7ebcc355fb9bf356e911aae5651b800fb9f890888c65f5530bef4dc849becf5e4effa32e206185390e315339794bcda3e1936bee4bf1af4c0f9f849fa57f4eafc46daa1e88551228ea0c51554236edac06e40f8b345565c6b15955b0e8e9579a8a309f34882fa079575e3c7e931ed5532a8faf743c93e7b0e85d949571964e8bc4844fcd41cc4d3ff28ac3c3745fc08a370be08fa32462f219d4faa39568522ae544f6046b7d67c05cd1298e0f53961ad8e79d7328d04f0950b7ec6e07d67f3699558b1a7520ceb4b8a7be562b3e4457541e089e1ecfcedbb8eb2576f8e137affb6e56941cb8f200092f0febf02f77d8157c4350ced57f3c30f4b4f12b3110ea9266f91a8ce3785dfca8dbd5552a52e864bbde03088ebaf57fcdc0ba34fc063293e32f2e7adf6b4b5d1248f878b3c2f43db112f36035c57b7598a419b389820958bf585ef69b2e8df3ed420c2521636cd81298ebd5408dfcfc3c4d3c3182541eb55b6d287f37a4fcdab21f50973025e8905697ba75106d218e6d4ebbb5437679c509b0ad5e85ab4539c84c880c6305f617efde46bf710d80906e4f93ed1aafd6ef7fba5194db2d35fedc3da20648e8e3b9b756bea43d635b3d9ae6f6f57b95f956832a201fd00b86455b6ed2dae41c6cdfe2b6e639b6c99c04126e22db146906033d73eb0b8fe6bc7994d89fa568f09ea5f7efd6f0c405870be65de6c57a27fa88e77ac33bc0c7628b18da3ab596939b89eae0764525486309bb1480703e687c4c971c3f6484372ce37038cb4abcdf800a40a0d7f0bd6a83e8fa7e4b39506841bdbcd971d40e054a98ad5e63467fd1d28e70cf83e88d71332c2b9d9403fce3efd68e63cb63d76fafdbd6485fd977d9e7fe31d03406ee2a5bc2e597f7cca8566a2362f1ee87416b1000fbe21ed9066a6dd71d6541bd2b2ac306f7d54e37868012f58492a2929759634e7cf29e3e8c7c914475dca80044b10c0e6d5b125b050f19baaf5d33961fcb476634000299ed446bc555c45bf0e6d7daa2721d110accfb4680fc8c9c3e646e992cd26c109574abb86a307bc5190b8090a85031bc827ef9e5bde7885f1f86e11ae82105cc02df7fd6f6ebb3cd6c5bc7a69278b1e48f8e5733f0f20087b0320b15c11b45e5bacb6271cc5fd1fd8530793881f11133ab4a067d89d50926dda4a52ad9d50a2302807d2549624d7b93eed64bae7ddbd78616336942512400c4f0d7017f6a558609ba44a8df03adc91a47277109e0fbef576979d7f1c2cd563f1c1e5eb4b6fbc8397613e04f356efce8ee7953cd81ac44c2fca913db448548403ed9cf7d4213b286d2f18487f587b49a38c86cd90391fdcfd9853e7ef3d5d0cb6f67bf4e5f53f48db60904e647b8efc0c9a3474987f52eef990562931a0b7d0450223e5ed3f2f9b2f74d94944b2f92bbabe56764d9588088c4529eba7f47020f2e91237bc65ec4cdd1065c205f18a2e125d5d2f191d22bcbb1ec2563843d1aab0105007121f3ee51a521bde050e01046bc0e49bae2ab3e2700e261a8a6e246b159cf3385acdb59e60e894775dbd800044b1bb2fe8e98eedc8bad18e4211b9e38d22cec7a2006f649b1e461947642742f916ac4d7dd607ed9dd5b403446afb4adf0cba6b8bf33fe2a4356771b01c14614601f89d4d0ed4ccb3d40705c66962d475def4bdc0121bc0fe0820e57ed0c3225b2fd827159fdc830e4ab8168502cab3d8b5a5ac3b76ad2f5c9cb53fbdab45a2d4042ab6b4370ee2b6ae11add1b6faf51099af63a109000d1ace7df34fb159f7d91e7eac2ce489acff0257f100fd88f7d628b1fa0de75f9f1d60fe5669d6597fbb52981821451ccefc60733d65887d874415eb1e39a92ea7140076d2f6fa697b5895bd6baddecc7776e8a7984ac2000fe880f84e591cd7d42d02194627d7d9fb0830fb74f00089fdbddef65be0daa0bbf1cd9dd8c88f5e09e9fc62d873f80c70e0842771f0b224a92ab81472408df0064da11e969d7187ff508d5e4b6efaef9a4bca5531cac209de1054fc8a6953702302f768abe28c62718c8817334ce99657b5f93be3431e4297a3290a5c9f7eff6c300f3237ecff7889c70f4cbeba3f10cbb6c5716c432ba0b792c9ae0f71f0404c18f09053152c157071f3b789edf1b10d9035043f4f27596774ba830fbbcd1ded0ba6e7c72a2759afae721ff9c30028bde74006f17107ce9ca5bb21a78d3bbfc03ec177393c0ca0d81bb068140b0fef6ef3ffefcf6ff1f9675ba06889454a7c74ed5c47ba5a050ab110f59a3b2c35d433169115e3fcce12aebdd1bf6127fd8a5dcc37747f283b3204a207efbd50d08da2de290175335784fe8dc5024dc737666a70bcae8eaa93eb560f6e4c475105ed55a0e32921ca819efbbf9e6adb6a8f5c2b695bdb2521c946cee63d1e461ce69472c9a3ea976f475020cc8ad3def72f4c82dc56f9dcbcc94cd84a4b58a48737eb6c2d1615081ec6de9141bfc56aafd0a860c291638d515edbec4afd83759f6ca44ad5c0c734945525a552b6ee58d586d8bded04b2cb5b0c0b6abffce2543f17493379dfdda470542c6d17bdeca263750da5a0956f55288483e77baeb8e3d7ea1c464b1861e4076f28701ddee2340d06e91fe73e1e655bef09d69722104c30aedc226060bd50793878effdc24ad196bf5f0b94640472aefdba2c703dd550f98160123466142fb6021cb2733bb96c36453e4fd475bfc1eca9fbf072eb153a4f28f7ddb2ea1d80861ea2f2dff96b18468fa27642cc1eaf70e2b230946aa2402ac8c25e0345cf19060e72a4606c277d4625299317d03d7392120a63af0214c727518fe36aabe550df3533ec698d859a0dffd7ea69586b93842a9315bc7e1e0f3d0c76ff40b00aefbf0fc93cd5a045391ebae419e611a3746aa91100098427b7d1f5bea8a7b7f0721eec33bb7b72bd1e918cc7fe5fdff696d78d8dfc5b89db336746687bae74444e1f27a5df0abce7b745cd438e48fd37ffffeb07277feea8c2a58f7ffbf6bfffc165c713b9f8eed703e1d19469e2e3c0e0cf9c00abf7926dfeb5f49d039fce7f6c84d7e01ffe28ff65542e1fcf5eca3207fc3341069be0224ac6968c8aee5e974f1aa78b4148c884955117639ffd4f38f395f59966f0a811c107dfe0d21a1866859f28d8fddaff8c62d37038efb2c792c631e046f70b33f62788d46f5f0dcd1742a3f4dfcbf646183e4b177f4d3fcc54b1e87b78903bee15fd00403a479344e2ca78d543a1e2dc3d36d95121a35cde76187e71aa108b8e1e7df9f0806043723535c68bd05c2b1ecad98138e0c69d73d815f9cc0050b43d060d0893b2f18cafaa550d91f49c3bee039e682eb87002483e9f5abc96124451c5a4c57e17ea7d718245cd4d13e7f3caae705e717f017872a01b3ad9b039d349191fd544ee4e942655ab9a15f477af3f9c3af615dccda82c9bf4dbf32bfaaaad2637dd21e4a892fb79ed77896b5b38657cbfdcafe63b6594bb20f9adb377aa4f63d3033ffda1715f885f375d5fe4f7bffc2a18c100a6aed4ec3900fe79c91e01e1004b38d6339423002209d10c281decbbc5c1ffe7114a3aa26979a059b37fdf01eb0bebf681e87f4c0927765bacc7ddc66f8be8424a9c212af255cd7ccc6ccb7bd7784a77a9c644e6834e30a465d53a4042bd825dc91943e8c7a455614d23c12e6fd7c3262f22b70da3e72f72225b14cb6beb969b5aa29e4cb9876f00b9f3f823ce53f9c7f00f93284ffbc35a6d3e952ec67de18f9d5c89deb1dc609ff5efeb9b00b24fb82f217e0dfd94da713e97e38fcd271f4f0e998426d428775cd5f79998b8effd1c561b2f11ef5ea49e376d169f97297917f9bd0bbe2335f15e957ee05cb3efa65fd87f3eccdafacefafab3f58fc714e32f42592d1b1189a4558f60f8ce703a765d7058ff1dce90a46ea433e651cc19d03ea9dd7cf7ffe6a6f5eab9a9c51dfb5fa9adc188bffebfb615a80cfce3b239e8738d733ce78b2b18aac6c9076a97f227f750a6ac8975b1745ff7779499bed2c14cf5a4ba5b9f5acedbf9bef19f08ddfa7b2fd48feda4fa7b720c3c563760ef3b6683699c59aa6d9cd0d4428eb0e3d9f5d4258a5eef17dd24440df7f6f5124e335f4951eafe3e82e21e6b8b49c105d3e9e9651da5de58bc1f5634a5fd0febba07d75c9f082a7dd8bda87bf24ff7f5d7fc194efffd55fffd55f17baeea478059f5434dde6126c4599f06ccea47c223c74e6ab22fe99faeb97f913e325c97fb8fe5afa8fd15f10ccb8ba01efff05ff201fe54c7e7466fec289bc0919fbff187c7fd5e8caf471eecfb6af8e9feaed2807adddecbf5c7f1927acbf4cb22b4559d7e710bc6aa8fba023b5175fb218777b3f4a3ecdca3ecc5a65636ce5753d8ac8f550e9814e64ee10251c220af3eafbf7dc48d1bdde698ebafd8afefa37f80ffeab1ffeab1ffe3fad1f30e7820eae0735633d3e166e1c8c61cf21be742300b658fdf1054bfeff2a7f52f3be1dfb0fd70f0bff21faa131f300417390be55f7efffa7080aa8324dc7f83fdd2b36e12d1fc83eb7fedf302916d4e8ba5c5f07f58f6fae8bee721f15824a42f597eb874b56970ab70d946bec38e99f3e41fa9ad25d3213de4eb0070991c077e526b4baa369f5ece3b7fbf4ef2e7d5e4ab5f8b8d49488ebbbe6b0beb6ea84e426b1e9bdd97ed17664f007881c01413857df0651526ed720d2964e9b3d81c0217f495aa04d14ce9dd9a83b261fd43af882b7f2959a037a9577102d0ef3fc1021bd135c45a351fffd96538c1b8ad20b98058f08c24238df18ac39d1f30812455fb54b7afb2b7be2076f0c43e62088c0c141e0fe78f20c4cb02caa0bea9cbffa12b9a0cc86fc519d99e9a869174327d87f9e3e692bcff68571ff4ba29dda4fe38e925463cbb2f2e874b3e00af16d6cb906424cbde6a934ef7ebdce666f35a20b5f8e668377db3cf2e4e071d92365af84495d5351e84bc261ae67b263821915b62b1f82cc9f0b1fd6de64fc6cd1b42790dce88c1925975499291c1379cffb0e9359c58e506f665545ccd0b61dcd8454d59beb26483bf2c8e8594f1ef3c2a5d4e16d2df93aaa63a7ab64957f913bd8d3fa7cf2a68a259fff99f136ab5e470081bf2f83ad8a08e903877eb1cb3258279a8feb0c4d8ceb2868efdf9ae8de6cf187353ffdad6034c769c6fc779fd9e1edcd95e1a4d9691620140f07a2681f844a88c44ef030f96a60c8282ad1b5ccc557690c2c8444bd641c19c2213025a7638bf3186dbe4bb3cba6d7ede03649c130f58aa29583bd5f866f602130f1cb3bc29975e8cc2b1f4fda26faaca07d51d5062412bee14bdf9de377daa335c44fdf57e27020345d9768a2ad41a38f7153e89b54c08494edba4e3c1fc22aad9023c24e379611ea91120f2c1a4444bedcd383cb8afa81ca5aea70e8db17de99a95399478f3e683fb99dd104bfaf1946619bbb5b42501f4476c3e27ffe544bd898c81a9f3f71ea9f7b0bc945f98a36c8fed11c91b7ef2fa84c5ad67dd2f1acdf65f8434a7d3f1d476e98f39a28e09ddb8def23645c236a3821a7fc68491b4cfc566401c80c6daaee294cebd9bc6ccff49116a17d35725bfe365059f294d354ce4fbd925bf829dc3db58cb4e27ebb0c00ea9be40a077f0eedd697baefa12d70893182bddf4601ef5ae61ab7863fa151466685a9e4bcdc307ba6f626c0a964395c3a375b57465fcb69d978f1a7b1926b3a00d6a553f1c3eec917579dab965f3bd0bce010ae172f06a0b019ca0b83f96e2392e92ea36839710b44477d2105323791f2528816f21d82753254646dad46a03d950a400663dc0839a2c16bcfcdcdf035f61187e7de6ed1da402e8a7b23656a30ebd418a43149533f7f67f8892c03101d73226b318e0ff3c57ea5a3701901bbf5d9e5ce4000bd877588e94646516cf557e2ceb9e366c3f6c3bd66a09095a327a8ef90cdec90e2719983fdd713a30c9103a07207aa2e81c115cc6b1e5df16d87dadf77c9c3c0098826775da99f3e5c7c5cdbbc2b39253cd4605d27220764813ab62e0c27b2176409622d8bce2abe877ec6ea03a0828b299f4febf1326ccc998b73647439e6c548c801d1cd18d924a72fdc37dac3ab032a78e6afe4d8ca2901951b647cb80ad1c323f122cfcc777233916252aab581e671cad065adaefd175919a8ea164d71b409377b4380ca17a28a773ea8f8d85c0436fafe86e3f0eaba0d464070e9a1f5dad30ce2f9313673aa450b22e13ad6f795002c65712f42c0fc1db88d5cd4819429da8790fc312b00ca7e5875bebc9deb51ba6ffb788243dd1a12c6bd0020b3b8e06dad5576b06d5961c3e9b54834cbf03b6d5e40edb989adef064988c5d24994d6f2d8724580efda1e50d9d7216cf05135ab391ab389f306d961e3bc20c93a90f95c5407c9756f251191b9c55c19f2c46837cbfd1600b58c41caf5e1da6ccf56b4f0bec8474defd7195a9140f34bc6324ca2a72584c2f34618079ae2e603c17297816e1ddd46e33856bd3b880459c6ae70c911631cc5e34066b722d60281702fe693fa684d0d09f1fd37757d1b4073a96eec32e76f068e4a0e655ded847118872af5f780e6563b76624736c2afe5ee6f7df72408b9522e866209c41f83df9338cb7adcf12bfd5ac624d4f87d8819dd11c8ccced5c13b21e956b54aaadd7f31d729f6112d5f11b0fef558316b4a2d4645b7e29eba0d0a6da3cc32d903d1db07fe1315ea010d7d92e412cd3da4930f7c99cc805ca39dcf05398d9786daf8de20b4b758bb0a73ee2a03907ee97b9bbe1a6e090aa551965758bd11be3cc524819ea513531e8feaeb7d57849ba59866c9e988769c4f040a602b3e4b9f24504614b7dfdfa0a376408cdb3dda019af365d6f73d5f6679338731f26254a070e82ee9273c2073fe12333abf411a390f0277898113219390ade9269039e493b57cab2a7ac54f2e12ea46b450308eb29a2675965eef91efecddd619749b516d2bae409e9b57360080321f4f7aff52d9a40fe24eeff498c4f626e99c039bee597a88c1fbee8fa8cc054e81d3784736a6deb92bf76501eb9e4ba2a45140bdbcdf0191fa4ba2ffe3f96e2aa0f269f185322d7388a6cf22deca7220d51338aaa43d1540fe639cfab27c67d2ce526b775911e61ba11c20069a93e993e5100aa4bb40bae9468465ea23fc050d16dbced2976e4299ef8e53bdbed91b5efb12e2649512f10288ae7ff3315122b34dec404854a2fcd4212bfcf84e0650d9fa4efd39f621cb80369709a5d523ead3391fe30a407f78dd8a6dab4b51e42ec31ec782eab994c4a5bd07bc7ffb02362c693b1612697706623fb86e9e2c2b590089af81eeac2637d8b6c5c558cd9e8efabd297a8fde08a45cbff3b896f7dadac7d6addabc05eee9233d4ba463a0b2d27a0f474e2948b2f095a9d778cdada23535b85540ff71eeb262ba5663234b41f6124b2d5bb31bda3480e66c3bd757735efa8bd4663d72a41c5e6b09430cc204b2ab4c3056555af2d9f55ba1b8f7d16cd9b6fab3d180fdb587be3a9cfe699fd1816be1abc15d327a87f97e60fd6483fa85d39c8b912752c2c8f719be3a4d077001951d016aa9cde53685550c8af295913df7e2a7e12b01f9b33dddc1f1ad676a8f4b6d426a86333aa0d5681000f661bc3e912436a298a36da7a8fe3216f7d6551a3900ea9bcc7871746524a223dd55d282a4cbc4e424690a68be8ac346941d12649c91c49433cc3e49f880901f03488673703275b5c3adb992546e9ef0ce40dcc145c30c700f613a78cc06a60e78a4910630215a84287e52c705bd5796505f9964dfb9727228d514ed330c5d9b48203b2a01ff9b845d3c2325c2e155b987dbc309b29e8480fd35c5fe11f2299f8d541c858d018f440af14bf20080fee1be87fbc5a7e34dfc5b39a81e573a94b870b913009234548b6e0e4cbca6807c94fc7679243394e77b28d04c7c6c2590a74be20b71c581c781106597da335321607d0b5e63f7cd46237c47ed2c516942d2315ff24a02206d1bda51896f2f24ba68bdb6717ede0b95935b8e03cef744bbe9c4a86f36ecec0f4d39a4481fc357603600fa2143bb6adee4983b2b2d75d030d65f6f2eea630470d2f5288be2d7c616c643148d798367d0199fbd430a9c94a2b9a12554156fb445238a236ab7d488a4d1af0e9da5bf9dc6ea5c2b5dfb40d09bda7ce810f9f44a9233a0bf0ec7058d9f4d2f19dd47d95513dfa4e67e74371368b5519b2df0acba38c919d3f96ea3b3e6e0bd9b45564052397718eae5cb1b2723713cfca5b1188248645cf44074239e57fe34f5d68ac20026fccc111deb75da3915a05580988370137770d8b5d0b4409ba47b8bdc3b2e434fced2efd347d2340abab9625a29bd55b1e12019197a0fd01f13dcb161784d3afa043a2a461872276621b119b08ff6037382f6b8a7254f0b2a6b4d3ef1368a6c89499ca53747b66eba9006cf2f38babd56387e899e73f326d0eae33b9e55fe4d06d9d2e8d01596cc705d5f876554a0b21263af519294e598f23d1ea75adcdb247dbdf151e0f991a61186c491de7e4f728e2297f84630321de900ac3a640d9f87e1e80a690208943aa67d869b5d10c76ef8fd777f4312671088a96eb37b6ddb71c1b33b4c7f362f87291f94cf8893d7c88cc3c8dc046a6cc2c1c5c5c5c563cccdc3610ae53482355e48385c3b5f8701da4fcaceb0618642d382db83c6ad3ec4579a5c7fcf67e7eeab26f87dc780207433df20d245f26ad300654ce010e6256bc47ddff296b652e234f94b463aa63bd278c53f3fbe7dd824cc2f00cabb649ecab0cdad147599cf188872607e3fb95fc095f1776cd7676c4150f8bedf372e63955870dd1bb9098f734fe02d761309f648f49d1e98d8aef06f03908218587483bf5764df68823b40740bdb3465568ece86fc8684588368daaaa5e6f7879de37fbfd73ed9cd708fa0767ab73ce1c0bb2dd385b2852bc562c645cc051d6b1de4708535309cf7c066301390c6580ef77e10c8efdde8d1bd4b432aac247fce777c40102c2909e35bc3701ec36848769e7379f30bcabde6216618eb8706e3fe22b0d617c67818cc020ba708160c1002ace458588e4a58cc8c279ddedefa687177c8dc7ddd8878d4f7fb074d77e82f38eaf02ee8e8bbf0f88bbeffbf79fc5f7e35edcb070423f9f4437edfe3f6d1e807da22fcde6d940718d737c6a2cc3ecbf0fcb49f2543d6ec062b0316e4005efeef38a712ff1fc639e546b44cfba266f9f355435ee6cf420f12bd60ecf3df8d730a093c977a0e7229f2bb98c08acff6b57f32ace8efae03788c8020ea11961a7844db5b69d7cf92145c8aab985a677822ccbfbcdb7885170c97e3b97811cbc5dcd6cfe5629f0b12ec284eb002e377460445871f33be8bb123477dea1bbf641b5a9e56f13edcaad1dbaf48975e5e35d711c678a40b8e3fefba374bead25ac1e6e14acc7a9530b87396bba34403d38f01e919f4d2f344a7efbc12bc0aa67356d2f78adb9e9565c15743fbb41aab736f4d999493b114423a9cd943493ddeab19d7c8a1ed5b512095571e226bf9a3b9132e9138385608c85a9756a4b1bbcfde2fe6c21b6da69e4d1269414a21b983be1062fb91c277311777a55efac54318a9eafb09e188532e74522610481065067b75b0b642e0ca1f0d15fde34e67afe7c262e16e518be4348332b405df6ff0d35cf3c980c1e2837f61bc60e05611e9de24174b896888814c896d4791fd0c100f58f0421032db3a55616b88a236ac5be519576201fca583702cbe0d71b4a883a9356fcbd4c272629911017f7f1dc26b488536efb5fcf2f061a5393a93e44967e0bcc69442f32df63313ff142947b9c85dcf9504566aa0b5e3ae25f2e02b46a22705b12e4a451e7acc79a52f818af9830f7ed475d7b40af9ba67ef0e5d32f7704be900cebb2d128602694defe6ed5e2d1af7c6fa36debf4106a0cd3b71bfd9a3a4115fc2176ff4ff5ac1e0f588151e686d771c2f59bf0cb57335fd1cbbf78ab0cd1d65b815a896ab6feee86cb37c0f41707bd9f098e8333932c10da0d97c98609a015795c2166f0a9dd51667629ef8f50f80c5f54d6f6bd0d5cad8b1ebc9c116c481dcf6661c27a001d9c542af7da3178de44d7e3cf1da7ec61acae20014f7a5280558d82cdd969f22d7fc22c7ee45a5d456a57e964e481dea4d78c88811ee7f15e70667bd30d6e378209b44b9d34d80ce0041e5f8fbe28b4c7473c69d8c32607d4ea8447b2a46681d4d46e14916020b98bded1780e75b89ec6d40deb36648f7130dd24659bb67790a00fe00ee0a3c3ca6892e3c153a96070e91f6f4c4242f017f5f098aaed4b2ff43a4ca99814f90991957d54e3d0089c7d27696d81abfb74329178e06afac5c5a35000a20cd1985be458dbf21db94173f6893a46332f5aa921f50aa93ae7889e189c94ae87408f10828741cd00fb2c167e99f07532374198993bad778880c1923723743558178509516f395676f5b6c85e644d6befa61406b647200a43f1792d0eb6a2ad9a2fdc813479c321bcf9e067d04fc1dd75e6b61e042653d684dd1444c07bc5be89893012897c185e1eb3c8fe393784a8319d83cbac479f4b901c5e67235b448550a273c4c4f6f54d5ec6991ca4c088094f2002e43626e7de664d516e9fdc1e59ebaec964a201d63d382c3e658962c44e070ded8e5598e7c3f0e6601c01fa6e1bbb9a8146a69db6951fc381e8737fab5008807be7d04dbc137059f26d83f64336ba2f12db69902fc37a595fc6a9a93e135aba0d31bfc5b2e4d55989b803f017e747c0b7dc89b92be2d8cea3ee2c8dccc243fd0bacdf68abbba8ad8f7a93b882c644cadc4a3514c8340ef348ca1aadc764ec190db180a85636e354f3fb68c03f132b8eb1e6a34811a4a313e72f51a2a04227bb91d80c9b1e4f6eef3239aaa573dfc354fec954d30d9965281dae0a2c1198d6e9caf0f90c8b504da364c5f62360a03fe2ea3f69a56aa5402190b31ac0f64b7bb5b8d8a7d80032303da1dde9e2b27c7e5d20186fd1e6df2fcf93ac0fb215e7b341dc763ba4cbc45ce41f15c374b7dad94fe2c1d45d70e6b8edfc17976c9dfcea36e4fe923e31d80ffc80359ac939ee8133bb7e3a63d2896c4a6de7a005834f702b1a097e7e167af9660f5865fbb353efe4006e08fa26ca53d5bcc8a81f6098334e811b60f566c2ae06f351473a2d038a113747df8502947d52f65a9a510b041f451447b2b189dcd0c1b320e53871cbd27c5d5807810346f89591653fae67396f9fb3b49b1b43b3770f6cfd2d7359b50242f6fb4bc404013b7d05b7f7a4a3d0facff1bc3142f863cdecf9c749fdc96331cde97eb5601ad17bb0cf68eb5c72c3087530c8e72af3f6ea59f9605fc6927dc51cff31235b65fe7bf18c4dda3d71fd5cb01fc7df6cf348aa81e46c743e5bba25e6a3ebe3251f77df32c3dd56585bc96b9f97d59fe1226649d2ffebabc19d0daba809d527e275a676abdb3d97a01573662dff325a0ee2563e47a055a6c6a30359682a5bc50ae2732c602f90125b35496bdcc4d19ae2fbedf510f7e4c55e8d40f1804a1743b89f36975bd94a184f5c442dc5725f7250024ccba6f08148cdd7bef07698553ca70a2561ed8606f03fe44a5fec794c5d105b5c5481f7928db4dd092f901f954d068c16dc9fc64a48932885ca6fee8ea8c9f0ad02d458197feb5f6624dbd78e3e414efd109b771022d10ef52d931ef551cd26b085c8c930eea691a8fb88d017c1f72661a339a17d8746306e6eae442148f57bf361602f20d79adc52068643d26e51e1cedb513573e350da01ad6ed5071f4c9b31cc1f4b6ccfc00429f47cc5216807d8014243ba71511c75e3e2eb1a0eb4da4b5231406c4331e155b134af9369ce02210de4bc0bbf5ee3eb403406adbe98bd07368670f254beac58237e79a0ed02e01ccad095b23b46fb6610c27079bcaa79daa31321224007fb47f7ca9f00197f3cec7b8620fe5c34107844f3400ff159364e5c7bc2ee44c48a5b82e302a543b86920b8c2f57e0bda5ff78c6eff34dffa63c1b0dd7fa996000892f90785c9dba6f5ed8ad40f8e65ea321c17e713470a45c33d37a18924766191f2ac8e2f0456442e3140e8827dcf5c7ea08a46177669631bba64510c7dec51a04c88f2f6c94fe4f1f2c20d44a3ed6ceb92f389dbd6203c8a77ea40612ab2ff58d8623254f7d092843104970816c5d1fd3b64fd1d63e562bc4111268b746a8b11be8ac00fe1f6ceabb3577ebfef5424222433c3d42dd147c80ff0f12d552ed274dae5a4c09c4727bf40cefad2d01ad395922d53f3475a1d6c6dc754ae9f3c55d8fbbc505bc5f6f5931a3da1585c75a64eaef2fa7e9e47cb0e300f2191ca877d2d5d7be28338b7b2a258c666c94d28f02f6e38d8965ed0ceed27ca6ca8f381b65ac093684b500d2e1de6882ae7197ffa36bdc114390441b4c55e6a1d0b374dc7e4c97ea9b2c3d98c1661bf7240c5222acd781d6ad4d1cb4888be44faaef5817da14f835f17c208d0290de703231b209dca2191f9a4e5bcd96715de57cc704f8f3517503109fd79d5aecb27c27e2efa5a8e9cbcf03f8570a7e50d388a441ec6dc5e4139f78677dfb947dc0bea74b8cf3bd2494c544f1396566ef7ab69a5fa20310efad35696b9414b6bee48c819b5c1d4c218106d105ec63bccd4cba6dfa5ce12bb2a404041f265791a1c740e69aad6a156b4a94fdde83380d0aa4ec5e55c50a3240be7e6f090bcafa62fac87749dd9d29aad2ceec7113b0ff6513d5a9ad3217cbca62c72fe951b3e968da6c48f786615b865e331781049171e405c01f5c22fc0738fddf5b9bd55c42bc38403dd2efcee2167b2a8ad583d2894f6557d0d475beaef47821867b0c48578fb95091db72c9d1f16b7f291c8ae3869c7aade3afb1ea3ffaf9bd7f9ff54a521ca52e9b3c58c361c8127fb2e82a3b9a5a9df3871dcbb01cc37fa35f10a01e890af29f7dfdd500f5280a66ae460a462e7f1481ed7f01ea35646f7b184b69f19b38f1bb3af27ae898cb5a88dfd232377770b1d270929397e0773272b2b1779196e650d7fcf3deffa2ed62452e18a5fb1700ecf94df8cc8c4dcdcda1c666ec9c9c667ca6ec1c1c5013631e4e765e1e734e1e7623763e2e534e5e0e3e4e73334e0e5e23764e2e637e136e7e2e6e7e333ea8d9bfb7ebc79f307f230e332e7ea8393f37bb1927b73107173717af2937bb19071fbb99312fbfa991b1091fb739272f8f310f3f1f3fb799293b3f8729bb090f1797a931278731afd9bf75feb02e512e8ce98e8e09a49a90d8f8aca901ffce2ab49d8d6b7a293b5bf08aeba5b1b42586b212ee2f9b8b3b2b143d11dec36db6ca570404dcf8a2d76e73a516714ead421556567f13fc018266a8ce3696b7c8ffaa060489a224e5f10bbac060891618312d58801ab0442f2418776dc4f857220fe6f18941ac930da555763a3d9a86e10866c5333020eed0ab7fff339fd0c6ce97c2a44af92e2f9c6d77fef89787330de730d5f8abdfffdcd8f696bbfcb9c33123ce699ff8b72df5e30b9f05826ceb745dcc4ce6e7e365c52267a67fe215be7cc03f26eefc09e10e70ccbcded6bd9b3c4b56762c815b614fb45815a4a165787ea88089ed02ac8988437d3912166b8dfdc3eafa3b7e680d42dbc5adaedf878bf94e0f6b6b2cc4949392f2a575133d8e6a15326d4f68aa1aa1ad9f098651b2ef0897cbd2fbbc2d7c952acf5f4189fd7aed57ac2e79d4d077dbb382648cfd6b95b974a7791df7420dfeb3ad2ef8f9ff5a5d7f92d56524764b9b83d7d598c3c3ccd148c582c7cb45ccecb6b89a948abc828a9bbba5aaa9b5b98eb1bba98da3910acf7fcefa8a8e14c7238fad5f7adeee163edaf5dd6cd37d782799f6fbe049ba00b2074a8a012af65192445b4ebd7ad5298915db3c86d4c01c52284d83d28ef673e57687680a7352985a735f2520087ed038181de9ade7ef63ee151ddc39d5328a0f84fbbae0a09f9cdcbd955bcfad5fa42c8ba46a82dfbf1d108cfea90512823e377ae307917c14f6feb7db439f11764cb3d3eb1e62993ed1c1e23b779b7d1d7bd7043f1276492acb99641e72e6de3f84403c804bf83f8746ea81eb3570c14828891430560200991cab15d1fc2670574196cb448e9143250f7d2ff67ce86a044c913772124a705cf32625a148b885149f398a9f74896c68c2b97cc528891238d8f0804587f17e8028b8ace9964c8752fa9afe6a2631966bdee9a612352c583ecb7f2d584dc60831ff4af10251f3c3883544d813df4a3abd5dd25e530ee2f0a335f995f73840c40d536f6d76b3c60efd5b5f39c41a765eb83f6e36c33d2008567ff9ab0a06bc346ec3187fd17ca59f5faa3c62ef1ec785c2b09d7efe7c954a3816b634180fa18601460502667bd33eb076b68b82dbc7e3d7fd68e0974c823408038c100e56612ad5051528acc2cc0be763fdfa9cffd4f1ffe997281c1c1c04b2cfb3ce6ffbcf28b9696c2c70952a17f64b1c20940604e190a206d10f89d6203ce90a70e3873124e0627b06010277a14602408ea84b5947cea46b7023454cb43762ac3e02e4125a2c120411091915ee123c02ca1f7a00ac3d7f6e8e68a33f21e47c3af7cd73e96197cee5c9c6682cab0bd16376022e444f5634be10fdb1052ca31af1d304bfc170b86fc75a8c849af19a82b67e6525b38d6d5f50fefdb5157157da41e4e1b762f7e0c5abcc6478ea163b7309d11f25b71766a46adea7b54b1a210ac230af395dddd05224f3177958d4c4d64b451b9f68f865a86a9a176b1705ddac34d642ff0e9e88908f5103e6c8f472ccd79e0f2fa8e8683a1365a2049372fb71385ec17a3f0e06830d0a9dc88a755753b3fc76354f2b3edec5c1ed9beb9ef178a1f1e5478dc64b9bd37d94cca2e4faef3c1dbcbbcca915a6b1f29675f1b71158e546fd170cd04a25a853f00e657308343d6e326e63af076ae05f19c4ee8a0cb08430fb63bed6aee3e6c1ead4126d320d4233a7615c85a37f25e1c582f7d5f24e043dbe8d4779d3bf836921707f1ad34244af72902fb317537f5f4080583b5b40e019cb9010e02e21431051e15190fedf675af89818d6f3e95528e7d3df7cba103d96b5eaafa50baa5e8c7e0396798b187c82ad8255b18f710d99207edd084953b45243da0575fb049a8db617e3abcebce0b61ea7a52588916a4eb02b61afb9b55855df12efb6345611426a1dab955dfee89b6a1695beacff0ca93b814be00d4c7be3756bce919bca9c6b86ba5c0f3db552455fbb992ab54598a9c660cae28f566258f33e5f16f62cb95b77856609b30796258648f2041b035bdfd5a35874ced03d9ffc8dff8dd56741f4862f221ce97b7093289cc5d5746feaaabdf158283cf5f38794f47bdf9a95afe54c77ff461157906eace279b590c834b157dc06ee92829f37f9b59694ef573b47dadff445ba39b271c97a89d879f00d688d99e9dbdf5658c6db1fb2e42f8be8995df85ab47d8d28def4d277ec26f803b8d47084cd46a61b3a6868bf55e0d03cd7ae810bfea6fa45e10f9fffff7ebb9fd3534a1623a681a2b1f51f5c67e9f78cdbee080ae4ab374dca52e95cef52f5d2453ee18272f070fc71e6875c30b88107eb9d618dffd19cfec9c3861e17108489521a44bf4657831e7caf58a3e7dfebb6868593850c1c1df4b9f044135fbce2506eeaba173d17102cb608a1bde05781a51110addd7100075dc5d1496d8ab4b9d2c093cc965ce5898a010625c68b19a77017c5b2ff0fbf609503c1218aca6d479cfd89bfaebb7c3dde81797a78d0dd1c198c6ec25602a017eab4c33705bd04e6068f0a560d9c5a24866963facdf39f01e2bdfdea3e32b7e5dd3b8f8ea4e7f533e1062797814e3192f211ebaf3c3eb9483233df31b78d9c820af2ee7409697366a5620415e37ffe16c1bbdabaab2c8fb6d47e1f0acfed904923c3b2f3c119214d1fbf6168213148adaf9a27a4f9e43bd99ff422ab2fa26a75db444cca545fe9231517924ffaae1ffd973fffffc79fb0f80f16ffc2e2cf5fdd47e6b6bcdf7f3a3b81e2eded58185344990df92bf9738dec75b1bc768af56c098d5fc75852d51ac3c6736f5bc5055bc752eca3277219aa53c52da60da7535e8f7b4bb203656707195a5e34a2ef0df35048d6292b8909ff59fce937c89593f288b75dbbd59fd21c8bde28b86e38fcbffcf95ffefc55fefcd57d646ecb7bf2331a432e2dc92eac82e5a74ffaff4afe34ce49cbfc7fd87b0ba82ab7ad7f78ef4d777787200848a774374897d2dd9d4ab780a0740a220a82129252827449094a48a760d020df38e7bef7fe7dce3df25ccf3ee7bdf77bc77dc650874c56cfb5d65c337ef330e5ea89e7564b09626c2c123aecc5d584696c4e3df678f639a3531747c3c08486d10968879ccb0635b696e9e562528fd0cffbaba885cbc1d58cb864f0eccfbf05d9ff3d5fa6cce7fa33809704587c28e0fb1f68cc56d8114cf31f75d6c364da2f8700107b2e5d0ab54d61d7fdc2741971363b870ff9d2177c101307dd4f659ffb27f9685f2e096c1a4078fca74c203fdf3f05b68bf9970d2c3b5cfe9fc990617c31a713db2fe6135502b9c70e28b45fe6b3105cdc7f01d58bfb6fae066fff7f0d99a61ab0fa1d432b79759ab0c2fb18b6a934eac104e8fb2edbc444868baa229afa58fae7d5a64aaf79086a0b9ec3f00b2539cede5490818ca2bc3d1e51f1c727bdf048c829d4f875c4082951bf539d8693a4ceb7040bcec18c083a16dcca7c42a40e10064724fb536b23fd716daff4333f23664a3c1a6e5a6935dffd6a421e6e750de482e1fb716dd66fa82a14597275ba99a9065e872e07e114078128c910bd7e5c9b744ce0d425d9978582adbab85fba3409cd95eda82eae0dcfe5c7b5d1964529c16c8683789dc9de341abf8f96ed49120451270cffb836837183ea8d634b0c092c9ab34b62cf1bc8cc6120490260f83faecdfde0da15a5569269b9b2eb62cd3d45dd2e599c3320471cc7efa55e3c82b905ffc3d11066c918e1f00a64a3c28b64038360c227aec0d73f301460a030f789fb65bd9ea0cd80fb677d639eb864f134b13a121051ffc728b150cfd1af3a160d709e9460d2e695dfcf6661f967251100cc6fd0dddcc032c3210e1bbaa44be86f50573d1474abc0c6df0a3b423cf8ad231ee212fc8e7888ffb4b4b44936a409987292b9afe6cda5f6b14710bc5e09be54d0117b856292bf8ee27437354042a055866cbee03c02f9132562cc8febfe6747bceb98cb5308256f1aefd0e21344201f7eb36ec50d838f73ff6a473cc4fff497c8ff7f1cf1f4a5cd54157c65fcf8e579e4e5f58504740dec9c1c64d4b8dd85789de5e404f56c9d1d0cacf8243d5d64fca5febcfec39bd4e24f70efb7343313e033e7e6e3e1e2b5b4b6e4e2e5e3e132e3e1b2e6b1e0b6b2b6e23117e2b51612b0e635e3e411e411b4e231b7e23513e415b0e0e635e7e211e0e1e3810fa5e72f77ef572b12dc6839379ce5f46baba0370f4d39bbbd92c388724a44547cb93db5488554a6dc48d1235782803f484c69c9d1389dbba9b6f95c581909f925f17bdfc4b0997fc1bd1fea1b05b568708e3ce4fcc7d907b58cea4d19045bdb9c45f329f6c767423cf472273031de8ab8aee0d5e1b09871d1b923a641cd654088bf7e824fe102d18c61505237c3529da02684316a16ce6b1b8c3790b9f8f5b45505fef8dcaf85f9e1445eca1be2a841bef1c676b7ec56c42823dd3d76a473c5bc73fe7ecf0a6fdcc7058fe9bf8cad750a7cc45a8e1c7d62787369fa48d36dfa29ac080fc7a389e2e71ec2bf459bfbf5a31ff340c0c1739a5ddfaf16ee8a2bbcb03e24e3f456d85814ecda891a9fe9dfbc39c34926c321c1ad47d865c16134d99cf5d845503512b05411173a72e07db8a013c344cc2622333f46fb91c75a9a0bfdb955f879cd0de887081fe32180301e0ac8a1876ecacbc9cb23c8f3873913e4c862c6bff8659c02ef71750ce79d0126efb6c277e9c0f2ffa8bc8e6a77791d9257f3c34b09596db6bcd55117ecd242a6d59498dc3dfdf4d6f74c7d16fb6630591dd941ef6b9935d39731972289610f1abeea5848974c2023bb91e0ad87dfcfa80ba5c67611217ef0a90bf33dd96b8f1afb11d506462ffeb447b62a3383fe2136dccb6ec1ab243dd32faf9c268c5596e2b2cf8a61212b0bbfadc6e97efa4197aacf9f5c83719d4fb6ad622256aee48e185915af0dfb3ac2a596cf7572ae851fa822de2384c87a7523aacb4a5e33483da88c7d58e2132015a46d4ea7aecc64451d4271fe985bd3fbfcb213fd7b4d5352cc8ecce77fed7b07eeac2010012b4b5e6e330b2b1e1e1e4e411e336e4b5e3e1e3e3e2b6e1e0b2b5e732e4b1ef8f63fd8fe998f86aff7d773ffcdfb07029fd0fee3f547d15eedd0b7f9313c1bf211d7eb68ad0130fe40e1d662ac2156a9edb04358db1ed339ae734e8bbd7d24028d5c6fcaf2c0adb43fce23e69b6a54bbf51c669ba392604167984691a3b2e88c7b7fdc02f39a4f15e15c8b9fe9a08972476a7c6c76ebb588758b8a7754a98fd902f26d327a258986f8a2c271573dbb6e88abdb96a6f0edd7deb4193f109937bea3824c84fd98b4491d5a8d27f2f69abed18bf78643bbcf9a73246fe15b893b8650a177d8cfae49aef5e7b50f7f59e38aad753adcab8dc0aeaaffc8acd982d943196be240aa5d87164be070b3c5992b475bbd66db62803246aa6785eb9d74fc9018cfb651e14de689ca37071effe9fbcbd29a8bd3cc5a8893c7ccc28ccbdcc2ca9adb82d79a8b879bd7c29ac7ccdcd2eaaf7c34593bf667ff7bdf7cffb9f713cae1663942e68ff7277256472d7e432198e08c4cdf33ef5d35c4e86a011d10798c5277c52ee26e5d7a59f97478290a2db549f8801702e2e5ee014ac714db776a5318e18881972af8b6ee4ee31c63bce5b310fa6c777d26c85833fa8ab8a3a3f799b390d41a1525533179ee2de8fdfab32a2df2e1aa128bd79bbde6cbaf72c743ab43cd826f7dd1b7c98cedc37a2e9fb98e7a1561fc1e75f06d6371ce995893254a8cd9d647a4b8ef8a0f6fdfe127983b738ae5cd135dfbf02d9164c87ec678db1e7fc393a885fe86944dd3de5d779699d1c519672d1dd6fff4fdc3c963c9692ec4cfc76565c9cf672628c06fcd672dc8636e6e61c1cd25c86bc107dffe61c6bb78ff74d8c2d9fdd4ffb3fbc751711d5944ec87eda35094b76ca674823e2c31d81ee2a3f3453ed39445fd8290aa1047c268eb1a117535307c845771ceee8430c635b9d864ea3aa6e52b6c669f9761ceecdec947911a822e9c0f4816e8b57b839e7bba7e1d2a8bd4b2345a9654f4d240663ca75e7f6a83727a14cca14a945dc98be5e0f07811bbfca95d262a8c39ea559c4885e3a5fb7b45de1cb998559aa5352fe326c92b63d85bd6d73aeee3faa932b0645fcb3ef40c58f6b956bc45772bce38ca88ca9f75ab8d245b34601e3bc826617f456bf2ed7833ba69be61172953eb86ff7ffafe811daa06a38461e33bd5c96ced2443e17bae414f7e2f96f1d7a7f7fffcef9f6d1996adea3bc473c43958a2b7b1bca7848576f913762f7a7847f7c9e7b7c28ea07bc161c48ba661cc53d7eaa16b28db0c7edfffd61f004606a8c3c19c65c1eab799b4fbf86e4a15297b270a4da74d8c32fd634dc00f87e4d5d932a17a8122c0eb2ae4b27307bc372d987af047dedf8110883904724d92ee77e70c91833c08d4f57a9eee579c62ba5ea78eeb6f62b8a482f30fc359adfe69fcb40ecaf3f53ffecd05870c9fd623c4b2e030ba3eca7ac4a24b5703c0d815806c482222bfdddc6bad7969bb5d4523eb9bb6f1b4e9e38bcf1ef260306460389183c1a0ea01900e82b36ef8eafe32fed0093fd987d4efd7cf15f1ff2774adbe0732febba373f9e931ffc9fe4cf9f9febe5da3d42e91e95f137be8f2323939bfc4c3c9c70c20bf800edff0c0cc3017abd9294ae92e2e8f2305563f7a91aa60d1f97627cafa288eadd72487705ad2db39bd80b55c520ae47e29e3f372a566716df4a5d6a0c26a21c996e2e25636e21825a4bc9369c2e8747a1b91673bf6a3d6ad71a4680798165dd79751146f69ca3ea19de87283f5c8ea367ceba30109d7850034a9af72b6f5654d5bd2afa013f17d3a3bea9b941e6ffb19fef9b9f2d024e250927590ea4124122868689cb78b8b1d3c1a4d1d2d19e97ff5978f605bc1bf468ac396fe378ceba8ff5ee33ac8d403ddac7edeb87e515a75d8ddc6a60230373e788debb09f5196b3b8e81c9e5c5e4aafd31f6e9a469ed2a9ec134ffb99c5002bff6759f47fb4be8ab2c282070f859914d6921c833f4aa6bb4bd19b83cdffc6afcc5e644cf61880c91880b1d3f94c9a307ef2a5f1b78f074d166f12665b6113513174dcbc634dc161b81f93c368941dc2a19aabb5adb0c3da28843bb78a5e0743fe96620957a6aec8c8d0d2f96704e19fcbaaf3afdd3b60597f5a613bc16194dcc7bffc29ba482aa23cf896ced1e724da0a3b42d30c0e437f7a3d8cd13aa91e4de6df1f30058635810c68bfe1297a048f80fbd14b79a4cddaaba667d9d3052370ee3090fec1647eebf0fdf3c13aa00b2d01f93ffd81aa04fee98536f84a53e46802d56aad098bbccea0e225e1ba62f10f4fad7fc5e1fae24f1293acd6bb895faa5f5e4439d6cd738fbacf5644c837e69182333762cee4f3ed2d74190bec70d145dcbc9413aeebc2a3dccaf3667aa28ff6b6196abfc826b7be2dfdb3021a282b1f6a4b5cf1e279a0be18662b24d0e56c1159f55ffefebfc7dfd893b10bf9c15816f78772b9880db8453e87b2e9fe95fc4d816d65d935484ecb9db567653659d452ccdaabaf404392c9cdba29755d381e86dfef5f801457dea4cbf1d84844c3cde3138970b996a9f13becfd27950a2e08eb147fd24ce1b522b4648fc274832ceff045e25ef67314fc30ff5ffefe2f7fc3cbdfe39c1433f38f2a74bea0ed85a70b74e971348e5145c58af2060ef0ea1c0991638a048a970c3a32b87e65353aab29143c7c9796fc95eee15764cc2b2b92d764dc34e1e1ef56d811e6774215e69f2154c1ebc302b229904118160dd0bfa6d539a2cd149fa2dcfc737676b18d816166cfeaffed4dfbe7fb09fd77d3fe76d37ecdfee8cfeed9412cb266a0a2d118d28729127f614c09542e0c9e3c6592daa9b0805647e4c2c7432d7653bd29a43cac847e7b0887f62edae4a77ac499946e61dc0e1a07042ee97885fdaaef4cdbbab9aed725c40f215b4e7ec63a5f78d1f9675d4a7691573791b8f565b8bbe998aca8ed5ec82b20eafdf752fabfc7df4f1866b5e2eb61294dcc0b0fd1763777baec36f27e545820747a302709be4b2918e159cbd4a611fa753b6dfcf3d24cd99ac16183a9f4f4ccbd1a2d39f1a5b9513dcaa96b4f79d3ab2957545fe51913167012e26797d24c4cb298f32d74b4153efcb31e155e070735b7fc73c2c64fb5f8150d113f360baf24ff97bfffeff1f7f6bcd71d5c865a6fe1f74f861f56761fe0550f7afd584480e925d0ecc2c5dfd4e511ad3e51547dae6bee97f5b369b0a2235f645dc3a91b5ced5cdbdd8616eacacd29ee4daa44c533af67baca18b0b19e91cd1d780ef413756c27dc2ddb94b7ffb3f8fba1917b4c54057ad2ca97ee8967932fc8a88d2412ffcbdffff7f8bb797958b0267bddb4a257e02d4afd97b383f5e01fc6b615472739bc7d06dff95d539594ed01d578f560ce046b208226aa5f2a3bca62007a7e9bf3f0935fd70c87bf7aa74bd554e63ca943c7d279e3149394174da50d8c39fab4f0b8005a629f097f14fdffc449fe49a9ea7b83c370d16c7e4d540f1bfa476442786a4660712bec502f8ac0a7d89c6dd5f7ff852c385828995fdc4d0afd1f92f0c7aedba94b405a6163d1d0c86bf4f1eeb49030d8f076380684aa3bb8f5085935381c192753ac1ef9cf782fc16946022aa1c7da928c9ebf71e8112dbb2b46d0dbcf1cffbe1beb2f3e4f2c03854638e11b03a89592190281e0feeddfbffff9f5ffbf6b02f30c96a8a8cb4a7c579fec978e4aa74d366a8fc689182278adb523b59a3b6ac5a96df225c6078a3548bf1c51cd9a90c91987758a22045d9654065ba446734546ecebbdd0e0b7034d89619c6e512c6f654ab251a9dc1757904a0287861e5b3cf9bcaa871acdba96f3244a74bfc48bf6f5e1e54cce39656bcc4408652096b1aef250a9f3ebdbb126cf5a4ae28b4b3658d82dd3e5dec5a2d5d979f36552909a585c26c12cc8cd782069ce4db7fb42b52a987c338d0046a2cb16ee97f799621c6d2940388955bd69f8268702e4d03f57a7d5609f3be82bca033eeb8860716b7fb9c7264344c131575d686b994695c3633ace11eaf25e4ec62e175ce72e7424fdc3af8a3b081074597208a408e0607fc65b31d7d0b6e47faa95cc2785adbcf400f9b3d36f5d06b8776f0ac148321011855855a7bfe470bd533a07e09abf30b2371c4d666352ad08da7a1dd57086b07ff843d71eacf917558f2ce13af8fe0dfccff827f03f985fdacadff8fdd77ffffee7efdf2f3f9f002c5bfdc0cd9c270f96e425c8240cb963a35715d499ca632779028f9af5d3b7aa7b9b291fbe333bf9b8fa244d9cdc5291903ba47a860b02c3717714efce0dbb36fad6a297f24e1f26e5cec067d1648a8777d204d6bdc2f913725dd0e5176ec2d2f8f53165467234ad721fcccb1d52a824aa8d74bf995fc077b3a3a64028f4219020d131b05739f6f164ec4ccf2643de43134537e74fb81e5e6ff1c5982b5c1cb346d22012212724eba591232933a3e69b0edbda4335fc7b8e92ce1c677ffc517c07e1970140201000e6c097310a81c094636311885baa0b295e27b2f85389dff22fabb1f6eb4bf49895c48ef76652589a69a222d20009eed252623f355a845ee50ca0b61aa3f93a31fceee98589db50f5f7bb7fb2f3bfe7033816c510344da7665a1546dae218815124e209096e3d82f244c1da9fda9e2541c268b20feba18c7c790480eb83df37f22a220bb6ae712fc7278b86cde4466f3f4c38d91f8c0ee625f0533a7297ada127dd3e57dc32e642cc189e076bc1eeec79fcccf53ac598a964426c124127d77fa096d1b2547c6d16e950f9d70043f8e07ca016b2be26b110c811742b388cfaf86a3d748943208408ce6d0f48f528f34eaed1c9f2763e06f7ee0d723601cb2206e5ac9fa1837d861b7a5ef7f39a2ee9f547c534511ca133e464de0519fb8f3d242ec964783cbf60e92f49d519ee5ed1fd99f1ffd3c7447961ecf92f72219c623c2890116c1fe05d417b1456646fee0b982922d9f68f681fcf5a15beee2da5bed2a65a81758afc4d02fea504258bd43ffbf0b25ada049ea89f5d155ae9406becbebf6c4dfafc42541858f06cf02f1230ced9f1df24e00b8373f1b9665fec79e6ee0fda493ff02a185d5570512bffe114df9ebed4a666a9f6bf98ef09e7e342cbe35ee9826c5591707f145f8daccda421b145992b2d3a8fd26d566b8870946619eeac58d8736297c7e1ddbb7f41c4cd3fc34c5812e12807ea4693e9f0917d46dbeba99e17d96f834ff8fe6fbea7bfb87e5445677b2b0b4fab3f2af6fd3f98094d557f3b0d3e690d1d6f2e574121074917075d2e0b5b6d3d7d4939271e3f4f0f43073dfdebaa8236ce9a5eceff39f32b293334bf768afce59953e3714a81325e301a5b5638da68f5a376a2a3d32b84cc8f0937731e9f9bea0656b961c8f93ee035692ca10c238e44b34fd591666576d077000fe5ff1b74ff538e30e68c8dff3de8fe3f4bbdc233f37a50d3ed38367e193f0967b0df100213788080880a418041d15090ff0dd0fdd0e77badf0d14fc9e0a257e2775d4c27bbb8fd4aeacc8be9524117d2ab883a2ea65383c136219e6227e34c2013b994e5063e1d26f16a7a899db23fd037a896157749effedca3d6f940a281d8906298b3d3bef822f7c9425c38a4d51cc5da5d8f0451d663d1870f7623637f7f84fedba7818ce63393ad99fceea1c171ae88684e2e121a9e38ebc7fc7ae55b77a81e2138d9b1990d04f7f397c29e84232985dfa874aa5586343e027bae2122e6a1dd63a0b172b9224fd59fd3aaea6ca3975466391828abc4fae8a03ad289d3fba45aea3883f8aeb9d84d4e94c11b0e015d35ce267ec49fc3a6d7d38e8c923969a345f332e5516ed2debb8175125fdc3879397dab29d9c3a846a22fb7554f745800aadc19f34933d59bb498cbaa1952ac8721f3d9329cd5696620f6e579c3ba7eeb116c288af0baba025530e46f0ed1ad346fa198f0ddc530884ad5928b7534d1616745da83303abb9025f135a70cdb3a23848fd7d52e13ce67c0a7d58506ff9907e3b1f63876b8b475da3b0f12b9edc95907d59c8dc51ffef2ff4cd5c51f1dd25f7c96239b59b8bb7878fce1f238f65e4eae56ee1c56be16b666ce363f91de1776e77d85acd185eb63edc8820376ec83a961c1ba318cf56d7bf669f34e0cf72643c3d4cb77c687844c07a17e0faf398ab8fd2ab4628fbc0fe3ab8cac93f9f2ed4be11f7d8bfd5d4dd40a3b828d4743eb62f486992090bfd7fd276d9592e602a6dbef088f282385856b3f22272f9cdbde660c1cd36b5fc43246eff177fd4fda2ab75a8d4e1c6ce947f61a0da2281dbfe954a4deff6158c8dfa7ebffe75b05ef375b85c3e95f2e9a1b9979efaedd7ff2566985ed4566e6f065e7ff83a98bf2ddd86d41a526d8112f5e7018ec48308ce06b493d2f225ae746337cc6099e56f8f61106081d7d1e1e49ec5f88b085371337e22033152ed6b3c6691e45d733d7a779fa71afee1fe72b7c4e973a0f3f4c2b7eac31587d44ab829a21551d387f6ac78b547354432ef37aa60c1f1ab892c1a1e02f292d8775f2da194b772ad6656337e8859fbd52a317d126256a22be460cf7e1ca922d6ee1fce3b32757c7fb6888c906d5f0a3ed5772e35f9406e705fab45c35a44f07eb1f7cf3030af50c81ef9c835098b9bababb785b49bb387bba9b59784a9b393aead979daaada397bfe2b9b15def204bf53fe67ce1978db87737dc0e0e61141e860214ea031683fb5fe83c4fb295ed7d19f11f80c0644ef35c42b1c4e586fdc905e68c12dc6221ae833a64277d8375622746d365b2d81cdf7119c78bf5f02a9ff62f50a02e63ed1d1982dbb7be406f67c2f11b126074c39c8c0959441bb6b9ca179525b3f022c8e09cc1113d1dcccc30a8e7b8e95d3d7c28a8b5f8e5f8847808f8f9bd3cc9c938b4740c0c24a404a50d09a4f4a52509a57504e50869fff8fe5a04631f375fc9908cbfff5f53fe077d7bb626de03651c6f27e7e7804bb5a84ec80749aa1639eb1afa6aa54a10daef5872d6fd478cc76d7cdba7f4cd2f0dae167a04b1f73a913c1e027662528eeb76708dee0ffeffa5f38bfa67fe6fd00c5564ea8f3bb3b90b3483c6326c13052685b79272e71cd338f59c7f9f0f46b617f09c1e58a98076aa895c5b2bd9b69415ec5e4ae46b638a57aaf246deb71a832138f3b0288fcc55bae8cacb3a0f418cebb21cd8e41e063d03f61fdf804f805cd65f9f8cd79cd78a4cc79acf8b9a579f979393905adb80539b9b9f9adcc05f804ada5e5fe33f7df1dcca4171b9f24f6dc5c6e06469297193322494408e01aaaea66cc365bd1758cc1b5ff2c7877754e50be2c2c06977cae9a9aa2dd3bb5235838dbe632792eb6558ea2aadefae862403968f65330fe76f17072f1b0fbc30f05f15fca73f1d9fb08f85a58f89af93af0703a5af3fada98d9085a0b9859d9f0f1b8da38d80af05af108b97b7372fa5a3bfb39395ada9b9979705b3bf0730bfcc5fbefb2297cf201dac5fb13ba850d221e8175501170dd86ccf53ec0e31b7c5f45fa94c02f20ef6a5c36e09d88223c1b55a02f46ebdbed99e9a09f2d1cd0960588e1c79a8915863c6e16631dc8ac125d12779d275306043ee395d90e2dd18b37b90dd5976b5c4eb5220bba05f0152178d1bf0b3be417ccc3aadb605b24f692b29d03388912ea220a31d77c21a6305773ec5452d672e1f3067889125d5aeaf27cc819b423ff80ae8b7a5e4af05130e09d47344b34408ebaa76a8865bc275243dac63dba120810065d82ade8179def87557fc4b2b5a30e7bf48e7be77b3a5562fb9ebec18bbb62b2686a2ce18fcee23fac0192b832e6bb2f34f909e65e155ccafa74b67d9ddeb6e3c1f7f44bc951ec3c6e56e94735845597aef019301ed7019c575862ed317a58e4ca5bfc93d367f508d5ef3de700844db3243e2bd5b8fdfe5bba570546ad04fae299d430c0be7555c138a04ca084235a900f31b02ca8bdf52d1b607f734345efd28f769fad16bb293a88d989683df40140e2f25a3f15cb1fbe47f830e2b0bfec6a421f832322c092c05bee29ba6fda9cb88b35c25cbca9b51cc9da0960307e439d039fe5e3ecea586e95ad7b6c8bbbf731f6bfa70b2cda8c347eb04bb50d33c92442b2bdb5205a0ac8d62ff848a46f60d9e25ef12d12f11053f7fcc47955d1efe942576267099b97100bdfe41e1749ac2821f518a57c4fbf56138b14b0797553376eb9614dc08191d09afdc6f77451f65d41cdf7d766758e23b5397d3337eca92a00fb4fb63826e9c911cb4715a6adc9841c4ecda39a2840c60cf99b22431f976acb5fd8b7c566da0c40e70bd6018f0d95c1ee02612fb293d785e5c7eb0f32a229b14c7abfa75fa7c04a269053c2b3c3dab742ccb5774b58900158d0f5f0697a25ef862a5888e4bdb94c29d1d397550cb07fea89867f43e8a8c8ac95dff2c49dddc5c4841a00e6c7ca20f679446e6a206fd85400015df9e991ad2ee082b1c99a7f17ad5ebbf49e5fc2e510f9db27f23e69c0f96cd3044d2bb8a4bf7b7483aa85b07c87129fa3156049b0e364bab15d7ede285f9eccb015d69671433c16e070e1502dbfd6c838f3d0b58ca57baec49486ef1b12ddf774678f36a1a108c79d9ea37bf923a24f35bf366002ce0f7fc3c54ee3461c5c52751cfd09860816dda3c7bc00baa9e4de65b63e7fdb0f3e7aef5ef00d4d06bb03f667e86e971b9f3e46f8a5fb5c775d6930cf035fe702e63fccb51f4d89b3c738daf5eac76012d7f62ee93ac0fc468a4cf7abbbb5e24e90d0d19c2e79d7ba16dc64fe9e1e33dcbd40d5adad1cc7a95ddd8fc7b3297fa90e00d87a47e5f10686f8d6953b05dc34ed05e3b5d7d1bf02de8709ccd537319e23e1231e3ed0971d87f56ba42e00209952ba6fc8c834dff8b0694ed17cadd3524ee78a19407ebe771bf911d1a928553e73c7412da981e2c3b42580a62f53efc8e2f22ae709da33b68dfed6894c0d4bf2d1efe90ff90fc9b2e7dc7a2cdfddfe7cf3a901f9b9771e3d806e926745a267be4e8b5b58bfab85de6927c001d85fe510bb560bf2c75f07d1ec76cb54d7766d62595f00e8d5d39fb1f8535caa63e770c256f7eb496bfa58bea73fdb97bafcb9d05122ef717507b9c0e94e9a00f7cdefe9cfcbb2491f2c7ed1140d925cd67c7deb28b2951900585885394a5fec7c6bec706740dedeede5d13396eab2efe97565e94d4c8697c6a45d3b573373af6d6d20a8197d4f6fe424caefc79ca4d89a93a2d98b4bbd1afab80470be352a656c250abca835d04c16cba2aaa2effee60b48e5d558c973c3d41071ba47432b78acd89042432e139067a6656f1a954d83bd02699667c4a1fb7a6cc5bdab80f569339213cd96d2169fe64830b5e135261ebf6d0ba8bf03b372fd5247d55e9e62d8d9eb71c418ac990ec0f8878c293bbbc971c336655eb2d6510a137e3002fa3e8ea7f9ead5ddbea3ca4a407b6cf75c77c8329fbef47bfac49bbe4baa51436bcfdedf5445c6333de0ba19edfe3d7d120d9a72debe47147645bdcf11c54b44bb13f1d1f7f4a528b1cc8e390f13ec6e553db6b1320aba8e68809e75ed867c17c39ba60fb0fa064d0e9dfbd8c9e5f8806087cd98001c0cd82014e99cac6eb86053efdeec3ce07eda9a7861d72d5c1e49ace53e7bc39e5142428201e0efb7cd4ca67bda74e61f3f53bab575bd788d8348e7cdf7f45d5ac1cf944a8f7bb4090c0515189e12ed0a180232097c9a9688981db2ad15bbb4bafae9a36916d5d544c0fd70f0c2a8ceeb72b47d1fffeed1e5d20e2e64aa078a3f233ffd8ec5070401040a922a0c85ee623aaa3288fc08e2de815507426fb9988e7974311d1b24252d1424d51b0e487612946910fdf42448ffc540f46b20cec5a82120e3b300a95f0964fc2c20f3fb0864fdbf80d0413098509b40c607e26983f40d44fd0fb23e849d20fc0902188b4d03c21f20f603ac1590fa41cae3ac8294ef02e1cf3390f9055120609a81ec9f3990f5058117449e00191fc8fa6180e8078841743ba820e70f1a48792c2d10fe04f100c64e0099df0d103a8862011324e1061a88fd01f51dc8fa165d4cc705c9f1870d128c8e9e0332be2090f501b9ff3010e13b1fb140e6071d647fa281ac2f2a08bc286a197cf7333a881f041188a716ba0d48fb0f40ca83e88f31c074b3f420ea5db02c7720ed2381944702290f0646820c969007a43c0a48795490f2a820e5d124e0d22f42d025e05c7f30ff0190f29820e53141ca638194c702298f0d521e1ba43c0e48791c90f2b820e57141cae38194c703298f0f521e1fa43c0148790290f28420e509c1fc3b243df775c45d1e41c8f86dc5d11769aeb45869cfc90891d9f09f3c8a92ed4553abc2f06c5560897b1e5ccef628625dd37b6e95a12b3ece22e8354cffba61fb3a5d389c402e924c9adad5623b501c8ff157e11f9e7248f13e7753d41e0bc3fb7a5543f35e738227f6f3f8c965c9cec065f76a84e09455a4b5b5261fc5933721919947491325c6bbf0b6afde28f9a87b2a3b2184e1faa9a562e6d65ed0fa5234d90381eb349e6d0f51999f39c8a9220dce28a79d45f525d7a37ee878859cc0802d8b2e277bf2b649a6b69b0edef64fddfad669720fed635a28cff36e93098f6c3e65ccb468103f7b719d5b586de6ccdde9e896e0a3ee5de8dba5c019fa4ef25ed49149dc2dd4be86b9a131bf99541b78db4f09ae1d5e45666dbf72eded9d39169fc6eb6f1aac67ee4e48952520311b766e74c1d89fa27c22a79c8ef4929f6a3318fc6ae8a216bca1b1e34e5dd72d8c79cb281a4e20084985d6c4bd6eca6b88972b1bca958a38a59f95e44263fc25af8bbad552967fab3cc40a1efef070e74c55430e3dea98014a90bc8ad1c8ecd9f3f2a1f0a3857edff47178c77faf9dbd77d8843d87d5634f0d03bb33e7a09b6d56a82a18c5268ce7cadb2513166245262f0aeb8a39c2f7c6657b5c629c78e5262f9f6ca66c6ba53a945fff1a1e056ffb8108f9218b38f79f8e8a2431bd22c97cf1cef82857b21e1d2b751dab40f8a36b9bdfe1190da77bf957caaca7193ace8250daa75a977c68643ee81ce16ed0ca129bc33bfff85f280b453ec51cf443b2a46dbc163c6ce44fbc562b72867176cdb830d9be46693af66159f1b55f8dae2ae5f2b96a18d124de330f51d1b442aa191497b7f25682b7fd230e597311ff4d570eb6b575dd89d6b0324f2fc7abfb44ab8b387a50d38093189652120ec44c09fbbb88d4a4e83bf6f9e134ee2b56ee3ef75b037c4d9fe0592cc23bffbc083a09a577a1568299aa13cc19647e9d7e78c571e5417415c7925b3141660bfa9a42541dc7b02e766a6a35bd05a8fc18833da74dbc6251812a7be8c6e2183fbce30f9e75de1123f2617a5574b8f78908ad9a5982fa103dbedaeb7917d9c370a117c23e5b5dccad495c77599c0a9cfd06efd8e4af4c3bde14907123997b760947f7cd0abced6bd1b13e786766db5f7e2f3c35dd995b3c782c57f7a1bb6d4c4c267d57fd63b23e31d3ae321665e7a7308588bebaf2cc950fbb8db67df10f37dbe5e426ed783c0de06d9f358f2960e3c4abf4e9e3b4ba5a0ddd922e79661aaa570f5c4eb2598c86a9484e6e1d87a00976b6e060c258024ca19ec722b8862419cdd7aca6f6e93ed63a6190c0db3e874171c68cb938f769beec115378835dad8d7b240b5ff7f4edf1abbb6ca51bfeb2df666b713ff90e2e7f20ae6f5a23f1e6f37191327fdc2c98bba3d3b6edc7088597ff5e708479d638a07fa85419cfd53b23a5474cafbf9a8752e749efec4ff14cf90db1e7bedc9a39eae09a992e63c18a754227d7a616d3b325df2288cf185f32764107bcedf7c43bdcbc794ae47a86462cbe72e3ac05d1e12a7bcd8d79e36da7febb5ecf2694216c3e8387d8d6ce1c6ccb0cfebdcc8c99d49fbe9ce235dc09f942b38e193432026ffb66d61cd219bcdd8ed11ff826fd3fa60ac859404f353d0510503e9db96d0e1bdcf2ee3da10decacdf7b877a3ea8964c5cc8ace0a43ab33760b28f7f14242729500a6ffbae762dfe5a4264a57b518eaf6b3673c8557824057caf60db274c92b4fa44dd4f9265bb374337a8b28dbdcf405354ff99491857ca1f823aeeb4eafe055b2189fb05bced732c63177371698a2b41a5dbecde2bbe9278e691ea3f30733f6931ebf3722e56fcad384ca5520b4266ec6ef4b38459a78ab737754bd04962774d750aa52d136aade1e5ff13396f219413f62fe2651159683df68da8309e23eb10ac009a105fe8ac5ebb9e9a618eabcc99d8e7789243b91165a157486a775ec450cf5761648c8a4c4b9a7c8377fc8bbb3b66fa6e8990361f266a35f7b80dba1bec150ba7baeb8f15d5bc6bb76df69df8f1d964429f3c4118926fcae925ac3d2c3f33ad2da71fad1ce08a50338c9587b7fd10955bca9762dfdd7aadeed55820326b1c3faa6815c699958335bc29b0689c8ea49f732b75ac3f6aa4d3dc66ffbe6e32242f8f9081717daf0dcd768e27fa0e6d04bced1f6ec9db79eade4d4acaa4ced218a87b158b37d5a16198845c9a3f3049109cff5214cfc8c54b4f56c06481e8caea0b1786bba5ae5aafb28590ea5c7b0833f7b262e05dff6ee9350e9b579650912daa54c539b1113134f9facf1853e76d39df389f142cc431152710f8d2af3a1d230e3738386e84921821d319f487e87246de4db7346b088677fc6d6f8278da62754ef19ce6cceeac93d38f2a75d773290a47a54f8665a5f9df9f64ed7176b18ef5296339e1fe48a972324f85a61157f8f9e14c185b9874a1088705bcedf3952ea9f08856c24a2f07193c091e41c156509129cf29d7a97cfd4c27207bf01dca962bfff917ecdb8c880cb70a9564e8efa75ecbe4d85ad2759c9f656d5aeffd08effcc73f0c60d8e5b81736bb4456685cdc4ac693ef36b0d061ddbea7ac7e64dae25dad9f6a3c5bc05033fcb41b571f45df832a92940009ca686b2a98e9a8f470e1e00cdef1d3d1e657581cb02c703ff5b55dae328f48195ce5654e7ed62cceb0b2eb374fdf417ce0a1eeed74b3b8a0e5dd79f7b9f250c965ab3128ab465f634efe554a715b2678c72fe39e9ca97aa858e3b54324d9f5a4d589dce94dd7093f0b82c1804af39c9fe898c0f917d13513e6a14be265c5901401e1aada8c3b13cd5fc4f6d32b43d4496e7d86b77dc5bd3dfb75e5b9f19225b772f9d0c096b563c57b853b2191593d927d136535cf74b9e5c80e095c49f8f60894a64dd3e8bd154f505917f5a99c9953a25675daafc13bff8a24e48cf3f783dfb491f01786bf51410bc6ccb0bb2c4d7cd82a9dc855d6e797c597eb3370e530e6251d67779135caddf1bee80d7df13a54a6b945f322699347ebf08e7f6894e85ecfe1c45129ea106bf9317f27a1a837a359cbd7af4e7851f3e9fc01fa16b0d3c64ea71d77ea90387463b5e4a043d5af54a7a2d98d7a0408b8344b37a5e01dffc84c7ea9746c6bb8d974fdbe70b0e4563aab03ef74af0b3f35537bed7ef8f8a75bd558268a5dabd718483fbc923dc44a9f90c35388744e68095941db5742a6e5fa99f0ef44a7ffb5f06f281c01e080f0ef1d2297e537830a11081444c78eb82d2210186b2e022232122a1a0c8a00f96338ca70857f0bc77cd6878b7ea7027a317de2c2e042e14403c20be977c3a82fa4279b9fc145bf57120d0f1d028120fa2b1f78ca7148f516053dda5841c6a2152df432991836bca1b82d6c6044be15c5588a3d85caaa6c8e77e873d770ce1b75168128be79b482be8e70665a9a64eb01462c5bd5d6fb5e124f247e1735e58080cad3e0f1e34c33070beab2f8ee2fcd0e5d2846ccf318c79f91d5de7fa57ba74fd9fff97922d3c0f4c3d3a9b8d20f23e2604ee58846b778d2b84ce4174fb7afdabf7e86dc9846e2bd6968b1a6dbea1f0f4b7bd1f890c843ccf5720811c140f0abc2d9127a928481ad8cb7fe2bd2e4cf4f44af0b1a88398c1378bd581aef4ee0e5f91277ef415057636559f9f455350f9b5e1b7949296cfb2d6b2e9ba8e349615db59d436b239cb7f27c88482a35e2fa8bbb49c2b89c3badb02374ad5f016422a0ac489f21f5e83253119fd6e0646b305b00582c1ec09714d36ecebe60ba95396cf78a881f01747198d61690529291f086b3a84b37ce53645145d33ce6fe1194bc969fa9ff773e305b0d48ff912cdd62cec457d8f7bbab4b6e7e280eaa566d635f7ec783e3ffdac1fdbd55ecb7391c25492369f8fa80b8a8a126a9f3cbfe46a8fbe56f442c0849a2f5b212bd87ad8ad95aeaf2b5d8a8b8ddb710edab3d08f80fea4bc61f091a3011b2b7f46f0611231d0ff93fcdeef9c41ac2b728435de932cec4a3f09aa3470c026b9a1b99e699fcba31b681a744b5fa9920e946f0dbb459c384825552350ba4543daa7de7d079554e1fce10254e47ce72922e7d51ff803157057e48b9818f43b50a04e1b6a7c4033106b6a711eadc1a9f121a5f8fce102a36f668ed74a93c596a14eef7577dffa4a96ce6a096ca37bf657af939a7ecc9a64161ba242c556b389cb8c4f11904f1a33e2d41b42e2d93dd2d26f40d94a66f2c76f71a9ead5cdb2be14c5e797229499552ee9cefad67fd8b4db1f74a07030c724ece165bede8a782eb950b8e3d41137720c8cbb397f1bbec6190ecbcaf75ecd4053538a228e9e7d8e277f3e94aef2f92338cf9e626ac48e64f054693aab5bb55d949f88dbd2024f8ea3c6a73c4f0de353ce30b04358e4210efb46fe7e673b6baec7842de20fb2a8e6305253d4eae0672f70ad4b42279b90462b43a3a27999db0909575a1c55afb9c8f446e6a621fad8d31db51ec48a1183ef3dde7924dc9fb7ee1b70bd8c77a34daad3addd7be7c342b79da2719edef3a8895c3c43a3ecbef7a67522c867202c5255149bdcc3e38f6d36974cc714e358a1b1433ef260951d9d40993d6c38e6d6bdc4076b34d39c2e5a307a7c864c64de6df58ada4d8bb4d0d0c29e70e3d3597b52f2b6cd015106f692a69058d8c646cb5a9ad7f866241d0f91c2b7d4a5b5d8a45876ad602cd2b3e7dd31c212252a70c8ca2cd2f380f22ca71cfc92ee819dff8e471d7b0e46af44cbfcf875ceea519514308af5d08db04047b673b45687cea36c3d637cd5c942bb5bd75331189cd88e5c53144f99e8738ee0abc3a0afd7369445b6d4d628246a77c5931592ccada53c5bdc43d591cf9a97e77a138511f77ce422bbb092275032db943d49b7b236fcbd7249ed2320e6e92af095b50dc4a662ceb22bdfb223b0923d7ba4ab04bc829c3d28015e7ea27c2eefbdce96b2b10dc88f2a9bd4d253bc31ebd53f314960e5e96906915561a48a7c2f6e76d766e7287cb67ce74973689dfa068e92856d35f7fcbcd7c7e4d8cf29ea5a44a4098544ab41b04bf4477f29666fbbb6e95b7644aa77dcafc2e69a61d92789ef63d4faf78921e413e04b92bd5dff1136dd99270a20b1e5c43cf4c4e4cbdeffdb8fc32d2a5f6b21b891088b523ebdfacffe41fa0c7db65fa9a8b57b2186a8e24c9e3af9429958b8dcd0484508d3ce2287ae2f6ab50d26ad50695d4d1fb9b43356a6fb40a04c28c5b2dac29832567992c764f0a9b4f49a96314c3771c0ad181809f12c8a4765cb42abf629306ceba9e9f9f1f427e8d1c67a5fd9f7f69201064a2c5ad5f718da082d92f20e8e3522edf6c1f1d44956d75e616ebdd2ca1f9ecf06d2044984bf0b2ebd8de3daf768de3056e79720281e70504718c5db72a4c79eb3edb995f12098fefa6b693ce5b5d9eb96736a5faf5fa47fdaec5d402e254995292e7742a472b9aec7a9d79ec442772bba8fd6f7cfa93b07bb58530b6f291afce6a2bbfe7ab36c256676142ceaae828bf1ec76251bbf1fccd9444c1d57d21b1d646a8b75fcaf857bdf94009951e433b1c63ccfa13bb3bd54f7d6106fcf6ec3c425f7d91b074aee61ce47937ee6239d4e846e452f164296a41fe9a0fb6f46169fd0ec82f8181e942e12cff43d117dda0e471fce544803f0951cc88d087b77ab13ee80574f47b105d2b978753f089cee0df8ff006f68af5743d265d4f7f6087c69289cc42f06c11b81031d0da91d5ff67e6d7129f5b364c12ca8d924bbbf3f9859cd31931f145305e5096fd8a8b6510eaf07f9a7fac96c30a6f75d99864f7f6882699f84a90ee81cdaf3c0cff9e2dd5b93c490bbecc8c59abf4e7e82c2c92ae4092f6506da63b371fafaff3146bb785116a66c65a186ab40cbd4b915a15dd8a56a3acba137cce50a751d913c8ccd54cfb9812f26ff860f056d00a3b42f48d42c08bbfa3eb66f92b3c21f5aa7d3da265914af62a7cb225f4a7f8dab55eb1f20ea434feb9d9a053f56bead8c536e1d8dffecee9b3e0a289579929dad3b9f54db82a9317f32d9b2f7c4f5a88e4fba536ab275967984c2ca7699ccd75b8846a837c3d230b8fb076905290ab101d8d986fbefc8aa2604014c0685041c3e5f06a4cc5461a23d541a2a404add0885f094e3d00f42f3ef75ae12b7f31197cfec1d3448fbd4eaeaf7b6fc6a782832dfc88d6519a0a119586a42c0e2d59142f8be82d58166cd074ed207406c0fcc076fcb0263384f9cec20391c466caef6121e9a4c2c35fff027f4a2e696ad91f25247fe6082bca6ceb137651cf959f73a9ca97267396b017700dda71d7c0e23f12e649647d9f5922bf16cd3f4c5fd2d512ccb73fe330499d614ee300af1e8c246282c0ec7467e12e12a6f1204dc2b86222fffaa7470f64847c02fb0b5c4f476fec914f5cbf9dde9d3ba52958667237ec46fefdbc45c768971392c73d8faf174981eba1fe0e89ca22ef5c2773b3a51e907e183af66e3fab82ea11269abc7eae85a0c64363bd917fe138431af9479df5481dedc13d48d4a82f239bdda8ed22b92eb7ec6173ad4d2cbb675a330dda17c44ce20947848ac2374d405ca41df54aa5e1bbe624976874ae21a51e6ebfe6dcbc385e018a04128f0186968c004287f56972a74f7ca1cde9cf38157470be1a9ea3426de772b3ccdadf9e64642600953285505bddee945a27e0f649bb83f886bf6a526e46af67dabc33b6be08462496052ac58717fb4725073a1f5f9d5887a078ca98e611179ba4128985552907082e8c6466485761bc79fdf5dbb4cedb92c32f8bfe2a096d512f79d7919fcc5b71df17eb7e7c83328cb6570731bde91e5528b4668be98a1475b1fb714ccdeb6e598bcbcd8d8b796be863571c28160cde9ae8dccce9bd1fa9d1ba9ee92820f688cdf36ce57901597f3e658cb9992002c47e2316cbf99556633b76350eed95c53cb96cb38f651d13c4c92662dbb89452379737626484ca50daf1d2526614a0c7ac1c73b9d2b7e6680a029f5268b684660d2c6cdcf57e75e7bc4c9522b2f6664d942649d7995c09b1cdb38331357b2f3d11f45166e9ddafbd62f204306e63d92093c8e98c50b4bdf9b4b85cdc1794af8d727994060bf7199e2c22752a2520a370de6093525d7ee2af1f7df4b6f1f506a184948d6f64689de76dee717f1c35b49bd23594b2efdfd1a3b82e90e0e0f81a503ef1605f75ea92c47b4a1f5b61c68e3292a5ec6e2437aefdb88678ee6635cdc66def3639c7d98462e49793c4986e2237974315cbe74b85de7add591fac77bf51124951c0245c8feec715b5b0a59bedd3cfe1c445e9cf87ab40348c30a75b95a21438a2708347d31d45d721b3b8e5a35f95d0e7b73433048f645d5e3ca6f2234c939261a038fd38539c285f456fa5332e63e8aab1ab4b04d38fb19f82517d957038c84c25b7cdfe7ccb2fdf3acdfce927a76f4d81cb1fcbe929f009c849b39ee8ebe614df70a3e8e75c7e6d24aa21f565514c52fadad163d9f7d1da2b4b887ef9cd4d5f8a0faa892cc31a156f64b6b10b3d4af5e7ef2c2abcb94945b6336bff6d6328f4a3bd386b0bc55c861ad303521ac864d8c880250d6da9e193e26f1f98de71177a5f6e74f3fb806fb2fdacb2e1cc198d86d551b42f14cba76df6913eb3e5d7f286c3a4e06b5d0a9a94f0aa8effc83d0af6010075fe40deec7c38e5a870fb976a8f7e4647f507fa08c50b60e8196fd0f26f2fd7e891bd437c9d29664569f6969d40363e69b4cf6c0d1d1cf347b8ca4733e95ae81e5a4f4524b692f482cf6f62e6f66f0e75e8f37fa05bd7438bc21ba9014880aba30ada71a4d4f4dad7fc341397e6c79eb44737fc8c70f3f1773e78e56a90e683e1144e7fca96f207c60785fb61d11f891569c12c18f5cb651c7c04630f0e47c659e1a8ff6f6e8cffe6c6f8eefb416e8c23ccfee0304ae1b17acc0e199aa374384f7080064296dc6ea17f98cda6e886a6afd70e6dc1894733c038e5b95919eea98ddef371e46a9690eb0d5d1a9d8780889e537faecebe17de7ed925d23d2f55429bd891bb7abea7fba5a1dd6319327e364c74e44918b686fb752f1e10f1bdb13f7bffa8fe060457618540a4b7df547a2897fd7b7a50e62a6b67405058b10386ef3a245e7fe7281b1091f876782a6fe0611417465c6fc52d76cf4e4e450340562bfb159a9c40f1a99478f3a40f7d5fb2a1dee64b80f1e1b1d153ae5eb3c8a444c96078dfa9cd9448fe4a18b08243766b6dac9331730bce9ecbeb376c8faf880110d950c712bc1ccfca6e2554d1c6a1870fa5e9277601102f76af28939bbb6f9a982955c7eaa81396924d67d67e4fafd76aa126a97cae1526db10a530dbc7894e1da0fe3dfd31273d4a80814fc57da49a75e9ae079e7c5f1c01c6cd72f50d32b790504e0dabe120cd5b264f90272601d8d8db1f1e50a1313befbd70cbf0ae680b6f4b99f6066812e2640b953f15fbb76e1937108745b9d5ddcba205440cc7d3062676205c7d79ab72f9c58bacd481afd7cc003734ea076e4f436fc96f1461e2182d5969ea7e355980674ba0efb78f3726458b0417ac646c18b40e08e7f500117fb92d11d5593cf87beb1ee25bb27d2f69b1bca601880d25832d8d8686fad05899a692e703188162c90880ec490a0832dd77d16baef9896e23377185538f2f445efa9e9ea94b58a215819f8a238be2978e361ed72a960fc8f540e0f77857c1c0e3dd8adede6283fc9e9b375528607c3a4627f61a6371daf111cd7b99d96f5c92a5390188419d0f4b761e09cb4a5d3d6b9cd3dcdad494e01204443ca33e0e795f7fb5aa5a8cade9731e428faeb58411201d9a4c325d7b7480b878f5bcce40e3a4815afca60c0011278e7427f86dd86b76fbbcf5ac9538e3ad65af2e40ff3edab47f633ec4e9327dcd5228db537768abafd8fe3d7d5ad05680b3815d59e6f294294b345b4dd38c08801ec9c0556b1774f7e93aca206bcab343ebeaeea4270071aeb6f77cdea80ccbe8d2908d59a04c5eaaf6f3f7dfd3f782db5af8d46dafc6bc0913bf77766fe275b32a0071e7de51d436d5ed984eb7ee435eaa8854687b862d00aa2ac7c0b8c7f63cdb4156bdec55e1bda383e9a4b37e40ffe8ab73da8ab0c267e8283ef4af6c56dcebdbaff89e4e2fd29fa2e761553434d7706e59d85d79104f0688c8d32fd5508b100b785176db7a134a7fbd220a790fa029c398e35fbb4370bf41fea9427b7bb28933835103c0c2790d93a87d995c839ebf58e289d5dea9459a040bc04adb6da8dc46b444b6fd4d9581b7265688afa0ff192021c8dd8d36a3fd9dc4d9904bc6e34bbe7a51c41a8200978cb4db37a2d068f8f5d7ef9b9d1add7126e1b00d7c093c5f17ee8b8f4097f0ce85f47ae2d8f4ac4e0b002f64097171df96a3d5e18768c1fcad82d20f03ec7a0188087633badb8527f88dbbfa972c3850bfc4f53aebeb7c4f7f13ef64eb58d3a7fd2df099f13b7efc573663c480f3216938171df9e6f5c6574eafdfad139430ca246d01ce774e7b7fe90c5e2fbbf357f3beeef3631497d2bc00f78bc042b62d1bbef5b9e4921a93a31efedaa6f12d93efe9fbc7d52c4de7f59c61e9b26fe7628a89833d9038bea733b76e13979611125ecd2e79d249e47b6b6cac0a80b8f40ad7b2bd17b767078591b99b5fed53b42c6e1a00f146e0125205769a5ee8d96ae8e0687cc5970f317c80f2b76e1f446e9511735c4b4cab7544cde3be2f4f37f03dbd8aff3509ad69aa58e463b6216156d11e9587cb00c43634a7cf9efafedb0f71a57377223477eee1243d05f43fa4b4615f13135571639ca7579dd121d783fc141051ee738e479da4f8ec9363afb715eee6fb76214b5ec0fda7a6abd04897fc8e3bc0beff706527c5d002a704803890266cf7526768175f207baa7dc41e81006d0a1920c5d0ba066ff490df76d4edfcfaaeb1aeae366037b8f87b3a79c5a74a86059d1562f1a6508b973d9ea99553f6dfd37bc878ef8bb0ee985fae82d063aa7238dff60d04585070138bad67198cf7b746a2ada51c2af7b4e48c34bea757ebdea9ca1fd266ff2a4e2de133b0e958dd4a0fd8ffaf9e6296209239d72cd030e5687ddb8e15c77303202e30255634985c53d9a84fefd08cfb86934adc470ab89f4da25b46587da5d9b84bf8c811249e5a86f16800d21f574733d939f76699eee21650065e4bb9a16b1f0600c94f24d1e62b504269f4b83262493c46667579761b8068e4d67ce42f37a8bbee2f6920954f601e11f47433e67bfa75a75071749f54571ac1556de5356ec46b1c6c0061cb8fcaf87438c13550ffa4b55e7ed5338cc23e0f70bf5eef976766d048b50d9c5cf4ff106d04bb75eb83ecf7f45153a9eacb82751fad0a436a3e6ff0dc7d2549a709385f1c25ec92df348b4a72ec09cd3e7f128a43de0440bce836789a7b8f55e4d51bbc858c6ddb90960f31278094906568a10fae32aacdfb8973ede56b3d70e6422e012082342f7807a53fb2bf4fdcb55f9e1c349cbcfbb8b2fe7bbaf2f5e7a91d06c8485dcd4bd20354a54e62a8d180fbadb4c5fe73953cd342a62456352bf6b33c8b9d9b00f955957ac2b1d5c0c6412f3b6ee079c350a0c0011a80ffb22358ec5017b34b1bee6384ea6f0f1c1fa5cf01ceff76649b4f598ac3e5ca4e128cf3736a71f8d37880e78c3dfedce67da4209d04c5db0816cae4198805e180fb9ff4e5483d2f35b435b95c38d2e83e61b9c58383f0efe9ebdb38dc44480b7d4a254aeb12aca5ad0bfacf002f00310defb5788a994fdca59bcbd8b4f192eba1e70044b2f5f1af374a3f2176875dd9b8c991e192f5a1741e80086266c8a9f172b799109f619cf0769ba91befee5340443d5799ca001fdb13cb4ca6444675d18feb584daa0044a64ce957480503262f69a5f547e22ff94a4813e502e8de5e36ded10c61ee53088d26c90d4981487ccdcbdfd3c549ce55ef6f855405143b1e8bb978ebe3148f02de0fa31b6fd990b379ac763ee97f3310cc0848c8c70464eaa2f4f141dc624fea704f86a8ecab27eaf3f42103ceb79d6d4cc1bba7ebd432efb41f1e8c6c4dbcbbfff02bec4d14d4e1d9412142fbdf738605b7c28e90f0fe91fda01e09912f4557054e6faefc8b2d0e57c4c19cdcd021c88870bcf9e145b606298e009658acf567d406186c16d95a82045f9fe4f80af4ee33690da79d478059d52f9cdf945aec9f99ac3e029d73d452c1c641533c1a8acadaf0e64b936b17af1f17317c9602886498eea4892bf5f4a1e9fa93d3858dfb238c46ee517225d86bf84c5aa52b635c16b74e956fa4ce5617edc68426071a93726acf2d276cbf54d69f65bf1d8d23a0ed019f45090a57da630ce9f51fad3fe2c1ade11ad1f79d1ef0695a41955e52bfcb33d0afbf9b26f1777f2c05d7049a825405f89168d816379264796a470f7532cc52d05536b13209a47eb23fb3fd9f98df7fac5f2bec08012f380ca5ee66185dce513d02e26ff3d2d79c7c6b4857b0561f29ca697fa0315533c2a2ce0aa7955de2afd684fe7b3fb07401504449e52f00010f2cf7ff6d636f9526c223ebace8b0c00245cc5e9257c4203ef7ccb8000115a96f353aabcd53a34efd913e7bd4e237c90f83171f7c023f3c7863bb181063409ca061972f19fc85132cb9127ab98a496153dbc05b948e659f2129fb7a7de795cc058ff74fc68b1aaf6b1c7ba736ad729d0c0e7e762d6ea7b5e3717ec464a2345ce2c6f03ca50fdae66df48c12d4a63d1a8dbc6b64305a09818421231c84d12a7a00737fbe8912dafd7c50a4f18f3c9dc1bf94c1d2c2c74a1071dcf8ee874730aeff6747813196a56abc8553b50aa78336e8955084a9d87f71c21af4cd0b00c1a8fc97b244607fe5feb276bc220247f1f053175fab9f19df94e36df1e4510ca550863301a38011aae1e07bbdbf2d0226b6fc8c58d30a1b8d424f55d674968084c1469f8563a02f9efec25e51927ea7aff8e62161b03715e118c8c7c3bfb01774328a32681057b1702bf49d5d180b43753db4a3ab6bafea8fcb85786f37880ab77e6646d77726dfd34d202447475b287f4e36e7130eabb87f7109afb53f5c9ed2618f89f782aa29e553b6ec40137bc18bf84ffb575f22adbfc947fe6522b815b613fcf7ff1419eedc0319640241ebdf6c3a7c587f814da7d751ba336633990b5b7ef4846ef45eeb12cd1b80cece14bd2bdb813a9d8499b4b27e742a3b067f0a02d0d9a810b517a770053f7dd1f56db44754ee91b4241700c5da25eada78adfa31ca1de469696cf4d7dedbf7df00bcb7b4cd604531a989b74705a4b419834ea84d3ebc02e814f0dd5706df06365214e9366f6531eb5d695d1404e86cc7aac7592dc55df15b09979c77e65f13612394004ca496a6f25858b2f21eb175150cef8a54b759d7ae0250f0cd856690069c4c0da2cf593ee25f0fb947d100440986cd444bc75d7b62f4944d8e79537df8350fcb2a0fc0044ffe2240819937282936751bd6db110b8ba301e8f4a5086919659d45b43eb4a6b41833f2667230f4037476bb385a9be29d78cd91b3b5e58a6f530d51e5f100f547e1de81e6108f14386214877ca1d309d0f6dd05ec1a8d083b83fb97885b49aff1b99ecc0ca19087840274225e0cddb95feb437048b851358e8faea919957900e60faa672f55721017e4e8d2ca3fd470c5cad7560ee0d5fc9c9a55336dfbca800af2c74184e88ff8a1141d009120c6dfe09e858421414383fe9b5b771f53e248b1dffa9e2edcfc5afe3de647056397672d2bee8f1d2416f10028c255e8f6718c2298af91f8b9f39bb42b28bd1ea201748e77df94a808e33bcd39e5d0a53d97fb6464466f0940b146d0cb34b79d3eef3a77e5a68c6bd07dce6f9f00b0aae261e1bced4aa7e3cc34dfdb59d878656412771b7029ce6a31e52c6c35163bdf76ae63a3998a987f1d08b0c9098bb2936ceeb4a52bd952fbe845a9c7e92c6002501c65ef3df6d91872b14fcb1756365b64bc7ef48ceee07bfa08a64c8757ec66a05815da9715feeb57d5dd5301fb6bbcc59234e2055d0a8eef52b2cad38719064d2480803ac25976e600bee8f7d71b5b5ff9e4cdea38af4801f8737e53f48ad7784174dd4e66eea283bdfc79533840e73c7debf0135f5c5bbf800645525cb4045522a68c02c0a6a44536f3521b75f441689aacebc87933fe5e0dc02677545f8e384877a4a3cafda0dac17e27f9b2522ec0261a90b8954015e49bb73f3c5562c8a026aea79a054009f57879e4b9fce45d7826f72509ca6f210d19196e009d7ef72b1f3e216eeb654edf8dd3631b6decc63d29000a2897ce8ae8579f4bcf4eca4a8d6ad0fa5e514ec501f81fa1ea534bc72e794158724d5fb76b645783ab0c00059c483641938d6566db5103df81e0ca4212affa30c0e3a66c00577ef79a2f713a0e2e522ecaeb86dc947d00ca6e2d66f9ed2a9b342289fb0ebbd1314df52c255a0094f0a39275074d8e330adbd909e567fb8a97af241c917e4fd7e53ce71e781e386ca3a9ad7b4f0d5feafdc60ac0f9c0515348fa45e19b4dc33a3394b28e5be7ad8456c0f6058f63b5278d64fe3ff6de3aaeaa6d6b1fdf6c3aa4110494da20dd0dd221d2ddd2dd2d2088748a742a488922a0d2088a740b28280a88d220488884c4ef73cf39f7bece7bd575381cdfdff9def7ac3fbcf7f0ec3df75a73cd18738c673ce3ce6668e7fba411e52c0a5d60fc8eee8465c78d6d939d953998ebc4bf1c37504f0bdc5f535ef682f5a7d789a88cab44e777b6bc5c9116019f655b5e8befd977c144a2bddb34a1bbf41228d7de032aa33370022f576173ab8fcac3e11e9fd6a8e89be1804fdd6881dba31f23d64cbd95e76024346efde69e0be08b4bb16058a95e69882b64db32a4329935b9095303c657ce39d5b3346fd23e24ac9d0bb4784efac1ec9921261013246cc2487dfbc2923e43ae92292c41cdb9d01050d91eb7ace712de57a8c8e0651fb3b1277f30274aa4018cff2fd73e8d1eb850686e5ed21c38e3b6136ad603c4cc65f8f9f8fb8a6718943d3b5a799e11df533a711650c93467118a997ebe3eb774e1d68d4ff43985116422802f4956275ae8013c46773feeb2a87c9e19be842d187374a116e56ebbf5e59169b5cd60e5945f4bd8db3780af450c8b393192d330ec903d9ac043dade4265f522e0334e91bace3b1fd58ec14d73a6c0efddf5ede9e05d20a1f88ce484efec0c23ca6b5cfa587d3e1ba6dc7b0d800a38e7b6c773ccb122fefcd724bafc21985e3229efb60163e14926d5271d568595ba350acd437acf838bb2002b8f5efd7a8098cc5a5fb25a0966a2421cba9d9309b0bf1321b97573bb0be39739f63ccdcb465b87e777b703f6039be17d14fd5ae3281b1f24d98f67700788d581ac93502e826541526c5cac36bdc70df7285b6ef15400fbc77d97a012b4516eef5dac9058a58b691166872a808a2b3cfa143a9e4f677aa857d2db879e9b35bbf15cc0fbede8dfe226901fb9f1febdc501cbea63b4fc203640459cf45c7ffa53f60a02515ade3538f7a98f7777a572bec6df85636a9019c4dd4f10c665355e8e7f4139c20da86c931c849abb2cf1683529e699eef991e877aa690209e735f45c35d7f7903f917b4abca81c2276b7a0700458cd8698c19b779e9058f572a0896d5d121af1674d040878493c0c87b697ee4aaa3eec3389571593bf3d3b06561939f9b9d041b68c8474dab2cdb0e103ede4ad6060ff523d51a82cc4ac7959bacb9eebd042e623c761e3f2d7b87c361dfea971ff29e75331097d0c5c73ded12b807de7bec2dbce9ce2d77537965075261ea5e2f42013b0ff970bddbeffe0d5588186bc2c7fa586d896ebf20050858678f569f12cad8cb3326e989e8d9de31b9f482540a5b7a9efb6cf2603667022258e3a013e9961ba8b0310b3f69db77e5aa21f2f20d122802775890feb34d10c707fe171e1cb2ad28cbb49c1f693468444f7f952303f7d8d3ff2e0dbe78b7dcabf13eadf5a4ff8c8a9427517e89f126bc96aa651f5073b01a335d958d3b2fda54f25bfc6971e8bddc4eace7ecfd0e3a512f0bca23f78cb1938f8ab64f562aca3c023b35908b5951a4b58f7f17200956f4e587993140ba7c0dbf3631eeb81450594f91700ce02dc76dc298ab6e1440eefc2ad8ddd2fce85658c2100e7454439bd3d5345980ce36ddf45a9727bc1dd2420e66c8abe2f336e1c30e23e62b97fc9f9cc72d4e208603fad24505ea1bcee64c9626937a82e24395a3dcb07ac3fcdfe4f0663eae0f021512726ac365f1b5baf7240f020bbd7b75b75e27d9fe169e4ca7b0b02fbb3a97c00019273b24d82dc6f949add2bbc3200ffdd2791e13520a6f62e1f215d143930778fd96ef4ca875cecfcc04a79f80e36c1af9906d454b03a6c14f2f4879350955fa18e59c73c0742c487209477e1338b50f19dbf76e54328e562d019353433dd24c50e2fb54a9e0c61424db54921c88350b686a11e2fbe0051390f860c71e6453da6b2340cf927bb09728f37be212fa07f96e42ca45a5f2834e468165b0df75cd77391d020398a2bff0f7cff98cf0fe9a791c1bc781bf095d956c8e9f17c71bf8ddbf7296c72baa331dc6a4ff627be3f893fb37fa1f09ff06c50fd0f35fea651a60dca8fd207470f85807eab45cf9c917a05b606e266d5f9fc81083bdaac28c763add1f0f7d57fe1f1817294f1f107faf6ffd4fcfc03fdf35f373fa1fa008ae301353f8f6c23c0677e5c9d06fe8ef42f3c3ea0e6276465dba66f1ac5674fe39fb8ffe8358f82ebbe6b698e5e4c4bca6eeef9f574a9c3d0edb4a23b6a7f69a31819e7f3ff25a3d89e23bdbe41166ee378fd8e3a4bcd1649183b1bf6df46f1df46f1df9beedf46f15fd628865aa391a14a16fe6d14ff6d14ff6d14ffdef979641b0119e7c7875264f4f7ffe54631d283ff318a910a144ea2e949cb9df8d2ee7c426734da657135a0dad7fec2232f92254a8c7842b528eeedd969881f0d3aa6d14cfdf38d5a3e017e4173593e7e735e331e29731e2b7e6e695e7e5e4e4e412b6e414e6e6e7e2b73013e416b69b93fd4bee9518ca26f5c38e7ad1c1d5da8755ddc1d2d69befdd286229053533784a8615fa76c20bf0c97e15732bcffaf3fd621b70ed255be3aca18fcc604f80f9ceb8a85d9ee5bd9baaa8792170a0deb37d0248b59e1dbf0385a479edea5ad86b754d21d1c321fbae64ad67aee4e8655ff8078fdebed7e7da93f0ebf2d4b3ef4fc790eaf70dbe39ed89bb7a188e150044a28292f38c41a078718aef049d85ffbfad967080c05677b2b0b4f2bcb3ff87d3e3b576b0f610e0e7527bd4b5aaefcea9a2adabedc6e17f43c5454a434b4940c3464dc953db4b9fcd4e47834043574ede5dd35ce2b59fc75fa571255d57cbcb68d27354b00dd4f38df6af1d3a3329d65719f8d7a9b1d5c39558b1d5e0bd9ae31e37126fdfdf2bef7ef9f578e9ab530bb3ff5123c5fddbe13c37683119a63bd83b4154ea15f7df299d9bf72b290a68fc75dfdb5b9a33c674be7f57102e3727f0c89bc8427c309e956fa5d10d5bebd528ff37dd4c4c6e61fe231591110b7fc57e7ae42561d0056f57fbefa9fb3aa0b1eeed6d7d948a9a5a6de3e6f94ecf3c55d2768b50395ab4069e3d906078142351afd9bea54740ae217fbd6fd9f7116858fb2aacfbe660cf8cc5d4d8c60ad69993e4d5f27dfbbfbe0ef55fdafbdaa2b5b799a299b79381c7b55b77453b4d1d2b41352d7d43457b67397d1d2d7e132f0e595f395b3e5363faf67aee5a5a666e0a3efe6646575e92fb4aa9319b895d2d19e637a3b4d72b658dc33d3963be8b1be1c17e56b821ce65cec905b3245a8f0beb01decc7d4b934c648bcfae17ac3ef4e1b3565da7ef0d07d6a9268fefb04267fcb77a9fd8f832a21d744f5a6e7cdcffd76d2795eb79ecf9d775129fb617b7852124df0eda1a050d4cd77b05034b45ba8b5adb9a869df9b07f8bd673ff9bc3ae3fead2d82c9dbc67d369f96caeeed600d7002396ed2e01e1577aa6c7c16f3d9dcbad32f6d2925a8276ffcd0436bedc83cf12d7aff0ecafa3f17459a41c73a94e9e32f8828ff61b5eb943747169d3f3fd8686eb7f6262c78f23c3f2c85be2aeb9e4d4badf2b37371d745ee949b2a7a04d73e4377e838b8556b7e940551bc9d92e67c0199bd63a15aa745afece9a513b1327fed051105f67f7c41c4d1303337f7a5d6357374b4f23cd68268e0ebc5abe569aea825cfaf2563edcaebe2e4a26ea0a9a5cea52d2775c14947d3d6ced9c55cdd56d359cec6fe4fb4358e792a9538dea91e26c57989cb9c5348884f40d0d2ca9a87cb9ad78a93cb4ac0c2c2c25cc8c2c2d29a93cfda928b4bd08a4fc0dc9c57c8dcd2829b8b8f4f50d05290d39a5388c78253908fef781b9a95b793b0202f1fcfcfda30ec57c657d3f889852b03f8a9c52798c216dd7b1225197c12b33e3e6ebdb94fd67376c4ec3d5bbe3da6cefa7ddf924001060a511ecdc0c0eeee291e3d349b4c7833d486f1cb7e41488119c24a2d70acfd22fa75530f7e69108df46f594fec7c3f21ebc954d9cc96a2f5c0f64adc793eb97aacb097964b00eb9684ad0fd3399dfd4dde65ccb5e19898d35407f7803254b4f471ca6563ac7bb824d572f3a62d639c171c0980e14c174875c6ae6d09ebc0295f4aab344a5d3c06605debd0e5dcbfc892a8c653f0c8f6535c03c598d510a08060b75f17d5191374b1e7a339452743fc4b9935524089e31c7224e905d5863cb5c1a0d5ed3a6d26f3675d465fe3f7ac659fc9f09b0628d3147cf2d14a514b364c0766487b17d68bea975146e51ea33aa75cdd49f3e3cf004a6b746afab30a8aa5fd3770e583453d2e85dc1e1907589d97e8cb2eebc77fb87365c3cecb6fa3ee7d5cba2e9075e21261f0fed4091bcbab28285131761699a68182c55fe306824f25265f7087ed3fd2f6e757b5beb71190012ce1b864380962d22195b0fe20ea513cca36bdc2b313c0fbebe93c3348d976e7a1e6e506ddc6c03493f5e700bb2094a21831db219ffb221c8bfba450796e848e09a08b9a3974cfe3068ec5c174aa841bc95a9c421dd59af7d7b8d31e15ce739b381f91978e01e96ce23373397680520b95ddbcefc98d76e3a494d39ad61e1c2d4d39cb8012dba8bb44834992e2e86d4226fa8fe209022ea7cf02acf8c97cbaaa5c564ca5f0298317160fc3ba05e963005638a9f9eefdaec4c32dd4f68cae46d6204fd4b542202bc283dcaaa52e5532c309f7e5680bbbc76db2ce95b35fe37e7c7886167b6c3712c3fdb4f20359b2da333232bec6871a5cceebd38475c7ec711bd19995222f2f2c0089e6dd8f248dbd12a6bd1fac9fe4bc5b72f33e73de16a014861af6e0f48300ed018fd1d3a2d8c9c9cfa2a30f01a5b0ec6cd157671518cba3ad9b4c93f1d162a4e94f004a4abad2eb6bec18f21ff977d9833607b490df5347797d8d3b9c5c78d7b38d3d65da3e5e237b59d65fe57a30b083b2b3298b951df09fc81e7e381acddf7833e3c62890f51767c0a4a96d10fbe2921f068b0911d7994b9f8380f74744ac199e8f5d27c5687fd2f8d402ec1db7c90560fc56f493d37d4a1a9d6e121dbb9a1910bc6edf50c0f235ee96841018dcf77dd0c579aa820b8dcc7e56e92590b5e06e77bb23e296dd330625f6469cf5a89d33e2d5c0fd71981cc03f697564ee658ad64a3191084baaad03f3b3e6d9e51d148b8a8e3a4a6d9157efd39b5a9831815a2e4fa7955492c91ef7b917a87b7b27d13612d8e8004a5e0e76ec0773eebbd6e26f2b2e0fd3c236db0ddf02fdef743847bb8fae52c77b96fc2d4ff13b4cd6105a206b4df6a5e30971d378dc8d1bf00435d7e10f2661b50bc0fdc3bd061d23e6f9db6a6237c8abbd0fd92c93805a27af3ec72319b2383d0f9649ecba3d573af5eeb51320544030ae57ddd69693e0662ad250cc4dfc81dd6c0460785f8fabd611542ac72dd3a1addb1227723c471b05ac3fd912ed35da4f6e79ebde5066b0517a91b5178202ac0f634228b61c8245aeb1436f5e9f7ade59f02a329bfc6bbc449bda2878b4c1b79c2cb59261663bebd1a9da7520d013accf705185c5b3fe19f2dc2446dcad3bd8ca80529698b4249560e615e357f528966ef4ee66c4483ec0f8958a34313fef8e09bf431b2efa320d3bb6c1fd3190f567973ced1c40ff129d2470d4282da4e7be45632b903575e1965e6886c13b0e26d80685ee224d23aa9a18e0874be0b1e0cd784446edd010208e961623e42c1702640d8e4430f0dd61523a21c8656cc1ea9425c56f5204cccfc2d854de751db2e61cd778a6cb5f5878d1cf940247692a6487765385145be11764d76f7aa4b16848ac014a99ea9542e2a7a8dbf5bdcfe1c1e596c9ee7dc0880294a8cec4b854dd0f24a69c9eca4a8de01422c0fb5c0164ed75df9dca920ceeb0474c9dc2b9fd31ea653ae901f0febc130375d4a5923574aee735677f6ef6bdf1881cc83a29769c5ee55e3b4131ffa03aaa63494b3c5424b4ea6b7cbba0c209ef8149854615cddab557aadaba296ac0f8abb99e4851e52b1efa287a95b06f80256e24ec0e903538ce36fab0de7e14b543fb49ca80b4554c803c1160df3d57d0bd63abd62f117ab7d355caa881f7062a121db0bf49a0f62759ab4b0a7e9e2f5139176591d5790e900a28ae633519772dc79b619867b308384c2d5a7d07642d05aa05526e862feecf93d0e0050d6a8870688b03fbbb27b9d89b3eb42f8b4636ecfb94abe63d25797c09c0efd775177f6920b832773e9792bff259ef072c7bc07b913a2824721ff5de053537492a4fec4bbdf26d0240a03fab60794a89a69abb8b639600c550cb31b32600583fe46ed724d7bc5641be55ede099701a3da567e51a90950223767f3ad3cf0c2b95f5b0783c46618ed52cc1f535dcf796f1b20f61c653c6453cd9ab6e85bc97bfc8028194f31893bd71f597b0553455bb67e6373e922aef01597d281bd6c8eafc745d437a35f7e7fc8a2c7a1d328123e83e43d8e043c745f9834c228ea43d61d5557d5c60ff0821fea82f2c7052eaf0926819fb4c85f9287518a0142ce064e5dc7fc1ca1a672ba36d5c75394870640b18ffa54c82b331b1221a46871b2d077bcd28756fd1017b97a8b574c6348ff68581aae409e3b9f329d4fbb9404d20b8937ae2556afead246ec66c338e60992bf67140d6a53e5fef81b3b62faab7a5f6ae83317a9e9556c43be0f913d0d1664771def8be17753daf4eb57d60530d6845e7204a4794300e07521424bb9c733e5e7bc36707b84483b1d2d9bb15130448f55f2a551a2bfb34fa9203597f4d012d9844d5d42fb71edd7db84a61a84a147c0f605f1689a0be346475e14bd337c474c9b53aa131a008646dbdb6204d2d3c5db227e52676393e276426fbbc14a0f437f5416f9c1c8bd6f072c56cd23b46efc21b8f7080df4717959de99b51bbcaa871efc6f2f500e2d53bd640d6dfe5f9db7976f73c16a4d0be54329da12e1b10b806d857170afcdcf8302844e4e2895fa2f9f62f35a9b600d1524dffebd4697238f9ef39f12a6bcff176f45ead005407deef86122ca80ab20f76bf88b5f57c54ba7d300dccef27e34eeb6812feefdbcb9f7fb9ca2fcc258aa60304e2935513522aa5b41ba82a4ecbbf5122ee6bbe2de305df81bf8840b4d8f8ab224dfd2b060a6fa27a858473bcc32d1c46cea26c6ef23aa11951baa59a9994c6c479b1e16ac6b6ac763bc79438f63a1b21c4d91b4a6926e8cf3c89b2ba5b5ddb16a5ae14c045d3d772898bb91077caf6bb1ffeadbb7edc220dea4f76aea09959b8bb7878fce1efe3d97b39b95ab9b35b5db2b03573b639023b0029bfc09f8262e2c75e48263ca840db21c405751b03270e96274a1f7f8ce25ea2ab1f6d7c63b44dccb015ec5b28ea28e2f6aba7f8b7e84fadcc7add7e2d60491d9532a3a4d9d504df816f85139d728b56fb9fb8127c1a11e6d0724c3710d4488663631c6ba41c53d30992f10a7c7f8dbbb14e57d0a6cf7d5dcf882726513c4dacf60714f95fbbf328333df4696509affaa5fe7be7708cd430ef3d54c6ded1ffc3ed5fc1991c9a51a63be6f31fa9ffbfa92df8d5d56c475355b712e423e0f9dc31943bb687b6a5f8d4517e0ceafb7f241ed1047ff6afc9149a9a71b9a809bebd15413dbf467afbae6308b6c34a08457b59ed74cde40bc0a339da7bce758e534dec0ae3d34a74c184f4d61a93bc3fbad861798afbc9d44d7daf86216a234d5ee5d9fbe957bfdb0295f483914befbe2e1c02532854e0783b73afcb7adc4492ebc4fe41dc5ddeec1f2f6a5c2c4df01d24dd70830d7c3f0958087c47308472d1b40e49465b8da9e827af024892d25a0aaa2a269ada6a6a4afa262a925a0a3ab2265aaa8ab22aff2bce6c20d8c32ef3ce736b581c4e182cdc4bd0444e301c0b1605b86b11adc5508a89d2abf1ceacb22084c676e9c60f9effd7eefc85a255dc71fd8ce9bf7a36a809be83b2f5cfb8d53f85ee7e4eecca96c1b211775425280bade174d1a6ada89320cd5ecfaac0328f835967d6699a6556e25ab1c24bb362fb6565ac18e22e2247895d4dd768c6b2b3b487e4cfab14f94d0cbdfddcd9bff977ecea2f1dbbfa1383f97cf2362a661e06ce966aba9a2a8aded2ee2aca061eae0e7c32362a1ed24252ae5cbe6a5cbc3c6eca7a8aeaff55b12b336e336e014e2b7e014b3e5e4e014e6e73214e1e6b1e01211e0b2b736b4b1e0b7e5e735e41335e736e013e7373730b4b7e4e41737e41735e730b2e6e2b0b4ecee399c756de4ec25c9c3f6d7c48aa19478e9f286169da291f5fbc421c5dacd7565cb159c84c3323a2951920d1f2dcf5a3dc65ab9a04e92ae1f22d1cea89c1d4a4d58345a52774b4d158ac862fa987a0c90ec341a1dc1d44d6bffc1309630c264a7e6d72b209be16144adc7878f8cb3f057c5d043cbf9574e27aea3e73e7ec3f6b43a0db3ccc527cacfc8fd7d4b41d13148a9678fb5c6dc80f06065555b601f561d0f7df2b4acd56118950d7ab6f5910df514cfeedff32797fe5c7fac7cd202b3a04d486bc198afa1e258aaadbc991dd2ff50737f3e82403f2fa05acdf7d33b0ce7fdd8cd9d7014293a0d07f1c3543d13961b05a756b47663d38678e2acfb7dac5a26fbc33f3adc67960b07f40674ccfb2963eabf242f8593b12bc592436cfc86b6dfae62f70e484f07ef317108d73f0effcc23fa06f91419a7690598342388698eb901184774581d4816ae132473e7efe11a73591a87a19fa67efcee1d71fc518843a2741b52fcecb4db07a3eb052787d72cea31e899a64d68afc7beb258a6de6b296ed8fe7840fb90154654494bf925be11bedbbfd10d584e5a2cf340d7dff031c30582f2cb76bcee85f7a79b4f8df6fef260ca6fa7d4b007e6587e76478bfd68ac5d3996ff28cbe353cbedf1caaea9a17dda28af52fcd7d6b2e1cadb97f5c5ecbf8d5ff682e885bd2e3aac243a504cbd7c4bbd41224397b4e4302df9a27040404845a2b0fb57caae36b2f8e326d09a7a206ffdb67b61f46e030a6e8a389ea8630ab26d51658fac4527df7067efbe4b13810f004c0cb0755bc312ce0f5555af473f5ef3e930e3852057189ab5d30d8813f8b2051db1abb0dfb95bdf5e738e896092fb43addd6acb57eb63751a2f885282979aa2e1ff1c568387649872677d2f5af34932ea89379a3625d4cd8dd36a740b23153c5abb871f9bb1ffeadabfe1f77d011fc9b838edde9778f384f3abfba57557f6507dd0e5c2928842a3bb90e2e1345a52225ac827082b1c8aade8a663c93e63574e7885df5ddf421a4e7eb64c269905620ae8d724d658cd025d7a09e534c1b24b7bbcb66f1f10ee3eee1f66339c8d7cef151aa6a65eee34da750bb3c944dde3d285c1b91769e4e966d6311d69a4db3f06946538d4972ab9f23c2f60c984ca1b927425349da50ab682eba327be39493a1fd153ecb1dda32e4221e0e5db3ae8a54d4018c469dd3537d0137abdb45af6cd96ac85e1e708da013ab1015dd24510abae78876d8f498349db6fae528c6dcadd0aac783fdd84fa56d1094274f4ea73f2ce1bf29bd8e0b23601145a89fcbb74d6a173ab8a19bf9691cd1e2e3ab32603b581364ae30c68186d17df5e32bbbd02f6cf6a91cbbf846569b218636a3741a83fce5cfeda26a530973330eb17d18390be608d9b86b67983273d306769e789f44438fa012a55d618ae4422ff10d78447033978dbc4258bfcd9f88bce6c1c9c882b6779be49cf4a359ab743049ab44895a0631ca424c7efe97aac4eba903f3d30401f218770d7689dc9f977e099cd1cc88b1cd2c290b1c7dd8526cbcf179d6204e30bec30bd5530bed44148fb356fe398c6b1497023ab74cb68be7ebfd1b4549fb1f6f7b9f60dc3497ce1049e5c168227bb4c75585fa7afd71358dace2b5f7529677da6d2fb7a46b467e14bb6ce27978fe1227d7aebd74f039c7da6dc57f8c26ce5a993ba8d940759f8b41996177c99833689607b1242d3c0d2ba6cc437f78ac242ace836fdf0f0a2108d80a917120ac2d1029f74c38e6594a1306437a7339c037e15fff8b7345dc3a1801c3b176641687516c5a3b32adc1607d11e8a28c42ff1cb9ce78c804feec1f2f0cdfd7e6fe8f6de9df787dbfb1d210f53f8195c6ad57c0bbd6ba5227f330f775ec5046030ebc01d0daf5e2fecc3743774895ca6eb3728ee25a873966f1c0d7f8c8066a129ca8fca5ef357e5fb6c3c672ae4c1cadaff1f9e19d65efc60cb4fdd270f653e1dc8605fa64e15fe35886575fba6d72ba3f2b2e27ca19e3f13b29b30dd42fa47f8b6c235e1dce793eacf0b5031697d6608c6d12b09f1af9aebd09279c8e1ab34f3e637eaae353716de1d7786cf1f392f22f971f4e4a31dc856335f99d99bd0c84a662def5bebad45591dfac42d4b739266d8612036a6d270b893631e66f49442edf219f4e767fdf84e10bb022f0b5accfe1e5ef540ecb9d32ecb89fd58119420fac9cb4eaaa22bd0b57cfb6de2ed6c8953f9cf2d445076c448dee6ade7abbeb170776aeca868e687b176edc036ce8bb9c212f3a8c24da8c9bf02b087df6b3712de756bfc67b4e4bdfe0f9b2dcb3bdd7b08629fcd6b1d4d91b882abe3215e7b219317eed3f42a5ab853c6e217e7f05f06d95164ac98a8977a95e1ba48a5f1c4614af92c400157e93486f85505ca389ac228a4430b65946c89a7202e545f07b9050c6ed2c57277dc3fde30e3f34f09e9105eadb19736fd96be7cd7cc87bab9c96cfdc74682ce007b09e0a7de764689cdf382a63d533beb0598ab3820d025aa1038ed10b2be2a5ee37a98b9879d05bd285cccd80fbbb5412564e7075258bf0f55505f5b7556ecb67c40156c5c912f508d3377cde0c3611435fa66384f6bd822abfc69f64500e16cdb0892e7cb6bdefb87d4b5ef55637a0b53dcb1c286013da7020be7d8ef2d5e90f6d077acf81fe2752a16cbbe5b4e4f219cfeb813445123cb03ed71d787f5f982f45d6f204cfa333b98834514a1b11c6030113c9d19e32c1dba256c6840fafb558a7a1dfe82f01729389af68f1f833dbc85c643fbf833b7ab2e2724530b05eddcca0d3b3c6d43234f4c7d93bdf59e87c21d405d82e37c788ddfc90d69444444e99bf4966bfa93f8e0668957304c99f11d6c40c8c4830a22f402778ba933903b0b26ef4de08930dbf47eca6fe76296f812afa133e0b707ee17cacdd6bb19f234bce77bdfad1a4cff0957a6780b5443c844d3f22c9a2b7518f8d4d9fc5ff799fc810608d2deb3d8c4b56946d0fe13abfe58f32c349665d40f4357e6ff7c0a55516276f4d0b9e9c2afc5a97505e157071d02fd47d565a4c9f965651d159d4baf95995fe1d10ba2cb6b8d8dd6326e8bed4ff8c9a37f7c600f20e0c90a7b0271bab9bc27442262756cfe9c4c64d2cc467074a14cd122f933c6ed3c390370adb6fbe77ce99d04201a8cb86fbf4d63d55d2850f8979b22eea22e9359f4c2b8133b2753bea2efa49cce11a623cf984974f09c366f2804ccd47952f13fb323751e78c4aaea50c766e8f959b02397e21671737a298fb6f979c46d23e645c0bb9418e0db06a84594f66bd7552e8982e4c2a51f5da3d1d19220c3c9f8736f640ed6e23d9bd996a24a79393562c855f80fd63d0faca041d8aef95011266373acdc72a6a024600eb90a876ef62ba976be8eed8a38c36132bf782db078016b1323af34d9298d6b3cd516ad4a46b141be73ccf005aef697749232ecb054d789db4c291de9453fed2ae0ae4752851567c10cea7ac25697c779b7480fbcdf283278096f9bd59279f96c0abae6c7a791b175c8b50dad4dc4bbec673e81df2fd37717d0a499666ad37e38a90e05e407de08ab465d66b78d6b8772f1192ed7e285cbdc2cc09b03a90099669aafb1abb530cc425e6572d3d6e89df020ab4742c9589798645a5eecc455c8cbc44ff6a45e12eb0fe5b768a088779bcd2fb18898248a27b4877b7eb31f0fea2a5a754b7c218c58a0f67cec639e8d8db60c500272b1f5e7af24f340e21f8ddd81443f71617cdc86c81f1139b1de88b3621729658b755d72c0c95b1cb440060e5be37652ca0db2bf188b8d7f72e2703c7a3c6ce076025b7c1d81f94bd492b67b642cc6489984f10678702fb7760bf54faa8c4defea2cba43bd563f57bb5751840fd6e9efc619732855b738fb98bda2adf503ca1525900d667f58cfea27c7aa625b22612fecde2b5d73b59d5c0fad0a7e9b1ab2adaf7c8964aad019b29ecb4da1e1bc0fa661275b5b4ce6246bce619a97b2a60503838eb63f035be789847dbc971510d5ff1b69cd19b6dd9edf079204aa86de9c5e55cabfa22d1b666d629ab9a0b7f8d1b60bdaf5947b5db35455f254de41b9858f892d232940bac8f1f4507a28598c23d63985599cd1ec8a78c5033097c8d67acb458149d33237ed1c257ef8cdfba675ba704c867f8c6dc4cb812c1c437d14758574ab22739b6600db46f1634a31e41cc3a78d227fff4879c16c56d13308f523fc483c7568ee2156632eda98f5f88a43774d081fd4b982e3d7729e75cf6bbc34c3382341776149e3e2014a79b5589cf122b1091889a69937199b06097e725b0bfa3d8f5bec6504e2c1522f264a68c882f7f3d95087894ae060d7bfbf444f5126ea14649f6c43bd92c2f03fbcbebb5554756d4cbc4388c2e21873ba70b6236520097cbc7278c3ebcf757a88b9ea9d69ad8a150736b6a00f57f909fcd5b06d72c3d3cf334a83b492a8a48d7563606086887e18ef909d08c45adb9ae0b8cd3b0e120bf04b4ac3b140af6631b2e8e644fe8e045f994a95e7a9802b0d6739a837cccc8c2548b073142ae1a0cc957e00b01ebc3742d8c8e15559e3d649ecad8b72d8291bde01510508d361bb47c5d60b43ad12ee1ddd1da5b31dd8902e468df696630c2256ade2391c4401a8bcf55777e9208acafcc7e12c2b39adc5d98e2bb58ecb996fc0726e6807ded85a1dfcaa99b54f79a796da5beec9ecc8e120cd8dfdea739e29ca0329064b57f10cc811c61ac78e910783f29fed22685333a03af7c12e4b8fa5d6e0d453301acafd9e8fcecabfe83cf48cc4dd55962309aa46f8802f60d65401e5142c6a5bbd23a1bed8143b4b832cb98408ef990a95027e7966210616c0a4cd69ad4d383ea3a383f660e361b3fdf7c7a9bdb231e077bf5cacbe472801a95a7e88e11edfeb92bf6d374fc8ad5726c6e6b1530c0575c7d6897b0ced20f0987681026508acdecf767ff5a4b154f4a22e44cad631d2a8af7c5e02b47f176490b20dd480a6c68f418f782b9bf72a49cb5ac50fb33e365f6cca5ecd41fcdabaa9c55582ef2b26bae5cc95585c148ddb289fffd6bf148df22797ce3e7a98fe46d83331880f00b082d700340df1d86df092e323493109e7728d9bee3c68b910a16ca8c348f599b51e2781e4bc873f191c688b56d83f3a28784a272a18f918f6a792749b503ff313b094a25e73fbc1f7498dba84fd58b2e87d121a6356e686c4f8b5284fce8f9ac721b7e9637b4ea068ca6d86e6d59d89aea20364bf283b90cc3e97ff353a8441fa5bfad1d5983a17ef4f4a4f8fa73eeae2ab9fc2d15b654767bc704beaed472ec4ca2caea89e948636352c1d572b100d335dd809271d3f026b60b1aed418edb5bec2a14e8b3f713713a8f501f57ead0b19d4753ccbc1173fcd55294218d21c62c3e56eac358af9caade7c9bd3705f5aa3e15d26e1f94494fbf5baf1fb06daed940265252d7d1df60b17aa190e8408c728a1e2c1cf237047ef933249c042e0838ea1d8709ccaa026f8b67a50087cc03714eb40aff83fdd5f9b62054dc28936b4bc1eac63841c4362fd21b41b3f9cc683cf4d01010df853fb10a609cd9f24a0a1f968c4484f4ec6a2a0e284093e7b1c29ded5dcd7edc684f82c8ce64b6cb0a8cc99abe85f8ceb33a7854a2743d3749c7f4039fd4fce8d1266f8c0c6840825f3b3c5ea3cfac3a28ee07013a8880804feb780c6cfbdfe3cce8d9ab2b6b3b301afbea087a2acbc85bb8fb7ac3297b28d9d3d0f8f9b8022afa28cb5bbb2b5b69914b7a496b9f45fa77f25c7f7c74f1b8ef61af27e200e4d7c4a3898917580c5eb82859d5106ebd35dcb5f62cd2512942755a99ef948bcfd20a7962ae38c9c14b1be062ef98eec84edf9c3b8dfc1298940b9ed8ab83d090bf5e14b8685c1b5b0a76141bf704a485e3f16ffe59faf3925ec6e1bb8cd23fff4c7a22dd856e3673307fdd2d24824866d9fed0bb2205828f652332c0cfbed88d83f9a1a8e906519bbd53ef96f7f1e0a0ac5940dbb1986acd890703354d60de609486fa45ef9a1f4869ca532f133d9be8bdf94deb89432417fbdd6b47b36314bfb28ec06a8eb0f486fbc3bca43c160f478daaf3ebdf9762013798a9fe44beaee2707a9688827403b8ad9d2a997cff40455642ece5ca8b5805724be91469ee5773cd47f9058f4e11ccdf82bdf26b1ccee277e8fc432bb9ff84d9acc764c50c8ed574db5a186927518c734c3be1be52313bae68df47d6d3cca6565f15ba2bedb74dfaa8efabbfe341c1442a7a41c1f0c87595486c0602569e1d68eccac3f313883b1af50f5ac691d9e1fe6b0f8b6d330e0da8371409b33de50c12debfc7595760f3fde593af7ce61095cc0f95f2d3d2a5211d66ab69937193f579b924a5ad50584c7d404b15d0cdfc559c455eede5ff6769c2a245203d8f6bd6c1527d6c3c819890b53afe1e8aa5923bf7d018ca692c289cb5b42e4eee24fc2efe2b914bf913fa902480a9c12fae0b030b3f061f9bdf945a413f035f7f770c07919303b8632d4aa181c3d59c5bb8ee7253f7f3d0f08ee08bf9c2767bce647afa59b56da51d5d18717ff19c0d7492e8c2663dd9b8cae0e3c4439ab68f14ca04cf46bdcdd385a5c6c1e45b456059daf79c1be1c6ddf0b4879d478465653606befa760429f60b1cd79cb5a8c1dd804eddb6722150b2b544cd7deb7b1a74bdd6abdb200d026de0ba1588bb2384be5295cbd40dbfc5139538b11708e726cbe1c222e160fa830089cc426fc9cd667292d0438ef377472139896c89e6fa4cf6f7c19d00d5f1c03fa6f9b7dd675b5df7f636113eb0b5e37f61dc7795d20b8b536de95cb941f4ed0ee497f8f4b9e4debedc74fc0f8675c4a9a1790d48fca6e279af0610c959d5cb4040a79ee1ab39a9ee90e14bb2eb0936c1248aabd6a130e142a33c8a61611c34973dfa868bceffb969b70c4ca19708ea645ed5388c0b159140515ae178d1a1729f64700ef17ee5b11313aa6feb19a42a0125b7f9871d51e01cceed84399d7e3c1b42d14f5ba2212b8063237c5348094ce67574b5c977b0bcab9ec74f3a8b91118e2dddd8024c6c1f517caded7cdc6f11d7b9e08048cdf1e9b5b06c836fac124e1cb89571595c7141ce6e45669946f1d0285f6547d5eb917e6af902a4d4cb6efe1593d546cf800f0fce75a2852b0754de6f8ac33b6ef6db06fa5a0640329d306372e6196056bf279f821afa0e3ddb6bc22f60848092c464f598e6b27f6136c1dca264f5f17f19b6c019c9f2f74c56286589eaee8cc52943d328c72cf6154008c50b717d9b39f52636e9aadf5b0993830de9f4fe5010ef24fc22ee81ccc47e972942271faa54d3cc0e58a7ffa353e91a9771e91e67995babb16e55660856e20652de07c4af440a5d9b52e3ad03c91169fd38e2f377c510a20ae2f0e4794f9f6f353553f3989af74da989831ad681008ee7d4aca7aa75db014f6481fc7f9cb4eaa5c230220106650eb7bad9cb3095cb2e6c3ae7f776f04c1fb0148f94536a076dcdf7eeb5ad02a9041e66cf0c4d1b719700e0659e21d6a0bcd99d9e23112c93fbf4a946e5600ac6fb7ac2e1799a313068cc6f225d3b28cf909599f04d6c71bc54959f9f9527ca2941de783bdc8ee07b7bb03ceab3b099bf5cece35ba46a8f64fe8924ebd930cb30082179b21219972119a85019b97858b494dd2d8f9d400e7b937b6f1e94f8a81987588244abe4b6d6279a1c41c40f0b5b76ab5ffd30315b9c0ee7c0e1bdbc0b0f3760059058b83e93e45c85856884bf5c25cf21b43bfb0052085c6862bb3f8632825a5a92beff0ca80ae09d68d7e4054ebb1e9e1939ced4e01ae65157f838c55da894b5f809d39d7afba5013a66e442e7b75fd15dac80b3c0c27607e520f74e4d9df5cee915f1ca3ab276453f8d47202581f189ed1c6541266e30eb0a3a9c5f0e7a479eaae01f71fca70f39a46627d735e5681c4558428beb51398b26f7f73017b29a8ce519524565d025be851c1dd36a010b6e4ad7d7c2fb8193a67ccadf4799756f660cd0f8036bc58a1d150e4eed084153e469b927c8185edde3bc0c111eb307307659ef033c9dc35fed4f899e2335c8f8142a6dc1c3bee95be2f221e511cfaa0b8f7c55e105e00ac3126867a439da6c3345696cf4341d37c681b850f81f1a71655765209995b41b43879b940b00e5b1f090eec2ff429dcf8812cd20b05a1cf4ae2a5939771d09481e04451bd13495b3715b34c839a313d45840415031d103cc043795345529fe9e562f74c03ce4aecd7a5de0abc9ffda425ee7dd132473c5fef8c36b2c9e42d380c704ebb6cb2fbbeb4bb60c5a86f9f7ac9405744e1012950e856d1fbf508fad507c97dca7838d1253cfec392694036e3eb3a2abc898a5a9c47860834ac593e6307c31a607f6b11671e36ab96e63c1bbc1247f40a595bd0f50630fe6219b76457d691ac732732f70a5fbf431e33c203f6572f62bc691c4214a53cbe941c5d5dc7718e043a405243b3b5d8a76c3da94653c6447808e37a581c5b1f40d66cd61d8dc931db8cde6377a5668d6260c8a322079e5f10d7af3c8ced1d3e9759edfe838c54c974bb1751809380de6698b331bc389a6cf246fc4ac46a667c112069d413a2ee5d1cb3226e14c2969bc0e068f8cceb0590283bb16afbc45f289dfe89da0cb2234161a462de0980bc116e1d39f298e3ea2c55d659e50ecbfdfc68a977c0faa3c29a222f441ee57df700feb9b4970cc500cf16581ff96aae99a5676c4da5c77c4ebe8c69d7b16edd0338f7d7169443da5ca2d81fe11a3eca5ddca6270e6403e6079ee85992e449cb16cb2af786654c3523a976524620f89671d72768f6320e376a4dc26464938144cc19e0f9f2fd255f160be68454849cc060a05f59742a3b094886d45c464abd7c7a54a4d7019dbbbbc38cb24e7e3aff6bbcdd7797f355b67b85b34b5d5883275a17e7012610bc699125a6db56ca1ee5ff8cf471f051cd0bbaa76700c9936954170caf8fe793bad54eaf3f74f3169cb29702d6bfcd35bd94e1a07b665f96adf33e3b900ce287cf0092046f195e30bf9a407ace76337f8ac038eadd3ba16a60fe0b14901c8e3dc58dfa92a670edc0568dc941cc05587f94d44fdf2670d177987893a11a624f82564a9e0e481a84fa589d649cd651c621557ee0ee246f212ab10a489ebc529fc7651fb3731193d35dc3564bfe348cd103d8078c6a2f9257f12d2465c63a8817878237e3c7df0384514fb2958f86a4980ee2a783f3cb19723f686502758a619f04d4dc90f4b823ad9cca36da7546125438da3abec6cd9f0eddaf4316569f2f0da7d6a92d925918ae03c84d6ffb08956eaf4b6e12069daf56b6cfb7cacee804c859f7ad8bf5f8b4adac2f1fda8b2cb8bce5b3f15211fadfd3d7b588b710ef273d7fbadeff407d118d86dbbe206d2e35f6053e525de8997d054146bcc9b3e8fb2a0519af3b692aa65a2bc88fe21efc84a9a2864e31975e3b7bf3113b77c519855036e2bfdd83ff57dc835eae667216565696b2d2babc17dc9ce5d4acd575342f98bbf2eada9ab9aae93bbb59c82bea4ae9db2b496afc85dc8389cd6769e196ce13598fbcadd98ccef67dae30ebdc0f17691e766ad1c8c5b5c6e4bbab63d93b2cd72f1ff15c5376ac2d5510cf3724d62cc0433b58364f6020ea3eb46afa8fa309429b8302920348fdd787ecb1a260be378b77b47f58ee012da53be8b7f4a4f8bdffc7d393b0717c577920521a7c687ba261c78a93fef7a52751ff40e705223d69e01c699ff4b9d83f299f0815168ba9ebdd13f327653bfd62bd6efb74fdd9e9493b48384121d4c1c6754828b90c826247990e7fc0350c30656759d8326303dc06dbd6cdba88b3c8a93f607c248588c07e8f4baf8596c8f3e4ca0fb4c971824c58b2ebef85400d48f8ce8984a01042e3f110aab1d6ba13a1e4e90f276df1f327efecdfe518eea13a79aa5f8530d27ef6666c754950ce659f271c06b4500ed69f5c80f2c48fa7b005ef2ac40fa0bb7838b978d8fde1c421f17f7c9f8bcfde47e09285c525b34b0e3c9c8ed6bc976ccc6c04ad05ccac6cf8785c6d1c6c0578ad7884dcbd39392f593bfb3a395ada9b9979705b3bf0730b40348fdd741433afe35ddbc7b93dbb6674d58b510e3c918f145ba99f423d3f040e55a102024786787fa841c7bb3d4c08073e56d0ffcf66d4df052c7ffcf37f17b0fcb9cff6df5fc0126a8d37df95fc0b8f8fbf0b58fe89fdff77014be83e38baddfa2f65913f383fd9be4f3140f3195bd3bf38fa171e1f479a9f7fe04c70dcf101f57d043b7980f2ff56fffe84e7871fd3befdffdb3e83ba7e4701d4edeb4121e88611213498376a43494ff4731cc524841a333c2fa97111736b9862dd6f9e58995a97b148d37eb71e7474071d4ad43c0cd6d41d1482b7c317ba5f952b06df2153ff5795a83a3299663d0ca8290371242085f83a1954975642cc080885470439040ec1fb434038fe10a721f033103815040ef1fe113410382d040ea1e0894040e0f4103803047e16026784c099207066089c05026785c0d9a0566c081c62fe23200438105c103837040e21ce83e085c021ca242120123710103e138420042e04810b43e02210b828047e0e021783c0c5217008af3602e2388480a0ed222038dc08884271080893190151d41a210f819f87c015207088a2b10845085c09028730c710100ab30855081c22f90e01a17b898008c2213421702d085c1b02d781c07521703d085c1f023780c00d21702308dc1802bf08814324d62020ec2b044451658439040e51da1a0151671b01217e8fb086c06d207088e44a841d040e51d310011111473842e010ca62086708dc050287909243b841e0ee103844ac0401517c12e105817b43e03e103844915e842f04ee0781fb43e09721f000083c10028748ce4640843c1057217088444f04441811110a818741e0e110384451754424041e058143b00a103110782c041e07815f83c0e321f0eb103884c01a2211024f82c09321f014083c15024f83c02142d2880c083c1302cf82c0b321f01b10f84d083c0702877069216e41e010652310f9107801045e088143d4764040888d228a217008bd4bc45d08bc0402bf07819742e065107839047e1f027f00813f84c02b207028ff5b15045e0d81d740e0b510781d045e0f813f82c021541d108d10f86308fc09040ee1b24640b8cc11cd1038440526442b04de0681b743e01d10782704de05817743e03d1078ef8f71586b2b4ad2d8d77fe89368fd4c9a9224387d4556d8da22f1097eb9e191683fdfb8646c49a4013f4d7900e6e788ca8b99683047219600f9730e6bfd107e5ea42fb09f73214105ad9afa2330de3564ad4ac0424ff759c182e0db9a415fe5b7ff275536bf40edf28effbbca174452561a6b72e3572f44fdd0898e86c119d404d96a77ee42824637a27db91afba14a7cb4c083d5aa1f2e4d686c57cba15b55e8a3881d7913579bf64ac158a4f9ee16a39034d18f5ac508a91968ea8b10d6534496d4bc1c52781b23083e1281c1a53e1ccd0e0bb1ccd809715fc80945529f8f6bda813b0685b0e096d4c12d13ca56c6207a9942d6c3aba5aa2fd57fef63835cc1a7aa33a53b4b8ed7f883b7335de8aaed2eb124188d1d88dc48cf2b61465a1c94603bbb521f1cb79ae080cf187c6dbc14e7c2ce06d46bfefa3fc6fd31642d83d63aca30a4e284d65e94f414644145cea1988366f0068a0a3f0df6379477d1859b2b26eafb2e46bd3a7dccb189929a71b9e8ef767f6d17e233db3d4121cc6f2b6a5bdf9e59074468ad6f34b70c73bcac4ca08bb5af5b0f508af01305f644156bf3787f9efa6baa8e134557ded025a9f981723cdc1abe6f1cb7fa3e45debaaee786becf94a42cf8f60c4394bfde5b8bf0d21663fc9bf93334cb1eeb62c6c993f725c4f1b290ae0f42c4346041857506f59c29d38f4c8bb6ad77f0e7cb4db43ebcacd9e9f2992a94b6242e5a7384efa0b807859c9c320dd5bf3a7eb20ec5f2c4c438c4411632360fc58dfc71a00ee9fef71de9c652a256069095f200654a874b4ed849e9e542f98a82e1adb49e5d790fa58140a04ad645cb42a1e49bed945188a6d5d6ed8abbdafb47c1171f90c8b04d3670273c1ce9720fc03ca31fbb07dc8ccf45a92b1591541da80d1ad8dedd5b6eefeb0b0047a754d3c7b44085214bb78267c26cef385fbf375702d6f0aa57cf6999b19a65a376397db34aed344227d90143264b89798da0fdaaf6d35624c3d94f7585ee6f0e088f8243f5dfbfbd3b635f8e1ff9199ce84a4d7ee487f116f932f4a3d78ff541b1b3e018ed6392a9b8ff687c90dbc44c2475fdf1fb83fafd5f6f82b2bb45b177ad014991ab48f689f72e91d07ab883ed1ced338b65d72194eba41303e2bc857daf6e1d2ecede2e3c456a918e5a85a74376ff419977d57b0c5871de2c1325dd6684d0fdc8f8bc89e8a2b2dd66c1a7e76c5e796d33c72c7721f82d6f389e2589b23c1999e770263fa296c7e5edf9d8a112734b728afdcf966b4b2c1d38a6f68bd9752c17b537b3eeb1e4207b6dca9e77d7ed2ef1bc496a3d9f27de7f5a2005fba42462cebb683e3e591d3f5be7319565c12d02cb646615b399568e8c174fa84a2ce5af4cbd6a09423fd3b8ef9b2cf07031843b0fe79c58e660737f829ec399b4de53d7e3e6dc16b81ef64986724cb1b796485f9dc7ad5e6faf28647e55ea2fae5aed6d2bdd17b4d4a3abce86844438fd5e5e98c78f502d3f7678d0f5c1b9f4574b4d3bc8c2e1532677b87261bf94674066fd1d4b3335eca75de3f72f3181b39657d54deede757beecca90fd99d9703f570995eed1fa54582946756db22c2bab61bada9cf730d4624592d7f7c7c3b61687a14bbe2a897b5fde20fc3173646ac13506df0e3387ff388b66660e8d6716e840d0683f9c3e96009b79791e860ee5b307b98b269f18bb9d3fa595751e87fad0302851fe3f98f9b18f9a3f651cbb9d0ff7d55ca73803d5d7d0be38409c1ce12c360bbb7a10aa84a8cca6fd54cce5179f80f495bdfe77160ddf41b79bbe97ebb50d555a59a814c4564eb8334ecdac4b2c317d16b532f8675b3d803645da40d7370e974681062bcac1cff7c07f01de4074121841498210c0fd2ea900bba82bc84a0cc0088be81187bc8bfe7f4f2a3aefddaa675379f5d3fc32a97524f83fdb44378361c8686d580840447c680a1a061a2fe315e2fd4dcc1fa1188c37671f2c77847d831f11fbaee71d815248e87cf041d0f5fffa16b0587cb03eacc83f2851a9ffbdab5b49105cacbc5383b3c89bc7e455fee0e60aaeb513020753c0cb5d8a30e9b815d35512b59bc8453fb16c95b626a9099d041b3c53b4850e34eedf5fc81ed089777899b8f1285d03d38887243af9e5c43d1f10f265fa2f7e1dcc4102bcc5212fea4e76cff68a8cea730b63b411c097f4e29b73721f9b4a2a8b263ab443fd4fd3176163718d0708d3cf952451212ad83961d4064be3714be6883b9f98687e8a527a721d29592a20e41646b6d8ca1dbea671e143d1becdef721fcbcc231176872a5167f7870cd62e089f4985a3b155b8fe2414c04b2b431def95bef36e8422e9cf353b1bb9d4ff9cab4d6990939b2e4fdd487f4ec128552be273d28b30341c62c76510b4df01d54f55f272d2bb5401daa8c1c67ee2bb69c24fbc53ed480c00d26495fe6732f648a969f7b99cc62d13d7d427f5b9efecb310f8ac07f7d5a7a6b6b4064a2f485c2ff718c96794914feebb51f7d3bad3f1e82cf0795a2768cdaefbf5ecc9c97387fe7f5c756a5a3181cbf2455d2743bb56a0c45714905e56e87325b1d057f8d0fcb6e34461fc7f9d06e66b697f4b20b760288471ca1587cf4eba61efcd2201a284dc9c9636ea891e8cb7440cc182ad3f9b8cf08d53e21d744f5a6e7cdcffd76d2795eb79ecf9d7751293b4e1f418f71c8217434416d00a285c13e8159cc08dcefb7d40e8359c072bfd4f4a105ddb845f5197bcf60f3ec66fcc849df967be90312a35fbfd56fbd866f75fdb786ec8fad24c726cd8bbfa422cbc3e4991f31689d54150c9be8b5bd9366b65b20f9ad9ffde6aca01e697c72e9f7de1073f2e07715337e5512c0ef9cfc450af33f5d8e9d02626f4b9d8c4c4cb11de8d1b742b9dace52fdb02036d2c2b0d49fdc263257515663137c07d320e2b29e5a045d10ec17c179ea9d903a4c39135d6da6e3196e3fb87efbb9e3ed21301cce4b72565632bece664e761647bf090cd3a37c1a6afd38c2f3a1f9cbe1d23425e194fed1f98bbb4e9ea1513201ac1fb203f24f7af0b5ebc635cfec985e8cc73ed11cb671bc35169d5152db0a885b4215eefc33fb286a299f9885eb5ce4f1dc7d90a9c4a6c7db8651a0f6319463c68c248ed2ff7f608e111ceffe9083fe4cc70854aad291c617d41ef7ebfd1f173fee1a073bdeef1fb9c83da1b6b39d878f99ab0ecff1d6e8df3b3f838e6207ce244af9617d58609b5a5ea1229b24b98c1a4e6279ccc33554ff7caf6e7bf27d8cbb30dd6c22f4fb1f59153247a36367aee9c16096471f813fd371f63b2eec63ae3f93c79cdf12c75cbfe147591f8eb43efe7685a784027aba04cbcb4b9f453ee2e60d0db8356ec7bb6989b57ff3ac4be52fa305436fe64346eefe7eee17c96f654929f37e82f2f5752f6ab36b5ae194fce225cbbeb2041cfefd1f81e0cbb5578c44a895af729a4f78e55fc98bfce4c6fe1170647eca555e9d6f5a9e54d593ef6a1221a31b879f015ca8b4f267fbdf67078d47c7100828be0dabd0df3a092c77a629b96d146689375c97a6ab5b029aae34beea0558b24b6ff53946269ff97219e58f904c46262277bb035964bccf6bb2bec425a261e43f3b84e7b025bd18b602588c94321a090cfda41aee05e537885ae49bdd3d2b81d91ea7584e5618f9586d38a637faa32ed3e342edf7401658d5e5f98994d6c0439fdca4b08c0dda35179d7460367c91a5a56d39595ff5ecfc7e880a17edcef487526051a4c02440d61975bbf6bc21cf62e09eedcaa3a07ae04d8762f8160b45c64e7a7ff944811d173d14b9930eb0b0dbfc6a4db5fc42d0563a9502a3483f6e9cf431005c7d75f7bb1442b52966bc66b9ef73c82af12e0c57032c7a7c362c937182f27394e246ba8b538f626b299b003faf507d7839cd947376d93e078e238378bd431615c04224889bc3be68a5fe208674bb5b78a42df6fea92b407051e03475590ebb89d6b91daa4802133764e3ad4380459612c95d3efd308cadd46b28c285f35a3c7a95115036aecfa40ac7536a63b077b0c7c33d2cab72abf30ec0f21e3c171b8c9c4ff13c5f33ed50d9a40847039d1fc802794fbff09ea56e60bcdc7a0c85ceba27803b721ec8e2d8f5fc68c41478c30ceb44632d2c0563795d951e60d94d98179d5fbc38f8a9342c54e409f9e430fe9814c072534ab5f2eeb71158e55ee044abad29f2f5224f045882cde56fcc95899f21c61712780eedb9fb03578780b27daa17bc4694e35b7c03f06f96f7723f6a8a749d04b2fca2d4535117b7cf4e7b0fdc14df9716b3bc12ef06b0207de1a883b76cd3ba5a3f7bd3ca4b9ace93671402caaa653192ecf8355b59895929b2829896446d6f4501868021673493477b0bb15098ffda298c4b09f1f9c400cbf3435021ae83a7cceb331dd5671fbb271bfb4d57002c2e07722f1fa402b8b1e22aba5c9845b1b004e328902544d718dd778f35042f381d3732239c430ecb220060f99a765670eff65de1a2c08dcc71ce4b7a1ad1560dccdf53b52225a66f77e10b2b893ea777f5d4a7bc99812c27a7d5abd7b311f3d76a4bc7ef2d722c8b3ed32805b2a8f8c83a38bcb79ede3ad8c3a5982fe84dbd7b3d066039761d7c8007d409c75de63f607470798ffb502903f00a7e4c9840817d14ae8f9647ef9dabe04c68b3de05b218b2f93c1c602eec3b2534af5d2975fb6cd25537812cea012fcfdae7d642ad210fd4e54caa8d28d10c4e002b7dc8d3488be6ea29d5aa413c25f5c8132e97ac3ae7bfc675594ca56d27dbd2b219547745903868455cd08153e841e7c7cc9b6cfee9237111a6f324ed89ca927b3b40604f1e638fd66d784307b6f47cea364ca9d9e002c07266122e42964d307ee8d5c4f706f341d97ac6854a4039f6cc16ee704898765b0c0bcb4b7b0b5ae6adbc7a8025bf5e602330f47e07b5c7bdfc74c023e7bd9b4d0dc06e5c67e78b6eea4626497fc2d8ba66ee13e58aa21590c5c5d266583232b14735c189364a288aa55fbbf7006089d79c3a417677d60099b076533fc5de256c3ce103c0f2956be2637991e1fa92e44a9be0f32b343a7cae448047cd4becce61d509cc72eacc4f7bab77351fa824bf0458daa6eccbf9c93255f3d353d451c6c24612396cd1007e8d6d18d7f9c43e8dddf582f3a46f3fbcc0dc6c03c80b3e58a356bdde1b569c9462ed0b9f59e003be968001f13cb53cb9d1cfae229896ccb3b26b3407275a0ad83f84df13dc8ac29e703eed43f232f40c7bf116a502701afeb86740ac90df27a637d8b54d74f8e6a6ec903cb0bff7dd48545e942e929e3f774f688556cc73f62016c812fc7c6daf07ebfed35b5e122c9acf68c63a398cb4802cdff5b11d9477b372cd78bc3d0ef3f21d0a3a260c004b3a34eb368395a54976be60326c5e8589f7ea5526204b4fe5744435c648ad48f20841c917a667bdeeea56000b570f29a96b092ff2bd7f405daff8a929aff757130196a596e78890307df7158f0664b91acc5e5efa761830bfb5cd9163562ca98aafac23d0b470a3aa6594a2802c9ba5c08b0278586bb4cc99edd10323bc34245d0ecfbfc64d46435fd44510a74e9f92e709474a9a8b51a107b28c883104ab694df42c0e1454d805e7eda3f113350115803eb970dbd1e0edcea9b216c61796cbe23c38db4016a8cdfe0b0661a9fbefac4c5bc8f97857696b7a5f03f68b4debdaac04521a1ed5a6b4b25c93d2e407411660fe68f45a32665a4844e4078434ea249809a8a96b2f7c8dcf324b85f5c3e24522e1e9e747b4b8fd895a2480f98334755fdcec1e9131d18171b51827f2cabeca79803ca42ae47af0e08d9ab3c7b9174f23d5ed74395d688030fecc7cf4987744d5b27fef6c3063b2659760970dd07fe16703addcfa09d6c4c80dd429709cbb96763180b308572fc79d1a791b92880d5e9199c861b7a2cfba8035bcd5deffe115d7827ac2ed4a73a57c75c73236bb59c09a0ef4bd86444868896668f2588ef19de583b2d7c0fcbfe3fb8a5f6274fbd3260501d6d39c4805865d2960ffe3ba1febca3df369ea949520539a0ca38652ad3bb0bf3e228b7627645a4a533159bba272cdd48eecba3490855fc84a8b389143b9fc4c4fe63049c4e3a2714924503675c6b2d3be9d27faca3439137d2817cc9de80e2e30bf5c09df5f282de3c5651e294c9f939f7e7de1791ab0bff536e6e1f7f28eb2be2123e723276558ecec9200b2cc58b3f199ccd5528da478df629c6f6126cdcad300b2e0a5553cb9c595076429f905eb4a70b0bdd7917a80d348f983aa4e2ad31b03460e572ca78a422da9541e00f6e7e086eb93213e1c7189bea7b367acd984860b338030fd43b3976902f72c64585da6c6101d819cfaa3aa80cc469ce47e6bddade6c7f4cf98344a3154384d3d6e0359bedd99b3a2679d1ed80b59f952bfa7eb5540aeba0b64793c3c89e56ec654a976add3d63867c5b3f2b1d91b208bd1c7cab8ac83965645e04e08e2597e0017277bf309f80e3c2ddc6003df4fe27f94afe1a188308796639e03a1fc0470ece39d83e13ff99c0a7c7f8dbbb14e57d0a6cf7d5dcf882726513c4dacf607022abf76e7517cc1328dc6924cd35cb8f91dd209ba07fc49e7e58bb68fd53eaa6afa319f1ff687fc2cdf73a8201d21e2f8af78cb5004bd5e56dd3bd8bf466650137c07473d28042333318456c0a00e4726b854a7e798477094e30d6428ed4d348837056a6be2c832f8a79a609d2c783eb29c739dc828bee382cc311d4e10f70797b1f7260426d398cb2765e324d6469f2a761dc27a54a6317386b563062e2560ffd517a47edc7f04f6e6a9994cbd933e66d9e949b15c481ca75c78c3f45dc113a63c3c8e37bad78f7383922ad9a96a36282e0b096edaa255d81a1b3d153db317a79344f67a78f5165f8461e106b77277733964177e0c536979192d1b47971cf8d8e5ce722d1b2f7aeac9fee7947f524fa588f39ee9c7e00a2bd8b85735e24778f3adf7f33b7f8fefffbef16dbec531cd9247bd4e31a7f779efbaff21cfe7b7df15fca178ea64a3d279ac1b9424755cd257ed6a255dcc86b78b2df60d0f3fdf0ee55cab9296b94dbc7da1f53285006b9038339bd115ca10e3a9c8790edf1364d1d7da8bb0433031c5d09c49fd9dffacf12dc3f745796704f755e315968c0c29240fb4b667be7f8fefffbef1adc7e55520fbffb1f715604d7feddfdbe80e09e911d2dd2148832838103670e87616749784ca8a364689988888895d58a08298888a2858a0d8dda0a2efb50d9029b01f4e9ff7b99eff6fd7256edf13dffbdce773c7b94fbde8a2c42c5c6fe0b86bf7b2fac5bd631e58946e90a02120c7d58272979af28d17c4434e6c75a39bbd71b7aae211233cbf1ef846eeaefcb7bcb36fcaf0497227d63f593fa79376fb42a38d92e5896bdd128615a136d35e6cf63329777a9c98faa7f0ddc2f7fa6453b99177cd32f1e97def1c84c86d3636ffe2fb7f0fdfe2d70bef5591c4f0e597d69ac985983bbca51822c7f64fb4143f1a72a7bfaf6f57cddbb7c6f6154f80836f64cdd26c58e52457f4f2be686da77375fc3285171494134c49afa4a65d7ad0ff32bc21c6cee141913ba42dc4f572828deb83c7fd4a4a5cbcbc11d6cfef3f7cf8673dff9f185470393a65dfb4365769b1ac61a5a9ce2db9f8a2c246f70bf753966efb57e8fef784ee50b22c0d6918fc59edc4698a157181cef2bb3d633a0fe9a276b3bec973277425972c9cbf89bdcbc4ce90d67a31697d6551f78bc55b1bba1ada1a9af9afb860e2746ebd3e7dfdbcdb7b5f6d64c99237273ccf9cdf4de4bd76bb669bec129fdbbc1d1a4a7f885373b6e554ca079c7fb433283728a0696b26398cb0f85f7cffefe1db56bd8676d150d3b4f4f01df91522060e2afe15639e52519bd757a00ee36ed07b5d53197acd9c4a11b1cfd85d67f2beeeb3409b858a9eaacea193124bdfc68bb9402ad43cc5e35baff2603a6a77e6f550963a342ef41126e51c5cbc41b55379ca4a6e9c26b6d39bfb10242aec441455efceec43ee6feb07d8666d468b8c7de7f06984f543d3f3a0f8c371b97da64355d7430979e74a396d4e81e39e3ff4bc133a5d5961c545624f58fb82dcd9bb8e9fbc9bbb7dcbdbde7d8ffd3ede1e99d9bae342180f0a6f8817d9e17739e5c54ac5cd59455cc67d39a186df3c19118208fffd5e3f1e622d26635be5a2609e14687fea5852fb7967a91ddf3629ef8d7fb793385d011bd6f3652e095385d27b5eb202bf0ec2b7fee433934362d6917eb37cef173e591ac4f946b91197450eb1feef5c1639fb484768b0a73bbe66af1846d27889fccedd5de9e09e9f71ec86062bcd7be8eafde24d664674ebe32dddef6e6c989131ce91bcbf5e16b9f8e0f79c49a7d237d1a25fd67fbfb66ac693f6d330ee7c9a7f2f8bfccbf5ffb9cb22032cfce37101c9712e9141789b00026e7e50649c5b66b8bbb95d866f921bc1c6c7cacf2fda6c062a3cd5dfe5bf87bf2e3baa5524536e3f7d765826f2c499a3dffae7ceab49786f7b6bdb8c930ab70e28567f96c8905ca8605b337975e5470b42ffd575578f3facafb8548359929f9e5324d58256e228da7dc8dc9445b19477a77ee8eb0fa782399c9554f37c5cd8e2f78737b29b809f6f73b9b6f9adc5f9dbfb7822ae855ac009b3f6c68524c0388f5aaee6a6771d088d8ba3396c9db198f987d408bb962f86783d07fae20284793e0e4daa670384f998ff354ab6f3c94cb6c7fdd077790e61f69514bb18e6ce78686f4bcb87bdbfafe0a76981ef953113e952ec8347deb85b6bcc75b2ed136afb8b783f49477138983e8dffb7cb3be8c8d950c65142f6373192d7380e64bbb9f34ca1f0bfed9771a2e00fef63826c149dda783e07aa58ac284655d6394982bd2231b71da96edd5be3d055c53691ac4fbf3e707e1e1f7f4fc62aa71b015b141bc3afc635c2fada582574ef0a1e6a8a6d8869e1ce9072f441a0e678828519d10a1746b4b1b6c65b9b5a58e24d4dedccadadad818d0db0e5b4941e9aee20a97a2eb2c7fae8551a3f7e56d69d98453e7b05b243e76d2cc55e949e77a2a111d66fb57eb04daf91f556655812d63900ebec4fc23a43b1ce0124ac1839000b87666b99999a1a9a9a9ac2677b478e762234390081e5a5937110884f311d07e1b545a21123abe1615593327ffca37918d5f031ab291eae0683e50fb224c9e220906a14863430582f89552f92592f0e62e5844516dbe120905646e10f58b81216ae8c44237010c82b1c04721f07810a0f3dc441a0523fbef32923d1242c8cf15228fff04b114834c6828ea023d1182c0f861e48075088309d37793427578e8e206379308c1ce440f2a859600a7274eae88561f2f420c4e8a5e4c6789c5dd42db07ab65c88cc24f78b6a6d356f690ad3b32b343674edbcbf6f727c3401d58d75f66730a99887be81d912b23f1d514c4720d1d97ede59e31ef88c09c0ba607903b05a98817f9459632833060ba3f0d2115828458e8483c010286c3645ae1881c6c22992641e1c041686c24160441c04268244a359d460c874041913808563e083f461b070ac1a15ca3bf4934251a2234a8a998dc00a52613808ec5c36a741070507812d61742dac2397cafa5e8e2029d0e44a11a45c399a7c0902812865fc6151304c0aeb0b0ec203c741781471101e191a0e027bc9ace913f3ef4b24fae7fc38088f031507e18130bfcec641787c2838088f0e124da305e220b04a249a128460bd8ccecc138983f0a099dfd2c8341a0ec263cac01b02894693c992b23808cf1a140ec2538c61548341a2b15032948ec0417816b0608955a34831723d67e43a40c6416031ccca4e30885d844493a158780eb39ecb28321dc1487a8283f074e7b092a95046bd598cba586fc55028cac33cc64178c5310a34ac200c07e17983442b90a872c58811a9ba5446630790e86204050b8351e4878ae220bc6e141c04da8e44870f75049941282f068583f0cec341a077906846b6680a832fb6cc22e9144a0e0ec2b39ed9f37404568d2a858592657010de1d280c8d51ba1245c983fa33dbc1bb31878283f02a21d1b941081c844f86c10c059a1c0ec27b3c97c5070c9ac2e421ef2314ad00ea4f09c24229d06204169e032d4360e12c4ef23622d1b95446363e0166361c848f07ab56204761358d1a844063a158583e0c9acfe83a069f98ac02a632241c844f1f638779f78da9292d49b2002231a89678ad2501640085b1c2762b93f08988c9b55200528e22e14ff2120ca5008430f85506c0184f9fe43c699601b0dd2812feadab9bbe0c80f530be0edc2f9201b00114800c20d1ac6a6400cc0245c22fd9781e2d03608e2812befcc62a17190043a348f86ac37d441900cb182a60bce7648714805c46b1beca00e84e14096f0d13c3ca00e8791409ef7c42424106401fa148783fe9e4f73200c633581637706da90c804251243cf4fbd94d32006a8622e1f907eef0c800284091f0e20596ca32009a8d42335a0ca04a585e92350f194a0750b9623a502221d180470189c63afb03a82ee3c945e60f86267a8706d0193ff27b16d381b20323ffadc1fcc18c2724b6fc9180f7d8606a5a311da868b2a556fea8ad8c91ea864403bee983f96b7fcd7ff647fea6623a509bc7c87f138986a001f4c18fb4ee623a80539068c0bf80990683fe48fb3ad44a017f569aca701a6c72311da8ef65a4a5b36880e917d38146f7481a600e407019aba40710ac1ecc17504c079a126cf9f040f0dc606ac22f2d8195fe78eb5246d900241a082159f5eefc91b695914667a4c5b0d2ceff483bcd487b814403e182c1f7dcf885ffb02f3ff27f2ca6032d03241a88c833ebe2911f4ee39166a41d67a41d62a46148a39bad310c200fe0c947619dfdb3054f65548def1c6079a180279b0ea6840c933980c690a84c2780cefc8b816368a3bf670c133bb268e0b022ccee3c9039ae839f359e055290a37048972101de5a14229b21517ff3356812e03d804200defd14206a8f440fd92f2c2f2910f09ea38329350cf7470d8a85938a7961583814ebec4f27339dab92623a99c9180a23efc7c1bcb280cf0545c2aa4902bec928008590a881bc306a4031160e2d067c2e4834055a4c1f34ea68722039808e8542e95838944495027c5a280a9d07ab26478116b3d42a1989c662b1305220e00bfee7d4f0d17e507369909ada5fa9b9c4819a3d3f5333c81dc0771f886e40a247874ccee88f6548805f0f8500fcba39805f93599e295c68c01f31017cf363e840db7d04be017f6a0ee30344b722d14396889f06240e31ec1d8d464790256581800f8a46a390c99280bf0605f837006d1cb3b7b170189469f8007ffda0988e4e3e65f4c71663b8a0a7ae91239a77245724abf03ba21b1c89e981aec96eef1ba95a1299dbb71a49e072b18cbe03022625c574a07d79b83d3524121070a5039d290c1f0201f8737f344a2084d928209042a1308b939808082061c8010820693de80061c9fe547f1920707c025c15d84f073ab7466a0daaff483f050874a1804027504942a21999e8244a00090b674090c2244116080aa2c80c7ae01418c99a6a43623816083a028da02330689a3fa3130469282018984321fb932581a0210a081ab0fa815513d43f87ca4af24201414ff624a6e1a40c160d4701c130b674323900812001c1852cd6d11140500c89469086bd332058c1e29e601506487522d1ff70e84247a2b17ca4402078fd9f8b9f10cfb0f809f9b1c44f68ca2fe227e437bef809998da20c80500410024ce9a15281f46a569f03a19441bc53a92cbc0b5351182c7fa09b3363ac038702a1150ca0e9ba31bb180b27537978186f730e20f933de07c728d311341a95212042cd2820d4f4ab8008758d2b20e4bf2220c2889f0504cb8f210732f80d84a306e5848e0842204824209cf5437084d25882c3e8662817360f43fdfda2c3e0c2329034884311b5dfd29d22441402881072800866a4ee14a99980948bacfa59778aec66e94e199e1f6a46a49185a50975330f10359e0025a253e840f7ce484a446dc84cb1647a170cf7fa6f9afda1512a2b84c1d297a22f2742ff033ad08b19a92f47670b6d2c6e89854ee06d62b3e940eff3c8b701312213057404104b25e7313e406e31d3e03135ad842c0a33a867c5ca5040ac945d99d2119841552bb60705c476ff924ac2d0d8a15cc48dd3c818b80371bf09b458dc830ef4f347b4984ce6c19072d8695afcdb3491e94108f22ff5fd7e1b31144a2010ff5c4c07fab548740902c1780182619024c4870d92d87c241a3106207227e47464fbd8dde26f6fbc797a46c24efa4064a1a6bd7fe1c6b645217357382ce6d33ef3e94b5e1e43774a2c666849fdce1fba1348540f81a50c890612bb867ead46fe18c54b348ea3a080c41de6c06c0c3d05242d27d0cb92067460103ca29791139623c989e83fc9557460f0904d8e24eb06e5080324eb73c0e4a4c1efcd43df81e4352aa62027a780295f340c299fbde70bb8038d94093b68c600c2c4d8c2d1b8161430f021b59d810f43de11f8903a39d458063ea4daa8988282820230f9e9103e10ff13115ff20447e093842780b249503a305c350265242c1493f3fb30e1c27719ec012c8bddb2b2e3b1768cc667cb9b2eda66b70d083c2e58e151e718726cf9ecb790b7f012de8273062136913166540694267d648f8e6c60216582c22c93380136cb44fc6214653287855926f78730cb940c0bb34c35b56058983124cc4fc25cc88d95cb615839d989e83f59839fac5c0ec3ca15fc29abc4d02739bfd457c89dc292ed66575879a3172c98907fcb5161151632502637f7178525974c2b2ccccd2d64292c3932ad90f119a9b01840fcf97f2077640c1002c546245a164c8e428d210ff8a30a0bc87979b9b9b95867ff5c3cf319b65b89d5a1f2d0f2b252068d4622483416cea092ca43c6e4cd7953c2f0066481fc541402ebecef885553c0aa51a072142809eb1c80600cafe4ed18e57290682c140de47b5040de1b181d45a24978069878807c0d0a01e44381d14d249a04e4c319bf6a19e3d17f0c37f955eca1d20134023d81d237e8c0b86b44612627b0dd4a1c9c6d3227671c4c9e48fc6732860e4c22d8da30d4ab582c060b658b5a4ca6a3c0e465e3452d261f1f336a412163982ef2e46b2830b97d9468433162301ca620343e9a140f8d89a6b7f671602c3429cc1a449349eee86852281c134d0a7923d0a46882020acbd9d0a4f01685000a5b07d1a4b09ff1ebfd84d0a4f0841b34296aff8c260627fe049a140f4c004d8a7563a209431ac3fb76901a9e8d75f667cd81930658ff462d40911bca0f94942940f1ddf02c29503224032569249a2c0394ac59d3644834866c454700252f3250d244a20703eee4e122183250f2651589fca94826192821878b90b0ced41c1e6959a0b41c0594ca81e233249a41c3861c4a2e1428d9315538160e94f6b11ec4300327258812d67b5a58efa104b07c3e3805ca784537860aa501a5e3c36f614e378fc7e8e1e603654532507ac8245d598b9d74652b3250961aae140ba7d018942b23504079d610e5ca7369941c2850d661113ac81859d9e1fa33c94039ec6792954b99242b27fee0a5ff8ff9f30056aec114349561c395db50240ad49f4c8306b0e64481f27e241a43a34103863c6086c051a13f3a56f9030528573268f84ece01ca1b90e85204509120e760e1b950a0bc1789062a93c940f926124d83fe209ef572c6d8960a0d002a0e641a507ec680c74f7c5009460115d4101f54884c3e0c75d8606f0f4c6cf50250d94c61d2438692c992c35a9322252f0b545a5040e5344b6d32350af30d40a51328de63ba512a0f2814a0b28e197a43a231234acb02555e1450e519d4b9700a8c3e38b1cccaca9012a06a0054b56940e529832d6d8c1a49d4a19ec0c269302a3487a1bd55fd5040d517a8ca22d1682a73f61fa8ce4791802a3e9fc26c3d2d000ba7414b11836f00aa2c133fb47881c2e82369a05a8e02aa6540250e894693ad80ea461a85d92baa0a3ff1efa7765c4001d5f3ac7600d56b2c66b1358642a6da60a854e8107fd4604075800654f70c378b4266228846a341a564819a2a0aa8a900d51d8c1ad5f468941ca07a0f89fe995e350f1450731fa6572d6888ded76cf46248186c556e3e031e6af351402d6d081e6ab47c4a014bc0cb9902ae563efca00c81c03ae742add8aca4da011450db0f145f31e93ac9ccac4a66401881c5fe92f93e0aa8dd1bcafc6a38334397907ece0c974401b8c46066b83233b39a19ab19e58892727ae910eb4954ac1a0d2a0de0ae28007761359e624547b057874101f83c00d763428b0a2d46501908a332980bcf420178260b2c833a8cc1232c36278fc122f84a1480570eb228e767320fa200fcc01099a7f228f95000376270ab8cc9407807f31103728c663a03f823d603bbc1a61497214a7e110475311450171d4b1080ba295037a001f8bbf185407d360aa807b00981fa021409a8473084c0ee5721500f1a4d08d457a2807ae530a8d4b70e824a5d950d54432643fd2c0aa89f196417f52776a93f4401f50783ec527fcbb41eeab983e6e4e7cc1a935040437a30b3861a3333032f3f4ccd38fa98cc60056588674003cd669380460c950a3402904c30d0d995ea209735965280861ea3ec0a720ed0b0602a668d0d54a0e18644930615bec65514d91f681ccea7000d804457208046331568a4b39ac45c6004346a9168444e29c3008caabc35fac834a041fba1bc81a610cbb8b39b234d0da639d2840c9923a069c6325b6419a0e9c06e1635679281a6eeaf165d33020534c387045e33f5278bae99cb7ca0e9c4c6661206eb4ccb6516df84029ab583c5693f759866130a689e1aec30cdb65c4a1e4b184a11a5accaef331ffd701710252349d3e241012dd810695a923f91a6a53efc808db41fca4ccb1d05b4dc862bf0ff499969e1d89519d04a663d88195458408b36fc6054a5a4b51105b46a061ba8b57b0ca534647ab5aea280d6956172ba59a6d76e84d4647b04b899995959d9c3018190444c4e86671293e2e191c9f0b8f8143880a76128d0806c10134980c7cf8f23268db1821a4e810630213bc50dab4f65fa10c50830c58f02b49e301cd41fa06607de940432986233ec53fef6aa4430e508fba24430a51505a65c0453e60faf48fcd1543c888921260d3532253e9a18c7681db3a9c424787c121c242424c5a71109e33555db186b3cdc546d3b0a98d23e5e53b5d164a0ad31a1a69a0f37f5d7a58c40fb08d03e00b477d380f632241a68af60f8bdbe3fdc73edf364a0bdfe8704330903dabd182a3487e90303ed370cd54303da2791e8418dfb4fa8921aa60ae8985098ef2c46705eb8c90374000ae86081761fc3a64087f9340282787c7c6a5c4a321cc411e0918464780c312e3c2582d92ff0d8c8e45890828f807082a0ce69acde70bfe85ca6009d98f1fa45e71d19e81cfe75f127d09501ba12acf6019dcfc38c24ff9ad111e8da5280ae22e3bbe60f0dafeb47c901ba3c0c0dcf189903dd79141ad0351cfa4da60fd9b9ec59f3e3002e86680f8f23ce67c919039c291144963402260758f239ce3686610ee89ec16a0f7340f72a05e8c68dc701dd0f64a07bf4073287fb83d1058cae00b1ac6e6175079c490d87ee18418d1e12ab3b4c8d1ea000ddafe351a34725033d9f51a849490271c961c424784afc08d61058bc199f3523a9e9c54ef941cd2b0ad02b1c8f1a7d2932d0bb311a6fe29253c3c222f191c4b814380ec480383c111e16cfa4266998564ed4e8278d5097fa0b29407ff2b8d4ac2303fdb01f5805fabb860410e81fa132bf9620288c22248617a77f1505f4afb0246ec8f03bb355f886e158300d3f99ccc3500701c0806fd819f8059f3f69ce08220bad98a1f618b862e1c3ed31f0a500037116dc476f8f413c1918588fc2ddd8c8b8949ffa99d1c54cee2673dace33cc5d8336acfa0f6a6e528041da78dc3518200383a61fdc1d549986f23f54a6a1164b65327e0c316798e864624a4a645cf8a0f10031f0e4149092ca221a1e169f044f26c6847124da306784fa32a45380a1c178441bee2103c38c11ea6b4c7f1418dea600c37a462b1e917380e129a63b69f869e438df0832ca38bf8ac90623550c159acff4fd809101830d79cc1f0cdc383328317224e78ec01296f9cc9f0d5f24e6332299060caf329e2d2f1f0e5831e80646d9a346528c56a280d1d0f807186d62ba3146896c91146cb7f23083781ca56481d10d14961743720246d730fec06817e37d68e69e0f774749601c8e0246cf9921012caf230f9697e244f277c46a51a052ece10463191402018cc3916812054a625f57666c8e6250cf4a657004caa82b07ca4c9bc54c632502e379142c1c9a03cd65061e2883cd669033ba7e8b8b8f331a4c60c22780882746a631242f362186184b8c4b61d72d3fe063dc84b51c868ff1250a308e1a0f3ec66fc8c0f8d0281238f865f8c549c428223e85c8d2b62c9f2999b3049a048cd0fd267329c0b86f3c6a4cb2c9c0c4738404b2a06752f9037a26b53fa0074cf632a097cb924d934606e658823a0c36932bbf82ed5b352132212c3958431a7e3fccf2fe814f49d315ef3c36a6473f41e77b6405ce7b78442960afea85454ebeea84e4f818bc1b4450166211919292906c6f62c2286a1c19cffcdfc43f1619199218986e9782374bb39d931692164d9c19183187901c1136c73cda17e797929a916e65e169150b10b6d3c2226388712096e8989c626565659c1037b829937726892a138dfb755366c45ecf608b2f499bc42fbecfe9ee6d39964bfceac1795ffeb8fb3c47ab92e33ecfbe99249aca45e2283bf5d0b40fdbfd349bae5fd818ed5d74be4627f966c7b8875942277dd2845dce95365dfdc019425db19c44dd7cd784d4087b451afa5133e7551987cd734b651a47aba3234f6defb2d63c49049598ef4a4dbed04b83fa3fded2d8378344555bddf72b2b2670bb9ef083f864f1581a0402ebe74dcfe3915abc04994860ed303c2a51cf4ba899b9fa1177c781402774719813462cb6e9b2983c2de261a9e59b491e1565c9f53fe7f9ba8b54d3716a656960d7dafaa39233af8f575f588c21b7872ab8bc690f3a2efb7e85d5f3ba857babc945fac10fcb21f336ef5c596ee2b93a20c4e599bff0abaf0e77ba944f2b3df5ebf63cb047e77a839377a4c307c7ad071c75deb43ce0f666424e874e7059be91bbf2e32773e63fa7930d2070baeb32b939d1b3dfa61ed9358059e5b944cb5c2ff4c52985b6cda9877d6b1daf002ef7047348d764e30fec5586d8f54a7bab01da023ea7db3bcac4f882967383af7f804f97ced727932812531b0e6bba66f27fa04db9a837592e66b2a99743686e549044c0e3983262254fc47a85d7dd4a3a459b1f391e0b0ed755ca30507ffad2e3cd4dcf25fa5ce2cfc56941b3b4b9f7da63710e4f6bd75cdd4248dfb044e67b7f6cc7b75906d1694acbc41c76365de0359cb15244d6e5b4e8f5eff0eddf1292a7060a95aa9578c846ee6d38ccf9d08f8bccedbc39b040915e0889fd74177f1215d6d64213fe9e77e657257d563c00aeea50690d14fdee36472a4cf28f32db37de9bf83ef4fc278f18c12fc34f6b95f756399cf5cdff29bfba79b2b61409ee7274e639e8ced88a1e0d4b2baf563c41f4a3936485f1d290c926c5133962a479c1805c40d4116783d567f45da77d5a79bab836e6df2346fe8f1c31829b9948880ff49fed9396199b111f186296199549344b0a0f0b4bf0b24620431208335c7cbd62dd5cc243ccfe8b8e1889e47b84472f2b5d0796c27c13b51e2e5eeb5b58708f9e38eb601824efccd6baef91f315f208679a5533834f6ac5452bd86dc26f5ac0bb55ad39b89d2f4bd2f35609674532ae8768adaf2daf873b527b3ae4e963f85b35f9861a39857fe421f2655d1ac5435cdbf1586bbbecc577827451ddd7d78b88cfa75bcf19cb065dbfe176cbc11b02e987a5e443c51377e3e87568aada6ad37a18e1dfeb007eff3a8021564ec44381eb794c42dbb444ef7c376b7f53c34c0723fd8ccddcd42ff1320acf65fb397d0cc6b80ee01f9ffb3fd2e40d5d44f19f3179f886bb7b8f95f29f21c94acfacce38b5b81f7374c7ac8f53098b6540a6e169c10b133179e1f465a149317bdf07231b82377d0a78a6f83de2f9bf26efff88c903e9335d131293c04c542021c22b3d33788687859f79a42748f3c9f0b6b48e47fae3bcecbc2dcc6cad6c23fe8b4c9eaf67c73719bee7b74d0be6e624a9b59e3af645eed85cbeb565f744f4e0efccece36759e1f136d8598aa4f4194fcf7fbdac53f066cffbeb9fb191e4d2868f51335f19703e55ab9ff7d3cfa2cddbcbbd68f3fe326caf78d269d6943aab2bffbdc1a632ff750b34b215e53e78df5c0415c408af6ddd96b56d4f47cb3cd27bdd2c35ff3c9fd2c7472622da8a7545413e1557eacdb04625dfeea5f874541e5affdf2ddabc907f45fb0f8936de356206d2cb226976020e4f48c08565e2acd32cd2632d7121a8203fb30884953781e865939c96e09e90fee7e8e736ace2cc6558c5d534dd2accd6c60c676d656a69614bc0d91200c05be0adf0566116040b3b533cc1d48a40b008c31170c022cc86086c2cc270e6d6a63873eb309c35ced61ac755fbf98969b1f666a67f4ff53d4d7e8369498df5770e994f583f7f8bf2e692f42bb78e5c99a2256fd3def94a6ab3b8d8a1d574efa05217d925194ac18ffd0d739213ed0daee89f58d3b8f7b5c6613867d577894415c78a502d5f99d196572ea8e5e0fe17de363b4a9796d990ed7020ed3cbd0eb76756e7468eee7f3fff7612555961463d7f0dcaf543f84430338ad3e63a6667bc46875cddc391a71c42a97cbd510691afb9c435a7d39d3994e7e352ec38de61edfadb80e7c0e3d1d2dfa4b465ad2da3559e52293099ea1e79665fb941d2d84d1380bf478ba8af93b288937ce9dc92717675f42c8f869264b765bd2e58cf2bea85bb2842a70fcee652afb850367f10feb02afd3e88809e71ac329675887977494458f970fe32fd940ffa5ef28e7b9eb6e53b2f73b151588c91d823757261e2805ffdc3bb451ad737273d5d92af3e9193c5394daafcc6a40bc7d3c3b72f47dce012c35c0e58a11c06d450de43c193a7714923e977f9c851570ce2f0370695ae100881d3b5f17fe2e33edd79c68289d06f62d45962753e6959cbca0fa96fcce76dd59110adfc4b768b5330457822954d487f0ce226ce8777aee6a9bb97250a8340f9f21d474eb63dd94ef9b0e976cbd2a6aedc939293c705fe2fb378f505ff1f66f1ee777d0816125e2b9674604a6b30fd0bbfdbe2d9077ece33c159bca55c769bcbb6ea255fe7616bf6d925f604a33e9eda52fe708ec2daa5574f7ccbf63a7bb027b0228834f7d08174f4a57ae88e2e697fca91070e7e55ad10e73b1221a6d3e740669f53faffeb6efeff9ec55bcadd700b02ef3aa3b721c25260cbd593eeb36c05445f1e9ba7612e76bdcff5a1f07989fe0fe7c338d0f7df3e8bc7099f2eb00314239ae72b5c55b4ebc9e7f5b3a3b61dea8a2475d4f3eab64cb73c310d9d6450351bf6fceeeb24297199f44ebedbf654df97373b559a17647cb153d0bd486fe376164f26d7c424c60ab26b115e8affebeaa50b67561504e7b7c46dbaa77c2c60eff5eda7e2a4eccea69700e280dfee7cbe57e7b2ce3dac5db1ebd6d5038f4dcb67864d9b15f00f8eee87fa0f3be4f550f720845eed5f0e6c435ddc02a7cff2c3a0a6077abb07b8a0307e2e81d3911e98c059333cfcfe0342c9a6148ddd7b523e5d9b0693a6d85f906a5494bab618cd76d3d187d675a765f2435f46e60d6cfb64105a26ea14b86acc9aa52e14675267bb34c2faf91287c734f57c044fd3aa1bebcdfa26272ab61f78adb170a345d040fdee60c2e14399e7126df8f80a1745369473a729d879e26859e91510b11add78d6f4c4ace3e1171af71bdf19aba4b6e1d9992b78f8ccb8b4241251a9b109c42463623a3e02c48513278a747dd374d37ff8f9fb9a5c4a4a4a3a50fd5c6c53c09502335752551f4d9f3891f4c5f1ba6fdf6c38932d515ab4fc4a2abc3355f54ce7c8f4d16f041ea37fb2bba69cf02370128c6eeeda0fdb2cf04293ed6aa503f63b62acacad3b62df38141c769f72a9c751f2f09f6c23a7fa27b01c8a238f0631ce65d8849335838e762f31541b02e98340ce4120bbfce1fc4a79d2c69767946b6f9e045f5d18526b31256bdba87c74af7559d4f1974365637ea67ca9f3387d63492e04de71ac217db47e2455c5f89dd3192d6554c918b31e4e72f48b9228bb2c33b625ebdb45a2427d54a83367381faa89dda6d23c113bf06acbb9e8f38864e8f2feb957ae1c540d998d468770287f86bb0e709ad108ebc85b735e107b6ede6b2a916c474d3ed84f83fa3fee6cec872d2651d51baceb61b4dd6ebc33feb213caa93cdb8df9bec975070d4eddb62e087bd1f67a8f0c0dba3efd1e87f255e33b618b2ab8343db40437b9071cf21c12a33c2f98bc64da995aaf68efa6daade5984881cae4ef991917ef145c440bc345398d1fb92c3fde87b2dcf0905a2a2d583079e5bc27f7df163dfe22d7216b6dfd78eef2196a683e390f120ff679eb17ab839487050bb73f146b5b37a3fb9e629de2dda7bc459754562f5859ac24dde5ddbbf009cc85e7c675832a5d095dde36d32d4d6fa6c47c290f8b92aa348f4f2a9b7e2fc63bfcebb6029da97987939e7dd27cd88483dad282cd4f9e6d91d9e8c16194f42f1cff341c8916ead5af8ba95f67eb2fb079342573dfccb2aa1de2ba3e85a8bea4d7eed394cfe973f2b6b82c3f2e1c2d62841eadbe115f5fa3b74ecf634074f7a7c5e2fdcf442c3c0cefc0765fcd212c4b55801c7f9e559751abbeae2def0aef856754ddd4456545e987cc578b25086ced505eaeded66485cdd4afdb7257a733286a86d92305e2b1af93356ae08776bc126ea5a87584efc3d71476a83ff2544b96f19a59f3629afc4acaf48e860f8d1a71a50ba4390c5afa76e74a59dd91d14f6160f2500da177bbcef805521f7339a2318088b1b97f95c2473db67f2a967ee89ef995ec26bc2c108560a5282e1b8364e6a2efb23f73d5ddc53c28c95d5d62d542aab25a3009d691c73fe98bfd8be7bdaccb55be26d0a0fe4f708c815d701e9428dbd46b7573788e19eafe4feec35b731fd769b465c0ce42c3f30bccc97267510be9511bb5e09ae3dd7eed56ff0734b618a80fc4352ee3c4197502afa4d5d43bcf9c1f5aaee3727cc8edc21b17d106a74dae3ee1fe976da8fd155ecbe11b128cfaac9cae76f2bb499f48b4d87723f52854e69c65d73bcb67c7a6946fb120626e55e4b61a6f733fd08381ce9cbd45ee1f5c8a85181e33ff3a656597ef6f537af4fe3a6838bfcfbaec53207ba7d5b897d34e367fafc2a1ca56b98fa5a901c2bb26cd6fcdcaff7078b1775fc7b8a1177509073cdb92d9a101fedf593f64968d079fef7ad4efdfe3e2b3117df8ddf1e6435537315be5ad7bcf1c0f4bfa38cf0316a4baecf3b62fb3eeacecdd3633e0fd3831e65f1619cc14ca6d7b77c74159ffd2d303d553bed7b6507231dccd23fcbb7ee82fd7ff071719009b404f4bdcfc8cc09989814909d18484f8583f9b14ff68cf28b790d4b00033331bb7990156be7e78f38cff1efeba4856593f47b8baba9b1b267fbe553c7f7bf281ed36086b88afe94eeb05655e0fe3672d0948766e356deafb7864b5fc62459f86ddcec1d18d1b7a7d54ab94b31f852e53e64e014d6431a6d8d3ab1681622d2a8db07e983f892a6a05a1faa8bbd6c37e59383b7145cdf1de374eb3179ce46c42138a885897a06f4bf1a6ad9539ea7a927baa64f89ad8eed2ddf1bd5d49d0b8e5ce85e285738e3b659bf4b547484ca4fe2fc4d87e61a18403f98d62dd0591e82f738aed9c46a66314aee31e366be60824e56465155b183f3071d59e48fdb0db9b49df858eaf78e7571d9ec7bb54a14f492a7f22e9135494b084d672e47885459e9de4280883a0e29dbe872a334fb21ee63ea7177a6b7cf7dbcc9c4b5f8ddb59644eee7d2f6fef9c9d9c5cb891e9139f9a85bee7a68dbc9704b68e99e82f6df4b44e70d35f547ee3cf128ff27efe5e172af55c78f689bac391eb946bbbca0485abc705d52f3b8d2ebda6097f75c2febad85f2bc6a08a2a7f5e50b2e09673d0b58ee2cc6d9704c61d565d3f23dec8a14e1153e9bbd0968ece40dac635a1a2a469e76dc4fdc765888f2e96e18cf7e78b4d09844fb950037edc4ecde5e5a27cee6f67d1c0443ac7fea5d2b9b565e638a9c080a7e71c3765f76f2dbc3891b1d344ca8b4df1bb69b1706c1d2d2693e526b9ed1684bbc0f67fffe5a2cc156f835dce5af1d62f1441a26ecc08ac17c2da4f4b369f489872e25650b0aa6f6161cb1afb99761e19e9905733ee46079a663ae6df349ba7e69067267744cc8643fbb85c7633c1e0c3ce90958f6b755dbb6c8fc7cd9d2173319872ece6986f5ea3da1be359913a7e8d8f04ff569c7c487f017c527c72f26f97e776e6ec3f3ab3f50797f60d6f62e74e01706a3f9fee4467b638b5f1e5289f89d4ff3b1bfdc76cfcea9722e0ed5b5b2e7518071dc2d33edac303109ed5109e5c08cf5448d5d947a1de83cf95278d31bee41187c03010e82e4895c083c62b6313b3180245f8f3af37ef3a207d33b4feebca26edaa0bea732c35fbcf8eda33bcefe5c36efc76d3276fd9459d57183191910ea7fefdc5a94d3a513f110c73c2dfefacf573818e5a8ad75871a12f471e8ddf5f90220864d6383193d56a2a930f5f1679893ff100f255cd7cb9c7b255fa3a55f52ad723949de1dd6bf8466beeff47721f4a0bf6b5d57a32c955c5ea18d65dda9faa9519162375f3a90caeb2bae9cf93fbf6737418279dc20696755067e32ff70efe50716272d3a799ed6a04a3cfbe46cfe8aee7a4d7c79b591d1bb556e7f7c41cbc34fe804e8fd3289ac429decd898c36b16f2feed41d7f5560fe4cf370e7b19ba17d32da9f28191ba7c63824f6f3990f8540d55697d5f369fd813d56bf6007b9f3647eadb7f7e563b8c837377328dd2feae466382c1459eeab2185acba77f46db24ef7b43adeb329ead55bf4065ecbaa4e24fc2982f8dceee4aa7bfcb95b7ce86b65a3d85caf6ca9ffeef0e7fff93d56a2010087cb80a3404c0c3185abf0a77ff0fc396973fcdce658674487dbf978fbb8bb7a047be3435252fd52fcd33350d61136b3d36707f9842404b8fec111cd441ccadf88d0fc833d56961636783cdedada34cc8e4030b5c0595998db104c896696167666b6784030b3c18799dbe1cd0856c0cc9268658e2758d9e16d2dcccdac6d715616042257ed1722a6c5da5bd999595afe01fe3d44f50a1ff691fb70f3a01def6ce101d12b396f8f72804f2f275b1ed91598f2fc62e5f2fd7c8b9dbc02030c17a9a896aeafe8596e8f483298a65c9128f0f8b5d374d5a3176ce43da6d8638288b4e6d2019920d405a767bbd63adf5498cb697e8b355b69fa6b6079b4c5929cb437ac9fd78c44952e59425523a4d5f36aedbe45ac814049af2737ac21379f39bf427532987bead57ca7eac2f7c79b30565f9c6edcb418973aed4428683ee9d0ac7275717e2aa2944a4fde333d26ed946342b94285b67fd575c097c6dd8802c661c4cde3cc18f5437f7bdcc831c408fffd90d07fe4e3cc5dc4e2f715107f4abdbcc18ce9e32ad53da782f3388dd895e77f0c5b5ff5eae641cacdeefdd5113d875f4cdf2dacbe7080f768db9c04a7b090136f16fa04c6263e939b2ae7ce6725f604fd055b09ad96f0fbb2cc8ae7ddd77bfb3e87b554f1c9a7363d2b0947dbd5b4ca68e8bc5bb11c21b140bdaf1557bc94825f25faa0ef69885560a44d44b227f980ebb4a2f5c159d9a1718b5678cedc5cb4ec7c2ee6b364d7ad7472629758adeb72f94aeb4390d5d4e0335f1b3e1d3927b16ea36170fcfae48b172d1e99a59cbfbdf09300da3fe6727db1985ef016ed67f999d6ea5738b5cfcc71fb96af5e7255daa6eaa858724beff92bf98f9659bcbdb765dd36b5fdb776c5985a5e8ccb78225ab2724e76a3e2c5ed940cbe36cd55d975599f3cb7ef7ea7ae4d10d85a1af84ce8a0b5e8c91dc78b6f6b57df5adcfe29668ec8d2dabaf6cbda5fc44bfd6cf63db59f1adfb43433567a53b89fb4f3b94752113bbebc80965ad6ddb9beae4a4070bdd76b45df58d7b288d372374e3549971f2a12b0be7c04a7d021d90877297616f6a39d68e77795ed7a56a4a4b9e2abe3427def577336dd09af733ca878a241bbb11f1a43a22acbe8d54309a8ab6b844636cefcea915d1754d4ee1b76d5a34ebe80c76d4edad53e1e331479c30438a50b6e17ad96ffe8e45ae2c2d3bafae3e6e8b0d8aca3ac4d4335bb270adeecd0b4994765fac356e55317ac9f2e7a4efe941cdbfb77e6b7111f2d587c52b354a7f9c5de1a73eda5c16c6bafa7ef6a53c2da9ee1eb7f99b5a88e7f81e881201b6feea63c3845f0981b903efdbc52e03f731cc1f98dc6c64a06985cdf7531670554e75cb5db1ee7521c71acaf100b3c1dacfda527e22a671fc77a9dbad26ca5e723f4ed43d18b8cb67328f17f5de5ff6a57f9cfad1408760df625a4ceb74285cf4f4b8df7880ece00ee2e3371b6a9565e5e3e21195e3eb1445458002a2a31300af93fe52adbe27161441b733b608eb7b40061c08ee12f875903608903449ca58d2d1ee0adcd8085998d8555988d8d1dde82686d63696e0d6cac2dedac6c2cb98b7dffede30810afbfacde6e2bae27f77ea0e9f9f3156ef4e80b87fc4cf7eb8b417d3fcf4a9f7a8ff0c67df7ba9a5858235d523e5462b66da74a8b8733d8118a14b3edd823a8d7f34ff63f4993a862f276349ed23c483d9417bd3085c265af713c39cbc2c2cad60a4730c39b86d9e189c01487c7db585b106c6d0916e6041cc1c28a606943b4b0300b33b52112ccf0b66136616661047302b0c5999b9be3389d5ce56c43c4d910cdac2d70006f6d8db3b109b331c78511f196a616789c951dd1342ccc8248b4336520218c08ac6df16680880756443c81886350d308eb174390a84226d95455b9d3f5023532718f7cb9734a0538304d804ba7969ff6bee26eeabde8caf9e2af6e8543b45694041ff926402e8b2a17f4b9731a92959156a36acc3657792723f9fd9cb7d3eec52fc90eee356e16599b7f043f91376a54ec38666b93b71e7b5b6c5d2282ec9d56ea7a9fbb30adf9d83b0a042b6685df3836b5700497a403590136fdfe1fcf5ad444ceec739b7a70475da7eea98f64c4a19b677107020ae5bf6e79a110d7531421781347ab7089cfaeddef45c61d4eaeb315da2153049e76ddbe3c7fd5ad66edbd9ce8cb87403e4178f68f7cb8ea8c9bc894f9fbab238e4c0de5e48870100cdec63142ebd72110c8bc9ffe990c95faa22bb4d1cc3db9470e131d0c5d0a7fb14ce090f370f451df0e02d907a19d879848422420905c48fc1fc81e16a3af6bf4c8f0947b4acef51d180864b2495624d87af0c5b736f6f65c2e6ac788947df059be6cec364b41cb37f2dcf3555af3d3a0c5b9ce21fce8e6d2d0fd92677e54f3b7b723739a06121fad7f3a9b66a3c6f6800e4160856346e4616110d84c0864cff811f941da5fe24f3c80a23d3e3eddf178f14d7911b97ba4c6d895ce95c905a309e23fab8e7425034ae933ba6d6127fbddedab53c2260dbeb7b323de90beb64658472ca5d35e5b99aed56b108697fb9b7e6c9d92b56cd5a7cf8d55fb158e0593e12f9c9de1a33d35202ab29de86a259ca1f4d550bdffc55cc9156275c6dab6df377dfc5d2d215077a9632e645bd7d85a625de2d3ad974e33bea29fee29874020fe575bbbba2152a7326cba49109ec71042588c7e0e3793591008444969f767cb950f6fab6bd7a5ed9b1fa9357ffdeb767c4fd1dcccdab934f8aaeaacd7339acfd4bea8a00abfdf126db8da63f63185f8392689f73fd7278642ee36db37d3b91caec0cda6fbb98d3b26626e381b8db3ac8d6ba3a58c266ebfadadb976257f678aaf9f6f4d47994cfe7a7982eab52b5756ee7da159b17cda9d6f27e73576948dd6cb8d7dabca4613a73e1ac705f89c32f07294be2afce485893d762f9f50372f11b0715f90e25fb1caf1482a7d6adafe6f6e62d6c14bfb42ca469bb3ea73e554354f55546edd34fc3d6c7775c09bad011eaa973a31a9522dcb834966ce9d25a61bd5ce77948da6d41b3bca46f3081a3f958d068d9e2a6bc1b2f50eeb37a8f3879fd19d6e54bfe49c133ced828dd516f8867388b9b60647fb6e978dd68b7da7ab4e46168b6784cdd865ec57bbbf90e8b9f99df8860f34f1eb6b2cdba2739eddbe9ec6a985b0aab0e5f76736bf5c31d985e7b6f65adee591e74ee81eed38465cb06193f82527a7c2d6aa3df88eadc74cd59fc32e866f3eea7b424bd091debc5db0534d15f6e4f5d99069d155faeb24458a792a9d4e56da1b1d37ead826fe71ce8c9339562b634dc2ea2ca405e4aa204137955df6e818efe8d708bd922a317983e3a5ee788fa8b9450dfd6fd3d1d79dfb56958da677fb68559f5abf247d4a3ff72c0865bc38668ef73ec78182dd77e48eee5ce37041ec9be79c812a2199f2dd51ab63e05775f9c48e1619451fb07e1f173cfb565ba4b15df28dbb29e155da86a9262216c7445d2c13851c163f144c9f79a37bdac375d42282fcf70d79c7cf57152a0aba859f15904d5ea11996a3d98baba2c5c1fcd38f7f5813d23a5d2fea4a6b5f66d9685abc0f5fc57bea70616dafefb416fe85f573c54af6bc4cc99fb1c17d7750f87d5dca2b91738255ae4fcc92fd5fba55ed304ded22284a4e2e409d73c8b768ac557ab4e3d5fa9877dbaa22bf9e6c922d322ddadfdfbe744160976451e3a9f592fbb3db8951a517b20f9d037d47cb46b32a7d1bffbe54c1aaaed434b80b8bcd095bf4a9f0b14fda34f3f2cb8aeb2348660fcf4ff7bc9cea50ebcba906c1beccb2d12c561f9e63bbabf25c5cb2a977ae84db4b4bba0bc9e51e4e7bdb3c60a7f1789b7cdda263e56b737d38bd9bafef76d96816b5ef7495ada0a9e8f6d915522e3ddaaa31b56573f7f7261cffaaf0d8f992f1f1cb47df251657096cd272c37c7e7d699efc7c7ec8cb3d3e5234ab3c7ff70c9e2b2b2ebcafd994feb60a1f0f3ecc9f932368f4d62a4b61d221dc1dfbdd93f779583cde92af71a94e23a19a2371555b059dbe779f14f57257d841bea0668f8b36a5152ddf9120dad601576b87a45757f924a9bedc554cd87233797fd6cbb3beca048d3d414e3eb61708c9af2edfbb3fc705d6778544e3fbd003a1f1f317a51e6aaae2ab18ebad1257de089e5bd51d3edaf05f2f6352d8ac84485848c996ce3c0e3663429382a3adcd182f7f588cfebbc68b79f0357ab3841a99d3eba4ff5c1c114e0f9fbc54d4d365eda96e9ceb47f1cb8b2f9ef6d95ee37f41756755577a9aa382def3f8ee4756f64dcf37f2af4ecddb796e227144da897ddb2cfdd35bb73b8a862284b6eff115e90ff9378ef87f248e0870569981c91e3e38a45f7ca6af851b8e90ec9512e2e591ea63671d6b994a0cc2476706795a2547f9e25cfea7e2881638739cad05016746c49bd9d9e12ccc8185a99d350004333b1b3b3c914000786b1b3b1cc1da8648b4b2b4b235b5b209b3b10cb3b3b5b50396e604ebffee38e2812de9e64bbd88be76cea1d511b7c3e7ee5a764af3d5f4d532ad5349f796352764aa7dde344bc33c1e9eb263565ce595a2775ba779de59e5707ad57c09fe888739f75b38c7115f91a8c2fa9554c585d23507f663a68c95515c217fcfd956626c23ac5fc29f44156c8ea06a449ad64bb853ea90e7b944011f770e060f87101c3f87795d210e281465a3ffc3fe906620922ceb19c6b73428b763edb5fd1fb95ce9cbf990caa8346936557d2bfebdefdc52c363f3f71b23a50ff3e9ddc269bfe152909d21ffd31f4e7bf220bfec97e9b220fa16f1afd008a979fc74931c3ce736b67dcc6330851ed7c0c33c25b992f412da4331ed17ebe66e881b58c493f1b8c2f4d9a9a787793b969e8b7f810c579dba655a43f129a91af51d8acb1ba3ef247c5c92207e74f5d747c71e997566b818ec106c51fa439cf24e381ad994e674f2edba6b1d011e98c25697e4a3ffe2fb7f0fdfade7972e2d5fa5f2947eba69d66279e4d565cba78e79f388de2df19eb3fe8e746ef0bd1167d2b02f60cb52d763def744b79bf6aeba9ab67dc56138287010bcfbecabf3e7c0180f227451ca55edbd27cf4adc7c73b7e56a5076a3a3f769af992deda2953be294ff10a7282bd7bfca2ce7179e6f201774ac5fb70ae228d4f22fbefff7f0bdfccd4cd9be8ec0d730c3c735e2953344daab9f8eb9be4aaf5a4bf1a321779e9a88474a52deb2779438f70dadda070b15dfe4bcf5c9408612248a50dd6a04e7c0b07d1bfc2a8d14ce9e4d2fb4fef490e2b2797ecee4a957663cbe791c2dd3d4eb95fba7f4f7456df317b797cdf1922bd1af962bdf104be86ea8fc4fe39bd35aa27ff1cd3dbe27dacf504f6a0237f8cebf1662f89e5f5eec44ae7c9927c1e250b10a6e5a6b6091adc18195e82daa0662ea61c1e8c36fbf6e92fc942efe180ebf503fbf97fca44f71f35e2b4a614474a6eb9fc2f7cb172e08b7ce4ccd9533bf214f1c7c22d2bf634defbff8fedfc377d08a693a3e1f6fcb09ec3b5f61fb85b063cafade27e3d6501b4ee506df2bb64f756aa2c6d1f5bef04cdfbdefbe33faf435d9b4b9294bc2bf2647dfd97cf56ae8dd4ac8f49b37efae5d7e54663dde40b45c2b3d4a758ef0039de2a8d60793a3e3b9f14f1a61fdfc2306d5fc7f6250cde579123c6cf57fcc7ed5597be5f68775d15d794584e71883c987d7ffeb34fdef095d4becc2bca3c7de697a9e2357be7e2dea94f972d798fdac57ebd10911d9c595d314bc33e0812709637cb2f7b2a3fffdf5ef531b3e465f574bfd16ed7ccfeceb97a34ab6d559654171b8cf579b94bf85fa9af8c52c4ddc78e7d6fe1d45620f1b3fbedd64fea70605f7c0396a4c0632413b3b2854d2554dfdbbf18d43ffe2fb7f0fdfe2d70bef5591c4f0e597d69ac985983bbca51822ffe6a0206b950731d3f7753bd2d9fd72a50256b7fed9bd1906e1371695dffb60de297d316481184f4a74c65d2795d5cda1a1522907339fb77a1a7cbce5511dd0e9bebd7cea5c25ae8c4a9f2f49802a2e1d7bc8fdf9ab124e78e5740bcd974658df6c126dbe5519847575f6af47cc3c5fb7d3f4a2614f3a7fe1726dbe2b6b745e668a8e8540a84c7979045f0183ca8e3ccd6e3f81170b765217065ca016c574d3a0fe4f641afba1cf4954d5678beaa1bdc6366459eeac19fb11d7cb6ecd5a35a342e6d0d4dd918af05cd28bf373e9d72792cee933e7292ab57cddd129a80b79054795fa8535d7ac64bb6d7b42ab648c65a6241d1d27026e2cf5b2e3db22a389b4ff978fb6f2b857ddfd93cdcc1cdecf71952cacef32f35a765a48b1ace0a12647e9cd6cd71b4f683b31cf62dae184f84f5d8b8fcd5f8d287faea7eda5adc067221355a0edb94bc460b5e8e3ea19f312bf6b8e23b787ab135af5c6aedf631f6dab4ec338cda5dbd37b1eb21d6429e1ea4c85f753ffd24196b6df3f1fae0f77452c5fbec93bb46cfe178ac08ca3df9f759df8e212cc9b70c745e9c441293eda8348cd333b4265273ddbb8721c95f9cbb4b2fff1dc4d1e8a57ae5e5d6769df7cfcfce2b59bf6d0b9733dff3dc8f22fd72f383d2e8a884f21127eb3fc886965628a9d192a213109671d10398390ea4df0748d4c8bf7b68b730970370fb0f60f48f40cb74a4ef022fa85fcf7f0d705dab7244bef3d7240fcd38a84a2a551b6010bdc34675f5332bee9b5f2bb44c383daa0c34153f78af2ed6a2f39c99352acb2ba0bbe6c55e857b3ddfd41ab62def9cf3ec371dab45ff40289aa2ca3552fdae4aed6bf82cbb11a5b3f0968db2cc9549c9c761fb1f364b3e91dd52fe7e25f8d4ceff6df10e2db642fb5a56a19798fe0ad6bd22fbad8968928c93cfdf6929cb4739a619bf36a8c8e9e65f90ab6892bc579af377c3d30a562e1d7e850fe376715c3b766068d4c4ff4b227464ef5f45a2bfde138ad3231f8034e5e8e2db09a436bddbd5eba2f696788db229ec6536e3d996c13e0ef5e5db8b53157789ee9b7454b37dde0a7ecad3c6c3c327d777351a2c8c7050b15bcb3ce9f5fef7b4be3a519db58f75e056d667afe93258f5e3e1114aade71e893ec1237b6c00072fee5a8cf7934de3dc117f883ed325e5411d98e1c092afc9c5564555afee66232f6549b7d6064c9a428365d093d575cfd72a11044f8be3aa6f7a36b7f610ddbd1e99f717587b392219a0da76e3d5ba1b06da1fcfbfd8523d3af5fd95121de294cfa7a4366f7d962c245a71e53b603338de5971cb9d19345da7eb77473dfc3a02afddefdb123d39f48ac5ab32dd19a6f7beba5dc677242569d07cfb39d48d9f0c5a717f6f0d4b9cf31198181be0317ef1035d96ed43faabbcb6feef4011eaa5da66063dead2fe9ae9a6cfab4f01add08d7f1ec9c6ea77c6492677ee38df64cb60b46523564dfeb202b883a7bf4d6dc13ecdeafac51cba64f370587de5fb5d62264f601179b02d98b9d91355bd9f05d5629b4957c17ba5bc7aaaf317fe1bba6bbf6166cb7136eb8e57651e8dbba60edf7fe5ba5f51a93a78a3ab16d4c9ad4e05d641bea16b04e525cd8fb84360975e522db040c2c1437f9fe9d8180f46b9148876ddbc54fbe9dc7b66239f13356dc7a7dbbfac57d1927b379ef19ab2675f58f4ca7d6c4165f14ae0fe981e81c145eb90bddacbe8deda0c305069debeeecfebc5da51dbd557199ec29c2d746b65b109c7417cf890c39bb5a9a87b76660aadaba4f898b5f8c4c6f93db107dc268d19247f3ef112e88be377c2c59b89dcd951b306879b844d27557130fbfc69af81dda9b67948c4c1783589daed9b9725b7ad1e659eb491ba73e93dcc2265fab7a24e38bef3cef8468be2da3ac9b9a1632e9c0ed91e96bba5b0ef2b4fa2c5af7f94081ee2c0bc149b21fe7b205869b6b7b6ef76e71288b49edb7cacd5cb941cc886d2cfb61c174180416adbffe4bdea16d17f675376d2e64b3b7bc07dbdfd9c36e78bc9d9afe267492e2f444bb0dcf46a627bcde7ac237e2e0f7f6256e0efd6f2ca4ee7b1f673b8aefbb8ca5447d57ed13858447e78e2e9da47cbfa0ef2c5bff38566ac02f5ccb74754c47f4571d7ca1e8bd507764fa541ed1f67611a851b9ffec1739076858ffab166ce76ed695bcc255e7acd973f4c29d957653bcda9edefcc4766d410a59ef7a497bb34cfc15abcf777b9fbe69a37e620b1c0a6fab6d8f5eb0fbfae7a88849a591629532f7ebd9ae1d98f769c3d1efa5ea062510fb872657dc059afb2ad86eee75960f9a42907b6df15266ba89e20763e7c8d07a367c15de407ff8b65e61959d56b7968da09ebcad5134dbce891b97e735db77df3ca2e2b06c9dbcfe913aacc25936ef5a4575e3daa4384ab4c2940ea776a79e4eed6bf73247a65f7dd9f3257476df42b5f767977aa9fab7dd0bd0f3627bffe60fbce8673df4cac604b96987528c664d2e64f33732340360dab7e5b6cd53f7d7b8c16b348dc776df9a91e915adbbe560a5c97147b5491f6f9b108567f24e7a3e32bd7ecbab4de1a1b27a37e5e817ef06d4f352bff4b29d7bdabbf974ca56176758554a63eb72914a8de9091e5923d3674ed51ba8e8a26e3c445f29a41934273e796968d9c8f4fc3d530bcb555bf8be987da9406c8bfa60894b4e1c999ea3f4fecea46f1a298e464fe714f2e62d9c1b3095cdbe3d6d3b48766dbfa2e8d1f630dcf3dd03ad6f72829123d3e30fa9b5c5bcd9acb0285ffa2be648c7cdfef6709391e9971f5f4f7cfd6869084c8ccfde2dbd4f4a37fa20db3d0d03ba5985b7e2f2458f174a94f609bc0d352ad65f38325d2e1e8ee3d77919927862d772d917c04677eab3d523d3112e6a922fb08d59d3a6c92d3e94728cd7575a9fed6a921cdaba9c351fd7b81cec74d8ec281c175169bd896d9f9214feccc637a7f7bc0dba2c443c28bb2f62aec21bb695278ddf2c361edc36e98eed9b03f785563e7fbd34e109dbfbb3b62cbbe61c672cb07a46ab93efadfacae9e1fe6ce5771e3fac58397f8a205dfb3954ca4b6281b1fa5436fbeb56ab15159876ace7ebc10fb30a162cbdca67bc8b6d8fa56af2fc2bc2e7de8706d75688247bab6edee2fe94bdff88a670b75ad5c752d5a735dfdf4a2b9bb5d39b6d40dcf4b9107fb73cd6e146f39e580fd3b7c481f41d6cb134bc6f4baf88366c9a1f0e051793ea3cf93ad785ed96d2f458fae90327b473ed3fc2e707752dbba9fb298fedacd92d7097f9942c74d9c11e4f9cff8582ab8d042adb50cfed50b36b338cb8dffe7194cf866c1fe92599816c1b07ab64d16714a10d4e9967ec6e877f733f577c620a5b8cf459faed38dbcfd20157fd046b4f9fe8c6605b8cd9f8afaaf9f973e92d5297e1fb940582899ed51d19efd8fc1f0246fe88737c817865d8f9b4835bdbefd44716b1f9071b97b56ce814b08a6f30ed489bfde41eba34588f0d9f7117456f08fb79592c31b35ebc6dc59b53dbfd9cd8fca318fd9055bb2d57a043c52ff45c7d82b8d0e2f3e5dac8f4b3678eec96b9bdef48c79625f6473224bbe12a30b69b37aab4b527cfaedd446cbc269d5074b01a53e2dcca761d65e1d4552d364fa5a652366db6badfb143a8b5ee085bfb9350749f986341ba0b32e6a5f3f4d7fbf8aed261f39fc80d9b420bf905d62c322df7e6fb723169cee606b673ab3fdce3975505ab06e687943bb67cb9bac157e0359b7ede790cb9f6a4fbcac5c42bf3d26e262ff79e752d9ecdbe675bc077753e09689a54334fee00757749f0333d36fbbe5b3570b3e857f3b57e41c65730affcb5a0551fd8f4af7bcd3c58abdea6c06be6c8ba596e470272a7e9b345a79af5f164e8ebea1bc7dc5697cf53d6c06e1668623ba6f1b4788ea7e15db112a441e7fa6b028dbbaccf5f7760f3cf1e770b2826b8e1a3854e97348b2bc96d6dfdc036def6d69cf7e2f6359e3bae9b27ad3e054931b3ec966593af2c0971de7bdec2c2fa1bb1fe1bdb2d7bdee5f8b2f9bfe22adf8e6a474d956b4a06979f543caf5cc5bbe2e35f1cce5c30a8d0f59a34c75481aeb7869fffd04d311549b61be2ea5acf9e0a1345a8bb28eebb700ab228b004bd96cd5c2b3d4dfd423c72af7a738b0b7dfdf669a4f27d1d6ceed4ddbab4f697cd0a1efef146614d6f8d5799bd3c11c016ccbbe2b249e1448c46b3febc4707f7d5dd201fbfc1c6ce8ed90e5a4f451b176f5f21f1b0fe0a4ae9597f375bfd391f4ed043328f596c7e29734bde36aa7a40d981eda6a65653874aad34f2717193ae1b2127db349d166e602b3f0f91319798d8f129745d7ae5cec6a774e5fbaa5a6ce664579647bc90d8d59ad31ef077970e20707733d9eed46c98d5977df545846421163b29b37f52d403cc45b663c216ccb15d23aed7b18b5efa6af9ea1d7d987d1f6bd886833133c80167fb1c5a947a1a14d66daf2889ef0a610b0666b506bb7926bf934821fb690b3e35cb0f57ae60e3cf8b53e58d15170b057957746a08dd0df836c9be812d3ef45864d5a6a389a9c202ab6c29d868239e49445db673ac3a179a539ae46fbd100f3af980f88c5f7f5153a4cac8f4f2c0f2ddfefb4ed8544e9a1fe6d0565a2df92283cd9d38b12d63f5c21af7a467b2027233845e69182e6e6733b7be4dcdd46f21d70a4e3aae1488d20c877f498860db68fa2eeab05feb8687472eb51172662cc49ca5d7a94d679bf28ada389300c90d6d7fdb135417b22dec8b97089b3bee3f7789904968c57229b057becbb5a775172991cd9d5698d7b05fe4d1c298f4a2dd9075ef3bc342e2b467b199a3a5fa4d3daf8cd41ce7e4256c8d3cd6db5bedcd361cca3488dae174d9f884f3a634adb579ee4ba71054edd9ca7b95ff3ff6be02aacb76d997ee4694eeeeee6ee9ee6e50babb435a4250a4a405e9169494140404a4431001a5a441b8eb9cbdf7393c7bdf4ff6f7a1fb9eb3eff75fcbe582e17dff6fcccc33cfcc6f7e33c6cb402cf11c9f2a2d1ad22e1c0fa309708785935d832f695d1dba60b962c5de1bf9a272fa022376e8905f5a7661f055253d2f8c37bce5efdc74bf161804db8b6f9e787899da6e94b82fea5eb44a0e69ffe602d08f9347625a614b2328553a446fe1e7df51709f6203cb0d52279edda9953bb2d0919ebde63d5997334a8039b3c2f19004ee42a399bd531ea1223f967ba7860dd02f873713cfdca351d2943e3d576f8ddd1d2d635005d211e5f8432be7b51983a4bbb2dc69a664423cc65200ce25cd4a381a3f9792c428b7af4ea5982ccbca968bfcaa9cad4b05229eb83a8ef563e291d3b382c0fd3e4bc05d37d98f9d3e3199336633984d46311aac7e33850de0c4ca902d9a820362841e094d4b5dfa8ccc1faee401cf1f3b82a025dbb2603467680b52df6314b2c29517c051baa812a23ca3535452379f7add597aef5d511f179079de61ddadedf7377f6a7ee1fc92b018866f74ec40e1aa1c9dc7cf5e837c66847b4fae79f5f1989e4d9c02e03f3f731478852cd1866df5d8c0ebe3de3b8c62e11c04b643c6c803c342d6f0cb5ffb3ae5a2690d42e20e80a1f24e8270bea5bedf7ddf89e640117f2cc7984cef01b60b64879f37d71b29a40664482c968b9e6e8e89707001e1826b5a024ded4ba1a04e1868c16969f8faf570a091bc9fab76a2904ff0b97f862f11bbc5a87e5da700f0fc368bc858ccb3a23e49c147761919b7370d1abb1c5d957b989de124244ce0b9b77ad0f8391cb9302f1800e19a2a5ae60e6223c97328de6f944f39a674c75e8c00ccf4b73a50d6058e383c5e45273e89799752bae0f005e0a5268959837c2862156af8402de1f5c46b8da3c94d80dbdf1599fc9314ea8bc1a83b5ae3f5b8bea80c2a8ec076f9429dda7da365dbba65fcaef28644baced17d09201de6ac10b16c0a81031df5959a66ccf839cc94d63320dde2141a354ae69354343fe6f2dad4346eb02c2e1908775f406b9bb14840cfa5abd6d205ed98cceb413002f6dd80882fe3f7f483021db7f55cd9b3e5b721a9d10ec0fb4168ce134b6fd76c497bf6ca53cdb6434f066f04f05fbd6abe3ef2bceef20ac8f0b530ef05dec63f0586c6dd4ede98c37c9a01dbb016d2ed55bb3faacde905bc1ffd4be54e5eb6594c04ba24960b5207e1baa7a2006bf4d2a7781278f63cfa5544a9dddc466d1b970864603bdf0e9349ac30ae21c448c698e0e7abfc549cc6230fb06f755b830c4503d934bc6ef2a15725bb08c7d24065683e0b76693b63e1fe913eb7d419e4a10887950110ee1a7dfeb228b6edddfc0d65765ad4d41c42a14508585f1bdbbf4c882b7e91b6799dfb21e9fd26b1debd516090f850f170bd6d886d2bab476673f640c47acc56d08bab72edd03d428c77045eb01f02ec67e1ce4b30543a80f53f518d98a0a1de83618532b0b42694b83ae5b92aa07f2927c8b2dcb73713858fd3dfabac86cf23597d03d687e0f6d55d52341c3436db594b4dc31e3b57aa97c0f5bfffe822a5d7d2e234d1fa6e6fde12bbca99ad441d08473f402506f708a9426ea5592709f3597328e603a560c36a9ae67788df32fb75fa21f53915554edfef03dbb940c23546bcd7bda1e64677f3a2159173544ea83a01fb8adcc3729f08aa4fff8c2dfec835c4a1d6a60b48c7cc6c999f417990a7b14d6ebf718cc7ad9e7b75117855fecc482881f52d9f9ac3639770cf6282e14feae73157e535bb849ece4c4deeba311e1b64730f2db53e3600f61f91d7454ff12d77cea98183cc77fa38dba3211958bf9394be7b65c4d5a0d4ea481ccdef9c40eb87f301743155df1f98144b9b9b26d95b07df8dfd6c6ebc38007c3fd45e0cdbe867ca26cb3a12eab6f5de9eaac343c07fbf2c9e30aca2403072f01e08e1d752f96c98fe1c48373c8c3bb3cdebc3c27179bccbcfb7aab2db8c7a09c43fda12be83b277127d75a83b096b32063ec47f0e8d05e2dba781cdd6fc5f3e8c617f67e708343e1f0e4f002a2db9c3dfbacc04565f0616a94255887da0bf989704ecc313cf5d400b7adf513f72a915d67131bcbd9a1860979badb16cf2f4e5555209466bcf7cd22f9ef4051bc0532ad9365b8beade823126e4bd177747fe6be66c2a906ebe9532d86898714477fad45eba2aaec85db1b504f00f6eb8e3814f7c0dbc44eb85a86b88105364b1fb8580741af4de99404f2cdf063dc1cea3d40f8a0c0a04c0764bda93b738a1c7097b2c32fe93eeab7dbfef01e0181e9e8f0fd966bf6493b47db485c98a93896521ef22f8d7f5f33291690f57cb4994e7cdc748f1e743953e947c01cbfbaca5689a1a57bb7830e8c4fa76035232c48060bebb11515a578df9ef29bc6a3ceaa0937386b683e26b2f0ce496cea036b417f89f5d78fdb39ff7a7155ef5d82d2d4c796cd975adf4d435ece56dbd1d5ce5ad34eddd3864d4e4645cddb82c741cf9acec2c7ccc7d34157f22a2ea863bff9fd0cfcbcdc667ca6ecac7c1cbc76dc5cbc3cd636acac3c1cec5cbcdcdc16bc5c6cace6d6669cec36a6ec1ca6e6a6561c163c96169c163656ac5c6c9c3c5c366ce677eb38922bfbc9f172226d6c24136410fc909a32a897108024f5986a84c276380b607e9c0e9022d80b8cdca593a2993920bf2e00dd5da57c8776d89fe495df99ee6daf0ebb023a11ed78f78fd97b9bec602d2ed92cbbb812da61c2285f90db6c45e165e35c9c119fbb75c250809616bf2caf80b97e8f2bf69c0877c73dbd789fd3daeaf39d4b417d91f8f68b01da6a107f6b21aaf6bdce37f93ebfb039c1648bf25409cb617bc88fcd7fa4923063aa7dbef3134231d77e18321ba655f74e6a7ffcb5c9f9904bb8ab682ad988c99331b9fbbba37bbb49bbab7ab35af92b6a7b59aa9ac92bd92bbbb2b8fa324afd93ded7f2bd7676a69ce63c669ca67696665c661cec767c5cac9c1ca6bcec3cac1cbcacbc5cbce6bce6d6965c6c9cb6e65c9c9cb6bc161c569cac7c76dc5c9cec6cecacdc6c377b3a5eb3f5c1f2f2717c7af727d3d0f9a35ccf2d690d86523c318eedd31dd1d91405013881da0871cbd6341eb9b258d4aa897a190375436a36690106594f2b08992950b39a000ad8a46533c8075f77ad7371a49737b275729f4f06f9e2fe89a81b32f7c156ed3c02f56d71d7e9f193b568f366e46bd9603be0dea0472372814994e20940803a10972e585731c503f96f1ba83be2b1b5abc380b8dfd117ab6be76fa7df235e7cc49f71d6bf3681a2bc8fd28555cc3727416c5955976ebfbfd07f82f2248cda94a6984a963ef09a1388688154f61d854dd31799a9bc03569c7af870597603de53832cac47b34c3712429583163bcc08cd27f04cd89e993f5cd8af675247bc11312ce6b340312a6bfb4aba32d32f66bd0c9f6a5f4049b8c2db5cf0743ec3eab957a51a61cf24dc51eaa85c01f9dc1ea3eed7543776f4afe2f26a6fa80cebaaa4a54184b07476d7b8ccf96a29ab8830ad256a345271e0751b95b462ee5bd0b3ae7d6d6dbd699cf5f095408e99f05760b128e5442c00d6028e714dc0c4b7d82a51a148a841b174ad162dc84f5331a74e06ee630a0af59b0e0ae79e088d7683aca350e0d1db8bf8d5331df2a2a4b19b9cc6a6db6650451a11591e81b3adc3f1b287ef9e7f7375044d5c3d1b2859ca07925dd56dce9fa4abc9dd1fd9b1177740f05cc83cf3733fa11cbba61cf3b4f956d2a207c5c97a521a0bebe8b851ecc095637f324fef0f084845dce149e43f396c69222297f1155493eaab4cdf783af902267eff67c1ad8b5357f56839079b82039c16ecb707b2a4bd87cb4b7e127dd2ebf7fb57effd975faebf51bf26947f24cd443b57bac235bf6106b19298a2c9d3f3c43914ada4df45b30ff05bc0b2e14a51e3c091e692a8b49b79f9dcb07bac15df7c2182df8e1ae13ca0c87c93bcaed3d709a8cd61e30b8b393b15d980fa5a8e56a14479adee464f6ff2cfd965c4c175f2e105e65d196344fb295276fc67b05f9a77efffbe977a4f76c3fde3eae69519697643c1f83170f71c4c28fbf02cae726faed8799f4f242aca9bb40d9952653365322bb3931e7c967461f1ed859e5d4ed7446ce2ae2cfb8054f97dbe80edc0769f0f84b7c3b96dae58c397b9783ec9537214d7f166b806f5c15766175463bd28b4a454ed99749629e44cd7fc627ff7efa2d4dce32f6f912d6ea88cb631c8bc0197b5d3dfab7e370e24e9518c21b25fbc4d43c3c909f311b90edd1845977af12702077eedc6e9c1e9f96ae735e6cfb60997837b5bb7d7daaf351f98b5a5e6d5c9215c35c7972214fed8f6fdd31785e2fe952fe2cffcd9e8ee827de1afd1c5aff4e7fbc05c732815b1aee9ffafdefa7df0aaa3118b80fa43f30b11192a032482738d6621cffd6c19a5c255e089f6e167fe72e84fac2b9f8a918f504432a9c3dcba6cd694da3bbf7015d43ca0e87f4ed9367723d2b2e2d66ccd0060fcc5ff5a5600d626ca10cd733b4e711ed1008257fd6b3fc59fefbf68b1d2fee37a31f9402122712deb3cac9f04f27fea9dfff7efa8d5ab337d4e8eae7d8984503717826f6dd7ea4b0fdb70ea67b8635c6b4f9ee46fa3d20cbb6a4cbc76dfe928756066957c963f26e9b6b050a8fcd42d8791f172c3d8147da3425eae50b2d1bb6fb34e4ba3ec49e05c89facad9252b00a42f91f549aeadc90f5e5a753a9c2dc2c0b0773cdf9e1af79a1c880516158b289d1bd7d354bd069a039d1217b4aa8f88af9d71a2d34ccdf1b6dddd94573baac95f2487e66c73395a9ba113a65fa5f6cb490ffbb8d16ea3a26081831f96f0faefee63a3e83dfbfb183dcbf891e59dde7fded59e59088720ce576543fce6dfa29fec2072c4694c84ec6482a401d87826051b13f7bea8cb74f89a0ac3444400dcd88086db78174eb9997ec269eef29a7a8de9ba82a6e3c5b17ce75b63c8932d52dd817e376b77f56d05843c2363a56a22b70af8f8086afe8f36cd60377953fedf3dfcf3eafb3bfdf3fb507b4cfdfab4756f7797fbccee489fcb61ca95a64c1c08de057dae722ae1c8bc4700cbb530c678bde1dfff4a738f9f0452aa3facf930c16df10caa442a91b4b8fa44232f76926c9f3bd25a451ea3b8ec81fcaa01e500cc17c53a415f4b3ec73ef43ad6727aeaf58a08a70ba5cd801c588e1edcd3fedf3ff3ffbfcfd1b0f1a8c9be891d57d5e9a1f9ebe5256feb744f409cd9c0c309696bfd23e8dee40e49aecb33698a53dd78f3df13117ef61d1d2ea525471640c7ebdeaf256d54f11f5b1c1f283ea70f91986cb89d865983a05ba8a3892c49650ab8c431f11a99f659fab46b7d4a05ea9ec0d76bd7b2d203515d1cd31fce94ffbfcd33e7faf7dfe5e3db2bacfcbfec3d3237dfc4dc40754ceaeb525ea2ce5afb44f3a8f26c7c83d2ae3684548dde84b4e084838d3685259d531af42a3e092b09932cad5e00114a895dc34195887fe85ea3c3c78bbc4ca881a6571245aaff7d2312e37dc544363fef7a6fa4f83f97f6130d719c4efdf108206d3f99124f8d963ee30da114dda52ac8f9e6baa485fff68c0f94fd13c16e4f7fe4a836958a270ae7f64f751949cfde593fcbc578c61d8fde7cfb24a3e73f74d2a89b4ce110bd1e9c159fb4ac4763fa48b4dfee6906382228e361bde281469c7b1c9cf4d44f0cf19c5dfe04b7f1ac5bfa3519065e9e0d0a88639c57d8e9710ca97343f41f7f4bfc6287e330b42095d4aabe1774d3230e16eeeaf348a69d6628ab07c7e13cf38bdefd89f7c6df1cd79dccede8eb4e951c0dad6d3f0850752af5657223f99f6271f0de05eb22c8b8cb7bc30a2a82cbd9b649dbb6bfce15ac2f9b6fea050f40d9db0ef7539c250c79541a1907244a10af2a28df9f6a544ddbf274edb2ee9bf37a0e20af9e8c47074b48158575d5fff3a32bcde9b5987b0421bd4480416ebd3555188d0f44741a1c50b2c7f05bbfef5a77f6467a5121a77854814a7f2a03371d7f730fb540e7797e93aa7f82ffa96e3aa08b43e9b0dab7ba16445998df9162b653fde7f40b8675ef3fcaef33e0ce013a685dd840eb0a0ebc43b402afb7a98d719641af59ff000c8e6cddfa0093d81d9fb5b9b05c953d65fd462a155d11155282b3bd26a66bb3b131eb258801147d4f0ade6039b19e4fc98070b6e456facefc359ea79ab5abeb61a8fc41fd05efe638b8578db765ac0dd510be7fc617ea625d6e9653305e26b5cde752ef1cfeeb25ffb4151333533f326d536bd7fdfd2ed0f1cffdf2d16da9a72ee1a627a6a1c8a4e5ef6aabc6a8e3ce2ba7696d2ae325ef66cee1adada966cea3e0a6ab61e92aa62ff5ed32259cdd8b839d9d8392d7978b84db92d7979adf838b9d8d858f9ccb9b8cd2cd9acb8394d394dd9d8d8b839f938d94d392db9d92cb9cc39d9adf8f82c38cdacd8ac6e74ffbfbcc502ad6f7d2a4dae2fc2895987e7de035e14974debacee8f5354edc792edf777f936084c0b05f7d1d71e4a39952be06d31971094c1bef5133b95c627c07d5dde4ca97cddc60b6a34128528b9a1b6f12f7cc6b49f1082da4ea047ffe60809499efe227e63b64073d3d305a9a6ba6a31b902fde66f318d5477c707e505c943ec0a342650c4f753910287ee8f33d8599e8b9e9ab3bcfa3d8e50583868b99dd62797abbcfa311c9fdaaaf6e383d2ffd98ef04f7ee39fd66b66c1aea66aef6d23e7ae6aefa1e1e1aeaba7778f4ffd1eb7bc87a999bab3359b9dab85ae98292f3b9faeaeb9cbff9ce72bd6f34655ec99a284a685f1521599c8bc1b9ba6c770559be8ca54618d48f522c12b2333d895e83750525be40cfb263818a42ea7a671e38cc8c8fd9fd68a5ec78a255d6be8c74e41a184c8e5fff1afd1a27b5ebce586aefbc7ae1fe924f6af8ccac8e5bf80826c8720ee898c56a35db29ee5207fc89dccf1e9e7c0fd7c3c44c3ee7b39f93090aee8f1a3c8c1705a2cfcd6abf2e6fb31a773e9abf4abb39cf1185f12849cf8dc01c6d2537c0baefa0fa603bd5047dd17c899e1c71f18807444843dd32de9366d46675cb5b18e61c1c891a7730d4060a38374cfc6155ad16d4f654e8aa54afcf331294081252297b4847fab1f639d8ee8c433f5c0126b7f06a0f891b2fbc0c1be41a1da9483c507ebeed40e9d800f50f010b64956400dd5de694b3ccc2075b42bcbdc5b5fba2a67947e94c79cccee87395e24cb09a7dc7f4e150fec0c6bcebf957daf7ceeba6bbc204e7f828df6dc5d039817e0cb40c1409856f555b98cd0928901bd81bda71da080d0fb70d0e3a6a6ec21bfa07b51085bf4d1c55943f6aa5c3aa4c1535da202f2e29d5b7870cc236a2ddbdb40d4c1a9a958ad7f28a2d57c40de790b1a15eed5374680a22664ac6158382aa1d48619b674aac592b24ab407d839b39eef89d604afe5bd241daac753a0381b905401285c6a9124adbcef0e98d5101aa3348ff13d3140a202c0937c6a47707575f81387632e6110a11a92513ed00078f89ec972bead8fa445614e8bcef959207ebd383bc0085ce46bb55a85f234afa2f78298ffdcd72e3c9a1b5821f9b1e737b66add2aa1b106c5e70e9fcccfae16031d831125b319273ad65c6722997caf9976e624de5701145bc26792c12d8f95939779e8688ef88d6b1e072601cd29dd16a9744f4c2239339f5a920a984023d8360f021476d14353f9adf77ceff324f004849e57ecc4d3a303f7bfc6b4c842311fcbc053de4dc5373c32909292ac71558e064d1ae8ceb4d82c3358fdb1393ac899e41819d83f6be16a8ccee3bd9f64befb042f337cf2a5bac62190f910c310c5ea917d838fb4685e818ac070979eba086064ad8beb3467519f21c07c7f9fadf7e1b34341515b80e2ef8190b84ed5bcd57d82780f4cea7eae3ae7e3daaeabf20a689d184ff4a016538ad75face2a50598bc6d01464b1f6a8a5971d1c1d4822c290c4e7a8f690ae71a407f3f40fbee70a4172346bc96598162e98895c1c9bf75553e5afaaead1dc736b6414fff32d6cbd2025b9004a03849151e51bacc6dd2a40c61fe9c78f2f2893f4108b0c243b5628c1d6b0ef33e33f3b016447bf90e9d9d1fd875563eca585d080ab1d73aabed68d140ac57982304ae4f4c3955707cc896cf138787e2f53ce64e46e668f055f9c35871d4ad2a1bc7505da5ed61c4cd4597553f80a2e9f5e1f38d73991034bd981913864f39fae308ae00fe05672a786f3873d2fa95d00e1aa4d1f4c35b35920023ac5b16329d0b24cd8b81fb19b81f3a3032b7911fcb5d95c3721ef3b16fc0d3b5d718cb0e50b71badf2de0218a333b3e79513dc463965c965e6f85b4a2d13d3d03cafcaf56f735caa3f58c3d7f289c34377675ab877be092cc6ba1cf625b77afa3f866e4d91e63a9084c2c6cf0014502aa6e8afee287bdc3ec8f9e8298948ff550c010e1885a86810fb5a6eee9281d5f89e0932a3182f2f1492d15539c75b71abec2a7bf387332fce7786e210488e88809c867ad8865d67038688756609594d20bff094460a10813e290e79b4eceef81ca7a681b3b1355f2d7f741ce8a69df3728615e96d9e8a10fea010d6714c3d4d69095008a2a0afbf86b913660289edc7c2708768ddc4e83310c2c27dda44547a37fb80de172eb54de844f03b192d90bd45c1b2ac280b41935b97cd4c89518f7c99ce7a02b441df0a12958f988c1ba1e2caae43be78e9d5ce400a5004ad3a4b8c88b466304f506ef03e2c642ca2a9e900f4ff0df40862e7ebf8451c9df9d0650fefe6c7397000a3f5b3f8fa56ec0a4dfee0a7b0e85bb036d98192d6c5402652a2bb69e120e1b8efe8712ecab97fbe28e124c0687adf53c8d778bc3562bc1527eb7bf66ccfb7325ac0fff8cc0815d3a9a1730a6abca6eb75cb922696607a7e556ef6e0de3ea20c370ab313f97d66ec7b038505e60005625697e3e6344f2a56364cbe7374418391470e087f69a5301dba18224d853e65739d7b6b618ebc5cba76551ec965c3bd28423bfc9d92c88255d39f36316816d04f07690f4e63aed73e45c18f5bc3d3c8f5377a9801fc9a0ad7b2fea3effef7f2c86c29f3452d10b745c681f7bb982c4422e9267bdc41157ab9638f14ef537c0e5034b63d3ecfdb7d52df1ab321e4134ba6d22e574d0dbc3ffbb60ece191425d109af8b4c47cba52244164a32407f9e08be600a72350a9aebb6b1ab45cf733f76077630c19c4a799edbefce9935b819905ed553287e3901deff1c6952e5e283fb025f19320808436cb7e2a31381fb6f30df93b8cb5346fa8e8fcde71953d9e314ac5433c03fcc1b9d70419411e1a830d3c7c167d43fde250628d850ee9013f9959fe160dc3ed8f418d0b15857850518d7b1372d5208eb3cf79827fad8f2432bcf2212f20046e1e71e998618e5a533bae84b0ef5419fef757fd6aabf2a27f252cb9f1bd91ba82c90494a6171c1d5099d02f00d79ccfde1456577e531743e958c20680f6f384f0013198615d59c159bd631e82abe25ceac503cff28ef080c276a13c793c180f8e220cc6cdeaeeca945f43e980088afc6df15c7a057eb17dfeaf2f81e177777f6bdf67be9abf2eae0dc898c731cd34fbba210ad0306073981ee807e5005c7196cd3222ed52e264d56da1ce65225e601148d2a76b76a2ef26b2f5e728e9c55c4d1aac83b97036c2a9cc75eaa2fbc317407d303dbca04215512e07281f886ee9bc42deb3166ee275d072999c31fa02547590146ff76950a1231d8527753a4f0cd37f919886dc8d90085d80a7b40e4c17c32fe570617e2864f1f60a6d12c00fb16dadbb03f3bc779417e017b3a32abbd44a1dc0ce0c79ec0aa2ce07ce5efc47238a8f03671c7577ba702e057d6105f856023975b396df141d46b9e92581409ba5f95bf24c184c5949fb7314fdd503f4074e1a6b9c420b92a778483a6944965b7cafa68f178d6fe8b2e615c22503c606bf13597a27ff39da2afc38636475c4fb86305649c7f2296e797ffb95857fe4e8343e839bc0a391435d4f67ff14ee433e3fbff369e14f3c87e4cca3fb00d6afc6f99f6bf1e16d606d535710d81c57f7ec8dfbb42a363dacfaf1fd6f2f7c4e4fd705f64cfcfd0067502a9fa5fe76c8294d454a12bbc61e6ed3a72074831098dbbca4ac6da77356425d5c4b48d95c434ee6a49196b28cb4b29fd0b328740669d5972c9ed685c040a2b847f10b30d1f733c561f80dd5b272618b8dcafd9d7d17aad5374a4b6897f69f3e5b75f5fa737b98ce4781bd4f07f3dd2b0478ffd0adba0c6233935664ca37320c290373b20c2911726842182fee3d755cf5a98bb21feeed73fada8a22e29cbf545e7ff5eba198c807a287380a3154abf501374025516144ab68fd004955f250123ff4b37f8d71fef78f507c1bb6697663ab3e715a3fcc1e23c0d139bcea4e6d71c9ff3e3526740de35977f5dee78058a42dde13a2d8376e69758d1b11b62e37f24045589c2d320c367acf9c0c3cc5218bfcceefdacff35c7235d2357fdeb43460e818070078815b438bb0a2edea5e37decb28f28b19c4b91f9e0200301e19e7df56f76cf6b07c67dad2742d3e875eb8baa963fbfa6f9f41353527e10a8e66c88d210cd88a4b97328acad10b6f9b0028cf841ce48df45945fc0ee9abea66f7fe5c227da923e3b3254838f5025855428c10ba1362e2a92e8a51240785d4aea4f75fd535d7fa2baeadb9be810386165ecf845849160f78aed97cfc857329fc03dfbac25bff6e580595aad677028c6c133642a32ff65cae71063eda375dd0d8a4d9e1ef7e22f314458d7a10b4ea0efff455da12da8587caf53378051de093fd5c9072ab12c26b1c6392b7eb51a831be1c793cd61d8795143be3cc08d13e92d94b927db55f83cd5d816feb1eba58ff7dbf9076ff5914851ae93dfacb20605542379e79db1947d247d2027bca50a8867d72fef62fd255c478c2c8168acd078a5eff08572e275d9eee0cac661503145723a84e3efbee7eb4d18f586f23ffac133da7a51e769e5f7db2e601bcec5d1f34783b32127f8a22d7e61fd80fc9394d4fddf166f33306fc95c1bd2b5ddac4806712b474792e4f60c7f707e9327fea5d0e3c0076389187ff48e4ea0628342c996689aa0c2fea7ad0e8aae2f1a183ae7b81f587d7db7537d2b0c32d76bf986abc335b82bc8eb48ecc29c246eaf5ef3370edf7ceaf80367829f24954b70c4ac2094683b3c6aad68c42ca626153dadbc18bb0efb75eb86c7ffe813f22429ab44f022e3844ed95ce942fcf644633bdd09b6ba2b41cb859feaf77011221b7f740168429723ae1e1e27b90351567942be57a855ebe81d5e3aba2b79d8f284f78f71abab989ee3f673c985d1eb68604709e63049633651fbd6dbc8463497302be3be1262be9c798a3037922ca842ede26b5b3900471fa78f49edc2d671fe50f31af70f75025d15148a156b194a633fd8049ddf17e4ce77b32d1494c9b52557c89b602a01734e31a2a1113b173dff76878d98c1551c02020ea90b0a021a01120e161e0a06f10f7d81c90da20934968eae9c9bc8596b4c5b7e2cdfb1b9899ccdc400f32672f6dcf3073792571e5cf7fc61f6c638ce98541f5021d338afedcb46f5753a5153563ce2dc5745d5e37db1a1bbcaff82f8fb3d88984fad77313cce347c336f391ff5053c1c75f48cbd4b9b76e764d4231491f0f15d099d7881611118697ca6eee6af7c0b3af653df6c65f6f491775a15974260e3aba959b7c75f4bcbd070bdb424d57cf6799e4de7ec590e8268b534d975d787194e2be1b704b998403cf34a1efd74a58ec37675039765de9956d7b8fdeb990def62abf6325bafd2332dca70285fb408ba23d22c8a6c836c81bcae31460e63b2007134eb2792d66f1047f574c861c260693abfaaa875629e6d52967af31eb7eca9346c087f1bb49961c6d2ce554a764f52df971b9efe3e0cdbc3ff983fb751d01ea04efedbb8ee5f83ebb33e52556a1c954d29905b45d2d4db5ee528f0f41ef3ca785d5be41effaee04d50fb6df1d004d8930b8597b239703a413f8060fd239c2589d52bdcd5c2d8c812db7625cbdb7661d5e37ce7662bf59fb8be5f7c7e2465799d3f8eeabb0a675191d494f372b770e7d2d273f2e1b097701797f411535355d596b3963375906357b05751e57294e7d1d67393ff79d7ff3f00d7676566c6c5c6c1c3ca6ac5c3cdc16369c5ce63c6c169616ac66bcacd6769c1ce69c1cacbc66acec7c961c5ce66c563696e66c5c965c96a61c66b61c969ce67c57ea3fbffe5acf18f20bee895500ac1dd217bdfafe72c9fb624d098472f30e3e314244469608e02afbe383ed918d32ce9daf2fae865018a68045cee635a44bccde538149db6b03b72d7f7871cab04fda7cfa3d3f06a945c930bdbb82a66c56d3f8c4e6f2d94e8e66e7b6ff05c299196c92aa5977e4c8e432b818892aa1a525758fc35d37ca54cb669ea9778ea9a0935c491bfe27904727f019ea759ab4f1c6e1df6cd9bdde07511c1446cf9f31240451ab6b58b717bfa545956898bf936d6f32e1e9101f5e0872ebde4f5340ef692cda4db193262b22395677b40bde1c990128fdb76016a3e66d0a2e6501c37db3280077ad9caa621d329ee22f7141e0bbf4e7169bf79032081bcc7e89f43c3ea59c9ad15c633cbf1782f561e13a8875a540ee7cfb89f62734e04687d76fb0cf1b0e212c0c3e01120c20868423225dfc7cd3024969ad8e2af900242f350e8b07e01515bbf8d2d41224954d1a67c7f9dabf2c11543f64243959ed510579bdb0424110d96188006480cfbad525da69147a33c7cff6d8e2bcafcc20d60e7473b36a037bc75469a8cc6385c7cc73b23299f0f58236cfac2da078331bfb5632a0e1e65e86d1e78a700789429045fcc84d9668d976edb288dd4cb3c4a50d8a3c08e28cc9d9f346f9a439fb29d09b6c1772be8d419c855a1122a0ef77f5749369a4031f7ff5c57afb30b0dd4cb4721b654ecf2b1b837c3606debb223266e51a8034584a25702ebec7232868d67932555885e4144535cc0c820c48f5371b65d8ddffd4f1b795fb2876894c1650378066a280fa2c4ee97a1ea46f8ad17660dc62ceb39c0c8391d2b7c4ea6b05bf3a9ddcdea4b6704b4eafbda403d78e0ae93d950c5cb3e6196dd3779cd083959fc2b4032405d6e4c06ab5325123d45a934c478f7ebaa421a809769a2d9087a501cec69204a4234cacbd98982f715488650fa775f3aacbd65ff9291ad5512af0813daae068cf42299c724fd289dc1179f71e74060dc983fab7901a837b3abddcde7e782095d38e35518a634fe7c3a6702c4f4f72bb228999d47697636eae4a933b58c2075ef0323f5d08ce25cdfc90f1823a7661786779be0f65966037815dee8d0a581f9da3730542ea9725b7634126f5f00f63708f324be7a54cfdf7b88f07622ae1e0cef1a1ae03c934c9f6937a38a4df7a34d1e753dd3a68aca1101de6fae5af298429350e0590dbc02fd3b760a419259c0fe61f3740a1f573bf2a66a148f79a450863129c80221d4ee4b77e5cdf90009e936c2d1de24c65bc35feee05f95dbae1792dd516273553bbb65a41501a3cb7374ebecaa5c1b7e9c51c4d9c0af02556fecf4b2c433d16c12d81d54969384575898e7b0d89722224c2fcb272ee602885d14c627c318f89525f692915ef773a57bb1fbfb013c444b88e94af04796ec5116f5e9d689cb3c9e7c3c60c26e9a2502149efaf11ac6db2db877ade135a2835380fd8a95160c36546b1ab12e5d16cc0a58d10832b101781b11e34d95eae99d9930926f451e4c1965c20af85857e5c7de9852301ec218b4f9af4687a24c6f4b5adf02f086bc914b6428bdc7d3f0b178539db37e7edea9b781919a6f1c9ecade421ba6b26df4cdf8cc96176ce28c0bd463bf7cbf77be488d44bd30a15fe3c995df9a8fe103f837c3499f3a7b39672c1f4d0a27e1a57c530e7c6a60a4a9685cc74bfceda66fec501e4209f5f8a82701df80f5e11eb66e309d0069e0a5f48e957a1b3375897608b0bb96e3bf6c8cd6b158f6532a68a97abfd05642e90dbc5f6ce688cbbe82667546126dc2bc14fe38cbfd27403d7a0957264d6253a17df0498e76d96c4a7731f71190cd78beff085bf8b2ab8cb20a610c1b8f9891e9fe3ed038905d6aab27e6c6f3ca9a6afdd881ecc24ef6cd298017cb418f8c53755028558017688f607798e01fce04f16af62f36fb88dedbd27af1c30c4cc2e27341840209e4264d374a180f63a6372162f6d94fb64ad32e4e00fdde985f348c8c743dc864d684256146ca1712206300ec7fa984f0d5636af8b5dbdcf75b5189e78c911081f7af29cecb3dc68a79c6a907794761b35e25cacd0ba86f43c3644a4db0a8bc0d8b7bd43c9018113300ff0c98a06d3e75fe01ab3490a69495f105711bfea1759b01f755f95317edf1fbba1b5f39b31dbcb5324402bf451a0223f17a28450de03658680a8f6c3f64a1673bba4e0402231b51d7030228e797898eac3ca05dccec17a2b5c481efa783d97331b21b4b1aef7c0f37e7aba237f9791cb0af7c8fea00f500328898ac1d2e47ef340552f46460a4ac5f55b53adb029a265932e56ab7f2235936c61160a4db2e1e8bb9735b8de641043a96c5ee876e78e93100cf355b63cb7227f6224798281227cab5eab0f6443fe2aa9cbb636fa5e60bc90802a229b104f4bb254fe75180a4b736eff4045d0b55fd516d45b4176efed63db84260100ae38581fb8a6116f2231a633c6c36aa7b8d05a24039398e4623a71e661f29d9d1e561f4c7b254e2a30f80ffbffd506fac981a651173249ef08d52909c72ea43e0faedd4e31ddca6bc5f36b10c703fef334033544d01f02a7033630f9e087c355b49d38dd1bb80a20a20cd07e29b4e78c62c6d35cc32a36a325cadf34767ebd9b5001e287d7238d9f602db6469d7cdafd72c2deeb5cc38b03e1636847d55bf6d13df60ac90dc51e20de9eed20c14f0b502e0b03e110e29ccd73c3930121ca098964e05b6282f3a97f3552055a7ce04b6e73f9ceccd06afa600f64bf59617057a8462e9ed6311911eb83db23c7c0560a45fc626e2709ac9e4aa696815fc426edd202bd10ee0dffae209bc36cef3fc66e6f4cf2c9b752fc47ce3a901ff54934bc9197678afb7f6a157d97e7b51536c0df07c2c310a03149dfb3e56b7fa3327359dd215d2aa01eb4724ba7640ab420eec733101a2cc7985dea2f56d83abf2bbba8c13c1fbe908a289c1f756de5aac3f7a7017585f3913d0c60f4ea19d2c64db20bfd93970d6dc2a02d6d7d8c9249b0b85f68f15985fe40dee627405966b02000872148cba6a726eb15431b4f9c52fbdb03b4862dfafca3f668d8d199ed0559b553b3ecec5502926e79604f0769c9e6da799f863c6e46f993032a7bc9ad82e1a8091988304631ee3ef76a61d236ace666b62b02ba73381e7db6da2d6bbee76d1f269e70c31fadc7b0a8710111879c8c046635c58cede67787b389a6c4c705685290b18a9cc9a25c83ae32921f30a19e325e61d28c3b952912940ff0e108292c8c845fabd38be8a7c31527fecf924eac7381c9b1a691d8e339722b4b7fbe18b2b3dad1196e75210d76eba2622a1a6b02ad25cc2fe0b39f2f5b13a7cdb09145350181cfa37de26284a3fbe11d69bed9eaf2d067c828080c0f8ebff7ffbf7b7cf7ffc1e98361c292a107ef8edf65af3c36692c2ce33fff7889a32643b25ad53dc775296df75a840ed85a3b2e4fa6397183ef4507fd7378510becd0c01d1a8bab58085486d15bc8bf6be9d4178c7545812ba09a3385075e5e3588214099761069556f557369652724e5c6f6538613dddc130359f134e2f143829d2f205522a3e42f966f421ddc3c40b1c35c47145bc78417efe4ab3349c7eed7242c67e0c39cd2e26b9f259ec25fff442c7de7def4fc81c6def3f0a3ec79f70e87f049f48ea3f29f9c709c3e2a0ffe306202060813c957f992c2ae937f2d16d8c68dd7ccbc13ab764e7aebfcf62d31b68bca12247a9be7d3f652e99ee154964781a50537f93faa87961e4e3e0c074193e14dcdb4ace1cefe11f5d099a3202feff2dbf3611094d75d8ef342c16eaa7b9181a6d551006a9ba5ede067502a31a14caa7ad15aaae4bd804f333e845a16ea68bd0c0f93f20ceb3466c28ab7fe5f31e7a41b574e952679177c34cc99f33077ef9e77a4edfbf778ebb35e6cf4dfa16b952dba4bb483d31561ee18f3effe119e073496f922dc37180df3f8de89b1a283694fbf4113a2a5a2eadda1d61d003e6ee7656d1e4436f65f1cfb4fc1fade3861d89199e8b6771e61e2be818e007ecf769db9cd08efae793fea427f5bbeffbe6453449e7c8e6d41bd9d835cfbeed2fb8960dfa7f0ed702d0c0fd115c0b07d9b39d87a1e7eaf47e3c6b543eb50a2939e568b472d1dac72e3b922284fdf4d7c96f965906712dbf59c887540a84b0a473d43c3ea35d496fd479d7320d37a5593d2092f64b702d50373cfe870efaa31bdae40fe4546193e24f7e20c7ab3f4efa3175873b6eec8fc4b0b5657b374ac5a7213bffe8fa3a9bcc6fdfccbf5d8f6b091e3335e9c238b050bfaf9c584c11f7e9f5e91ed91fbd9fbf64d1e50d7e41165dee9281d7ae580ada352ab3971b82c7bc852f018044093cfa30e9161afb4187183d3d174793963928a01cc8824eeb12230726ddd5ddbb736ce8d26ef1a5b0142842730e66ba419f23fa5364ce7d3f9e4bc9a7fa3809745d521439e9b0d406ea37b6c926a3e597c7045927dcbb2a97518146cf1bd2acbddbabb4a2566735c98d900764d1b792b269709e4093f64475e98741208efafa4f0059285dfc34e6afd2bb0ca6f4beb1894edf8b5fe3ae0123030e6c7c79831f931609b1b1e4afd82c3493467601bbe0e8f7b31a6b887458709e42d64de10cb9da7921c02ebb5856a1f2b02979bfbab4031aaa7f31a8e0cd8135e0ee9cccbc4adc1a270b88bb8730215da95ebc154eb92a3f13c1dc09d699f0b7f8ae3d199b543018f51a02c892336625cee3ae1db13e929e41657925bc6d88ba0674d5f4efa612a1e06baba2b48c7b04a53ee789bafb06d8a50e3b23c69e2de64d23747fafd75338e4d5e83f35bc2abf3c8889ce125773dcc4a1cc2cd3d608c3cc1088bb2a4fe4261963e57fa616bbb8a6b76a84c925f77549fdaafc3bee0bda6ab87739df06662bd9101d2e6d4572812c59ddc7bd5e760bb376f5058ee2f5f4be1e0b6ffd79609dd66e5963b7622456b0f84c35eb595ba29a2d0a54119804090d7285ad8a62fdd613b15bf857a845d580aed1e45d97af340b18883591c6cbf3f1546a664fd080ae20d2a8577185d46ccafdc768feb4a323bcea69df010c464003ae0b67c3d11b3c97db270baf95d0f12df9802cb53585d9db53a2d00457e8231498161b1168f35300c3932b2ec8686f12115b5087c514f53db45e506f1ad8aa1d1a6cd6b5baf6e86a13ce9761bf7dac5b79f119a80234a79bf1233cda1aadaa3bf4cf1ed714486e2558bf2ac7fdde91dcfb24d5d07aacf0fb0407435eb6f79a3d90a5fef4cca89f7446f2f88357b0516733823eaed737204b19d570e15b65a444af3f43e0e2d3477972bf1ee87af1c11a41107e8b8191c2b42ee3686399d6178f0f64d90c2cc6d85566e23633fc840a961e4c4abccfe906b27caeca4c2f445bbca953f7c9233e085433206a22035592f704c754a3e13dab5cb3b2198f32596a8229f4802a044282197a6ca7d30794d72e9dc9c61aba4309db4016d2e4d2304289a62279e26981b62c3aac30762304c0aaf52c7eec16ce68d7dd2486e9dc2f64d6ef3acd83813db776612bf244cf5a412c53c703fc5e4a23e47a69a00a34fe4033667bd472ee3e52a654d5a869ab8b93483e90255a4a2e5fa57d89621977e6ddb69d48c2451d055c7f80cbb0d4b07911daf4dab368742c9d600cb215a0ebd34ef0046d963ae375394f3b13ea685c07e99c21d075ef1be4fd709356bd7750a80216654b3ebb84b701c8c2fb54b1c8059a55393511d7f56673a57d6584e807b2e02a9d7ec478e9301d4e30ee613885f8c97e394480ff3cb07f082750fdc5e624d3ecc460a5d00579d20dc8221b66258eeeb6115316e424571b242e990590de797b55fe4da07a90d7d24611772d0a1b7d27219a83a901c00c9a1c7c13e8b6db25a3a3f1d392a76c132469e506b254fb99421f2d432236279c5d1ff38b73df0b631b0230a7da5dfbefa7efaa5860873292aeb734c204d4e1005d676a15adfa16f4f4be5e58ded127996b3d5c47da40e8905366192c518c72d1f514d19d6b711ac9ccba0e58ba3d5ae196a34482bfc9be3916af55f154786d1908ecbef9cbfd18e010854bfc374369c46df70a842abd8054c7b09359f8c2182fb90e76b89816da739ca6116b6019dfb8ad9952aab123f94977061225a504e6f0ae3d08724fe2e46dd46e1d4580b4a964445667c03e6631be2aa771ea7a4cbda5969679b6d4835becde88570e035471239eba673fbfcb2d831ef5681a0f491926e8c208b05f85ae8a37724acfcc5b1a3966b664adc78b272980f5c3aad07be3146d66e9ddc5989bc1c6c3449e7213c07e6ec95959aa182a05e3f776a2d7f5857e0da3eb00aeafbd246f8657d37a5aea21aa95b7c68372abde39204b8e87c873f801a9aebd05a14dbd68cd789ec41f1e608587363f20a1ae70797aac4df4aa2a6565b0160a09903b66cec4f74514ec58ac58f8f8514761c4d24801d913987d1e8fbba4152a497e7e787d4ff8a6588821802ca4ca6ce0d673dc7a59d78656f8efd21b8ff372941000ff68ed460d6b219746b39589c27228acb2ca560ef85f6a3f47faa2b45cf476d7aacf9fdfad72c61b0f03f1819635b1d0f68c32b6e260604d19fe92f4e62b32e0fdf8123188d269518b1c1713983abd602a89794600f8bf5ce759658ade84f3578c85035d53c54b2c649880ff165f9a7e4e74719b76fabbdc097b87600cc6df8db8df6aec8bcd176d2d200dc82e8f7a091d4a54fdcaf7aa5c3a9822d76e1e3a66bd29860ec64ec3e929711c300b87c93283cba76cd38c457dee201f351d65aeadd21bb0efea37f96facca16598a38b393a498b7483e040219ae38866ed6b1d15485f87dbbb9fb32a54c2b1d4740153ec6d72a5dd8d2d333a1344a49fb9232fef9e416109f5eaa40b7bca5ffd81da7f68d8bcecb5c7bd4c819585f2238c2436c3e7a8c920a43e405ae71a1ca90de03e2cff84aff05cc60dea26508f387b282a88805046d807fb78931f826cbfe6af99bc43843c60cc9ec11920dd0c628656020be9d38ec5414e2aea81388526dcf3603f03a3941eabd940b31f894f3342dcb41286e0289f501e09f7c511ef70966d7210ce869c994bf8fe410279d9b01e23f2dc9c20f119429ce7235707c6c71a48243b1ae4095be4d4266665eca3957d925a10a49a2b9099f0418a5680b41164bd358fc9c8dca329ce589db87337e5c208bdd460fb34159862270bcefaa83b7a42b1047e406dcbf86b53331157477aa87afb56319ff83e98f99a94057f28b19effbf26f29df2d68102514771520769bd300e7ff520de166bb7fd910b8513653669799553733d37155be197d8054b4d50c7be7bef2f93c3fcd6d5a185500d74e1f1efa2ae2f0549ad080a7eac864788d32f93ba05f43915ae473778965e0b9d913975287d568c702f8ff0b6744286ff08f6d8e64c62d285a5e1be8f36ea435dff728ce93a0ba0e7fb49d419fec45fb6fe812717ed83f26e6b1d8e6eb0fdcb20e876c259eb9e78eadc93a2a95ff708b242c1f724dcfed2e7b6b9336aff55b973d1d038e98249134e1c63bbf7536d48d310e0dd41e22005bfab7cedb5f832d254db4c68d479116cbea5c34133f441b81a770c92f59a081e4723099d6372641ebd17165d8e160a9ce5ab8836ed5b5e3fd8364d23f604bfd1597ba036f2d7b65f0987914861631ab163b5bde6cb7fc27b6f4179f1f41d1d2cd54d1d4f5de1f3cfebfb1a55a3ab6527c3edea672f2ae62ee9e3e6e6ecebc521a8aaea6b29cea7276dcae9c5e165aceb6324af666f6f7ac7f62baf986298f9f802db5e0e0e3e662e7e0323565e36363e53037b3e26637e3e060e562b3e4e1e2e4b234b7e033e363b7e4633565e56467e5e0b630e5346737e33333e5e433e5e6e3e1bdd1fdff726ce9a94800d20314c56a5625b6f20cc3e726853ec139dffbe737fc25a6ba69eed6ed5a340cc063ea236ccf4d47a215ae3007533dc69bfcdc78a2b1e6ded68374eac74a786d99f33fbc2954bb5d28ddbcfa3fc7607079cda7ed7f1d5db3e2efcc9b3bdc346f7e5df3d50d9bb37e4ede5c646807c2818b29c4fdf9c7c50e51c56c9bc3a1c5b0ba573573bf246f0ef90bf3e6d7ad722fcd74a53b7ee8660ebe5dd31418fae339dd8422c637bac46b8e97603e0cfcd579734ed70088539e49d7f0421a16194ca6c365ddd8993f9c37ffcf7ed00dfaff3ffa410baeb9fc9fd10fdaf8da775c9cb2b394a05911e629cc85ae00ef3b777cf69eb6d8871062bb741369d78de345bae1f13ffa8448caf615b868b408eec0f6bf0bdd83c4b610ba47b734c847745b00c7788fb3994680f3ded9a54350faa90feb975ac3146e3786ddba7d1523e175dd6f394fb0482909f4362fc58772a210ee3e1def268457e2777f9156b4a1dee58695590d5fec4f51e0dd9e240bb96b74a17567a8343e2299bfd49478d621b081fbe276e48b71153f24827fa68c2341f10bca38d9cb2dd6cf20aca86ad2ef176caa662494f11a0165809afc938b5c0dd6f8ae6fd9065a670fa71c577aa2aecaa7ee2826eddfd3a0913371d4903f5c4b7460ad02c0a443c4bc0c189119e22a9b92ae1775393c0776c300b9dfec7d23f22cd1728859e5cb4beced879176c62098148bf63d3e827177f30031a213bb8a0b1b5af816c0ae6636f89925d0e3039baa66b4ef4bb79532912f1f000446e8ab37b4907c0877a81539578d2ac6e1aadc238088eab46426ab22ba76803bef36cce34076bfdb11e800b957f3f24b712911a4db5d58a977ec299bcbdf9c1b0a5e9573ebf22d48d83ff20f9a6e113a2bcef994eee5700ea45160bfc755edae0c09a230b1c69e6c4559417f055ef6106df2808eec3c44a2251c37d33dbc6454ed1ea0cc618dc34c0fb16f23bd5f727129c54f832b43e009906bf69e7fd9b8b40f081fddf2904d425941a931d107c267bb675a9589a3599124a8054b569d2a09d0242300b95949b4e12dc85aad7b7364f0a522721ca88d4dfc807cc2994a61d87202e26ca7c2a9376d723ec80e15e8768aac9f896af22e1de2987e6b76401c3c19a9a8018031f7bde54f199775ce52f9a7965eb00448d4f72301694e9177d0ef9fc8d562c6b03c29cc76a3b0c227f102c0847b35f6e3b2c7b70465ce24c651323c3afb9f3a01606cacda2a1d02128fc8195313b251f4ae4645b12ea0d920bdefa2435b9632716de3b86000574a1b014e0de009b9e7d55635251f9c67711e2bc8a3d958847f1b1bb83e6902a75eefe339293ee5612742692f33eaf74c40853a2ae6a2da371455ac133e63afcac8d543c54313001b921d730ea375b5402633688857307068bb4a9903e4bd459f091cbe3dee8a8ded77863069c1e0bbc89305d2b4e11131069bbc47cba7dc2f0c8731dee65796180115683d45daaccc64dda4d95318aeae59d677d67d48c0f9a132c9d3ea5b08d3394d5e7cc4a6ac0e72c702d99b1faa3d40e2c84392d7367a6858f074df2599b217281370beb09a511a09f732e99abe252427441bda2c0da44971fc453c8eb6e56eb3b3dd0d5b8d51749b226a05c8455f8c3013da375a1a470658e399d5bb446c423102e90e44046d8a0d6892f30ab6e5c67794b4e4acbc93c072d961ce0d81c0b04c83c5d05e1736906415cfa60a94219c710ce0be4a30230b28380ff6b722c048efe001694c3ba3c4c6cabb50b4f16a443aa60fda72d9cd88813431b9bee4a0750463f4d31aebf6693318da788840a08cb815e96ebcdfba887f7ff97425ae32430052d51b2047f5513e6fd64d9bcf7ae382e8a6c0425300456c0f44fbac7a21c1eda8f4ece2a251dda32e3a0729a28600989c54b8db55f8e54bc52f2444d9b519f38c25a5f940d492c8e9c02ff3ad72922d9f6887c0b46da071211628b3ea40e1509f25a0dc5577849d82596d74f624dd00fc9f04e9ec20ee0b1f2a631bb9a7fdd156b644a91840524383f6c9231c7e6e5f0d76310f411c228c7753b2db80fe0531457bcc4475c4ee893b1257e68e8baed802080fbcfd7e9ff55e2601f802db021b7717e74f229700f9ea87b6ce47f20d51ae78a50d7e7bf1f9e7e8a5a680bcfec87c63aeb1fd363ecd6cf777c1dc8f3e262a7857e57973db1a8921c3c53889d96e66629e91698598c052af3d17da2a90501f20f21979623a35935cd4e40228b333f93abd7fc32d292dab7a87436f1f2eba32ce2f0128437fdcc131f67a9c535eeccf7ba9ca7cb634a82502a459e9f9bf0e08b0bd4fb3b28ee65bc9ddcb7b0e0b94811f377fd5aabeb5591f59d5bcac1ab2a66a6d06864b0d3a8132d63a42b07181333d986e90af82ee4a00a82257d57af10b963ebccba14c486606eede47a8f1004246eac4105d5ed413b6410f724d6961e1a519322ef07ee937189c165fb9df2187e7a752c0177cbebef11628037ece35849ea63e3c19a072abe8dad533384ffa044cc4df257cf2465f47de2fdd819e1fbd9e7ea43a4a1f28137884f3d32b318e1ed1e8ef8d3029a6a83ed25c02e287976b0f4c723bd5d95fd2cf96566c18b02ed7a002e17231535b26aad4f24639a57979c25b6703a1773ea7004c22c7530887718aae9e8c536de9a9d8a7b2c75e6c57e549847c6eb7b96a4d5e85a2d6ae95e8f0f44fdd0907f4e392d17f464f5304234526a9b881819d749c080845632d682cd28f8de4ddcb33933b68b4d28f14107aaeca99eb91d4378f61b11d0b6431523885bf1a576e02fe6b7bf814a25bc555f3d5943ac98cc11b628129e43757e504fd08f45f9ebf3a78211192dcb45c370861d7f014f09f4224db2b5dbb7184dc59096e1db710457c62001800e3293b8b8628633c12e18e51f4e4b382ad0c34e0fc99678a77f10879119d695d44de7f5e5f13665206602ac9a2f6d8b4d410e9a27b8bbde973e5aa8a6c81c0faa0c04a187e917c316f448159cee250c21ca9960a90cbd755ce1ff3360ea785713d677651307ec3aa9a0934233a8534e1ccda1bbcd48a7a56df65e8aed21a3e9b05c45fea25be07c190a3e91466898d8628e4419355c04ecf7b84ae941c0722a99b246b555f516eb15af28929b0be433274b9c8713d7dfaac7c28e3798550b24b3ba03f70dec27eb06b2faa1eb951851ade854e7eee9c0dd84f9840e9675d3fa918c30e8e8980058a6defa11aa099e1405a2a7b6bff91862be6a242c11ca6dfe37141607d2b6ab05cb67b98b8fa6adab65132fe90e3352fdb7b0006a16fe2d11961e17cecfd587e7b22a35204470e9873f4bef290ac23a0ac5bcaac9406d7bfbfa978ec21d0ec93a9295e67f31aa14988235d34f041d52871551c403e9ccd703a9c7cbef096a8b7b22a608540f6edbc3c50e66380860f242b3d69cacde37a6f95da253de02f0b34fb24e01b10be31349e6d76d414de3be935bbe0c402f81a84041e77d80c2f383a5bf8db6fbf697653c4ed06602288eaf772b9bca37ad75da969fb8daa6565fba881660615336781447d8607c21ae7044eaa700ed9d3b701fbd4cf7a5c38fe243a5646171e958819cb2213170d2057cd6f65ec5a7bc6956fbf63b058df6e7fb9cd5a360d7502e315098d191ba7e56cf197dacbec46138c45bec2d3b5df9388fb033b55e02347b7aa541bf7291da759c22892c4d7635b3f48efefffe6bc32287fa2f349b2c67456530b86c2e48f77d28cd7941bae253a15cbcea499770dd7df926887f40fb64f8ff9245415da09b92da21c3e134cc6f9c08046d8d05d7a4006bab54a631afbdcaad3dfd464cb201ff719e42e1d1eefe6bb2cc2ffb7e972c81ba6cb216f22befef95f572e8220fdc6a39714f4823689ac22719cf6ddeb0a4233cccddd12a14fdae228218b2edfa4afb93ed29b3d3f0a90d77fdb1b75f2313fd7f7303f58e1b9f2145458cd4737d1af7f423fc50e1b0c4e5439b3798a37c7be70121245d4abb9645451c59beabfcd88999568ba43e88164454bec06b53a87d7d30bcd2dafa0b635f330566a47354025074fd0fef88683f5c53ef88a3b593f79e1c98b402f7637253d3e32b405b6203fc853d165bf423c81f9f21e8f33fd4361f15c31870dccf1448ada2e73dd1d985b0c9fe732191778378595fe09aa8acaff2ecee61bb37c055ccea0bfc338a5072c1f1bf1bbb7b544086ab59b9e80374366347faace8bbdff3cd38ba7ff905afd5dda65f88fbe0e977d1f68ed8c75a4dddbcdeb0d444f8e7934ea4e3ae54edc3bf51b87521fc6d3aa4a92ec475d534f4e785f21b3f2498888494a87913da2b635a3c9f0e2bac7dcd61f148a71dbe02fb32bc722cb4a9c6571822042a1c6c2c29091f8ca82daa08e5583feebc77facb46f5e220d62a0e2486969cd4a0ad4ec2f0a27e836fd5601eafd7786a7e3da5e106d5027242341a152a9dea1c6e10f9b48ba7e423f11dacd72cad0d70cc282bbae387acdf5215de36e50ae59aed0ae592e30ae3167ac6beeffd635f77fe79afbc7bbe6fe09aeb97f22e0fe7da52f5bd2ecab4f140404e7b25eae94aa7c9cf4bfe172735db810964f7c7ff39a7300d414efa9526bcf2898111e7052146910d36c8d611912921c093d4e2c5ccff5301c598f257fd68c4a69fc8877c03fe2e35799a3faa200589277b17b30a9231650226b016f3939a12f0564bc948bc94f3edd8942177e1f7d469350e6e2fbb493d9f93c20962c8b0dcbfaa0a452ca8547804d231b9e3aaacf964ecd17562aab81dabc847aca5576285c8a2a84f5457f00164ec538cce9c5543c16af5a28bfc183f8e7f9ded47bcf5b931f7d9a17db64834f8f60bb347cb8f4409b42e5d67be5eeefa4e9f2f7dbf1ce4894c5c5b59c97f1a2728a5162c208f06be958f0f890159188de50288f2f4031c05e36bc2581b5995cb595cd192d894e297b836fcffdf48ebb1a8e7070cdadef234135699bf1795f3f6d1363ce4ace0a3f1fa3430fbf94f87ea8d298a61d451c5193f0ba44c717573ec04abb23a6b56a2d136b916a92edf3532b997b4fa35a3a47d1058a092a1d22e14ab1dea054cd71a832d8b9ffb36bc3ff06fd68377f6a3b13dcbb44ea01dfdb5965b2ea174f0c739d7e508504fb17aa7c7a4e8217f592fad4f1bc31cfd882b3bf15bb088791a4cfd39391ec25fdadeeed61e26771ad36e5cf386a72623b96e7d6576b383a2dbcc434b55520721e1e7d8279bccbfe8239c50e492c45d5a2247a97822b501b373e8dc52ef43eccc793cc4776ebb96684ddaca33bc4451a1f3cab0b2c2e993e8eea70455c206cc01584e8dc3de5e0f30f6e771f725f6ae0c225d48db6770adcb3aaab181074f4c3189a927f43945954d835506640d7f656fad0349862a8359388d74834424a2719ef791127abaa4553be0226291a2b29ae3516d2f9e2fc62529fbc8a19c179ae14acc9fb04eec27aba00dbb4ac4418e32106d86e685659473cad9447769de5d149302acff8e6db0f9ab38ed982e611835ef2a64012a6ed5a2b1773da7d920b954afa77d28f226327cffb381c5dc1d3eb116f038fff0f7bef01166596ec0f773741901c04c9597268a09ba0e42c417206a18126e79ca32049411451c94a1204949c332882080282048906324816f07bf6ceeebdbcb323bdcab877bff9dff7199f79e8eaf7f4095575aaead4f9d535ac601e2d54fcc1267136fc1df502e58177cf41c152fc858411e358f8d9d2a9eeb08d4d2c5ad70d1a4895f0478f476524e1d82fb2706b0778df4b7c231e2a97ccf40c7f55d89ca244e7d57e8b2716c46c139fa1cfc69acbcdd09aff9277666e8d784f048faf7d248d232ca0e4650af8c68dd03e616db16ad1f70a01990545aa4b07913d75ef5d1fb6b2e19c335cc1b7d9ca9efca4c9944bcc7145e3865ed2cad799957b632dde898e9418585c911fe3957b5a1e46f54d57bf9a2e2d865dc21a89801148cfce85f7ed90b2f824c5166d4e5f267a4216ed3136509fc88df7c17368beb4ca6a8aa64c07ff25067be1c3d99c095a5be9c5a988bbfbea443ea4a5fc12d464b2694dbdcccfac4371d03013fb9ca1832d907b4d7708f83f1e0cf3559a1ca65f1a531da38f8823d5fb2bf107995186e5eeab073bec8379594bf0e5294ca2e7aba8f8835ba61f7d0f611484f30ab78e8f7c53d15460ece2b02f0e6b2b9692dfb8d255754c4dec24cc65a61956ae2bf48df4cfd5f361fd48ac434190f3c8b7c97c2ee3709e27ae6941d267ea6741f6b756b3fc352779715f1475acc70e70167fca41e44c10cbe33f229bc79ecb6c1ef95a3b50915497c093eef46ef04a9abd74cf925f473719b97c9edd67cba8f5cbd281e7b3c6cd491d5bc8b9f50cec94c13767a336dcd570075d8387afbb6aaf95522cfa1d09638e59783e1b76d4f10e4eb71d0ad87fdccb7edb004705b38c89880bb3e2def856009f9adc4a71629385e7c0f64bc17b37954158f44f284784af37b0b32d6985787c921adbaa21d031ba77fd4e12def38e2262e6444c7abdd4eeecaa8e3cfeee95f707948f3d617c1e3c9fc8149fc970f7dfbcfd57e28f5b1d9e251695f4bc8b1f46e6ccaa0743ee71600aa3e28f33b18f9cf543893293ef78678a644e305dbe7e8d361139c57baf3c8ca2dee605060e6899f0c2cee1c3cca76abee2f94726e215981708afdc51fdda4caddfe65b9c4e54eda3de9f12101b967adbdec44997a18e4b0d2ea8016f33ee82945474e8e792aef1a59de394c0e53eec68ecbc93a7dbcd86bf672711f92ec89971e47914b8e05ca7a922d67bd7571378abfaa6545eaae78d191c9cfbde3949f77ebd324a6f7e8e3ca7cf07a318c69a9d02befbbe61ceb8ffbafa93dc7793732fee47c54db2d23611b6af9bdb1c6078ac691219d021c68a4993bd3da10a49c646e85caded9bc3b584f7e75e6a721aa8623c240cc0fd023792263f9aa003bb7f3d7369214af8410b82a1114b1a874fe8c6188b67bb5c1b874a0ec9eb44eeda301b711a82c7742529047f25fe18c0a7c47d4a41dad7b7dfaea6fd10ad9afa4006a57d8a93aba75ee6d8441cdbb983dda2bf212f987efbe64848527d64bad9645e507e1a980f7cd63a5c34ab735723db5ce0cefe32db687259f5b5426cbb7d7a57778a329c1996d2a8ebccb75663cb4cccd5e5871bd3587cd6af66a9be2acf38e8937a937e41bfd976c67130a92f9a739ed5f8a9576969884a2b03fa25c6eafe47a5736e413c0e82116df7b5ce465bcd1bc438ad2fc934f045dba8d79c259ec77e84193016c1c46f988213fc7198b63f7a75c04d783891b575adffeb7a6fafe6e7a21afe230222dc287f7e9f6dbf504cbea7fe074157be107ebe57b27bf479c9ca30f49c66712373c0c0bdcb64c685f782ce0aed5f782dc46bea7805f31cfaa03e9ce42689c4c70dee33636f8274321d1788f63362363615bd2f60e9385c6a8d3716330a11209b7af357e20fa3a2db3ca4ce7d043d9e4f548914b51bf2e9549a50f10766f879cdd72493d7a4299cbd44a38ff25b0e1af245f8559abe0ca543542ff42972d47c2ebf484317bb36ab1706419028bc76678a9693945f2ce20a8a7c7d3bba02443ad8572fef04f2ec844b75e8970bdfa351ee461828bc230ce37f26e27ca04187b6e2164feae56a9ad6121737e0bf6816fe72d62e2df915c643c5fd576151b3e81fae54b00f3fc56d0991e0d599bdac3b1ccbb95d23f0dc14a30844c716636189c6e937f439c45bb44d3b8ce5966c5186307ac8428f07dea7878dae9b4157a9042009b5dd55c1ef686a7d2cb10e666615ba9f92c75750b099ab9827311663acd0e144b6e4b8def61afdb6bc82cd8dcb65760bfd716647dcc71cd5fee6924dd297036082a0174ff1ee53b15271d9cfe6045edb266fc4353026be94bbd658575f4432f757e20faecc6af4d40b821a0859beb7d90bcf76f83d3c1b51f1073eceaa3e1fa1928addd702af42fa599137c48c2607f1c8ae82c388a9bc9dce5edce76f4ad4eea0630657b7b1b9978f9995c5b1abf378bd4b970c9321d9be24a8c27bd5c580371659bd5e82739ec4d476b874717f4e4934976911eed8a75b4b20866dca6a5bdd93da4a0e492b6175651a3c1f92b1f5d1b908bfb1e623dca97a330b4796cc2edd7285d268debbaf2c92ba9f91e90ea2725b7cb4bd84d1b002b2b24d9fad4fa3092b2cbff631e0d07b3ebb5f7874991e2e1c3ed14ffb25a6a66ecb07df38b9d45ccda86aee5223f8cd0b97744fca8ad72e63e6e4c834eceeb45be7f719d4134326a73eb25e25b093d1526d5cc80f1df3be39161c4cb07be68cf3c399355678b4e68334936a46b3e1cfe05c0a11b4c70b8f62cebe549d0f4d981c9c2935ff2bf10713f9283bb5deab4b6fb07a3f461c35ec3c609d1a46b9bf3c84adf3884fb47e7b917c57c7af76b3ebb15d920351b851013f921153e73c29f952bd9b149419c2c2eacd8eb376c4e7353de3818e59c911f0e2cc8332193a6b5cfaf59eeeb488876ae7279929b0d198f4337bb911cf37be9dbbacc8bb6cec62c9497617177eaf9a7de79bcdadf864c10596e866da91d7afeb2c75de52aa3a47e79257d2dc3ddceea236dfb9752e42f9f363b30e8dace955562ff45ee2b003345b69da0cbfed2258eadc5a965951aa7632a9aaa9d61d211e11307552299e66a4393e19468ddd0d2d7a8408a17b6136b51fc3bd8656dff47b63afe72a7802545de03a045c78ee09581c72b277d36fd2a4ed0b2d90d42df2ab1416e1497baa30543b4ee31708c49eed2ec6af8b9eb4aa6870beb05be9f19c575abef7cedb42e7242d99e9bf127f5cbf856e72ce562e9bd858f012b6a244038c6bdd04157f30a87be5af96aea8eeb45f7b8bf8c2972147d8e29f18e27bcd94b4fcaa836b6b14b5142f2cd38864575a2c60cd7a2cef6c56b7a93c779cd6c3046b95abec0a8d8f2f9426469d7b64f0eddc57b9c928611ad74623acbbb451145fdd638307c2caee0a60f3f77ec585c5cf3b84d4ccdfb538dcc4593ec770b768bb3adbfbc9ab95ceea36ee8dfda2dad8910f65fc4fd7b3b2217bb6c18e1044b4a8adabd493ba35c2457cb6700c3c0d486b67a3007e0818e37dcca5eea37bb3d47e510ee2bc34e7c79f6761e6abde70133d23529213b2f3226360922fa6e93a3ec1fb5dfd6afdaaab7d41786820eb4bd28561413e38de9f29c1da24f29c32eedb4930f97e4d99121f6e3c2dcad02e477c6df5e53d756de9c95a3fb1ee84ba800cc2cda7196e0b119c1731d5fe4afc0156193f1a5f3ad08effc21af155b427e17cf12e04157f9c17a1c198a139bc22aee816c2b759d473c3f85cde54a68bf3a307ac038f7d53482925589647032ee64b388690042cc825d99b62d374a4a8110fe16aafda6f6ea5a1535c50662a5249f968442d4c4f47730e5bce7ea81c199573e5f347377d387617d550a9449cc97d05cc6b77cfc609e07f696980cd3071d4cddf1fdf33f14e7cde487ede23ab4d3df8b6c532dea51eccd5a9d6baca83039d8b8abe92c2541eaf5fde22558a2b5623713c527bdce99f3f9d142c6828c35939ea7d0b534f543604bd7a9baa42b296adab8535ca1003eef160b6cf903de5e2135c6b2b112ec58390f2ae39ecb21846b0f87622d9d446925bcbc70a213da98f60b62b06efb6a822d178ef2cb392553346967ed9bdf024ca25653946f92a57eb05aa478ea917b7aabbbd47ea8aff4afcc11256cdf4a0665058c49c83659ea75f496522831f157f10e7f191aa7ea9ec73e55b99b2e310d8c1b0a1676dad1170b196d34c4b7961368f46a2ff291e469e29524fce7b1fe36c4ff695fc202242d7b94a12951dcc8cf9b73443f44dcaccf733acad969c2ef4f87de67351369cbe73c42d414c4898dea61d43f185969f59ce3123ef7ef53bcba120250ce91584206b5e9ac1f40211c9d58a19507d977b59a7bdcd8e62585abf022681607cd66144edba023dab88d24dfd20c9f3e878429f2d7ae3cb3994d453b6a7537c6de7c7fa096b982326f5f239a48ad278ba5f9d9b766138702d5d2d24a641d4bdd7be93908fe8efabea71a87a12181ac231b2c21dc1436dccc83a11b74233894d3e20f906eefe6edfeb5e63d775f09331fcfb480a09ed043295fc89f4af81db19a2beac05c168077ef9e2668a83f5b58c7f25fe786c2c7f1b8a63b1d92b0e45cb5fa9c5ec9de05e42c51f1c0a2ebe52d4dcc13324ab83b5ed5eaba689322636968f60b21fa965a6706adcc97db95535627b399eb3f290f0c915f796cd92bd51a09da5a9ae93ecd7cd0eddc1f13df451da7a1f15f2d4cc42c5405463c6ffb3fa5a4fd2de2b8a4e2f4be275393450c233f391589d711f2db10cc276e84013486f235e8ea2c590732d03434a1967cadca98ddcd7c5dcf0e983b5fab4e5fd271b9d19ecdc3702cb83908c38e11258317e57f0f371081d174d6f6dbc41a0dd46209c737299cee8bd15f7883dbb868d892906a955f416bd5b348339d6cb992b76836dae062b371e3f43436687389993d656eec187471a7973af76779ef30893b6ea77bc53b555be3301f69f0c1c21c2235f0bb308ec9a1d5b67e30f256cecbed4a6efa434575070afb6fed5c65a7a43dc5f893ff83a78a2a26f5f18aa5da6962fc77d096326e01947793eb7d7f2b107e782d7da167560b974cafbd48997ac12abdf16146d9a198c37da92709819053ce7c9edc97903cea6574baad5b412b39c39a4b86b948ced3bf090e21e3dbc0a3c8b2939b521cdde34ae3b21d246b687f6507309274c61bd4264abd7178d0d2f9585fe01f9f9c218446e605a7fd4d9890eade151262f364da1eae097f69c5d03d3bb82d13cc9cd42dba52ec2edd19609f1c1deafa82523f55e187e6c08261e79cd16e267abaabd43e5ec0881ce0db52a08e3c207af32c04217d1152cc6c06efe552cd38b2659899b6f5ebe9f1ec640b86e18b07d86d95fe3363a423ead6370dd256f40ead6eb61663204b909dd98c75c754086bdbeac8443abf3a026809fc56d5b46bbe9110f9121afa2ea5c5a94d0fbadbef74583cb15bd2e6abe07cfb14f4ea7d8c3d88924fc36f052ad3d9c5a49e06fff6a30e636a9b82b512cdb292f6e83324fc77960c0d1373e665565e73ed6c899875fa2f95a6836ea7c87237e84fe131d38e958ffbfa613c5970017bf9b0f9f06be0ea5bd6fa3535387eb8053cf70fbc5cd13da3f74e9265a02fd898ff48fe74efceac7ec94ef9f12aa169889f863f3f32fad7fc82933edd05fcfe295fec24c4110aa5bd83fcfbfffd21372ca4c3d4b50f16accaf936122f6ab6f262dfe2cfefcd1b4b4df235bfc84fc32374386afa3f9602b97e287006bd1ef2e8684d381aaabe7c25ecd0352b13da6bd47b2c72b3818f685d5892f6eed316c35159cc82207582327d3b1c7bfdfc15b481f999aef5ebcc7e488873ca2e62a3f718c2c6a275e20463a7077eda16ffc0397850e54fd8b30594c4219c4031c95488b93ca3507b21fe5117e6b9ec4f52df2ad9f2432ea298b2c791783231c4c53695ba30ae1864bdc3d0113e49f3159e8760296c3b754b0a07e700fc13771bbd21fac164ea719ff0f93e517b78fab698e40f8fe7cc5bfffc164d144f8f83823857c75edbc5df41444ecf59130472b2b98b3958eb22dcc51cad0d3c746c84956d95d44d946f7cfebff7f00260b1c6665c12fc0672ecc6f6e652e2c606e69091711828b20fef61fbfb0889508cc122a0443202d118248b830d4ca120683f2c1ada056961670a485e5e9f054b0acbc1c4585056102bf883fa41202f4ba9eaf366bf76555f0bded922ee817d45fe9bcfa8d28af33871b921abd60132f4064ea3f9243ea89b443f3092562fee227c33468da34b0ffeadbd768a6aba82e1140f6d0d643c2a9258dfff6af066d6e3fe521e0ce1c1fd580d55a598b9dfecbb1eb76233ed2b359cd8f4e67cea35cd3a95f668dffff82e7c19920104ed54d92be9abd3368d09b6835f8345ad1b41ab52a7f8714fd3a70aadea346d4cf3cddfa804e0b9a2fc583864db07a5f13fdf9cd9478dff60316358b589fe2470a185397bd5813c5f1372faaa8b73d1fe4d39196162896b9f35450bdab8c0d3d20698cf2fca50791ab0d48d4171770aad6329f7d4ec3a2fd28a4e4f8c494d1ba9098e46ed26f02717442b97dea177c0e9bc4a79c5fd029e71fc03fc3b93629a1e54ecabefeae3e577bac9feab2169f8c32e563bbd9bcfbe53a982e860b391344104ecd5c5b3d4744da8b0a1f1f804374899179b6cfe8e3cbdbed1e58d4db5ccc5f73a9dfffeca8c0747c6f7b54e5bfab15c0342d62b47ebbea28da2745d5a73d8866a4e117423f495038353da80622dbca16aa70fc0be7fbbb22ac59486776122b0d4566063edabd520b3df9371f3d3dfed7ca1f3cdf7ff7b7ae9c4e5582437e84af7eb07f332387ed6c27dbcc5c19a76465d0afebbf41716d7312fdc9fd47449db6ffbbca21e1d474a9d5b21b3587d528dc30e9ef35c2772bd8e3c5240874f2e5a05209cd813171cff8c17b619387762b4705726a2897bf19b287de1ff23f3093edb0245d9553465b50201071baa2f256cf8230714fc106e8a78b16a07279d050a86fb4cc1f8916a12a2a842a9af24ff39b5425ff2393f59244e71b56a170dd2b33223aaa6755118d2c6f3f9dbc7e50b5536eff52a4cb9a7a9c8e394c0575afc3c38ac6b735d09387f468bbf11733036ebb503e32a688db82b5a7c6766af4ef77c57463ed2b243c2a75e4038b6362a9e4dedf2e7941fd4bad33140ce028caf9fdad9deb2dccd084e1943a03550739ff9067c09b7f5660f687a29962e14bfc1852c5c9ed2f686f412c855de46e3e4385ce45714a970f8c6afe0108ba9023aa70f63b16ff26045d63704f07e46916638fa0470c66f764d9157912517e7d3521c2d158e22e4ab19d1f89d60426a6071419f6f1583324b09c1dc0fae8aea85bf67fd19affe868cd9f87a0abe366e92dede724a360e92bad6129eb0555b657b15454b3b57694b791777477d5b330d7f516d017d2f294f7fb4b456bacf89122480141a8084c040917b2b0148009c104a07021413802ca078589c0042ca096020270b8b9103f3f1fd2ca9c5f0421c82722286469250c15b4f80f47d09d86ece139980b2db59fbf78e41472bfa699bda2b8e1ed94cec2ee5b7fd97744d15645b52424b90f92b9b4f7cef6375e1e74784bc4d5c4fdf65b4b6bdc8c97d67c326a04ddbe907fa8bc88e47b01b9cd90a128348ed121d07360e07e0fb21c12814930315203993b7de150080ac60017fc5620f46fffffc7bfff7efef639a01a02695a55c9f06d3756053413c32597333d95bbebc451e7ddbd56f273af7d748b02cf055b67be3dab2c2289597940b1eaed22da8e8707022f76dfd0a3057fbda667f5c96875ed009b624c4959bc8f4cff9b6bc5e2a16e921558dba446128b6190c28a8d353f54994ead3f0b847e6973b4c546e0b9907021d65b0c6a7fd5ec50293d421a512958888484a389c5f6b060d3876a35becb0b6f8338e732b0e394aec99594f9b7f116bce6af8f88e35edf6e0b491ba3269395335781ee7951216f65ad196d90205b76d5a32edc7f7586bbbb061a6a797fa111a733e5f94ef803347e9d0097ecfdd1c6330cf7b4d0e432886806d7acd337307d361189eafc2e035426e5a257e6351fdb2db1603cd24e19749e5de43eb87ea04ae0ba97c0cd29b7ed1be4d8efd4df2847fdb0037b55086f24cde3a3f2c2ce6d353d22ef4a7e554fcf8b5475c1f7709ac6a398c7a5ae3df0f262341747c686e2dee6fd9420422b6e8c7353f64cf4e375fa9084d77bd8fdfc52e474efcf48f19b946746d85d71782030422b51f593980ef1687f5b001008747c61415db94ea2699411e366bbb97c6d4b6211455fe7977eb4f0293117fbad029d828092d12d5d8f6c24f1350e7eaf937a423910f447b77c7ec354fe28f8af612a033cb59fc1544605607a4a80d31fc254169e7425bee227eb071ef695cba11dfffc4d89f8b7e257d8518f41d525da8d464e4bccc34d45eb3d730bdb21f94c49297f354c65d025e92fe9272641a4d4139c6cfb80b97c7ea9e540a672a2db95f2261ad5f49d1a53f9e20055182f92715c5664a521c313ce65784db8ed6787b3874e7a1d4c2f98a481252c174e977abb061dbd864fe8148841e004b208df1728ac3b14bb14f11714efa3886ca085fcb45d459d475a24552df72391010085ead0fc4a39aa9f67c0407d70f03f8f86e52b0cbc54ea848d0df59e1b76811cdf04cbb951bc3f753a2b00a5fb812ab2447a4a2123fdf131ffa9efffa73f52603004048af1d533b073fe7dd0acfc8af921d5697534640f27f1b70a3cece23e353811bdfdcf4c4e17ad4395db838982e7b05045ab3cce92fe5161206252519899ccfc57721d5353b6de591b3d429cc516624ad23f8a973062a1a7621c3568ef7e657bfa02c27756945eef45a298dd17f28cbb4db073901b9da0c9bbcb03efe61062cc3b0348ef37de623fe2729e72d73d65341412723a8f1b954e451552404789b2f7b4e5898b0f59ccbe62c74661a574f4da70fbe4331531d78678fe9d387e43d54d5c3aa55b568e8ed51ea4c36264566977bbebcdd91748a02f3d1235a3c877ce3d392dca199b4b2ad2c3be107f7c3ce5318489e583ca849badc5d6409c08d9ad78e3e6a8473a68ef9871cbde77c9592bddb1b778a8bb1d8166d77b1017b2f49822effd32f6c22951fea4c831e57083835ec7064a5381d8c4858a053f26061040465c2730d83844e4f7dcccfa1eb614efb62108149431c03409da0437eaafd8782ae3f5f6deefeca8b88c76daf1cb900d7c72e73c90b49b1e0abbc6619199f5222d5dc670c4cd78c1867ad7a18a818abec39e84244cf7aee13bb9556d2fcec094f3da773e784ddfbfcc328fe0dc2d39edf849c53ed580f688a3de18760ca151a75994d76e4ad8c75cb6223e17b73f6bc6080e325b4a7c5ec23768dbcc31de39bf6b1dcd24de2f72d5fbd5996eb97459e117b9a71d7fec33f2c29532cfaf4657aa6736446fb521680370d667c8b36f5614ba1c861e0432ca5c57426f081492b7645482c7b87e3ebafc966fe86914a1ac78b4c546d7a8f4697fbfd02b4125d76f082330df20656e80fc597c63688d5deca4c1ae2ecef348379e0a15264b35d78dae2f925a579d0d31efd03cb0f5876f90e514403c47acbaac8d284efbfb5b36c9f3c62f98c3455689d4db249daf9dddb91b3642d1f4f2b9d67536b3dbfe60cc805e5c7de766f3afd24c5e862a8634c1897c549a709aed437a59fb1009f1d653ae3fc06660a624441358d9e739104e7f0ecffd669ed1d2297b9cceca079b2620b4bcb75a347b6d2cd315f6b1e70600c0556733aba953a74f6799ff2a9370d6e4a72b9eaa8012e47a2fec407bdffc595e1aef59ae67253626315403b212a37077274c526f3c95143b74b253d0da1c26cf0104acee809de04fcaa7ce91c35ebdb9c7119ca346d600d0ef772e8f7b289209960ff8b41b5cbba8e6d0e2f70990ec91f279e0ba87b0c867bb108cdc0a6a6f9c26efeb009bfa3ed25232823086aa5534b5577d78585c0b3b1e90bb7f3f6ebf4f43a26c6836c340f3b9d98d1423b14e0040f8fd0ed60492e5a32e156376af8370cad79a1e10c0fe993a0472a2fae663f244df767584924b5adb38edc1717ade4581548962d6c6f70155dc69201cac83e864c09583a6ecedd8bd2766e397be52afa2bbe357edb3903203020a97bbab9b1f4a6e5d7b8df55293c6724047ca35ff38bd8f7842f0ad4120e6b7945d13cedbe1a3433ba39ac7e9fdb8e1123218d318a296f5ee713a68a19ba1e80080e0cd73b48bfb54a95113b79e338b9e7f5abb1a779ee4387deb49b050d8a75cff6fd9776dd458fb327a7ca800577ab73f8b3fbb6e153f3ff3396c2f8d532896f14edab99fb2697965831ed0c68ce77f2f694287ca3ae00249c745dfefb84c97a2e5a4849d5ee8247de77d2dbc2acadd7738470ddfa16b7ce079f70e3b63eb7bd613c5bd3ae52b821c78df7b9f0b6badcb8c6646dcee3bf4c970313fa80699d7770a1d81211e533cb09e550b82ce3f2ed7a5a6e522b0b9dd18d6f39df18b283bbfd1a8e74bf84e2a14182d938f987db1bec2fd3bfd33907024a0f414bab9f09d484c073b9e992fb82538a7f08fe989fadd3c45c481d6b5df69ff627451954599f7c2b3efd00394e8c38b6b2f5c2bf80e5d65bcb282675596f67b715b68f3d562f89abcc6f7729ef1a43b96bf2edc30e3faaf140008d16f4639fd5e780d049decfa1a00e0f32bd5b7176f45aa87e8df7d09dc46be53b1714d4491357cda0b1960cb4af110bc53868b4ee9ab8001ac0f7641644df27ea879303bfac647eaa83f434ff87ffbb00b30be4ffa97a4739eee316a091ea993460eb60545547b9fa6f166c81ec6df99827674b30603ddcb342cf847ce08648420926dd8db3d58e76edd2747625ff07565bcf2671e3ed9713ce1a15f45545438a9719a0af268ad04675e0181c85d53ff499ff2ffc39bc2389d2709640810e402103f1c3c7872000e620928b503227c0ef43c19bea0183faabc93d3f223f8d1e76263ad534a8de40fcd218a353e2d8f206dea9d16dc25955573bc8dbdaf943c3f57690f3fe524a1526cff849e8c96ebf8adea4cbea90f73c4a3920aff2f476d0f044f1adff8b0faaf5218156920867cdbf5655124dd51dc03a92584ec059adf9517538bf991f9463a70e5a2fa51c3cf828b2a1906250a2cf22d1f60ecf6390a6df7eee759aeac3eaccdddba74b71b8e9b7205a9160fee7952ebaaa136e41ccc73b6713eabc1b506cb37179b6e00f203de87f437e34fdd53496eea601cabc70a9792b0ddfb03eeb9451d92d624dec8451f2fe81877ba482f685cff5668eddb5421f9769007bc6b4ac136595b43edb27332a24ab48f1a9552ec8ff4baf87580febfeb3287fc17b8363ed36fe0daffb6ac930b6dc9ebcf1faac0328be803f78c280ade1eed23729c24b1b16f7d2a94548fc8e63ca84ee179295e651ff704bb22c93af1047efda7ac93cf7765ceb442acdfab920bdc956f97eb53226835386588e8ffb24e7eedf3e7659d582a2075b495ad3cbc150c3d3d7ddc2e7b29ba09ca590a69b86aab586bc1749decdc10087b6d6d294f9897e02fb2647e6253f813b24e8411420202427c823073a839dc0a2a22602e22081340400584a0560873183f1fcc42008a845a080858595941e1e62270380c0613301736b71280422dffc3b34e1a1ea41e7144ecb56a70c77dc8217b30ec465528e94cb53b505b491dc823cefc164233d2666bf66899bbd0005d3d084fa34d49d73489eff1d33144dc07f71c0d6fd459277b686f7faffad0da4faffafef944cfe2a685c42b72459a5aff238d054c067edbcf4674caeeee90f2b75dd112ad4ed38f5d16c8151503164da49804de3c7263f811d5479fc87d40fbeab1304e8db7511b9b580eb1a67fd87fb6ea834cfd9feafb93549fa19a8f80a28ba58bcc65692d6f5d4798a20ff4b290858b818187bdae9b8f9a94ada52c5207eaa2a22267a7f89f33bf520de10e4a1f649e648c9e490a7e01a50bbf16f9e08ae9b0770374fd8c8792d3577172261f4e91ae339642c31be969298741384de19d4fcde3c526e3fa4a6febbd9d462dda8351a4540dae8ecdbfe58f45104d3f04853443763543fee7ef3f281af28900f2801a12c9991ee392370f0dc0dce63a09bb0a6d704224ea6f3f15ea2d98333c02fca9c1a840bb82b94b82bfff346457d637d1f2f79fc64a49f141b57fff293888d93de577dfed8f24e64b9d970485a7248784e7bfe70df9adf8cadffffae731b1880db98312a459bcd8cd3c8dbc101f8a3195be7bf8cb815bbd21e7fc8f8af67f4a93ecd9cc94db5cbf59a1bfeff8eecbff4a6eaa6ec7babdbc8e4ae5a2b98acacce9dbbd828a268b414a7185aa1444aeea447b21acc4298bec06c751e1fc68fc7dbbc60903813c01492bba82ed3947af532866db1d230bac266e2b8c382980409e803b40eb07e52f87fcad87c3ef721854e63d9df9d4c4fae1773f70ec9a3db568c2bfc984fe361cc64d33e82fe04c0cf5bdc0185f5e944fa2dd417c79db2c3b79c9fa5a4ae38fec234a72a2c23b39a217143f253884ac48a5b8493322fecf84fe8fde47febc6bf6869adace06ea8a9e425abe9aead297e1f2d2ba96b26a507b2717a8a0bcac23c25c5a5a45dd4fc0108630b0ff4b99d0483e7e41217321045458c0dc52841f692100b780f22185a056e67c968296082104d45c4408c10f17828b0843a116704b1854c00a2a0c13b4e41784c3ffc34d68f36cba5cada8822792ecdf9619d46d95e22ebfa39f8ddd5fcd5eedee9c3b9f6d6e22434695b53f3633e9ee3db8e66f3192197733183bdbfe6da4a9c43da13e4f41d4d7ec211a7fcf8c91c5ac81c8da7911039402def202ee2535f935656cb5d6c2f7e6aaf3a296effecc00f75fef4195100992c5d3e7059cee1954cbbc70f0e8f681144e73e239833e45c9e05c3ba5dc9dc853c7ffe0a5832aedeff5d927b482225d49a288f7d3419f4e2bf4cd7b90be28ba3df7eefd105038355d6a0da4996e048c7b3a5500016d0ff1aabd2ac0fd2643f9081b966fe38948d99e752864eebc69ebfc10f3fc2bf9d3b11d38e4cf648288c26ea5f341aeb559232b0be57a139478ab76a9dffdf2dfa70a45ff647f31df629a5bb839bbbbfff4fb04769e8e2e566e3c563e1636e64ed656fffa9b18d82f2564123e9e1cf265ff86a295901f0103fba3e735ded1f2e493c6d568fe45a6dad18631e35dd20b3b61be39971c2eba42f6c03e51604b19153f03ffffbeb50cb68cea4e7a856a07442c7d909f3456a2a648e9b59a460e06446a9536b6be8f2c2ad8982bffa4b63d71fccbd703a96d684b3c6c0467f308a4da258441f71d2fa21af62977504ca89aaa9283cbcf6b93be57be6b98718d4d8c7e17df7c69157824346ef604ac9bcfe6541e8cb18aed282a36a7f679b588247eb6950fabd3da5670cda06f98fe2169a3907276cc7a6c8be2bf74b5e8ef53fef7ab45c702de7f23b069f6fe226bdde6826503fea85ac803cc7a9adc2d9b4b0f0d93cd8c3b83a73c8b9638bbbfd279c56140a8cc1425737239f03c4a3ef08afc88b5fed82246fbc2136cf41ecd69f3f247e10c368b6992ff67adff475beb9886566eb6ce4e3ffdfeb16b9686080f4b1755793977293d75117998be94055c0e61ae200575d1b5755345dadb4a09c8491b68fada0a43ff52d6ba05d41c6685b4b2b0b41282c19150b888a0255210c16f692580e4b7e217b1e4872391fc50045458080ab5b01214b412b212121484c1ac848591fc027c42ffd9d67a7c2ae923cb214acb060f32cbf3b6ce3668e18c0a63565ec1fc023df99aa9d32045151632322bedacfcca475992837267e74649cdea7908fcf30cd52aa3cd4d24515716de03eb45329b8b748780c271bcde869f4f56ae01cbde574c3e220721a722c82c41204bd44b8fea727db97fdbb2a3cbda958f06cf34531f7409f9445b03a032c34a94640363cecc36dc69c0d3fda229a529510b009cd1ab02d3851169a84d421b738c2c6db0df2d110312b214f73dd99a86c954df63bfc71f60d6e833782e75c2a2fc36dc13e85ae77d2b664ea0cb5c99f439a1420a8ee7e3a0b6c1df2266589fbe84f32ea656cb12a127cf1fffd24bb84c4fd8ed67505756363905b12525f734e587281d01c81e1af93fa07dfede720d1a7aa1522f2aa0bb5396a047951cff634643b17ba6179d0a181e82490aa50a78af5bab749517c5fb6751d1556d9d3ce8651ccc6d1d7f4ad030a03c7c3cffaa1c2b0df36eb5d969ce316097d7c3de352a8f0a9601ee592e3cad781791d353c3ff6d2bdcc684fa4b58e4bd09144da2809ae1b8fc7deabf04af646ae3e1e1f2b79dc2cdcaddd9c1cb8ac7d1cacdc2c18ac7c7d78fd7d6c5c99dd71e0675f5b38579ba7ac22cedbdad1c903e02560e3ec2425e4e3e7ee622b69eae8e8ef696ae828ec2700f017ba8a09080a0133fc20ae6e3e76a2e6c813afe235532ac98b95165a2b78d1cbd6b48e25c52725ef890364e6dfd95200909084d62454cf5c53712e17782c3425acbd95bb41fa4d1779f83231aba056ba32e5cceb81c875a910d5ec79b7a147e36a819140e79231781832dc3f7df25c17ffbf39fc3d6c16f99c429a042c9c55937329f7d70a624a08f3811080a348377a719b2ab1572ec0afa3fb72ad6edaa4d2a3dca70b7fecc5aa6d82aeee2b5beb4135be50c788dba5512f9b4052c9d76e777c698194784ba2a6cad6b27a2e05fbcdf7d3c6a5f25fd6745edffb426b514c1c62e4421ff741050251df2471fedd1ce46a1b5f05da02d16ffaf783aed60841d55e7f1067f08370b2d3ee851274a271985054382e27d1214924982a203248073d1248505d601bccc2b0f4b6ecb2ebe8eec92d8d6b9784a0b0dfb74e34775d31165b17a14ef9f41f13eaa62f538282c487c14e323f8215cb71f022efa97f8efd41095a7454c97fce9f1fdc4f30bdacf3c1d7f83fe099c2e13511c936a4af438f3c2c10597d261e39e5836fdd3068a7ee1fcff1058c11f8dedd7ce3f4abc15d9eee7370a7e5686c85bd1919163d43f549563c12363b85689bb9eb4f5caa787afa36c191f443b9cd0bf04b2db3e473f32c7a8d6f047e014ffe8594737d37aac1b56be974fa51d2aa29a653fffcdf0ffc7f22ffd9f26ffa9c449490918ec1425509d4daa4d328237483be7ff45f997fe33e5ff8fc6f68be51f55f79837c991df8dc292f74be714f02f7e0fe7132c93762182de072cf66be5df17e394f20ff8e852ac1544afaf62c36de7ded64cf907f5a42c4a8c5f188194fcc5f207fac563fb4bc9f74fcccf5f59bec1b20ecf95bff1f69e7c490fa65cf68be59bec47e618d51aa2c2c9fd95f2fdab75cb2f18db9f2edf3665f2fa025fddf2f07b37af4dcd7535445a1dc8fdbbe4fb27e6e787e4fb8fc6f68be71fd509cd1cfa9c61c98fccc1a2bc8574fba0527d8656bed5d0cb047d67494d5437cf010714afc8b6933c35cf969278bff2bfbe551ba7b83b8c3c597f38ce9f4cb75dfb77c9e74ff007aa23d433e63e0e3a5ab2323fdb3f740b2b87d3ec111c7c3e165650b83c5c44400806e3e73347f0410584842cac84a48585913069296119416179615938fca76e349e2dfd91f9fd89f8192affa6f974f2810ae90d03051d1345fb58a8e4f3579f709fb6440eaa87fe94ba03f467beffefd8bfbcfdc274badda478f15c1c9b2fa55144f1d9ab3cf977ed5f3f313f3fb47ffdd1d87ef1fca3e2cff689ba4f808bcc8ca363773e149bb7a0e7556e77c5fb26d5a8bd00e3b9a77f3de0fb82f6a9ffae03c34549c96b78949b04e457a46cabe5b70fc5787f8847bd4c3e9145e89b7f346439c862acb349a97eca7672c11a98b3edf7487cb742a5f21d428dfe17d7ff87527c7e78ecff8fc9df4fcccfff13f207855522dac2ac5e5b697863d35159fa5f59d520427bed00763a5c8f954fc2e3fa913954a07d25a4fe8e2938a97a56a4039ef220dae7d17733faf9eefa13f02878137e8f2ec7ef68269fad78f37f71fdd17f64fd7f64ecff2ef9fbfd01f34fd8707f9a7dfe13f3037e66aaecfdc239737bc8bac358f699d9d3df7fe1f7f4df12832d6a9d2277f9fe3bc7e75f4b0c4e9b458c72171c8a0830ca7f85880b96c476857c7c1d1e3d24f67eefc22b8d7940f16ff8700f124dcf82cb02a758addf63f93e65be7f2c8af64f9d18ccefae6ea06efdd34b21854779e6aeee6a68e4847be67b3b6bf1ee82fd99999b1d71ce76d86bfc794607faeedb62ebf692606d1952c5c22535b91a15bcadf2a0627bc518b3e0452d8cb35ca873487613fe27632be2c0d9e7e49cf333de34c7ffac94eb7acd9fc86553787755c9a5b0ddf92071f5fcef5f41b533fc9ede0cd9031385fc0f1fa01b057a84fd08cbfe84492e09478820113001412b24824f44c4dc9c1fc6078309c38590e6028230244c445008ce276c05e7b714114020ac9048217e989085a0a5b9958820020e1741a06adf42444040002a04b71016b0e0835a22cc05cca1824861a4391c09b5e24358585a08f3f19b23a04841a839524840801f011711b610820af15b218505cce1cd90e12851e67376a374afff313311608dcfbbcd7d517f3f10bc06e6c0d8008540de446e222559d02e4518d8f082220cc2f06e85a04eb489fd508691bda676b71d66db1492006bc9dce28f3c91afcfc8d3a36eb5f46134397c74087cb149e2e2c1e12732cac766974e6a956809770175ab8e53be83436d6934096d755635751f29031b19864f64d906c3843f318387d766514e7c68edbfa0d7762271d71cc71270feabe594e4901ac89cc2398953322b30bf24599efcd9a840e77c674b1966e878f1ccc5c72980a8c1b69145f27bfb20ee1884cb6ca1837f696cf106cff7dbfeadbb274b38aa1297dc76a78c0f3033b105039021b71ab8ae22dc8c92c8ed48de1da2aff28c62c4dd38cd1e8f4caa12fbf9edef5f98a33fb9e0414e5a276f266c959e79148b2978537f57f422ee696a2b4a3d301ded7c174d6aefc9db201277952598b92c734591e85b6ae1e81739c7c8497e0a8dcb57e36ecf2ede8f6a191b28a5cf1bfe28a78dff382a7fd7b9e913753213e3bf90418df634249cf0f954384bc8e71ab4472f423c454eb793a24caa0281c0a799734050ea0c34203ad29a74e9e10b150605963b4b208cb339e898603408d619103af6cf1d0fa002173b319b17bdeef5d4c9f4a5f8d3d1774f4cd840af3fb9c4357afd45c993e9f14b27d33b6127d21b324ecc16466f744695288ebe3800b65a47fa280c9b2ffb7b9c7fe5e2728405e10d038771c6f6a48e746e6edb13c2e2f7f17b707912e9d091fd55da4547b620b6bea7e14777259097bdb7530356b46b63a3ef7a35e58352ac7af50ee06e912ffa5d84751cdb9db4afb8b7b67c645c8b6d180b9f6b2305cfe115c41dfab6508beab3934fe35efbccc9f0c4231955ffd02b524b74c5047956c69e1fe2658ed8e97d92bd7b7deb62bb8615bb783ba980a997c14d22ae83f3948f11161ab24979a33d8a3675d8b7df6d7accdf7f116b7aeeb38f3032f43149945ca3d5559e72f796caafcf9cd3d6cd8c1fe914361ed02776ec28868862bf3a97bf933d55e49e45bb3ad64b9193f93430742be74080649027f26ef35e734f4838b5746e4d73bb2cdd5eca8f0419ffc0000028e06b58a646308f682d3cb829f17ed32b587da91d208189efa21207d568f64dd90eccb909f93035cd331d809b8aae5aa9e4db44baeafa07ea6e2aba999f27f65901770de2faf5262b9a73fcb84a30ec798330a80b22a70049aea63e841b526ed2380e1be1995aef441ff0278ab21ea7ef1eb49e3730df793dfd4d43c7b37d214b267a0050b46af630eacd08b970f4563ac7a7c912785972bfd6d871fae82b78189147814edee30cc9725a7a825ed94cc05d09ddac4379d0042eae5018a143738e7314e36ee57d8003a724b69eb925648626527cc1b70b37e261d37b00b4f10db78a523ceee97b43eacf2259b68f6e101a39003c977ded21fc0819e4dd317772f9e5b793f6aa690bb3c7e99154df6227298ad4e56ed3aede23dcb974db0807a08f585498440a25aa3e0513470adf76bdda385c250ed0376bbc95d5878f2975d439cbd6ee68bcedddcc0e0104b9d85503ebb2b74a3f04a45709b0f4cd573084dd02f0471b1d9f48dd811ed296a640f82d9c90c44c7c0f506834fbfac0fd4ad95db584f7bcdde5816ff69a442d01870c094b9c44b87589e1fda0bb0d8a955c59f0d90c00f8efa75c179c92d1b84881bad9472ab18c34a4086f4080ef0b9b128d22c2858077d45fa386bfe53ee233f2f038fdf1fdcbb965a6a9e90988e0ec41f8906d94ee66df71ba037b2951f1fce45e2101d7216e8aeda0cb320450b247b5c0adcf31d7c9547655f5f53934952d13ee89a1e3749a9ec2ad41627f66082fe54795b749f25d578d0050a6712526a5755daa1238dd9c9f680543b71b73cb9b8ed3fd2abd52c2acc5769f3ff1f52c6cba54da5ec90e285ead400c46c25a0bdea3795e330d8310cc336b7a000e3102c9228571291cd2355b5b161e3ac27d9566160037c537dc6f115c4f463bf420a815616fefa518591a0704b96e932f61753fef5813e1166b63637d6a577d910150da832fed6630ef598a489c80feb3896f60ddf114f2801ba72556b70fc90b0337c789c0154dcf1c3faccd7a00dc8798eb890a7a6f64647cf4708e5a941fe244dbe803ecebeacb104f51c130d2c03c176516e1db2cf7e4ecb28fd39f7c7aed796986a124602713f359f45dc523de48a0fb12102a3b3a29f381460b591221c26d59a69402583f85f7f01b79185b4d5efab9737261e977c536656c8ed31375185f5c7acfd6b67dc31392bcebfb911b9207d08fe7bea18d30f9c752bfbefe51c4d9a5a254c1e900b0614aeeb88cc57053089a44e413e34b62e4a47a8900dcdc07b1796b16435f725a127b4bd7e60f55024d3f01f6eb9a74f54dd78c0cb42b19eb0e8d165b4f721f3e9c04c8f7bae7560d4efe5b1222e13daa3d319cf9bc150034fd4866d80149a0ec6e33331698305d05ed5e652e003c9cec915ceaf63242eb8c8b7780a916e58416733300a0004ae5b57c44f91ef3ac156862f335de58425169d071fa32044d5ebc5e6679b4bcfd66a26f59004b8617005a5fc2c3522e7de85ae12e3a3a1306da3d09a8a21f607e5dd87726b86a3389d00f6232effbd6442b1c2d005cb642c70c99c3e7ca8b6cfcc57e159889cdc4eccb00fd2ee4d18a77c0201f351d2c2732dfdc5517491f0538c0deda987f45eb690b2f7662599cb0c7e0dd78470a7000ac1fbc35b2c4d22f2e299f8fac10b4529050b695384eaf2d99bb8711851721ce7e05e7a29863cd43810e007678d674669c8f693511c3aad917d3622ba9c4a81200c235a94221bf5324df7b0f42a69497e60c4970f47ca5e3f4fbeff240ec3422984111ecabaed1185550e803400095b6783e5685cdc8dc396a1cffe80da7f9fcb9a780db6db11516c2b0cf7ba08207328a97eb2f1956760e01f65f919c7433553e55424e64d605f4eea3540e161ce5e374ba9b8a0d6c431b039379d7a69fa9e0f6f0e6da03e46f5577c3afbde6cbd848ab6360ccc41686c54a3ac00162f1edee703b1365187b09b2403af4b19c2d5a1060cf33692796baa636d65ebeac2b9f82b63681e39f0208030574cc87855ed67b674698a5813dbd21deb5bd02287cda317da55c1c39d033ebeb221e5e67172904170024e1652774b7422888ce1a233ebcd7b6cf4e9e30a14f07e8f7dc2910c1d617ce3e7fd2667ef567f2f694ba80d20e0a85649693f89ded532c1ab2e78817434c679080f977c35fce2a7b09de79e370b537d4bcec89c41949c09d28dafc2cefafbb1f3aaa6c31723a468b03c9566c8d012eb1e76c58fdcbe0071241afbbf35e13259a3d6b0a001c32900714f0f7ccee5806cb501c7ca0abcfaed600006b3139385de49024a330e50dc4dcc0e9c7d754bc06b05f7817f125da238265f44071d704ded2957e6350773b4e4f3bcba79c621f15f8ec92c4aa96df8659ddcbabed00fd9a7c83038d8353edbc7986f4cb6055fd6f36177b8ed30f303eb06cde4e2e322c4a4cea81dd381c4e4e00007e7f76bfcff27e266a77924497b6bac187882a3000507a42fbd1ee8b2a81e4db3d2fd9159b4cee3c17be3002b0d737436e0ad73ae229d4124c4ad1c581c26d823000f693d7334f19c267fcfee65f8441c52f03a837886fd71fa78771c67139f7de0d463cc1ef0d8f2118ba347711a07f0c9c75ee5a58380abfd356ef3d58bd21e4ba6f680a906f70a350ef57964285ee3d6221c6049ccf6ad800fb819b7e8674c9a4a9ffddd16aa6ebb9d5e11a7fdd2e80fec0e96e71e3d1dbd279d95fc75dad07a9e210051477088fe16c8f2dea4e2756d5185f105b1ef7b8ce03a8be579fb03116f4d2bab0b110511f1ab3ca9c693509a8c8e8b8f2f6dd2a84a17c801f7fce23b42bbaec190120c85e5923ffc4bcec2baf5bf1c1a572f9404b28c77580fd0489f9d4ed8b1427b3b1fb7a2e82256daa0bdd1f70008300d1ea243ddd1b1d2b4cb8102bceb51b9b2c0fd0cf0c3192371f93a705274e3cfa98aadd6c6f11a50a3860d555eef4bc21688cb71f13dd5c3186b7a18f9cc43f4e17fc601f13623fddaef576063c5f4605ee8c7b0190cf82cf61ab415a695cadfd9ce7a7f9f29c32a9f200003a0fe33ec266fa48e7a434ac89de2b2bb578915400ca05127ee4c7102ff54d6ad2f170f259fbba429fee0e281df24cfcbcbf23d3f3337e6f6e8b8dabbb5b91563d0524f05c32147d9ef91833e20391d2c25c517a0115350b607fee7dba40e5a4b6f20e3ffd75955204ae8a9bb234c07e3a4f68017d80173a16c0d2dac313a36f366f7a0350da4298c85fd8905f88dd8cd13e59cd497b25a06f1750c794596bfc7aac9d5b63686adcfa82ecd32192ca5200f8b623eba2654ed0c57725911842e53dc22eb65be5fdc7e94e03bc31bd7df7ad2f3f7adb7abd55efee672f24e050e5820257179dc10301a530b501d5031c98451b1540be481f7b3e4a984bba3c49f759b0ae4493f2ea9a35c03fc11e2b38d00e1645178f27ab69ab7c7430308e009cbc285f1bd385a99be57b405d66b673acfdb8ed0f00f669b908a4f1e63dcfc4869c59cd6ccd4f5f3d5ef803f607d28e116c95f28f9f582f92bb2c1b05cbb8666b03d6bfe1f575738146cc053c575b34235f7d6e8e620ac0fe4d80a0228fc2358b3a90619eb94469b47bd6bd16605fae3b3dbe4e599237dd11af32ecc769da79bb4a1cc01f9df6ca9d8b0cd26eb65fb0d0ae6a63c0bbfa5f03faa7704d6c5a29b567857588cc56be7bfac9ec083dc0bee3b55e95f350168528e89e91a0baf3cc212bd70e700c02a266b77cc5e4134fc2c13655a21f4c26289ab40bf08fa5088a9358b96f6f64ed7d6a30221d97c4c507e8578116063af10f78a1e96f2c3a66be5c670d45b8018ab34c2908309f1da9a41ef06e47934b49d80b8c65002072de1773857ff0eaba823e543110c23f5d9f11273d779cbe37d0f78d5f7ce22bdbee57bc76ccdea8beead60b00fdf5d22233792a8305f6397c5180649c79137d0a7031bc2cb1d879bd2dfe9399da131792db98713c2d94fb80fd1307d36c97d1297bef66d9f39c6f329c3a1701e62388304505b7e92656b294137e37425d76a887e532403fe99de55df8644c7c9d7ae78a2971a6b266d7b70b009c56b790f733d629e1c4ad0111fdc49bead152780b00ff6ad006edd99416c9b8fc6af15763116dd7cfa6e28cc7e93267ae1de674e506f97f9c39da08b917b6e87709607fc491c2b4d59278eb3a7299361aa1e1fed0226380fd428508dba3ae494b8f9096c0d22b985e58e4a407603c2d97ec865456be8a6dd99367f91c7cffb11b9f3760ff08f43629902838e460e96f6d98a7b810f761c505004ee6d5e1420c7bfce5dc6478ee13760b1dbea5ea0940c109460a57abc795d2fa65c3e0f8b0b36789f5d93c998ed3a3d31fd64d3770b8358abfd98c783e3a7deb793f00f43290574cb7ccc1132cb37e1543b5e15be34c512040ff885557e9e3f564be4fd037e0cd24bdff6011f21650ba4a054a5381956fdcc9755f22f84b3c728825af12d0be79cd9329deb3cdae9ec956375363ad7b8207810535ced3f4c40683fa17895af8679de86e74d6b027ec1ca75f947f13aaa8e993914827814c55e4bee7b9b304f05f82f01b30dafb2e7a7e216562328831f51faa3007f877ea8e2c0d8bf5197de4c6f1238e067cc58ef5e400fbbd41b1ce8ed5dc4f40f3cb0689322e439b6ed00e0002dd14bf74d9b63b815cbaa2408a171ec8a569cb07d00fef797c41f2ab88cb6c379a744d4a47bb8b9e355a1ca7a753ae76219885c7fd64ec0c7c24d0975af221b78fd38d74940d98def2d11b53e30e1a4fbce2e9df1704f8774396ebafc2d82fee9fbb99b188ebd797dea8eb0200bfae4b32510f30115ae9fc14de67c41e9861c5630a88375e5a08cddff2bd7e3537f84a2ee5055d69c8576a4092e8dee5644e714611568d07e17551c6c851eae47d80fe240dbe47e0cf75e10c9278f70d6e2389ada171141d40ffdab93cb4acb29c61a3e71f1af7e82711a71f223e4ec78abe6aca2294da1da48cc1aee2a4a312caf50180b476db6b9526c55b081adda9e6e2803f28b5d4310300402c9c13cdb21b9a345aba5de8997805f23ad7970970c109596f986acc1529f3b086f669d84d9dfb1d44a200ff6de5db0d4b6edb34df1bdfb61b3282cd3754f2be02f8c3b3c13e3f98ba424c647428b29ce819eb872b4580fd2d2a6b534c0777463946dcc480c943c24bd4c4e32b803f469b3c2c9847f6e49a11c2bd970736998b4c00f6c3d0866cb3f0d346e71d5a6701aff5c7693c62dc80f9e3f9faa8a7bf788a2b4348deaf3554dec4e68625c0bef26239a22ed65fe149bbd65cd3e0bf9c454510b87c9ceed11c8da19c4c11154942ec8c2e25d85dee1d05905fcdc07738f35f2fae29db08e56c44e175d06b0903f8dbd83b6d4c75c97c7b1374386bd6472a747e460e20bf66574a29089b880986e22d7bbad6c74befc5b8911fa7e3b9544f6c785153ae230298b5882187d3f3ee00ff4cb29f1d1f16c16ec3ad393a31d895b96dfa6815b0bf9bcd7f54448ca7f1b3068bbae4eec5a2ef10db01f61f2f02ae0097a5d905f067ec87de71767b2bea970109f467aeb524efd139efa764094fe488602a7a6728ec01e203ca2f0e6a8dddf34208d4835f17d75552ba6fea01decfa6c1fb36b1d3c73b763da157a240a377571b8099f26c124cb5f16e4ecf9d992a6ff2193d2dbf8c3fe01230179692ff253a08431da91f32de97c45fc35d09609f46ddc8d89b83a97de31c7493d8bb199e1ccfd29207300f38ac6530321160aca75d6f70ef5595e3977800ec87cda0f37c23be51126bb6602e0b2aeb0b2e965c00f9f17a3f7e44f094d57343dc7665d63ce4c51c5acae7e3f4237d1133136bbd226277fcba84b009539227762dc7e919c33c65439dec4b1936a4f41bbe09d718aedd78739cbe439156346f7e35f9223ff587d1a53c872dbbf780f8eec6f28576eb022e2dc50fcf864d6194b8da2c8900e067456abf84685aea2f16627adb1a32a19b6edee680fdbd39fc91175760cf57c94bf61df56a507fafe669807f606b10e88269f72a46abeaf3fad91addf3954b7200f9dc52cc9797c796a7bcf1ea966fa4aafd43117749407c5c5971f32d6bece57ba64b634df7a00c0b36440a80f579b24a2947d56340b08b299db67ba67d5f320e1fe01f765ee326249e15dada2a1899324ddb7e66857e0488bf84ef6629445fb8b08ff18687435e85e84c8be647807d794d55e00bfc5c409f094665f7573455af39d832203ea3bb5133e714cdfe468ccfb3f485d952c2537f174069460cf12ec5ad5d56c8e21b8aaab9cda8853e5d770062d9edc892879306d29fdd82ed56dadc6673ddaeea02fce378f2a179b9f65e3229dfd18486ad418b40461c807ed89aa53b43a02e87a4656faaf1d9563ebfa7d30b90af570ce099e0d77c71181f1f7c099ca45f0cafc007d83f8f882567123f68fa423223244a720ee90e556f03920cae31b19043f522223dcde6116a90e9300ffd5de0259c3e0d50c2b300232fcdbd569d590ef448925a4082a38e44633ba78a92fa5d183959c20bfc072942dd80f3ca1149f2687715daf30d2ebe77c7183d85cf10f701ec53dbcc9a829bc36126394a9b52f0181eaf58bff6ab00ffe0edcc0ddb73894778ce7dbdb91a8cc506956900ff3f810ae3867c781be515922f523e15fbbb8958ac80f859aa99f1753d05a8d28c37b5ad515edb1d250d4c80fe640f9608497829b2324f801753f4f59c4ff60a1c9004a2f461ec2d930591884f90439a084b568e731e13e07ce60909f4c594cd1388be4f103c6395569174f213c0fe70b5f77dce5942bfc59c5227f4aa9a67f19a6517805ee57b8e26e09ba4b0ff5bfba78ef53d7967b65901c50b3bb2c3e123ed1a0d3a18f3b033cdf37c5b11e04180ff8637a55c3ad62432a6f6f128a45e99b5f34d1de0166356cf02e28513558e4c993fa1c6fd127838f716a0f8e3da00d2ab114a75857eaa62fee2d29b16c1497780fdce28c7769e72b6c982d10e2453f436a85d005312603ff8b5659f7b10c5bb28ed5adbbaf0fecb594f612d4069ce9dd74d55d11e77d60baeb8df0f9d7f7bf69e151de03c3aa2476748a5424e11eb51196f3876bf01665a380009ffbabe7a8cc382e338575e647c9141f290d89d0f80f38d827346e4ec93fdd26711add00ffa1fb4987ba000fb2b1e2a12371d06736beb4ce7407bb956214b99f718b0be3d8c6753f86ed65b673d99ca0a591f677b4e00b8c099339f9b76d3f0d3ac2cac8882bef091a3c75a03e08296f97e683b4647772d19fe2523d21bb3fdd4e406db80f325cb8b5c661ceb3c6e36ee41efb9134503783700fb074d951624eb3353df536b02342796ea0cb43c4680ff71fed6e2fd7ac57b8cfa3ebe1b06decf1c432fbf0480955a16a8645c66e15b6d0705f1545617f3b2240a0212b53e0a973f91008579da0becdda91f29182237fd0af8fde8febb3211247b4cc217799738af53de6ea8b1009c9f528650c341260bb339f118d6d2a4a6ae05a3d280f82a641137c7c218cb63c8f0aa09ff0e8bb3f5a032207ebbde87787b59ba3147d6574b8697f9ce45217c03c0fe6827a6af1ff79e500bb15fba56bafc40850ebf1ab0bfd474f3803ec4862878e36f5297a86c64e6251000ce7f6f17896d3c9f4bdf5bbefc1054ef44a9e029ff00707eadf554cec7e47a7a477b7e7aa1f9a50c34a33bc3b78ed32fa7b94faff77b91f2bfa5ca6efca423d220aa01a06b933f6acb8af4b068a14ecc54e00e0da9309b00e85f8d5a2a6d196511c7d59cbef19107cf73c79f2f03f6f758cc210689c8a3a95bd061c533747a2ff8747200fed19448f899d2cd170b566cc3a34a47f23db48e4a5a802431904c914b8394899d00d15a168dd0b6d2bb2b80ccd754638f33cc959beb8f8d933d134507b387322fb01ca77f98c8af2fb8ce6427281e3de6989b9a5856f50690263a6343ba7b5fe859d06ec18da6c7e6680a1616b2809cd73dafc2fcc5a89ecb596cadb96f5a85dc4584ef5d07f8a78dcbddcab75fdf4dc09022874fde7039476008f0dfde5f936d374e1b667d2ad43593b951a6ac1d7a1720df5d7d601609eba30e8a9b75c14dd5145792e87caa00fbc3b0198b2443e9a5a36ba0c3e16ee35671582c20fe89719ee33ab8c41b7b69b364fdacfaeb9ca391bb80f3b5a7e6f27a8c85266681bc35445dc3cd8de17ebd5980df176872995e1f23c37dc3887cb2d7f68ce0ffa3ea1ae3f2ecc278764bcbb65bb6bdb08c6db55ccbb66ddbad966d7b59cb5c58b66bf9fdfa9ecfe7f7f4f4dcf73917feb84e5024101f8ed4575f33a1956657dbf510c258702c17c91a0081579346721a89cc26234750cc6c40d30fe2eaac3aa03f6e5efe966944736e7d8e8e02dd272611c2711b0ee03b3f63aecc55d6efbc3346441c1299214d3eca2b00fc9388ae33b76d57c557731f6cfa6b4e6dba59ec71a0bef8183544c171a58af2845c7dbe7b13010fa75c0df44f49a376a6c25cc2836a97daa1bb72bbb5e24f3ac0ddb91bbeb83ef9cfc9af0f2beab0d74575c7cd4359407c89d655b9712da425acc58060e7b77989e2c20e03f08f605deed2f86b748dde14e63d4bd2ac2166f939801fdc0a80b971c986a882455890b84ea9a84994df002e3ba4d97d8c3829b7e665db47e463ab9460af265018fbfffada7d0a0c4d3fb3f17ae80a412a260115a1843880dfabaa3a8b4e776b9bc05337792b7641647d224f06faeb21b5ba78cca049734285c1305d0e5dcfcac41f00be77d619dcf77d39818942bc14cf4f3859485ad807c82fef6f6af648eaa07a3dc5c717b1c8681303fed901fc8d3c0ccdeb454f749570f4e43bcf5e9cfedfb73040feecec7faaf1a7e6aad477367ef8b590c0d5f18d11a84f843e8888bff1fa9e728db37d2b43130c8c1c1007f433ab6d97f8dbfd86a42e513f09cb28bd36bee9dd010378cf6c46fc8b57faf06c762dec5e9c374875a0bf03f927f705e3383b99ba08b632fa57e8fb45539819fcc6ffafff2118c4c8f86a1299b2f3a22a6fe81d9dea2406c427123ba115ea67023b3cab9e2d6cbe8685b97501805fd43e7b6033fc7e1cd0ade8b6bcf04ff7a264c703885fdfafb64c105fc97a4fe533db2ba07d0fb3ba31008d622c7add95e250a667e0894aae6f83d3f6cf5d4d207f51656625647dce2c5e687f698f55ce1ce0784d03f055158edab4ebe7dbc6c5544d2ee85697ad5c5b5580df9043a137b67c843bcbed904c29b13060bb1e9b05f56814457d34f2271ea57130f782e526b67b7581801e2e8659e733f3a48b1fafc20d2b6a00267294b005c00f880f7cc13ad05b4b5a194abe46d12545cc5d2505a65f560d1c05542cc01fead975f972e4437073d8d003f8eba69f0de5a39e949870c2193185f0bf95ac994120ff96aaf8df06bb5137e7fcc596cd3dbb9c96aa3f07eaafc23d1d47b9f5c09c85270dffeeb443ce6f13acc0e777955829eaecbe8559ffb9ef14cc5611917dae2301eab7419bf4370ae4778f8db67fe713e25cdb08959effbfce955264a7bd416250907920880a839934a0680bf02779da92a438abfcfe3f5b5d09de84d27dc367e3007ef6ea2fe42dbb24ce2424547d1aa7a450f8e1bc13905fcbd237b3a6b597e1d8978533ca88481be7d61c017e53e033cfbf2abeb51449d6ee0b8170b27abe5c41001fb39acf530888f98b3f4866eeebb67ceaf4b46d0bd40f3577d2e406f94f558c3426e3f3a1e493534d96c7ff5f67ff98245a9319aaf4979433e89ca88caacf520088af424c28193f9136324b69bd2a6eef08e959f07001038fef71164b374430ace983a346e08fcbbfbfcc5e01bd68e66535bc1e2f2bc74ea6cc5ce7d517f69c7708ecff5fb74561ab4c5ebcad70d082f4140ecc7b299b6400fae78ace6e05115e07c86b1f718777d78ee6b1efb2815bc2650d3255589977857f99bca7fa2ade372118380be457e924c924a1dda3ed7adba55a4fd8659652d30580ff45c32f30a4a53289ac9f8df8db487ffac56c2411e8bfb803c2b8701cbbae2b907d2583f1047f1ad26c03fc5c7520b426e7267c825680fe7cf4849313b35806a0cfcaa482266efba7e70025b3089b1c05311df1a702a8df8ca364a183dbbe8db82ca22d99e541d74722f501f5435c3242f3be48fcb1bdd6c03c592166def3f92740c4ce5ca2384c0365d24ea67a1eace6a2943e9ac601c4772363ae93d041c71f818599f7788e7dd54b10c6407dd4a0226aa6bb67f4dc11ac5ce64514b84750ce010c3118e3c6892c33f55eb19ca5ce7d920de8d2299c05f01764688361ec942261b9f296cb0525139a982571403f80a021e7c5caa334c967c6c317e65e10da5c2102f00305d70cc31c660f2f1429adec11c3ef20887517017d596c54a77c7d96a6057233a99d652a11e56e1bc8ff5f31b51e279831e48921a89ff88ce33ab76ae303dfaf8cab187cb6d111398821f47bd0a46b304b3a14b8a17c705005eb08bfc1079d4f4a93f85e298101061ec8df9c5b4371e573815f90df2b6c79fd56eff1f4834f02f52b7f8e6e48f74ca79d76dee4e28feeb0a8b4d7febfbea83a212d1d40116155484ab87e3d55a6fa8f14c05ff40515c72eeb0ff4105b3d8322cc44a53ad5c580f9bc2b7165532492b026b73ad34dbc48249a0ecdf0a0fe6d7571d57cfc73c61f5f8e7d92a060c335647e80bfc82069d058a90ecd3e7468c8be0fb5b647fc6a0ee8c390b563b1f30ff4c68f3d791b88bda978860ca080fc0e31da3d6794973cd9a98d798ce095352ff5b72af3ffcb271da8ee63b578e7194675f368d581848e274740ffd391d0b04580a3b58c6c55d09012dd264790a10fe47f4142cfe20f74ec6552456364ffeed2ba24284300fdc1eae8abc99b7755a6efb0a5bb530505e6f0c7bf40fcf3aa28ea12711a56cb0bbe6de3971e9031756c07f02b486dc7dff13816913618c93248b766e2d175dd40ff7d28a32ef0f2e5902333fd8e829f3b796b848c0cd067c4df770e85db268e7b11f206260811df0f490c02e7fb98e05a79fd36adf43b59dc4d708f4cdb7ae7c66fe0fd920ccb8cfbf58adf3cfdb844c57c733c525703f4a14105943c77ae9c6577e289c5f44a5faef8297e14fe7fbde494647e3827bb6944a2c540f64ffed247a67c02203e8bb43d5a12775292a7e4c145177bde3136e802f9bf2ba54e791d45939eefbe6b6195ad616b771a1788ef46db23a9e2dd669ca75fd69772f9abcb83eece81e9c7ee09df9f02db7d57cb685c42040fa1040f76f91681fc53de9027279bc4f2dae6e187c0738814a48c0df08b93d6c615abb3926f22f6975ccd7a0657d2bd1e800928bb96e1e48dcd34dacefea6d8ac13bd98f8c804a8cfc42bdc6f940615e6aa602aa2855188da4cd7f001fcccff75805ec7ec604af0c136ac554a55cef46218783e2a785d9c0d6183083d93a2dc9cfadaa479b3f700fea58c1c7ac6defdfce7b35f3f8fe02a8ce9e0a6e7f2ffd7fb4e967bbdcefe662e188a65465711a7ca6abbfa03fa830fad505c35ad5b50e7c7b3d2f93f7a7e0b2501fd9307e9d328df9a766247f9b73f5336819dd8833200fe6b199518e93e69a886764ebda4f672f4c3670817e01fae74202a7d36622a020c5ddbba5d740fcc496e00feb48309691405624f2a84e12732a5e37e75f18f7780fe39f68a95af4fae7c0449406c949eb116ba782800d01f0edf17fdcca7672cde3fa6e8ebc7802a39e74803f4a97befc5d6f0bfaef264652ca6c78a406b363e5001fd4b2e3bed14525c0c6aebdc566ce87a5190cab439a0ffc8df444acd4ecf905b835d20917ed435477a6700e013645a7e142478ac35870bb67869c1e7161d96fc807e288323d8c36b76a5a8d6a7bd8b529e57dab9f701a8bf101b4b5227166dbf505d84fa2c782b6d315bbf02f1070663ce336a7ad9ad46d37da6b4a3bd1975a11b985eaf5c8ca7a1d86eb639fef77146d913c73b953201d8dfee0f34d4871f642b2c0e90e9388da1d827374880cf773318bd4c877b329b042d325a0be2e7d7ac3703f927d7d9467b034be7766ee247b6dae27702c7b05dc5ffafa793ea9f1dfbd73b5b70696fe16f7ed6579add05fe7fba2b6cd4b491b839f4b3843dfe410b1f61565d003f556f72b1a127875dc4fd993a2d19961ec6834f09f4bf1d2f42b2d72a37ba232adfd7b94798c8fe083c03f8ad206c91ce5782e6dfdb1af63570de92a22beda8c0fbd359804962f5434497c87a0b8a6e68da09246907fa133e0eba1f42fda4d34eca6a584e8869d999af8300bf9887ff55ee43bf977c5b4f85182565fc5ad5df39407f9d6cfac616209fa4c00a3b06ab1886485bc7ba08f0ab2d3073f6babffbdaadc44a3317a84cbd97f01a80f889432a2d53d699bd19e587470b95e867d09caa0e7cbe1eaaaa05c60432b5b63bf4c3fcdb42451dcc22803f38b95d411c0ab0b89de8649c8c4467971aa97a00f889a6a6dc51d197111e15d7beb279719966dc1107c00fc4c97fd5288aeee3e6c7131ba5a011344e909605d47794e507cbf7b2d40454b842899aaaf74e6995c340fdfa5e366e231e01416706dbc87d38640a1f975a18d83f104d860b3c0e4d5810d9059b83e407d898a64fb780bfa0f4d8f2289cb1c2e94eaf483f830b25dd4010a89fa25ff757973ea7655e1fa02819ff62f49a093403fa4fb37b173c0b16713819c2e1cb000ec532dc2e74803f6c95844af1b9e03dc4e6e034a942190a4171e607f4edb9042e1b94c8c2e33f67884e3a616adf89c0c700fe0e4814329c56af84d6bfe8044bf7fe0782a55d2f407cc751eea8539471b6e9e30846e1b4c58fdc7b7901a6b48bc289a93588ff2c51fd40e9d81b8c50a58bb9080c88401569748b2567b1645474db5e69ec955093ee00f8dfebac7af58691312571a7f7bf25fb7a923a46cf01fe0095bfecf3a7a668bdf24eacf5125a44392f2165805f91506079b1c27b1c90e4fa1aeb47c46ef5438a0e88af9de2423b50d07faadf695d0c571ad218264345027e42974a2bb7bfe3b5afa41f98c753a064692bf8cd819beac6d04ea87c9853a9b3ef7a7d64fe60ef27527f8a00f13b9d422c9e6c5d16ceeaf744ae6951bf54c981fd236e7f7dbc6c2a92ed5806ad2211ced0f02d0e15a82fab7f7568e4ff34cdfe6e349f9ce432e65b4c540d089c2c344e17427fddc4956c9a50c96ed0f7cc86e856fc7f3d25aad721f0c43ba4ebcad9ef6a234be8a9cc1288bf3aa895417c0548061619b35d8ac249c1bf364c81f88635e00af916d57369e8d51eb8b64dd361343dfc13d0d7d32c7460dc466dc8e53bb8618672f37ced2b05ea4b8a6b87845f591666215f67d19ef1d677137fc202f83b6e189485b0f39fc46dba9f243583e9b9495adb80fe0227258d6b644ee2b2edda90e098949f8cfe990dd027c7496f99df93f3d851538ed8b326f30569d08702f806aff7e1906053cf21e579f1bab156df0c4b5437803f6c1544e735ce7fd1e2fbb697a50aff371c6f5f1fc08f156a084d1d3a9c11c8ab935906f2257d47209301fd63ad84e8eea039a2e662b7f4ad4a8c9afaa3912cd0ffb13dd086f084853240d065790f95b804c63d0701f11b1567398278a283093595ec9cd75766fb1e2115e037ca82f6c693a08a575e3069045d9bda4d27d01d81fec183ceec6f4c296744841cea78866e09c216db0fe09286e4e4c38fc1493244cffa5999e3694cecfe99c9407c60308fde2ce1ac999b67e0af4055f063dd8de305f441e4e52497bf14d00a1ebe5a59c0ebfb4743bbb302e70b39c295e2c7c6c557a8c22e410894b77a614e43a03e3ad0b8f8cd9aab9d9d1842769bdf1cfb2cfb0d0a38bfe7ba9fa80444714baf074dfd8cca97eab48ae701fc4692776399ea85af4e8760545cddfe3456dc6218e07f4f3b6ee63f2d1938d7bf725592628a92870ef200f8c6667b81440b1bd7887dae21bbc47bfad2f34e49a0fe12e38fe651bd36bdbf5195892c7c1c33bdecc002ea3f7561a711591fad2a948e7418d41fde2503fd92007f31a18d9d348564c34fbfd5c1c42e70f56ac2fd0dd0d7bf71a305ad32dfaac712cc35dc31bd57159378b801f42316112e2cf24698ef4d43634f7d5f7d857c1842ffbf8ed9ea23219274f756682a5bf3b52dd1d6258a16d0bf7278f44123bd21223b8c658c6733d42d72529703fa92405f82ec5dd63d08f7b95f786b5e746e5fa93481efff50df64e9045534f7ef7c59b59da02ee19c5517e0579e0eb62cdb9986cdc4288a377dc40c995a4afe02f1ddea9848ccfa42302b8fe0e3bb5a1b1ede4fd786803ea3b6fea1a70c772ebf53cced130e541c1f527f15107fbb9e95255853aa3ba908f51070a3ff7eb4874500f8eb2f7eb9b97aae0656bfda3ff521e9a143b3bcff0ebc5f35f390ec0fb79a263e88539752327e482fefa781fe7ddb00e6ecee3c0e822671f5c95d2c40d8cc751de03f2aff95a3eacbb03f7a50c2c47195998ea9440802fcaf98aba74d48b0506e571ca456d5ca3799e37f4600be5d43b3e31cd270e330aea3ff51837e08a19eac1ed06fa3359d2b48696154ccb13211f84b2cc2e7393803efef0056e541288cc57863f24bad244c9423b3fe3b005f69ed68d430b1b120449f5ff8317f01a5206a3505e047d342988be9ac038594abfaba3f3a198486645800fd70c41d57093e536fb5eb627d91eb99111f824133b07f8fea9b8b7ed311eb2685be79d9b5ca50cd223b00fa8747a126f3618a158d6ea1907cdd669bca854838e012930c876f398d0748f3270718e676c1332161cb2fc0fb319fe9a832e8c267f9a340c08114bb5c468a46e50cac6b670bed1659c25b0bea25a33cc0f248f856e103f5ed2cc7ee33e34a92f5eac578c3b03c72bdf647405f06517f7e80f8d1d12b9c9272ed7728bf376cef2bf0ff29846608db98118422d2ec77400d327f3e1e6b06f403f38bad8ef707894441ff8a8d9fa5256f72df8401fd828fe8ba95f85774f594985b0fbb2452c9cd0571403f8db399f7b1ac3427f1f8bb93d3e6d2166bc5f9e96700fffc11616723d68de22af5ef5a756b71d88e430dd08761484538e37ae2303d2619fdde9a11ffad908b0a7c5e69d8120655599578dfcab6337506ca91ddb61918a0dc94332fe717a0879670d7cde0bafc143ec9e607f89b318e5ff17973213f58b0c265bca2935e8ab72b00f802bb6d4444a69e191466367efd4729f4d0d5b23d203e644b19f5861904f4da9e5066fac3f55e667fa104f25f20a1a144fa1076f690f8f7bc9e81f4625cbf1580ffa78531b84b56d0d9be0f7aebf1c6672066b0e806fc5f3ab6485f547bbbf3aa2edffb7a0638a7f4b22b01f880f0d73226f1d4800eb81325fa0b27ecfdf82d0ea03e1ccfb6a3b85d10d02074747068db4bdb84b94404f8cb28fc4c2fd188c7ac6f353e9a6247854d8f2d05c0fe44b5a2d083b13b5d19b17015282bcb23234dc904f81f5e3fd48b010ae91f2c3f75db1fa1213bd3b1c581dbc7ca4ad36625d46d1c53bf9f8e65ec5d782aaac202f84564b859329d5bd106445d66e792839d93b202177093b02abf15cbe1fb94fac3ef970261e656dda7d57b007fd58e579679bd73e43ba6d25c7a5062be97719d03e063d5ba7e569b2e5e3bd5642dddf62dc29026b4a03f5169af8be16ac9c9b828978df0e5b2e9ba72e717d01ff12465e62f08acf70d8439745cc3a17b50ad8a01fc43024fbc44f53554507eeada9bfdfcfbc7fd5643805f5d81dfe8972bf32d0d9def29db1cc7f529294f00e2d314411387547ca52a81571a0baf86740895ee2dc02f34c5f7391e7c0f98e728de7f8e532339413dde06ceff678ecfe50c0115b411e5111c46d3bc7ef98c4480bfbac32c8e46f915da58d04b99454b96927411d20ed85f9f8ef5f7d8cc4634967e77754328abd9fd0c8404de0f0bd66313f5077aa8f131b2fee967c8aff503f8007eae77e24a4ef479979f5d38e28b700f9e05cb1acc35a0aff464aee0b7bbb0b5ae620f3a8f14944c562502fa7f2a2bc6bc40dc3104f4f035518df985788ff229e0ef2f59cbdaeef393a5aa50d3c53b9e8a696d287000f967fc0ad3f47adb44678df7e327ae90073c11ed56e07c1241bf0b2bbdc3e5bd6908b0d55626486aa4f607f4bf84020a9893f4dcbb7021743f31083850add9e2017e8422c177e9cada2dd3ff2705961dacd2e8c1181da06fc67ceecb269e3fc3b4bf250f621a7a654b0dbd05f0837ce4a3855f6bd4de2e5f8ced31ddb58f5b6b0a41fcb09fc5b5b889ac2119fd0f7f92f12f04bc0e59203f0efb41b142228f9c71ad8a0f7442deec9dfde907fc65f322ed6c15ec6f0b964a2a616bafffd8bb6705017e5247b5c0c71173c3e464f937c6cf3401b784943e607f10b5c2e554711fd19509c373d99615f16e134e03f5e7cc3e8da78eff929e90cd64a0226ae5af43b465c07f2031aee25dd345fd10facda7dc6ec039a47b9e10c03fe9b2fe7cb9e7798c2f3bd6f18fb81a1c36a03802fc991b2efae392661fb085a5b6a2b46907263efd4b05065ca3264b0ca963536cb0ca1abe237fd5b6a5eb8b01ce67a2eae8bbc2353ac9213812aac568dd7515928fc0fe999dc9e1ac7cb3616723e3c7b5fc10e19df2e52f807f677d8f1220ad9fbb101b5524d21e0f21e2182b01f083ab19eebfd707795f823149af3044705dbc863b01fc41d94a4182ba96a3758dd613a231127210aec21a9897d2ab79d2a63b8da1e1587ce773c599fd279a911ee05fbe2ea0bca3a13879e83acc41998456e8ae7e7703bc9f80959c06ed0876de666f7ff4a5d71c4459b55520bfec9f1b7f7a2780f9c1f568c17e6fdc1972b3ef17808fa1985104b65db238a40ee1e7f47e89e27e2f830ee807713b0e2f6e7838d57fed5a05b69222cda5a1c900fe34cb6e69ced6fcd6c1ca07ab6a416f1e8ba63e3c600054e8da3fb361de504a7c22ad6ce5aa8903aa305aa0be189a61ba534aff345ebd387af7bea31945915f1b88cfea4571cfc8316297064f92ab6cb98f013e1fb8007fd05034e43849babc5464026c220d22bc0837013ea09f79d3f6469b671b0e09a85d155c2c87cdefade202fce9770e4c2f96f18eaf95d5c1333613f90bed45fc803e80bd79c5f6e5ed014fcce7a9c0b04ad981a5db0be80f62d0572b392bf7259ba04bd6b1ce86f48255e481f84bbafbdac6869abd79d141d38c24eaffbbf4873ad09f4c84859572b999d55eb8f73bcc6d9b54e753b0fb02fbbfa76c24e89148e2310bddad70e5b642234301d01f0e084fba6e678fb23e9941969d15fa0af420af01efafef37839ef38784dac0fa7e972c44d8f26eee0860181379f5044940e62d5d950a256966b01566e53d15c04ffdc8fa0e413193a452180145db454bfbe743b40180cf09e816194ca71e7a194e9e7484bf89483ee5d501fdef2fc2787a16d8b5e0ae0b6deab841b98c840413805feb863f40176839530e20fb473cbbf045d15f3d19c88fd54ad1c9d5df8a8b60b2af7ebe797b92fdfe840ed45fb9c7944ed95285d75aa82bbeb123d0aed9c1af003fc5fd2dcb834511cef2b1c66dcc8661b0cbdf211c989fa04e584c000397d8ebb271cf7c51bf1d88287e0ae46f725d5e9d10723ab39258627373e8352443914e15607fa72af7e9ef53d7603ee5633f3d6ad64db71e03faeb289c1eed7731ba725aa1347ecae675988a41e280be484aeb78800fcbe74467afd9daaf6abcd2e2a20cd0bff162be835d1a09bd86b0d6a06c26eb53f764e0042ec852e4768d1220ae3f88ecb6f06a173042466cd702e61b98d3063c0b8bea84b73650e2f125360f31957302cff7b7074296a22aee394be904094f071e7183cc35a0bfd745d1aa0f0e1140b9c2daed26daf06eda7d7507fa87dd1c11df381377afc66fbe0b490a7d1df4a54800be951de55bb171b8cfae7649ceeb71e54e24d31108e81f6aff2da4b4e8078ec77b6af1a6b12474f8d76b00e763e3e09b5c26d2c977d22d7d933e31e7b453027e203e9ba19a35cd2cf1439a334835449fda342a708a00f8b0cde3afcdee0f96392e49b736cb6fb14df6bbd6803e98bb3b502974e7bcbe8360bb255adbf89460e41ef0affe29b56c836b7d8e835682e6c0db1d747e5f950ef85f9ef1c650cc0e2656b17fbd44feb428d05e62cf04e2e337d53bf965ba7484cef56dac509d165f827fa5c0fe4aa57e4ff1eca2622840e9ddbc271b98f6f3f6cc05e087bce19fba3e6f953e5e9ff006a3413f4a331802fd252e5a4f29bceb725f8884de075aa7b2498edf53a980fefa7ac098c3cbc984c7cff727e39ebb4e10542a50df4660bd7c49aaa2fa6480b363a8b8699757588403e45f5dc255a829394406e37f2c7afe4c1783b6767df6803f8761ab5e6168830702930922e59bc003960b0a501fa25b32a92bcb538e9eff9c7a29d0331ba1b9c402fbdbcdd46833e1f5cd5fbb2e8b7eea5c8b1a421b803e09693507aacbc2ca2ec23260627912959bfd681da86f38e053456f1b3f7a40c4c7fc42549e848b3aeb6b02ed0b3666afa20ecb786aefee301e634576bdb200fd4f922681c8591aed89c1bfab2b084f4a66b13d2c603e50105a094d03bca158317b7e0e49e054741fe40cd01f11ffb5a2aefcf4059b40e8b7f1ce095946ae9602501f591d2cb44e97bc630bbf52ac4284f786ec2d5d02fa77ed6dda05a521df025bf3f07043416125d8a808203f3bd2347010a11957b3f67f3ef0a0589656218101eac3e92826e8c7f9b22f495c11667f155b344d28bb017dc6e1161ff1b2a271b253f253c6dc6daea5120f1480cffb10329dd0304eac38ca64fd1c2c7ec0a3339401fc6101684457082df40876864f68686bab8fd3d211407ca9ec0ac24668be740fb650666a831ffa16d604f2a3ff2695e382b7b2967f69fd6d3f0dd9e49eb18e04f0497f18a42362f36b2a6e1d4ff374ab747df4d332c01f2d78fb341e2b9430a83d8a2bf6ab380c0e8fe00db847d8ffcaaf84ff3ca34ab47439c9ba5738a483e71ac017b05ef73314f557e0895bf671b8a71e2b634d02007cac3557efa5f66cc36dc5fe308eee4ab3ecd236aeeeffeb8cd4e2f07fca14d1dc5d427f777faddb0e5aa705f8d19caa4606747ba53c27beb8feadd8be0b8746f04ee6586bd7e10e59a642e53c5a339f1ab5430cd51b60407b6cf7ca813ef608055519f17192bd3dc1c57820e05f7c3b3485bd58844424b16fc1b71be7df4f9f3a05ce6fbb4071c805843e2ab79a6aab1f878eaf491d3e80efbe8f57b7ac88fccbaa5f90bcd927fd1b4b153f15c0175374e238320fa01d0f1a046a259c610e8406e581fdd16e8086a0ff39c3ee7789aedf3fb855a2a8b770203eec28415eb57df6d95618bd7e15e0bbab94d42589ffffba61494d8f4b6790d6175fdf90ccbfc27cf797ab003f2af022921d7ec46b1e9cad32e6849620b127a905f0634711d05d796b26ff72e5261f844ab4addcffb64f00fe8316462fe438fd006a79b12502520d1d1cef3b40fff6362643c21eb2a63cfeeddb2879d3b75e13c71d20fe50441b3cd89b262157965351f2ab26c9c079a180f894ba4c01966d4302b491ad3289a0727245342e50ff2c4e1a5d43be2ff60fdd436843a3d098d3eb1402f029372ef8e5c0d3b2d91f2abc90c109f03ae61f16007f1a8da167bc67cc877f1f4c04f21c684e9f5d33ba007d1a214d9ef8b043338c0b0c5340d2705b79ed381990ff3117606c933bf5556bdef9c590f527ac14691d03fa054ecdf866abd1dacb91ee337c53fb80664c856f00bea4c680960187f641d334b1abe1fda9c047bec833e0fc0f4b116e9904baa94bf58de0a799a90ef4d18b03fc87c1fdaf2592e16b869d8ebdeeb045062fc58365c0df767a5b0c53a283aefa76404ffd7373e5896fda08e0efbe7689055f2318d964f9664c5eedcb4e0d6a3401fe505353e6be18e2789d470361e21043d4fdd9ac6be0fb210b69d8e58cb8c2f351aacb253fc2570eed4502fa001e9de06b5c5af6802edd967e9335fe3d0f495c809ffed23fd6bdb558a34c47761a64569f0ae1007f0fe00f8eb42770fa137081c10f2e355d3fada909fd4c007fe86f2fd523be7705079f368d34968f14b70ecf12007ce167387f8965871451d21f9fb5b7d68b9c574b1800bfa6081ab3784140e2bfce4b648584d19a9a2fa901fc9b41a17a4bd644ed956884c6f55f05d7041c137b80f36339fee1a1e07347fe215cfb028cdbef3b5a816100bffe26127eb7c7e231a6a34c9d59285bbc99a48b05e017cec1d2d90bcfe4b4ef53583f0a6d6e684944f303f3454e5fe7fc2f12d3b818b11ae71f3757a163a32901fc2051534530b8a94e85e78734ab5e76364a773814301f4dbaccaf6abd31f7e7d4a7faac66ec976247fb4b801f67942e5848175e7aff2efc54ce956293d217fe18c0bf1aae4c6a0b7e207b7d0bbbafdfcb40ce699ebb00f05bcbddc71582cad79dc198729f48de62f4fc7b3c40ffc723e2a87ee8783ce9fb92639ba3ed9b2c54d100c46f38b4335ebb4f8af0366fddfede657abfde191102f80cd1434e2e157fb4d54c5a1d6232abf7641be90a501f66f550e58b1c31b1399af906fa5a53674e8f4102fbc3d95090b678d6f08f8ae85e3c9b4e99f1e29b00387f4eed70bca39fe7844581a761a83eb1892ec01ce02fbc574c0f3be477caf04745d87a7f0ba690e2df00f50da284612fa5419c9a4e7eb6cf65974e3117293180ff72cdfbce9830ab55d2b2bd0ae54cfa16a3e1dc02faf0e17c2bfe37a61842020a88cafda96481fcbc64a03f66f38c25839785d0b0d679d331f568e04174cd06fc0ddc777e5142758e056aa3e62ac39431991d6b5d003f74ffb3b3757bb2cbefe893c62a4bd1502e63dd1d30bff2ee44d4cc9d51e720434ef5020fee6ab54df811f4afcdcaaa47e29150a80bbaabc4ee6cce95f58501fc028ba1ab3cce76997cba3087a63da9a064efd047e0f998fa8d69aadfc315505c7c6d6bdac26779a1c106e283dfd5b9a22d8735f386b6c8967e06546e519808e02f984fe6cc29df684b145556b53c1ea4d3be523204fac749e565553299c39abda1e0f3e9422ed1d46d2240bf99339c592036bef1234eedd3af2acf0b8e32440a40ff35acde6aad3c6d1cbef4d410b9232a41d34f7f00e097ffba0cba1f537bc9b52f7eabc96fb4e655db50019fcf5460b8c418f4d7e8fd05f975efa6b5d061f515e0e7ad5526bd702fb6856f43354f5d262fe12dd4b200fd8abeb14cdb928ab93b469e42217d72d1af38466d203e09f07e53ef4f5673765489be86acb9a278e84702f803fac02e46cbd95988eade096117d60a166d1901c0bfaef8e7df70f1deeb9779d1e05cce7f99ac8e074cc0fc8a82cdda598801bf077d4f5ba32a73e900f6674c805fcec9a01b465c0c4df344e62432b499dba085b7073eefef431342f207efb8eec4446ecbdd66809b980fb800d3c8c2193990b00f7aeab4542adafafef73f1c6da0fe71890875138619df861a8542ecbd930877c78106ea2f0e3477f11b39fa51915959128148bd63e6a81680fffbd3f9bb4b3c8d6c1a12661351dacc670c4eaa06e8ef912bc85b877cc861d2d24798595b25be1cd02200cf6f33c9f94af9cb846b31c62afa5805c6b706933300bf196ac0f1f5aefc577781d92d9b9b7df1299adb1ee8efc8e8a5b0bef20c59565f2b35feea561060667007f42fc2134ac279c2eb508c454df00eb166df05bef003fcc84200ebf55b117eb924dab4d7dea894a7b15433501f477c160d8dab9446f65e64b06bb554d508b55607e2bf674b72417fa2b337770649af495cbe389fe7293d90dfb0871cbdbcc529968273679e8b56a564bda580fcbe0ccfc48ac0694fcbbc236d9fe27d4eebd8a403e06b18fed149aee150f2ff727fa03e7ef458ab675b02fc7b4d6e7bf4e27ac92650e6f03ebb7c027a2f2f02803e87c25c87f8ba37e9dd93299ba36ec33d95848e5203d01f3e0c4ea5ba5b4018b0b5fcee7575d5aae72404f8ef6b2dabe99aee645a42c80a0aef8c0439ad4e51409fecae1de3ca3c3364e7eeff72432b107916709b04d4970c357ae5f199977b48a5d4dc3b144603d3ac4b40ff8ff57b7c1873e0e3b68ffd5cce1bdad3cadf1367003f16fc58abb568d78e5fbf7bd8b21e6345b91bc700d42f7bfc0afcb546d3f43c8ad0e1660d62b2583482c0fc86389b8a2221f88fc4f457cf2bcc8fea07c913d9c0fe0a7b1c791da65b20e0375f1df71f9a599c4de303fe3ed6a74b1c06aa6e2dc7e60f62978b7c262a7e7f81fece44a66edde8effe283f7411dff41bb4d057bb6100dfd663fe06d1526eafbff7ebf5b3c97dd1001b662a30bf29bb8027faf6ef5fd71adfc9e5777f8be748bc8280f97a6c0c5d3ffde55565be7e657b253cb25059319805fc6d2a699ff93b1adab3dc0b2543d21ceb75e6db7980fe371856b37eed7af463ad1fd53ded0b0729b6014432a08f85e98f3c15975dc965f82ee9306970ca965501e8a7e2c883c77165360dde5093f0c712b01dc46bdc81fabb90f00a03a3ed4e6ac155c4159dbbb8d77a4300a89f2fb80551afa48b55630226b1218e21a4f3b857017dd93ec72d5a6c1dcb294f9bdecbc5097b9e04371ce06f85f44edcde7fbe5cfe14dc763e999a1f13cf5c0ef4bf949cb9577ca489dfd9a9c260d7ad26de5f9ba33202f8c2c710ac447bb83aaf56c3a5c656956adbd475c01fa368f6845444e049264fef17a54befea3e99c903e0bf5f94042a558d890b46be4a058c9d7efde1755306e0f3fdc3717f3074ab3d1144f3e3bbe7e3d34c3e8f00f1c16605538d92f7e6219e561442514af3b12a6a0bc0c75fa51043961fb890ee0664e370463f1afbc78d03fe38cba10af35f317ceff14e3412c6a993e0f8b46481f8ff2eda257e626b026641d5b362fe3bad4b582e3f501f6ee228d1c67c7a9594a5ded03e6f89e9e6461207ea2b191cd92cc96a05f8be24375f03bb27174a2359c0df0c7d4fcd5ac6f912f6a128549acc69414cd15a00a81fce6c8ca75edbd4a944f2e7e7ac45fc2a292d82017e660e36e59058868b46f9057d1d95b2f67cfe5731903f140726eacaef545f319e341d9eac370bf9053100fc57f2c5347691eea116a98346f17e8addd1194511a8afd74bafb3c27f669d07cc04deb3beef475d72cb06f2a7511c43b0d0971cd22f84c45f6bf73dae0294c800fd3ebbb6a80ec777b5e7ca3f4ee857fd950d781607003f1b3e03a5e26c9e30fedbc6ec3a7ec555778f1207f0ff96f650a810f162baab3cdacb7c93b831df3b8901f4771cfc1e30a9d2d7475e0e3d6f8e0153a8b98939003f85297446f2c064ff4819f18211b50339c39bc60ae0677ba28a6edf54612a1b92f2f2027e6e8d0c94a601fc7725e5fb8bd43a9dd9020b5fee01f3a0ea1b2858203e2b34ef3a151d555b582b3d3653e04746c1dd5703b7cbc6b1a3ff334be1de12a277cf3486371b5bc12c00e2838d3fa61627d6fa2fdeedfb94f881201577221420fee960bd8ba93c5c21e7e5fd2264254cc6ec381802f873b66d0a11d55119c4c45f229e1f08850bd5b8f900ff11b941e5e7367da86f8b4259e26bdf6420ef64c2007cfb467a05a9b1663c1bb6ff7ba18802e2ec7bc4ef407cb271dfc178e80e5f37dca1823959fefb9998f20ba01f45a5bc43a11a8cce70862caef933c73244937e0ae80beb6c2d7107bfc355cce13c70e6e45976ed337b00fa58e29030abcd04cfd8788f573445519359e99563a03eca41e64281d9a34514faa0b9da34ac5f31aefe11e87fde66977c0ae482f1cf9c1909c69918dcbaac41fc32e8f187ee3a2984f69b8e8ee5d8dcd33a866d3de07f64f6a2697049e0ce7d661c7bc7484440f0ec9f0becef8764d1a0fd9e32659b75124d23915ab8e48d4e203f6495343c1ac5d8e814f5c9f60be04213bcd71603f8c95d35b8f1d9f265de07b2073c34d662e29f0ad7c07cf6abf72d8724bd563b1abea31ac3c8a28fba101e40ffa3e8cacfa7d6402c935a267bb894badc406c7a0ec4a79a6723658c65e6cdb6755cf534ace1c0aa692360fff8747efee1a49b73bab5658a751fd9c0542b410ee043c8ab65028a66388208692bacd96b2d5c8128ab00bf46fb8b5913a628505b2ce5f44b68f9bdb8b7c16f80bf94f597bcfd598720fe6ded529ec861fcc35af73ce0ff297a6b4dd0edf3a681c42fc9e96a778cdd1c8f05f8c72bc30a0bfbe209c4af22f5738b8b8dbf146b8e80fdfdf51302f66c82cecca81ff75956f1cd148a3396c6ffd77d8ba04ba3154e7f59b006c6303f0a417540cc03fab1176b0d44d91657a6260cab1477c984c6ef684a00be419c1cf9f76fbf747ffb4ee7cab8030aaa165101c0bf1d70d5a658207ede9d35cca1b930ba95204a6f04f8f912a70348d87a4ee12bcfbc735b88d3a357a64f407c4517bc8e4178e060e16a193353b970a66af92206cc5f32d93afe147e1ebe9a5c732617d11b50074f3b09c4076cfe9e9aabdde8c7475927e7ca3c9d7dbb02a400b07e18628a61cad8e709459ed0155edca4fd230dd4a7d1cdb2a37f248f3c957220f0fa2e56d4d50ffd81ef6fed0c675d87f438cb717048799e0ab54dff800ce01bfa9fcb09760a8c331c72561a3599a5eab3989b80f37b1739d1e5a0ed12843a2b79d50c2df2cd5c7301c01777d0e4b5bf961bc8b32fccaaccb7c2b46d969301fd27fe7761c8bb8feed8efae67682c6e821102580d007cfeb9b726bda1a606f5759e4aaee4c3967a2ff423303f6a68f8d5c91aa35ef5292d9a842b6afa6b046f0810bfd8a528e6a068ae793d52e0bd734da47c910d7e00feafaf74c45567a8ed01d126753f512ee0fa889503017c5abce73cd5e7e38cb1fdcf297ee62dd695bf860a007f443c8a7a3fa95dc31dc417873496904d22e73a09e0a3f2ff847e2f8ba1058da7375ab8b7ef7a0c21ae01e75f0efab3df7b7c7a09f729a2087bffebe2cfb476c0f930896cec4ab7fc585a50d1bf2a8c87e1a58c1c0af45f849e92de42468b5b5631a48295d56b4a2565ca407e350ad78c3e91a0b6e236fa4254a1b3a4f3b06b0ff88fa0cba2cb59327eddb63032a42bf38ebb3f7e959500dfef76c2d4447f78d37af4e081cecbe3797026501f286ec250eadbb3760dcf1e91c6906bf420942402f991ff8c70342789dd10435df56854a8d8f7a12c02c0d7235d9878c5716ae3b20a46a3ef23ed02656f2f01fe86b799e7777096f1777b61bed8b553eebe346a79a0fe4f0c2d314f3d1dfd808f8aa3fbc7f30b2217c315b0ffd8eb1371d9b848740448bf7b3767f2198c20a701cf7f57f4ac332c34956123de919d4190f8e937ad3fc03f73ec2377ce92f3498ffc4e539fb3c1914f724f07f007e53ec44a61e428f5f6d3425955d70e653a1215007fac0dc91863f3afa4bbc6c57754f6a81f3c644d02f21b3f49db793c760d22ef12f288729cd1624ae40af0fb4e031f070fee951f338c9063b2698437b8382d01fea5136f98104a8db35dea5e9ee26664307684dd0fd06f99351e476291a74c643c9d0d2e88c06aced06000cf57a08d9e2224204b7cfee67dabf6ec0a635f541d804f99485087145c8b390920274e7da6f2690b9c2a00fc0bb398e64165cbb4e43ec4a68291eff7b398f0f281f94454741096af13de1a7d4ab07ccf0a226a371367c0f7872e50b7487d93d768c00a49a71e453ebce0e700fa0b0f7233484d182db2d7a990892c97c2a2e40342603e406bf87c16917bb0c54e18d76ee9d3b4569de03cf0fce792d51ad1836dd8290de68339affae3f9675a80f92f7ceba5f884995dcc6359cb847a75f49c1fb52280fae8504254d1ad2a0da20b1963a15987299196011fe8af7e653549eaf3d91d490989d07c3e21e39fbfb907ead3b513bad715e22bba92fd182544c4f61ae49e79009f9d50320c84cedd3cc3b3b69afa1ef61333353b16a84fd20946cfbc6ed11192c7e6978f4c9545d1930e80f85c176600258496d412ff21cc6a69898ed3c80c12b85fa7af0c6214397ef578afdb83cdf953c23cc77905c0ef994c1f887fffb87614431d957f8ec3336197f609987f18ed652f1ee0ae5c2f38e9b4a0dc067d223809d0b3107a58352f66cdf17f1c2eed5a669dbbc3f7b174007fa92ea92be24b7e63339e249f8623f42c6bf2482d305fc6516f29ff6e5331ddc7912d334ddec7f5592709b8ffe853e29ec033760317e9c0394b9d9537ce8a951dc01f782edf8afff0951f4ee5d177a3843280618c1f04faa3a5fd3bc365d6f50cf7350f0c7a7ff3aecbfd6700dfc42bec3a627630f6b5188abea7a265f7a0702001f41becf999192e7031d6b1123a05f4f36c77d34ca7c0fd090c1178cab0252c070e02d3430c279c1c1ea479c0f3872f6c54c26bfc11e3aa56998def9d881d98470ce8375a76dd6ceae1775979522f067ad742a06bca0780ffaf557eb0e28e06ee47fe28de6a23290e45bcf236505f5aa1dc92c30a43b82773f0d3e83328be69452302fce00e895dded9412ae19f6e9cca1ff691288d7345407ca0f270ed91ffd18840c81adf538fae21f327f4049cff4d91f890d3c24249d085fd450bbd68f4454817d0d7fcece8d809e58a41254c3290c5fd8763bc63220ec4071e721768136bc34e0f4777cc087259b9a58815e016bdb071f18c570dc2e79e37d354e7b9ba12541805a0be58dab386fe600945da6ee2c0e88f4d25b2135d05cc9750b32df74e17b92655883cf4aafe86019d7cd705e007beb14abff4b9d30742f5482905ed975e1a900b81fe30b7325fd2cdfe83b1f57a9bf5e72039ee56793e20bf71c3bfa5f2c77c0ed6a27019f43ef1e69a6d5b07ea17d7cf58e94e734955642472b9d3a2be4b658375c0fc839094b34bd9de71ef2998a8f7687ffa3a76a3a480f9becb3b091dbdb20223131032fa1af4d6459fbd60017c93bc7d5c2c0e6616d704ca59bf8bfce6aa3d0d12c07fa94d10fbb29addf03362b338593719fc8cd402017c1d8f18ea5c79a6bef89ca3a0d57f27d6775f9c07889f52116f6a9f488a6e980bb103ef08734ad23efb01f50b1db2b00e5968a8da54e89e8826f4178abcd12600ff7f9430717b7c16bd608d9326a53d316a2b449b00f889f81eb6d463796eeec37a7963d6b9bbcc1bf801409fe025109e80578380a5cc82e1a21e79d24dac5f02e8233ca8e22c1cac2321ffbde7d339d4f34925809d7103f0f10fd370e16490f2246def151646873b5f110680e7d78171f4e65d5b9ddac99a9b74eeccdfc9715a07f80775ea1ae5079dfc224fbe994a292875a03191ce00f91df90b09ebc2be6c48cd78b8df088d1e169f6234309f748b99c48d217cd1e3d39293bc338254dcf56808809fa51c3477f016179b318a7366fda87cf71145fa03e0df3cd64c1d2460890e648cb84c8909a59aef525207f409ac353c8b45a52a70afd0f4037ffea53e39f7c100979c7ffa97d093b55af3b183bc70d451d90fe294a30ef0c7f56241d3d8e7a2fca8f9dac1a8d975858f4b1100f8ab2fa0c65f695a4d66c54748756e044d4dafb10880fab31bb567f085c4cebfacabdfb9e159952caef3179000688b090cb50f262d77cb1572a23d9b855cbf1502fc2782589c0201f4f4b93fa4b27f5e7adb8dca1b2230df76a6053eae5de59d1b51b6e038c9540352d640150680af1199b9bffcdb81d641e125da40f5d57f352d00f839df87e5a8db1b291bb64b4c9b7425d2cd94a03ec0bff9c6ff99eef063d6885f7b5237c53192e8ea3114503fe38cffe6c0253692106f716c8b86cc42158f65f404f40df9fa3a11a9d5776e0a7b7f09aaae386ad17781feeee6969bb334128621f6d555d29a97f7374d02787f20974f3de9b7003ce9919494a958824fb9011d84c005b7b2529eecc5517d565d47dca5ceb365e3bab937c07c393f24be3721ec345c9ac91eac63e8a1f88c580620bf75c51cd0daca4e16703d3765225e32af8f1adc01fcab2dde054fa9fcb5b14a7ccc2213d369266a9b2c509f6b67c3b592bd6b172cf0ff6cec5d7d21653c4b0ce007938ccf4e04888e8d26de41c63f6b4d1ad0c91181f90cbd2cde4e7b50152b9f6225de2755d237328ef001fd179b330d54e5a45c95463fb9e80e55b2d833f62b80df40cc6cf31c094f071f69543990ca9613fb478d01fcaa07aa4a2e83fee09b8d66095d3e149dd8849212a0ef0ff4ce5d8af479f79cd9a28b282d1f07d1fca313e86ff61c092f35e4a66360544d5bb897e7992a06c0eb68312def71a4183f224eb1def69b96cda1a0955300fabba0993b0dc398630e5591f48d8ee847a8b8931a00df80fe63556e233d8b32b9eaacf2dc999e8b085703e023bf14dcea315d68e7d1730dc8054f4d935cadce00fd5d65ba7ddbd78e8a6c11158544ee146a84cef15b805f4c80bf2bf9255437c10b79a85f3f443fed5e810df80b128d427a3bad258ba3a3e1bb274fc6e004fe5603facd589883e4402bd37e18694717cadd4f54943103fc80fe319889b768fa6b71d298f99c680614d74e713ef07eda1059537039087e93d6a2be5ffbf72051ad8e0e9c7f34e6333c893ebf3d5e0b13087cb1683c4df13a407fa8d9d586d2d97fdd76cfbd9e9599c18f27315f0ce01bebee8e375fae44feda46fb6aefb00c20e784b503f72710152d5094941b4198fdc5c5652121eccb642b01fce3494e4ccf1d9b7165f74687249616b78c426ebe40ff67577076a5277b3cdfebe9212cc4ba14ef3c3b05d4b78c87aceff8f8b068ce083f424b20d7246b944603fe24223191a7538438b132dff1a6e9069398e3da13a07e9b911e57ab9537ebb198b9ad9cd8677c8fb46505f87b92e8e7a03214880f3ae6f345a61feed564525e00fd1771a7fae70cc904b2cef96d75048349b377a527c0fe36e85ef99791edd0a133705ef659dfd1da923b01d0a773a30947f11c1a4bad39f217929fa1157a1daa02cf9f46a1fdbb573519eaa407d22095d3c5b13d953d101f243ec0a04c6ebb9123e9d02631a5c370d019ef02f1e13d8da38f5b930bee1fa1e674b9c80265ad6a4360be88e4f8f5f7e4eb36db40e299a1dca977b81c699540fdef0f51675cda7d96be883897b1cd9b21379572cb03f03f0f4d300467092fe92eab48197322d0446aa07e77cbdab3569bdf24516d31e366edc557f6555010d0efacb5dd2de32665e38abbf979dfc2f4ffe5f23a01fc3b7ea67e88f18d5343df13887ec40830eac1ecd001f1a95198157937e734c2de643584ea4b35c7fd2317a01f9bef961badaaffb16a4122b84ec50e9f0db35d01d487e9d84f15a96cbdb4fcdddfe5b2ae6879ea10f580fa1bb377e8309ce5eeefd6eacffe60e2ae96294853e0f940ddc093370d3708d220527b3a97cb6576fcbe05f867b59bedf0862e5e621acef633f4b2d52ed6ec5700ff1d8e24b2ec1383c6aed442fbc3997da0bf7eac08f4774eb6272f1df6cec7a8b92ae9843f36ea2e75e581f9540f67ad57d49f337f2ac28666ae51a6ba54730803f165e7db169f83f44e1699e3ac5e6e5658b380aacee5ffd7cf73bbfa37246d78d1326ecbdf0aeb17f60c3d80fe57691613d55f8748cda2e9ef68742a9b63b0bc09a02fb04ac0c5fbf1abf2e3d0cbf1ad5198a3ada97c1c501f7fc491e02f1d475d949f6c2d2d6db0a942ac1305f4df6e2e28ab6c0f743c3e2f54661ff18c643f7f4206f89d623538aba5238c36972f9e9c721e8bf12d5b2500ff87109082a12d77f341abad945ad8ad9abb975e15e8bfa39244e722e5e0b71e54aee2c536d236eac61300ff99fedc987d2fdcfb2df5755536ae1a98407a6e4940dfe4261709b7f812433874e0f3fa8d682288030602c07ff218a21c32c235f43a2a0cd9c9f58387a22857c0fc5759575c8eeb2636964e6bf75b994b40382115985fd15213b7c04a0859c3f99984312c6ef97a269f1ac83fd0e2cd3a50e1254f81076e14ae93ceb37c09f0003f44134cce5561f04c82f0087b72c0945404b7f909a8bfc7ecd660aa77dc563d7247709ec9d6f5d4701881f9d42f9c91634d619e50d4636d5b1b083e1047abc980bf82693d6832f4535b499f855e9cf02dc5589d4d18f0ffcbba31d530884462e0d6e411596a894c718fb901ef3f71a080ac53b282e54f6b50bfd5ca0e1c7a4309f0fbbcd14a2cd988a30ddb0e36c67f649bdefebb7f05f49b77ca5016397dee97d5590cfb51a3937a1febd4017f30ad26a12b5d952136062aa5c2c1491673d52a0b804fecf4083b0a48a7e567bfa2c61cc7f33536907c00e2fb734b6e5d44fe25aaf96b45db4142329c406c0bf0fbf485d216fa26f737b98a1c9f9fb007c95df79700fde5bc5f6a914ffc84182ada8adce22a0dde08ee2ba0afb3e653ea10309621837a9043c247a6f32e333d00fa3765529f6b77d9f0a636b1e793436bbe3b947904607f1a8c33bd5ba17ce4e67ccc2c67cc3ac1b2bb4401fca356c1ac47507cdb823f9223b8fcbfced15419e203f8143f43488d2cca989e92500c863c6b6c590fd715a0ef6df86188551565b5c29e96da3a237588e0325406fc7ded8d54c44bb61bdd329cec11c6e8642f5f7653801f8780c81f6445f51ae5e910d697be28be2a3bee05eae7d9e62b1ecf06019e80817d3e9578b2dadaf71f80fac455a8218c7129a70f9f7fc06c5ac6637ba35300d87f6be40b6bc12151e1d712d1a9dc305c59a2026e407c929911d0ee7793c0b814f870abf1abf4e0b9771f985fd556db935e206c17a7b9c2601dcbae018944e703f89b427eb475b71b747ccd29c13a37fadbdb5d68b50ac47f77b256ddcdafbae7ad2cbe138e885478a974e0fd8d52f83a2db0129065efaf8ca1a7a6f9daa2b41e80fe75621d5725591a9664beae620cb54ba6d05c4a12d0874ef774e3a4f4e462dc06a7a3124f4be14c250c00fe3441ca7ed228a99396d01e417b23683b14ac8105e07cd172576f27979031783d51f7c8ed7990d7d7e601cf27b3eda2f8d84abbc26e98702df4c95d54d2c91ac0777d069f42a4feca9e46d25044e23ff9ad98300b01f18fdbba9c409ea565d6d11015e5e2aeb6c593d8d51dc09f39131ccb0f9f1a1ae8a6d7cf7a87c952880f01fd5c53781eae29ac218d8a6a4b65f6deddd7b2f41540ff698fb8c8d238e4757f47a0a1b9e494f5dcf3900fe0fb83e893cb0b4f5add064f30977eb1063ffa49d7017f23f7c2b829b49611931172d5a769a7d30c82124f403fa12b1c89d9339641dbbb60996514b03c81d3a907f08fa2b63d470e151f1006140ddf74fd715c8ad14e80dfff49917c81b9b898b81ed6e0665d23b84457780298af9a955b75efc1361af309fbf7c7b20705ec0a8125203e191d4309d760f77930956d70c49fb1ab1415a700faba67edd4cc7f061b0b90e799df1990165bae0a6501fd9056dd5c807d54c627fcefd2586f869a39ebf78bc0fec1216d143b305c9af5ead83f454d4e38da3e6203f887a33f97141449d3cdd107c9cb3f0fca4607e53c017c4ee4d00d41c7a8a664ba509d1793c68f5dfc1417783e71d6b30efcc3c44b0e98b62f45daddfd57d74b407c52e90a29962298999dcde5e41fe81a8bca29ae03e227458f23a7d67339059abe3f6b60a86f4ae7a72ce07e872c01d69a0d4d8bac50e2f2fc6c1dd449f86570be38c5d93a61014186927feb3645811cc41865063bc02f9ffa6d5bda4d25d51ee9850a98b82adedf28c703fac167648b102c71fcc9bf86ff3acf9c7448b0d2e7007c0bff8ef16454b3c32d4cce6feb5ad51cfff0be05e0b784f94c7a6ab1b03aa162e4505befd3b56a928980e7d3d75546e37f74596d99dace6b1db54303a3920de87f90af6b9a825bc9bde03f60aebcb7af77210b9103f0155a4bcfb3bed55b422e58d6d2805667e4190d58007f9ab0faecc93169ffe4c46ae496d4fcefaa956a1cc88f2698113ef57d579a44ae158b6f6cc8d95bebb8803ecb8ff6b44474d93ce5bbc626bac07b9bad3b627c6990dff9872183e63235848b95fc55edba09999a1e385f7337f7364b6ac3fe287f8c30db37fff88f27ef00fd7954a991a010a54cd4faf3f4b10d892333857a3dc0df7f978310b771ef1d26378641e7125c3f16dde304e637c9c305d311b63116acf520092c1f969cb1b9ce03fd69d7e7fb55b24f5c938c465f14b9a60cad6fb7bf00fe6d5f9c385b6b2a0d3b37693428dbba68c343366720beb1abf57256d09057bcc0c662147e63b145f6d404f49bcb38581c9c7e141f1690279e33581ec2ac7d02017f6adfb3382e7aa117676be79f408f845aa6a4e73c0080e0ecec53ad3bb02354c6dbdaba7d706dfadab303f457736c308ce884f41314b4280d3d513c90745d95c0fe313d575959721dd4dd26b7a1bf12e02da33f2004f489ef20d2f5ba967cf5dd841f3a376267075d07a6007cfadd97af2934625c8be5dd617931ad8d510f8d7b00bec6f65926a0ab46568b68d8dbaeebf8341f158a16e8bf59393e2e22a74ffdd42bf42396783279b757bb0ffcfe887618f621a70fbf85488ff1da060f58f1715881f9af35f9939b778c4298dbf8ad9fbfb4f0767559b802fa7c7f67821a6c754ba4132f2412284d91d43b3f38b0ffc49cf24dfe57b478d5166cd2476a36c511140acc77c627a69aea5696db546256997235ed755a751006febea351378567c7b59a151a4f90836445c9bd8e06c07ff74a49e8078d07d49026166eb965376a1297a003f37b3ffac5f11eb4ee44a58e95194b716c9188efed01fe4e236ef548b8396c989e5389ba85e4825442b77440df40782c3c61602fca88e0d923f9bebafcb1f1d407f09f2607c17c0d3fef3aac37cc9ef8c16aff67fd733050fffe869cb35410c7c5fd9d5e84a9a82c62a3168f03f40f52a34de3ee6ed335b1b6bbf54915e42b283e2140fc200fe84914670c755b7a5706f34dc2e9f7d5e401a0ef45d6326df6cbd00d60da93e85d7f1ea69ff3ec02e2e3a52aecca1d0ffd30f768bc7a7ddb2fd5550752607e9850c769296d20d40169a93677c5df6ea233a60ea0bfc6be9110558316a9baeef4692b10951532436e04f25fd64b5905842fa191be724512518147a6ab610db03f652c7dc974107ef1aefda1dde1372396777a8f0dfcbe05d2792b2a3615578d8025ef3097c906c62c0fc03f7ff26a0beb2a6f2b6aeb3c76b20bf1517e9b6e1bc0bfd54e912a84ab85f513a778fae95811fff425e502f5b736ce3ff87808fbe92ee4dac5cdc1ba9d5303f07e2e1e8b4ee420350d2bd29d7799d4adb90969537c407e7581e41dd60e3a9c0e3fcf5a6d24d4f7f422a606f02fbde5c485d8cf4d7d55333e0b39e31e4ba3634e003f18f338293b509d1cf8f8a72c6da05e2abd00961a882f8ff89842ebe534f9f7f6eca1ffb1f716407976d99a28ee103cb80587e04e700fee96e09204d7e0c1dd83bb06825b70b720c1dd2db84bf0c0ad39d37de7ec73e6ff73bad367eedcaefeaa52a9627feffbbdb2f6de6b3deb59cf9a921216f0fe4602e0cf33e95784508f878ffc3caebe5fbef964492aed02f8b2e68eedcfa922d517e9a73e4d8aba43ce741fc1fe9a353a9047a4e382d242df6db7f6f2cc4dede76180f57f549d69b8efd578ab640ff5fe652a8fe7ba0d11809f76c9418f1236aff11262c45106c695f60fade901f90fb72117e961c9d94428b8b17ac3a3d134163b0ec03f9f882d0c0b7ea3fa3e843ef39af246fa35c41b42809fd5a67f5a1146054f6361e2bc2962356f0123b307c437e49479a8633178efd285df1156f6f31de621ee01f97f92b07bd54a1c658a576a0dde3890736e56efdf01fae343ce4d6f9f1c7bafe5595cde7a7dc4651877583201f2f3e463e3829fce1fc69d1b350e167bd8aa5a4501fface0e89b1661e9e19e9a946cd15b86620cf4876300ff32621112f7e4986c917714da93fdc624d81aeb02f0d7b88487853b0949d41a42220cee2f37c8dd3f71e183f54b31438fcc6517ed21ef11e796162e340ee781fcefacd1d42af572040aaa25d296c7ccfde0cb7917801fbf7681e8c443a158ea203fc739f21262d264011f383ee80dcb13856b613b059937da831ad929b187b2407e85f9a5e0b988025301a71d6c77d7ece7be368353c07ed7e4da5fa81cc82aef8e877ca0c1e7bd0cdce306f4d1683a69e47c779b99c63379463f0d5e5477325302fa96440834526f64b60cfc3808168f299caacbb7b001fbdf5b99cbd8ed4ba9479ccaccbb4f0c796e0ac50df8af0bc1ed14e1182e8ddc4838dc6c687ade053872c0fa36858d7a466417adfdd634fdc8602e4bd65bc715d0f7ecd91d7e21fe5822e4598ec68e9730b57e669004f88793844ef3f1fbae931052542f0a71605e3420a902f9c7d1f72f30524bc6e2052cb6ef621aa0066789a301fe962d1dadb49b7e9154348b215e70c84ab46e7c05800f8ba2c73053c95de05506d3196be4226d21565301fbc7ba94fdd5b1b2147c93100ffc0f384a2b345f25609ca7f1f95b5bcc8b891f17f42a9b42e86c16a8b100bfc4b3c50613aaaaa92bc2e4e510b29c7f0d0d4b00905fb6d0302efce6c48daaabba3bf861f2f60efa7904c00f98141ea3798984c0bfec17e3905a69eb60322806e497ac9e3f5b1f1c874dcfb9c9ae0da5b8171633af03f029ffd2ab93f88d746c2f4d4fd35a73df3862cb7a207f611b1351396be688dac61583ebfd8ef6752b6c3310bf32eaa3ba73f60f8760a8be5a33cf83a140b5c302f4195af7a84b26bf1d2f96c168312ef536e238171f01f88e1fecee45d7046dc2b80ca3f4131bc69ec9567bc0bfd4ae72995ad2423fc9799ac2b4f9fd9bb4a3cd2570ff630efe4f688bd55e6f608c0d201d9933d7d67f03e283a8e0b919fb43fa9ce1af5222df09690da44a4c80fc331bcfc70a88a7ac9eae68539551cfb81bd1116901fc70ba2ed684101f678726fb4dd03d9967350bd203707fcd6b38ae754d248239b183648b265f6adbd2d1017c3cb75dc72a4222ddf0482ebf2accaa642bd1d80a88cf9f5f63f2d04793b934dab7ba380d14974732d403f993ddd2999bd628b2137e7ea8bd77cc5259c84805c0fab1770ef18e668d79664be755cad9d78740da013ac0ffdcff60db4ec8482a6c5987ca4d86fda00b9b7508e45f845f671c4b5c9b40b3dfb7b07d9cf4c382e3e603fc87763122d26b3ba3a5f6d7dc094ab0f92841060fc0fe1a8e3b64e68cf52830cc75d1ec7c7c4135dbe700ecef86b1b079ddedaf6b128ea15dbc7d1d2dcf5e3901fac5398d7524e1fe3cd7c47ae74817d9e355de1bb840fe465d43de0a61cf75c8c64d230446604477db6e15e04788d9b2522b5022e3636fd08b22d5bed736d27909f427225ab9a208b18fb8842fc7b452d28ce986167503f01b8b33825335573ca1d271655baad2842951c512a0bfbd5a1bbc78f953db25095925bfad82ddd195925ec0bf8dd025a2d38c3f4cfa727e42970af730c44b0d0fc4873d16fa48666e0284dd05015c0d860c2da6ba2a407cad3d6ed93367235ebfe78ad47a2c032921fc3a16f03f5fceef0befd71426123e3a4ebd17846059f07507ec077bc60601a2be3ca453c255a8c16a3d29a2b00ee01f6037e7ea1d886d5e4f39603a137a235cc1bb1003cf9716eddd24c674d25b74f401c4c30b0fed8c0f66407e3ed3c297e73af7125ba5725cb3ad5b1efdd0fd3590df49a45ce94f80ef45f35824c689743437e8544006f0d7a2577295519b456adf17ae22bddae5201092fb8d007cfb3b82387e90acd66601ab0c6337b7db59740fc02fcc2aeca7c6655fee507aae4c2059aaf37113a61cc88f0b493947de21171823b1947de8f6598dcfa4d706f43b588a4af86ede375e6ce13d0677db9f395404b701f3eba4c0e875b3dfc6334f9f57a4528f245850c377003e68a888c56fb3b11c508e6d5588f5a2e1b94c721b50ff5643b4ca4c973313aaf0235dd2a06e467452fd15307f3079f5542f0d863c216bc7918abfbd3844f39503e6cf4cadc6fb49672959855c992a0f27488f087b6c407f58a3ac178e97f51ea6eb91befff4566be690a506c0cfc8093f2979f1487a63320d903a91d2e89eaed103eb7792fec1e402a5529c63a48a8f8bdff4ce03bc1a10df76a98f0cb7e316af31ab28ea97f9d899e31cb700ebdb37a338cb9af998fb33eba90e7d0a2433835a15a0fef849c37e37be16a7db586e199efc1ef92a039127b0bf7a2d0a9f6c7ae942048ab391acd359552950510c00fcfee3173dd0f8225f1c5a902a9269a7ed58dc9fcd02f9fbe62edb2bac9627db742aef0b2cebe7f0f82880f822a12f78c48621c25e19158efb299958f53adb16b0fe271caf6c5f322042126d1bfa0df6a978915de003f83ad2bea4816723d4ccdc7e52119f10627b23d33ab07e1ba719cf7d4823594067dc6d4c8fe623dcc8e005f627413468d95d725386c9749154b1923232027a3ae0fdd3e74dcef7abb5bfc778e723af7b467524fbd210b0eff23981d96c6c13c4f4ae4979fa75bc3005081ea07f1de31c291774d698794cf2b7f7c9c91a4daab1d3c0fbd580e5291498e9b918bbc2798a1a90371b87050fe02f322e3d85fce46bb8a882ee0151d61275c2ef00fa1e045974d5f396e5c0647a0551aa3183dce4b1d607e0fd4a1f37a0d508b6afe4ef9db812d321053c81d701f80f930366839b42dd5d1f7a908c699f9985798fdb00fccc26ad6f2cc35b96eae36feaf42b1a33a8b1c57480f5d1d76faafe79c7a69852a8b79ec645b608aa858f23c01f7be85b601759e8ae0a4b9e579db8ee114b8f05f0b5221dc259fff7c81be80b770b83305b88769f6980fefd4fd0ef596945ad669164829f7d8be6a5f12fd600c653e88eb13489c3ccb275d67c745dfce639880ec1fa785ef8a7db3f0cc4462904470628e0e2512a2301fff232bbe089ee1cafc82dab0fdd51f14f9cb11d26607e1dbb9aff24f6deddc5f3337fba40368236417a02f00bacef2cbff48865dbbdf9316c2fcc2f2d5787e300e0b3592a8606fa492ef2013cf7363564f13c103136407c94f77c5daf62a28aac9eba493253650c01533c08a84fb65d17b2b2abe9a79a66bfc1d4258786974f0d01fccb338f7387855bc3c1e7b9a6e13953d9eb65363380ff382f9069bc5fadd53f4c5b93c68d7812a1ed2d0de053a40ba91f5bc672f3905ed11fea39228fb3337d05e6d75810aca01992ab52ab0bfacc49599ac666a012c04f3785f3e39736ecee89c887893ceea33a2af0ca01ea4f0f38dedaefcc1f372cf428b9f2f8f21c31daf702f521906c7578acef6b52fcfdf3e33e42d3a7991f4d02fc0b0e5a04db3cccd6d70c3f6968b0691562f95f7000fc4e2194644f9cec2d7efe6f993198ed5b5b4db7bd80fe1cf2987473116c408c2fcb677487b5b5f859784ec0fe62072ad9de0f86f3068fb4101320c059e3973102fe83e114ccbc698fc4cc935c0eaaaac0d7fa08479a80ff4a28e2d7284d5d85bb624addfa054fb43173490ae067bf85886f5167c9d58e42f3bbb89b0d3dfe91e002ec9fae225654df8e6c5f2b64ac45261d5a920e2ff502f9f52b8f8050c3b5f98f072e2e7881bb4f1093904f007e23128de07e7a2a4d0d9ea9c8caf45c38a12e943aa05fba3e2ce2447a0c2f552e0f59ba6451fb2ef1eb6b001f568e42b2a3eeb55a48c529fa92be04e5a0da740af81783522fb08664fb72a65fbcf01b76360e22ac7e02d4fff5ef0f3e7f5db2f86cedc1b5533b310fa320340d884f8b3bf630e9e033386efc8704d8558e0bb39c5180fa3a6624acea9a69213fd1be09d87571c1aefe0d64607f267b1539ac0559e31a7476d539b4470b07774a06d407ebab9a7995678522e5c4249c6cfba8bee6b8fb00fc3e471753dafc8bf264757becad753863bfddf43be0fabfcba5a5cbb3604262d4c3894977b37c952e0a07fbaf19995777a81cd33af088966324d909237f7c00f40ba8730779b052bc23e72c1f5f657c68d2e3c2f101e2439ce293e022bef18b7efd6435a5aa10d2564f001e80a8e5a17f109a0981b234e62294be55e9c5ddd701f2a7a2a7313eeae3d463afa20decb3290889d0204d00ff63c88551cf83cd8a75c7cb3aac1013bafb94c009a84f89d73deb7ecda9bf8692d738b344c3e1b22f2603d4c7c9e5477c0a70f18bc59f9277b4c665e1ffbc150be05b3c5fbaa21c9f9e9e43f9656195f98dccdf658502f5bf2fce21b2cd1954f35a6992586d60a9c6cd672281fa567e032758e6713dbba206aa521c8a8cde5a8813405fa1a4bea3ee9dcd74ca2ba24937161caccbeed07aa07ec471b93b6203d23da66dd0bd6952b52fdf26d306c80f4b7797b5c268b4a57dee098f0ee9ab1c40d1ea00eaf71735bc2c5f9f5513f3f22931210a969316185000f183b746923ddfad8d0dce463e05edae9ae60bcd46207e3cb24136764f57323e9f325d81a05a621d94f201f41ba8b0d0b647dff2e525cbda3edfb81b9b2616eb06aecfb923abfce54d358fb321829baf85dd11a91425807fe57bafef8bb8e83d063e274b1e6151bbb5abbd01e22b5762ce18a9a6c605139410fcb51a9f46af4b4f60fedffac941ebac953a360cc036556d97abdbd63202f5e5cf5c84b5fdc786c474e2aeaadc0df7e6ab211a007e7eb7da54de17e22f56f8afeba49d8539301d717780fd2f2a5e687d938d76d12d83538d50a799f88d781cd07fb8afd608464ccae66756522714c7637d8ae15507b07e8e164fe34ad957ad3b499c9c04e9d33706a2f103fb8742fe0257c6ac4103f2b7e342ed4c884121390e007fd447bc3016e49634f6b4d72d309f1d2a9978370bbcbf604ee1f1581f55350c72d4cee83cb276d5d53d40ff7e54a892e9cec15f2549ad98eaf57643a2ac3f0bd0ff42306e62a9f309b29194653ab42796868310940b507f51216c538ee4148b715ccf621927406f4734e40a5c9f920689760234278694f7b09fe52a2a94f3092e905f177ed82c9c46c053a4eab764e6a0ad3a50f20a048e9facf3862e2242cd29e7dc6446fe487822392907ec5f6e6419d2f68a30eb34633f05b7b41d431fa3c1fe2643d56f8471b8b4761a9147c78791cb1dbb260c01fb7ad1349c7b6fe2956dc4b397a7190bb18acd2107f04bfcce6ee6ad0f6f338b5262864eb8d2b6ca672901fdeea0cdda14d2c401e6f80c66a5653a0ffcabc07e00bfbcdf761ba3a3b2ad979e1f892f180cd26e52e402f8bb3903e70e85c9c9f85f8d36dc14840bdf56e56500f1c15778d361fb387d8af5d1509ba909627576433a203f2256f5c44160a990c3bd5c914bee4a2845ef3a0dc82ff1513dfffe94829cd896795bab434d607cbd131358bf3a25ad50d206aed121cecc4cf9f50376556aa680f8965c705587db32c4ac8794aa8efa3b27e6e9abd700fefcbc4767a48d90bb8bc92598fdb89e8086e8bb39c01f75d3d05d565ae09382735d721746b54a85cc4905e64f3f434dc3f87946a5626844f0cf938d8fe4f1c9007e2f98f038114e6398eeca58b7e952689032106b0ad4cf4b534d577ddb9631e2c855e9bf77767a6675430df0eb598c472f9f2c758908fe30bb7ca7f5a97f662f0de0c79709a91037d36f7abea7246bb163eecaf2206503f00583277b5b4fb660340fafb10f1ea207a31b9c1401ff5e8fd1f78276d3f97e8153e10d7bc50fa5b45875401ff7bc67646b0f4233d3d60496e095cdfd8d40f223e07fcb23eb2ead5ae916d6deb9d3af1409aa4b08f402f50b63b421325ab7a50b4656e33ff3996ad0822ebc007d5b33fd5ba83cba4b160e46331c313dfc398dd24220fe241178158ff10e9ec3f3dcd785bd453ec3f9690050bf54fd55604c041e566bcb3f61ef8e80ece273bd34101fe2e6c4f3c7bef5735fd3bd445a7cd2642e8d3c0ee447fa91ac43becbf22dbc925e78ce5e3879650ccd0fe44f71538702444abe1df443f1aaeeca5e7f38513007f4b54db9c65267bc9869ecb41e23c4886483d8d12b80faec82cb09ec8de8ad18836dc8a5ef42796d92d1d480be2745f4fc91a4cf016b53bc5d337e1a766eb0c030e01f9b73193e998ecaadcd57633cd49ebfb476175c07f801a6bcf250cf10ddbb1313be287bf30ebb87f18e020e888b46ed21cf9bb982ef3221e95676e6533f963e01f5471f90836a3aab8f9b949095284cb077e878649d017e2aef792af5ee14d643d5f3f6934d9c8a83dcb05160ffe5bb83663ae56e1315c3670a8655e295d1d8b32d06ae4fb0706928e6a57f45ddbb738759ae8d352567c03f71e8fc623453e06f66531eb3ed79acf46334ca02a84f97192c7b8f1e1ba10c7df131998a5f377443e519808faef1303a405657b123a041b59e7e76e8f50fa703f4d7b522dfe7aee32ce97846f73dfb5ecfa30c411904e83b5cee574c9c65737f0f5a5bd533852ff6af905f06f6f7437f526befc327c990999aa14e6b90fc28f02f017d475fda50f443bc2172f963d68b97e79e306e399c00be41f6ede46bcdc9485e71c029ce86ced071a43227a0ef72fd94a2abab1787db552fc66fccad9acd6ed115c00ffbd9ded2393b8431ab3424ffb89cf566309f9204f237916d9b851523be92b9b36b77bba7595e05d88300be562a3178dcda29a09215bab3a5cac75c8cd1430ce00305d22cd5c9219a291d9d039d4888e246f5aa1c40fd9bf07b9ecaf1a3db8b781b4195c9cdf0773aef0781f8d5e5cba2d1d669da82d13269308337bb53d71e3dc0dfbea9307a9a4ac0aa1028dea68d5bbf21a7a86400ec1f16f6b14ec3f34a3e655c862f7cec8be126d30b80fa9357712b8d0f4e7ae347ea52e664c52ee4a37e2f00febcbdb5b58ef28ffe47afecf021f504ff29af706ec0ff29adebfe7035356bc6cbf4f188c7919b9712810fe0ff791f54b5d7b58d3220befd246083cec30b6b4a0ce4d792b21490e550c5a8e2a64e85717dde20bef6c200f28b37c7b935f463e1bae773c496e612b564e8a84e40fda78a67df9cdea8ade051f35e78038a14aa2fe931a0afa17b79b84381c2802d8946635c57b2ec6884e500e4974d9c836de2310654fba5e1aa75dcc8e404db7301fb3f9d57878fa070c3bdc243c847c7b2ab4d4ac907f0d1bdf6ebc14e178f222d8cae45cd64934b8f2966007fff56b3a3cfe8a3611550a070f7ad22ab39b76e1788bfde9e3989ba7f0b7fefd1fac5220e2787dd613208a81f0f8257f82a65c1c087aea2c4d48d736e2e6dfe14e02770f0d62997efcc7dbb22853c2069d322221bcb00f8fb02280ef42c7e4dc926928bdb72125da7cac4a9407e97e059433656f54a691b7bc67070f8c547ac506ae0f7bb8ebd4ba6911cb9d21dbb1eac6104e1eb633a80fce79bf9b0efe3a2621d950a5f7121b436427bcce181fd29b8d31ffafa42b350fee9f4b32e7e2e254d21f64b10bf12aee4272e8f8aaa0a73ab85e72ac5f66205d6bf22045907818100d25ceed174aeb64f57e814b300feb2d62774377eb3b85d2760352b873fbc91f26c0ae0773a39cfcf8f8e4d2bbf7b221e5129284beadf8b0ff0f303476dd3c2de4a2f4848554257a77c0fd1d8cd02f8833acb9d24db4df35e68e7fa155170f7d78fabaea0bed582649395c9872c64b6133d02466e93bc67b2807e1155aa65e993243295c48b13f7c0c0becf5a648500be97ac266af1b8c3207ea0e40e19abbc25121a450ff8771fb87ce3323754bbdd0f695b636f3ca75c8f710100734b44f379afe2b4671c775e283ae9aa4197fc17007f80623c392095f8fe5524e6509ab207ef2924563360df857685c25bca95c219575819e7e114570e3b07c0f36d1843aeb4bdf58e2733df6e533074aae626fcb902ccbf0d039704721db36d23eb84a5d71cd855dc0c807eb2566c98544161bc5d13dfc105e71cb6474bd209a09f54dce37a16ee0fe731356852ffc6f6e78a0bc567c0bfe6b34df05552608c4baaff0cd5dfe4a11bd69304d82fc627f1c9e74b83935d9ffaa03172b36d02251d81faa9269313739a6e73cd6b4e6fe426f2d191c737e5c0faf9b6c9650349e98de100254751675cb36d40b133a0ef2a805b56dcbc46aac1cf58f43a26347497d10e7902c0bf32db5183a214a80a8abad58f25aabc98f07d01fe8e65aa232cbaad86d7cecfc48dfcb6fd7b1b390e80ff48ca87adf823bb83d4a5367a645931690cdd5106e027f51c7be88758b650bf1d2064916c8ca3449fc803f27fcc53b40122babe639073d3f5d763082d57f6d0c0f1f2fd946bc2fbce5498cdc8cd21ea4d4cb0f63600bf0d8246ea3888683a6e42a882e54be57de02e391f103f85e0f7c70c3779bd297ef76589423132ac8e2808c88f6cb94d084f6fa7e57d120c15edaa111af9a1dc0be4c7faf8346e94ed1417834a3d71950d261babe1b180f8f7da38ddb3803ff0cd009649476fc0eb1e88f73f80f73bd0be7b9845c7e31fd65dacde26924f9b15ef0cf03b64168dc339574fec98a553e9cab62f13a110ab01fdbc737f98a4d3e24962591a7847f72725c7b1188980ff6b94711721fa50aace4dafed5742a648f9c5c81de8ef73e6fe3c9d55276a874d3c5746b22df323e7232cd0bf1a82d4e36230888b53222bb61f7ad64b85502d17c0679d3696310f72043fc7c710e53917957086c7d383f8bee4a135ef94203183d952b6e335ec9ba9070da07ef97b9d6a381343af5fee8ec227b7e5f195cbbe61e0fd88dacd430bb96caa4427212dd28a3646763b8801f87c3f9dbd937a379cd0bcc4a286f173918dfd7069c0ff194f3a8d12401d90a1ee6bea147a1b1578bbe500d8e7b4d8708aea2cc590595d95564dd927a3d00a7a20fef10c8ba3ce36df7b362876d3cac3cb091d7b9801f00fa76be4ddb388e88ae887323b8a369a304ec2a401ff785decbeaf457778a66de7fd6848e0846adf692780df924dd72893a95ce1613bf66b5cb18adc65870903e38ca1f550a836186bccb7daf67d43d1bbf1b7d300fe24f3a0ac5b3234fa7d5f99171e3dbc6bae78671ff04facab864b54478ccb73dda15912f8783f1abb28d682fce5070cfe361f07a3a8f4042d57223de7a660a0befeee3566c73a34de2d1d4c0f321cc4c48fe5ca5bd07f19f24156217f115277c5f73c6f8c1ad9f45e02e06fa6bc4e81438232df64e6475e2990a38b4a937206f87399e16b4d9558cd283c8104c2b3cc31e147635100ff0f87398dda7a6bfa2c4cf066e687f1f01799f95e001f79c7c8fa7d28f77cd8f5bebc55194de843474611a00f1ffba4b3efcea159741af515baa3be22235da519501f847932ae2b7cdfe6a4c4cbf65c546cace1e0a311d09f3d7f75b8ae5254dcd738fc1937ff8db0c60b855bc03f3b5ff93e8bf74c284d656f4a48a1658f8782b812d017b2fa26a7051755ddce51f7f062a8106d20c7830cf0afe8afbb8272530fb4264f11396797cef8ecd46c80fa4061c24ae96641ba2a914a62aaee302e098df22e203f4188fc92f32ac0a552c8628b81a41739e2f9753b90df637014c5cfea9d6a488880af2d3bf76891922f00e2ffe7becb45f86f76be21b81da8133135ffe4c02305e2fb1c4f3f43e50fd72619650c99d42bb19fdfa8f280f869d36e98bc73f803cf8840df6cd6dba71f0bf380f9f94830f591676f916687ecd0152144ba0ac7e923e03f7db4caa75c25e69a488759f964394ca7e331e900e41fe8a458ccb286b2f5ea790fdabf69e151faa92702f9a3ec16697594fe7c9ad06ec10197933deb8ded2f00be1ebeff742cc8734fcfbc5af5939a9e6a9787551d70fd0ade796fe453243fd62b86f52e8499c7da96ba01f5b1508a16c804af434df4127d827fb83f72d163d302fa562ec7224b84d04f2af3358fc65ea87543ea7c3106f23742a88f06d87e36b9db2ce124333516a9e8f8eff780fd0daf8089e1e5020c6f9e018f31c156c6e1d714207f78129fd6d748fdf51a06a6a41af11d25678eab39505f8a429b225b147a93f6fef3d0b7d5cea634a67259809f766765ec3ac07ffb7dd29957447cca803cc62908c83fb91887b717181de2aaa5dfdc8d1fb5789e54c303f9050beb194d0bc627bd0e4143649b4fe39b679f3701f8c43cf1581d0cef58e5d0073be8579f50bc0ac38c017e80e1cbf99e490f76620919f94762d8a117f5c606807fd592ee8fc74a8e1443d444b2d36db9161ce20005f08b46f0cd1114d7f98ac291b493b5b66bee481597007c4dcc8bc71d5bd8ee5a4b81b65c6829f02ce02bd89f9f55d85f2ae458232752340d452fa7011b369f0cc03f6ce25c1903de94bb26df0da3b7a2555c701a3b00f81ffe52c34e6909e1e3a83dfcc7eafaaa83e0d25b005f6ef620511c5f7f0a2f90a87855ad21fdd65f0cc42f31f3ecb592da961197bef1ec6a7198eff75c6703f9978ec48358c1a972bc657eaf819f9edc44115897807f858053a1b86974852a0075fe0c4a89c96901af07c89fa0e9a3ee2314f0b17f474bff203587c074752309d4ef5187cf44bae474c2cd3d569c97977e3df586fe0ce877170c74b8404f47f8f6f3370655dab3a7cfc26d03f91ba2d47deafb7a9c419ff85676ee81696be1c326203e18309f6097fd7664af9a25b4638d59d1d6caa10be0076a5561a24226cc6eac8c469a041c6f239560e500ff543753a697e615e68115f43923823744dbe6321690ff092ac7a37bcfa3b5b068a297db11ee42a1a8b600d60720509e48189ed29ca933c740512e4ddd147d06fc931b8b7262b75569974a4beae529e7db55af311600bf8da498db5245346cc9a61ac77016962fb5c3d000f0b335e83ef2a38b4cd40e8fdb4234eebbdb44610ce0fdeee94c70521d1fc2303fd7494c7f5cdb1972c907f8d96379710773e189b369c1a37d5e05a75169dab7c0fec0fc6d2c40e1ea69d764594aa03fa92757f2da14105fd5ef6f4b5ef39144dca272a6a28ade8f28cac203f97d4d7a9f08e83c818a10182b995b132865889520001f0d1a162075d4cd5ad43f880ae594c8e25a0903da2f4384965970aac20d30ab59befa54907a32f30e0112e01fed72af6006e4dc0b3b0f147cf6f3217be90d3703d48f5a36350d4010bfba2c9ce828bef2f1dd9592e702f4637c67028e1f8c1a9016c949fdf463dfed976180fce32d797236ea6525852f379fd874033f4c48223d05f413a4496a22e06db6abca7ef8e2f89fc61b57f981fd1b3d7210c23ba946e22b86731c0a2b0e6d7a35f881fd9395bbd7e3124b98352ba93ae3ce812a72613c1f887fb1abbca073e306963b7519c834058976a2a49980fe48f1a55a623b9877dfcf3bfd5bf666e072760ac581fc5bda4a80daecb60e4b663f191f676865c0ae5817503f979f871756e2b784200f8f26edd1c9bc5d6c00c69f3a9699a46cb2a1fccec904d6b496c949b0827a003e3d4a0243fc0845c4a17c27a53a13b6302309b90ff84f2b85e7345e9fb3d98cd65e393d5b09bcc798e100f26b853f9a5b6f5513b3dcd7be8c3cac37b886517700f149855267c04fc4e4bc702b9c6ccbe5a29044c30f80fe6971d425bb77a65c689df7bcc6330e681634622200df1047b01f592a714ccf24c3e4e0d5ef449b924602fc37e6d191296f6b32d628df672f2d6ab7629d44b380fc6805292616cae9888d9d010bb95cdc12414cd00130bfdd0c1e8add8dcc11dfadaf240a3edf6e2e8c0806f0df3848bf77a7b741769ac2e63383a8d36ba13fb580fc5c7e20c2c2c6f7e56b12d966a231514d72ca9160e0fcf7ab15decb27abb0c4223ff7b74b540554ee7380fcb2abeda413d4d856d3098debd1cca341c79b253ea0bf085699eaeb6162ac1ade2e550ed99aa1e721c151407eaa8658ca2e251515062798876a4f6bc74cc4d519e0276766c40645f34576746ec2355a0d7951ecf50b00fa7c8a7d0e543b28b2a4eb128cb52ccd14e8f726ea407cb11adfca093955212a2814365460a0f844316005e0cf1becf7d614902aca7120b6a037eefc74993cbe04f46b0d9f367f0f1bc6bee3ece68663c7d93474ea1204fcaf138e5d79bfdee7b6b99cae17e72144e39a7d1a00be1b3a5fecfbd2975ed7bd306c1239d79033cfa00488af48d42033b14259e962d93d3ea19760b79fb7be079e9f752629633aa42923cb3b2f0d84be60d3863c6560ff5595ed21324f1f98aee0c51ba4e426a7c2be4c00f4fb136c55dd5cd4c6a0913f7fd5e181cbb2832e06f9bb79990375e138a8a963d861ca6af9a2ad443b6c007f836cc585d5871b89e767dfac99edaccd1e46ba1ac0efdbfd7e29b43e679f53359800d57bbdbaebeadc08e87fd01079651b8ef7dfd4af90e844496289d2866903f12be40d25c9a1135bb58ff344c9f0fc6785aae2b7c0fa50776112dfa6146805c9e899c5c34c40557b810ae49f0c8a43c5777b9e4604678c411d9fa1cd9ee87502f9c108fa9add56149a4faf0bbfaef23c607ef1f4780af0ffaf486b167638fbcfd7681ae7ad267f368e402001fa8b21d9dfc58522c43a98778aaeb1064973f8428f01fdb3651911679b74ebb45359d48e5ed303bdc7e543a0be6a65c7c80e3a34b6d629f7f95613d305d46c430eb07fe91074abf139e5382c3e202bbc4822e62dd3cb02f21722dafb3c6cd33fdda231c6c81988a044c5c36680fccdf74f7118778757eb56011fda5ad853b11d556201ff1185b0d6c4c63f1f3afa884abcdee1032743c624503f652198b6b6ccf7ac9f9c934db1a2865cea62511ac0efda081e7d28142687ca07e58e1b9367b8b457d300fc233afb28e844743c23fb4af1711a35ebd02b0406c02ff46d445f9a1ea00969c8e4ca534cc493d325c601f893020c47a4a26f3b560ecacf92e992cb8f4f9a7d80fc9656bfecd17efa8eda57565c9316832905225924807f4454ed93fb78f83aaf941ab9816de64c9f04951ee0277c5fadb8b9ca3a6a9638146fdfe59f65d4750d03f437b14baabbf797de50bd11968e511ec9a5d608fb0cd4f78d79ea2dcc36dc64493b5bc63db8a21c44e14402f9bf8692af716850e2d77e7c18ddf9691e77344b1f80f86af77866917c1a3a3638d858f62cd68893cfaf1cd0a78ab4f348ed581aab1d6c9bb1c5e6a4bbc56c4900ea232d873ee52ee1be99579f6d1d985a16d9b452c601f853b271c8e5abcc454eee0df4cf5bc8f1858f2d4c80f8eb49e0131fa88c16b3824382b9588798275999f1007e1ca3ff03917d61a12302428b0be9826f2dc4c81de83fc8d6f3a6ee1b45609c15dd079d033c0276632232a07fb515dca906b39e5f92a7f533df0879a172edea09a07e4e1b8af85d5af1f35dc8dc1bf61caba9378b0575407e295d8390918a5da71e8130ca3d5d652f291b771cb0af0ca3beaf7c6eb69c68105246e952f2b26bc210803e6773c0d752aee7385c9a7b85c2ebbcdab9a7dedc007f67af6e3f16e506e723d6f96387369abaf7bd801dc07f3d614376a4fd42ce17851d7658a84da1fa025116b0cf9792295b76d412cd3a02bc26ed749343507928545037b063de7e4f4485fde89e7c6b80edeef4ee87254168096cb523791bc84adbf6038d75677ad33ec58c7af85d76c80c065f802fa089f0b77fc8813dfb58b1eae5688cd1532a52f517b009d787bd2cfb667f7a3824acdc2f7e60f5cf87a17f310e35089ff321853bef04595afe5d82a662189bcb7536fc2eaad7478c848cdd01aa850c3831e80a96d0a4dc629cbd0189de79041ade55b738ea8745fc4e7be935cf2a5f425e51e4e26113688b07690f123da7e8af104d682e8cc3959e99f82cec861fb3ad3df66214f3246c5d46cedf4f0ddddd8fa7755ccb3af0530b6d1ebefc91f7ea1b2aa155962267bfebd2d17c3026aafb5959ec765681042e95ea836e6f1b32fab902317f4c4c5c491175fe13c7f2264b6b99b3559ab16832279c7992059a9eb80a966cc2370ae55c7c213fea909043bcdbfd6a0d8cca0c83d1753b9dca13df095018a8f018e19a49f83e482611e5bb36dea69f30503fdcdbaabd9e7a338db89dacf2c6229e3063eb4dde48f65bb5a082ec8f08e1f8df8c26502c59c7a4e5493806cb870a579fa148d6f91557ea45769805e44550d54d1e94c37754e341abfb79bceff0ddf8114e98ec19a0849f176f508e464efc6588dbffd16f55c4fab391f9c093b897dbf056ba32ebdede4b68035a8c0cde34efb5c97aeaa5247a8b4f961a0ecdc6d1e0b76a9208eb1033c896a9f2ceeab5d25da3c89fffe0e38bea9145fd54b190f8e5049dcd2b87644a14f2c78cbdb96a1aa1eb0451b87195ebaa8ece9139f2b5f6caa4766bb50f9177981f15adc4a4b54051e669078f2fe3027c8cb7b0fb100a7ffd2aa39a3afcc727c1b26b8453cbc6b96abd076dca32a3dc42eb8687c9f1fb675c32e6ea3c9f2a880cbbbd30c474f5ba0beb96a97f2ac1e775e56cf1a596bf7671c019f1c590f87643b3d83672ff64911855cb91eacefde3dda02994a214213b5a7f8d69276cd0023261f384cf867058db3899e34ffce317e3d687ad31125aece5484c9d1526bc346e1676b7c22392a49f21a571eef6f094350e3f63045cce3258ea5f72fed4fbc14a7f41b4958cd9e1ad2805b5793b41fe2383fe98bbfc048a2a2231a447509736836d5815fa867c3b8ea9c8c81557c0ab183f33f021bf8f33635fff31e55304f7a601da18d1df313f21db7ff105b2df9bff10006665ca278df23dd82dda87d7adf5fdc02505a180e2afe677d62faedfe017c7fbbf6b5128f8c577847ff31a219b6cf9f0391b747b327c0d7b68bfa3be34285e17b1c60b263e31d83174bf482afc8de787bdcd493a639b6befdb408c23b8155dcbe93abd04e068ed5037904f03e52450071b8dff6d6927bbf16b80842963b93cfae3b34a773ac4fff1283577742f43f3b9c42f2e0d807ba6bfc61a4bb508d9e54127ec455d185f1b21dba4fe2de37fc767f5575f6887ba8153f6f6438be5f5a3d5ef698013f72dd518fc4d7b87fa5b6ce97fb31f01e7e7104ae5ede960f6953241a0b96ef346bad6ca68fc95adfd62fc17f3014afc9d3326700f8b3617f2af3f32b6bcffc2a481d9084bb768447dfa7bef055218e29ffaf3cb3553fc336c1a104b63ec55cea285371ceb9fe0696507232808bde3a8ffa383d90cf11cded4fef89d3543a4bad6e404ff8ca0c81ffaf13e8a5d9d298c4d3f2061ba5bf4792cfdcfbdd7557b8a58ca35624a1b1456930cbcd99254a7d72b6b94e456af12f55b3151cb5439d088fe414fcaace939339bdc28f78620e4a5fb153d0e032c14ccbfecfb9fcfbebd6e52e7c70cafb765730f0c30481133dd1fb934ff68014cf0ce785675fa5b6e85c80fd6ca3547c63c6388f54fae5db7d61382d4c204819eec42f7332bd124d04769f2d710a1da06e8f8234ca63c648cb436d056fb5ec6429e1de48ab02c25f4d65c84bff1e3ed503788ff6353d1caf0a315d76840fc476c2ad0bf67f4d0bf304838e0f79f89bdf149f5b895c6f216c2b2e4231e3f232754fe3f3d293fbc72966bc6be314b0df6f3c896411978da850bffaf49f98f9d944e1eefa786892ab8daccfbde52c99cf84516d351fee9190a5e2dfdcea4fcf8836760431abb39250272b57c4eddd1d9421df3b3badc3c92bbc54f290eba52ccc4c5a2846f8bd9fc9eb9e9c2c5cd45d8fc576ff9ee93dc8a14af3b3109cb6909ff414f8a5f081337207798d7c3771a962d262f239474b8fb5f9bce3f9f7dab084babd0cff8ab09bc5e8889b97b99a39a7fabf44707d365c3d4b859fed6231431dab9d17a0371ad1aed8e8d8b557611492bf39132d63a342ce507afbd55a8d339fa58921c8f94a8a6adc28abed2f6f608a966c4c8d869cc638642fa88cdd7e5b7ff28a7ea5d8e61ccdea79ae04ae1862c28efda4b6cae648f7fd9f73f9f7d4bff6c0845f0c676de1f35d51141574de90c3d0afca38347228d3a3d5a7feb0245e451828abb3c5cd456671f9075e44f71bd703d6f2249f85651bf3b37ca50241ec3b98aa70e9d0f698c8e1cced94da4f9787ee8bbe18c2aaf55dff5c8dcbf5b77fc4da70a52d9db0f61e7dc8f793fad01525c5d89eed3ef45da10debf7a2322626a328a0afa9a326ad2e22a229afa0a226a321a12fa6a8ab2120aff9537fa9b4e1f30e998c4d71cafa684a0307df9be61b413604c85eb024ee5ba41fd1ae15dda3773cf172b10a7b01d1bc277a17f78668c2e570a29f1a976a81b98ab40712e39dd0a083fa807423fba78e30698ef63cf6a667fefd1c2fca7f5a23e9fecf8f3a3cc87664376a14f79756f495c4c5caa3ffaa45e603b881111c156e796f07d5aa3cb3b5783f73d77bcd00aff9369f06f97fbef3f8d7e867dc89ef8c4df3a60eabec23e56e1774f39ff1e4603f58bfb83fa05e808f58bb90cc6b8a2fff90ba2bfb840a43f1a409cb77af110f47f76e5d263a0b3c59d44570fb23985f781e8912eedca4bfac5210832d6ef4c8d1d4d4dfece9fe47c6b6be6c0c7ccac6c6524c6a6a429f75644cac88e95d749d5954dd251d5d5c19c4741f3bdb98aa1b4829582939303b78d388f9185e63f70b1fecda9fdaba5e9577b81288b8ba1a931b7118721afa9919911bb312faf190b073b0b8f31370b3b0f0b0f270f1b8f3197a99911070f9b9929070f8f09bb1987212f2f9719071b2b1b0b172b37ef6fdd3f82a9b3151f0f0727fb7f9349897c0d695433cadd4662930ef267b0786a783a2686a0c21f3e480f39fed484d63d4312954827552e77b86441e55554b05e5c4c03250b27b2573e5a258dbaa817cb29e12ff7936b256fbfa4046fbfc23ad1faffb4b552094c3940448b5239d31938e93a1b6d95c1c93cffa353a9cc55eba5e3fc434ff92938da62b6e21f7a4a557169ce03ad7fe829e97228092e192120da6fa0e4bcfd88f8a21ba0c443481544f91428ad20182414b3436949129dc63fff8deffe0f7d4d785f8696632e965f1d8fd662511fefd9d1374a24a945463c16d43af7e5b2c91cfdb3fbb19f95a1a6856924ac7ab6c987b93e99297d221a361123ab9f04e1790cc27253efdf6ace925dc0ad4938743f7a89bc6fb53da2fb6178e156d2ef797c2ddee451666cd7626116cf343965f7405d1c67fc81361feba76c5087db1b8e9a246f4b98663b29f83c7d178dc8c8ab586f0eddc839798fdb703e046d9b13387ad69e28a5c8a19876d289fa57ab25e275c841aee54c3bd5ef91999adb640b5f4f6f7c9a43c0df0f697bf515a1fbdc1ebd5606abab3cf1497a928efb278d389d8e78143fb29c8edc75ec161c914465b357a594f5db0d92c4cf1ede467135eecfacd161763ae45c06d4c3bdcda929f914ce37ae43c61238807cf31cab03b519155da39e5fd8f1c30714a710c7aaa8aa52a712693ce4a455a6f81daae4cd2a773c75dab8e8f86e0ae25a87b16eddca2b3e5e6d43a6ec1a8d82e92b074ad9b3315ccae4b7765982c19d5a6e0f7cb6e5e72ba6656e8fe3641bab2e16044510ddea182ed224566bef2e659f974f7e98d7ebe41e9e44417020104efb31d049e77a8b98fe8dd8d97119fa908007c2876c34edf4885c4236725dd4e473ef1b8fae24d5e063410f7dc747691716d6db7762be0296f55037907f212190b4c4354076e35ed102fb616a0149479b67fba8a3b6cc088abd7190826f20501803d14932531a6677ed10b83c60e238765f3d13c9f8a720b4258fc9efacc7bf3efe97b18b48dea14aef8699ac036d99399cd7daab5a7554eb40cbca31367f2357785778fe7ed7f797d1e3aabe1e0db76ce8282e6b3dd5db2c5ff09c0d260c98eaba6e50e6dc7f3376148121c80b31f966626daa323f2c5d24f8bdb180b37107a152d4b14aaa5c92742f88b5be398f83fffe93f0ad4c74296f0b7191b3af0502e5d030d5deed07446984e65fafb737302e41d018e1111a7626ffefbb8531c9930345a1fef6d8f397095ee0835adc4eae6e01f9c32536f1ee3137a369cb47b9e83f7ee7bec23b6fba2be5a3da7c464333badccc9fbf7bc6d2df0c33444a29d1a760e21cacaa5f21ecd2a669ebc314fb3a3989735352101afbf4d2e67f17f3506709f127be9b0d78fdee4131a1c3af69cf7b59558fcd2ab4825b625c9d9bf0ff5b5f06f2377d19c8df19fef5f3ff952f0f41b633fe15ae9e9b906e5780ef457b8b11032e8c388c6c894c8759d1e16b3edef3905f5c1fd9ef3dbf67c0f3813a76459d49e6e3fce9ef012bb85416870aab9ef03bf6f55fb04f9187e4d8569bdb25cec2b182a8cf2746c68842fbbd795b4c597ac7af7cb55bb11fa0cdea49769cf0b96920b7d6e5bf4e2111cd3835637d6dc595232d153bafc946faddb5c75702ea23328b57c253368be429a5ca1f97aae173d4b84fe5f94b7b96a0a16d61de31d55364ec338bc6617f36828351f4bea1d9f4d8d296becae5524f3f4385dbfaf5da73ecfd57e243de739f82c85f830dd78941500e11949b64107f851ceafdc5e44780dcfb929267b488f37da33e4d6a73aacd87b0b5a71a4ebfb794fd5760908920845ba8851d35083fa8d16d7f6408382def76a86b656f3fa891537fa47b31c7ffecd36973457b3971ae298e92c85e93390456d2883c75ff53e7fe64ccbb1d6a3a1836a3e6c6511002c21f79bf13220079655a10c2bbfd06eab9b73fdc934f980d50941ebc632cbf17e8fc72f1de82808040ffcbff7ffdf7d7cffff83ba06888bfc853566604ed2d864286649d8caaf3431af269da6c8f58ba7dd197ca504a6a86ef4dea12690e3ac777221f846c76157b91851b2060d7cc2e9208545f981a856d6637f6a8c0d6dcd80e3595b6303639a9c29a8adf079ff140236ce05fe2d6e3a6efbdc6700e55c51ad90a43df4915c119347197f291ed6622c450d5d629fb903f4f27a8a07e579b485e742ac15079faac57e3c92b7dbc4abb234752eaa451929ff1a29248f6f5ad317851edde3bb2c8c64ab264ac79f08d2f96fffe191401fd3f6ee03fd228512a3815d29413905c9cecc5c456badc3f4e5023fe47d4a0037dfbf6b386f7d764ee6f25b9630375d10ceb807cb2f53873e8d0488af9cbbc99cee04ecdc45d67b3ca3f06342e64b6f50dfe77f8e25fec25f5e5bfece55ff6f25fb097ff159e16b8fd5e782adbde10bb0e45d3d70e75035de9ed874988e8475d0bd3009dd7efedc4fb7b3e1bd4aff2edbf748afe865c007cb8b678aed677e317afcc358ceb74f620e0903ae16010a0a061e121e010ffbe1f32f87bf1c57f9ba30548067f3e4e9ef6a7e39ffbfed427142ee2adfaf3f101d3df1a2f3e51fad3f15285953f1d2fff6cfba7e335bc6f7ef17c6158f6db3293310424e932ebbfb66b04553174c59ddcca9f8dce7bcf08bea5d7804639448de40fac3a602f1efcfed12e905a183e8fcc6fcfaba1256651df6f6054dbfc74c05bcffe856d2d4930ce679b7a0ce979557fc32d0d42467da4706fb7db71a91923814b980581ed5c04544d8c6916a333c4a7659a64531fad90ebee64cb197f757d2e7bac1936bca5c6c88a73907d4494b81a15156b4572d8ab1eb90e7a4fb16f3bf5763f691967a714660de6e0df3d8d6f7b9569a7217a12a3949b791cadeb615bfa6af2999952ae1a648aa457ae767d7c7d5d0a0a173227b69e81f29503bb146dbd9ac76a5b0774546f777d1229d1178899cb16042b1bfbb2f3da0919b94dd60fd47f21d1a0af38fad1e705feff90449355431532fa49803a61978e29e1f61c0e03fbd0fd5f49d87fbe242c17d5826bd097f4a0868cc5191ac74b1f77ec30ca5f01a6bf132cb9332f0daac287c9de4b107c914943c05fce72e4425f3920d37dac67e1be4d14c16774b7983ad2de545dc257aadcf280799f18e76c5d10c95098b01a2f0b493bf28f22d168d91a5bee63450bf8af855a4d938b8eb66d9f75fccbbefff9ec7b7c2b75446f8fc79cd2e8c57200aea3606601cefe1fdb370b7e02f7cae4efd877c492af52c4bad75d615152b68fcc5ab22f51124ca5bd11fc96a4c1c1d7a9b87c8b7c218b174c588fbcf5be79ee264a6103e42b9895a19ce3df368b5258be285fffa3ec1b51aeeef9e4648d39257a5b1ffa5cbd9deeed97b1ffd3f6fd2f12e47fbf7d339b981141775406d23f0dfe765755e184e82416f0a767c8ffdcf53bf67d601efdb569c8c9b7608b207d6d9639829b5df97033ea5a22f59dad32ceadcfac75ed5cfa46d5683251a0d5d980dc4c11ac24565cc28258527b798d4d028cf297df22d1dca07cf3f623a2ec6f40e91627bd49fa4d7f0a487af8c5a05f58cac52cfa5d8f49b1a98ef71779b201ebc499804e563ddbd0ad144d06a24c5942de002d3da056d678306c9f725c85fbb92192427be528d6aa6715002e1db43f98c8ac88416d632048184a2aa7e4bd4800627927ed4b65b81b3786163f7b36d8d8602351b47980c4521fa99b2f5d55a8db08e1c97717d07b9615950174bb7b8ae276e67c769c8c636accea73e11a8615960a74e37dffd0f8849de356d68b2d0bfad4123a0953a0560f9cbdd2546bc8de0ff16684a45e284fa317cff68120328a86d094038dae4f84441b4ee0ad61b5b58725a066b2d5f2e0f80ce1c54feba835c174ee8b20f6822e408d456e4cf4fb117a2ac6364950d4e26bb7aa82ae34c00e1998b19851233a615eb93c5443c2448d0f7ebeb7fef7e36fb1cc083d68b710b6d3561c75b286bb9b14ecffa329a1603ee64dd21772ad65b408c025ba74026a388a5d8eb071d9936f3fbb68793c3dea7386c0240280e9f15d11b365aae1ce17817155d8d91e9f1de54afbfffd3887422b05dcf17dbe39e26ba71a521de33766184010c6cde87dde827aebcf185c101e8bac8b19a0220dc8d5486842b78e70e7a288a7e76ee0fa72117b8d6502dd90f38de19768c9998849d510520ed83e5dadf1d500646565978d48593aa99501eccc5b287496399b071540ed235eb729a7bee3c77c84e87be9ea5d64618e5e6b20888b84b3f1d5e92955e8e8cc3c8be3b98b0a343f04d4f23e734c73bf324de3f9a21ed72046f31c5f463914500365e2301dddd6dac6236b28a60ae520d036e0db05ba51328c4c70b5247b25f9c14071ac71416a6315f500dda25e3ab4d71a20e0df2735fbde50bf105f727cb102d84feb5516d7890456ca9791f303b224a215b5ac40406da645a8ce266c9f59a22b5da0f4c37e78239fdb19a096b8c751138221a6b9cb05f995013adb7f7a54eb1ee86633e3cf2afd2107d9a95aecadfceb948d2f6b1727809a0189e44ed6f87935b7b1eca47a26fa7b568c087640ada40a73f6ad757cc18dca0f0a86745f3eb7085c3fa09b3f5900ea74953139a3a0f9ebca16edcc245c915a406d5b8395efd8e4a1a2c3e8c69bb55f42106b12f919100f2a264e603027db5cb1b8ca293de341cb33dd3607f65b3e5aef73c1dae5d01871e388f9777ed6f4ba684062671d99505acaadbee469c04f9cc5208b7b428575e0fe33e42068642895ebfadb477c0c6c3ef22a591d03e593ad0ae68c3f76183facc83b5a315cd28f19b4e700dd923c82ba4f3bdaae278c5ea1100bc8304bf9e177036a931d90dcafddcc3a0be037074228aec70efc0f37816e01499bdd460f54673c2d3d54dc31f986a8d64a1f09fefd38810b1e7a45283fb17fed9bc7ba0adc672ea92680dac10fe570f2a05214add50894a2048f8c26886b74c0feaaecc23165497e428aa75926e8bcf1d7631ebd06d44225b24f30ce1ceefbbd15ca7798c2cdd0b926ee01353b2827e6c44dde7accb54eb78a32e5115954f662c03e6402a16c5a47429f2cde2d1e0d23699a700e5402ddc2af0b79732d335d0388d043ac1e6d534e5dbc4a00b5e3b8be69f3a2256b1e119e6624e7e656be994628408dc42cb172452a67fafd787d6b7b011aa581d4e92aa85605d36c1a7758f69c4efc05d96afab55ace7b107d4c30e0785a1aa13272e063f5c8376399b1cfed007463ab5c50ed3a41965f4fdd43c1c9cfe5476a877490069eef58f17c74a3d50064dec2f061785e60ea552ab03e059bd57c847eca90f949689d23f491159d44970ee886b006d9849c2ed4f57dcff26bd99bca146e746f3840ed2d4287bfdc428b86445983d5b926ffc285f1a934605f5fe678ddc2f05234adf33fa0aef4b670ccf91c016a119a569417e6af1672395a218a7084d2f7b32f9101354093988355a2a71fa564212534857245cb9dd1a200b51eb882c118abe6cf61be27ecfbe7f5ddf96be981801a330c2691b9bed5229a98a5644eca049ea357f932a0167ef36ac7623e26307bddf213bd13eb9563791935d0ad6b29d58c6b87b60b01c3edc78f097e4478050d7d80e509cb8d97af816a144dd5e5f45216455c04fd5335b0fe5e635e7447665f1ac4b536aae23886ad73bb3701f313658685ad25d20afa856ed771a7bafcf76f0fb9805a6e88437af9a58a44d193401c56eee6f659429445406df941352ab5e5e4aad57a040f26502e306b1e921e50e3151573a4ef59144999c8c116331025ed1709c002f28c9896534768c4f8ddcb4ed2d338b3a8bd6482cf806e5648cf3959b9bb263f792669d2fb29a50516532f016a5efa0e7028d8c994e5a8acbdb7497a3b46111706c9ff7e1c353109459438456d8217ddd1bef1a7e46da018303fb8943f4fef452e7653ad415f9caa914a2ccf2500452f2abbe3cbe2564c892c8103734d8bab3ad6555bc0fa87e87da24850711da9cdfb955401dd6de481d20050f3cc2bb8565ad28cf142de87f47d1382b99f3cc205c0ff333a6f11a2e42e2278d5bf84f4852dc0ee2de8016a4807b4735aeff29e17d3511e22dc3256ef10a0d403ddf0789c90212d4f92891545730f4d08b72002dd10802a770cbfa59f795a3815880238e1c3c6fae49bbbbd000e53d38220fe5ec953888c97d70e77b0550017fe14b87fa76d03ba5a21656864f7f96abce4816893f1a50b204ed03c568a57387c027532182ce0862d773b5e09a8853e45cbbd761e751d47a125a6bcec77d2cab43ea0fef7e3c5f0aa5ddfe8d65da1940c5eaee7a140a5e63f01e6bf6c13272feecac61983818a42fbb460a4d864328057861c0627e606dcba51f7c051c6571c04569a8d02feb7e7de508d6ad587d0605aee17177a8ec60642b3801a73f1de8ccc38b78315495a2726875b88f38d3001a066d382e9eabb5f1954917b39b16e19841537a9470894ef63299d1aef551dcaf1095234aebf7133dc15af0552487ddf115099b931775ca550b326e914e8e9b90900b548a512498caf671d95b8b982ee9da107396556a780daf87cf708912d9535f1f1f66653cae64cf3072516a0dba5dd4fdd6defc40876cda4645a027e52ed669e31e87fcb15a18d2dfad19ad4d78b23ea15001e0726eb72ed0fc78ccbe1b762394ed913dbd2360a657f16ce24c5ccd1fe5510e02fb59bfff709022cbf2e57372af954f10c936acb64eb23337bcd69d1bf60b77f3e58a2e4980aad54645faf87b7de5a361aef1a1ea551fc8f0e465dadad2e30f93d58b9fcbbdd3db90c4b035f1a1b4ec6cced0d1c2f7218c6840b7b666834a217f18991b5626a5e5c435c44e093f02fe4f697486df952d6b11aaca2422bd34bf9b9a515ff28d8ed550fcdcb2a5c43c72bd5f58b1fb38f73e9455f27fe65dfff7cf6bd99e245d00a2f11f55eec4a0c7299252315f165df1fbf221d9cf599dfe24e8924ca35111c680bb9b745d2c09dadeb2bebd70c2feef9d16f75672563357c2460e4908f73fa2ce655281b1c3da4fcd6f46d951e4362acf3d9d7a7c8578796fd18b8bfc371fb9f94fbbff2c8c4c509046dff6e93c668bece7dbe27d10e75ad150c771ad84d6c7bdbfcbf4e6d61fcd2e8cfcf0cf23e0007ff44577ba2eadfaef64fd964878903ccc142270f169c9914ed730773956ceb7fce509432e5ffd539c9a0bb3b1a0cc88e43ca6ac461fa882d91b20323feecf1188b28d3b4ffe398221e5b964aebfe1010ed379025de7ee43dad0d907928283c197ffe289d80f84883a33bff6134097fa3db2af0b3e9529cd4acb5d49f1f8f0ca8d99eded70c4eb99b4ffb25d26bd71654aeefb4d16ce1150d4f90693ea23a57aeaad33a25d3b72965300e4986886bb1afdaf611137dfb8515fd42fe08f257c4e4f77fe2b9104aef6bae4240000dd3719362d9e028071fad65486a60a3b91fb52eebff22d80fbdfd1fbeaa90fef8202ff993b35971b2d8f5051d16ada6ec5f9861347fc33c879a0c42705ff9d8200ce587a9e81d809cc7ef08e1dd3e1444fb89e822ab25c48fa875dffb067afcaf158e44a4690dd0ddbf5fddf89f89004f8ed6db3e0f8865a7c9f3fbbbc1bb28298c8b7715d573aae676b41dad046196c55009173bef102e5e97941c5bb94ac5fdc942f99faa1b719e91987a76e32892aea606231753a8494b5ce2fd9ec3f9df5ddd08b5fa7ff97605fddf7c7e141543232357324d434b4b53c7bfe3f8ff55dda82ea9aca1292ec7ae2ef1925b999ddd5c81535bde8a55dcd88d4b4de4a58a9c9ba9a99cacabacb926bb9584d4ff3dcf57e4d1e1b49800e38c455682157d7aed4dbedb8d369b4ab8f8390febc1fa5b29050c97036bbee48ed676584b73cbfec0321236b344693b89f854c4d76bc47918fc8cbfda6dff671e8ba0f0bf218f553a50285b988cddadd71d70e8c0cd2f45a97a0c14804811374a28e2c7cbc66b3287864c9e0e9a408a01aabd8c1ed7755530fd6721d9049f5742f83ecc37a002aaea12f3a3d55513c64be4a5ecd96f757c6d32b7d0009cf32afbc5483d1d73b5978b7995cf79ed09ca090ca02aeef8defe336f32c791b403e1fcb4e038b1a0071d8003531b3f23bbd5533d3c80fa918f7390e196b5b10cf032c8fddb127e5af5a726e5ef45937c314a57b1d3db04f6211de174c4cabe9bb80a1b5986639e065b2bb06bcd8e7bac97d889ecc73aa48dff87bdaf0eabaadbd6a7bb439010894d4a77487749a7349b4e41a5a5bb14501a410405290969905290464a404050010504a4ebf7fcce77c279cff95cd7839e73eebddf7e1efee17df6da6bcd35c79c63be638c77084465b3c93d3914f7fa164fb7940f6a610b23fcc493ee7a23be1b5542380438759b3492ce747b57086cf73ef122f1e9decee6ba04f060a3f898d99bf70d6bbd18a57dfa581d36f395140980b945f4d05ace29f944ee754fd173b563665a92db4081e113f79746cf988673adedb063184df27961c6c3c03e4b4da78f2430c6d3b4a72bba89cb86961ac1ba0ba84253c73bca3aaad327ac146b19e4d3eea70e5cc60754c5ab12aab5e7274f6e3afb24313ca4aafe349a980ec469bcbe2a9358df767ecd1cdbd2fcb86a22f2025b2b100712b16312197ef7fc49eea559e373dccfe0973e99025505c1c3add69559249e0673292ba7787abdf1111d806afd87ddeb8baa0a1a646d45e67ad10bf3e577e3aa80ae3bc545ca1a8447da9cc6782f0334296baeb934a50271526dd4697e38caadbae7f1e7793b96de9b05df1907bad6c626ede47499de6b4577af2917407e596f6f940154354c8a972ad2874f6f1deb0ee84824a9a65e70ac06eeff7ac5b1e11bffd630c31a279cb2877c4628413540d793a2e256154a8cb687daf40636d79b69b9afb55c05fc1892fae89c927dbe2a9ec00efa74fc8660d3d47ea06b9af52d573b4c7a4e516924f764bba4764637417c20ce1752c5c14b062b5a67707de4b6741410affc6807e0f9e0b00115f4271f1623d3b4f1d7993f9d4393be0ba88edf784f8861f0d84a1b992771126fd82e7eeed647404c203e2c3f314d4076c307bd84a9aa0775bac828a9ec5b9c20f9f45aee9e7fd5caa0730a7d895828f7983c209329778d9db5b269f9407d6aeb596126be40590a1cf08c0ee7303ace0be59f620d997f316ab9d1d0aae5036c21421446ae85599399914df0900b24b4cfe4c5e2011e94cebd6ffeed97b7415deef72604cdbfbccb647d07e4dca3293196d39405ea7bc6b90e6265966fd8d3af00aad1ba949b89c9fe9fbaf9dfe4f0a4790d4a882ca401fe441fa2bfc69c06e63e0a6e5cbd6a78ac4342a101a000adfef103b7962aaaa99a3361586c4c1bd241aa221067b8a0d5af413bf9b220236c30e2468432bfe597377bdfe2edc5740499b7b73861ef72cd8e46ced316206902c9b86f8c04fb88afcabc43a3daebaeff421f710ffe05989f07debb49e2a1d4c74206fe45372e3a6127f50401a72d42ed241559fd565aaa6b71627163968134fc8d40d779e7a7abf97a1eeb6212f921d38515fa8edb3b6091bbc5a52fcbc1c9bd389d118eb5d72722629c43e46080bbf636ae8a3ae9c5de650ed2691bd242ce8c0175e0fdaebcd0f6e37f7b2ba1d2b7f65026d6c847cd9506e8ea3c6ea28291f1d19ce4b9fc56d77b8eb5c3b07262607ea75dc7964c11c069fa7cbea8d67b27267509160e741d306d40c67d753aa51b60ffe24aab848d5b813d13e0e7376015a4698a77d4d83e4ca9484092635a184c000896d6a1ddcfddc1373ac604fb13cd19931d92118801318e12a6348397d8cb57655d8ff5fbb896a5f01c3880ae5c714ecfabaf7e0c8025c67b70e2f62e3607859903f717c4474d8279b4f571966ecdb2e84e3ebf832127a05a5fd2f084788ce8565e0b3cdf322327af6975c11b585fb2efc69867b846b368139d2ce63e9a15533aae06ba66e8537190dd52d9e3b964e57fc7b5e602f55b0b3460fff2903e2ad99a1380adeb2e3d58ea964f3ffe6c0f749df9e833dbf374944774b1df4bc9edbd56bc54ef63403ce9b5c44551630e328d05fc26e178787d81e75a0730bfb39155b02edc436eb96a3e45d2dc7f9044c6280274a5e30d56e6d1309022ec4736b37deab02b4377b704c8d38876dbdfe7b8f19e90d1789c84c4a8e9e88ad70e705ca124d88c428d57de1f421066b56ba54b91a96106f62786fb9dcee6ce9b49bb8c41d5ab74f8261fc322f580df6f37c80dd54f54295a4ffe7c9ebaef3cff801390a7418d3b3ae5951a1f2c93b0ee79530f7537d0ff13d075e070036f132f5051c7ac6fe226265709566aeb17c07f219b8ba90e4aecb068adb9e911c7646984ebc50110e88f5e6d33485e19111330f5267dea654a703d6b1688533d52c52e7d73403c8aa9e66c1734f6ca0d27d31688537046920f956f2dbc4b91c5a1c299f98a55447b00c89dd8bc937d41f9c9ad4f63e6860641e7d78d0bc2ef00f1428add3e1cf2233a52e648a3fad857f33d2e9aa3d3401ce3fe13b62dbd64cf4a2c629f07fcfe3983d9cf81388e98676ca0991051fda6bc53c4f6c5244bf64914c0bf0a6d663ebf10b3774a2f79be9872e6f381be101bd0d5d1b043a85948cc5f5eb4a2638e3c04fb9a6c411c10471e6a44978b930c98280c506959ceddd56e4aa307db36e1e20c5e52ce219a9bcc4a942ba89dcb4ded02f2d51c38c588d5045ff9300e7a72e13adcdf325d01c2c808cc037a5aad978ce5ab6ff16af9a84a3e70216002fca75ee22efe9b87e3f84c283c5cbb23093a2f78d480ae2ed814d8be0ec4b42ca3702ad7f9f9f8c4e8d6eb409c39f0fdead2d10ba63bd3b191586e4fee8f35457001792a2f6baff2eea9560e89a4d0bd7cdb87f2e0f6db1b00db154888a9f36a5edf41b4ba83fa3a3f096a81462d303f0292df0e3fed7773ecf0a49ebd2a327ccc4a7a1e78ffd47c7d965fda3ec9a7da9716d528aad94c867f00ec37c9bf42cdd8cca44e288fd330076e9365a2ea07f8670d2f912f465beb9ab0d0314f54520546b7afb003fe7fde85dc3b15be9b5158acf86555cff62e1a628a03949b7a5d473827d71ec5b255af8ed034625f939216307e4b0f03a7d449d54c6c0b273663487111c289271f238d44223ae7bf158be131f8adf702ba56602bd270048664b2d794c25f45bb035b11f751ae47309b06aae5e020848aa6f720d4a158b71be804401c707eff3cffe76b4d95486a7f8865f14ced90237949f57cc77c9d8bfef3e3174f465e798fd4cea24114cca29c411e1d31f15c98c2c8d9d8e5dfe73421f910e621b37132c33355437d974ff9f3f8427c3ff06c7c0b149d0155b8820c55f802451492343a061b20d8b1aceee2706db71d76c369a7df3eff01be80e48c7cc399bf7fd6fbff377fff977f5a91f6b402ff54061e4a38ff00e1efd9f64f4bf848195448e16cd9d16e85efb9fdd0762e7d37d64ebaf000fba75f147be26ac74fbf280921dac59f7e51ea57d23fffa2680916733fffa21c755d3ffda2f8b86d813ffda2e8ab8e92ad48af2390c40ed5f9cd7f132d08251b0f4508fc45afef1fffd4efdcc0cf0a55fd5573ebe75d5230f84d7f56e29f247af0350343088b2242d8f848ebf07f46ee0b94983fc4ce8202717d74889d111bd8d9321a83c7706fe3a03ce55d7eae5c5f509d4852def76b730b9051fe6b6e41f5e1497daaa2cd95a1fcacb63c8dc9ea21962bacbf38b700f17f766e019477848822a5026af440e56ffc78591513c159e6918d93d0ef364993b3ba1d2cfb15e2094988ca7f65b466b3153b37b850b882f2ab1b1f060ed6a32ecc8af1d69887a542f08b3913874d1fbdcd958f44c2a60a74493cf8259450e325e449ae5d5f7038456f8d9a4bd0bca0fab3727f901c9dcc9ef7b19b78975c2a23ee6873bfc06f29ff877dfe619f3f6a9f3f3a8f6c9c847e37b74e5b25cdec133db9da779f50c5a6fa57daa7e6464bc6eb9131db9441a2f68cfe682351dc5e3e9b97ec0c301a3b235ec30f56a822a4b937ba3513463e4ec4cc7bc5342d715d927fe31e7989d118b1845793fea7f54db83e89814eaa78548918da44aa4351510a2f6be3f9c33effb0cf1fb5cf1f9d47364e428abfeb0f3739bee6bfb919fcdd9f2f20d4fb95f679f0c00685774f7afbb620e66ba34733535b011215f7d5a81ebd6a5f3bef752f13956dbf14cd53a339abeed6aab47f4248834954419983f89aed4109d6644dc870c3cfeafb30ae7dee511f02d2c874ca14e7898588ebf9faccd93fecf30ffbfc51fbfcd17964e324a402755efdeee7617eefafb44f8bdca953ba57c4e1c6c636ca480cbad97641d347234941e1eb98ac011f741ad178539f285a1a2e3a5f334b96f025cfda793328ffb256478809a68704772ce5ed3d5beeef3ece37876a9cff80433514dd8b06617098c0fda178ca795c1dc2318049a01a8e845a650bbfbaa8f86b179d3f12f6cffef9f184fd1b474f08527a754e5f0d3ff06973bc9218c568fbbbc7d69fd14c493aecae83ae432ead08c28d07bcd4f70768e5021f5ba0529fb2dbbe7c285a96ba26658212f9d96a3600ad82a45b26f9b318ab3bbbccfe83a20f1c950cfeea6b4d177e96d36b49b51a268946292513a91cc88572ba5970d972f15f3dbfffd081f9f5f3db105506d6ebf0656cc23a42cf2bafe25ca99894d4777f21a8eac659e6b725ed911bacea9ec17aed077fa3aa741337e9c9ad0ada868e2043cadeab41db4f5ddb4648efbc567da13e75253a8e5c80672656bf88bb3bc0214b2691c79822b2fb67398dd6a86298c1785a954262b9ccc37ccf7a2e9d763cfa63fdfedf37bf8960d2fcabf6d10942abcfa82f56e315ccbc72fcdda6707fe9157296f9fdd5b0816181b9930443e5d488696269803e86eeaee5613ed967c16731cc5aecdbe86c961b79321e1aad37aa3b103a63311b8754c94483e48ead9a95de1f9d88c17fd6fcbee3eb2d7f2da439f4eb18cdb5834de1dc873a88a37fccefff7df37b452e4b910f473db4fdfed5db9f377c838d2eec04ffcaf9fd8e87c8cd5e8f72cbfc68ed002bda352ce37ac6112d36536c492cc36837dd553af494c622dd7335f0c9acf238941bd3ea95ec5ed6d5e7be507efd32c063f7912cf84c878adfea1f08377e41fdc387d31cb239cc50957811a3a5d482560ab916dd2fdfe2f0b082d80bc8fb2b9385b9b6770a9333d677fc81721f77c67b34b8226c86ef30aa0785184c830d0b1072bec50f3846ac2e15be6bef285c15c0bc9720cba27b0ae88055b7260ff42ef3c6a5519333283ff0bd759e4d12fb5bdc2feee212861f337d809dc8b58f6daff002995f03f9ef6a5b97340a7bf33f650f065919347ce87b96f40ac87fc7cc4e7f0ab7ec8129d5e98c3df1eec6fd5a3e0ae8c498c65773083c76d1f46c1c0a9c3876092457e8047c2ded0f61e61c0cdc84ef3502a6fdb407c9487aaa019d93d3ea7364c9a2a3ce045572f8211b1936f8aa321cdfe2f32143387acd173875b8f8a702bdc2938adefa010d50c24e6e86615e58bb5a822fa7343a173dafdd8f0bf821cad868b6561c430e26cdcee557c27d85dcc4d4019d32724a590ad7afdcd524b99b1a6ccf50b0de4fe1020980be9b84079b6bd35d2bf535e7f18d462f1bd038013a6d34a355f6e71ff8f57e1c69b3bb6c78d08efa0e0ee005d35c660d62893e885e2a0d7c7464ad37b45fb97f8b3b17ba33dff15b6189da78c29ea011e34cffba10a84c0a19f149c6160880b7a3ddeb794ecbfec4b724e804b8ff0fa1f35776eeea66ee60116d6f1499af1c5800e4fb5481807608351f995f2de50039cbb536b2fb5780e6407a1a25ac664971c39a169df55abe98cd765f8b38bfc58f7b11f38d942222d884072c3b781770d822e580fce5f30af4e71ca227c286e8ad9d902f74dfeec41d01ea4067d5d2de9921a40d16648ade544d711d8f454301827b5b4dcacbe676933df472c5e9eb3b8f1e8533f181f983157336924d4eb9646e41536c1aefcc3177e780fce8f321fb5a5d61e20fd3390eac99871336227c6701fbb58dba713b752235aff5a98e43a4d2853bbb0b440bc03a98958e80bb299b68b481fbce4b2c59039d641a28176410fd6012c767c31194f38237657ac6f8e9581a901f5b1471ce0f85bb27d3f3adb66dd67e91a17b0c3d90bfd610f51a2f7670c8997f7eb2d32ef75e74683637d06732194f62a670f81551d06d4fab59322a6e6da21ca03e6263fa1a2cdac64a2ff56ea1a5f4807eb5eede2240fea0ac65b2b53310794d16ecd54f0c8ea5eb7ca4037400bdf4e9bd96b16028b156631f5db4b6b7961ced81cc0637a44963b2f5f31dede9f88f82d626be20706a01e3bb3696abeb24b2295849f5d962f312b57ac6037f207fdbbf9468f8f52cea9603dd6d4edb35b13d17b77c607e593974c6b3245a0b195a5665e4a1f225f8675a02f54382fbf73db4f914b3bcab8b9fb17e64ea8a796a00ac7f75c6d694069cc3a2c2e304733adc2ce1115caf81bd7c6979658a3d1adeecfd34c2e592d2b246e4940280b73c5885658ab975d09c9e231c15bb8d7f67bed3ec5bbc147f70a4fc7a86114b75ae0f913855ce00e1abae6f716101aae85cc5dbca57df555fe495787fe37e771550bf45abdb8d313c4087f3c27f8fc066fe8ba5f047bb9a6ff1e7510623242457c7c7e68b389af4069b7bb6da81e097a80bda6cb86f66208ff1b57991c7b4655ce7e9817d58f7e00aee952ade614bcf9a950af328a940996da0feac2fbb5445f2e1db6cfb50822d2d5c9d4ef7e27a60fd1fb4fbfcbe31f7f24b9b687c1ea3fd6702988cfdc4dfe242f38e1a040a1a04646b86e2b76cf51cd0c29780fa9889a28c4c06220ae94f973f5e4c239f5354666f07ea6b6c5893591b9b9c1de7cdb3bb3cbb9e6351d3f93c00d6af6bbd8889bce4979e36a67550c97c557b5f560ca4c786b691b7f8bfcabe4cbfdf67b1ce709ab23c400ce417bf9123420b123b8873e7d5d0e58d36c93a104b0674b058aea1e1cd85bde27c2b4d416b6dd530d9172d08a4cf16b4b0ec389c4aa9661a9f8f622e2984bb1e1900f555cf46a471f0a9cb5ac6dc1f7032bef6f01c36a001d6cf2ffada638c564d6efdacda72b2e6cec26e1929407e3bc215d3b9d55158c2dd9559b162e537c33a6fe5013d5fab64abf878148c47effa6af03af4e3a2dddbe580f4e20016f4969c655ba566c702893e36034c7ed34740f282c55471893ee797694cae2badb51b08c354455e40fdc2be5eabf9e70b68af4c9064aee3714b5db650a9007418b5e4e8d03b32c51b0f2372f978a8aebea34c2b02ea5326ee4d5adeefcf48c71febde8d53f40d9d8ed14c009c9d04ded48d0ff6ae6de3d64cbc2b82958b87a4009fda68dcb7226043ae74786767e22dbc6dd86eda15d0c97ba66bf8da69525557b9b79d6635b3f6f6bd8b28c0fceb8d7d2057758b09cefebee0b048fcb2ccd870bbd0b778273ae56dd99bf7b9121192320928655eb3aeba007ca98fa5cd3afdbb4f93bd5b9c31169eaee572935d409fe332cb66fafd71218a754e17d452f590cd9b8bd731001ec321af2d57155b98a6e7d6d1e5479c84f225c540fd974de9ea68fededc55e180b23dc9b2ccb7eab3caf3c0f895a35adc36dbadb23c52cb29321d886a7a5f0bf43a1e765d2128d9cfaba0882f2fd25eb1cef0764104deaf32aff8a8432ab113b138e2b139c60c95e4a3aa2d809c1e42745aa8949d64c2e37850cb71cbe84b1606906b57f2659a6d598b607408f7a02862e4eac34a3b03c0be3496cba5662dde48dd2c0960a81b4c7bd9e7439907bcdf42c4cf27177750bd97d52fb126e9e94da95ab600eb5370e2cb4c0aa6f48a0066b5af3d81aa36718b6dc0feae58509af15c132f37d24e3b8ec6784cb9ac13d0c1ba67044735e56f473ec75eabb794afe065716e166826a239fd99919fc113f3d538e94a49b6c608fd33f10380cba3499d1d311ffc2a3bd6b388cb3334db7b4c0de8909e583a442e10a4f50e9950221ca9297df6cd1e02d2cf5fc9385a072f3235d5c7610763e6989a6e461102f58dc1700752e6052ff108abe4b4818b2a261bb16d807f1a135ee0ca28fa1a87738af7d38d1bd74873098801ff7e647cac4ef4b07a2d8e5d914049bb3796dae029d08c635668144ece724f8151d3b1368e63d5fa48ed2e60bf115a9bed698a2a8ee65f287c848b439910842981f74ffc4e85583b87d49fe5d13236826014ebe3fd197ca4bd9b1188c79a2cc4087f6bb7653d25bf0938eef27b83e9590d5ade1ff60712eeeb612c8f93ac064185d6a0a3337f5266c90f0be13418af952544b90bec45af04647a83939f72bb3331cb295cfeace491a5f200f2e08ab48f6810811c1790df1588f0974bff6bfb956beb6a68a81afe68b7f27f71bf720705d77e9d95c42081801754c377d35730bd75f67f3f64f5db70b6f60486e01f45851e57df17ffa1f4db7fed7fff9a999b3973c636c0f1a5f07bb64713ad4843e1445c99ef2511fe72dd9ff92b2cc6e704ce152120b40e861b6d11f848fe49282574354d1b7d1f65f35bed945fd319fe62a22d793c8ebc5476fb9ca5f40ede5067276b100a8ed2cca2bd427a0e471dd29a84c9cc85e9a690839464f259d20ff4dfe184fe4e3be5228b1cb1b1e00bc7b2ad2bd51d2daaa2ecacde8fceb664fc6b3bc3ff077efee768a7683938eb18c9c82aeb095d17d6d173d4f454d717f6d1f3d2e2555410f076d3d1e4d3b9266beba26e2368a4e5f11399cb332ea23fa3333c9fa09035bf005c90df4a186e2128c46b6361c32dc06d69cd0be7e5e5e112845bd85859f10b72c12de142fcc2bc42bc4256c25c7cfc16c2c2fc9642d602fc677a7e34f80d67116eae5f363fa45ac39d229edd5666ad184174f0a4b5121e53b94f6f2d7007b3e4c560760a2ea73a52c15d7aa709fa2186b5e684ae67fca2fd84ee8385f27696e87a2af9af97375cff7bda32bf845b6d85c307d56f2dc48d7e5ada7213da9bc5db480458685a8540d4634455c6435d0a8d64d1b9ed21855a809b21a51c2d8b64a7d7f5475b698a9c921df59e1404345269e9cbed51581932a916579fcb893d63a6e39f02c2e08bd1f874c88c9a2d9bcf31d1265c5bada5e5418d7bcae5c54a92a73a7b1f36940e3fea625579af06d8008bd3f929db5c2da49cc618d761b502062c42a37480db7990cb9655764bf77cebcc8d62178137e79074ee02bee5c90bb10c662be337f6dc5753521d46c374cba981c43f492a74e2de55f3a7ae923676812ca7fe68178201ee54bff7934d0a7fcbf59bbbb7aa294c3ff7e2c40f02671722be0b624d4a3538373765fd8cf84ba5d26bde307f8b8f9af00dbae54e4b3e4cd06499b897e9453c4b00f8a60845a401921d535f2f26a016193a06e73e271d04b41bc65be3afbe085defec2d44526f39d026ef542900de7f80f3a923816c2bb2d9f56835ed37e9e3ea09d2808b1aad87bd9590689cb8769f274963f9e2eb218d2780f6c55473688063c16bef7312412c3012cbbce65e232680db4b39e5ce0e44f3512c8a8df2decf7dd0ce430cc8e48948df8d1f43988d85736a445d983f7cfc52750c8875a587cd3f7cbefe518ccb2876bcf21566bda439baf1b7f8403739ff9a5d7a18792c5ef26485cf7cb34a0e505a9bef4c59dbdc46a647fd0947084ee8578c39b74203704f97e46b9570ce25f4eea9e1a3b24ce05c7959093402149c45bb57e7f6a845ffd8e461eac2c9a5f9615660fc0492096f0da2596e33217c6e8ee9465f94a1e801e62f313549524b0587c77bfc8a21698939b228b35da047c6d347e7112787d830651fb530cebf5ec81d6ccbd6fe1677bcc725977754717deefc8956a2a9787bdb0a2f70f67a4b7c50f6a0665be0895a752a9c66b7d1e4dd0a90eb11aa5094affdb8ad82a919bedc7b94ca4fc06a0dd81f4c2e5c946edc6660beeb15e355af09433e4d2ce06c39597107f3fcd3dc21eb8f44de998fbbae92e67ca9fd1657ef0b4b4b7815c39f122c51c38a2125f4404b1be0beca113cfc4290dedb7bbf310cdc3e4c39be55d64bf72d4ef1d504e1010517437cf7cbf1b695505f1fe11d6067c35026d0b5cabeaf6527dac17cc2ae9c3efae03cc05dc653c55e312a8ab2e76b76b3fc6c28257b627a1bb0ffdda1c74854ddce6e4e8f46a688c51d3f6fef23647c8be78c047596684eccee284586069fa8ca7864637103eb4f4ddeedad2cb3d787aec4cff14231f86f715bad03e38bd16c3edba8d83cd9e17f5a94d0777fb06e17f8fee029f68e4d9e40d78da163815cdf763a1b1c6d40bb62c5d0cfb7f50d87693c6170e73b87674aeffcbe1c0367d71b89dd4c1faebf938ebeed2790fae92beb2603a0ad92b7621f1f766138efd9a3db6c182e97bd85c646016e3d51f3ab7cfa533d8b9efd186e2dab711bd4b231a0c78156c95dde7102ffe5c37282d188f03021a327ac0037e424ce18cd628713cb537f5ca1ddf992e734a908b03f93f1e7a4a91f18146f9e3b7f6ee82142aa21a6df30f0fe0e6bb8df38709aeb63b38dde537aaa5f61ef0870b7b5ae22e316ea0fbca4d025ad67d06ea2583f1305342acdee60223caeb91d50eda9bf4700d7c1a567f300366a0a0bce2d1bac5702bc6a03e24df41346239cb180f64dc908ae4396e4a8a56a1b5206d645d93a4bbf3da04c993f87b300de94f9e5d11af3a30544cca74f29db000d765b436a75f4263eec086745bbb0d095cdd2d3b780867ddbe20337af9b81ca2ec99be6e2f226cfbc2c1780f5ff965690d742efcb1ecc0ed4a77bace66203d9b500b78fe83397efc740877ce770b5acf5d9eb3dc9752ba0474feb508ffcb91966cca294e238796734ebf5c0cf803bb583ec7ba2a7d6b012866bd9958031a5f0e8f303607f36d7659def8427d85cbf3e246eb27fd324ded704d03ef97a603c212bb169c331b3c0246170c7a795d505e8f9d88dde36a4ee3b7e0ef5b1a8de694fadf94b4232207644e8a685d4c7375a6881988bfe66fefdbdf16e56405a7531614518ceaecfb36978c3b6cd537f807cd24aff5bbc3ebe7a77dcfdf38de1151a056f933eae870b2880f6916175505e8cc215a2acc9467e869cfe8ffdcb36c0fbafa512af7af96280775c31760473f0c2fc588e1bd00344e271c3fe662ea1bdb235128c197104c1634117882d0ade2c2d0849ee93796e62bf44e596a3c89061083c1f59926a724a2ca5bfed8abfbff9d8bafbd4f30120b698147332573d2f2742896f1d7e8b208eeab8f32ed06381c5abfe9dadcbe2b156ca65bf56bbbc2a3c1156a0474cb907711302daeaee974c0e86d1e4a2c8ad8e75c0fe2813e7c9d37937dbf827e57d0bd4c78bc3a56b80f58b5376edc564396d4344eaab9a78d6b0a5a754f580365369fde4aaea9328e3c2f39899419c8c2d9916f180ff9294ecc380e65ca281a7da508d21b1ab3875bb10d02edb4847e538e0bac134fa75e3fd39fe234d6fe148507bafbd8e7d4bc7e9fa57eda75eb3096d9c98346480b68bb2715c6bf806679b3d328d0011b785f7e29e09c0fd27b74af1045855c74d9a4585ed665732b509df1105f8ac134f9c261caa599f5aced7c37970ab24c244809b9e7a7c574ae89ed2c022cd02b2674f5080da84c8cd6f71e397140b706d1576343a9deb1e61e22d3239d855406c4e5491268883ae60e3f633e58f433dbb0f3d8b80f75f802dafb350bf97fc069725bce2beb89f24be10b0fec90f85a5cc712cfb26c7f06f9a1b72e930bd8e077a18390c5e769f8f09b2ac7d92b110e6714f90eb1d2eb0bf6f0b4c198f79558787592537b5dd4de5482c2f03f6574caece2c7c13b3bed5cc8a72045a6cfca7bb9c00a74655a6b4c246d21a6782b8bb3b8e2fde5eceb603bcff7eb314799c814c27b2fe972ac85b6dd22fde83dc6eaef140fca7b7658db27a813b5cbd3468681a7bc0fefc41f3dcce2c1f8a1496a25d8efe8cd502111611c05d9b449d43ce2a8b2b2c1e78572249d4b0a4d4ef05e43650c95a9657619185a73bcd254a46887127de4b063a1e748a575ef8f8e6d8976c2567b46d3980531e65301d691f79fc2f24ce9f75647e9108ae76c3d855037959abfc4a5c33028e38325559be4491e17ac9aa34bf9703d333c8f1d49ecf9cb7da893b842d7b3d676c797f84c8d16c0e2f94a3181e19c9e113e96c7e159b5df834f13f9bc8f93f2f828ba1e4e200b7f284ffb38d09fe46e458c873c365bd75ae097ae8f3485b3b727b1818a82af00bc9d85e9781cb08aa6b3bcbcae8bbaaf13b5ab9c3adfe73c6574a02971901693093b062c34402ef3c296d3bae659e8aa4e58e099e33199d2d751c5e63d9be7ec0d784380c9db89cc7fda1bcf8227c14d3f72217d44db12eb7ec845041062ff6110903ff64d4ccaffaeb10519c5b9c5e9c915dba0f653516423c3c9682425c16706121610138dc864fd8925b50584898cf82cfc6d2c6d2864bc0026ecdc505e717e086730b58f1f3d870c1e11682fcfc16fc70383f142f8454133bccedf6c4a2a184e291b900ffd68e9bd6e55b33355c8bc98a52b7f12e295ea1a42c50cc7c3c695e51c6e96ad512dd55587b97a920e4b54491f1c08eedb504cd56a47da49be17cab2d3313762178e6d8217cebdc7548d64fee6a4c9c91c939631b1744283576eb486e6ba04c51fa1f906b10b8f4f77f03bbf6fbb780bef47d5cfc3b953dbf0df9ef7ed54acecb24ea74f53bdff745599ee4f8ddef9b8bd38f73dd01d81ce52c8965ea6b92bc432c191ea9a8388ab767cd79bf77fb364e6cf033d8330a112d5e34443071f0cfc1c46be6b51db4392664dfbd20fa4deaefce37f90b02dffdfecad87775c990a41161dffdfe87177510c315f4e7be0dfd69a158bb480c7f1f668acd681e4c6a5b336e3b9c27d91a2d11f7b9f3f8de77ef29a0e17eeb9e6ae09fb4ebfefe72b4a4e11d30c443b4cab01ab9ac478843d196c65edfbb1ca5c7db57bf2e8aa7fd9b665018f6ec98f83fb8d9eedc09556bde77fb7831c8b1d3a277cde2a5cf7ff77de15b4c3cf89ddffa36ce2692f82f8ab3b91d74bdccdf278d2548c6e61d8e09a0c0f10a50d1109270db1f223024c62419fd11f74c494e4468f7a108a3e252a253e09a54ea35693acb3fe26cff47e26c7a56760256f06b3e0636ca7c36701d036d2f3715597be91b56aad276ae2aaaeaee2edc961e8a8646d63a6ac2ffabe26cc2828216c23c8256702e3e2e2b0b2e1e5e1b2e3e412e616e1b6e016b416b2bb8b5050faf80b0202faf05979090900d9f908530b795b590b5b505379f059f00d77f769c2d1ac31efeecd331638758851101ac3a008be8c6486f4258aaebc0c6abf39d8a33ce4129952fc29459b3ecc616ef5a2d1a9d5782058c3dc87e6045b97a22ab67a049f9df8ab3a1a3ff82389b5c9dfb2ca2b81d055b9fdba246bd0e0c39680ee8856b284a76c2a57360dac28a4f6b03578e19f72b02049c900324c6b224cfd5edc0dffa78ddaf71b8a0c22d0ef0bc8d646584c2ebfa14e7bdb53e283ef618c7577ef32dee799078a38570613cef69fd4ec141cc891d9d0d90039f30a9a49fcd233bbccf48c19320343fa559fe1ae071f4f7ab46f5e3d95fd24b53b3ae6d47a25abe940234743b13e32a2c058e36da6f72df8aa6313f36cfea3afc16bfb698a873ba6f46a3ea6a1cfb9a9061cfb1dd09e8657a218d2ce65d4549ca9a2c39bf68de8e83f0b20be059e5c8852a0b6ac7d12db923f6d8f4571d22f95202e36f48934a9ffc6ec99ba5473aa6b324ffb07b4407a805967c8916c03b1a895150512671f3fd43620a7302802734499b582c7ce11917dac6adc855ce41fa40470fd82eac2fbb1ba1ddebb461c9336d17cbd84ec3c5e0067278798c2fb397cb8fbf8e686fcf789883cdefe6a40d04ad9dd31dd90967cb479f5cc74cb6c1defff815df1f985fded1d3a44c5b4b6cd886026f5cce25e87dca5b01e288adc4bb69e46f2adfcc93b94f2727d13652cd6a007e083b09972e2ba7b0eeb96cac3b6c8fbd6e4e7e1a06d25250aed8f721314f48e96f051d646fc0a74f52ab8138437b147d5b6397ebd0f0397d07d237d641048e928067acd153c875b7d9ee9a65de9d5407752fe5f2193ca057321935df7397ce61f7b99cf6ae017317ac10f63220cea6770177fbde959c2d1ac588dd9d17952f0959b405bfc5e954157425cbea67c44b77de8c7ea258cdaf6801e23c2fd509a2d390364c743bead499ab790c0fdd8c008d75e3c34592feab5df979a37b1e0735322e1aef74009e4b0c83f2433a4166506710a67c21c9454d618364c03ed06c390ae46cd81e45a49f2aca318516b0ba69019e3572f95b6b0d2c970b043cd30a465faec8d26ce902278b1b41cfbae8e7b7b30b6e623b626eadf8aeef3b03aeee6aa9e3eb8223e2cbb5dc5a029889b222f75533011e58e5f090fa03b9af4ebcefe489c9c7bb375514592f7c8b1fa647fa73d4b1afacab04199f3cf3172e358e064e0eee65c32b66c339c9c2f614b4091b57af06c7f9011976d177279abbd9ad027594101b6eb6f4135c66120272340b046fe6320bc963d2840dbb2b19df72aa31d70778be91c8d00f355225dc2889c752c114c178b0ec74a097740de75d4429a18f4f99cbb3a8fad783965709d9803872e8715a8116460559efe5f1828ca29df8f5f2af803b17add2f8bc9538cf32cab2a2e933613abf706d3ed8c3669ceaca23b5742f85cd69b539b3541baf6a1980a7acd3f9d47e787de57927f2fbc6a268db944e694cc03566937b50ee4e53f9f46d6bce853aa601b597092e004ffc5c7b4b15f5ab7e4bc2db3a76b6a726773447918193ab52a98abb5758e6734ca57977c9b6dd2cacb975a039604d86c6065fd271e96ba4995d1c0cd7e7462ce4c0c9d1cc2c99718d3a245236c0c6a3dfba750bbde308e811d1283497b7ce5bb53051bb388758c2dff0e286105063f2aaa3085b093b024d29cc787f3eb3f23069f50590836fa8f1ea111e5d92ed4724ec4bc40f8313521f8e023d783a3a5c6453a93f77a918ed785b5b5e35f0f2fd0cc409b2e6320a7cde601914a83fe332289f3b0e411a0634ccbd854fb2f30fd412fbae2d0dd415c89a8822bb007920bc1aa1a739e974d3cd3561683a251c66d8b485c05153b3cd24151d712bd7de162e596c9e51e3eb1409ec1fdc54ca63e84baa629eb79e4cd3b5f6523c51ee016a841e9518d5de684c0e4db1155869109c47700e4d016a0004734b892d8ca89f582de211cbed8aa355c51b03bd909f5ad6877629f54c69be8ae94e97b9ee42217e0a68e0e31ed98c0f711358ebe0660910c7d8ac2db4e700356e63f1182d1febf75c6bda8733ade5a489bc443e023534f86c772b736f46d8b7b8cb459beea1152166de04e26014fc39cfb47aad7232fb3ea3db3ecab2b8f448300be041c8a9fb7184076687c61a0d9f2e5a5fb4cc9b01246ac2503455460fa526cb0ede3df3c4149697ab93bef62d5e35fda463bff7feedc3275e1b17454c3f69f9fbef7e8bcb944ca27529cdab89e8d706785599e4103f120104745e75a54cda675512b6f4d2443e5cb2e3320efe00cc9ffd511fc6cbdc2f0499a9fbddab6b6c6e2a65c0813870207338bae46453ff015268e0fb143d2afff95a204e53dd85c1ce8b8e119bfa26ce3b8d14719ef88300506332b21ef8e608d3b2b0dcc1d81a5e584cfcb0dc0cf87e967031054ef8c4968fdcd7e58577cfee155808007930aa13f1dc5cdeab9534d3c45fc83a309fd7a73b03f333f76983daab9b4e03d76891b9b3abdb68dbfb9a7400ff69ae5cb84b42d4a12b7669da6a34bbc9fe8a3460df99cdbb077aaa6477eefaa04e3035e332e4f46e035a0b1f384f452e04a6d8b92d95b6d773abb9a9fbf000fe49edabab3457cc82852d67084767e6bcec05789701aae5ebf341cce73504eec7f179e73fc4cd3dcf548c3e078cafad1fc7bc6c5d303c7832c504dbd2546c310c4845dfe84c25c54027a88f3d2ece8db07bf898cf34970df09df56fb1653d277a9346dc73ef26bbffc950ff65e0bc5c3c3bcab3a36b1bafb2781275134f44bf62c51ac813425ae9e26cc04f2f481bac5a2b45b22c76cce602f60742028c94fc88edfeeb1aa5a60beeeb083394e4401cdac53551967e97ca084fd2053357a59a8fee8b1d307fe925cbcd5ffa90dec115b3eb8ae5767dcba2850ae459c0e5dd87c4482be3a8dfd58ea7f005e38e2b8c02e343e4316ffaf5deabb515c39152f5c34fe397d2c0b6a34ba1ba2eeb39f5c3c28539aa4ad692e4567302401c9efd53f3e56073dea6255a77ad0a16a38074a314a0c78b97b5d378fb1b66ac2598fba956bd38866c550c60bfc2e23b4cb2a7e2e9ddd1915fa224e52216768b801abe0d7bfb5b2374daedeae97105b1309a2bf1173dc1ece7c596fba57596fe1d2d89ef771899ea3f63e90312f2867d4425f4fd125477373f0e5b4578cd3df6270204241df4945abfea5412d31ad75177d13c8abede9b5384b48fe215814c181ba7e76efd274efe42f2cb3a14eb7cd5cc8f104717083901c41f6a7c6b665e9992c81ddf1ad7175b842947abc78a5cfe77da9447e581f963ede9493a6fb2eb1a0954c7bfcf025e82d2b6842a4b90f2573a1feaad404ab979e1ab156d63a8ea548aae14776e1883a15e66c7e31db74199ca3b1277707b34b1ad10c9879d26b9518c1c2bb8383279090918877277f5fd29ffbd4c01e2199902c4b3c0d0e30fc594215c5c793b1cdadc987cadecce8e31eb3909381123ed29015e43ea89280659df97ec2b10f777f16ce3470f8c0fd2ba37ee789a08ff71a81faaf84c69322eaaeeddb3ccafffc6fc942aee7d2aaca1272bf3f99ab8204ed67cd6e91ca1f810c5fb205b2dd29e12fe5c7a2a35917b87e60b639c2f4c33db9dddfc951419b19baa1118d8e7a4a9ccde19b59f71fe4979a56dfb7cc0be42d3d814ac6e5ffffcdd024dc253c7c69a5cceb06c3bf2493b14f72b9edd0eb6c5b44bee171e54e6defcb43711e637597f6577d74bb12831b4c40c3a10b8a711f897e07ead2ca66921901530d7dcf1d568ddbabf7bba0e115765fd907899c4e6f7deb5179a42b54cf3da442bd23e52df9f638b47d17548ad341388386763d49010fc597817a7e633aad0e3fa9fd66514a48d93e3dfaebfaa0cd79b735f3acdc58f8498875001b6c01f7929d897ac32b58588bf16657909f6ec306a0fde3b05526dbe7e9ab5332236533da4f46d8ed1b12c8e2278f35dc996d4db93ccdfffc5fc8a5fcce8a259585d73f5f8e74b19f01dae3bbbc1af71c0bdacec2c5c6c7f2456162b4f789f05ebfb76ca720a65e7a7101fa8bb18c43d597d5bd2bc1ec5f389be7eb269eaea1e09e36eb0f743312751f7d66ff360fe5c63f88bf2606c7735d56b8715931f2abfc7d2355a7fbff4593faa518856f859ce32aef45dbd972e15ee719159febc45699f3ec0b54b547e24d0c22a5b9515c87fb9ad7a2888e1ed1ebfb07c400dfb7f76a0e58f3c1835b8a7859a8587e33ff9fd6f022dca52021e701e6b1703191569216f3b6e1d3d07651d7d5d1d59212d05033d690f2fb890a080aea6808b2af77fcef84a4dac743193718a26b00772c8eddc9db2a85711a3c24cc61d7a6457a606bb7b3ed3b821df892963d396f92d9d1525fddc7d56c5774781a3b81634863ccff51f5d28fb6f6c7f9a8121487d41a1589bcd8d7f1fa8a55e6690aec572bf41fa9c27fa238b2c43cbfcd8c9f7433b5204ad487b6a81e8217844ceb5b29fd7ef209ecd05463cfc1797a0fe165b41acfc05b115bd6ace873135a8484543c27c97bbb3a6e5caa4811a954fc48bb1e43678f10ac5046a245b123a418c4140acf591d5fc73c77c1af6f76e7998e6448182051aa0fe46f2ca63d173d7af61491acb327a342f6dab7ac50039e6b59f6fc951d45c91a50a17141cfe88f3aca4a11438fb118bd5383f1b95d3bd9c6fb941f9e5a6c37c74107076cd79cc5da86bb3ed727d504475c998e32d79c96b807ba8aa105cbdb183f75199ffc2367d0be7f29522f86de0e8a860c0441e175e5dd1a78bfd9e1321efc47e11e0ee7645e7663df24a8b68a928b7ed39c8f1e4973f014969ecfd7707e7326e7315bf0a9b67535571bfc5d40364f5f0853062197b07efde8ed8ce4c7ae2bb57b7bb04700349f6a8c31f2414ac79c72350c82f5a98e6613c036a5816086bcc30747768233f6beadf649c8936bd1700e8fc3d2672dc2478adface14e99e85529dc66b73e365809ba37995c5dde37cd3bcba8663d5af0da7cf4c846df95bfc6369ad325363e2d3c96749d893efcd175554c400ee7c503d9baff6b276291dfc4a1cc7e606ea97f8ab40ece98d47cc78fcecdde6f3bdf1fe01859be1ada643406ce0ba0f3b9edae6726b14cae26af982a93fbc04d4ff0919bcd566ba732e8b50c6abd4b7ed65f7450951e080ec371327bc2a16c9ec431dc0c25b3adfc1b2610df44f3f11c2bc631fa673ad8e8ae0b007267baa60ba07d4e8a837762985a7a886c34d6df2a573ca7da35dd580bafd0b8e32080f58dc67b3930fc9af4df5168e764d01fa236e9a080b4327b9ba0f998b5ea8798d590f7f4d020e58b76dc85efb5f99549a6be1a9663f79c4d979cf05e02eadc574166d565f2964ad5df15eb01ca18be101d34a8cf6336e8d5deb122097e7f4cf43fcbce58ada05e867f0bf139992e3ce6856b04ba3429d8ac608962e036a6cba31520848e687313eed92d6edaeaac13abb7d80df87795694a5327cd8b60f0ad87b7a811fe1deb004a0ff3018bd55c8efe17291bf6344617ef76adf5afa6d4280dbec5771304fe52dce2f683e606e6b3cf00a32066a8c0625d557163add7575b8b53dfa1d43de967bbf04de4f5e30dd15e6f0be2b33de8e1c476ca4b1450c57f080f9bbe3171c53f53cb565ffb1bf78c914ae7cb329b0b36ac57f31b72a94f83afe6c57ea2511a54d43582d60df81c5b7891562ee714edefb607838cb13182fcb02d4f0503772a68ef10fcddf37cf94c08d50bec45f800ac4ce94e9c9d7995447b999a396d3190b33acf57099811a96f81272ad91bd9e297aed32ef1ca47ef7131d5821685f3dc8ebca871539f417685f6f927e299d9f055c3cc7194df4bc9c11576941862fc785197ae38caf80207fa55ea9c19023176d10a69bed327be763b35a4620369a16f7e9bcdbba08814178830eb5b6741cc366620fc0ede53ed92e5a696ffb3451ef9218c15ecccaf604581fc3962f762a54e90717cbdff2ae5f28b67d31f11ce036dfec1f992c0ea9bba4a3105fdd9cae904027e60076c78bd57b0156064947f3d9e46e4dac983dfa6bde803ed5862fd984a118f66d54cde003bdc278e7c1172740d6de2d3b53340ad12d442dd408c684f7a5b5735776017db6db6afc7786c48b2b3aa41f37251513e9eced5d059e7fb0b8298db58858669c57d0ef4d994c694bdd2ba006c2f99ec24a46773bf336c308b348f4484153781f10dbbfecae1c20842d7f65f9b66f1563cbc5c85e2c64e0602a257a95095dd7e31e261d55e4985379ed070d0920ab05a791e94e04aec74e737a38dff4277f9c88d47e607e0fe7e3c9e8526a582206d85d53ea4753c1ed9d05f6974b786f19de131379ccbf28663eafe5af1b6dd70364858e6a22d80f1524fb1354dbd1d33547b41721e601fa5cac0d03fdf14d766d6c4a6c9794cb63677a1c4681f5676161d5d74e370097ffcb95e462a55b854ebc0b407fe7d482074e84f94841574fee8c6d7e5227a739770d88ede8882490d1558b3b66bd5a8e4638f732e022f321e05f642b2b060cb3db8daa6785c8a40e896505e184030743cd3cc1a19c872cc8c265cb978a5fe89d4f443f01623f06aa0a4604ec9e74ec2cd9338a95b03558ca1be0fe4e2624dcddd2284c51cd532b6d9347dc28b5dc801aa1c52b5198920e8cf050946b6a6a58bc248759a0fe98c5e78b63834a36792bd7f91e66657cdc12bce607acff5fc5efbab1d7c579608bca7ba086aa7225adc181fee10c1f152e2ce3e4173e1f65239cbde7904ad54f00f827b70cfc29d30db2f457bb22fab0d595cab404fd003c680f4b3af0f8743978998a76b11a1b8506a903587f89f98ce9e5f78faf88567946d25f40916e6f7f03dc9f8dae10e95d27f2911cb88ccf7b7febc9aa32be1d60ff38b264412b64d257ef424cf0417ef7f1eabb0c20f67df316befc07d14aa6a7016a83bb4ec6fbeccfc781135a17cbac1531328c7e5ef39d5c786874f72d7b1580094012e67cfa71628881397cfaab898b5feab8bf3c50434490d226e16714fd950556c9d838cabc71477219585f5e5eb4a09e7f4fe81328f93ae04893f225cf3d6ce0f9781b0a103fcd2e7cdd7b75d1a9da636d3b6b5308d85fd2d2b3c650651152538a68c869ee3d934ecd03cf3aef67de12e8b3108861d0a6f1a0542c8770b07d02d657dd2617d93532256f55c7af32bdd9ae93595f8f81d8026bfd67f6cd4fdeb736880f86c77973a3837a5181fef4bd199526464438cb662512c82efbe72c3b161e0335a8a3c6dc2c4f4e5182f2b514d483168bd1e8af190359d59b5d1b0ceb078d649993f849599bb62ef8c7f781d8ea976b1bc806fc35c74b834fa470c895091b2c3280fd176edfb0803df2440549e64d03a3d32b76f4f7b6807fc784e8eede026f8a3a30dca352bd651e3ff8001dd07fda5be8d2a5a38bf1ebe2754fba84f0aebe869610d8df12de7823322cf5d29b5d980a6efc1050926af46ce05b7c9cba96c4a0a5c5e56d3baa7b26d689939dfd45a0468ba5e4b370fb351a7952a3abba98e9cc63f946edc0fa2fa52ae23563d4606028c372f7c59a065b95f51720b7c50fee47a3bac15c1c80546049c0a5ca533181b684b4772f029d26eaf9a134f55ff5a142913a8f80a4076f650bfe383b39c32bc9d3d4e5ac303663459292df3dd2fcf96a083fc09d6cf7e77491445e5db38f382ede65bb9a8c23ae93d18a3410f8973b0abd9be657d08a341a892839956a92df8af0b78ea37f3aa60d9b2010b9fddd7fbbe7048b88dcc0ff8e46221605f489ccdf07fffd939b9822edf5ddffe917451c6317fbe91745a095b9df8a341a21602c3b492089f04d56f66f83f5e541d7a5e9dabf1b43adaa7eb18889bffb37c2c3f74f9310c07fefa370078610953384d0d83eab4361a89886e72320be4a138854d0b0e10f791ee489b0bcde4983d219126e382b7985828d4af2fb676ac61b88faa613c4c4c8584b26c89c659b0c3662370d5fbf10ec432b8828d87ef901b3910e62ea41b06648e690ac24e2fffffcd33133f333720681ff66d649f26c41d77f3ea8285b1a3ab1fcc0136c28e8f0e475c7df32dc7014a65dc4a1c8fc0f117ed86d6994f7693e84e26854af79550b86f60ffadf80e7d844da3feb1abefdd541e1c57c88d56a4fa0152b8d69cd166b4f8f2d16ea45843aca5b7e31acf2154f62668e92ee3ca2ae94269d6abeef4957536afc42bfb8694a2a2b3c08b594c71ef568d483ea3d5523b67b628fdd0b39349f63479199c34f0f54b9f2f493ccfa369cb3838df84f8cb796483411d13f27f90fb3bac4961c3a59ec5e3f5f6760421e848be4e8f731694c6addcf3fd242315586b18fbfdb5446fdbadd8c8efe2e266726d4f371a0e6c7af78bc55cba3679da45ad0a053c6f3e8fc74f312d55671687f2ef2637b2da45e513ea3870f90965d71617b091bd18922da48038aeefd9d823a168ecbbdea1194e8d54cc85b18dd3463be011348705732d98bd6ec1a8e4af258cbefd417c88b60c8573dfc9ebc769c97420dc2583dbcfb94f9b493d93f09c667e1963d11a62f25148714bf4038d329a0e8d175fed854d469971aedb3223db12b5d849fa609fb2c3fc35ce46a62acd091cf1019db333ad1b78f8dfa15b3caba6ef9d92c4e6db702017f4282732117b5bafe0d01ffd6e61eeb1582601cfd1b2d538f661ae075ead36c670cf8379c35e01f105721532fddc7644ced15fe5589a354bc736e2ec054863e9cd236ea78ca1a8927d0b7c8b11be34603a97a5138154dfdbd71ff77f35d5e5f85ef3f8d32bdecbafc7f3ce0df70d680ffc14e8a9ec7d2bd41bdfadbfe19985e9da78a4f0a369857a23e95fa1e746f3721fe0f0ff843cd4f29f7dc61e7250ae12e05142dd8da87cbbbad58f3f5c1b603b8bbae33a4e2944f9cd01d68582c16efdcdf4eb40b0d62a52ddf617b7fd1c472c97ee053a7d3fe7ecfc4197b8f48c50d7b570e1126d79d2bd57523a2732976594965ac7ab4a97cf122fd71e3868d2fcc0955db73ce0ef7a36bcd320b7d22ca51350a0e85faf6f08c7419ff017c6e0732e2b18fcaf3ad30232ac34f2818fbbb389f1da37513dea47a60065a2375c1b69d585833fa1273959e4d8cd6e31cba187451e3da872c1d52e93b51139b0bb2152b3d3f12c764dfcbc7ada3bdff98e7337fdf3c06cfb0e72a8ac17f761cf38f82b19f563066c3edec25ade9784dcac843cec759cf53cbd1d145db4bdbebbaaeb7adacb695a60cb7b7aeb5b2a5a390fa75b99f77ffff09c28c027c706b416b1e6b1b016b416b214b7e7e6e7e1b0b412e7e2e214b416e6e211e3e1b5e7e6e0b2b410b4b1b413897b5b5850d1f9c878b9b0bcecbcb6d612974a6e73f6bc118307e146da8eb9fdb4eb80503982ed749a854c875f99c87983e505df3a48e8c8f560b1e94dae49f629ab465de8bcff1765aa14db8d072b19333625b2e2185dab7f7e26dba527715b394535adf5151530487708dc879ccbe511c930462b25428afe14f67f5dff498ff7c56ffd9e7548de3833f1d31e3a45ea81ebefe2fa7f79f25d72b10cdca9ea0f497acaf9f72c97c5cd498f7d1a74bad48fbc815812144cf5c4298e077eb90f3bb03af0b9fcd19843ca89ed5aabf35bb3d31ca6a982c9caf3fa5439e30e02a1a029260012a22123206023a0a2a1ae63ff50350cef877b39e1e8823d0ff5b7175c59aefe2664e73dfc73d61bf14bffe4009627c5182f3940a2fc364c78da39231600a12761f26ed442a345d45444d5ce47664abfc06dbe26fabf18d57135689bc6ddd0a7bee2e66171d5a39146fba17817a55ba406fec5dafb17dd86288724e439d1db778c1804e9d68ce7b360752453e5d5d735983c765bb11e1e14b6df40da15b662db1f4f26ebccf68bba6eb9ea92ae3d3678b9142dd1fd9cbcfa62ac60ea197acdc03282b50073ef1e896d589149c47bcdbef5978a9d4932ee57092f3a9fdeefd2c4cea91f724edab813d730f5ac2e7ac48a79c04b877ae510dfac585ce3a7cb123d2ee1d9178b2fd2242b77ca3f28a53cc41968a7c97673859a67202ee6dccb5aafa6cb578e19d82aba58703e8b2c624996fea895637c8f85af71135c3c8146f2cfde60bfea134ff5f95e6f1a322c710e9a2cc96f05af6d3bac2f7cb44ecbe27ccf1a7a144da47d60c0cc1be311e427e57a50e5916d1a9deec6c630a999e2a79c653d9993ac6a2ebbf0476874bcf1e0ff8cc3327ce33aff644295ebedfca390d6c7d3fd0d110097583969258a818f623d7ffd5fedd3ff1fb50ef07a5830254db1cdaa87fddfda143edf3858cad3a25a48b57af8eb5ffc80345e95b9f3258267b098e66b7ed6f601475a608bcf9fd9f5f346c143ac680b8e4fd333d23f2fef7bd4714bcef76fefeb35e15d625a710faa5fe3a4494014db5f1332e54f77fc428ca1f3f50f6ab834d698709237007b395669ade04520b24fcbcd24c22fdc7bbbcf95045a4368f15f4994c11ce762a85a6ef90f6d134ff3628683fa33ffc1995aa90414b299b3d396a18435d3db148721f2617ef96c2223ea325fed13ff5977f7ebc7fea4722be4803ce4bf96b8424379f0993b4b098833583df7e580a6f566a90d6c59d8523e33a1d7622db185ad67cdd66a2e39e39706b94e7ed4c77d3c3f75f7016cddec807e05f40a55b2f27a477e020a9e55d23d7fc70f13dca4bb3c707aa047bca7081b4e09fd5dfddfde6d31256c4fd8f0b0948c5156fbef88bd7e77bfd31bffff7cd6fd4f34537128994920dd4825d706e50f31075c6feee1462c9231a61ff3478260ed8708ddf3b92c151f6e565476f4dd9f41b81efaf17f5e23f7ccdf942acb2bf95abc3ed617094ef2e39238d2d890ee3641a85186d9ebe1c4b4691d503ba6dc38a9e48ca336d2afffa864754d205ffffefefcbcf7c97f26ddee0956e12d0bc2d987d39b8cc5fb0f2ee1fddf4f949f7c59350c48f92bfed8a48ff7f578cf30d61e087d7fd6131ff0e8b614d08485393bc41915d97133ddc629030c1a4f4bb1cbfb64fbe553aaec6992ce6eedb35659c676f4eb822637a638768eeaec762e924578c5761e72aa0262a3e6c9069931ccdcf58644ab9b525de6d56e85471a92d26262c7daad9639f87b1ca56e8bfa1978a7dfb6f930a3bf427b85ac867db0aa0820ae8807fab26f162ce9cc23c4490c6ec8364702e4c706ff5faafddaa9051feabe1551f9ed4a72ada5c19cacf6acbd398ac1e62b9c2fa8b0d0ff17fb6e121411507a148a96c45fdc8e2f6a3877b04042682b3cc231b2721bddfdf2a1928762e410cc1c3fcde5f38c052953271bc2c171a35ec1bcc2d8f9610b0f422d24a71ae9f204b994652f817dd47502ce24ff3b6263da0ad292c9dabd5af2590aa439236d6295ae894932189b8edf2b35cc9c68fb3a49f926ee667df3f6567175fe91b64f2acfac33efff7d92794fdfdb87302dae78fce231b2721fdef5e1ed3b7f2779f5e9981208cef85ceafb4cfa776dbaef8edd75a1cbc8c7c9138f883094b5dd33d4f6e50ef269105e3282b75e944100ce1aed25bc52e0d2baa3c300aa967343c37ea9928c928d1a5ee1ee5a1fab3ecf363694cfdd50e49ab4aada7a6a825c5d46ffcd70effb0cf3fecf347edf347e7918d93d0f7935bb80e7f57b5017b24f809be85f4f1afb4cf3a0ce1cb6c773d9911e692f22eb2df13c838c6f68853b4ea2c15f12fe72851e2666072353b4dedd0742f8cc8602c462a37deb29ed6805b14f06e3c1271308509519d8dff24d7fc6b0e7d1db96c9b0106cad98c8e0ce2ebe450f704b16933f441e0fd10f800040e413c300c41e0c310f80804fe1a021f85c0c7207008d29f0122938d6112027f03814f41e0d310f80c04fe16029f85c02172ff19e621f07710f802040e91a9c3f01e02ff008143e447332c41e0cb10f80a04fe0902ff0c81af42e06b10f83a04fe050287d8ac183621f02d08fc2b04be0d81ef40e0bb10f81e04be0f811f40e0104e1fc311040eb119339c40e01042463088ed0906e1d0c120e27f3008d20a06b1ffc250217034081c82ac804144b16110895c302c081c1b0287103a83e142e07810383e044e00811342e044103844fc1646028143a444c1ce41e0100e1a0cc2418341649ac228207088f3240cc29f8541948dc22e40e034103804e90ca385c0216a0b61f4103803040eb140c118217026081c42480fc60281431ca8616c1038049d0983505c837140e09c1038448e360c42dc0bc60381f342e07c10383f042e00810b42e01079ee308894649808042e0a818b41e097217088da4e9804040e41e8c0a42070882e703019085c16028728ca80c943e00a10b822040e91320c5386c055207055085c0d0287c881854108eec220c2a8304d085c0b02d786c021c860982e04ae0781eb43e0109560304308dc08023786c0af42e026103844d2200c22931806c16fc12c207088d66b308826a930888ead30086558980d046e0b81db41e0f610b803040ea1c4097382c09d21701708dc15027783c0dd21f06b10388444300ca2ba0f06917400bb0181df84c021f2ef60de10b80f04ee0b81fb41e0fe107800047e0b0287aa14e8e840490238ca3ec98e1db29424a1c55b72223656775a08ca8ca1b27da08246b276a432809f50e68fb91351659a8e86e024cce6af70d971a31fe29c8108155cfb673f90b226ad3d8121f8bd95a1c7d5f7c57f13871dc409c53a7af20f3a986e3b4b1afbdbf0ea88bfad3bcc9e7914a8c8b7f05dfa07bd0235e95f9d5ef743a9787ae19e01cec15bedbff5a02838ad95dd6e37e8fafe78e57f97f1c4f8b28bda8ab4fe5785f97c753eaedf4b04fb46407e6ff0cffaf12bacb51d3fa13535644d92b50d3797858d3017af859505b7a515dc86c78acf869b9787cfca86d7c2d21a4acf1c71b759ad5bf17ecbfe624a0c92488343ed2d6e3e6422f5748b8f183e217c5ee539ad48fb48af23583e63a61a06fead97fecf91c67739210f260f9cf21d8d388161f55a9560857a1bf3f0253232196c2fc687a86443dcfa4f95c687fa64535ae595f7679f242dcd8e5c42cf4f74bb5cf0fb8cd69f870b62fefda74be313fe17697c0ee7fff65ab5afe01f55fa5dc6f3df2e8dbf8f321189583237353f1cf85b0d21ca4bf4557a6097ad112975e217101873de108daa97850dcc5f26003af6acfd83cfb73854bb05a8ebe3b66c47b008451a931727e0cd163e97fb7c8de9774fa97f7994ef7dce6bd1d09e71138260e9907fdfcb40364140e644b8dffdf1ea5f4fba98bfbfa5212322209923dc477fdffa8fe2ba48620888930808f60808149252ffa863fce0026ef9ef6a5f20297646beb569c44b5cb37afefeffb1f7165059656be3f8fbd22d48774a7797484b2329a574b774883448770a8252525252d2d22052d2ddd202d2f15f77eefdbebfc7717cc7616e7cf3bb7b2d5d8bfdbc679f73f679f6d301ae9ad2e094083b4a9ca283768d62d28cc6afda65fade924ad41906c521cf1e62e467509f53db958ce8f486d26a4ca0839ed53f809f46d96cd7d73f8f1ded02a1668260ba3b235e5d679ba17c534a20fc24e36710e95787ef5133a41c34c0a7f9ee5b7e357e3e3613b8fe67183d957c75df8a933c42551f3ef94ccbe52bad1f972e81f27d82f6b7cfe70d7a8ff4d68acee868f60d4edab8cc73901cd781c6f71e070303e3a62a79b775abf2e0533651ef8c637f7ae3dfc4af7f8cc77f0cbf7e1938f1fcf77e7940d2f20f36529f574ba130572bc71ecbc10b3e99f4f8de92df7d40b291fa46d7ef9191effefa6f83de56620fea040ef77fbb7efcbdf6430d1cccf5abe5c0fe8abcb819e1f0f3bb4a0da78f1ebdf0f4f9706f30dfbe3e27bd8b755be75317f487010fd51e8d91f2d1e24a618e56467ea8b0c61f88debfaa96b3a0573d4f78f6acd7f4b1e02ce8336cf3a2f0590824890202fc9f5c2d07960cf49f3dfeef74fd30e150537252b1d6b415355471931237933736e39537b967ee262b272be962e8a47c57cbc8c8ced2549d4fedde9ff7fcd7cc21851894f82754d3e16133e4e035e1e6e2e1e4d4373064e360e534d2e735e23066e3e26535e0643734e133e262e533d237e1e3e0d6e734e463356435e66235363230d1ff1b84fb7a12e535abe9fc09ef6f64a4afcf6eccc3c3cec7ca6668ccc3cac6c6cb66c2cdcbadafcfc5cecdcdadcf63c2cec766c0c9c1c76daccfce6ac463c86d62cc66a0cfcbc66168c4c763fcef7d7f484344da21da33dd4a74e8ce5dc5a00b591f89e4690f3f187631ed393e98f7e09ec81e7192f683f3e19cec0695325aeabbf00b3982924be8b7ddd8d1461df172041a4c7f5fd3b47ff0845f67adbd3312adc852660ec24c7e6fef5e3f76b700e332189210de742cef1d00a687ddfb4e2da1d9a8ae9af897a548da017659f6e095f617e9353f74a388c31de0fd968aafe2fd5509dc5fdfaabd49ffd32ee99c8d1df90c43b0221d9e94efc31f7aec61f713d121af1a88d6a0b634075a7c3d1599404d0bb78cc823ff433b2f1c9245d0cf992e4e4887bcfda1638340bffc5743dafa27e4674108c5809414070d21140f0e121780f07c4810c8000a0457001a04cd1a1d021bbf09e1fdb120bc3fa4505b7c08ef4f08e1fd8901efef21795597685d76222720389dfe76a9e0dee2e8e39f91adbf33209562f5cf22b182102e08acc014d03c3f37a3e06e9ba6ffb94a8c97ae626da5c895f4e8767254cea74ce707039fc2285ed4a252e926f0f63c0e5cdcba7b5499eb054b7d2bf741dde9254ff510ac44630df93695fed9705971c4f44b230450f5d268340d05b3b444d161319551a885f16526f47c4851558d7439f88805df248d8c250fed761c7195387efddc19a5f4fec60079e45813fca76c620e0a196c7ea7bc102fc67a21fa4fe21c9f58c9f7a11da86556715eb42888bda2610e77a9a54c6c253887ba43c1138dbe3b85e2802336ea309d3aeb69ff508b61444386e4e1f11a86ba1b0d1d0e274bdde4a70f708ef457a1e8e6d45497716ba6ea88e96b8a8712d276f80824045aa3ef744b263aa8ee52b6c13ccc87657966af50a9d4561f1aefe82f3f444c2ed0a7aff6f845d204334cae9a1e99e6c250a02f9ade83e013e8de7a44d76984c79725f8e80556c5d8a5976a1d9e696472ea0bd5f7467b29e004ed6c2dd1047e2fcff8bf801f26d4bea9e4305208e1398ffd7da61ef12a693d7f04093f0817d9cf6bec022fa1ada6df0fec190c1f37720fab2418332a9df487cee275ad61d663628b6f6adf8c1c1acf9e8cec4feb8d200523ac987021c3c547a3d4e3a2c0140e1e9fbebc5da6da7f2999f7744eadd72c43b1d3b1ee93cf73e180a53bca14b7149bfc17093171c50ff36eb9facb1d337428703da0452797eec7ab968f6dc8a5686b3ad3bfe4412d9a241f04076917a6b5f68938e9aaae3f2ba5ba597a3ed04133a4597ec9184b24d0551c2e4bb3a645226f2bcb41d36f71a16191aa470fc50a076d1af2b9702e78c4a151ab391f2b6b9fe57dfaeb5b13915f4a51b17dce053da890b0ca83845b2c1f8d1262d2c7a6104f801c85eef1f5f158be1342432d6995277dc874484ee1533610db9f4abac3f938a936cad0077a053caa6fb2a42365b2ff57c28fb9da679c0e5283f0e4b7e8715c5a0a574630b61b21e1c7cd6d8587165d9e7ead6b9adbfc860a9fb6076cc8e10dcc262ab9b0d05f0b201ed0a578c493ed7c7639b5220aed57cea272e5b345a2e6ddf2335d28e71b1d6393051baca3442986a0722fbfc10091b394d5ae348b8fc42a7489ad05a39b5a91a5803b260c723aad19a199c4da25d38daeaef4c25c13db46b28acab443699614ac08ee9ca6ac2f773dc95788759b53b8f82cfe046cf760d8ff4de0809caf43d7d439dd064cdac3cfc85791ac61daa92162ae79bd4c5f2478eb242e67e4a1f2d149ddf01796f53a4bd1426bde3ab33b64402bd433360507df9f1e1df2749bb74f7a2b4ef5a4094e23f6c6ed9d0eb7f88a54c941a377da857a587b462d6f3371486df4b461f96014bb981a9ce4705a1fa2ea3b57662b5095bf5dae21984e3bd049950be6fc2be107ebbdc7391a1fd5473743e97c107d8f0363a1f18620e10789a79d00011b86cf27f5488ed72f67051912f947d53e6f519f98a6dff5248cd3067dc66827c450d29f512a8c485d5b7dabbf4e5e4652d96a3d9fb34e9b6e3589ca2aed4cde54aab95bf365be35f26aa6aa3f1f319ac91a4ba3aadc5b45f6923ae0e6ac92aff98b752a022903cba9532918434df04896d6f3db16a4128c782b0c8c481b5fc4d95d9cdddbc37db9124cae3a0dd23c7cca7a791ee9f98c660b63ef64129f9e91ae35515c7ada0e177f9aa12e39eecba547811a48b80d9e2c4c371771a7e3e1132eb2b7e85fb122e355f9b0d472487d2e20a6faa240e513afd634f9500e7cad06c26ebade43b63e16072ced21f877f0364b3dab464c5e15185e2ac65398e8f46ed41f381f04b9aea6faf30973b1b2f22381bcdf3996e9bbe89c7eca3a67ff2be107733e053572ff3bbb90bdd714e71e9baeb0d4de9f20e10776711e261eb6a1a37f8d96ca3ecdacbbb400c81b1f534454b19e4177431ee50996426c62b47a95aff513c1ad5789507ada2a52ac36ef893ce64b16cdd4dea8bf253a365c621d8332bb47d330532f27c660f4c42a0b663fbd0a6539733ddaf2b0974236d360d5fc06e705128d76c240008afe73a268c48f0c9b4db4e94faa0ba18969421f5b8be51b242777ce1e5736c389a97e71946ba2e2d2132a95f94c8056a74d7140a2784036d52e44e45f157f4f2fd19e3cf5f9868e2e19ee00a2c34e9b1f176dd4d2c80a8990dd6bba87ef5e3cafdb7086967bd8f9ac69790a66f2d65e8b2e139d62d6a03849edab86d9ddacf2ca6478b98f6834d62b2ae9f8989b2b79a74184cc982204f8c3b47ded99e4eefce188e223682f0326615bf934ef2beb98bc3babff2be14707ba934c4cca1375f578f93e6e76c9b8aab36d1848f871c318a1242727ef4ead4c6356d0f8d1cd0cd1474b91129a4163f78d37334071575885d891da1675fbecee3a4a6f18a68e23fd9c927c6e91794a2a2028496f8c13dca80439d20ba12ff79a230daf6faad11da0b4d5d73498ccce44d9279b7f36b26531bddb4b6684e5508ea59280f6d9bb76bd82f12996a4518eb408c3971066cd1534d435d5e3f205139b58bf7e39c1beb548b7bab05abac50a5d1cd11eea9c94038ce316a8befb04eb88a92f14259599ecdecae95d0e1285de19c95a527e90d0484f6981214a97711743f07e8e183f0b0d4962aa2de5d9560c5af50001c8d8e33113eab24e9ff289a0a7526234e9d37055ef606a22061c4c10577d03da2bf82c8757373f9b339fb1eaf23671e8c338c3c83f55472b267e60572541a204439250067afe57c28f19ef4d82a6fec8dcc183e7771b4b77bb452804ee42c20fcad5ac76453c0c47d77b6236fb0dd31d555f22b534b2ecd49f6cde3058357d9a433eb40a126621571c2aa82a634a993261d705631b7eb6814ffc50eca351ab810c0b4e454bd42a7d3635ec882a17300643e46266e2b8277e107ee3f21646de4a3e096bd21d51f545519be33293e70706e246085d6b262fdf5560d3b1a7af15ddcb669157c50f91807d53f721c06786425ae780b28e039b52e74bd4f12993891f4b306e62eefb386ce6429ac65c998f4ce68883c981cb3dd8594e9e1fa08ed7cb96343153ad5983f4110deb67e10d65e3df4bd11d29eebcf7bdb21b1657e28b7d9cf98e96bac6b7a805cfe4d9fd1e1d5ebb12be3c91f24baad431a3ab0db4776c2e7103b4a115d31bd062dcc4ac255a3a756ed9c4b051a11f6492cfb613f36eeba3a2f4fc95f0a36c4abc3fb16d6dc82c987d7de75dbf5be68cb72f24fc207788bb574258472b6f362f335b3cc0c7382f318c4d843a1ebe1d6408dfd0a344324e7a6160387b279451ec7d53681acdab2f62c2a7ec89a85f92242f9e23bca4431862ae407b1969e9aedbd0c38fba8d9f155076ee197554c978652fd0669e4c0cf660a000bfe09b5073667a8f20fa0631cb822d085509fbad91db0c46419ce3310ca81ebfa6c002bd30bc8c5ef6463af668fc94c68349049eb04e3d71ed92f84de6e59ad3cd0d13aec87542bae1d3819190edc5d97a7f395cc13ef069790cab214a46b3a38c2ae1d08d43da3de6e9ec5b35caea0b67b99562020e44af381a5ac3109b82fbb5ef7760cfd8836aa63eb753f90535378f2cb2155dd80af75a61ab3a5247516fb6b20582b3c6d1a11e0c0545285dbd3c9e16352145c9d16db64539e3fd2be147427516c3854cc49c9e95aaa28cb295e370bdb32924fcc08ddfb99b22437c3189fc8ede3d58e5d5fe2cad4a91167394a25fde4726684554f26ace8c87d378c98a0598da75e16d37b02cefc26cbf88faec1b5433f0a458f51981b44d5aca9bbbf1879730d3784a183a6b7eb04a6a2698d1de6be58b47eb70a5c452e68f29420fef8777bf7c4f3adbcb3c199d5adec5f2c5a49a08b1f328dd7897bee3f961de597ab929da7d3c89976feef14ecc180970e0ec26af2c31f1bc6f64a5a1d824f6c2a24753b325a87dda3b99faa4e0536f8c4e1322ce382bf3dad4142649bb02b62ebe52dbde1d6e560a13ff94718f75df784106e2056dac0783a3393ce43cbe11a2541837892a57b395734f3943c651957b87fb97caf54e3904b6d5dfe23331dadf379ed5f2d84c1e894112bc148a419e745de69be550eac50cd313f1fb2be1872cfce5b8fffee84b3d7ee1923d0f18fdbc48317748f841aff6402e92c21b4ebeeedee0eec12835cd21bbb438e7860d46767d989d03b5e70d92a0d586febeb533cc630aefeeec5b2cf1b73c340448e01c1e3983060dcb1e13e5d2336987dd7960d32d13eedf9c97782cc79f637e7f95354de881b24debdd01f84964f62ac907161598b3ca7e9f8ea4ca428ae1bc856a753e95e73319edc92e42236815d2e759e23aaa6d048bada336643462ab0dedbe36d417be49e3252cc185e377b17424f5cacf4fd4c87701bcb650ed624420bea5daac3fb486e5b1d04584dc46e141db70a26a9b2cd65de327b23f59dc0c23b0374988594572d427d795449c12514f5b566bf3e0b98005ebcd7dda9a719ffe5a6909f8f12fa69e01c9b9a137aa187af1fa5db0e0c98cd1cef59b3de54a1e08e2b5d067d48867a56912c06cfc95f083f34e2a5f5b338bef5d23049ae3466fa4638df45a48f841eae9184221ff417878de847433195b84eea810cc1496e1d732bd72c2f994be852e9ac5d9fca30ace613ada5213954587dc176e8991715d29ca212e04b2cdee20baa01392aded52e2ec948f631cd526d9dafab788cfeae50330b473389e23376cddaa1d49ae9020ce52a62bf2e14b2ea9e3f719d034e34d511720f914e83ed5af826aba846ecfd853907dec35e66fc4e3e390397682aec87ae6b98a73274c523b0e26a3ecfe8ae462a66e7fb095bf40b4acf8918bf574619af91df6794581be2b2198ea490c22057b95698bf4a536186d23d387d13d15e4740b3dce96f017534fb450e27ce8abcb714d85bb71b396457b97d7406c7d6bc68aebd98cbe9f309f9e53435df68fe0f62f267af7b99262b84b5a0469a9395b513aab8fa03dc7e1cf5b5cf82be107c267b47b175763f74751c63a5e3c530be41a40af81841fa09cc5cff4e6e79df0871c92305bcd709de111582288eb5759275098cfab999eddd0cba42bc40ef624b487cba5f273886de3ddb6c91cbd8b95fbac95c362bd630ef570f10cbdb59d23220a530b639fa20ba141cf041b943d9560f7a04702edb100c845a2a24acf3d590ff52afee5c891d6aefcc3bcfe47bd9574b6e4af2a39c4d6d0ee96390f30f042b90e49c5df785126056fb48bb463ed5c25424b9b9a5082b047e48c41aec85071c02f93873b3131f3fcd9a3fbc3d92df1f593ebcf44d8fc4cd9c9e8dddb06edfb6b5ddfac265144553caaeda35d2dad02991fc13de3f36d9d0b01f5c4d14b04d8104847a7d33edb0e797d41759be3390705a7ce80adb818477d3e7ccd7c6c8209946e9f3b27db36fad9de8c379642835ca62adbf041d7d36a89febf127e88b6bc9ef55da5190d1d5c49a042da19e1a9687081483f6a4eef6922be3b165735df1ac89994c9d9b8b9402dcf0e93ccc41926cb80fb1c6734748c5ab32fbddb37c0e948faf69bfce228530f6c02354fd181264ad82b66b0ed15f7c733966cab5cde835cfede8f863829bb1bf6830b2739f8dbb19acf626e9ddb7b533c6d7da5e248246d84fe1e4af22e88e26390f158fa2b65dffdedf65b4878add9628f72eaf4dea7f15a476db9996248cdd56438ef8dbbb06fc7f44255d1a126310c521f4c153a0823f3d9ce8ed0a4afc151f5e72f37d33ad98927691533e52b650aec276ad4782970183deacce110b22bf7bb9f844709ea4c1920dc7fc650fb4a437f2209230feaee4caada38afe27ea0bd7ba67824dcea1e11b7f82111a63b2d8de4e6db3efd3b0387cea36f32656b967746eee9448f3adfa089cb1671f92be1c7d81adc5dff6caade59a6ad8e314c8b4125a3c40988f6d3be0039262fe6cb958c1eb2f86c75d27555bcfb3e8fcb9d761f302f7b7461bc60b13b6158ddb6b32eeb748c2c94f01ad72e43cbdacc9e687e467c09f379d7ce8a2059447a6557540d6b71a7b7ae3455523bd7c1f5f2264884c29246a5fe7eda0434ec880c2ff4bd2bb14d8cae37a8f8956e5381760f3dcc8fd19a887a7ac5b9986160b46f3db12caec1e6df0a650f7c25eba9a08bedef1030c111e3385deb70ec82ef81371a4e9518853fe87ef954250c8f6cd09c9f3406fe3c69b8523b687d31f8265fd75eb7de7d3798d4c0f7d9cec5dbd83dc5ac891cbbae58d37d913b634b2cb7ec61710befe88f085d58a534a35e858fd2b0d15d48d114198f67e5e738db0f22bb98def09a7c269dac290d6610a2bc8ac8ab22e3ee90d76e166c0a693c27fba1fed2740265e5ed473f5b5e0365145dbc0da1b40e8850de7c133f25739e4e4126b4c6dc8b2e52513d7668c958d1239d29810adde7d6cddb1ff679f953ac5e6e742b71b850eb22b8124c127167773776ae922e56d3cddffe197c8b61a042e63021331404a1f3a9b4c412dd61994cb95e580e581faa8eb0dc5d997992281f9ebfa57ca6f6fdc3a763c4d78c85814948f6ccf9efba7f5f17c26f4ec09bde7ec46eb76bc04bcc3c3ed8d78bc202e625dc649ba9fce2987ed8672ef6c229736855ca5601d033e4634ee43b22a330ab33e164bce733e4dd74982f7faa84b8ef6b69f1c721f08bf5f1f5a8eafbca22ca776a01f717fd99f813ec7b4753b93fb819b6622a341689f8cfbcffafa3338e7e58c9e7f7e42540384e10f3567e095b72e18a030540a9222f7d276c294a504ced8e12cfbbbccc5ae48e88d5f312d2edc81fdd915ef9b67013d4676f7f4d331690bfa6cf9e6ad65afeb1102bcc0a5191ff7973c7ca05636407fe33c5b20bc68f1bf0cc023969352d4ce3d54dc772defec4ef8dbff304c76cf50a6fcc35e58c04329e8343defb885457feb08001f2275774a81398216f7f1c2de4267f4d27f4fc1a98d6c2847b63d78c85ba664827a44e6e7fc6810376538374e0f8e40345c4914b39847a9125fc1bb9d2a2bbc677fe6d072e7a20a62bfc0795aba27b1e27618024af79e00effdd07ee1a6bffcbf1e3bf04f93f0b3f9aa086831ed8bed94a6c02b6c61c0e4a13d3e899eafc76564cf895133fe8db592b137a8702946f6705ce072c11ee7d3b6b68f1f1022eeedb59919a9c350de16f678d260aa12361be9d95543e629d3eff76d622c9e622b5f7db59b59bc4525e46dfcefa160949b3eb7c3b5be65c5b64f9ab2733664cc4f834f7ed2c9f0e25cfabb55fed0ea9c62c4ed6b7b312e207d65b44dfce96a68f5fee547c3bcbcf36d728e0f9edacada4a55fc5e6b7b3d91cdedb68bfda3395b70dfed0bf5ad788d671dc82f7577743ef2d9397fa76566efd86de47aa5ffd56ac3242e9575fdeadc7b8aedceadbd9db2b43f14ffcbf9d95c53f8c2853fd76d6faa06862260a307b022d1004461f67b36902f991ec0fd74033fe0e4cfee765b78091e00e933a01536f2beb6f609cbfe81dfcbc1642344b8913fc74a0e66796c488ff607c2cc07fdf6cbf356128436b4484d1a8ea871720f2e45f536dffe130b1c4cdfc213c262b13d21adc28360ddf9ba735baafc7199a140802813ca02841d1b95b604ad0a3239005485e2f6f78955833d507e61ff5f720c1aff1fed7cdaefad1fa7083fc1fbf15335e58829a776741ac203e100d1608749a8b0f89cdd66218e0e71b1844936b596b624181692d14f11fca9af81edaf047d5f1a85aa2c23f5dedd956383f346a40ec5e6f5f886145475537c8164c662437244f94bd79f53fdfe097240a12ff5ffefd3a89021283fe764077de4c69823a8119f87be5009226a31a985649d68c31691922a20766cf9ebe55193022d5636c95899558505012610900e9ed106ced1a5c536b07fc257da9a45df8be7f6943890f1e3decdd78c1dac68f0bce08c14028480786d443f8c637e9e53f7ba2e8595d597fe7f86344e967c8e16fe663fe4e38a454f1b812847cd0fd6798f0253b8cd229e32161cb113f2e182524eb0be1f9e7ae79dc117e365dfebaef0869fd9f3d7810f708228e434421482cf3f50f542710c810947156f51ece3b2d93f410f95ceb0bcd97c8111cb7778549fdc2e35f7fa7ef6decf736f3b7538c7ff7af7f7508e306b07e5bf23d01bb06810d1fbc0e41c0fbdf4464b05150776c1fa4b395b66830cef4ea828f8342f20c4a88f3756887f76abfdfd38fb7674fa8fb9496fd01deb167f1af71decf04b7ec144e170bb0d89ec6b64328460cb1ac0aa44444783b3363373773f33f8a18221588aa47ae6b65abf97aa558f6ad8c8ad63e64663434375bc9a9731aca0b3ec2688c3df0c993346f306ba94b2035af7c2c7224ca0d0dadf42eb7a4c2d614844bf63bda7ec16a0421308cb89f35fd3d099c7961a60656fc6f7ca5fb6db5c4e68b20211194bb65ce6545d4bb6e2202950db1393c62e677f56e068ffe997ce5bdd1a3426d22d89b57be87481e773223882d4f7e5cd9c0fd5df37ff9ca9fc757200d48341930fe814eff64be12fc9f5686e5a7f8caefd92388387e5dbe02feed62ae60121068f787852f7aff706115902dfd416f930673ca4f155681a017b8df1afaa58ec65dd05dfab7d4aa388abc0133bd66af12f54fb344be5797e4cfa9a3f163be36140cbdd0e2cf4704fadffa1ade4d50c7f783305d720c18575dff87de568b5b1aca4010d3097f5b8cbb39ac6cae280cfa8195fd16139c9c00eeaa2a52e216eab8163ccffab9e60fcbb8821b99d3a106832803f76c9b41203f72d5223fceb7a9de4dc7038198cfd85e39b8fa91d7a255b71a2d1542a856ed947e4d93a63a08504fde43715e343cb3492776cad2f24d4e08216274c83f583c781f04fce95bbcfcd4fda5f5273d4a55afd6f2cc5752f6d3ff51ba1ffaeb9841a813f8726fbf1be6f7feee23a981cf52e1238650831892e31d2836746fc2a598dc610a6fc0baa79243e986e47cb9867dbd630db7f237da08830a4544bff56c6b66753d1f8e36f196e5393b3c02151a3fa7ab7e66d7e45460fbbd38c582d33bd51ef12d5e6713d074b62cbb715c2e2bb2c4da97b40bfd856314bc6d20184effd1c3a1e8add7563c77244e6f6e1f8f6dd4dc6743161666a864afe040a3a676d7767aebf95a2df6b6d906718d034df3ca63845d9b63a1b6f055ae7dfeea534c102c65bfcd5d59e116689a94a45d57d00294ea291f6d5dc492a81dc7abd921bf9e468364b4015dccfd0cd6cd195d477b0c799d77b0ae0a4fd1ef3e2a2f63f24e7db10382f3779fe776313af0367b56a0b9c8daa73f6c2199edb82cdc4e0a9652c6e9d83c707c9c877444c4a94ba8933ad865607ca8b3479ab58f30f1704dd32cb51f4e461604ff78bef5d55b2a2e9c3e49f1d984267df70af2d0a1572a8d779d384b41e8c46facd5d6d31f0ace9f832bb92e422492036f4a33b6a02badbd871661a3cda7d205178110361c76535a3184b66ede0a2ecdc9f58f7f1864764b5954f96327bb3abfb751b8377afe387accddab641eb95976287904b587873a39ccb04df96e6de13113a3462a20a476f6c92ea7a54de2bc5d93007cd675c9826cbb42e4ae4e8bc9f186247d22da7786cd8773b3d0435821aedb45515070ecb9e11cfd462565af33b4a2eeacc9baaa8390930636f254b9df60775106a0350f8bef0fe33d7a8f38739e4b375a00be710e370a26d97a1a8c535e7b0ae7ea1910383d38f9a169af17c544be58bea99bd55c110d845a855c19eb90cdd2b3db934054ae393cfe66342082f620e8d2773c8f42efe5c488c4e4736205c2b51cf1a880094b37ba30359a31c33e964e865ed7aa4a2d67f76d30da9336987de417de5a67a376abca3651ce32855e98074bc3dd8fe3022753f05b364c863ef6d9acee29f67c695e5bba8d35ce54c6143dbcf4f88be8f495ab378dff00e84647a7207c7421eae1f8a4017bf187aadc3b3ab8116db6d19c12c7cd1d54d36830ec55bb662a66e5852337fa9778f63ee0a92e9c6e9ac87354f8c51c1f66e188a081d1f57a2e4aacd6c238e03c87850f0fda89b78871c41b59ed795e72baa1b3b5d3aac3fa9acf1de9146d85e0891fcfb1cd722f3556ec2f08b53a4af7e43f4b4cdd5c076120bd9497412ed2184d5cd347534c0f19583bb593672d8bbeed65cec789c1cedb4d954029762fbb5647a72b19c710bd935a91fae80c7e82bd6b5a8eb58ff5ad5726c8c48aeaef892b806e05823aaadc75d951a23d9d5c34364aa2413edcd6ff101030c82099c5f0c067f4d0ebd5eea39c4628dcadec8249d116210f7af210e1b115a914a88c6ec54c60fd0c09a0ac63f5248bc3308252a792d09e7ec1e0246e56f63b880eb4ff2fed99fefe7a3f7543b8e137b4a949f81c8b329f1170f795e1088b0c55feb50b804020ba159086e15b669cc04c4f99b98147dcdfd1aba0d4bd0eb4d3e2841686c0ca5c3f68ac8a51c7cf2a52f7db858e309a39a8c4c67e6e8fbe27b5c00b52908044c09cbfefe97f8a3742acf989784de9f19a767ec0f3379afa061799e02fb5cb04849ae49ed6bd762972bd8eb3ecff32db6ea0e4252a9c72d03948a219b03050a11f76afcdc8c64ee7e3d88f214cd5814595ec99e8a24855bd6dd4b28cf131d437999cae0de54b070eed437e7cc79cd3c938108c5ba7fd3b6b2773c45474196e69eceae247d3a394f8b38818e0028981f0e32d21ec9e5a39a2b44121d1cfd3abd8fe9ef66482a1f657661fd45e2b129e3c2f25ba00c126dec184e6f1f8102bcafbf13c5e65b490f999918e6442abbccf67b226891a70e7a32795b7129e0af285c131100f85cbe891203c87d919d3a9786574a0accd1bad04866bce923a422ae332b5b36074730b33df3d1c04176c20a3a5ca48f84b719bc75560f15e6c4998a59a4c8e33a3f277f432bb1943c3d45b7e0898e879269e15abf41804cfe91adca1df651d45b069ba16f678727d2df36ddad156febd0d5954255dc24a3df9074d2894bd42172e55238f756dfc4f39a59c1e7e0c58e12a61a636c623c40f0021cc8bcdb78dde449e1b8e1ab18b31ac96328a75d3e620aef37538a76ff3185dc67608415c41abc17fdea6a24b16f33967275a6c42ddf408a92a6fdad16aa04e49008464fd8522bb518143ee297e5ec1fcdd2fedafef29d132e56c3c216a9ebc14d8c3a3558bdf0e319f4d12bedda3eb91d21dbb5347d382a448629ab1e2bf346beca2c60942e654eb2cef6e80eede290abe9d839661b5b1e374a1e1c4d223f1f1c5c392e0e44098b1071b1c1f8aa5d86e66f578b5bbe879603de8942856c1f6b36564f384695000a1a219cfa39657932556b8cb43998fdcb9c79ba5d2eeaa0c8f4a882fa361faae2505bc7d7f072da0db210dffd18944f8a853d37aa4c8bcc6c070444d2c53d9e0be33186d2a60923caefdf40558e56e35b3049a90538d3c7e28f1ebedd3dabb92048c95e9a258920963bea04fedabab332d8356a63e8f132db3f742c18450e651f9d3da5ce01b3caa1335f8b9fe9cc5881e212366dd99ee1fbc56447a5ec74eca1c66d31295e598dc14ee004beda9ef3eeaf2f4e37f81dcb53412e6dd30ecf62e579a5916e79d34087d8eacfbbe78c396ae16e5fb19b9f7e9dc5ab72e0e387c13a09f5a33753d3e60b8af267811748161b7d8154178c7d44b80bd43ba0ef44891d3ee2e3d2c6c6878603708e3c5a23c743de33b5fe3c68eb2a9888e888763b855037a6ea2024f1853dae03b4eddd20f3c3ae513239c15046ec0e5cd778de9ab1e9fb4d8d84a41e5534e067de6039b585165fe14db2687ccb60936f0dc1aa2eb87bb88e8720fa39ecffaee0a4fd0d64ef0b6b7bf3c8f01cf35fc6bd976e65f946d0b743b7fe427d90e32e75e9eb6fded3858709b4d64ef4efa6f065d83db61c705913cff14b68dff5fb60d0209392bd0d28612addba23af75a4b0471dc40987b741db63d1804359ae43708fa7b897062eb62ef26a813288cbfbb34e960e36aa060284ab3b0aef98aded7bc1e92ed92ecc07fabbd0291a029f3339a0a26c211fb6984bba5bdb6d20786d15078765531e56bba7b7e0c4792043c1f6bac0b475ebb8c8e5975ca32090115811e0b47e175f6c7c48af70924b784e05bb949ff8fccaa1ee71b7c0b0b79013d911206c921f21bd1c99afda26d72bce09bef07d25e711d0b9a3159f273eca698afeba56ddf2af7f75d70975ebd6a65f99df507d7f6fd284cc7abc5316012006d557bb8c57a7de3cad8ec696825eedede947648937d09e99337419dc0f27c5bed1696eafad56e617e6597248b32c58b409114497f3767207a8836403878fca2a0702bb9f779676dd79b3cf2fc63caab029b2dd6c0a73b62e26ecd3f08cbfc75b55bf1fa0722744b6c682f3bc4a2ef5f72c74addcd81d011f4df5ded1606f41f3efeef54bb55b7d132523057d2e776b2951451313035b6b43530b5e5b067b3d370e1b6577134d797b079a4a6a56faca4e9f0e73dff9fca50fe00b515657535e1e6d467d5d7d737e4d6e7e0e3d3e7e064376433e436e2e26433e632d2376033303131e6e2e0e3d5e733e0333036e0d5e734e26167e536e433613761e531e4bb9ede76cd6aae80fdc3fb642283281f8e295b5ebb2682cfaa75c91a01c1c98db204891a335a5d6868b2cf2aec559ce23e2d28ea9314b098c70867fb1824107880d193df0167661b4c5c19dcd35bd75fa5936ce2cf257d7852bf4e53a46ac48ce2c04a6afcfbabc1fee2acf8d6bb90cb5929568ff7195f4197a03f12cd284621d9e7474dd4f1eb8aeee037411d6b04838ba104e5ec63157ec60f42fe9bfd75c1852383506549314de01318a720e8b0568ba43c90ffb6252da806c6e89d862a84c643a0df2673ff58ab3803d5e4d31dbcec7832275215c92b9388e20c836df48754b4e9afca677b3321305d18c03130e69742590a768ff2e1736f70e93ea420bcadf8038f0c380a2720dc0f1283fba31b07914d409f4d3d9a8483d0381a84f487d9cc3ff617c2f5ded7634390a83c340432050dc17a040d49e8c4d2887079b9803dadf538aa9372a99a5709441534f3136404eb9a64fcdad75ff7f9ffcdd7ffd34713d4f1a0b73fec9779903f1cdc13a2ead60cd8c4df222a48fb12ee1a92d9aedf431a3a370bf152d390e5e49b37329e415054457fe601cf49d913242253e969326a8847cd8884c9e6d2607fac0930d034411deb7afb838541207f785610a85ac9c48a3e038af559fa8def3d0dd2adda879fbfa74f7380407f0391e8d130167d78e344e56e628531b98e6590fca2b5e904aad7db8f25b8be06aaf566be2020d40d52c805a41d8194840269fd3b9cec18bb525e15fc7b73ab0eb56032ec15e3dfea9fcd858ce2b6cbf1032200bff82cf165511a24690882b8fcafed66f59df57fdc3ffd00d8b987e107ed5dda4120c3dfd62dc0b827184c258dc6bfc4747c0f71bff7e97f89d2d82e5375a98cac7e384e77c49f00ebfbdbb780ab5a8ea9b999fefa975b7c0f337fef2d803f39810af3f6232f65ac81f22f158391bda6f07c5de11bd04e53dea1a88ae1dd34f75393adfedd322c7f70a6ebc21f968a7e21105e03101e1f92cfc5df4e0c6719c26faac2311d7cad916b42898b13d4c755ea5d96e1ee919dc4685ab35e9d90e0c51a407a07a46b5effa3e19b1c551769808b0cab812d7d01ed71b4fb66ce96725968ff6ac7db8e7035aa8d0c2edc506e388c8820abaa31c01e91fa026d60615160a260d6fcfdab6ce27a4c0c42988661c32d9d02927e537bbac4e3832b17fbde013d7082bd7ce240281d06befb7d3d9dd8303bcefbae3b6fc11ce1117592e5b86df801b0f346dcf75b589d292a2134ec3f01cb79fb118951d680c577ac34137e4c7b8200b933d5fc92e26ea05d438ffc92c8d3887ecebbf3c8bb3fba9c9e77eae467d64fe22f923f461aeb300df57add42be2f4466b1fee49abaa288fb422e82f35955f797b92c2cfb77fa7ee2e0c8410e0aca06e69a96cf9fb040b6a817a857fd9edd25eff88d95490baa47792a17a28ed0499239aa8830d416c21820ec26e804a5d7db8f0833bb06a5559cf424e99aa713d04c3889f411cdd4dde1884fbda20d6c9f0a72380b1d00cd64fd071702b93e1ad3bfcadb6b31e8242f7f8c6d02a8eeb1b9f718db781586e7cefbfc7aab9b18746942a2e35fc3b761683e2867c4944af984c256b7047ac13972547c0d978622f19817b83d89b23670a1a73cb46c74da04503d87838f24eabbe5ea45bcb5bdd07d350599073300cd9e090e939fa532b0fbb32bccf66527f0ab6f39de9afe1ade1c894a0b2b8ed56398bed0d82b14aff3519402e0022673ac5d7dbd925a14695f5f2250f286bd098c70f1355c693ab632528917978cdecb87bd1ae9eee39a5480fcd7778ee6bc383b43ea41303579295f165be4bb09106f583b2cb14e5349f469620db9a1b89e411f333502d015f3de87bd10eb8c5ac9d1cb25fae215499144956180d5b1b5087e50c1d005aecda1332fcc7422669411906665c87ef786dbd5f623cf804039aa423fc16c1f6e8cafe1a4bd37f6263925a0c279daf947de2eb34c13321c7c0d2f1d18c679394800cb935aae10dcdbd2c60b4d0568223b115fe42c85bf932eaeccea6febc914f8e00b3c40278c3eacb746101fd06af3a9dc1c374cb24bc75560fe1a3eb2803da539e080a2937c598ee44b19780b990a200cb5be8c4274a07635dde8d8da32b6ab25f4aacd01a44a459ddcc4210a271432ebea617bd3f1f043ccbd11c0f5d675b9e9b954f20c307d49e2376f348f99aebd01e0771f695c6b0a81634c44f4de86488a4dc2016129a099f20ed634cba6ecfb99356ff5c691a5a1270bbc2f0019dd7ca13b305ec5048eacc51baa28bc3a863056ee809e4e038fce2baafa2dfac3d8754f89357a92e80e9c10be86f31b072ff59c77f5c72a1f2b878dd49ba2e67600aa5beb48b3a4303f4f085e29a4ad79f1b6617f47f31380dc51327daa786bd3d3565c1a8cfb8ac7b81723301920ac929fbefc8c364cbbc78ab88e31be4634bcddea0ab093f4ae74bdc9f554898bf06e57272b773f303fb9c9f2359c265728ced47f387345363246281149c752262d1ee0231b17d8c5630d73f7171ed77885e55ba7dc985600d83fba6ee3e680e909a3ba868a8bb9271b576f0301155fd8f76c5e1b8e05d955c736df3c860b161a261103644483c94f44a12e44ee59e4bc103842c689a53a5003a47b5e4980fcdeb80978c6156016f0fb136770e90de07d0d0ffd52bd5520065bba333a44da7458eda195d404a0a6af35daf11cb7b1d982688c89e36898e058a56982016cda93f4836dba93da92b2323ae6e2be4a879614a03a7dad7bd75988d42d26fdcca92e9212e69edc6d4580b0eec0e11b332e1f7129158773e688bdc6ca3ef708507dd8c5c56527145a6be5a234a3a4ce57516240803ceb6bb84755f7b1a8bbb59777a67bf9ce91aa326cb80420231ccef0030d9399ee39b31ea5cf8bec8806ac94168030ce20f011be40feb31b7651dc1491fd20f1b437c13c40947a09fea42fe7a9fb7eb584288788dc4d40520e60fa2f1888273f8142ab9608bb234c7187ee51d733b6b4afe12d9f54baeeb789244c660addaab4e13b3c31abe6079cffbb8d145b06d38b7aabfd83521ebbb15e4cd300fadcb3fac11b0b191ec54059e8f61a3747daf2e436c0f4b8198bb6301ab0fb186426c15fb7392064dd8020f435bce4e64ca62374834a3e1151b93fb96c007bb23ca099fce5676a5ab7ca257d21c6d7e086e786f6b4e45580f87cf5c14c32eaf564d655e6a82adcf51739ccd8965c5fc3c3acf35d8394f2a92768e7832c6a5d88e0425875bf8623b66d703814e327db1b7e7ac1811702bb7fa80168238a65612980bed963f008fce9e100d74b663a94acd6afe19eeb14ca76488cecbc92a7c63e5addf68c29fb805ce2ac40a1ec24595f02b65bc11d63efa8ddee9556019ab57faa7747aba549d1e61e3f78ced439d3bd9ca547ff353ce78d684bf37b930c4ea2b12b33d45469e79a771fbe8663b496d2421d75aa581763704e0716116e3465d202becf7d97b031ba393cbab6fae045eb75d8077b4300fef3e2e14b4fee23aaa5a6edc783d857c4acb199ed00a3ddfba8f68513e30edb2456a71ed4de4ea2bd0a5df4afe1b74f7761b051db79184ab18512915cde9b1e5800c4c19507e6bb8ca8a55e67645507d84603efa7dc05160162b89f6a9807b77dced01b215e8cb387c154c8f700f47b90c91e6e79293da7f1f4839420d9eba0e3cb2180d1fc7d22f5992dd1e3320672ed09fa09fc2ae66a22409bd965d5b7538bdec3261a05be053b6e0a791a68c200faf9bcc59373b6309fda4525e081610fec856da97ecbd7702245034c41b462a6bbf8a4976f797130fd0af8009db8432469dd9ee4f96e60461c68bc4fefe5e4c4c634fd1a2e95ff76f269f99534986023dc7e0dfae5dc0e1d2047a515ff893c47ed311fa3532e0697216cdfe3e031406bf05b9b799fe81f05dec859d09864aca78ba1f54b0528f3f03b3e04e307b601849735f579e105f9ae69d000fef9001ccfd086c37d9c3d3ede482047bbb3446102c0af8fbcdcbd20bfbac666d945cab1ab2b7ab34b94a75fc3bd1019de66a5e9ddfe02b26e177eaa2f1246ba41f5357c03e1bdbb42f2b02f91572b91e0383c0ab6e9a7b5afe1f9ca32d4f3fd834f2f959f9ec7bbdd11f24f4800dc7f7f2de8d64b76e6bafcddd738b329f0b4882e6f3c01aad0e60b2e610bdcd1e0dd0d5ee5f873232ebb02807c464d441ddc3008a6bdeab08c47db7d4e0fe265035414cb407db736564bd69c99bb4007037d0f86262f00909f673af4bafebe307a65a6d507918490d087f7df8800e447d699f99672c68388298204ef453fca78532926c0f951ddaadffa02e7a7e03333bc758be458b7d4cd67f26bb80553eafabd8dc627359483aa49b84c3552a4bb80fc1d777b141b33f86c9a7ca5d7fdc244c4124e1e9700f9d81fa1c6c0e2d01a995dff4bf979ca9be4a70c126c5fc3838c737143c8e89bc37566268b1c4d9e9f65f601de7f46d9f8038f5b41a74a758e8d826053178b3929407ea952fc4045001fcb7f16f87650e94e128b7c723580ff833be1e9232e02e80236179d9f045a4eb82e9e018c35ebb3cfc3391ad84967718d5c43da33b35d5fe8887d1b906178e5fd8db7a73e7019c1c1d1d448df96d9d8d10cb2421c5f92d05bf9514567fbc0ae5b64aa8145bf9d7dba09ea047a2c083adc2babddfbff77be43b772c5aacb41584eef7af609864848e6366410dc753cd490ae85648e9bfbc35e99dfe51581e87cfded1bfce3735dcf3cf5c7d7c76d978c41145463bdcef7ff1d83e1bb2662f0c1756deddfc55f488bdef6db648715294e68ed22898132e2b593882c8b82b03efe9f79ff9ff9c0fff37d9ababdfd6eaca0f85fbcc9103a8e09827208a75a2603f9659bc455fb8bc97f0048e875b7d3883ed5377186ef9e385798f43810e32b2c5df3fc431c7f37d820fd330c3625b52621f7eb66d0039da85d172adf3424718357002e015139f64918eb2b568fac344cef01d970850700fb95ac84c367aba638afe439cd0787b83579baa66480d0c705e21722abf78a9931cd9a62ae9882a04decd8a4be86279f279de4c6e0517f282b53daf799697d383c0d601884304ef06a0d288df21a036c3c9d85046d3b66005b35950b8112ef62689f6ede719c10e2c6272caeb9178097cd75d124eff35e0f0ce0b588f7d6fcc8d99a060881cf2450eca9c1910e7d8b34691c62e3a016504103a862a96d2e7d83ae4fccdd9c2a9732ed1e763e161c3f20a1d5003bb74913d1c9b938e58db031e5c0736e264d40dbd7dbdcbe8cf45757fe0f789ce1e28fb976845a9301f4e211fe148b2b386eee3d19d25b85ed69b51cb03540a035386da64f686b3779216e46674ff448925b79ef13607f029e05c8c0b06cf471622ece13c2d293d8b90014ded26d663b99dee957af1e3de9e411f4339c3c1a0108d45bcf5896ad978a37963ebdae5ed15d7877a62a0688c5caa598717e96cd9cc7e3d44b89ca54a8e3753fd4fa6b38f71399a85e49d1054994bae759635d75222398808c9527cfc91fdd1de6fac056e614719f95205b72b90f60507af8099410a314aa452b12e34a92534ee62bd30e70d26f6f2a2db5dd4c58e5b3929621b3788dd522ac072854f174afc928737efb21cf2c5cd279fcc1edfc17590085f2454cf1a970f0fd14a2fc5426faaa8aadeea1e7625fc3e5be70fbbb7229887766ddb54feeb3e08e0de706186cde51713a54e68034f767c357cb34e16a8b3e1cb6010c8a7294ea54692661ab981bb73f3e75bab39c096df835bced027751365460cabcbf36a83a65e7f9cbe03e80c2d64f91b883630fd71aed365ce71e7efacc98ba8314602c2674bdd5274e24bcd7b23594e2a4902c9f550ad8bfb8c5c5e7563d20b1bd172c96b8028ec83388de0085192dbb392c8b60a5135e08ff84e4758d44d93dbc5900ef1f592c49f8207d7e8581cb4367f64abeb4200f2070591cc880a753cd43242373087993c02db20a02007749bdf969cfada9d7e42c14219b4f6ccecf0dc46c8c80b4abcdc5565e366d2e915c1f21da98ed4e2a27a09cf0d30f463c82e88c67f0f1569f7d2ed3d57288c100fc21c2a233303aeb78ea44e0f5e11c5de2e36b2f1880abe5f3b19b5368cb6851305c1d8f83619ebf55fe342014a8b31f0126b49b05b72b811cb98295d857cb6601105666682f775b5e4a73ac251f6c60daca3e45c78d0348a84fd86674f2c3e143fe64481c28889ea6b21dc504383f27fa2ff036d0ce2dfdb5e0ddc9a86fad73953c049c8f68777dd864493a37ccb8f50e623c149b291378c0f92420c0f17d69ea154b7c66309ee7df46bfebbcfe06c06ee83f867c2148378d150ef380c72b9dd293850108f4f3394e2cf76d3919260ed89ac7c7af4a38cf4201250623bde45de43bed1c0c9624ce4e3a59b1b7a6dd0106c32f9c06176b280b7a5c9a399a0c996f271aa13b01edd01e5387dee5ed4632d31fc98016870e50ea731d019c9fd72eb07d8f723a7797b0f7422222ee7c16355805ec5f4e9f2bd30a3cb9b86b13c7bda56796ca2c8b66315fc3f72c51663f1ef12ca2f1c4103063a19fe78b3d54fa1a1ef821e396e0c48547e7c73de73a7664b95bd3457b00fcc5bc359795b4765f08ab9836367202ef54680f40df114c6db78e9c4789db53197474e71b287c791b010649f9a1c257dbbe7507d3066fa2e4a659ce3e3c8ac404c80e777245e169ba5b2d83cdb391c5a0db555088003d9135c0734aa50a29848e71a67131a5c3f53c708e80da6bb7e9e5973fe131ddf2ec0aa5bef3ce46d06c5215204e84ebfb4331cfbdcf0973c2b9596f76e9c65d5605e02f549f1ddd4b76f8a52ca1973265f5de90f0a9a9018260c409692edc822c713353f260aeb637b725264801f451c6e7f6495c09ef79238fe0a9031aaf53797833c0fbffb1dff2e1b0a0d0cd9198e1d0ca4f5fb0d51f3e53ff1aee4cfd31c43ce5e0ca4dcc495a019bfb0522261a40ddc1758ac555f03cd6e63b781bd398d079dbbba8005054c5c554c76f3a7750ed2aa3e87248e9e49477ce00e010983792d3ac410eedad234dae8dc474a7773a170c03385c2cabc3c9c758758ebb188bdc70c65be763bc01ea4026d90add094b679c19943265fa9dccca74226e00fde9f1bac9206a97468d256458c238e7a0e6b24bd101a05f29d81aa4db918a477c7cbaaf440dc15f1c02010a7bdf9393789e57d8b702d3b7612acd795615313300fe3de54b3aa8a432a3fe7a71ef3e858f3e0a5b37dd2200f4074155c473e8b3bbcf2e9622f4887140b4fc1780c14683f5e141fcc6ccbdd3d6148ea94d2f0143961b00b888687cb8f57493f0aa8fd24c8ff9f951e77008401d7945d341f636b219cbfb8b5f70578318bdcfca00807f969420623b9d232bae941f93dc503195ddcd6406c81fb74695c54ee708a2063b3d06ba19d8cafdd5e3000e09011e0e4bff21c6370b41dc215d75b1495d653b0087dc3a62db137b5732527add35e5aee568dac7e667c85fc385d6834db2430e91a96ab046178feef4613bec00a223cd995a85d73ed2992db28a6430bee4a3f6ae1707f01f6dc187cc04b87cfcb7890fc8123d2444aaad350006dd1d74c2dae5b5971920675e14900da76e137231403ef5c197ba3fdeeea1e75f13972dd19b4b2e593403908fbbace305adcb4605dd3b091f38165cf92c219e0012af06c51f4ff285dfcabb113d27611357c6f2d14cba12108f14e61db591704f4d8751728cda88145a8cca2b106090557c9eea76ea720033a2e117d4945b1a50a3731f60f04aff42acc27b78afae945e2dd390ecdd0d4b1a4d00fd44e98627bd5d41c34a781c633ede854ae8680a28327b695ad7c8b56f7016095d8e8fc952cb5b13650568a7ed468e7cce26edfe591e21d0c6b1f6e1d3933c7780c1bb8ff9b99202de8459370d1e7973665b9f52c49ccfd7f0bbfa360d67bbaad40fc6140ff7b679b90d17c801f4a320cd8e73aa49217638b1e8857b0589bbd7a1812a607f1735b7e79fa9ddd470455a494e79eae24ea8a40b75023dfa6d160374ebf5b3187e1dfe68186978a70f578ab8d6e352691d8e9c9d0e0e29ba4e72822e55eb6a93b5ec28ceb05ad4ac453fe84c3fb5f52925baf70f0c21bfce6220f1d099284a420bc563aa087d49588ba5dc72d0754d43c83f398b016a0ef49f3dfeef6431e8abb92b39a989292839bbf1da1bcacb29d96b193829386b68d8d939b3ab99d9695a5a38de95b433977371bcf79fb3bf22ea8ee1075b2c33c4d91306549f7617b2407b9419f24787497de82e624f6b755c1544aab2b73edc924b3376ee32ac7f859837ac8f5d2d1421867b24a88b4cd2740e39ca7e30086d63ae02eda5e0ff585f7f498e4b0cd4da477717feffabb341f9530558bebba6dd0f92610f0af97adb75cd7afa10ed7a80eb3fb3d7d7dce7357dff684f43872334e64ea25035ee6f5ffaf7ed8460f602303a8a22591ce4e1732984f2d6801a6cc33889453eb86bad0fab98f84fb66b8a7e3ff4d5e87ba5b04020f04fd577fb07ba9ec07cf6fe5f1e04b3f42f30e6474332c623fd5b8df990701e1a8275111ac24707ff9431f7ea3be387fb1b5b25ff339bd583a9768550c0fbb64f0f8394b0accabfe1d6e8da8fbf1f9bdaf59c212091367f2ff9bce00a4e1d777839185e2db49d497a91aab9a204e8b98b908af82a2c352d63b2682dbaee9e473366ca65ce312de71826c75c91fe1d6dcc74364f94d409af75a6feb0b303e6624255b1d1e1c56f3750aa2bec9ce7bf6f7b4da200fe03ce0ed1ef3b3be0fe19ce0eb87fb3b303ee0fefdf3fbe4f13d47189b71f586ac14f4e56b83acbba80b8ed671e69e755b765cf3d0770c2c983c1c12a124d156d6d4d08d7775eef930b2934419d80f7ffa75d8ee39171cd7f5bd1fcbfd08aa6e9047af07ff4c46c93b87f918e48bec05836579acfb2bd9436c4058ac8f3b55ed845dde7cee392ecbdb7d855a5f7333a22e991e796df17390436776e47cea1b063d115e3f5ffea88ffd13a228ab2be81811bd97d7d2b2b63c76be9880672ea26924e8f24e59c450d7854ac55556d1d25d4cd2d1d1df81ef139982acb9add7b64646a67aace63cb61f61fa4230e78fb6bd18acb63844b2a6406230cec69321f50bfc8ae7ed949dc5ff28c31169de14ac5a78d90c43a911ab62cdf3adc4c9941f616aa6e4aeb1ba41816c391e01188d907bfa4ca3da9fb3f9e2a875106f5052beeb75f137e2973b1eb84b6e97ae2faff33a9727844d4c8541d01ffcc54b9ea7acc3cba0de73f3b550ebce9ed4732e6fb6fe847f95f99e45f2f937c1b6ca8dde9dd047502e31a048d1116ae6e6ff4f7be36c9a43530465972cf56af674c00ff54e4d08c3656406b3bec3889e3d047c7e3cfe1d98f0d7f55bafabcc43b6be45d4aacea447a4d1dbadce88f955dc6f7d7b4c0895cb0a5515ba9594d72d7bab3eb90098aa246c8ab87b4efd32ad9593ba9c57a161aed2d1ffbe2d245cab9536052c20f46558eecc2189482b72b5ee592b632f22d1381fea9daee7533579bae77fd8fc190f71f923008223bae32083f4ab00dce17ed19f3c73dd01baba57c0ef540bb6cb05dddb66daccae6672c883fbf7f9480fd81da71431d4de6e7baf0f784159a2e8e4385554bb80e7efd0efc14e930cbade5492c90793a3bfee4e531bf3a7c5b146e12576ecd61c007d18c55e304838bcfac88e39f64dbc5b70a624070ec0bfb01c3865aeb26167526187aca6ef7ae897f2288daac7b6282c3e1e9a9a9db7715a8fb9642df30a711a0eecfcccf734a3cb70d44984a3953ef0d939c08a39e8752ddeefc7223ee4b197ba688fe9c39999f8dc2faef286b53f5bf41c97f233db44348d559da5a46363f43797eae92cbef3b799345c22a2b61748e49ad12589d44cd877a3bac949baf3a0a877adc86aa67e1229afe89418df7c695476fead6bd7f55c86d55c762edf3864716b01fc1ca9d1f84f2b9d2c4bd890d91bc4b721e1d6a01ca3192f3951fbec96fb0ce4232160cadd63b570b6ee6fe1a4e77476548cc962798c5bdbf222a3cfbfe7d3d7c40500f26e3534e3c38645213655a09b1d42f7885e8cc001b89904c6fdf5c692f37e6719ba481419efa2704444009cd2edcc80994655d35c68bfa50c6a79d6422130b230045733085efd9e656349513755e996ada057a022f20687255e099ff69d2b2350dbc1f5ad3e0d082d8f97e3b60b79ae96e9def441cab8bd3ed3173f304f274dd61f81a1cd58dea093f0d7a18efd3f65129428027b2bc0b10b4b36ea173e331786c2140c3b11163d384c71afe00707d704f32cb874e457576cf52be8acdd70d0eef64007d59e3785ad5e992f99aedfa72cc10cf3bde0accf1033ab9bc930d845d5946bbb366d6a822378d8dfd38361aa0369754f145d51329e7223f8ef138604ba2ccbafb1e10d456a2252abdcb2e7b9e288c4018cedad55d0d2e07049d64a77b2663c948708badb6225fc95589ccdfa60464d9e9ac12ac5b8342dde45ac9224e3418c8d5675c0031dca8a6ac640ccc95ed56d65c1165ec292597dc6c004ad4a214795b62ad49684da8556da6205a8323fe0ba0b65fc5b0fb035c663b0519f54a14a309196a2ef3e86a8094c52e57806ee1534ed35f44fd416c4ff7ede9262028f565f1ee3b1abea4c3ea1b86d8cf43f00903d006015970a1a4c9a871bbed62d3d4ebf328ac0c2b4fd45c007d259ec12cb1d4d907ab562844883b4bd74a901a190154dd8ed00caf63ab80f0b33aba478b19e4cf458ba843bf86bbc1d389fa77dd97b5122159983f9bc843537c8cf3351cd99f429c96193b2f392ba6b448df20b379c20b90e513f9725e6e9a9aef928d34e6587f13344392de08082a4454a78d2e9377cc5854f2ae2ed43d527f590c4872013de5f33a342b27754be032176a0cf78e184c6503040de2d097de67a6608897d10523bdf5f07c15d7cd0d08fa9ad4e4cfcda9ae8b92c87df3a2e9aac3f315232089152476aef19e5a2891b27b102cf5b27d03af31931f200b877663411944d4171578cb6bf5a7c4f00bb19900b2a01577e749ed7930dfaa05d9f72c35270908b2e002b2008539eb9fae9456f76613e5370f7dc495e7f2ef06ec1f3d724e145b5b77f9e35687b2d478a6cd84a90840963454e0e213eed73796776a1205145467e5f49e3c0dfa1a6e03a3ea917a5e56c5a538e21d925eef55f3b21e50390cfe3495ed41ce53abbdb945dd3528e142c5b40d80a458f75984b3c80e5ab7d774988b68f59977a07c0d20a896f56ea47aa66ff42e32dbabf33dea9ee538c95940965da0c5deea8dcce0c6a1e0ba11f257ab4933b8629400fa2916f7def666c01d2fcaa45c75a1458130d23140d00d3ca7025f5ef5e8f1cb0e58f1b20cbbd35441164050fa56de7d699f20b915aaee8e73fa35a699343d7380d5415ad945aaf026e66c5d55737abd6b30688421eae3d7702d249562d740554d060535aeddf0796bed295e8024325b2de863b3d7efaffad0ae3b1e9411ff4908e3ec6b78c2cdc878aa16ba8e44b6656ede136defcd7c600d9166652cbb82d3324d42b57abc02f8cb983cd3f780a09d070ad1abdcf8ee9b623af39d1fdedf28684c7a4dfd359ca2d22abe686ce37603bff3cc5498760b69a134a04ac28daa776def7cd87a3b50a215ef04645b6877a503b2a095e11e25104bbb3dfdb2d3d9655d8b15f5d4d63cf56bb89c45705db8de053d9a781936ebc5420b3d6d162068280a27e609b29d80715b62fa299d31add4e3ff8fbdb78ecaab59f285e1c135b8bbbb6b90e0eeeeee0e4183bb0448902081a0c121b8060b091a34b8050d12dc1dbe75e7cc7c939ddc37cc39bc67d6ccb9ef5e2bf9a38bbd77ef7eaaabbbabea57bfe44e8b1fe52688a45b5138ae1c359de406cf55d79c5136780049879667a39c564d16687b9490a4b1bab4d82ad3b30014bf7bdac78cbeacc22dbecd4779bcd15c4405f29c00fba0814ce2e96d59587569f726016c47cb4062c21e507423ab718d7049bee415f64987303a7c7dfeebb1189d1fe5ba64328ad47bfe1554aa311e195d47e4b6432780a442d14567d9e811e5b0a548a9565154313539ad77cf00eb0f24df07fc9d00761acda669131a7a0eceac10407d157d91f06113a9179f278c94d5eef2a56ddddf7b03400fa91cb7a8721447fc9d9736e5d52fd3828e9245247f947f49ed37a4ddbd82d8685de3dd79a76ecc2f4593ffa35ca225b6734afc199b535de292a710e2dbdb463f807e2899a1240f515e475e122be7851052562ede1e02ecf793627081f5542d814d38cf62687768b4898a4b008a7025c5e0c34ee4ebdaf3432f96b31a7d736b2e7de71fe5b6c2359a50dfc1a4ca5f60a3acb781575e1308000a3a47501026c0f46ce465f65f09c7cbaf6c5e063302aa5c94f1753272d38a480b9f5e7d4a841aec398bf003a060157dd085ae7ac1d808a6a36962f0b1880e16a89101be02a6ce60b1b682b8cfa489df6a856a14f246a200159b65ba7a0c2ce2ec991c8f222ba903a5340d062b0149d94852ea4ab7851024cb4f6c2c70a310a0df6ace034ee2fa26473a2e72d24a9f1cc6aff59484e7a3a685009eab131df5de958e98b7a040d4b994dd57332b9b4180a4e056d211be0c57f611630f6d81f9429da271e53040d2b4b2df62454394fed005539200a8f60c73b5330db0bfab8556ee6593624fb7c0762e63ea4c707c393802b0df5c1cfb325419e14ae21f058b44cb6411e4c89d00a092f5a1ac6f5bcd6d23db110cb47e9e3ae82f520b0149872fa53831a0aa1cbb09a3ecabbcbb13de75303f05d8573cf56b69b174df5c05061f11b5dd13cd9ca65480e38a0e2bb45432a8eb66143f9d9468f6ec34677a0eb0ffd11988f41d7a5f1523c1566f10c70db92823d60b585f464886bc54bed1281b923fbdc4d0cdd7f0f590a1049c0b5b171abd3f74211f57307fea37e79d1487cd64ff51fecc84e6258b9333db5e74ca2196425db8fb326c2a2006d961d6d71c9c7b18fb396de2930fa5d5f4b227c08ffc8442392a88edf0d390ab3bb5525df4cb6747f600c7d3b456ec7b9111e3aa837e3cbd2e88eccdad849abc1fe5fd489b35ee78b4f9ef0ee286e2379a748b0a5d01eb3b8f9c566b6d78e7d4825b21b3585e7d3748cdd71d34f4ff27928424a5fae4b7832e202a038253920282cba0b01b21def606b8f13ecc9d02baaf7c231838f84398bb01e53727a645873e5f3ab612c24bbdff16e8f2080cc45d0d0d020783848182868380057b706cfdbe88ec2fae4727bbdfb2a2433b4728fd5efe2ae4f7f249a387c9e756ffb9f2b5f9dfca5da814ef0b3604c72f16839acdf230992bc599b5b21023276aa36a6c0d4e68e6f423c403946b3d07fc78cf172b557ac22d90f0705bc2869d8c24efd67215cf2e2db2f5cfaee4d12c0d7165945aba7697685f7cb76f279fd536681ce8a8db8e5b5bcba99e8fd1625b354f7c73adf1e475b8f438b68ba17ddec9f72d2b917c750aa5cfae7160695df7f54f251ec7a5f50aa7744db147eaa2dedb9454723ed7eeab6329c9e3eceeecd9b1c776975b78f1ba3814c27325d072db82549cab94ef5a2bd51b754a36698ea453de6ae84507dc3116ad4fa010b0f128ed8df2d75e765604df380c4c1a9beb21888989bf2bfd72ac9a0a0e9168ce63b5ea76d8f9b580cd6b7fff6361379ca3dbc999d88f931645f07fdfa48589d616cbd55a35e5d7b3d430add7f90e060dff11020c1a121604050e0103f7df3f69858a87057f2b6f81fd6d7688508f69e483e4bde9bf8dad0af5ab9bff5ede1df04f958fa3df97300469acb1ca9f44945b81183460466c15dfb5aab28b290b5372b3f54c582971ff8b53c733225790d39c5b53fa99437b6948f5cc13de5767dfdbac7d91d612b23ce3cc6fa17115118dd066ab160e3a9d66064d4e20f1a5d9f80c9d64bd7b1c82a4bee67881699e965e783d328ebb26f78353aa9951b67817a47ca9b7834eaabd995c40755fff9013d5e4c164c0d463dc3ff6903b40fbcf2bf0b43ea63caa77f91c89de6158160eaa0ed213501ceb6db0565dbbf9f0d1994c8009079b1409ed6dc5a85ca368fe7797fa57d7d028dfb3fcd5aa0294f2d2b1e1c9b313799bce41bb2ca5214874894a62f9500aaf18bf1f0914481c9b85837fb5b424b57a91dce4702ed949a692fa01b71db417104c80c5f97ffebdadab35a4fc7d7fb1c5feddf588a0f04f703d1a22e0b49aa7d098797767ac9263e692a2a7f71a00665364257211396f9f0dcc7b2428375ac7e24d45bb1fe5afa6e5501e912d7c09f75ebbb0a64de2d26517d2036c6dfb085e328931d4e710b373b8324ebf4bbec605f84608552f21eede4772494abc0989d0af57d3111e2cfe51be5a1fd84d5a642697020da9d0fc0557b921001bb035b552eaf5329bc193d5ad48ec581195280939ec02b8d6c216a46bccbd38d1bbcae7b18739869fada72b02827e6a7c9b66a6ef2023fb83b488bb9aa46506cd360078bec78b2ee476358972dcc890cfa0631c9f779df102b68e9b6cc54ccc7a79d21ba15e8527ed343e7b660b8098278a45c6515d0f01846a9d8c73360dcff736c77c005e3664d0a233fe6ad85ba4b327f006f6491d727c24c0b536501e13c8f87849f7716e30fbdcd29ea9ba1d3b20a348467f4b6238f6f557011e11820d2d1190a14d23a0001fd224dc8274f58cfc4a424595a5cd65097eb63ec0401253be8d493228b83d80e826f2294f61fa9ef50df0fb946e13a4d1b0f570517208c996c6c87749e1d503f867239cd3d8b6929f123fe32a19743f3bf73ef300d6a1976d50c8d97ff37cd6e12926f4729e75e74ed637806b81685e91ec91f8214bbbb70ff7a30e36b2f91c5200de0a14e4f20e71057d7c17b36db9f01d5bbfbced1d2063f6a9ae84eb5a3fc9526cf8ab34e7bda269969d5c8081fef40189ca6e2c316b334bf59637141e72a4ae0dc014b3644ce16e97191a13373020b84ff1ba6c334402a03f39d2cd4ba64ab70c90fa7b03607807711a1f1201acc0851112c433eb8a62901f2d5743bac03cc5ca5901ae5307b5997a3bed46ee27e6a3cde2e45fd4caf26301aea982a84119b07563b145aae43a362e51e3641c7f806b46721f67b4a95022d30751e3266238e47dbb646cf58ff25d2af29bf21a5b5cd980219ea5c2dd7d936074209ecbed6815776f5d2af9041396dcad645f60900e80c7c4612c6f58320f20895b12663f849e4554c33504e001f7264b4b5a828283334ea849d357388dc43ec001f07e232e7930cb200e0e901599a7bd551a483b4a00e09afb34560f23b49e5e51ebcd7a7d7d14407f940d018899f71dca709693adb4b9fa70a60ecf6a4aa257df005c47cfde8ce330aaef18f7a17b5ba27a7e0a877a0b0b708d71eb9e323fd2e0a9454e4867965b4d6793671400b8cefbdf9acf42d0f03cf52f15bfc319a579e2863402807190d552ed8a476819c3906a270b8583a5528592000a7412692358d09e8894f446e0ada4b595817315db0242074c188c3c46b85cc4668689e1c93bd61477cf6801fa49d6df2cb080d09785d8be39b744b051def684175060ae74372a017a013bcfcfc45d67d172ab4396af1e603f325af1833395b308c439bf91d24c0f49b4b1d703caeb2830e550912b38e2a2c8d462a4a70f87665d150032e29eac5262ce8fb9c22cab667fd1e35d0be2667a0bc05b2673717cc3b71eaa9cf12bda05d37517c4b79a06902a702abf6e5cc1610b757bc71fa3626b8514e8be0828b057490e8ea6fe5c6e9a7f038c630ec6a9c0d6a400e05afb34cdb86757b78a3ef724d0162a7e64018f421d9042d4cb6247bf425a6a6157525d36c5dc90893e5ca60af87eec9a61f309485491e56fe36058a153ea65fd80d080844489a28893172f8a93146526ae89a5ae2f386003973bc701356f3964e199ef4dab039f04c9dd2d0e983f3702c8efe50c871375cf5f87151f960f3517d0015c2b0bfe57aa2d57517478d8cd8ba6649e098e518200fd6f39a3916a730a4345fa40843a07ebeb00415e08086dbcc64effdedf855bd5fdb2c1941522327aab200bb07e6aa61ea15e298b495c8de470895daf979e96970b03d65fe5f7d4389a9ff60ee04cccb6cff4e25cb6220005322f710820dcb5922e095f5b7ec81dfe2c1d970fac376235dc911d5171d9ee66e76179494032dad1c801189f833e14eb34ed63d8b37d45a38ca824314bade7003cf70ba2e6358a989de226b9774fb84f52890486df000ac07186620e5c25e5fbe1b7c3b40bac565be1701d01ea99e007f4385d4cdf9ed0e7d0248f3ee7328c9590072c10dc29294e3b04a8b197746365d7545f16dabec001ea2d3c9759bfd40bf470a44d5f19595583b6c651e600e4d418e0e7773ce37ccd757357f385045d2dc2e18d761960b7c7501e971981671d5072edb956c9d2b42fad0c3800744fc1bda05925b9c2d6ad0c248fe5413a636e05cc7f53995ace41fcce0cc11483a684c369839a5e5a006bc667c470faac82ba27e242b52970b1f863ec2c0600fb4f8ba7ad6dbd780dcf0c9376056a47f2c03e8d45fc51be2f8611654e51bd04057587bfe4e7e08115fc1d508f80645f40275989e90a893c51f84c8e1e5da14318e07a8b3366671c08c05f95ef25265d8dcfe49365ae31fe513e1a3da73ed1a1df8755e4f2f258cd8273aa494cfa47b9b1b92e1572f8eca3ddcc469d3ddef9fe7e5a41806b7ddb67b9433c39200e344cf5151131f8d8d7475417103abe76e5134fe6a04e91d0a00e23761672a11d05cc2fe4507252f1ad2719ce4da249df35bee5f0a22bed005cef2adf9f63851d501cda2f427659ce68c1974d03bc064a6f9353f736665784bead5728a01da449396900ea454840f280177123a0deb24cd24780ad1f0757e701422b299cc6703374ac52eaf6a547666212450a82118024a2bb55eaaca6bbb57405192e7abc0c9fd73352cbfc3fcaa365a440b7041064157b62a6ef5ed7294053ee01ea0105997136fbac7df954de626586b2d7ee9aeb460758ff120cbca71fb94d7d54ebe4b55685726ee8cb1c04ac0f1eefc8fd232a2e6f6645537be18579f406491400f6473f88e1181722f598a02990ccc4009b3591dc13b07ea0b2f31c8f0e3466227f248de90d43f1ce8dd0061ce06410fbb96cb6297ba8d86dd275b05125600b3b00a1856fac645e3edaf316318fce2cc765862c676dbc0178f9ee1e5bec491e91fa5145b5c192b7255514e8e780d0120c8bc719e7e9cbbcd8ae77f994a99d58e968d48003189519c126f6b5ff1403bec85d21a54f2bbadbcb999f73d2949703da4117109aff01ba44ee5909e654956c84102b4d529a7ae06907e281296df73cffbefc74b010f4efe3801ded7d598a4a7731b7a74af40748486b9fa9bb3bbce2a68eef012e2234fcdae61dd6da532180af58f0f49d8bddc903dc356611ac661affecfedffdee3a74ff7d720f5264cd1f0b7f070cc5640483b173a6113197fee3fb6544a2e6e7fe802c01f9827752d4650b42f7e0c7569a4f7da7c95f09aaafc5d06b1d7096eb8335b48c7e9f16469ffa80c425483452e4a87b52ae9402fe634e35881d36de00d4e5ef46a1cac90ab5ff47f08069f9ebbf070fce3dc2e5709e17ed75f207430a3d0bc660136b3093943b0114c3f69231e68cb112d7564c9c23aca0a3a4d795c200acd5d0abc2c1c17d96fe1fca9aac3309f2671261e173eefbfcf60b70b5704ca5b3b90230b01084f318b04670317525dafc7b6e7b3061acb0a89ab4a282a1aaba92929cb6a182b09ab486b8a19aa2acb8c27f6d5a3dd0aa01348a496cc9f56cfc09082de8713f6a3b1eea78b4eee71fe547b19093167cc62617d9d7f18f6b2fefc43fe071fce193ff7d2c4163e1cc698f336f02c04210b63ac042509772c102da4163e1eca543a2aad13fb7d225437d8813fab995fa440f97a9f0e7562a83a8a304b09f5bd978de5c3effe56dd4f6ca323615bfb47e17e6f7e4f8b99562a7666837e597b795d0ed7e0bf9a5bf629b83fcbf3c97f9db60f35cd6cfaddc9a69b51fdefedccaa4af7d9d22f6732b03c65af9f0f0cfad8c2f98f7ea137f6ed52d7bd9544bf3cbdb84dfccbb10fcdc4a2f0c3e5006fbcb1783f34fc968fddcca23467ed148f2732b0dc53189a0dccfade40b75b598bf8c3a6790304917e2cfad1c1fde889d36fcdc4a39f26804d6e9975127f6a474fb65d42912ebd49dc680ad17904f034260f747fef65f23a4595099c6e707ce8e0716420062c6a7e0beb2847d5754dde1f51a2ca35cba73a935cb7d60b2e93d7b0e90988d3b1ae01bc870b2b1619fe84ad12334907e71d2684a87ac1c7d5817ee03eafc6fbfeea3660413833328009c540eaa4d8b8d7a17395fb54b7c22f140594dc2fb52fc87b1cb7d5dedd1aa07755098adeaaafbedb21d6ea54bda5241162c196d656549fca728d0e1b6096bb6ab1003d3e2554662e4a2ef3006aab4aae53aea867c42e5332f81deed468ccd0d01a3fb0a1bfc97afbfe7bbffa4109cd96eebf5eb7f748efd09630fd67e2e171002f9a906ec576655e1962f4477d1bba58c7e0cfede419f86f2319efd36cc0d9272be055de0b3078440248483fddb7f8df8147f8211837fd804beafae06f43def87bd07588b700f7017f91e2540b9678b857ecff761ddf37d3880eff396b86b4eb6afba90e3e39fcf78bf5aa2b432e9fbc02dda7d469c82f7dcf7fa777f611e0c0fc8c2b3bd50ec41e9289895668557fa0e57ee49c640db4c7c2690fa327f33db5d7f64339a2ca70989c23089e7b36fd8ca8ee4595d811f14c1f9eaccf5a541663fc7bacd71b7c8999c773627759581643746e5677bc4e747d06cabf25f0ae599851f5b686b586e267e880869af232860707f57d366c284978a6b6dbcb19e4af4c435f169641ca9ec4a12768502b34d7c636ee447455853acf708c107bb7e3cddf677ea438d2e7390c555d5eff11faf1a60d9a58b6fc5f38abf411f7c8dbc31179e25281fe563ea0049f1a8360fcb3fd2a2509a5347e88d46b5023de9f6e325d61b170e136dfbc7d49f7c87792952b2bfc16d5075c17c5097235003ad218df52272f1647077f582b49a5c0f47bbbf728c2f9fef6e0ba12a3b33b6a05999403f720f53729169dce5383f7aafbf3f12297ec69be101c69f03227d5a0cab2c8ef0d6c04f994b7004f7308c325b9a1f6ccb6b71126b3b2c547e0cc551e09e9ff8bee21ec261479150becf12e20eada60815fb49d297629d6877b83023eb1b57cfcc38ba5e38f923f9774cd33a1a5585879ee3ac7e91f493088691af3a1ef13f99d47ebe948aff2719e9ff7efd050f794b64b775cf330059d2b03927fe7b2f9feb577186265ddbeeb38c94f979dea7bf70d9e5c40cddd868b51a82bc87dc04237b6c03679154faaab4da37c6c36750738416db2944cce7cc5009de9e2ac3319fecd6b257dec6a532f75ef564c2abdad980b5151c522a7cfcc80a83a95e3ec58550c15cbc0fe95c765de65b6879a1007503433bd8cca43f1838d4f175433a3938e19d97a4031fa5efe3a7b72e049e03c33a4cc823b4f977d9df031e679c9cf03707e6408710d50d3d86e02d26ec02996cfb40e67ea7ebe13e110dd6bbf663435b6d73e7b6767ec3a81734a2f508815e86bf09c95644dc80ddbaaa4693991931ed283c28a3aa0be530cf5c41d2987aeed58731a8b00690891d06adf6d71dd6e7def8868a60ebee1f1a46edf570b4716205245e60c1ec7da9252c2dc8e11cce86d9e5a786e93d02c36809c77c19cdfc92cccd03f43f573feeb76f16bcd480d2a535509fd723d23eb82a352816683186afdc0a2f0ddea71f8f0c434a9df14aa74490f68c46d21b5eea14e9dc8605cf529ad85917049aec95c18c3ede4d2470805f83e3110eadb3ba222ef75cd063abdd67c2a182b0f6a7f7c45ef854bdc33f773e6a5f7c25a7d94597663adda977129dcef6c9522ed39bdb9611762d1d199ca62c6080343e759c346d827a9d74663feb652e6b2a847d237f2e7e8723c5edd8271af2f7f2d835de6ba13a316c7ecd016431bb4bd7e7a4c22335481261e4254977e1e603ef6b1dbc5b946baa0b0b7935e81f55edfb8b190ddb74a0f8a4904a329d8fb6e47bbf9c2a4ae9538dc2d0b0ecc57d81316df279a26a380792c5329302b335fb6be8f3804b4988336d95b749c9ef54753e0d998b142dc54c11d67da22ac55f39e5484848605b6f7ddd87635d9d6a5054de5c1bb15c13a097f150fb7697176cd4605bf1540f17649bb72708a34d9635e7e5ee7550b06b544883e0d0e1e30503a3d27d0bb958d60165cfa8ba6c31773afa42026de68a36ebc803a2b0ff7faf7dfb2faccfc70a4f00aefc0ada901277a7138226c4e0fc714ad4323c6ed7fbf4173c2a7d3caec400cef8b41bdfcc5d0b0585306f37fb024baa42243c3595cfc40e7ab885b5dab3b55d9804338740a54dc0bbe402b9b4f4806c3897160fe2bbb6086c0091b96f2ff96e3f5617ab919d75f28b6ec1568d84ba9b4e5f9961377c331c7af3856b212270065d93716c2d5203756918df20b67da54c393c05d2a8344a19942c547d43ae3e621ded27b3a9c57a4fc23facdff4ad3efaca98e9374fcce3c70b03f1fa70adbdcd29f32973be555eb3f5f1b55f9a95af32be7d9feee39e1e46415c3e5fce8c5a50bfcd537613b4e6975c0d90d3adbdbedab19e37950e5006c7dca07c976436edee27fafe272fc802277c36f69ac6d9ccab596a2331585855855ed1c0d9c01a791f872c8e8b488291677d029ff980123648292f8756ea0efef4a5f943f597c149ef1b48b6dcc358b768bf34ca803ea45c4cf452b75236c3834332f2d4a852608d9bf916ac9f16170a555d60000d81af1b79b4778a78013b6d3ea792eb5cf55f597f2d1423dc1e6a7f2983a4c7ca828b27a55df5b7fc5b64dd5c9bf74dd760cc5e0c612d8abdad7b9a44d769601e6fef065bde8f11617eeaea4bbffafa3b4b1b8467c90483902995fa0bd8af2102f1f688ea171a3e5dc2884618ef33aedfb7179176799b267f973effae4f9012e59aff796d66c362d954e5db524b534fe0cc478ce72f4cb9b5a0b114dac051f2f5ce0251262083d135a9dc641106cac74d3994d80d72b8b01c435874a237a84efd5152c92ba210d55b444ff39653c531cafd96a33ee48df2c838bfdc8089c7006d6b12a10f4de2e01ea2d14d67cd9ee2ace1aa79fb38d362eb7dd4106dd327d2dec7f5978ced30f734e78321f553fc5e24ed914c454d82874d803c9d14698cc8fecee3c052c69a378ef63c0dbb1c26b17246e422facab73f7f15a1427559f0a1faeb85654d1e6ab935e9e67d6c9387f7a6cf80de0b3fbad7f0b2d2feeb454cb7c08605d13be28f8ceabd2ab64905326be9850af1c8ab213ed08395a4653913cbc62604ff0fed2f63a20a0242fb63570592544ba437be5ffb6ccf16dfa7bfd81f9ea23ebbd56d452a03b1a7b06c0f2465377df398324a3eff5268b39471e4031fd4ff298220b9359b499762c83ccb988154b28d82f98439faade266f5f5610c220cc28df59b19dcd27726ef105d35d3bcfcf42c12191d263ea54b8acace2b70e3247eebb17cc61d326646f9e875babfab5480c363e2dd13c6eef392a145a73d18f0b8fa8c7326a9808a2eed450f2fcacf2e51087b33a13dd38f7d8e0d3c314b6c8827cbeb281ecf8ff2bb0c33e39f908ee2d0f22ddc214e64f0e2494197e866bb52a5297eb1c1eaa5782a17b67b1dfba2f6ee6bf745f53ad98b666abba8d340cfb347a9d6e7b4168e43cf5bd5713624cc4ce6d593a08e863475a47a08b8a94213faa89fe8e1c4499dd0f2704cbf32d81add7ae57e7dc862c7c4a100c7aaaadde5daf8bfdabe6156003c33d37572d77e9a1cbdb828a2fccd8c186a29876588f7e9076ce33be678a25129848153aa633797f09a8926960cd591e5954626620ec9110bb058ff844bd3dc150bce27ac5df419d132e64fc848984ff3cc630767a05f704020872b9ff332a4aa6341be9fb84352c310fddc86d294f1a812f5e0006a59ecab12711597b2b8ec2da3f2a08b9293d379d010ad204b1d8fcd54cceba3db02dd7909d0be306735128ada8050dc6cdf5655b8f18b75ed89178d37c7b0a45ce7f03b2d3078d526efbe59726864063eed527fef96867a7246eaa182f3bdb9487db52a478c5edcb85bd92e77d929cf9daf2ace7575e44d7d6e6ae7ebeadd4e7374b3ed527c1838853a25c5f4b39976e346b5582c424a91c3f9d59d15bcafe0e7c2b39f2d44b1112c4a3dc8c95fa158f808f80f3df745f3e7fa1095dc4f1b5ac9d740cbb79ff250fba6a085d8d17767a19c5aa8c9d273bb17601c29bfe6eb9035d58a576f557badeaeac5c010e036752ede615c2ec360701293518476b614120e0fcbe5a75abd9fd943f0afbc3e53f301a85d2ed03b3504184babb5e8d0df4c396179d33caf76ba4f7f715a1dc1729fbd82ea317eb796a99be4fd08d74d338630aed889095b2a8124b1074a5f08aa5f0ebcc6391387a9955b2f4f89d251fc74388632a4cc186f35ec2c8559c620122731da635c8b66c5a78d8cc92e2487b6179459d0845d9c28dda99c80f14dab3547a61da9ca3069a178db96da9b224dd68f7f23684b7ab1a1efbca61eb20727b2604ded2dd9b8d956f860a75db564138bda086ce279add2543e52411d5c95f823bcc2c68d5b58eea24affa1aaef1aed0770b45dcd790b0a492a84dc10f0582c66e156e58d076347b11c8c07dddcf2511318d4feb00b4f1af85a8bb0f01094b731478d783f6899de4679f41f2be62c8838ca1e4dd19758aaa94e206cbf85dc38698bed1e1ac3b0d21cb1b679d1ef654a5f80b346ab808e02cef9bc818ef4a1faab014562dc55ddb04bc79fac7bc8bad54c5be152ee692f538705b1e1065b169644c8b616f87a05a91e95911c8a4098dc8abb2ec79f6011876cea581757afbe1df6ffa5f5d941eb1055ef29541b62572d2672bc314dd5c5a9d17dfa8b46c36ff2f9c2dd7dc3355b8d0ad6245f7ff038f57c3de0c61ead9d677c87ea1207636c5716c1d609c44014e023b5829bdd1202c19271a8ba3ebb8ab69a89f1089da4b5a5f7e0e32626ac01d39ab5995a85e9c4045e547814c30b7fe7f18b99f73871fdfa9de4fed93db3cb4dbaa3cb2b947cfee632d9ece58798e901db70afac18110ae5fc74c09933c3dc1bebabc6993674a9d4baa0ceebcc3399baae4ebf1b3173253ecbf4092e4c387b93b34eafb1f632914bf54507e32c7dc53bfd6fd742938f547bd11d25759bdba05d656135ba9b513990f75be4731aaca121db43b5afad51c8f50e1d6706e17b623e3abc52255eb4e8473932990e7277aa9d9a72f77f594c9466ad2cf74a2dd19c12fa8bcd85f9abcd5e9e618b6af4013c3eb86693351becffddfe1f54f687da37e8e8e51791fab58ecdf698acd52b9a5f4fd1e1c7e66d3ac3e5bc4e3392faee5e3313ce3b33327fca69e67df67d6d96fabbddd30e8b10ce1d712218763b411cf8565c597778b42f15667929c3bac9d4234859af48bc77f0d56c261d1538df17c15dc0f45a870b92606e042e8cb1ebdebcba04211550943f557bef2f1622dee5f2e516d9cddee54e9dea1dfd2b0cf6d2a8316d293ef0babede79dfc39a4ef2a71fa3b849eb9eb5e842d8b4a8aea1ac226958abf4bcd00e5cdf044f7c3339d4a7389b89cec05cc4ab73922d134bf5c12c2fa3dd122e76b514826134249070e1454da3102f3c54f74026ce587df598048a1b15b44e4b960af31a5b8d4d994218e734d8c9060d9881da81e9e3419c48db31c9d22e11b65e3c577e489795e73ada91d2e15feb1e6adfd482dbc859aea3e72299aa918df7cb56f759ea629af874b0a644d2d48825b03f72c8cea2ec05bd47bf7a79b4b6d191448524b257988a3b42bddbdb787309bdd3f890f3f3df1293529202820b27ee7e0dc4510a8c3f057b2942e94e6be4a6eb6eb2f60e5a9af18f1e45abccf20aacc79ea41d74aef56365bb1018a4c58a06317b2703c0a345fa0bd40c824d1aa3956f90bd4f6bd79e23b172dcdfdbd10808dda0975775759ffe2d6d91cccc22a0fde7544695f48076d005f8ff9fcaf848e0309866aff4afa4a45f929210c0b19e7b88e847c9ef337bc933ec083b74ad8ffdf1a3ff369cff0e8403c5ff1380705602c2145fe5db181ce642bb2d9a9c162f29de02888f98262e9b87d6f19d9757bb2cf47a4cd995b73f0112d191742888ad0b68232fd70d5b3872b633ab2cc1004097d26ba87cf72aafadf7feaf28370714882b5d51014032fe0d6a26c1692886b655b11c9b1bd7426d7d952f3fcadd41912a3732d1de8d0efe26538354a377a615002095d530931fb89fd069d4c0e5b96079f81b62d57c80f52537662121a1144ba029fc902520e97688a4ab08a811c6c0745bfb1cfd40f0f2e8805c2e5a68a8d91d1c50a303294bde37e3d5798c990979c3e372d688984d36405a266de9c1869422cfd332a7bad769da617b1644f680dad2c409b4d696d075223cfde3e2b77541a965e0e780e8a7e77c07afa001afc22649575dbf676fd665de73c0ea92d084b75729a878346dc0fed9cfde9b5bd7400c40fcf4de07a98925126bebe364544759278f570401030008763d69ecd6b534a8e03a1a2be99a83e06c8c3a0680d73b5ae59b164aae280b55711817ebb94e095e7f03d430721f51fc320bc387656545a7e08566933a1213df0c58ed70f3504d041fc3534c7864851c899fca1d9e026a288d7fe34e43050f153c377997e9e979272546c407a8e1707db4f24c2fc6ebc5909d478c27cb6ae0013c2480e9bdabfd4b37e6ad4b37f5d947ba1bf1f9d250635e00d3fcd9dedb2cb54708a34c9106b693b699d7e852c800202777091c49d3cbeb24ec68f465cad943831551608d1b4595450c8717f6c2821ea3431868b079ce6ffd0189e24cdfdbd284de14a88d6a71ed7e56718843e39600d43822ac27bd259cfa2874c7844eac6ded52fff4ca1a40fc4aff244c8505033c56595e2761d2b13d78d80f1d0064b999622dd18eeb4ddceecf8e88e75acfb9b88c05d4f2ad0ab3b34da98a980b4ce445ac5af37e39c4d2094811d106fb5c3b87abcc1dd62061378f54525b6f3104a841b7c830a36f85d932b7985a36992870a386b65026fba33cbea3b996759bffe44321e90caca553a267b82260755d4b3a8325eaa5a78934ad8991aad3f75a51e66dfb51de349657a01fe5e23985ed3d493b42aea890e399fea35c16ec09357ee039d3f6edf254ae6e8c45c5b41cc0defa52d2be6b87ce4edc91e5dfaa1e101560f9140a00020e0aa1e5b9a069e7a64d43571c319a9ecd082f3cfa517ee57db6affbe2bd4717a1d00051e1e2dd96bd3bc03e41ed89869564c1e557476e1cf959963679422d00d693d38223f1f0339512622ad632e93e8cb9cb2f73323fcaf3ae91bd7a3f1079ca77a2626e0d46491b25d101ca3710d5ee1fb2a8cfa071c14a6d9fb012dfd60b5b014e0fc847574cde5e61a54e8b98adb3c26c03baec0a801a398eb77d843e39f5f2efd295f688bc997a5facd503927c4efb6f3da01737518cd7bc224acdc0eb603a9a0069e76186af5d9aeb4e97234f21dcc1a54629bf0a9301a86b5a786f46f7bd16c82d1bf8a50643f73b0f550e0129323573ebb9beb39418bb819e19d45c2f15ada1a900487d45222491af4f9cf3ab710fbeb1865546dbd7d90280b219971be9dab1816d704f600fe5146758cde8da4b00f32f28261377260dda60cafe281c1527ce50ba0650fc58145d1eaf0c336f397f22c029ebe6d4ec568311b05edac623b28d44ab21f26b2e6f5d7d435396d46100124b97adcbe41e24ce8f59056f07eb09ee22c0ef03805c4f8412cdf6156ce62064324b98b2e097d9be9202f4afd4a73e34088254c8377cedd1e856bc475d863d00283b89de7fcc35238b1b6c6ba81b3d59593ae1f119f0fbd3bf2a0f4ad9c12be9b7953c7bbef76554e27135a0469318cfe0bc6a2da2b3eab565bcd2e7f755dfe38135d2b27a02e4d7f130a7c35fbc7a4297d57c90371b00206e95a56c2537cee7662c49510c61a1ec65bff0ea03cc5fc474dbab018dba6227a5556b28ccccc737ef430079eec6c77b86ee01ddc39dd43642ef3b56110f243d00723bb3ad9883d9aaf7c732da72227dc15b538740e2cc7666ff4107d66aa14f2ae431778c0260b07ab9805aebc3cb73a8aacbc514c1a59d23538c0608494d7600a0776c0bb2cc861195d3c72a9429c7e40a54b1f6112f403e96336866aeca7f1ea258c557c23889c8f24410407c9b31a9da593fd1ffd54cf0c9aebe356db035473d607c8e3f424def783d2efcc42bff1eaa7c041904e17bf4a33cf89009573ab4ea236d55ba3d45d20baae842180031ede1aca83f94702f184efcd45359a3d9485e9f38807e80af596edd2e39c1b0144bef5c209e052ca1a5006a84b2b439f646570fa07c56cf71d6bef3552149adbaf851fe262b00c7c904297b14dbadde524c22bcc4480cb0fe9e92a4e6b67e32b3a155c82c944c443a59793c62fda31ca6d692f66bb1951a7514f59a149d70541a5210a0069e7090e4449c887fc6d78997b54d70d3ce98e03280f9c9dc9eeceec89a7a9a4133746cd7049222959802447767113bf0db97f9978724582043b01452f62dcf01ef5747eced140c68d519f378921a2a67ebf7dd8d0b30ffc6f9930d5e5fb7131f66464af610a309dc22f000d6d72b5c33cfaf72622a359a0b17f327272abe9c6f5b01f383e5f1a8113a542d0fd8b8d5b7eee7f0ccf4023380fda1fe966751df666980809e77a1ee5bbb29ca4a807349df3f457c4833970cc9248c916faf2e96e14817607fd3ce1bcc5ceca92d9311e4bd79b95eccfb2d8000fb5fe9cdf4357c6298570ee286681ec2d128b7fbf1801a89d669eacf3a19d55557b70eec1c35847844fc5700352cd5968749f51c23e11670a46bcb7da0a73ef82d0080b091d91992f552220a05a499ceeb235a1f7dd3c201dfbfe7e048fe98c6adf211073d0436c1ec32cf730f400dd1e65755ac498db2a18c1a77265611c6baac77af00f7f3ba2daa93c41d2bb985530eaa4bf8b1518cfa00702c5f19cd728e1a3cbe8e1abf364e627164214ca505ecdff05ed817e2ac37be9bdff4e4c6e5f2cb51a958008c6f9ae81cf98ebd60312d816dd5b15e4052024e250008daf26689ee312687ffdcc5317cb891bd6204a123208dd6781d21c90debe905e473ec33b68257b9471b91e2a00b08ec5f406f9025d2031a0ff4dddde7bb0978e04913905c9dceba44f1512e59b42d8915ddaeaef382f084b6f161b9ad60f0f2d60eae24a276c6d6f6ff906b018a95898589e5bff8c794ca1f91d2f3441462185b9fd73a1150a3c56203f206bf57d6ce84e4f537b2dd9d045be9131c0585a5cedff3c87b48f5e85cee3be9de731958b9ba3a3d7dccccec62fed4d1cedd9cc9dedcc5d4ce9cc9d3eb19b3b593c353665b4e56e767d69c6ece6e9c6636c666c6662cbc16361e4e9c5c1edc9c4e6cac26dc76ce96d69ee6a65e2ca69c76566ec656f616aef64f5d9d1dd8399db85cee1d32e12e1ee257a586610804538b11d5ed3c012ee4588a7e4153b8decf5bda025349e6681137580d7a9e818fbba6f0077df6f3f47cc68f9b8ae0dfca6b157494c09d2f8e7fafb7e50294120e5fe8bdab1af0230b27ff0adfdd0375f7375af7b7d7fd3dbaff7fb990ecac2dac85cd8c9d5ccd5d34d8feee4e007d760ee778384bab6c1d9173487896089e1bef2f8a39ff1ef93fe0e5f9c72ff897e92416e9f7bdfe37ca8dbd30b00d4cb677b679fe6658034a2fa43ea7091edc2855424739fc9e178861e17f1381b9b90261559a3d4934dbd6718fe499e383aadabca89d9bc7673cee068c01e4069c4298c47f7a4aa0b2f25fe4d81dfd5e37c7c3798b70b56a8cfe86640a45589810040bf833dd99391478a70c7faa8754ad20e69df4f2bf4d2c44e580609385d8601fbb904644b13f01a9f0400a4f887b562be87bcc211ca07fd699dd8c46e068cdb43ecd6269b78aefb21737fcffc991905fe05a738ec7f2fa090c2d1eb54c1a684d50b4732654070f9b5a7fc1b57e567ea4085e25a471cc3da9e6784e5ecc6f7281d0e3c87f389f0cdc540c621f446c2ddc4aec00036aae497cfa222e7e8cc18cbbbe2f81e5d1363558bc706c7f2f124cae914e495043c7d679218de050e195296194b6734d4da106144ca988362b2396cc9f9569c881b866b1a5c9cf90272cfc7ac3cead5627e4a0ee2ffdfed7d36f98d1a5eef42dec518d493255f0e0cd76ab5bdd3fac6cb8b54dba4bf8f6411d14d6c93cb4167c43a18718a5d1d34c56ebbe896c1247bbc288dc7263117e5d22a94ed26f30caf5dde6053e4727dbc0236bc99545a5826e4d459ddcafd29a082c32fa7f56a643a2d596075d8702e5955000d1fa314a6f2ed893a6bff4fb5f4fbfddd0054a34b567336377346ee3dc5b176cba9186ffe866aac4e7e2ba490fd36fcb3e6c591b169ad869bee9245955257a6124ac5b76faf7f2f6cfe5ab996c8a3195798de7260225e61927c242a0042220124f0e23b792500c9aa46f376fcc3d5eff59f6bb506b60e7db19b43b27651bee74cb360896955dff2ffdfed7d36f0da9cf548918f2756477c3849467b01fb3d7f23fddb7df7f887e57cde752c011411f68e8b82732c8a0be5d4ae9b77c642abf64189cee5a12cf3ccfe389847313541812985470b123abd73e7f822a403ac88789bcbd3de8f27968e561991c178f7e38543cfa330e15500f71b1808141dc73e880be67d2c0dda3d08880feaf9b4e630941ca7a92b12642fa5af1ed0d5d3178fc35a9fff526751e75a2a565364e910bc9265a2d555242f3a681f81fdd9ca7c4bf2dcffa20be5b61d3207085406238ea7614d767646b1ac4a1b6fdf531a10396067651e1aa9f6f38d0b52ae9c44d9e448fec695cb6aa37d34b84ce4f7f118988b5f9be20f15933e2cb9fb568fddf52ebffd2ef7f3dfd16b2ec33ccefb93df671902ecebdfe581847b4bef647377f45b07decfeb01114de26e150072fe440bd2613fc00fb2c3eafb82106ad8ad8a24a728d452646b62e012e38c8b38be9239c1e8888209336160baa991e8792bee18da78deeb99e6af2c69fa5df1a69161b92faed9dd61f75489313d66414a605f6ffd2ef7f3dfd6667b5196b8766ddcc05e7fdcaf9c4cc9acf76e20fa34488d6272fe1d61f3484c2a1f0c8a910c1f8fdf9a1cb1e60a434a2352c6835537ac6cf7571603929e88b421973d31a96c4527b739423b1091a55c9372d9a105266903e0efa2ed5e0f4d82bfd59876a4dfc3e959afecf9f098e60226f4ef93fa79cde8efca5dfff7afafdda22f7b6aeda6ed83266b88e07eb84a67ab7e70f29bd39527ab65cde3fcc7e77c244ea4c3abcd3a16bee7eaec74be9686f837173be449a1976c1ababc7edb7c9d634f184ca05db430becd17977fc37c71c5d1e1b81ea8937d754dc870d31c47f1abc8bd4a4da5d366d808a6c668bce645b319956818ee22ffdfed7d3ef47bb8778d7dca7b9117b21a9cfafd21bf618b730fe99876a3fc36a47becf9ba52c3c532049c7575fa1a639b2313db6d4182662bf3c57c2da73ee86fa4898ac7bbea6f37a2fd67497b5fd8a2526ae182769b7a6a44a701567e621fadd7e5e158ee8390a210f3d104c8772d2f0d6cc239af88fcfa37ffb4b76368e7ffc8d90ba006a83ddffcbf5a35c36db93fca3927f4d75a79a1d9b1a5bb2deeccb1ad005d448c0df88bd87b81aa13e7504f44211c1b684b53a135987b1d2b49d20b36e4c7c73796d413568931d3989fa383448e081531fe028d853ac92198e33c1a62456e7874a3adfe962d9b2f8bd0e42c9dff3827beaa041dc23077d4e38a2583b14738d95386ecaafb120d11df16cc7044f817c9af9eefb47d541d31bdce00029e8da120df02d31a9342db234a9615774734d92ba5988a2f4973b3305f899491fcabcdbec624f7657ae5bd54f6844212ce79deab3f340d3cc25d4fc69a69c883dcbd1536293bcd9a70bd6a3fd8fad8ab256ec464f9ed1165df95ccebec7c4cf9ad6d08b7527b4e5f7f92ece57231ce27dc5ee422f27f7aaecdb075faef26639e2adc946a45390228b7a029ad6de4ecfbbe8e291481d9f649492514721239f0502324837c1cff2634aef3bc26c76c98ff7f0e2877ab6f50c8892f502b715c7f6f2d1460735e73de7e637135e7e437857fe85bbbc5c3d5b702b0c6ef3ce9d91283d04dfb2cd090c0fbee8edb030ede914dff0343afed24d0199b987589d345ecb39363bde92d5e1d7c8168d9e8575a64c4736f2418c718e4e070536ef83dc0412a51d4282a5b46556f7d6399333bc0dbf0d5f3a8b882bf08e01763aa1e952029a8a5399310a19f7b3475bddc881fe1415ef3109115fca11cfc07b04ba1d914c9b7eb4aa16c5f7529ae374e28ef3c95ad1eb9747b4570c1861d12b9ee36c5bf7291ecffb46a5a61fc9f5d6c51f35baf1a5b6a1fbac82c88c505b4aa3b499bf249f2eab24e9ca5b7c2602a84229e255fabceef38667c220e7b4736ba28061fd70420c7bdef642b7621ef2bc8cca77167c8d99b4866d938c56165738df1610e28498aac50625c64762f00be3bf980eb7f2303d1e08ece0ad2aec16e4553ad3b6cbb0d77fc45c7445bc30049e20fbe8b5a19121f327ddb4f068f78479e1820e315552755a8356fe9e9dfa2c827eeb152f07c14dd132ba90ac5b59e372140dd401aa504f9517fbecc6ceaf46eca551b6bc7932c62fc962b7f20f8a36b03cf20ed5862f2ac25567e046cf1d470bf1df62b917a7b6ad59aab253cae1c6c194586236442c7f450ee393f003cf55add851fbd2f0f546cb373becb972f655995bad088efd12ad049199c84ce027db13e877f8ff0c23ffd03a96802c70f3c75288ab11cf5e06f23e6bf5e83b25c31750947b9823f1be5c3ab0109b1685827bfe46e8817d04bfde0e2bbcaa4b5939299a375145bac44c9b757e0f766ba481c79f84a171931df380f1c358e7249e74ca75096a24c4145c7b59c7e935310fcc7cfe27a2aece43df5a574712adddedf48c5f8d9a5958a43c0544fb6eccd912c94421e755571812334648b1aee1ce00f42326e1b1a59f37ec5d37ecc625c75b02516286ba01f410b306bdde28667a6c77290879245f39f752717a00f409b64be4324ffaa612b54754fcf873dfa17b4d9a64ff282784c6d0df916b956367a47a51d42cbfc0b6860e60d65dd98511ea196696cd21c9b9b1982332f0795507701c98168928f4908f5f1b89cd5f7f56d49ebab394073cdf7ff4830e96e23071f1974c1fc1e806470c813a40c6a37b491dbba69b36e9909195576e9488be0ca43aa0ff5f3c440dd39a13c7e34c08363c6c17caea286e00ef376457ac470fe50c8352aeb0eb058d92665f9f01b644d8319a2772a7bb661e7b93754fd2742db238d1010958103b16827dde97884a4f29cbdeeaeba486192203d66b4183bc77d2631063243132a6d13acdc66670f380f5960393b2728780d1e16edc8442406b2e3113011690355bb8d430ecd5cde444f0823b5b42be1b56850a0dc06c4d9327a71e15f4394f7e271556f36ea9be207d1580397f2ea184827d42cbe3bf44dd97c571f638b9321850138d592d665931a612c91b0f5a592412b7085e9ebfe84779f178579118a256864f8516a2059c8d8ee85901803eea09a564427a5660940cab4318d3768664046320003500832730413c76cb941d154dede8e2a6421add0a20aca8260dc9c64b9cbbd68efd44295d38a9e1d628044035b9168d3917a2a9787ccc128f69eb187650e96204d0a3d07b46296ce545b38d9aaf961db526e71786b001de7f858f9ce2a908c6909ba372ce70ddea693bd40d406de1f882fa8c1e198a5668ba8555f6d2cd1c31d301e6df2729c2d76d90cf52b3f02627b40e896f4123d7ee3fca5f4cba40e15179b012d2d4ea096ca32ae9ba7d71f851ae642e6b7c158fc519078b8299bb86b847933a06407d71ce3d95b25f8421c4097f245a7467a3cc1be80e78bf3a02092a17481f8c8928cbcaee3dcf3e9e4a0f60fc11eda9f33fa4740c11b93f2f7477b5a608065503beaf0d0563adc30a3abe121c0793f7a40c419f93181065f36cce1b90d224c89f407598d24d5634657b5d0078bf88f6c418c554d9c5c6f57969b3a796a324342120eb3e58c7380c9758a63a6c78c2caa36d978408ed3340ff355a96ebccc86976364b191189fa42a73f708b03f693d3da2c492881b418c4c520eeca3a06d557e7958038b642d4510f8c645502659f60da24c8f61b340321a0ff338ec7f9fa465f7dcbda2d6dfc5e51349b490a00026b25955da05cc6c7716d998b9443a5d91d43dc1f00ccedcf60277890d933e2044e25e78288d785f76c32010763a7a65b43a9feefe45b1626f1d127e4ce262d6280f923200d19891bd2dcc2e18b82a560e0e1e9145c62fba37ca1a6c859b4600807cd514480587670f8d1483f80d9ff4347b7394cf4a746e31edae441b9207523d459006aef13b4c9fe292ade4cd5c47e049248d9079da96140ffe6b28dd637a8f9755e55b36919a8c2aec1af0402d6ba3a2d612b7d11d89217a96125bd8e45acc287830054d270d8d72855dfa737c2f484f85493d0d138cf6d137e949f661b6ec0613baf9c44f4846b9d39458da0d401d68f987a5da1c73aadee7c54a51baf262c36fc465801f45c711be6e70c550c021285593e467c055cdc7285003a4f1ae31e3a774484d662ee3eaf51933a6e1dc73e00aa4221d974c547231061da3f377832bdc5c8435f09b0577098d13f38c30cc6fdd8961526d7b8ab5e5c840e2025c624b352961e43af6f95fb7059dbc5122fbf3b0540fd0decf166b2db1e34ef25ce6da424085813630800d6e7c400964c2fac04d19ef57826a35417f5e2c02ed41fe5b94d0145e390aa29d4500144b36d2c6595effc00e79dd33a373046a6d8a2954057083657742f4ae91a40feb1ad0f7e1f6bb0e7d3d6dad2d0749bc7b41f7b9900a8571101e70e250edd81c18e174b42468b866ff3df02e8977a48675e68277a604220c323b260e3d6fbb7da02ec8bf6e6fa36a846bdc22b8a27ff26721d75fe25378052e4f35cd6e41e548b6644ea93888ff06d873cbc2900fb3699332a8b7e45eba62f472325b1ffe98841c107908542607be8fbf495ec622a4154df47fff92701a4a400fe9b5a210f252b4f7afb008c06e88ad64c86e7725d00a7d0ec8efdf1975bb54778bb066a5cc29b92a2675b8080cafc1db6f4fb2568a298096cd53769a0ee0a692a00aab3529869c4432ce2151b5f6f966682e622e2d623c0fca4ed1bd3745052997b46f85ed1f495d14bab8c03003d6718784184184448f665475fa6e4654acdd45a25607fa0f826d31ffa6b408449ac40cd5426a695b3c922603f7c69af60b75433f2f56cacc7a9b6d7a3d0525bb613601f8aaee0be4d9a35b4ca86cd8afb6d37b95272008aea57bd6ccc8ab0b33c452ef3f97a80e54a9f5529054065210f950b74c07a1ae76715f59ae5d0bab14351036a15bcb860a00b5407af2e41567626b9d39de8218f05e89fe048abe913878241784a1725c93c9227f1c78a6a3fca2d59872a9392aec0c72708bbd6914f7d212d8f01fdd390e1b31c183de875c393eae56ce59f773c8d33fd511ec8c234093f19c2edaf6d58c648bac1a3a7dc06907f47e30c435e2dddf282a6ac50cb654569412203a03e61fae5e9685cf15f653193893cb1ab92de37a404d03b6212e1bbb58b924cb1c2b1389ebac0db4493e601f6675416ac7b6f2022e50ecd3441dacab434858d2a802a127af6ead58b83225bf0d98525e1aee9db10975d00fb65028fae283beaadb64434162f8fdfaa32054109200ba25743009f4e6c26d132e924ec81df847ea5330ef87d42a8db184cab599ecc8f06198a5c4521d4298e03d69733158618691c7106a681d037aea524182d538d00549fddf730ddc7dddf7d2d724520b12718d2264688010eaa804f6454a5e98b612621f518359b5474304c6a80c032c3083ff1e7539310633f6e5442216f77fc2628c0efdfd1f978b0bf34f482ed73f3359b48015ce19d28c0871511f44d3052caf574b534c7005ce3d108992486db0f9ce9488cff4d9ce90f835d00ced03ef4de32edd71f22aad7ef74834fb5fdc1a0e1dd40b09060e0d020281838887fe8050fe04c873f8fe3b1fccdf8c05f809b6c8aff4eee7eeadbfe3b7904c36dcb43e4fd33bfa3547fb0fc6ab7d129e137f26b34b48dfbea3841eee6d8c37d5ae816b7f7e6c45dd230089c0c127f4ca2d74887cc2f20e414cebb518cd85f647aeef19c89503464f1a9ae712955b83bea9ec4ec6c9ec9bee6a06823131ab2e0cc689fe78cc8254396c06d3989a45a41a16ae109539bd7856d301d7ed3c7191704dbec8598e3fd2269b62f1083d0e4941b0b7e060bc7262a53df69d6efeb9fa1d0734ccdee9df479a8337e75b1f615b87abcedb5ba142486fe96dd2e1afe1997566f5ea5e3a1118d795b8af0ed560e891611eab0d7ea8de9ee5567236e09b03c7dcc3804d2a8cc50e6d5ed7589e04ae1fe38f3aca4974feff420bb60f4a85af9466b617ae168b4371b85e73bfb2fab3e61ded2274ca39aad901e38de745e7e68075d8078232056340f69d2f5e0ff567fc7f6bc1144f10e21eedbc3949b27e27752acd05bd436e7bb114488dc4a93dd0b372b0afc0c79ad0614e342d531adc45326b84878c7874571c003fecc98ca3a0709efa81f9b6ce815dd7bf40dc39c2977e4853f344bff3e9cbf7d202cb5d23f390c04636ae2626d6669fe8fde0f6b676d61cd68ec64fd879114d005b85638ea472feec580ff04dafe5592e9e7924cd03532393acccfeae2612d5d6e4646cb5a6a86ff38b8f1efc3d90eba80980c13e392d3adf801c3fc6984bc66ea61430bf18b9cd5dfd4f87241bcb1b64a58264fb7e9883c71357a8d8a945dbe4f06ed5905948c66f2c896da6ea74827b7ad031c64d46f0862fed6dd1fafe8fabb50f48f9e0521b6bb8d77e369b29b635df7702fdc47cd00bae7fb40f7f89def2316052dfe0f8fee42fc939f0f2b6fee6a2c6ffcd4f61fbc9fd3dac9e2e9636666657b35312b59cd67c65ab6ca52e656c672ea4aaa4adc96da8ebc26e23abc8e6c1ada5aca1a1256f69c6a72cfd8b9ffe78cafb091576b1d2193899d3364d7d49a00975e6259d08174f287f210c4f7f89b5ae59486ac0daf0667df7eb1d746c134eac7e98cb4bdf35370b1839427f0cbf159e4f3b92f38ddde1710fc68203ee4a6364bf05c36209868d2bd414c5597530f70c05f86eaed9be12de37431232d6db96556cea7f963b657f250d714b27db0fff330026a8b5f01ade52c2f73e8f05c033fb4eea9d23f7ad3e879d3f1f5773ddcb5a501fb3788ecffd90f905c0437fc42ead608a179d860df369ece0922d517323a233cef43fe6d30e9314a473be85c2b02fc1d885fce3941e13f89536d4d654cfe9e5034c08c974e7c0155a5c4b7832e20b17fb69690907f82b5fc652122796989138b28219cf171d144e41479a4d25c3abdddb54f66c0679cf613b4d3723ddd766289d08e9a0077fd2ed312e9c6df632d0960a925ada4d78c8239f0e6f6c8dcaacbd73078ffb296ffb3ada5b4838db9a9abb9d983ada5aa2497b51987999aad03b79ca39535a7860b87baad83bd8eabbd9985898ab607afbb8d369bab9c12bbbb8aec7fdff801364a0923fbe23d071ce4d55b337aaf6a2a50f06b6faa7f7f3be2ea7dd656507b9cbc0c17d73ba6ab9bd0c3faac6a8878f2c055c38af559012457449ab028ef35279b319fdaccdae03c931fc6ec3b6c76ac262b42683485fe97cb7c09df98efaf7831168ea2597a72f60b1b2b899ac5ac6bfbcfad64dfceacb3f67f6e459789352735fab9153e88a775eb1706db47bee6693cbfbced91c4e8a7885fde8692ac898fbbf8732b1a4bfa37a15f987c31229688067ee10226ce4b23eb1afbe52b461015fa7fe903614d300fc9cf3cb1d0ca01c1ef9f47068f9de036428bfd4f63890ddcccd5610f527b3a4ee3a2f4fca983a96fec0efe03f7f67f6520fef3adc6df9d81f8f7fcce7f460662cb2be2441d18e309f92e2d0feddd4c14e63b32bd64c4dcb51e3e3f3caf5edd6e0b86e8374b47443e31f4f11773fa2e6622e38a3b1f571122d59a1003ca8a4f8ffeac0cdb96163852245d93fa352f2748fc62189de28f391ffed2ef7f3dfdf62d4fc37ba944514a8e8e5fd8d1e9cd37620afe874b632193de029dc3a4d543f43b3290cc9d627a9a3d0897fdd47ee3c3c4f107705af972d95acfd432f34603b7652e37cd82006c21814a61b8e79631fafed4b7e739abb897a22ad233a4bd173ee70fcab0bd0095060493c64e3482de568a42ca3e703d79a8af06e047e49736b933d19abb2efff2385084bb7e62cb99c4f49efbef293ae6b7f4405d590591ab3adc37a7ddc4b722564dce4f425a9d39f925f6832a8b917d262dab02e65f7b982278c7dfe708c7b847aefcef830cd2000373036452380844c8254d08a93852f31b2f0cc1c619b4f8d18381b965fef83707d7359fc7bd2d278293e9b4eb0a2a9737daa8d7fec403398f5ae4422e9d0c8a061c198f1ebecd0ee9197820b4384f064cc04bdbe60f39d1341548297a35ee4494ded067484c699ac7556d8fcbc024a78ffd82f8a70b0958eed3e5bfd4f52f75fd13d53573bd8ad6bcdc160a3601443cad6e43d49179bc20ce10860b13f32522d227639e7d3ae8e589359293aab0469e3ce8b40bd1f80c5f1ac970589bd09b870b0b59eabefd2d6828e03f7c1c2149a93ef9eda0f19f8a5d86eca4aac2fc8d2f010e47c125eb2fe7fc1f3be70f5c87bd331243523f124632f38b59f7d4bca2ff4d9dc6bf0d673be84b38448cdfdbaeffaca118007041c19a500433e5e2fd37b9a0e42926e19c616829deab0622d345243b9c3e4feff90ca9931b45d6287fdb1dfaf5ef714125d8e7512c12728dbe815cc8b71ba4d5f1197b9af9970beaff1187bd361787a8e43349532f4b5b3733050e3579b967aa5ef26622165a8eecae2aa22e4a96f21e4fb5e5bd246c9c78fe855c50a1c9aa354565fc62445162dab08d5744ad9ff91af7fb7da4b738e5896776a6d9c1e33c15875317ed627d7905bf623af12f98fd7fecbd0554555bd73f7c38b47448a8742922d2290dd2dd9d87eeeeee569090964640014929054949414010944652baf9c6bdf709f6f5ea79147d9ffb7eff778fe170b0e7596baf9c73ad19bf395f7d7a2ab4eb3724ab8c7eb0230a5d05b52fed8b18808e6d5523bcb27e1fe662ec15e6e877d55320b7f46dd72f9441dd87820d5fa8a9c82d4b889963bf503dfda5528cacb5493811f38b1a7a6b0417bff82db99878ac36dd17caabf514a57cd23fbfc57f7d2f3a5fe90bb5da47c9c6eda43fbf4567fa902124f585ba4ed70b13c7f28b367cb41034c9fcf35b3caef6dde70c7f7e4b28e6cbb5fb45bd146c4baab05ff68daf2369dd16f8f65fa0a10f897e3a0ee945ab541416635d51fbdd260fb7f7852564f6e26208ee0bb58229b55103fa988c6f0a423d51de8ee99d8647cd5d7597eebcdf48c4d7ead0d0b96273e61bdea093ec32db9a861c16d83ff83d6208a1696492d8a505dbb588478b459cba3a8d38f7c6df5b0cc181fe1f17438872a6fad68e3656a00b8b21351177291b53214143332535262b2621c7bb32ea4c8aa6aefa922cd61a5692ec56ee2a66ac46e69caee6322ebf48ccfcc0a913daa919dabd4a90c195cdc888c388451fc262c8c0cecaa20fe1e464353660306467626165663266616360d66736d6d767d4373436303632366061646383b0337018711ab330eb1b185da8ff0810672b2e46865fb63e04f027d8de87bf7e3adc8dd01af53c38a25919b7f9aeed5a1aed43775addf6808776529986975ce3898d6c8f5c8b04358ccd2b898ddcefe16ff927d0090b6667db735e85ae76bae51b8880c1fcb2164ce9c939c070b109831aa0f8db9d11f31ffffff3df3f9fdfde03d22485f07307ed6ee12dd4ddab23c96b39f21a4256168d5740f788226928120e4cc55725b166a2edd5d0c5fab895adf8ce49b04b43311404aa51d82b3d49ae6c39a57161d33db6ebf6204cccaec9b04a1475f073f25eb42cf23772b1bd8bbc404a5ea2b4b396b2234217f6012707ad5028ed046b0c9df0b1b661d7d506e5a6c27065b19e1de65403e40709945bc75626393d594f9771fd3cf09d43f20889525763c6029f140febcd2cf5b492971a2167c0d1943f0da4aee382e13543fe71456014ec6f1d0081e001acd1ab580c8d748b7c701d335c3d07f2bad231ceaef5cf9eb0b45a4a6d54e4a8e57896f1efe36e349284042502b26e222e3abda6268aa9f65aaba03a6959c577e4cfbbfdad966012da32ff154bff69d2579ff27429aefea70af47f9f110ee0187d0370774b02c832d16be128cb2620392098ba20b2184a14697ef71c214fc3d5bb079a78d40f4fe9a858909b20cd08df86f2a7b681a9c6ab10edb9a1c6cf7f9720a5796721c7e166582cf96beb4681872dadca74320fa0cc2cb4d07c686edbfc1774dbd6bb20abf6fd2f8b62fe8b59707e5c94717011dc2b25bc5cf68db2f04a0e1c84d044c5c62bbff0f7540345e4d7c8d6b41aa5ccab19b6e36a70d7af39773e326cbfeea47d277f55a87ef3ce2bd4c81debb8645e541d9742b7e5c91a297306cc235eee24f93d1c72e79834d160ee68cfee47a9433dc393ecc9a6d4233efe287bd3119d35d85c4fe6f92dde8fe917ead0c2ae5805d82a4fbdac6f15161c8af0bab5b070f370e1b1a0aecf1946516a74b79106661e0667f6a656fc4b131d2dc57e03e5670fd7c5d5ae637b96a43fb67d47715bd43a481969b67ee35958904e4ee1baac5f3a5e02346d1f82008a7d84e659776a28d3f69ed6ee9596377a3373a2727be2e92fa87983b6cdd5d96da4b9d6df69622911964994e019aff9ad478a7c78769bd43ab611d3241134c9ab76c5532a7b0b1443f98073aa785c69c3b78cad9d2e587581ffe46dfb02bfbe07f747f00db430f98e728fbbd403bed9e6ded2b07622f755b3d85a324d830338656db8ee5c4b836dcf9d630936353138aaf71c7b78e9a554af177ab65052b81f07b9ebb06fcb3ed54e5fea9e74bbc71b2ff83dc46c6b6ba24c34f210de6b52c72ccd07302bbe0144f622b530b3f4ec7e972fb61e1790ceff3549ac1ee16780e82022125aa36da12db3c2a4ca7b9ecee2e00d3a641f7108caa3b92d8a756b775a3d72fc7b16efdcc8cb1cd993298b1b1a975f88e179153dd1da02841e0afec57c7d5d5fb7f9a047f71b1f43fe5c60680793f53dfdffe2c1598bff16f9ecececec82db192abf021fa0aa87c23ccaf669239e8afac38b3fb4bb1655b81aa336f762730f06d5c494cb19cbeec0d52f121a8e8da2cc1dd41cd27f6cc917997aabf3ea3e5d542194b6ff8f7ae93f799df9d4a60a836c36ee03e5b2136eb9291bf157e7f69fc3f56dddda2ff7d2477670b33230b3713073f8b1f218e64e56b6107b7a88aba1a9bef5777bfb4393c3c0f983b6f7a0d1bbaf1fcd2c359fe6d1a31b9ef151d0ad9c2a6b0c7d8facbf71fb15ba4c95251f04b776163cfe525ddf5fdae082e62cb8fe19b4a7bf707ec1dff3e3efe26d7f2c50e15e8814fe8f9f872ae749b7bfbe05ff6a8f406b23b43d58b3e974b457a8be5d3f979cfc681d9b1639dd0f005da14c91a44e5d845da736a1d18f365198e03a42eefda37bfc8f67fcc3f74cd1f7af51e079175afffee2c8058d071a41ebbfa599b1d9cf5aa349bca69c8bfc700b314d0b468ca36e7da99b1b80d0d3362b7c8f77e99fc768c2c48e472de98449bb6c23a1543f4b1cf410fe427c18aafae4e7aaffe8ea1a1c6ed4c8d05d6274010ba07b3af47fa0b4fac5eaa3d6188bc922682a362874b4af362e6b35399756f7ab630c9399a9237e2feb42bcdad80902ffb3ee2cf52f0dbd0e8d92339c1e5772d9432a9fc5d1f32fff77d730d4f9bd80f20d6c346f5129f7c3eb0706738cd1faebdb03264b52a2099ef2f07b5a046dfd0bf6186d0726bc9f5adfdfb2953ec0d76e247d4375b1f1852aa729bf470668e4eeedcbf4cf8d5f92086a89f7ceb25064bd3df44b650c128ddc0579b0b0b3a819d1d7f727975d5482d1e1c5e4e08f3de7c3950ee02e87c2dfc31e925a78e61488b21f05aa8583ab656087f971de0d138b17d4ab0c5d5ff4ad077b134a7968faa81fbe83c04705291790b138fda8be0a3e62733e3a18dae7c9a0f055606a4b6feab4c54dd6d38e5cbc286402c5eb5b29939a507ca561a09d8f482f285ba1e9b3702fb84471bfbfcf3fb5fcdffd118081810181ccb39a74b5ff1c54ef1b1c3dbf19017fc10ff4072368f707d8820272b559027fbb501dc00efed3089dabcdf28b02970d630cf97af1c588ea3c4ee53f2190311d98549bbf7bbf2d6196d01c215441b5ca5fae7d8805b3c4b6f090fd90ea4ae4ce3798ef17066871112e8ebd5c2e6ab1c5584bdf3581247b417283ffc913e85fd0ffcf0feadb0faa82be81811ba9aabea525c4f107ca9f0bc57352b76015b1605591311284d8bab988a82b73a8885a9868d809b3b14b09dabb7028188b2899081a336bd8fc8d76794f6e1fd90d22d74071f8c80a1757599b866209efe34e03078e89f832329df10c6bad8417295333d7523d74cbfda76bd39b162a6a84c96d0ba5048c6712d85f555e83aa654490f70d30f0d60cf0440efa1bc68a3dee26307eef33c2a81342a24b67de44d6af90ed77c153fdffc5d2fcf2e7fb6369d047c2a7337dd10c1ff4a533e2a933716ffad37d35b1ffcf8815bbec37f19226657fd4705f09a50c86b40df7060dcca9e8e5768171329414beed51d953759ebacf92bcc286d5cee938e21f362092e6bbf73e92ec6fb7de1a476278feb362c58861e5152af70da8f8c31d0937afda4f07c41674fcdffafeffdffa163406a907f428b08289e110ec3905d4af9b457e2d9f080c9c0ed1cae37b99175adf432e0191abf5ea93d32b14713a707a46b0052f58e6929fec1ab9d8ea7a7f421a96bde21b64ec623127923ef84691b87197abf3833157eaf17c22f9dca00056f8ab0ba5383c802bf60d202bb1ac85cba1baed016dbd569fffc3f6ca035b77706c71446c855d7af45c39261b52c0b7cff9434851380efe5628b51144a5092a638a0d2e730872a407f7d5ad18ce0e8809e20ca6a1d12fa6a5847d76fe2f8e493b6c5977617798b76e22b9c4134b67e2d8f27ffccca509542df2b950439a8f834d814f2bc0e48e9b8fb4343537c8e6bbfb0c9d07a05d880e9eff5197b18fa5e931fbd244db5f4d240183e0d5c793b688fc8d8fc3192630cbfff8ea43a895dc7efc0de0129287048c14fb5f6f5f86552d844fe162fb1faa810877e5453abe17a5f295fc558bb65315eb2e7bcb8ec2efe03fd7bb3017fd750b3f8f23c8f7881522bb258eefe96bffd5bbafaeda8aee404c077c403c6ab7f8e47b62ba7907946415bd7b6df7a3319e36020078a12539f9be79fefdc9bc2023fe2972063c14021b68aca2f84126005bd637082587dbf1771485ff29b02e95272f43f3c4c4061a0ccc3e8f07f97f58bf89f4785e5accaa689bdf6f67d68c5ce0b004d532f3657fd8c0c28ad6a2e8d4f7dc79d5692f87d817525a77cfc14424eebdbcceed4468f57f77debff59df727c6fe985a40846dec186dac649484c424add55cad1cd9259c3414ad0cd90425c5c414580c5421f6220216726ec67fa33b6fc5736b8865a74ba3c49320460dba5cddbb0c0bc508f1be45e283ad7e3ae19b66e406a822439fe62dc66d9f6473c409e9d0e11d7ab15ae42af45f36e56cbfac760d2a58177830e4b2257ffb19acdc3fa3fa7edbee30ae21308675d6c1fb0cff8af583310ae98aeb85761f4e9b3118bb5578c2c94c7ef708cccbf224a2dd77a13f206c9867ea80ba577e0e60a56abe851756dd384835265d5b5ce0d3deffb6ad30154afdbe173b9f8010351815cd15d97f38ba4b807f58612616b3630ebde65dea448c0fbce4288d7c64d3c963d61cd2e73ed1eb716a188b88f8bb153366628612588f4ab2e85e3e97ed528457c3887243b73c78a803ddc91b6ef3bc66f1d784b6dc2253ed2b97102acd9e8c10e1ca013f96d37ce319aac3ee8cbf5653f2b9164e079946a8039df8de56e5210a275d07def4f770598281f640132adce9bd7b551a9cd3830be6bd327e7f6f2efbff7c68cbcf03f992b35570356475771316325457113377d1771533707656345614911776611412545033959351b03134507317fd8957d00b6ae97e42680b0733132b13848199818315c2c6c4c9c866c86e680c6133603482707070400c380d201ccc2c9cec064c2c10234343430e564376564e63237d76560883be01e442fdffe5a12d4f23246d6418cbcdd1296091ac50986e3cf7c94e8b9fe31b4b7fd11de6e579bd54a458bb5ea86bf1ec7eb499f9dcfacccbb392a316aba9b466ee25a35c30253e07d490fffd58df009afc82dffed50492982450432970f75bc48ccb9580193de3a9c5b9ebe4dcf4f9e496a4db504551d2a5951ea84a0df001f667df80cbf4c60164c78f6bb167ff084b4080c9f661931d2fe53cd0a71d0e3dc255d7cd4315fa2873aad3d5d05a8df5cd2a9d1961d6953e7d685b6bb8f2900309d501c2bd34539ab248466c73c34e96f2f660ec4435944641390c624159e6d82018988b00ca63925e709bf95eec9e8ef1e1bfc526fe2aace0e30bc90c04ed4f838be9338afefb0d820d35ee70df43fff24179fb4d32ea35938b751f990930da722844d1d15bc4649d6944b579c2d25c53d8b65f2b09bfcf92c1cf7e1703caf04211c32850d82c4a66314c63063c87975f24a51b3565e134998104ca9365d7b714085754b0d94234a19c13907dbf677e0f98efefb52cda0a4775a825333cc27f394c17d8faa3238b3e1ce01e974b1a79c1c32bdcc5ca4373738085b23f11bf6b7fa1d019a62a72e06c17a5b9b277ed522bf6279e055d707f665e50916e445163fffe7bf6a095c770f34bc4ee2c761f35b8f0dcfd573d716c38dffac05497b0faffd499efbf30bed0f8f71728c9585858d84a645d56ad0a83618c82be99fb81b490bfcbf87d6fdb2e3e7e7050e8b01d88ab146ee7df5471955ab2b2b1bdb5facc1d56274cd5f79107b3ee7bd600342524b4fa7fe6fa474f8d4218b0be197f41110dcd15ebafa248c0ca203037084c0582190765762e6889fdeb5cfc758e0a5302029d823211e79abf86dbf00e044a078164bf7db954bbb964b566f8620e86b69cb6c382aa41385c3aad59a0812210a290dcfa57d3f7f5eae0473917983fd853ff561d2808f7bea819d12ef9a7cf050da84f86505ae5950a7db49ca5ea710f8c5342033cb3c451bbf05bf0100560fe325c018efe8a9734f4f5fc8dde8340a0886ff61ea95591242daaf68fde9354f4598b7d5e2803e32c548d7a4921def119f7f8abde5facb99b8716c6dfa377ff422cc36320f6946db935ebff257320b590fc500b956d187f783ef38b5816f8579d4ffe33fe05f59afb53afe977c45395e8d67538b03e12d22da04536f36831ab5f5076fc5a35004c832e34e5e22f6edf05cb4353a35d544d02356c7dffdc3881ff6a4cd39e44de2b3d9c434945373348e3b3cc41cdae383d4fbfc96f26efa6eaa0f901750da3a434f491214421062081998d61957d967122d810e555966ea72b131d7f61f483b6eebef7def6e77549c47fb2b44a2226981eb39f87c98ea87b9c87cff91fee0fffe9d7a54b72286f0f3e606aeb7338d14c3a6de4eff189d7bcd154e64eeb55a7dec694557a745c21516b9f21288f285a5b8152192043e3e30666eeb97c3963c589e39a1f22438161542bc50c7fd104685dca9c0af15567829f16ce531cef2d08c5637c3bff3caf15ed2da3f816642b4feb3e84648121cd2bd7fdd940bab872fd6955afaefdd2fd8fc052421715ab761afc7927c653433b114e87412d45e4e3a95d395de47daa6c6761d58e5244cca6f422e68826f7d4f671f0a8d0635e3ab0e7c261e8903ea7b977ad0f7debb24c37cab0610238abc13729b957796f5a9553787f925c818db1812e91c66a4ac91f2c5281745cae464c026bdd5f9a7d75f552aec9f09d937b49446964e3d91f72b3653fa874043a3ec7c783712bf013e3be838374f40295a048a8c2c43c33b77519a2cf8a731cc87f18ebf8f2d3736919412b0b15efd8318b392fb05929b4fe99444b187dca6f97fc6c794be34a17b76627b1e64a3d8de29d2bc6918f44e1d471d0f0373eb529df3d42d51a6fbb45a9760f768eb5474f63fdc8dca16727fb8144ac3d229f195fd1e221b7e5d0ee7463c6b30971b1c9681fcdb6bbe8b9db6df09897480d6e1b73387654c96fee5826a149842f1bfa32f353d6b08e67e20932b9b56ab9575ecd1fe047a872d5ceadc8a9bd828acf6fa59ac83a487f53e401f7826627995d746c375bd8ee03160cab44be29b2e8912df2db73cfacd879c9176e5077a08afd01ef414272e7b77f35c2f801daff5e10f0bd73205843451058431b04926d07b13e76833d6d36ffd678185bd28637830fe05c4360b122a354ec8c7e8fdc267ed0540b679423950a2d799cde05d90de071b8019ab56f1e4dea25bf257fac3f8237fea1bcfdcfbf397eea9bf3b6e5619cd2bbf4da7a4ca9916ff78d6efa82d77d01e68f8416bcd61589411a485a18ef836f1ae39a984853724a2c81c28b1718ae8e2a50c505d851ea21365d23ee458dbd22de28ef1ef638583afbe0f892642242c4936bff5dad39cc058f13301721431f7fe8e2ee648e435f46b7fd69e0eb7902698c47922967079c0f6a8fab9b65064b143bc91a2e785d8642a7008c0f78dd0d6d24998bf524d0139ef77d693c1abc72c245d6d77fb03e05ac40e0659027dc93e475ff02869286febd15af07c3b74e14e588d0ca6e81eaad5015f49230df24f078b35123abdc08e0ce7ffdd99ab0d41ddbc946f4e1816c83f1d50b5a15403b498a2f243ca73609f113fd1748e033f7bb16220f24af6f1511d5df0d82431585d56bf1c755a71ed472e1e8774ab51b35062bd08ef6875051c78d86d5313e828e4b7980f045d624849f0059fb6540b39b111e1797abd850fac8de234fbf3eb999a60a8bebc7f7b3a47377991ad30f788af2ba4b791a173d6ce411e7587ad4bf7192ffd2a0bad39bd1861baab5661672f278efa6563c2aaf52ca775c247e606b5cd4a00a4f0afa7b3fff8bdc56243994e5996cdc5d0d9855f51535041d0d2d5ca5259d5519455494584d0cd4a484151494ee5a5838580ba9fca29be80fdc04f82f56fe3fc2126466666662851873b232b31a1aeb331832b33333b3e81b3070b07332e9331818b072e81b32e9331831331b4118d858f521cc06069c460c4c868c8c9c9c0c6c173bc95ed0e0fa13facfc260c4c4cca9cfcec8c2ca68c00931326065346464656266666563d4376062608618b1b333183130ea73e87372e8b37018b073b273723041d8d9f5390c0c18feabfd872a1a4c85cc1f868cfb5812258b444dea63dad23219c95ee7538d3df22aa8dc507990eef29893f67ea38492315bcb1ca13562e9660d43d648d5bdbdbc9949f0a2c7474be86e4fe75d9cfe851eff1fb93819acccdf9dd412bf4698d403f9683ce419acf8b4f1e5547071e1e6ecb345995d8005422dda257bfaf27b0dafd80e8ad91a0e791065c8e4058f8dd0d60602239baa92f48f279c142897509678a1f1a14b4a07a62c097691edf50da497d2ba79060e4e9ccca337b85439059d16bd5a6d5f7e98d64abbba10cbd1dfe1f7fc46953ce589b0f2049e264106f4b1ffed96f1affc887f362a70511c0811e0227d6aa5c432dfab76438def4efc2ac613f9dceecebc5c536033cc019c53c865a9873e711ba040ee875da05a38a3163525ef1fd688fda3ae5743218b7197b1f8700a449fddbde259b3313c18e9983391448efa4aa6667448048a4500b0cf215c62a8b3a1eeb17e9cee8d2e5dbbe4577964a5bec14363f182ce50a1f059286b21a7ec87d9fce5c69749fe11efa18cdfa51f3e67fc637ca194f7bdd839049a988716787fe1c07adc7e943a62eaa0cfaecfa55147a794b1aaafab6d687cc75ebc70e0f845cb5fb4fdffe5f2bffc69de97f20d24ea8180be044365d26de74db094ba79c2cadb95f6307f3c72ddc1f29b8b39874808bcafe0fb6f48f62feb5c5ec400a75c0307df4c0fb7cd9f63f444d8a5fba6520a1627ed69f3cfae14a65673a3193c14025765f811ee4f50fa3ffb53c13efcbf7f4a326f6ae1e99f31ece10e24e69f7c816c0f6b63b88514fae7b788aa81b485d7fefc16ccf688aab3e68b7a6595df561b7df1dbf4adc1883f61ee8f86c0b4352918f2830261d6d51302614e34f90261e497779af7237c0372b5596a02f53d3201a2f82fec5f825feb3dda87aa8afc3ff9647ae59275371a2ca68ee820c7b547449abed435837ac9d8eff60d204b24ab69458a5f8516ccc87fc1cc46bf3c73d101ea6bdf80ab279f6b515b85490e922ea8c3028cae9ec76b75b122a6cb02a02c8b450c8a2b6d696c00549dde5752eb5563be901cc377e1ad736b496ff3290eced39943e07575c98ce2fbb3a76241cd2378af9d6501c33dcd4dd5c7e8133a0aaa587ff0c9dcf99d6d4b22cd797ad4e37b45d8eb74ce9bf5ef7448d2c7ea0bbdb9b901e5fb19f0af0cb3998a1c595a4bdca9df807f6609807d1ee0739c839cb90eccb04fc35c59c13ec6150e025855f5c5b76de81fb0ac1b6e705dbbe68b53a779778ee43c5d7b7d8757692eb941d72a3eaceb31c51af74020607cea604db4a44a5ac813b97695f933e4c9f656bb002e4f53ce050ab660f7465c8b7e9d4194e78b5ee4a5c2e7e95ae0db7dbcc80d7e16951bb2749b9f4e124ecd001c31342932f954b93b8d62ba0d4e3a57ae553b21eed179ba0bb68bc9addd6487c9838c7849d89110933bb8403f511fa9ebb62ce19feb178eab6b037b72f0f1ccc3ce9369488ae9cf1ca787e94e7a46ad3a23046a9e9300524589dfb42fa1cb7d13924bffc61285e192e48b206e8af3744f58eb643ffe6b4d38dbcd04ad70ec453e57a4a3011cdfc1b33b87b8ebc863a42d4fc5538e3081b09df63c7d2c3eaf86669b0af71a97ec13a4290a1531b1a4a6f3f4eb242ba7511628d9697239acf604f75751f8d7f3cfd33b613aeb71aa3692359e91624da0983c908d6d03c4e6217565cc22cba723e2776748cc1f8d5fe1291502c4d1c919af123fd6fc541a75d8b4b596610a5e19bf72749eeef7a0affd127547a1d3abc729524ff7dff1a610011099f214c37a6e3cb70b62a95a5cdd4bd9195fbd8a0f7027e9444fdaaa40d227edf954fb5165170724ac100bf87ef876c5e724fbcf740cdcc8bd3bc209b64c074580034ab507629a68129f14fb8907acf515e1ab05e6c100645c760861c56ddc7b308efa792b0e14cb0f140dd80126d7e454141f6f986a8aac74f3a4103d843d9e3b7d80bb48a560762ae6f58e20b2faceb80713aa48774b0e01c84a1b0d3b3d3a8154c55c6b83af650ae5548b1543d2cfd339b0c9b8bc8c8e1f1cbf33598a4cd9331c54a600e4acaf7b5bad691eda0cf72a13f96a9da6f63c2d11070035ac2c38b8612dd21991f2b6f9d93dd094d2222121c179facbb0ed4726a884e810a9f21e5331b3ecb96816c0015392cd698f1a1ccb93b259377e28579075870407e012ca4ce964c1b9ff09c72d367b93c6d71f51c2aa030fb0fed765a91787689685ab62d1ef4b76f874bdef723e4f57a6ed1e2cc8c57aedf7fa151957a91a9951592f607e22e2caa997e25cb53356191ecdc4b8ce71983b1d9fa7abbf75b64b22a77d9d4f9ee48d594610ebe77d407e9e9ec86f726449f18ad70b4270ffb6c57edec4b0ad1dc0f4eaf29933b8a1c562b3b117a7a781714029a71ab0be36d81aeb5c706e6dc693355e8fb7af5ecb902a173f4fa7e5cda29be9d41426b5d42bae0509d9119d3d045c00abb3214e0cac9aa36791f91c9864d7622a8c9f2f01e6af0599d2213975f35e8a960cfd5ce372e6480840c59c7d1c9195869936083120125d13bddd5c6303ccf7d6f09434dd00216758bcaea43bcccc896f42ae68f83cddb282f3051e7681c8ab55f3d0312644940fcf9334cfd3731fdf6e9b0ffdd4fd067ce0f0d6b2fca41e240b0892a036574d5c849d9b4e2e4078924b523e8ade420870397f5e60e817e4471306238fb7fa211e0dbe8ea503106f55f1396d7be51097fca679fc630ac483f078ab4ac005b17017a7e299daa26b2e5378a69e6c8f45747820003889a5c234b0493edee3eebe11fee8c2e90b86fbb900787f5f65b7e16530f7e3ced7fb071a8fc5d5d6e15f01500d45e5f8efbbcd354958e63ce0bae4b3362fda870fb8f9a3749ded3e5335abe4991130db082f271dac3dc03c4fe7eb5790402a4678b2ac74786030796359f4d212c0bcbf55a7cc7859843eb127c0cdd2b78a0f8d533d01a06c7970c83b76c9e5005539ec317630fc72c3a3c2b2aaf3742965c7943dc3727d5be296b6dd9cdbc2c2ab3300d33ffba31067897c30b9683e96905af783caee875caee7e905ddd4cf786361ab3c216bf49c1e140f6c573801c928c5ca603d90a9f8d6f0360e7106831afce90b5200f2f1488349b86337d0355c443cdf8a6aa9fce0593160fff92d7bb7f255ede0b1e587fb8a8d5bd4bdadbd7cfd3cbd6f28fbe5448f1a269b65841dc1786483fbdc2740e4d5636e790bdffd354dcda87c3f7b8a30dc89051340bc3ed393678d5d55ecb0731d11fc580ff57ae9c529f501e78bbbf897562870aea9600fb61f9ff556a646bf078c2f7dbfc7204a148d608380fbaeff83d6b15def876f00f3ab727af2d6e13de721e486e5911bd7fe71061880af33b69bb048418254924704ca4dcb6ed66bec8c01f0efcf216482ed9733782a17fa9befcffbd7a94701bd3b687b5468cad8871f42e88ac3a6730cd5625a3fec9da71745f43535062a76fa397207891d9a75b8efc7be3a4f1fb9953ab30e1f4a4bd6a50c0e56936ec389cb00c46c2766cb9eb9de1af9588926c836f1960d1fd59a109096a361cf65b690e7b58926dd20bd96f5d8f250be633680bf315555c9721512aec113b1a916ee6bac60d5009073377d9e2d7338d06ee5936cfbf6f5a5de10413a042a38729b5c4368c731d4155da8c69cc369de15c103905f7249c95f0b3e792655255b1d57bd9f3e6978f90e40fe2a7599862c9207bd33d4b382a7a2733ae2adc8046442a06bf49c7a2646f7b62ff9a9352279bffa84d234c0dd3493a97ff06ee2fbb523ea91cdd51708def0792280f3e1cdcbf9f7641f39b9dbbfc373260bce5a0ecc08093f4f7fe1865841895efab0c0de4fe966d41eac68d04dc0dde82a5af83d9ae05a070b58af5dcec90ff78b394b00f27195d40882bf1da71b3e6870ff4e6a24ad7d9c6ff179fa3b0fcfedb9b81bcb33c3f6487056f4f49e77a500e76b1267d679633de572678208d04e99c481ce1b3500669a4a3b77aa6c64651f49d22d6541837b651cdb090044d9fe0f7929da8730917c67262a550ee96be52ad8b1e7e9777b5b77d3ace9c4745f386cdb10cc494667da0a01fa2f976f0c07bbd12c688268cd7a3249c895a116f86d5d28b4e49e80e7cec7c6d814128ce6bfc89f1a8230fce08443caed9fef0324123c9bc10760b510641487d102b937ff528183855f5ef703481d0df777426f1a3d74ca22ad603e3e510846c3dd5efa1eb5e3f7786c82fed19c8ba9c0bf0f73ff3bdb6798dbbab20cc5f2fefa87d5ce3ff07c5ffb2fdd9e9cf5aff876fb0d9e5db4fdcde03721b8010f2b269e8202c03d098197b630587ebe760881f705ff4faf14519728f6a7570a7bbdfae7b7144907b1ebe78fe9b3b74b3fbd52786d899f3ffba0044ea49f5ee96f55fdfc312561defdf995ea18f7ffaeac9c62957763f8a55a58d03db3929fdffeb07e82e68160e43b1faaf1500251f62d4c0361e457b0f7bbff509cb6a60ae2022e7758adfea8a666437cdce2d5cbe21396c86b03748fa1702168a8af5f4f3d01234037d87b1bfbcfafcffef440a3ff3b5b5a86fdc5b2a5dd4a88cda2bad2fcdbdaecf70d309c9d09f0e2a6a869ed93971eb9a0ab01d464e7e7ffe0d9c41b6e197832264bf24cf9a5e9ee36f9c22585ef29ff97b2e20f8dee6ef12fd0e8aac70cc8dcb34dba6d80fb3819d930f9fa9a686ff9793a1155b3bdc298fe95111181c6a46cf3154ab23940f9a90d36bbed072b038f9f53e26d1a16f4c6be7f00184f679ce0a88107c4b8a9eb448727c94a76683c758013e938cfc0ca93624fe4f6dafb95daeda19cc7032f07ced34d4014065d30c1ee2f646beb730a954a25df7c022ceab01692c2b4defcdb2815bdb31a4c3925222fb3000ef38fe83238cc9028d9a2abdb3115d0b83ade35f2be3b4f4f13cf74f286adcd4828bcb2e61276906a0e3308d894c79f517331d2143730e2b98509ebfc560f49b1b600df77c6bfbe151c9bff50e925f94d753da1074b0880c8f90d0526b2554ebb575c0c082cceda7974ae9f4a01df7fd12939f9545a67db66d921066eb0526c62e17ad7793a013fddadfbb4c63ce68ce13055b11e2faa6fca0334ee56ac37044bc995540b9d090a3c1a6890f5d44401f3af2aff70dd46d17d609d4ea3cdc9da227bfeb23920b79f05d3e7b072913e964403ecb0d427dd8f549c79001a976b8536ebf00aae2dfed48a8bee0ad1dafc64a5008d43f250e9f03bbf2c2e07d14b3c05b7536fd66885fb9ca72f41106875040e840cddcb398fb9c67a8bc4ca011a015f1ee3aba3c570979b0f192f05dfc15e32dfcb03cc5f49dd0bbae76c912509ba53222dcde478b9c4b580608181db73c78289bec80ac515fad634a07c1ca476c0f72fbbe332a504ac160dc9c4f0b7c825be41f0580404181edff4bb229d3585c28341197b35d47a73c0f11300dc084f23e344e8e4605a562281e349c7c264c41d5f00340e025e53ff697592bfd9a5ca22c692693466a3308046548afc8441bbf07684b40297371da73e32833a0d402378e4164a46f5f430a254c14cbe48a8bebb618b741da0d1b4b1d3f2311d294c6f50bebcface0a7d806210a01119dd8e96ea66ac3fba9c873b559d9553ff662f0c70e3d6dfdc4d9343a45d4b50d4123c8351750ef479aa05d0a812c11d53bddf0ace7b15fb7a6dd11f6cf5ea55db79ba01ff04ad68c5ddfe0d9767818d68a4e926adcf0137e2a5a1fadbeeb003aca70e22cb8d89f4a99da39fedcfd33179928c14ead0a3e8ac1d63c47c26151e463c03cc3ff7f0096560635383144ddbf807fec40812300ae0eccdfc548b9fc040d9842d573c34da1e6936d407ef1ec02224b8a8c4944f74c0c32eecd6233171a26ecf011011f7155078e1e7babbc93ea65deebe1ed24628cb05d85f6230615e1ff5ec6f10d9371dddaa92d1944aa80a05ec0ffe2d11d976b8329654bbb275789bac5bfc6eab80f5e1227f8b702dbabf69a72b183c4a9609afdbfd02605178bf88f8664689c0fe5e52c88d8e98c986d13ac0fe657438dc8dcde3b71d878b60745be242e21a0b59394f2f86959043b0be3d48dba6232b8237a042a9080740f67077e2d75dba7357732fa12641de77f956897d02803f6632cfa6e58e8011a341adab447726a3399e5d01dccd304e2be2fb441771cc825b33efa5a16f7d805b07e490e07b689ecafb2e839de36694219335262881bd0ab03f4b673f53cdcdf74ba5a2df62b2bd0447ea9f430ed0b8763184a815faa04b13b52e0c199ad778789c0cab02e6bf98af72ae7b718b7cbc83d1716cc1ce68340ca0b13b7c3be50353be3209eb26a6bd3bc42e65bdc10c3854d09ddd18dfba8a6fcff59e66fc83e522ac079cb53540bee184ef765299395d8e2d3a78ea16e0292a8c01b09890516ef58c700566d5b6a2500879b1bc21ab880420ea068f736db0dc28de2c27b6ec699f5d09bc4f530cd088aa379256d882858be844274e6f3ad7ebaa8c9001e85eb294c65215cf2efbd4c4776c166b807515f1001af34be120aa1879662ddf3353d6bbbd91362f561167cfd38569875d649b14ae29f87f7cc889cdfb2cdb7211902be3706c72586eec30ea5e78e8a0d97a38f9d0ae2c20e264bb963020916477360cf1b8bbb2df71883a3104a0b1a3a1bf9b35eb5783531da1b88eb118b1a8d1de03b0f8ed4711e4a02759e0c5eccbd093469721f1620c0302a00722023bdcf784d010d9f04a244df47a3b5b47001a1f9556eb8650920055b4b137fda6e238ba1988e600d7eea727fa06461c92291a9d9fcab08370c71387e8013e4949d62cf05e524d7e9368d6638ea12eab2b261a00df6f06efe2ac7c46a167693d96263ced59f97936333ae7e92bef842bbac99d93e07897387cebbd7924db6fb19ca7b75a0e8f0ee36d5d46a0826fd3edf6b8fa4cfd3ec0621b69b4c153f290fec0f22667dde4117abc84c4110051717aeef14beb3c63e2c5777d836aba60aa12477d807c11137b69fca87d3bbd0c329e8eb7ba6a47b30b1203f0effdfa6bd3d977c60b255203ee8818cf9bac6102dafff6a393f6e873b1ce64834f04658cbd7e8a15238044c8775ef7b15eb59d2b6616f39eb731bba7f9b9d715a0315539388bd48cb9c7cbf5e98c462d9b7c69fe666b0480ff1071d28b6389dfb33814bae35d3acbda6bdb07888ac0cc277ebd5d83d5deb78e88a5f0d16e555acd0e601173bae94f24e246233a0dc9f37dce8d104d5c45fbe93c7d9d6778e4928171cbf2b4e2a05f2f1cce74c3138017f66b5c3ee3d5c1a07b47b3d261283efe414fd264002e75c78c5936879aba5a63555ef7da18b31f73863d0058cc06cced5037748694fcb539ce025899ac7a0c54001af950cf2dd889339fe454c6d24f8afa49eff5c9d480168df7bd30b492c99c5160a9eb8f99c76e5fadbd0ab048d253b39cc5c7b0ee3192f973784e57c3620e7e0494cf769df4e439605bf6730273fac2c8d74864f402ce577865c44f2783dd841be5a5a9bcd2e9476b56a401f239b46c885e6071e0e63c5e997b31e9f34a8d87cf0197bae002c279ce6df7c42661bce6210d99ca6bb94500fe9facad34c9fbc83b9dafa0a6309339ce3f39730d0052f2d96322ad54e2017d21693d1dcc029af12c8212e08ab50f96cfb87da5084e403e3b9cb6a8a1e0f6953d807b6f6720ee147136ad017e7e03fc2ced083bfec7364070774fe88ef365c1278a229653524f3ff2cedeae88ed07584c5136b72742790f7b305568eaa362415ace4ff5c10730f2beff464d145696bb91f7abef6802424ae2b232baaae24a62c20a02aaba32024ae22a22ba4ab2922232ff810af1a291607adfa3b316637c5f254797a3ddb8ba13217a8354bfa70a1ee5eb556b6c113d7c120300a0fde7d0fecf00d012bcbe9a7c79725837e02eb678b7872d61a9c1cd2e6953b499cb4122b8d4fccfbe1147f06524cfd20d85d3a809d4b871bbce8f8999eb9196f0cae8df6edfff01d04279fef744f2285a989b40cca4d44494e54525d4f405156d9804d48c20b68aaa724a82d6824eccaa464aa2828eaeeaea62f67f9ff11550bf86907f92ed54dbd31795fbe206172c6f44da67d4958005fd177b0d8d1f5e1dfa2c481f3a0e0f207390e8f33be8980ac3ddedaf295589573feb3173ca61efe4861ea4371c9caff636dbf75f1b3b105bfc55c4cfd737e242fe700fcd2dd8be3dfb4bf5a52821fd983fdf15363c31f31729e68742c0cc39e47ecf7ee9a0209069ffaeaf861dfebcd6cef167e75af80d4d355ba55fda00cc5245d29fdf2bf098dccf37416d7ee2fe05e612a9d8ff1d951258ed65feb62610988fb3dec07de186cdf9a928face176f59f4ea84477fe9fa21a4c3ff050eeed2cfc87edf15466376cc52bfb4fd8887e1bf0f2b58d3bbed92e59f0710c924d7d2f20b5f7ac4143791ea893f354bf18f6605a14cbde5fd8b764dc7bf30b9943adddd8bde0d275bc37c5f7d4920ee9b5b081552f4fb1058bf8b9738fec2179fab776c9afffb5a70fd1693566c7f0d67d7238837618c88a6bf886adbb75ac0dd941c0abd56e69c09eb1bbeea5af9f0a8b17a57085d116ae989be55abd8a70c999f3e897002cf46a137d5115d7c75723ffd3ad7ba59298c3d9124c932ea374dfab057f9279b0fc0b77c031130d8f96bc1949e9c030c173b584345e798ff8d25ffe3ff7ffefb17ab06814000305487a3412d5e3fafd6673d69a0f5dd519e9ee9a95d46c858f36cfd7b72f810baa50433ac6cc9e7f87c1d2881e2a8cade3df6de9c192038382c91aeba100bad170212541a0d7ce0b8b4430fae8c81fb5ee10a843ba33388b59e5241c95bb39fbcba4aef173e5645c8a6b5a9f0d8908c54dde7d9654513b421025d4509b0148931f938f52e9e947dd91b61afe12ea3aab8b5c7987102e258e249cfc05d1e1f779ae8fca8899acec1dd013c9e7ce99afb47b55d0fb236b63bcd919eeefe38444714ec6f1d008140001776af623134d22df2c175cc70f51cc8eb4ac738bbd63fc7e1d16a29b55191a396e359c6bf8fbbd1481212940850aa605b382028dc2773ea76456826e52b7971a072e99b78f8481bbb767ff1fa002cf5bba9b6162c1c462223c825436905ba29229b157e9d38d169b0f03bbbfbd5e502b3ce40f8b0ce165a79748352f73857123e9a9bbcf9b88c5a3a973b5dd23a835fa45d9b5381b5228d4bdb1ad8f3d133dd1185a92f161910553c152e25137a65f26e8fdc5db6fc5abeafbd4255809259c75e1aa357aeaab7a36161097a96f2e4a30ee5b72f0fcd3dc4e473c56a6f7e384d861826f53f14f1aecbe49ed867b24385f77e8874ddf90491e826367bb11afb7869804d1fbd4da7675a153186cd7b1c7cf278271e2f97693cbaeb516c0f591caecaf8d96d28c870af841253ee48108fc8b1d1ceeeb8c563fb122b4d0e3d2e3bbb01b3d017332149897397c0f5e4ba8ddad2894c98f0008e47ec4a48536400e96d6b6db447cbbb53c8b0af8a1bf0984be09a9d8ea2d1885c33fcc3e7cef4237cc3954d452dabb0d972adda91bc99fcc298d9ac056062a9343615434a8ec54173e0b7f8094fc2dcaf6a41f01a6fea7a3e0c79d7467c59226f64e75e72762d44ea6cbaa532e6d2ed6386d9fafcf1385e2484095e93f6589fcc8f62340bf51e662d05da5bbbf31a511c31ed4ef08e4a086861ccd64ad93c48d1575dbd3af674f70b16eb3c1aeee0f736ee3ba35ddf31104ae64e60466a26787ecc5809ff6eb3b18a4c44327a5ad0a8b0cdd4b3254931749dd753d7f14ccc9581f1d05cc89fc7b266c837807c5b35fa0518645826e482aad46e6c491bbb5f167c9b75c4c0d237802c80bf26c768b698e6dbcbc069e182aa969b40186aace44bf522c57bf7b0e785dd8ffd842ec528a9fe2391f9c457e2b80f60567c0388b48e6a6166e9d9fd2e5fac39404f3a03f7231c81ee2b33ddac77054591c7736e85362c7d0f1ddae3ff445cd82b1c71a6e141039aca968280025f1de0fb82dfe34281d5e2c6fee11b8a2bac97fc5c295dbedfd3ff2f1ea2bde36f7bb2795f30f12f744fc23f9c1756f57e81f382376b1661bc9257debbb21c9a149bf9929da6c737cfd30b7c28456a0ac5eb53df5674332fef58ee997902d219d92c82e36995594ea952083a3eebbd99e1c77802d0e674f2d5e180f8f3bd30037c51f3dea7fb8be99c4a0094aba5cbc7943a725d143c67974268611886052fcd01ce2e5c04c344b9a2faaf351b5de46f4a283f0b6104183f794ee6f5af368c150b8133df51fb3885ecf61602a03439f23c7775a76020ee063c56ef396e84a975a812039c0b28f312ccd9e8856aa79def5ddff02a23126c0268db56b3484f410c52aa0f9f22234fcadc7f2185400a189fc5cc2e4785fb333ec98fec7193104106be68645ce7e9149a16f91905db1b239ce1013dc395f27a550180f6c97bac2ded57b7f1739854e6dbcb37c63eba950b389bc5981ef55f6de60bc82cbbb41c74754acc66140c18ff56a968ea4f560fe8996fc8db6edd82645c362d0718671b11b8af3af9cade9c79bb507fe330eac05fdb0c10ee65ea148f878b48c40c59b8a14297717fb1107cb5fe3c5d822e5799ecbe3dd6d84ad41a1ef2d557452b788ae7e9335cdeb2b7de1397aa657a38626fc2c5603e4000b48f4ffa60f57d833cc4d49b9a2e6050d44bdeac06c05d434dcdfb84193ec12ed6cd8626510a3ac43d5e7d799e7eca9201ef09f9608047da4a1429817b0795c91580cefbee11024e7842bbdfb2f93d3226a9ab5188f89f00e15a3bee4f06906989419fd8ebaae52ebbdcd6cbc101185f6598df9f5ceaf23ef860d17395cd779d64692d07607c10dc38e2909aa2beaa18ce46d85f27619e4acb0238e6efa4329450a7c4b8f625bb7454d56f5f41f3c10318a7ef1fad2134d6d3e63de3c87ae62cb18cfd9a20c5fd3c9d8530254971ab2f8cd8ac50446e177f8b070d0fb03e3e5195bf3643716fcceb801026a4bc58788cb40148d537bf9c3d09d222dd6de15adeb2c465d3cdc2ec034827d497b4730e8856e98e4386530dea494722fdc400e3446f5922cc9d390bb48e9c18bdc5932e97961a51807113456f76de55c541c13c3689f9d593c2e5709846bef3f46a7c7dea81ea5d67c2ccc541ea383be4d4245980f393bd453a8ae63579292798ddd25151be2122ef34807108c3c78ededf352e08a2af7617a413e594a2e608700e6aa5b3418cafbc15c515e1149db5d678196334057069c061fe144e90c88153611e0ac11a704554eaee9c0030c3e2a7090d0b0e5d98dcb2a5a5156ac3602e0bc0fc9653e6cabe0fc8390e1677f77ebaac8a3227ec08707e78377e25e8e016b2f38196a3c6caf00463dc8b5b007539dac25dde7c742d3d7e1c5de6097f9d947e22f4edf3f48a92510b614ee5325b24c7a1fadd2d0a02fe0480715cf0fd4cf1eeba52914e4d6d916ad5e611ebce7580f34d7bc9231e9fd50f32267748e0b06caab0c7c21a78cfd3df18f0b052f7d41d3e4a434a494a79d9c3b2070b18ff9996b88426b7cb8cf423191e8b1f0f9fdcb4c906384704043d633577fa24d9c51aa52a60dd4c61b4aa0ab8c575d896295ce1f4e991b60dec52b9d9fd34b2d118106e30a6b7e0ab75c2bab0d67c1fcfde2c1a81486aa3e63cdd8572a1c7b4844cf78427397667782fd6a9bf13605cacaaf88c27d9848947aa3d26417027fd29687b06e09c53bdc048f4788bc5b2c8e6ca03d1b499febcb4eb00f9917e842e8fb126be743be54d2152643366479f16803f90f2dddf377b1c2fcdf10481cfdf4748076f421f1030fe80e016ba69f2e91bff10b18589865d1bcea0314680554cd4934c670b264590f585b0afa66c4875b206c0792b6cfb8a3197925d6dc35684fc88631d4b24ac1fe0fb510e0ebe13cba629d67ac24c971cc06e1803a7807029087ff2ea843b0b250a36ac5d70b4274601241ae0115a753afa32fe4ef3672b596da2e796e149149d9700ebb329a24ad4b79131e3c9165549c35ddde0ad61234038deae4989a1f21db62cc9cf4b671457eaeaf9dd6b01e1ce3e33ed2aa1a4a536319ac98b7e67e2473df4ad800c1e6e95fa2e771ae17882d65c4eeee156463ebb3f0e70ee0949a73bbdedf8ecc98ec6ce25255d8fa7c292b7af9ca74f5a3570cc972f20d1c75c9afea468604cb82c030897acb8fee49494437a2ec4b3e1acd7bbec6d5a6a03802e6369e3d15b31f5f1f2fce9dec7ad2a896731920043dfc73a43bf3db074ba06db6cb107e11d4ee5786cc0fe7e33f15a42a4d35248d34bda3b8ed240e2da443e309e6bd3f295f7cdce33c83c42c718e766a99c5919e01aa81c8e3b3a35133794506d588753ca412c9bdd0570ae1abe693e547bddc47da52dff03ff68f570d2235381f374bf76b1752f5a5fc3fba6d586bb780f5f0bddb40184636345ca918933c6d608303e2a7b61e76349d92903587f8f58a22f6bc4e0f06cf7965b113abe89dabc0602a4af1bd123f4bb31fd3a749e45cd8de7a0dea8da8e08605cf7a137de1a9560c192bdf9b932a13a8934b67701703ea93b919d9d7c474d94e1693f83d857c547df920b901f731d821893d41a8d621a344592d8778eaf4e6101e607253280c087a42fc665936c1384242c42e65f000817b6cfaa70dade1279851e5f493e8416e6e32cc7050877b72b16c2c573eb99c009c8ee37709f68cfc39c03e00ed6754d4822a17c2ac8c86116c2b7243ab95dbf0a987fa2c2fc7dab2c5e7e4531c5b41db77b9e1e0bf20600f916ab8548392cb8a56871ff2a633343411a830d607c063abaf34ee53c53581d4539790fb558c87c0e00cea538e52c0713640ef645c14a92735e4dbdcff66d01f299a286ceda47469753415516bea5a9e3615f7c0a004dfaea471ddc11cfe086c3b17e023bcca646b9d195c6f3747179266aac01d24147ac4eedfdd47403d89525807c3dd0963b9d67c74427acb919e7f25cc8cff7d800e07cd8d15d5c59f8369ad37799335d99c5af0d39d800100e4caf386d8f681543250a96f97c374eec76567e21002ee04d7bd7f1d6c19d83ded3971ee26fc39a51333e009c4728c3c6e4494a5c2b3c083c95aac55f23480667029c4722088ca9c2f0cd1e2995389b962da5517cf4f1039c6f700902de7bb834df5bf38913283a761c3b6c47a0fe12add719f45f40eb357a3e4b809620cb3db73c5469789bb5741321bbe6cfbff94eb4dea50bfa58081c6ca3686962aeef5339f88f7929dcd2f26bf18e3a0c50425bcb38421365ac4612ea4f7273f779f3282dc7b4654b0cf4d15bf669e1fa350ec44eb5ab0f1fd6f6b2fd3f8ed6bb743147061069ec46cf0171afe1d23d733318b8173c4522199e4cde278af55322024cd68f2da1adbfbf3b5a2fb4f529700fd9c58cd468bf063145602be7e507f75d1264c30fb7e2d412da0a2a8c2d5e137963300c6f6261d050de13a8d27fbe374294bf268e15fddedc2d2349b7566118e3a268bda21e453998d3cd0ebc43e608d44282d76063dcb57c0a493cb09d33f2723d3442f4084eda1e2c54b7e1546e467b92667ba83d2ce6b78f60bf3edb288b60a3be7ffd3f8305fc8de75c1308fdd2a631540e3391945d9f07aae7e63fea243478e41cf5559f0c11c37bfec2dbbf57a91a02e357c9d697c0f7ef20db1b0cef72be56f2af2097be5488d3c83577f906600c0e079e5466f202a024c17b7b013782d07e0194e4ffc66cb985ab4755d27148877568761aae51bebb130af31a71d5231f3708491203454155e2ebce5790234a3e969fc18fed5b1567cc3f6587b58d1a475008c1ec877d3da5f41f203cc30ef8fac32e063eb8a63f5a0bdbca65181c72b14e4363143017ccc20615b3101aa3021b193332e81b733230eb1bea331a18428c990c598c199999580c8d99f50d8c204c8646cc8c10560363083b1b9b211b03338b21030327131b1b9b3e3bbb3e0733b4fa99383958bff5036616d68b20bf427369c3b920fd82d8c550370d9a82beb509c49e54d149df1ee2fefdfd471532b5377320bd6b09718558fffcf183790985ae03a57eb8b36f3ecf73be5d9ca5fedbe5fbe8be4d7f7bdc7c00b7f50f5fcfdfe3427f510e6c536aa306f43119df14847aa2bc1dd33b3a9a47290659b8051849294b32728fae0f9bdada2355a9874c5ff74b8a523bfe467eaa2f1d3db7a2e1468cb9f50d0eb28eef73551e9e89bcb8c272b155fb7f39b07f71fda80afa06066ea4aafa969610c71f28ff6f474f2543414e0e25670b5609130d2b315b417b3b2175153b0935530141470d075b051669378dbb101619516907999fd7febf410e6c56234e2623164e080bb391011b0327b39121270b270b270333038711b3019311a7918131c4989d83818581d1909115c2cc68c8c8c4c2c0cea8cf69c80631e6b850ff7f3924f9f3b4e90355e496bdb1fe783ea56bbc7bbd32a0f2e3ccf6a38032cc423eb0be3dee8abcc88ca7986e4a7c805e0c3512245d1193de3ce5d575ff81a707e4414750b3b540f5ea791d228c753255ce1a3c0aae85a76b637550f0f5ff668dc9eafdcde003f0a66f00de8c5ea0ba9f837b2d78166df27d209496645eec2a0ffef6528359fa3a5d5bf00e44032aba08c0a8cbd7768d4c2c87c0dc3257aec3f0b508d1325a24404b8dcd0993b7889560dba6877b16b4b84cd4e58e0400d5c45c6082e77dea16d7a4ec68edba71b4469aee000831f2c20be64025b44c5778f9e253b6159b9bf8f427002ab78050ae0e72f5fbd94e05211711f385bc9d2d2cf8ef693ff0a1d4e7fca64d3c724eaa647dffeb74f619b538f46fb0022b8a12dd6fd50f8dfe47fbaf223f709ddc2291815978dd9ddab3a13b4030e7f12c65824c39741403f90e927265bdaf7a3ad56975f21da60129d16cb1c2abc58eb94e1fbc56b08daacb09cf5c5398e8e69c163e21c9451f5d818052e0b7d7473e9ba5a69f8ae5268ad14418c7166d95b5bdbc553390c73a4253179e9af2fa3042b1d844ab26526f99e9d3a76b2a2f468e821526b22d3ac9094725756d176c5ed27ab51a18cc2822916bab04a05034ff4aabfc126128023e8d38a55da50dd306bf94058f1600244a99fb325c69abf120ec429ff566e7687881ed530048640adf6b127a77996db8e34198caf71f7372583d01218fdd11be0fbb664f9c6e5c13cfaf6cc0d1501d91061c32f9fd503bb7f33030280b48555fc113f34cae7403ac1a76e6f0e69c37139b8f098c1cedd1dee19437c902acc2642fb1ad6739c55d5b6c427a22e97daeda55d2ca01b496c7a8316144e4a9b3a778becad62be97ed6d30010b3776eab3e3bfd950b52fb9a2814b08ed21ec837014cb792c8be689e49bceddae88a88825bb85def44ea096085f509d285e8f0f2933c1052e6336a687eb9c3440918ecb82152bcfe4fea85d38b8847826305fc377501900ac81476147cf9458c214beae5e829317156305c6900ab61b85d6f5f5cc2423ae4f8f1785795e44b1f7a80569939747821737e9d33affe23416aa43b0dc58c2ac0aaa0bb469385503805cbe637723f60210bddfd49084034869c351bf66866d53467f1f0e2d7b6671e68c0799ea73f19bd2eadc9646fe2d4374deb16d0626c677102c8ebcf9cc693243637da96b3e5a5f79aa18c93031b1190577d934364452552b80b519aba88614eabfe46b9192064bfeca49df29ef4a1ec758b94c8d502fd5288d41380ab644eff8370dc33d4d53c7b2239641a2eae55062c80d5122e44778637a07b731743eec31e3c6ce3bba49a1600bdb4c68da9b0b2e7c699621395fe2c724e991e40f5abdfd16e28a51e8abc7808de0b2425013f143a04ec9fcf89ce38b1aa112b4848cea59e7a2ab08eba14007e4c7823f84ce7c471654efd4951629d57ef30a1102064d31dabd53fd39122315fec08b655091bfd73b2c5d879fa1a05d28c688acef6fe2dc2f0a327fee119b8cb8090c3ec8774c657c2ad6c7dd76b73e7b27012c4d096004747b06ef9a49495a557314d3bbb5bfb23a7f99b9500ab874a9b0cf6a37714f96eae4aa722a919e5620ae600af14c128eb896db9acd42c85a1eb379291e61e753a02b4fe28bd1de84a0d26369a5c5df915056cd577ee9802ac2e8644ca48f2f3a090072673b2a88a8e1a052ae200488ae495c34c05023ab0144f9362027561d311be2dc02a54ee6c1eae8754168ed08442874ce23551a2a70e08597da0c689a849d0707fcdc2b6f56e7ba2bb603806008412c9e63a2356b9921555ee34a18e15ef6c27a80f1012dc7ae51ac395d5a8227ad3785b5ace27d4cc438180905ffbc7fd98cb8291b80934d39e37e517685b88b701900eb34ac6217a2103f90b6ee1b3475bee2ba6fc320037e921fac521af12abf94b1bd24fdce6259a359ea302d69779944ac8b2b082ec03b1e2f5b55ba33c72834880534ec4cb5e111ab4967757c7c30671a7e19730b6e70056cf93975cc6cf072ca6f14718d611711558109efa02e0d064961385394ab9795e9cecba57312c7b20c09d00c2ad027772dc48f05d90b2f2f7d3d8fcf782f2c654009017b389d4f08ca405e813c3b71449e48b3621837c00abb323c7dc44dda888aa78df7207a6315c5ea65630c0aa3f711a35a11893c58b20d8abb13f7c19215c80ab046095bf9320834cbb40bc63679068ce6809c396a008f03434aba0f1a920649f23de258748de924e532a08058cefaaf3a9a2cb50579dbd242a4c11f653b6c78bba0090c37a4332bd9c86073bdc974e22bdc71dee17931c0140909196df30527eba7df8e20335115b36bb4551af2400f35deb9a84db3d7e1704c238cc155eddcf2ad897150190304ed953692c1fe31b3d561ca66695b30a8a41af01e3c3a6ababad4a6de55ce6505dbfe8c6aacb7d830bd0feedb0ea34c915c7fa1971df519470bc5c814125007db63eb8a0de2d7f42a9a1a3bae7f3861f3d553660fee792c64b8c47d59fbb827c9e6a5d8e644722fa5c0938214dbf7f573a9beae4fce9a4dfc7ca3d51e9c00c709e2bdddee49dc821a2d3348bc46d7b84f9b26fa91760f5e4ca064f8d9dbeafbf4dc47de91606c19a3a9aac19c02b227ee8fe147bf8b3ae377c88144a9794257bbc016a6044ac7e8b5d3572b33b6c23d7145d8ff429aebd07f0472f2f41bc7e1e74b0a79d38b2c55ba60fa401a3004801735b8aeafe887718d7aab19965f1355a8429f30059687ccb8b6e99c83967f5dbd2ceb27b8894e1166502e008b73ff96853df6da21c0de3de56ea36be6d2bd209907f1afdb19d2b2481ef3d0f932898cd32c1c326d4006de019597e3adce0316e88f5ab10c7672cc38e9a4a80fd61287f7f9fbb0e9b022d26700537b59661f09d2d0044b471c9fe292eb3ab988352224c9ba3e9ed6e6c9012c06b226f5f77f2b349df199dd601376cd58372327c80936919117a2f562335a63d3626aecb70645b8ba421c06b6dceb8ff7ee47190b8cb2324e90d16ef81161a17c079f983da89981a324270b4a224024b853852eed5a780f3019504bdeea218483276325856f3b40346fb351d0012c63982e74edd99198aee35de1bea28fbad3a3a20407a22953db4d2928f57b4f82789411f264783afb4fa039c588d991e278460177615ebbc17855f41e5f21c0b0178dd7c127597df3e352242ba2fd33f4f7bd74b0f5301b0ff72072df1c7510cf40aaeec10318d5d158fa8a706401278d65285a16f1fec72bb373dadbeb299efcefb14205f055361455683633884e6e22fc54c5aa4deb21905c83f769acdb0e8e3948e1e5e5529bacdc7f7ad368200fce92c4bc82d2f9f3f4f993af2ff63efade3b26eb2fe71babbbba5bbbb1b011104a4a51b04a43ba44b900641414524a4534190ee94ee94920e85dfebbbbbcff3387bef2db78beeb3cffeeecf7f5cc335d77c66ce3973ce9933efb7fa93e997018de4b6c0fe73a41f0dc17b0975673c5c193b9dd2dfe6530129303fda08378e525b8510623b91a2b70a347d33e10500c814db816c56e66103041cbda405bbe8e12e4c522a206349ff3881efd3711b29fdb2c67c3edadc21454f357034fce4ee498f8366fe2bed2907c1d9b5b03715db4240d5da561434319af63d0ce73899e7aa63c51db1494380fc0adc2777ee217b725c0b51fc6eb8d04b623e5209c8d2d44d2023ca40edb140fbddbbf824ea22881d800b545da4571222d6372a3efd287d77877fc8e2cbc45dbf44a07f589d9cb9000451ebfb87515802b9b01c5588000b14557f2d85806cc8729c8f85e394ea96748d5e1612d429ccf17fdd7e4717a20ca2af52ff172545f537b5794f2c5ef012a3293d77f4f35dfc8c574ceeaa1cb0fd2a91435a063f12ee4792a2c89078916e92fa11cabb6c1ecacc5b12f6adabc37f2645ffad93a2703a664e560ef6fff4f7bfe1b1947377b7d0e152b374bd63edc02bc7cbede2e2e4e6ca636e6e6f2a69c6c367e76aaa28a9e5612fe5e428edfe1f951465e736bb672ac0c9cdc3c729c06e7acf8c9f9d8fdfdc849bd75c80ff9eb929178ff13d2e3e3e334e5e5e76765333817ba6c6fc9c267c3cec665cbc260266f7cc79febd93a2f5fb7c89e6f21a41af21ba06b7e469ea709106212e6948586a671b833ea565d3a141ec327b320d3e24aa110bd577a578112aef5edd2ae18e86cc53f0a18b7741ef0ff0346263fa07213d4009a2ae7958830d1358a8d975cd4585bd5efa13e60aa180bfe20018f98afc1a3a307eecaa1d9f69a8a707ce5831d521333cbdfa674d9fae29f45791dac158bb620196b7e2fca23645de5c752037f3fdd35be315030caa8cd7d44bf16bbec3bff973157121248c84d23e70b1d3f2b3ff975e4b5ecb98b8e05d1ef62c8677481440b1ac9f9eebcd7a9c53f3f4b020ef1c05944efc663cf82b7ee3e03a72646ecb6ff7fdfeb169bedbcc1113fe0b27582220ad2ca5e3ccca745ed0a3b1ac6f5a12eb32b8b636f48578a86befcd9ecb1be1442ff01f92f8096374b09bb9e2941886978ad29c3fea79cb761f338619d1b6648ce4270de4b0e2ee877bc8ceb8b2e6b031771e8e668d541ccdfda99fff79fa39e570a0ac9fc0dce056c1aa89550bcb30654cfbf97afa4987711d3932b7e5ffdd3bd1ae87b91c2bafaf38851b577af72bf5538aaa1b710c2da339ce77211bd1b132f8f0d3731779afb766b0ab75080ea2cb9818eb065409bb2399b45694b8e933283d3d22ca8171ca237b9b82e1b29ec92b673f4b3f07ce5b35aa83fd1c9dab3f9b63ab250bdd4e848fff533fffd4cf1fd5cf1f9523735bfedfad2261784a4374c47cc514e4e576fd4afd9c1a871dcaa65277a9786b2409b96ade112aedbed9324b27430f2d31dc8450204ce0abe56678d19776c46c3699f4be7f7ac01e726a7e41e72e0d7f92b21b5c4303f14f1ac87cd379776ca3a8a9da14596855bdc7c4cb4d34c53ff5f34ffdfc51fdfc513932b7e5f7fbbdb67ec6f287bd53574891305fdfafd44f870a314e07c4faaf977cf461ee753102b149364e1c8be344f2ad06d00d4453177e0eab28bcdd9fd409e663e9d66d767b3be2eeca3742dce4e751cea3be9fd3b4fdb3f413018d27b525a2a4991fcae6c0fccbe2f1659b2bfa9ffaf9a77efea87efea81c99dbf2fb5e73fffcf02bf5b35ce0ad95e19d8f53d9e7c6a43d8e985b988529ddbd6188455fd68a586389986f98f66dec473c0d572cfbc816569f6be0338e9efa283df4286a228d3e97c26d7cee3afad908750a8df9df2c30357f2accff86c25ca5105729d4550ab3459cf886a93af53da12857fe9b423cbe7c4babab14e6bb797aa82e3384df6b7b5da36b611a6d15f82b154674fb31f42b075a843e975759db19ac05905b71322e18229c0ee5fd5d3cc8ef197d118c4410c80678da45f5313e9cab44a351480de1ee51ede5dda3b34e38767ffc0732c5f003ffa314f02d3f21537ccdab24d04004bf26361e63cb3830b5d46f6188cba3e13c17a819f9e74efaa761f851c3f0a372646ecb6ffbddee1b9efe6e41314b07f9d757269070bfd230f82814a840373f13ca397eb7524a70b86368b0765726b764c5b0aca00e6637429d9566d96cb98d757be6592ddd56cee24b78218383179c49c432ed6c4cf9ddba933fcbd35ddd2ff5f5715c421dff128b34e3fe3c8c183d06f64ffdfc533f7f543f7f548eaedab821f288a87ef797a78a571a4504fb7fa57ea2134ef9363cc94e93f0cf8caed1afa179ea7914a7dbc12eaac2ef0fc932aa116d9e15c55b97333142b72f4ef57ae4e65e78cfe42459f8c9416f5d96c2cde5a19b24d7f37451d4fc8310c5bb82a8fcc76b50a47fc2a67e5551c71502057d8552c35da1708820819dec6d71f677be18ae7cb1e1785f7ad71ac78ef17fadd18192fe7ba3f3e3027f6da3f37ffc81bcaaeee33777b792485272a2dcf812aa0a3ef2c7de37c2acc6d8b87fadf0f60aa51dad47b6598132864fcdc528594ace1ca3b2085dea087d4522a1b97746c83fce242800cb9989f5351fd234a5800c6dda13e1087bd7a269d75da826f73e4cfc3bd39f75fc326e2feab4654f728e5d4c1a32ca3483a3658a14f1a77cffe7c9b7bc82be53a6e3b821826c1a3f19e3a9e2c3e8f5f3dffbf269cfeb4855a9b86bc977b3398a4812df76bdf2daf30c3b1ef70142a692c1b0f26d15bb62d148f7e1d32d9af058aa7a39e867e6fb706737f0713bdc49044ea905e45934d008d33c8849ce7f967cbfecd64187a32d98868f61ccced3626577125fd4fe53befff3e45bab44e462f5031bf6938e3edbc306bc60e19904ebdff7300abe28cd5fabe84e824478f0519533073f86e19789008d2f5b56c3c45fc96e6ba5256cc7f6b41d13f56acef4b5d4dbb743a2a17ee45df30c632e2628d66232a356e88e9395b98f438efdb382a29603ee07a5e6b9179bb0708d3ec423d6078fbf2efd29dfff79f23de017cd64a764d639f1be827df9ee8e527beb25ccaff44f766987947a9a6eee352db40704e0e8bd2c4fc1d57f127498a8edb872fea626d9033200525db6d8bd1a899e1ae2e10094be61788164aa17faaef599434047c2fcbd6ba6cf21c71eeabd47fb52f5f27f78fa5a82fb279f5df1cdaa2bda517fa43dc951ddd35d63001a39bf4d871f2efb3e7441abf0ef7e1327d1c588535bf7f7db6d183b64d996afa79957aa05ce0f8df9e73f467f6469f7fd83f174901b83efba4c50fc14107e905de34f10febf7bfe0d40f81b4f21d542f0e55dd7208248305afeb59c9bea776eddba79f7471937ffc59c9b171ec4a147f045366bd259a643091dc44bd65832dfe9fb2f53f9570c85259f5f80a1a0773744f1632a04ed17b74805a115a9f4c4a5180059f72c554cc1405faa5a8495d7b8b6c20d9138a707405e6d8420b197ed5651daa19d7141d86a1e3f58ed03747401961ed1b23e1cd6b29db6fb4dc4b3db0237c0e53e367ca6c4bb1d67007991df009559484e20b00ca4d8389e3cd4a5f7ebc628438d5ac15189b42396b34afdb6bd0f460bcd4ab8c41ac2fa963f7cbe0d9f49d23ec0e3e9a4cfdef37a5b2e668069cb9aa93cb264135d1c600e38393deac735bf749243d0f8d0f58c379951660bb8e751de1e6f0393123f48c9e4cef0591b976a32f90bb035993629179e7bd3e724c231b4d5d3f79cbcea10018acdc4579f9bdc57dfbcb59ff942bd247d17fe8cb4fbe8db76dac9035699ea1d5d3df8d79e34989d47a49359800e974ae261062fcdb620d575580b1f32111bf0329f7edbaec1b920d1d9e796f78ca120a61d65f0fdaa1e25708490a0613d4d6c7aa77f28003de93646b240dcba2790edd0a9d42e85d1246757cc0b464ceeacaee9b3b003b646aaa65266e228171378f130ebed80ba7b5adca800ed3a861a1681f7f69e542d0a4db3f01a391384ab34303fdc01e3231ee3ac8bfebe66e9799b7b31981c5069dfb6dfb4980fdd958eac59f3667b1fa5417086546c06d0560bbb2898dab80a26a8b4bf18dd7b1e07e3ef240120e3536d7b796813913e4f7bbe499790c069ec8e910adc51949b22eca24267477841653522c31ec5c8c6d90014e387c5d7f865d93d20ff405dceb71ad11915e74301d0528b397f31f6583a56168377e5b158ef8728dc7401e497788032380b855bee5133e4b852f1cbcf192db9408a7bda50f730a5d08510de9ab0def03ecac10a032be87aa03cde19416c97c1c4255eb7a6174b54abc507e68feac5e3cddab725fa8d6ba3ea971f78a046b0cd01e4f0bd1526daf782f2795addf8d2bd5972b49a378a016527ae64b01084fcdae98324cbe92725f1acfbab1f8031a24b85d453215bee6ff0256212a202bfb237761fb8a3dfc21ea2129f8573f09822c5c2e12db42fb5c1f0836fdb37ccdb97e0c47591b3298632f7933ce60fb63000d4bbf8dd9ee56257e96afb3eab079c45256ef4544480fcda591e7c5635f868e088c1af25c54db15aedd6fdf737e9e0350414850c3a83e3b73a0e789c9b8d01fbf59e3fcb90a23cc83a076747c09be0bc11999613b8430e7f17f7c5f329bffba38f6af564d3d554aa25a180f589aac33ad5d855265822a016dd7d7f37dafac5128011303af01876828f34e11226dd00762e1173f8563b802c6f72a7d369d5ee8eb3a4640ec2c4e173537bfc20e00efe603be2d7ad00ba9bcd85ce586204be67480bfb809b46ca18d92ef342286339dc4b89e664108a40a91388a86ebbadb9888c27bb502994371142e06e0cde8e013030f43c3258cc7489233559c4f66a2ee2a657a32001b764b0691d91792b3695f504d54f40003f36ec9c1c28e649ab7f3f0f3d2238da8650d33a7701df9e2e7819f46dfb6bfc2945e8435ac8384ac312cf94e4734f2d5560bb6ab0def97243033b95f2dcfe209329a5a14fa8e2f8db76aedd7842563c0ce8f99a118a9361f1b58fe59635dfb69361219bb22e625fdcf03789670bd8c1c3681c07307c9a150fe8bf5ee235b1d1f60b7ed49e7af7e88c1a70fdef9c50212cdeea7719f1b032fb2cab9f96d5b10e604ab927f93f6f48e18d88fafa5aebfd050c8d836d15f0feac72d31224fbe7a4273b6fd7d0ef7efedcbd4ac9fa6d3b67cfc06ae9717f0166d740a3d683b637de387d40bca551f9b671e989f22de6bb85c5846c4deeab93f4c0763f7cbc4d822deb5ba35c523e92524ebc7e38810d5cc5cdd97806d1d613bc76e396a94d5d9312c2a7112dc0fe3caa7345538fb5a3313fcf67d6585cd1cfda1703c0d9acda884ee065f6a6c3d72d4bd31f33ad6a4aeb01e93ca90dd95d7b3a6615d3295c3189932ef5d5c795c06585de677854abf6ef91d9d878241a6a1e09587dae03de9ff811fb8142f770d22cac63d046f5f393565a5e00932bff0612e188ec60bba621812496952e4c94e13850db8282adfb325f092da133e491d1aa586ee55b7ea341c03e8b29bc961412f62e4dc295d44474e56143ba6ff96d3bb30eac91a3a273d00ce55611ed57fefe6e1b040039fe92dc9f11fe2215d5dcbf03ee8908ab4de41b4c004307bd7c2ea076ddd061349cf5a56419568696f336b0ffb95353a1397a87912da7d8a9408c77be22df3806fc271a96397be501c39bb5cbf0abe1d2f78d21b18c018c2832f13c894f4fe1571f57a9723de79dcb38440c0730244c2622c3613dbb9b9cde4cc8bb3e2d512a9d7a02646dec7b46f38ebd060bef17e7b443c044a7c1d32901f0e9bea666f666ba22a213a12fc4e7b399a13f50050025042edb73b047eb15128a2c7c831e81e8accd462b00860c47c0e1ebf9add7af31fc3a713fddf5bd41aa6904600cf8c4b41ab5a00b7a47def2c9f7ef3628f5ec830268f71fcad33946d6690f57fb0d92c132636673bfa7068e481f90b6eb9310d6853c19bbbd34d27ffe58c6ee1440ee877b4aaed265caac1a7394f7b151c90b273abe0cd8a0d85f79418721b2ccd91978bb173763d4f1be8507f61792de7ebe237cabdabb2faa78d1e78a2c5fe5b701cc424de2692c76622259de429bc21a1dbd39371f8f02987ac3b0da94488f5e2f057393bc20b2ede0fdd2830560e474faa36014d832064f4f664a5691b3493f38972600fc03a7e189a5e991c7e7a816c92b8577df943e4407309eecea32780c097c38933b6e308468fae0ab4b8d02d99cdd6af29997f2fdc473c6db92215b4fdeb52dd700fe5f461d41e013ca22c90d46751d7cae33060b593d70fd2eb42d6d23fd12db6c432a629feea9f0627702182242d413b6dbf862ce3649ad1afd0d2f02180bdb010c85497a4341343409eb511f17e8f2e8e10d978a732028ea806934b9ef98b5682858386ed2a5f13cedf233b0ffb2d16ea3e6cc2a2df421be165adab41f7c954b0d60040d39e40b071b621de5598dbab179f3dce57eaf0a248416fc26eb2dfd5a5d5334f82fb4d01ae36fbf91a4873a857ce31f04c38b13840331570399abb30439f5fdb89523e19a21e47543bc2598251d406eaf2a5d406636c950e7c73e7895e9ced77944abde9f7c19f2237199e0fd78ea44e64a8dec2efe627a4e8a26579b42a4ef7d199a7cf0bb893868e2d27a886b65ea4ed4fd83dd78122142a0349097fe0126e649a657c64355edf477ea2f2961315ca9ca0c5e7d6f55a0eedfda58f9e9bd421830a662fcfc5ef58fe2b87e7eafea771fcf35420d87f2534f788f584204437634fb0743f93014fb379e42def40fca3383f9d72650b41434e4a56f4b68fd680ae55f9a40b18b47def078b54fb08a8fe250cd119d216b4369fabdac1b959cf4c829a4ad7f10a583660da4290a0a7fd6f747f300e08fb01709bb99342a7edb814ef8de6c1f42bc41832fd315c7269adffef5f94b79d78897c5685032e3ddca176f16d6ded1ad600d3d417f602333cd7da7f6142a643e626fce449118af3fc459d755984391ef0a281688b96be68e9920c1725a9ed2dadc3b6af306166acd79e76ceeabd07b3d7ff1b189985c5dfe2963f1d77cd582dc2fc857b15e5ef6b0490cb006ea840a0bd27d20c1a024bbfb6d7b11e64d1e0cb507385efd6385fbfe447a943928000b81868bddab084fd2e6bbe7aa41ae36e6b0e41a2e40bc06b3ea2c43e68286e54b71cc2614cdfcf5ee163110cf6ab022e34fa7b07fec94b31745c5fca84bc6ff1ec88730476834937d2d148630c973ee4296d3803bf002c4017124e77d50abdec6d31bf3d51934e16149276440ad7749168ec72819620eee8a98e0e1a7092525486220672ebfbac1c6714e29717c538345a12cd56a926d06b0fbeb7b05789eda013d8de5d421d91258bac98dd65b803f522e973bf7a6e8fdfe5846e2dbdc583e0eddbb008afcf01912994d4fe4d36afeb401ed770eeee2d60d803fc054acdb655f29d2c98f9ad03286d135c356033500f86b2b79d91bb716a25249dc193e1674cac5066203f1ee3befc064ecb337aa216c73350b0abbef6a87c281cb9befc23692b2e3c64628132ca00312e5c91d8ca800c66ce2d941642fe3a4d2aad8c91bc1f75e3f7c8b210e60f2a557ed1ef579ad9135c98e140411189b645891004c4f2f7b64b60a95e80b9266aae7a02a9c4be5299b817ce3eefee102566cfc53ec64cf01af0d8d75da843c80e94a90b3f456cb079873ad4f184fde592384a957dd6d03fc5d35128a3068ccc5ed3e796daa0413eb7dfc270013a54ecbcbcc4e9686702578a1e153ff194588d2b2d86fdb1df390bb93c6eddbb1bf12ca312d9eb2a06e5100f9ac8f2bbddc966263823b71e91e9f556a9bbd5a19007f99d3a38266fbf9200ade09ab9dd56a794c919ed12360271bda59d3124465700c2979f9c192783f026e0d602a7c8ace7083b042a005f940666821989eae6cab0c60f2aa6230bffd21e361a93deec7f3bb8f4f273a254f802a391a02f9fb0c3771240a895b437791cc1feb40ba02d429c502d316edbea8aeafc907b0d0615f91613f4e07e211169427c7a62a1149c99ade62f64f37a32a83d601a656744cec66c6af11f2e7ed91f755c8ef2ba86c2e02f64129fd62605ac5c25f4689098a922f6c9ffcfc0570f9d09118b15688e60e3bcee33dbb0b64b566ea0900d210e2b16c6b6ad1857a6a29eec38f4d0f046d8f47ce01a63a6a53a8542267b8c8b88509eec0cca2ac19db45a0ba8662d6676c8c32d28609f76849a7e232fbf9076b80e583134fe1d10c5bfe91af58ae4d399bbaeeea3111100fdc237cc2428234f14a09b99eb577237bdc46441ad0bf143ffd83dad5e9e7b786112c1406ccf48f6f910398667bc19f70dc71f972c66ad86e07d027600e513e07ec9773a26c47227f2592667a765db1f52773093225209e7a3189dde533dc540035535d46a45e3ac5ca9c0f8040d17d642cef988bda14f124433b3aebe7af4aee9505f2f1b938a73a7e071ebaf592b11745deacf2437280fec0e5d20b28dfbcd8505a52aabb58bd1565542009801f1c102262c6f7909176bef1eeeb2bef1e2e9c0a03f2e1fe77bebc5b6860406326339c38b1d3b3ddee9b05f463bf9937a4cb8d5b7f421db5181ff56594b5883180998a40488e5b803bc3c0c5e3106a5e951a8757c50ae4239a84785006722cb3dcb1ef8bc8b12fa58a8d1f01fcf96a2d1ebc6d642ffbcffa893e154a1fa2531f860198d286f59b2682f8eb0a726121aa5cef0bbbd6b2f1807c4aa42e6763ea89d363bdc19729b628a481c55118c05da2581c2bfdd190f83c93e719eeab28a55a5fd10b80333fb7cdd768d31afa87afb5f7cc2366c3dc769ab570807c3aa4874dbc17134b580221956a6e291fa4df33209e677185a88cc6b7ab7cea20cd99acea6f82bf1405ec8fcb1f14b7722536496476c8cf188999a6ec8e5400a667ea053e9764e799cc3be778f976758564fa5f5501ef2d90b7f5f3877d95e80f55534ce845032f5b450d01ba8e09eeae04f17bf22dd2460aac26a4cbb03b25d2403eb13b94da56a2af450b52d3e32101a3670fb49709c0e4aa56c0cf8405eb1e72fce0460e7ff23ac39a535707109755bc35911441c345327676eac01c98975540043009fb9a363cb8df1fd8b70caa9f693d0c7aa43ae20830f95af0b9e71748d4cb6d454cbbf5aeb138997696e800f9fa796587ce788f6863eab52db4d9c9c7cfb0a5817c53fba4d1087fd5740ccbc777f4b39c5e28ac4709807ddf35575be3c51edb54d35e4b34dfe8591bf0e501f68f9421c88f1e559378eba5a492a7c2ee91c4b93580fd48e675c36621d15066c34ea67e339a95c30dbb025c4b1821b78660ac71b76532121530f8fc041df3660320bf6893c125dc507b879fa255f3ac8cf70954e7ac01fbf5a856638f47212f267548a89a40e999c9d7e9a059209ffee26803bbb7cafc5efc2b592f62aace92c85330dfcd1ab74a6b20e9a93a5c4b840e0dd1f1aa6577eddbf695e0d6c898ec88f7fdb29581084374f64ad628802f8a68ef0847bbe7aa2b1dc3e08e632de7baa09c096046aa42c417a2acd00bf81db44afb7487af57ddfae4f66d7bf6d3fd0f5a2aaa5b8c2d6afe16b9dd08fcb1e0fe67669b999bf2756eacc8e2b954f1180e74ae7304904fe69dd0bbb0aa0b0abb1909794bcb082dd15c5b03b89c1e535e8f74f4ccff2c705d14d9ad03723d1cd203c8e7ee76b0de4c4680dac4b5b53b8abd2846291cb404fcbfde52610c05234b4f617184f6a7f0c1476f6b7580f31e55d71b0e07fee9a2823e4fe32e5e9541e7d65803e711059f9fedc7c5b41b43bd8b5b0b082e384ed7b800d6ffe0ddead3bb819dafa460f3a5d0bddb09cc4fd500a6638f3a9f4727c79d956b335a7c3dc43be6f6cd0360be2f56203dbbc89c6388503e55d84a632721d304c877e630214cbfd9306f9c25754aa54b9ca686732b02aa2c6928160ff3176a39a860ca5d2456726af0281000ccd697c255d3b796d3dd34f0d260a6192a30caceba00903753d29c1e035ab70b4f54e9d98e38b8b7eb7a4c40bece00ae35456d363075893a8dca99603ac5bb830260aaa56979d9ba2a88e6bfb096f160d1508d902eac13d8e0a75e53725417150c7d52dca113e4e3951b386a07306fed7879efe99386231c9276054fca7bb8b7787f26fb2d13a765d1ff021367acc29859d8f0d13abb120b8ef3a730ff2da50ce4bfff9f1f64e2dcbc66e2404286f5a02f7df49350914f18f4a27f0de9d8c5fc884246c1a8d3f19e79035be44d439c0a8949eeb758448378af421133ca0b1e0c3d9931ff4a51a40d95fe44a586e9ffe74c9c57ccffd54c9c136d1ba7971286be95c40de7a4674922a56634bac63b78de1e9aa7e27cd3067c578cefdf9d89f32af9943812528291a1a976923b8229c47452129b60da7ce0638a9d11b1c65b091b944b2e7c59a159721753324fbdf17e71b68ddd79aa94564f73f2538138221d34d1e729d765e2e42cbf13ec2eba585c66232caf22c8ef445c3f0233c7fd16b2162ef0cbbb6d081545f1d71911d6f253e9dd9d5675ab6b76595fe1dd83ef405b0c15134c33ba2fa2fd81abd4df305afe7785e44f67b4bcc873dec934182977e4c00decc8c010e66fe444bfa66db892d1d2d1d2ccc3c3caea9f9e7b096f0911256b8e1d91c20b1fc7ac2f49681f3a1464ac2d67dad22853ad9819e288729777bcd8de7966f4186a52289aa61ae3c6494f78975ccc27ecbc21679e5e21fe8373af5f1c8140f00be71e21e371315ecf4cd8fb9dd7d345426c0e6709ad17ffee737f2310965c51a1b649799bab1e81b1481399361c465c14bbed9175efa08f9c942549c9cd5389a8cc37de2223b3f6e5d8f41c41f5a4baa7e75b02a1beba21d64dd857cffd60285369fb467d23c47fcdbc3f40f4fab373f16a68bc863f3fc3aff42263ede7f7aaa0464df3f37b154ae2a5f83fd56b5f18ec32156b1dee796330698f19847f5f9842a39f85fbabb7107ffd1beae4f65f59dc8231e79ffd831ffdb4860e954e02f590292bc2f1c5328737dc1173d17785520411a2f1a777cad917f4f33bf58b13fff99d92dccb6e841a0e85e24af1951587f89faefdfffa298b29b5fc6f3e65bfad1c9dfd9b4f390793d4a37edb43dae06d9a3ff8bfbfd3437c205522f8e9cf9e8430168fbf0ea0f0cee39bbf1d40004f9ff66f3f4d186575bfd68bfde34fd975331bd87ffbbfc31b868ebff99443dac9f81f2cda3f5ccadf79b790e9558a3fba10bce5b73f5eebdd388aa342f87fdb6f26ad01c21f1dc36fdeede4a67f3024e77dfe6a694c9824e0824617af5477606229c77d3a7a1939914d05e74ca5ab2ea0406ef807e5e973e7ea11e603a4535ec83b1f4aa470a2c61af42eb68feb4d0663661a4fa107ff8b338204a3a506bae5fa7c11d0bf6937893511ebc59727adf5ba50db80a3e4946160c6604255234ebaa37f7b4b7f9df63d872abf42aed61765fa4b0bf564f9ef40f2fd962f22eb687ccd5654e594a4511edf6bcda1ddd610bafc8ad005e27aa1cd75f922a0e620febd9fff4324ba926677a4ef48f1a9f1baf1de95d2e2f2b074b253d2e457d3d2d4b96f6a657edbcc4e854bc651fba693a6b4d5bfcffc4a8cc9a850d363b4bcf84881217f4b5ae3c9e3e8222ff79658f852942c4be9f527a2be564ca43a84ef79c5a9151af12544e009cb68c2bf48143937acabf02b3e7279793549ec2dffbf6483284e83aaa5110d5e9401c7fe9c0d355afc163d4e7bda7a5c918fc49245abbf8f2601cfb4db08d5e7ff5fde6e7052aaf7f346a8532835ff20a8e3e32006e3d61a28e97072154941151a3b082619d59c087ab2e40783f93f382fbf9b72825cea1a3a797f25e32b5a6ab6aa609084d426df7b2e0203917ea53b37d41dd5d66163dac5d086ce3342de2f2d2fbf64bf804716132127351de12d4c7cadb020b8d9c7cd0509d933525e33c473bc3f8da966913e6f3482ada86fc57ddfb82e016f846eaa6eeeeeda1a2d4c732b2d46b6e30a75dce440f44d682bce3da4ec39449a3d7952e117e24738f8870a2fa311f41f7f6dbab4b6b062a18c187c76978fc38fcba95dbd902d9f789f4882fcf550e067e84c0c8a5759ed959e5a6c461a829fcb6e4be77f7c0febf9f1f03e0e3426a49bc23baccff98b8bdb6d845fa6b92d2409a06b7cad612a04437defcf4916bfb930473c1f4f25b0f3fa224128e97131f9b9d4de6a3b368c3fb48f3687c0c0aeebe3234536c849d2ec6de16c39698932920b06d74a059c4fef21e64d13880954b7f7faab3d45c33a94478615b904656699ce295915fb0e7b87d1f6081bd6176af4ec5e9fc1685195d229ebed4baa66218ff4ef27fa240893e46c5c9e1cd59eae643917bd82b0caf04070e618f2b2b659a70f6d18ca63edafc4657c4171104123da3b8c82e04c249e71d8f99ec1e30c31b39bd4d565067a8b881f2280a23fe3f336a58c52cc82a4697eaba577738a7ad88ea8b7a1cba5bc3b3bc799b554a0886df52ff0c6a314e57f41afe1a673bfa0577f42c15fd0abb78cff2fe8d5932afb17f4ea711ef75707298c631fe60fb88fa7909b7fab455bfafbfbe00fad6b3498d1f55bb7c2c409c790ebf8de8f46fefec60f1ded9bdb7a6529da7ffa7d70a3ebee6bfff4fcffc19cf78992fffff3527fbba5053838d1d094677ca6d36bb7cb0f466fd0b967b17e79c5f3fd4d5290fa548a000761a38506d3fab8ca0325b12b39efbb63c3c8e96f843aa90afd9b28fda5677a35edea5c5d1d53fb1fd9f8cc04e55196c23ce302043cdfba751e51118ba8debceeca1665a39aaf8b11e43da67840ae2e7b691e53946dbc8d6140439f955f36db9df3baf12f9381d6be18c4a32ef7dbc9f8cb4335ec0c8d8e6937b37e542ed816f9ecaae9fd7f0e82f67fcfc6fff809efe903e40039d94f8878d1ea639865ddff40078f31ba9ebbec8a0beae04de3ed7ff05c4fcfaf4a515e7dd3f81ae3b38fdbe87e7dc5d1c355d81ad93fd32afcd8f8a344f0337cecbf3f7ee3eeeb8eff2fdee8df44ea6fdee84fdf8a8c92727f45afe6e58d90a7300f42a1a35aac535e42040ba57542d4c098366b6bf8fed3b3f2b7bea6d1268ae177cf1d7c89ad3f7aa8ea0b8c33c13412ee8cb17e95c491f64153f7fb7ef72061d98f1921c838bc87d82d57684de38f68f50f45edd05f89f6d855c5ae983fa47f3aeaffdbfc5ef17dffeb6505ae0abaa1afcaba5c05237a95d5c2a9ab78b4f6cc9a1fb298b1065a844857dbd1abe7d60f180a9c6b1a9a6b7fffbae3ff5ffefe2f7f1a4fa1121ec2f39329226f0443278442d440057ff2589db89e3186bca65379955980fa21f46da758d159eb0a3459380ebdf768c79999e9a81b8058b5f3492eb30e361faabe4678f91a1e7bdd818efc10e29fb63bb010f191dfcb9542f145bde5daf891fe134b105e41686560c397ec302ba48d47442dc700254cffc4d5adffc5f9f97f0fdc15fc80b0baa75051fe4194da313550c16fa46094ae3abebe660871d5f71dbefd43d9b9b08aa9799a37dc7cab7fb714271832c77de13aea626eeb7b7ecd182cd8510aef0aff18a23a068912330d326cc68da8e7a1c61b262b9f4771cf088f6b66e79db9959bf70ca5afda29aff9fdef3d8114ddf3af20f34605028b20e0059eaa176e3960506c7f74df7edb5fa0807c8b4e96371773efd3e4b0f90dbf2d6b9c428b77696349fd2f83b55a5660e8925e74183610b7cf04e7e9e7bec4e7f30cf74d8e39e00839ce6da89441ffe0db84c58369ed3ba686542eacf7b57c22f4e4918b8cc11a821612a2cb126795313647a0087de540fc15e7fb50a7b0987f8d0bc994546a60615c0d02fd7ee43c448a0fcace192be834deef5907a1b28b7ffaf926edcf94596bc642568a1de38a0a7b1526036e56f56dbf6c550808fcfb19bfd93f7ba1fedb4e7dffe7297e28ef0145ab03360f7fdf844379be0703e576f0fc83525fef0acfe58af15d977f1b3277bd484ffd9a08e7e2d7cc1dfd54bb666e596fbfe12caea49ce7a6e7a65adc8e5b69c37bcd49baca2efd26349209eb628f7a77c7fa3e65da7b27310cd54a44d3ef9e2bb21523fc2ac7a7221382f2a5d5e72d4173f28ba874894d63695ad2bfbbf9a712f123f36d6ecbbc7fd58fd679a66f85f9c87654d219d0381e7abad468edda3716109b2ba9a15617eda33ec3cf4a88214b616b32cbf48074563973fa28f154066f91ce466c2047cf0c1943fe07caff242f5b53f0d2a53c9dbde351cedc71b94829ebe3138dc2766fa82e99bf0d392610da35467a9492fb01cb442dd391028a723c823d0637cdf2ad21d5a28d4ef43efad33a92abe2ed9338ff20c471b520aa31c8ea607cd45eb69f29ce6abdaebe260e2f8ece3ebcd3c2c75462f8da96fdbb607c116dd430e16bbf7c488daa0ca66e39dc092a90c319720343e3a4775c89ae1ed229f4d87f1da6ff57c2ebd71ca8f35f9ed5d65848de4a4a7a21af97e876fe2c9218ffa67668460e6e89f903b43c2be8857c1a91ee66a265c1a560046e89ef4014fcf6409d4fddb500f6c507b8f376d66655729a4dddaca48a6bb9807f1ea8ffea037504057b6b33131733d37ff2fbff73a0aeaec923a7ad6de3e6a82ca9e87ad3dec65e425dcd4a4242d34ec2c34ade41f181b5a58a8980f31d53b37b7c66ff3ef32b31a2b532398eb4d3d6765676f20692dbf226a761978a7bb65ee667a6873497171aae013e561b9616d4283b7b1d2d82a3cb7e7b22adda9c8ff25f15b8977a151149fc815a6798e3bf576d98a5ebab36cc6ff6558a380b8218145989ace63963c923b4818c9966b5c5f6cf8c0308f49766d179e76fd5464a1831106d1b9042a3c4864ed47e44b5a90a95f09087bfc82394b584d4e09a24ca2c0ac0fd7bab360cc4ffdf555bd9cce59ef23d679b6babb68aa9c64d0e3b7357196b1769752d45394e793727c9bb26cef7e46ca49c1ec8705a59de32e7b397d3bcad64e5fef3c67fdd78fc2abff9aa7272497677730e331e636e4e634e731376330e13137353764e0e7653336e1e2e1373333e6e2e631ee37b9cc6f78c39f8f8b838b839394c4dd97978784df9cc7904b838f8cdaff5fe7066ae76821cecbfcef411447e96125334d6575cd9c8a00c2adae86b87b2dcc0875318c823a0a5fc64cac82b7979f441df87fd4ee42d32a6d5aeaf7aedb266b9d66364fa3e9749e8dbf1d87fc0f42162fe4fa10f220c5f2b8ece3517f59a8c7908a61a63a8e13f3206818ccf9de9d1ddbc598b758c667838ca4ba2b780bc549403fddee767ed7ee8099149830f28c61f90b58fff6ecfa2e1af98b4f067af90fba4ebe555e14ddd534fb77e648c3f921ba47a61a7547bfa0238d196300b1187c0698ed24be1b3d871c9552732350cba621ddaafa79c70961277cc4afe95eff89be7c603c7ebadd3756519e62a5dc1bc66ece07f4d5d85f855ebf3c71223d0d76d87b8967c5cbdffff6c7837ac3bf656ce6ef71c35b9aef8e24f92efabe40388ad961f497a226daeb32c6e6d9313cce17ac33ec435bd66e2e8aaf991fcc73267fa8f51ee4cff891392eb8defba0ff2351da0b95fec60fd90fdbbca3efc5031c9df9e878f830118078c3f6823bddf569adf0c3d17473dd167fbd98bf6d7c243b2d0b45f401db198dc1226da9e35fc25f3462c446bb863245d2a0023423e91ee6dff7ac7610c0d82df9a8d12d78a4a04ef87c6cef84aa094fa221a094d4f956e631c41f8812d90a0ff31ea88f4a2b982ef04699869859a672a7ad7d2315cdaef820c5e5e5e5e5e3346b9b2a0ebb480d93f88048fa7a680e6faa871b05adffe45bfaa7fbbae605c5ce26231322c799ddcfc260960f9a7b5755df008dad7296063a2a66355d29a07e681dc339690102406bb6e3e2167f7fce3a17baaa2db115e804a4244c68c721527b82c213ed147470fb42edd07ee0fbdd8dbf18b968228baf93c858a0d757925ef280250e9a47ac7affba225018fd5f4f7c3cc9e66a804f203a83077dedbe5eeab70897f814de7a6558f9a2aeb7701509e2d67ce775d655e3dc6e2cd1b3078560977bb4a1f40bd30e96ba8b4c9b997e898fe1c4b2d408cbdf100ffe1b7edc5aeed77aba120d3c5dae98b14d4c893fa6f8c0128d3ef9f1ce366b8ab9c409c2e1e4bbde522381c9b11fcb69db9348f56f90d56efb8854f2532042787c2db61006d14ff79795b6f88901964f111aad8597db5c0cb41c024b9046db4e41842bc363980c43ccfeb9782e6bd41f36d3b8e7257a69cf3e1325e128dfea7a7ee0db9dbf0006ad3192b8c4f750c12e6e1839d39d30c59ca95a1cd826fdb1984d5624e3e19b55658177d74f5882c0a978c02507e29033f0e5956a1b55e3ced7ef641e32461b87419408d5a5f9d0eb13dc6b1cb554354134f558d5ed5d602e65fbfac40738de052beaf6f61eea9502ecb0df12400552e72f9bded9400dc0cfbc840b8d2805512b57f12707faab63396b0ba4dbd35b5b431b4a3e089c4444322805af4ae74b38334ffbe0f090153d9cc02f193778841006a923757122491277ed5633ec86ce31a09ceb6591d20934e708ef19c804dcc84d1a673dc7b81ad0da7f025e5b7ed4a1c7102acf93b3ecbd00dc5b4ab4c510d210f01541803ed5cd5484e291135889d77cf5fcbd6c1e4c900957408b79638cbb3624a535ce06e627723f7cfe0390228db8450c5e82f1b32e68452e92206fac7c3e9a20b2dbe6dcf9dbe7c2dca4ec4431ae8310c7b0c4f6ae2360d9cbf63b604a2585a0d8b0929547d5298b245dc1e6006d697df4dd3c418693058ee2039ab0ec5b5b02f560c083d270e4f7384887b5cdf7ca4d5af6617f6906a350050e2fb19cdee36aa6d098631bc0a8deaf4622aabff0cb05028b20441122d6c924f8ad60d51a172529cbee90358009a8796be20c9963c12868e0c1e9ead40627aea0454f291c8cc11307fae695dc48ca7309de20e41717307a063b69f6fb6e725a617111dddae55f41718dc19d902c37fe57015e9e1e99d0ac976d6833861040dc37c00b5ef2bc10dee056cb2b426c7c017ba293a2c3086fd000b481de184a26a6c7110aad482d793f9f0e1db7c5e00ea1e87ea1b5353e427891cd0a4c22284121b08e4b100f44647db83dd03b58df9335f04a2206d2f73460d5d6ec0bed699fa9f22a2853e16b193798226af6dbaad07d89f62189f3bf03282896115f8b85129c6fea4b38d404e177d8efb380ceb58ad1fadb6753fa8db2c92c95be1dbf6ce51bbfefbcdc9dc6389af2cf804e79a8d116701d4260bce72bafc3ccefb8b3750da32f389f0b7799a807a80e52544586da7b579f3c8468faac10f4bca2be940d87f13875293cfdef7195de722d4d2b8d84ed58e0f50e9987baf3e4443715593437b5134ff9031bfea3e24e02e3c4dcf7a7f52d46537b9c969ad09773f8527de4bfddbf6c2050e4e77fde7271ac6c2912f793fb0d60c8903db6d0acbadfd1c467bc6c0336efc298a19069a1e1b00b5cae50b8c3aa3b4da04aa8e51ff9c090f27bcb93a805a7643d7886359cbaec5999c15e5143276516dd60b406d82ad7d167cfa492aeaaef0c7b75566551187dafd402583349374e522b96041f2ddbdb304b8855e1f9f9b02dfb65b85a81de94cbdc0fe924cfb82ec6433735cd61180c078ac9b47f9a81d4ad69bddc69b85f451a5b98d2280fae429b26ed9a51cad4a513e521c201c95ce9ed304d42f6b23a608ec7f39265e5a3ea5a67860afd31356021c54e76805edad32c40ab6f1c4c6e034d56352cd71032117cf4d0eb7f217fb84c9f53eec653b62ed92bef5c0fa6bbe717331be7732241fdd279ab8e717a8bb3305b8dcaf8a2a1df0e232a5026f8d45e80b5ae31c5b4f02a88838859b589759834e1c882c4f10a68946fb7306e981f9974e6723a478fb393e842d41aaacdd2978550febdb766c76f4dd63a5500ff9ecdaf098dd294563417a60fe7a5dac2babb35fdec92da51f232c9d0fdaec72074ac2b6ee8674a43dd26199b22cdad9103a2383eb840502aec8e8eee6da63831275556ca93d79b62ad41454c01dbc259b4be211bff6c995b9f3249095b1a50a4e8eeedbf63745793256465935b4b9487d4c988409f371cf00fd8fedec833fc24bb5776e8cf3f548f1a0856b22005898169a987c1e6fb4ab623944c0f70d888a293c5901f607f45808d27aaef6d1ec493681421b120dfc9af7803b79792a89e6c4c1715ae671a77ce5191326ae403e80eaf5dc67b63d9f24a4a3d85558978e14de8277110bf08725de7c827f78a12a641c0e7be43e97f072669f1b408d2c1639f0ba536ebb3842b76f732bb8e459da650b30fed47d1fd937c7375edb637bb2f4931d3f405d4700d0de61f27716fd86bef48dbc2a9192867645f264cc075065d9ccefceb1f9dc76ea758eddb0b0c588f9e2bf72efdbf613ed13df575b34a2349ea4c97e4fb3cce49b6a0026baa7f18e04383682f28bb1965f661a6b747cfd200e7ee3d2a607ab150a79e6f7b02f1073494300f2838069f526e9e8fea454a3802721a41a079b880db0bea163c4ca9769a2b20c828cb71153c6bfae8e5903fad34188eb6c5cc07c3353baa9111ead7fbb2d7f1b6039f0f3f9a8640ed52999dba2e6ff94992a943b3b1c484ba2be3b0c65e00fd3252888459b7dd124b3e94407d4af959fc35228b4ec0dcd0bee7aac70704d1238354f7edb7e37347a189178d36062bad7456cae97e032df18a807b37358b81f772e9797bcbd69785fbceb1124c52ec0182018eff3689a863b7fae4e48e9b84142b6fa021db07fe57d06c3c385724c6d1374cfc2b5da9db62cb281f9519d8ec0c853ee40cc1e6c93f4993ca343373602ecebe3313ccd52ff7a5c6aee400783e948037ea7caa66fdb93c7acfc03d0506e7764113c6a4e3ed1bead03fa57184c52a8ec188fc4decb31fb6dec4a2efb324601fb2f2bd799b05aa0334a6d6c2efab974280c97c30210819385e40d8f99056143c09dd93db7f0d669592dc9f9b67df6c1f6c86eefc76381c3aa40a27b640c23aa0240380e898881d04a22283cdb43f24430ec51f970ba35809acdce45b55b9ac5bc83acd2164c2fae20d7739b0cb857ec1540249ad74a25158ad5c9bff95965522fa00af03fb991207b57df2caf8b60d99740b1944572ead002fa7d776af42c25344da41aeb296f7797856c44251ea05fa9d4b78966374a0442768784db7c6a3569c2d00154c7b052e2ee1345d480c7ed14554deb13234e1fc681f8a92c92d1194ad8d545eceb04fdc93b4213059941a0c615e7e9587fba0e5d9266f45331d586c8526d536d00b57a3241ca6ef783a105f3bc65933c836a61001376fab7edba6b3d5446380fb661dfaaf5f063ceaa77205301f333b562cb77dbbaa725ae840569bf06be96d2c00bf87d017ba89aae33120af1e8aa24363714a602eff1da6fdb2f3637b3d63cd03e6f9d04129bc32c8e4f795600f2db7342ff75b27c0cf305a20d2ce5463755275b1fe0ff565ae914bddce7bb5dbcbef3c24f2cd9b15dfb1e605f0e69867023208fddc6ba82f59f3bc345142a9f03f2ad654e5aa00e4b73976bc389cd8580c4dcc6ed0db0bea23c6117f64a2942cddcaf11d2786f6ea5c5d800e9206c82c522f1cb6e2c8598e86ec43b018c69834b002af13c262983463cef521f0925957999f3f3d292a54fdfb607d8653a9a0ceee2b5a70b3e637f16c229e3690ed8770f1be73e898ceed0778318eea317cfa5711db980bb738b424443472f2636210e9f0e8c2c6838d1c14901fe658e6c52a065e0c91d96c7fbec5285f7cee56db28023b91da57e88b183832f2a5c2670565f51e7308af481e359d40cf7902ed2d7035ba4dbcd70544d6fce8d0801ffa6048b354afaa94e4590c7855a129c2909790b0ae01f28d2e3c3a7bdab7ab37b9fbb755e59e1b69d882290527478cbb257421e2cab68ce82545dd75d13235d77fbdbf6881d3bfa9759a4e9e8823c287827958f28d6c29dbe6d7746ab887c650fb9cbe2194ae0b5de8f48aea1011c0b1545b285f0b8a6fbba6be63d25e7a9097daedb08e44ffaa37363e3c3e35b95a9b50c18fc054b82c31b00369339ecd3412905784119c497212974198d6a79261fbe6daf864189832827503384b38f60648b17ea47ef016ab0fd6b9825bc464dab44224b5c1d0943b560cfeb01fb1f9c95ec3755ed3a774abf1882e5cd338fdd8b04a0fe86b5c6093561bd605bbeef9a40277bfea053970938fadb95fa8c238763151fd8b5385431e7e2efb46b07a042bfbe8013715579edfe8832f54e1d0e0d9b607a1db07fdaaaf76ec74b25e0bcdd56358e823746bcb9520aec2f254dadc289fe6cbac6504ee3189ca88a518507002745ae3eac9b69dc93376c9d01187b6b156dbaf99340fc779861a53665d2a9adb549f6ce53233d163bdb0cb05ff5aef1badde924210c4fdceaeb2488aa3dedec5e00fe793697155a1a91ef57763213f6c9e397bb012480fc534744c7aa43f4dc92c02dc8342d5f412a195d0638d1a9268bb52021553849f22db5075f9cd95eb455ba02fb036ab835e787138ea60cd6cedee2d4a22ccf43209d8a7d2cbb625af8b504661cd11dd14484f2253c15c0e2f7294259cf0c4e155b8d695d581c718e9576880958ff2ee1dbe62127469ed2cda25cb6f25846cc3c8940b9dc5471ef66f8f1d2457e439dddab14bfc2a5ce4f807ccc157bca36abcc2d6c7cae22f11cbea057e0d802eca7c059ab5d260c6c4ea8b8fe48029c2a93487309c0aa5015216ff65072d54352c1102f5528915d98170ff0cf8e7b39dc5623d244e3574fd916e71426d0b1f081fcc1bbc3e3a6264b7792c42defd8dbe48dc96eb58f81fcd9ebfd77067e0e9a0efa9270f17b0b46c6dbcc0c80ff93c5e71369bf2cf4e6d57c6c93f3327ea6e25d6755c0ffbf7faf2bd31be65eb03e59c38e2cc6bdfa3270ff0b56cf2e111be9d1319922fd7403275fe4b6922c7047c224dea53e4a6e38d5281beac8e94b9d45ce3d2e607e322583173c1e4be5b089963262eda167e81fce02fe457cbdc476936419a29e5ca043f002777a99333510dfe6d4047d2da3aaab48b6a0a4cb6084ef90cd2705306566eca8aa6a2e89610bb5d1e085be387443f45600f3eba6707b88e968b85287844b7ad43beffee33676207e614ed7092a8e8b796931c30bbb6f256008d1bfe40ccc5fd581a06484920c44aa67f073cee33bb0263967407c0c6b5208cbaeadfdf1b82c75b8d2829cd070146095a0f2119fd5721bbfadf966689dd1b9069d33c20ab06f37a858de093da47cc6fdc456801973aad1d4971e60e9647c23d534d12c9c6678e70cdd6c9df88d65451880ea8c766824b1da64f79a54635b991b093784ceab14886f390ae07d2673b30b7b59976f6188d70d4840e4032c8735ca73ad3e05f7f8746e9933bd8941bee5228e08d49c29a52d3e183e4ae3d452e7bbe883aa3a985659035814dd595eb8615a0dd343056b3ccf5a3f891815d3c5feb65da6565f607097848e4c852f310a7ddb0319a30658bfa364b722be24280a5c525ac452c66117cdd908307f5c2024f0386cdcce7b46a3b8b67a188dd3ee2140827389706971ab8f6ba23bb293328f9ddd38548105b869da5845b1902703bf1fedfcb46bbd5733387cde0c607520ac0c21b70dff4cfaa9ab159d16a3f02d797136c0f2fae14b39b60519173fc619a37339c2f0dd164607207f33821ee3f27c3fac60e4f5605cebd91766540f78e056200383a885d306c7476534a7f867d1475d9b926280ff4e656401f16462994fbd909f3fe952be6aa9e81590bf4795c69ec9c27f1f42c2d7e02887511fe3d9e61e068ccf4d003d42425752d47b675ffc850e948b7e10e07fad3edca015cf891cb55c6c2cc01a550ad9f2370458964363829a871852027c99c5b4769002cb2cbe0403e70b6517a4868f3e740df7bfd332e95615dd7c50d403ecef95b348b24516c13d50bca855e1fcec0c340627c0fa4f073bfad398d673e8d12e3df01fc137acd85004e6df4076353cd08a1a7e45926ec73ee5edec4cbf3ca09fc359624b0ff58e87e07de6d0d329b82a6242328123e9a40fe574087b3e7448b2ed6ae6da9836c6a48440fe34beaedca292377d4b935dc034bff980f6703ece1bd8ffd28f5cf0b8448493dd76e3242aeb5d75947681f38ff58b97acdd2e8b99ca77fd523fd1798f8aa063032cc2569c5ad14c3b5a3b5389484ad5ebb5528784bc00be59fc214b4bbf768fabd1ed74f964b7ed22fce7af81f85dcf700aabd7a85efd54356f7cd69e65536c5c0ad8bf4ae65464b63e8b2ac8317f087272b92554d5310cc45fc4c13938a594260a125ecae65d62b877da3bda01165af504d7a5b88c6ad9251682854fb1a121c133b780bb109e5e6e5f84c82f0f1676e44f5b235ed03a437700f62d8b534fe26dba6c415245cd40ed733ca3ea067480b541857550b6bf31db3129ffbef1e4ae8b8dc33b58c07ef6f79f5b0c3ef359f125f290647fb7ef3b6fc1075c49854fbe1c243482b0feec1898d5b87a82825b5106ac2f8a278d875aa23dea1ec698d549cee79832bb5060fe474b04accb4af46013c956c53f914044a3068502f7a130977a1770def9e09edbcd4dc4aa227e92ac88e604fc5f292b25d206c1fd793c95aa4d24cd3474b80fc071693b397bc04e71c1fdb5d1d0b68ab898ea67f32f00d694eaea377e2d1c3b19106e957dc4fcccb5b8162a008bee2bfc8f344fc69983a19b1dd06e22dca0f9421407f837de6782012e105164b704823367910de38902bf00ac4808750f22ecf1b337287299ca94a20d1488bbad80f85ac4536294bede3ffb8b70cab074ae6e9f002906f07e3850e9c58a2ec3a6b15ab9b3b73b48bd982b4680e364779f90af065f7c1e3e4aa1c4a5420da4f097a907ec33213e51c3de310a6d851e5406d6c47cd3488a29109f752d4ee3f30fe23417b4969deb97bc2070e1e80758ccf1667d289a78345607a0dd16aad769cd9c494f01fbb54d8db028976e7070c24218715e1c18f104e713f07ece4995687bbaf0ed86303d0c877d9690b3276880ff8c7dbe2aa12ee277f6745c5b0189177baadbf32dd0ff721f0799b8f059cb4374d9ecf0151c43f2bc7120feaf1178d5f4c823af8ccc71fc86f696ddc3205118e0fc57fd328d0885bc1011ff34d3ea427986d2122214404248defb4cdeb1cdbe7ef628ffd3bc86ef8dcf51bdc0f94400eb939bb6c6872afc0b274cd0228c4e469301807c6925553c6e6bb12fa958649cbb4737307f504804e49f27e2395196d523711291789df7878925252fcd00562ba95c6a8fbd1a21e6b7bc1333b65ecba124151c807f862cbebddc100cabf6ac0ce153df4d47e4268c125d20ffc346ee3ee549fe29083f9338c26c07df1ba116b80ed29ecc2b09998014768c8c214c53f09288a32f04c80f521f6ebeada5d5f62f30d73a8e192a81ae7877089cff99bd1d10ec628d871436485ca87dde6a8ad73b06944330fb8ab73396f4b58ad1b8f709843b9aa293f701f9c11bdc035aafe20313c3d16aa18d9838f3a38c8680f85ed5219af89ece032ebfc6b041af19b192b74126c0f949fa334d288ec5b9429f196a0c07dd943656f67280e53765c94c780ef529759d8774b76c9433d6738a23c03f9b208795f610c462ccc47d4d544abd8ec888c5450ec41ffb5692178aea5ac74e2136b2c66c2658f94780ffcbd7c697156cbbe76640b0a78eb8e919ed8a08565d120774ea20bec50ea843bba59dfbe9b53ea322035032c437e80d71a99ceea73f7de2d68db37a4af85c01f00fef850e410aa17fecdcfc98fed1c33c682983f5190085b8df7187156515d35a4ccc2494992404c6710d05381fefc37fc4b9edbfa15877cf3593b9625233e1eb3b8035a7cf45d2913dcd5b0a03e2a97b188eaff369001fa0ffd4ae93c803efeebd2a8cb4eb48e87d1993ecb90dac7fed734b1f0a2bfc793837e573a974a326acda7dc03e4837f79e077ccdfe5cc2f419f160c826fb60cc0e28075a47c7910f901c8811d0e1cc2e3be8aee97b126dfc6d7b5cd216cfd323b9518e1143af1de787d336e5f74c00ff93a941769e695e542c7936abdd5731380bba10f0dfbe78f2bb88f1520971984674b13e2237b8b08b04e2d31bcaee1e52011fb03f603f91c61a95dd2277ec04ce777849ed0c6586e55e4fe77ec96d4229a8ddc4ad02f413efe12962b0799966d3ae802e131fb7721e320fa0ff85905b2c6f6ca19cf6aa5a896367ec2f2bdb1580fa072c34ffcd82ee3773228f20f31031d9eece665103f2f138ddce4a405a50a9f70239cc7f7609f5913a04e0bfae74bceb4a84afecb399a51b6196eb2ebb404506fc2fcc410a198287117286b0e70fdef8781c9c1eaf03feff247f54dac7d04392f918d6e302e63d914755cce0fe2f5b6bb0acd6b5d1649489984f907fa741541ec86f9d578a9377daf35ff0d938ed9caac515b44c4402f52fc13709c95f190d39af43669bacdb2c7e95cebf0be42f3dd99cb532f10791d3cc8e5b876f9f336cde9007e28327b1b26e0781354f956f10f9a8c9a521e0df1b06d697ed2d65a2f4c5212fc9ebb6c7d5dce776fceeb97d807f8216a187ef5f91dae547342663eca1848a6605f8cf15c66e7055b2eecb3db9a9db56ec4f21784d6c80fd5f4855a3d033800c9317cd2a361ede41b593c41b98ffd4b90b6a8ec571c2f315e12f0ab7d3e866223a81fccd40230c53c2fbfb04aea2de8e24e3748cc317d9407eabb7cd17fae1f272ce935314a991d49edeb9fb7701ffdc52f1968e4a1c154754f6e9a5bb112bd5eb9ba7807f5812c462db2f49368d9073e383f6529b47a50e0d507fc0bd374a01717f54cddf8d86492673f286f8cc2d207ff02cb2f0d1d7c4cb209154d286b18d84f7c569b919c0fa9d6b0b6244d9c6e989caae969f6d6e28dfe200edcbaddbefd9acba389ed8b6c563b0fa4890e91a00f9ffe2d735052481059ca39326a7700ad3d808c7879140fc10b743275b633440b91478961ef2e9a3f118221af07e612915b3fc1f2729c242e8abc80d395e4db001f11ff6a55b0787fe8c4e47dfa493f669a68f2324015033bc202c9936f2aa8e87feada6f9468269c7caec328073275112c75a89781117914ff9a8f8312f7e762803103f31ca2b7b41ea653a3d108f2be66aa6b878e75201e407d1726cc35d6deaf9f8eb66b45c38ccad6fe599022cfee3237374f9d44d2be673ceb2dbed2bd037fd8880fc194145f76896795b1eb116b1a9e0667ad87e092f707ed33d54333a8ac5df185963d6554532175fa8b308cc8f5fb4cacee3ca001423e5d20568f56926bc3b5040fd506ab546d5472cd964a4646c3359720efcea8410e0fc7d3d0aba1e0181d600c9ff400ce9692c92bb0b47ffb7ed1f6d947b78e31dcee8bcd03f5d10cee8af194e03f559240a990aeafb5a9d64fd086264c48a8235fb0343807c444d59c5f5dc41c11af5b05de378575c288c05ec9f3e30822a3ce9a8d6a9150341a4f73b0fc872a680fc430661c3d4e15d969b3767add36d3624e2c5e86481fa9f70e382d45942ef3de4838732b7123b28448cde00ac68dbb93768096a6117f7bbe745b11f24eb977b9b02ac9e7889f44b97e1b5dbd6ced550455648cbb4cbaf80f35d24d4381384f2bc249899e299576f560ed983de0177b988ee2051300eab7ef5e970410bd7c1ba4b81580ac4cfe5225b3111864f3fad98910b7ab946f470170d01fb6fa3679c4b8bddd70e3efc2567ed997add56733ec0ff152e7df2162304ff22149d4bde5ad0aec2432e0cb01fda95558fb5932e5f978edc187ab5ddb76c4bff11c86f1bc35e50b7914c850acae042b689a53acb65ef03f6dbbc9bcc0d5bcc8137efb92aba16b5b1ab69cd0dc03f93beb1689fb4a328f7ee598619be624b77c2a42503601f0fe53a93da25098959d5f4dd44f7d964984c006aa8e05bae9baaef8bbf585629d552e7ca8f7dd45e05ea07e042589433e28f43293cc8a1c28845dfe6fd7f845d555cd44d17a6bbbb1be94e692425454040ba5bba1ba43b04e92ee9eeee0ee96e24a4bbfc6edfb9faaef7b7cbf2df9933cf79e2cc203180df8608b4738dd6da89da1190f97c219eb30f0e7900ff9664cfcca13b63f4dbed825f2234d34e68136e10e06f53b424fe9ade3337102eacf395ec3dfcd18bd927407f6b941847b25b4f2523da3ee590553491a3fbd00cac8f621b3f0a6768884ffa5746fe7b35084eea3637c0feb8e3508a7ea874e1d75b19109e8ebf6b48693703f6df6c29bdc2760b379ae0a17cfcdc07e804076611e0fd1330905b9c5a48b582017a06994d360d76944dc0ff2fbfed5d5d326bbe529d3b10ee15917e15af2c0cd4e738025506f5182305ecae18945aee2eaaf2dd6aa0bf8986415cf95e66587878527268938078841bd50ef01f9f4ab0ef5dad2786d09a26f2244fcd387d956e00fc7f9ff85827ec21bc84eab54a88c522aea561b80becbffca371ff17474e1f694d27a4704da3dc3f9558c0ad7ce89bf46c5afc6b370a47183868eff47abf932f01fbbb1a3dd20057934628ad7dbd6c5e29d7cf5e8638e6bfaf1f7b37e185d9bffd420c890ad13bd2fe5d59550ade1adb59d5d372117f467505fbadaa628a1c83dd0cb8f5b37adfc7236fe4dfb724b4631641d6f1bb1f3e10c0fe1b166949697652ebbd39d5b0ff2d263988962bd2f1dfd74fd55110cb56e8f161573d593ee8134bca134a02fe5ffb1bc785db2cda0b2279cf852a4ddd26d58921c05fcabdf9f37250dab4e61f69e0fe2c4b2bb3964f18d0bf329718bcd39c8216fe16bea7b173aeea02f74d07d89f6f1ec215261da404ee3548486db717867b2ab664807f0dcdfac498dd117f3027c0a5436877339a72b913e017a6e6b5319917dbbd0294bfbbb16594428b9900f329e14a7e2bf91414d8b41978133c595522bf837303f0e17e1c2b4aadd791b49211ad3a1f7d4e00c3c922b0be6e15e627bdb9828851238b621ef3d8fa2ca30d01fdc94ac970443dcfcc1831d3b7f28ae0b76fd1062db03fc4b7393a9abaf885930dda09efaa4369e0c9e601fca36f636a28a32c97b5809f26f71ce5c17161d90de83bbbe6219177adc44bb39a2719cb41f61f5cb4d880e717f32e8db1f6983a12ce4c38d0bae559b152f601d0bfb2997a235a2be536887d9fc245faa814c7276901fd34a70debbb54b630f1a01c36017ce609debc341ce0af53e8fcb55f91de51a1ef23a090662d606d28050bf0c3073061ee1c1a5902dadc673397eb37b95b9822407dfc7fb1bdff1717f8f3d43cfcf70367cdbf512594091f31e3ae0e2dc07f50533ee611bc531b15ce828106fd3d3d0bcef606b89510e20d82c4f2a50542e69dac4a87f1a74149065fc07fe0be7029e23512a1f201fb28855edeb04e808d16d83f4e610502c4179581edddc4c8e54168d57e423940b4123bcef2962d5a0293c74e25d54d7f8c77d15a0438df2a092249048e157a2bc8753ad56367bbdf5e0d80fadc5ed3cd72d0d98340191d2d679e1c40dfbb6b0cd4479aeb5688970ce52b2ea743f63e420d440ffa72f092fd57d7bff3a3247d051845b8b839c7efefdf3ffd00f8f71605a8312329943df63beb0d9a0b2ac3525be00a2dcf8af968b8b30c24ad6b011745d9c259dba755409f7bf9c5e2cc206361b5e1db45e921bb6be9316701f853e82a954b1e0a499a2cd98df83d3a54c909f60200ff5aceb7c6207426a14018b6969d383d652e56e129e0f92c22f5bd34e917640a50eb8618e1bd0f58fbc50df8bb7f556194cacfa2ba6e9bb642288ae25e7ab26d01fcae6ec92f3be6a1373962ea8de16e5f248c490573c07fced8e6187fb02cde7b3c5bfdc0f76d793dfb1f1df07cd8c6b4ac60659cb8d886e888119caf3248b9ea819176c71abdbf6b8dbfa9bd310d9fd44eb2c3a5bbff02f8894e7c45f8ddec7ed985fd0c86c43bb24c3e3505208e12252f24ad633c9563f5ef48fc7093bf232909310fe0b7ac6af8e38a3b5e542a14311dc2d468e0d0aa80fa9b533e6d273cddc41235bb3f6b3265154bd28807e82b2aadb6f01463064a5406aae2c3dcca04e7241f01fd3e56c5d9013db4a9fb52a5ca168a613cad176d00d0474fb4f74a04aacfbbc8a2fd5f7ec688d0ba6beb00dfafe333e61e1a340903db120bea569db661875a141099e26db741fd4a9c4b5deefedee3df1833de4cd62ee0bf9dcb90b9d82d44634116d01b2076d8ae3035f602fc252647131e27eb974a932c3e95c68fce228b8b7e407f8237debb6860e303fd608b37f7ee1d1adeabc323d07fb2f62b717f11c26aaec8c4b62543a94d143eaa04ce676ed75ccc887c413a977a0b4eb90908839a800160fd4ef9c585f0700f732bc6b94a9014fdd00ab463006ee5e67ce05edd1f6acef080d77326d8fa13d06fed08f8fb15cf617a5ef159ec5b8e1715a83894ac0f680681fe3f7539f42bcec4f0696501f684fd17a7c5bc9627a07f5d38ff3acc821f6d253965fb98c27396d6549f08e087a1e5dbd91e1d445c17b2ac9b526ecb0085b122e0368c74b5e0b69aedadfcab19648de92fce5d1c6ce88d003e2f7a83c3beef297e6cfc8b57d21129cf2d2607ac4f62478220552ea28967f2977d5156ead599084c206ec45ace8e6fa11f8862f7454e868510492a67f104e87fe7f350356f020e4d4df26c6dbfb0f60961b1a201fe794848b63fb47bc1d69150dee89d10136f334a2200bfdb6a685016d4805d9c9f4da75af84e3743db631078bda15dbf78d1e48375eb0c1bced3a7e5a48c334720922d35373bb532bc87245d742edd60c7d420697e08dc2a7d7ca20bdb41c34ed3053719d26437add79e1d0ff8834a6d37741c08cec34475188d0ee56d6b60f76280f38db5e27a68f7aaf27cf674bc7e6f688340b15e13a80f2609ae8edcd9a590dfd0ad3db739c721d4d1c901ffd9e5cf0744e2ad4054eec83258527dcb8b4cd541201fd53f26f634b8b5ce1462e73ff6b027a9f877ec17703eecd5a9d4bc6bfca718bf0ae3293edb1fa49a0e073c1fb99fcb07b7de533b694586f2db0c56cf21f0ef80fee1cd36ad892914b3559461e2ed78c02cf65d1839c07f159b474efa357efd229e78f5ae00b76dc13b960fa8dffc750dca83199cbee81f0f58591c3ea3e15082fdddc9c187e97d53a2ab136b13c7311ca3a33a4a61c01fbe3ebbf6038ee37dce3981a60342202f3c7a033d30c3f2e7ae4464ded3876bf53addfa323839a8711659801fb4b9c5f7db22e228d3a14fd41b69c2731e53db02faab76da375724b2aaf73d2790a31d1c5f1d573c3b80fa9078883a5d74a8bcb6eb509f8cc9cd68de2d7a0ae85787d456b80af2852c35323a5f3884fb9b92771801fdf25f8bd4997d6fbfaab37aa0c2ef2edbf59ba534204e59369ec7bf4df6fef86dfbd61016b9f83e8f840fd0ffa87c33c30af2a177834dcd13ded4cf19b55cfb80bf5f9515537b2f1405e14b67e9ecc7b7d13ebd6f038c72be72538f90e5e99827c147e57cd11487d9e21405f0a94b2c263a69de6aa77dc8888e4ce3d4382cd33e807ffa6fc80cdd6f2a73adecc8c6e69fa1734de1f5007eefa74085e203d2d29079a46f552fe5b50885d531e01f86535b10613fdbb883bf09b78ae0ef148f963307f617e4636d03fc2aed5d484c15d94852c08546c326a04fbd27589275d97f31b475f6ca6e41aab7235650f5fcefeb883383c9dfd8afb8e036f4cf23eb6fb2ae5bf101fc5efa422a1f5938b2afed1ed330f16e444718550de067fedf15a0edc39cc47c04c7a5d051ffde107aabb13456ffa600f8a6f2b3c64921deb37ff49a27047b174606ce47c0bff6213097069b18dfaaa0d5e0ca993ea539652218e83f7ef5852fea70b48f19ecf7042fe093df2067bf02fed048b21e8ca1d26c98c38f428d2219310e658dea007f4d46a3a9f392191ce27d96a9a74bf3bd64b8bf1ea8bf97b630b9a94b01550e1f108251fd0bc597ac0380feea5e29632d96f1c2b1aeb000d3e7f5e122caa11df87d3dacce2f886d2932ae60da6f7c89cf0c6b188300ff22379df87590fad3c04f15d12439da1d0daa5d7c60fd5cc0f53ff29946f0f5faac8c4f7aacb393f65101fc9c76045e5774cccb3d7d0b8f02de4adb0af43d16d0ffb05c9750f96c3dabe2a988d3c4bee5608737a101f57b53dfe4db49d6dde293d7892a055114aafdfa01708de06835e330dcab8730d1c7e0faa2848a019ee95f003fa3ef06a3d868db81db25b9abc3cbb1ba55586b079cafdbc443cbe888c8537e34d4569992671b26b40680ff34a219ce189feacfe8f7981ea7f180a10f473fd5007e69adf9ea21f677a3d7663894f05250ca536bd51f20df0275ab39f31aa03f9935ad2f5e32a72c28183508f01f06e4a64c36bfef74f7b6c28f1ee70fbe6f245e00fe01de951e182d337c034ecebcd1602631f15b1213c0ffe1feed0bb19f3cff26a6859bf376108cb425aeab30d05f1fc78b5b23b9303d16d4c3ff93a6b0351f9f03fc2b96e29ada06f6b3a27428b3ee225b9c8e49d198c0fd0c221e269d7f8e8c2bd6f87f1649bf84feb0790d00f8a17894eade5f26b32d2a73dd2fdb6f0e88d56e0800be96e091688498dff542fac97c4f31552003818695f9dfd74b5049374ed3be294214af8e1c363a0e63c18503f54f2fa3d22db82c96f229d2b32cc6fdf68530ad17e8af3596bc1c7bb38cfbe5b761501ff73189ba5c31417f91e205e9921eb6f18b84ab3adc5e18b2d2c3a9cb7f5faf6d6061b2d8e217eaadaae7f78280b622eaa502fa47f86891213a0969d719f70865fa66a7a6aa2a6ea03f5e8b45673c873c11c06cba1744f064b362dd8503ea23cb3665fa6bf2fa361c9b503fe96798c9118b7ba07f13fa33832ec5ca04b952ccace4cb294041827f04f01b2b16a3742cca81593fabc4f55532769b62607800fce6f926d17674dc2333927cc3adcde87a0b3bcb02e88786a2f03faf14c29160d22664384a6661dd9ef601fcbffec9274edcf5a5559f2ebd3dddce2f721bff0bf07ceaef23beaed1b0996feebe2739565f0b631aad00fc450e4bbd9592f219a4a59613afa33c48d3bd043700be804be8a81726375c4f51a8498a21a6987919b301be1fb646641f818d61e6ce21932b9bf1309deec203c0ef8eb2a967fb13f24333c4f1a4230c9cc49b0c6a03fafd090c9c86a60676f2c9bb470a0416859fb87f6a817c970977238a4183a9a61cc79065bfaf984d70183690bf0af4e8623647b697488991f3e6fdb7fce6452909e0d3355189c428396215f507ccf6a6eba9dcd0570dc07f54ef64b9d2e392c8d99ed98ac16bfb1bf313da1b23904f2a80f5e05d6a5789b0db52953a10bd3098bdcc02f079c60bfd7a04dfe9f4779596dfe7505c9fa47300ff2ac557a80f623adec2b00abdde292e0b2c8ded7c80be827a41984380143b9870faa36ab991e939896e1ee0e7e75f8fa6e5a8b58caa633bec22f44784fe456001f9c48aacdf76986b937c4d2a37cfcf69acdbd8d1d1407f7329d09944c6c0ae04ff8d9ab49ab0cd7e226b06c827417fafdc39b199ea2e9f76e566102fe570d16606fa67c47885ce46d52477cc1129636b3bb1aa11527ca0bed9ba8b4ca4cfa9544d084f5585a9cb63e47c64017e3fbbcfe1672581bca5910bf245d14a6c4ed3da1bc02c605dfff6c4f9273b5e4d5bb85bac33b3f457ed48205fb0f56f51b882555463ba67e89f53f10341b9cf1aa0bf2b34e16662bd3a7d8fb24f17e2455144208b1107fe7e8ab9aeaee82b9518b17bab0212f753c52c740280ff3f527fe62f39ef8135eba4c88b2a53b118684905f6f7217b4d777706f77428871f2e09c4074f3c3253009f7e40c6fa2098f4c46bf84c5ee84a2c216f46c009e8fb5e0b9f71fcc997ccc977aef62428ad340c63ba01fe85cdfabb707a8cf10aa76d95d77b5e8d46792e0260fdd6f52b2bfc646d538991c2ddd66d9e1db91aee06fcb327aab7bd753c468d263f44dde1582dcf1c30fa01fef11d0b8d8ba952c23a69b5ff76337297c1b8e58210985fa1b2cc232b93acbb64265ef9daf67578ca1ff007da45b5fb4eac0649c5cb423b91b53a44a597fd02ea1b9a38ed3b7f28a4f4f0725fe85d6fa22fafd6c9c0f5f88eb9f9a2b63e65d2ece34a058d3d0a6e0f708400be6bfa510ecd017d9f4f29962bff122a4a66da2c0fe093eb192455251a86938db559bcd6ca540c85ad3f80bfd6a3fe550c337780858aab56e6532e73e159ef14e04fe8e8f111fcde9ea53815f884bde9d37cce4e250be4cb7be0a0295ecc4d9faf32edaf2d77d72db773dc81fd5d6e0ed3e310b18244cd71dd5b1468c98536152e02e6b75e4df59c041e6a042ffdf4c2f3e60f63bd00fe33d5939e9f553c9da5d82677ce8569f8a67d731ac02fe42cd4bc065ce1953d8bdeb1cc15e4ebb2a79f017d1f91925f8723d52f98881e239a8f8bd29af83c0bd89fd7fb91be8a93c359d0761f9ea9d01acdaea1b100ff66f8a7be1e1649c9b4f7e9dad148aed8f7fdb926c0f9bc7dc05f373d793325f6ec9e508fc42a40f3cc04e497fec0ca106bedac47e1c5a1152a47cfa6f610b302f8ade06829fe2df19fa3189babdd038f31e1736c2019989f61e8bbe0cbbf2e6870f5be68349de1d38b0546469957189f8b5779513696348ba26478ec8bd6991f00fa0f4e7a3fa3f5a140d228978343754c63ac0507c0ef366a930f9285d73c84d1f0c178205ceb5bdee303f867c0d53b05d3c5bab2037fdc04e754c336a73e0fe0377ecc72affffec5fc08a7d2d56bcb85d74428bc0cf4e7bdd6e6d144506c9a34185d0ca26a71249d61b7fa80fe58b821f1e7f08728514557146427597300fa26e06f9420a3f9b32dd325b59532156b5022c424a9300cccbfb0807cfa79605957351c23764a22bbdad0ad9d0de05fc70fac932cf7f8e5f22d8881553f3b932e441a81fee1cae628292c98e9eb24cdbd3a4a421214750f386df9a0a146147d4e67e619bf8e3dbc94e09deae62a70fe0498f2279cdad5cb5ee0c5ec7fac311a15480a00ead78de4cba248f18b736cdb2f773f760bcd4fa662407fcfce208331c772d1f3d4e2e3c4b95dae9cc33a03ac3f56dcc1cb9ac51566e3a8bd7f09f1168d3049ba40ffe2c943f5c9bb8e49ec9b6c6a5ea760fd47ce2e47801f46cf818fffa34e5dd359153d715cd2f803a3a015387f38194e361323627ddef9cf94311e363be362c802fce7fa63ff2652d15e9a5eacd1745b0c82bf8e8e2888cfd216e825206ab487a1fa9eef563bee276bfd007e7174049e8da8129b2d3c887581b2c8ee517026b802e80f85be43e6be76c7ed5558727865476f3cad6e03fc23c68aa0c3550b7d03561fc9a266923c67541315d0df453eb43ea7d9374a68716f5107f6dd4dbb6b1703ebdbce8bd729576b0649fcac7bc990eb19899f7614f00f2fad6dbc8fddfb6d992f455e252d0df9eb373ea31250ff629e063ed65aecb71757ca884f7334e86e055d01fd0b5d4cbd56c146afaf907f273e9a7000b70829e08fa6eb336a479260135cbf9d1b3134427b339f58fa05e857cc9b2f9547748ef4e9a4d72b3d6d8652ebf9807f60af9a2d2efa6a96985314ef85d10ffa107d140de8af15853d7c5af7935d11286c4fe966226548479500ff9acbc6292cb4abbe27ad65d5c7f389dce2b1cf4a80ff87f25fb0fab49a441e16fc55c347b68c7a7bc66f003f976094fc9c2c5fde294bae3ebfee7df3e5f3b623e0af72e2a23be22ff9e6fa9bc4bc9d70b2d564d2e83b70be5e648b1f0e6d962edbdcf8628747541beaaa0702f5f7dda8efa4f64d895d484d3a7a7d18d3fb501b62c0bfe32ad543cc29c12c2b46a39ddcf8d18cc22b831cf0671d54fb2d5fe32f862128df7273344b6ced6e3b02fdcf262185eb3f957b3dd3ed8ccda46bccb0354d72409f8a1b50aaf8fb2e8e536a399efc35fd38cf2bb717c827d55ca2a7ce7fd9cac7b4a68009a791290a64e900f62ff73d36dd7b659b23e3cbbfd0ee9ed9896eed57c0ef7b199d1f4a42c6908de152e9f02a73e738c0550ef8638de1057f3314d264270c991fce8ac9a23392c45d00fb5b9cd6ed766e564c87c396dc44242310b15a1d986f23b83d94b500f759e92ae9e3321c3733796c7f2bf0f913eb0f1fa7d746395a17d584b31f7edbaa99f500f34da829ca78c7504823ca47bc3b822fc3223b6f2500fd88c9eec7cf4205cfed7896400cc68eae7c5dce61d05f5c87e191dfd8f28a3db97141320113104c6c06e48f0c98b61265f4e9646ad3d909f260a3f54278bb00ff912a54a572dbaae6e47438ed28c6577a7f94563320dff5c3fa27d58e09c2d2bde145e509d7cdf968f224a0af9893e5ec7672b0ce50071a2acf9e8fbb1f44b001f527b8ac924e3b465a5ccda7b329f8bdbc5bcfe4163011785634929ed7db7f63f9186358e357574ddfeb07603e4eccd704f55248b1300abbe7fc41a971dc5fee41c0fd87a7d212aabaecc1c353285735f0da6ed44f82b5407f94a6d47473296a0149311a8024f2820ddbf99e13a87ffb2ff43e06ac4b50fd18e2e22b73195444e9ab40fee1e7bf06b1b275620ca6abf11bc8f71d88c94cf151803fca15ee0249d7a0a09594292f41f53321cc3e3590bfb3274d90c2f91aea57816038cb6247e5fa75550fe05f58125caa3349cecca9d9ef99d1cff691f03f120213e4642e0e0b08133f359f73737f2fcf67b34d439202f097722d4e5c785641fcdd5276f83bf25aa6e5966560fe9558062581b2f8a5f0cdf5c09320ecdb9d6ff33e703e610ab574269d295dae9ddd599c0f3bf951efa203f379927d0fddb87726826bee0bf7c672dae7e31e7581f53bb014c7ac99173c7dd93d6fa9395fea29501201e447774737b26ca9b2df33c4729a9c899666a3362acd01fc184d87da0c0a64c08e83bb0861d6dacd87d01e00df47d823c5fd3177f027174f91c9fabecf3bdf6709bcbfa7c287b63aca49934436b774e1c548070e3709f0af8e1dd3225fffa0518e8a2befe2864f48d991d906ced72f761959755cdab750cc98f2a9cbd5e5f94482807e55f077c47585b0d26cebcc5ab31a8e5fe7d0e318f0b7f4a584224049f446fae64a57ab4f29c17145feb101eab72bf9fc74d1e9824835cda9c3c33a9dad9a14803fbb6464686e623d9b0f543cebf6e7e2166f7ce081e747d79d13d008b1c865474ade82ef1e9ae96f4909cc3781ade37113d2ec39a846dea8e21d8084596ac303f4938509cb2d67369fb96c2534f6b62a4cdcf7d244df81e7db397d123cc25694c6473e31e35b1ccac25f0ae03bf9b650a42f6c9fe26386de918cdf289d8dd3fd03f8d36414425d59b9aade87d41a07974a8e5641880a001f0f2443bc4c26e51cbeb092f935d33c8a66477a03f8d3599a4abe3cdfa0633e56a5db01e1c808ad3e179c0fc56227162f74638de0cdfd8d1a2973e285b307e88f79b8e632146c1dd8992c658bb72445f41dcee1007ce466e2050bc9a308bf81a1603f2265d574302a039cdfe8dee56bb8a32e39320f6607033b9f0ee57e9e03f86ef25ce8387697fb7b92cfd08725b23af8962c6b80bfe0f79636659c2de59bdf63838259e2bccd3cba05deff84666f9995897c2216e8d0f0ce1b2b4ce710c4ff1bbc4dd5b0282b6a92147f2ceebce2c9d40b3301ff64a86ffb12318fb88c39defafa57161ac4643722a0bea6265b75c1b420d0cd8b14269a2f8f1041dc4e00f90509be31a3a57035c9f5e7945e8441f59c63e224401f121399c9fc6e5a5e32bbb081e4051577ca5a6b00d4a7de35ac81758ac869c6a8bb0e0abc08e44cc903c03f5cb2ed5de6c9b636629dcc7cb1e2bec26d329d0ecc7fe0824b92c9aa64b8615ebd6642e7b5c4f16a1ddd06eabfcddc9afbd7fbb769c75fc96e940a79082581403eaa2e46fc66c870756939c0f89218a94cfc4d8b0cf8ff493b7afffa050e297292cfa49a8cde6ec97b0502f9949bf7f090f5c9701a4f482eb5c2b94c956d7559007f9abac385560f1155478f0ba9c263cebae7900c0df8f3e43e4d198aed8a2eb568e745468efe2e99597305e63fe41f6eac28503e9120157877df405926d02a5200fd35a742a1f8c87e7961a1712e962a3dc71c8d333a80ffe683570c3c57d5c3fb62b4a2d81e331d2eb5ce81fd9d0ba36fdb8d2b76534c5cdbb7a129c6c51ba105e41b3c66823f192458d6ad2f15e2a0f9b11484991d01f395a4ef07ec2644e7ccbf10482bc7d5526d8a607802fe6b873132b31531dbc4c05c76c98685699608e736a07f4be68c9627fb6cb4a9a4c664dd62d1d35120c80be0877511f9e20081eb03d204860b52c4a5b7a0981580bf58f0fd2b57566f40d1dfac40f1456803c9f46504e0b791306c78566f55e7af68838dd6b684d084f860017d74476d813f9c881d236287d464ba6abb1912131dd81f73f9b0ae548221fd66dfb6886fe1f78ba47cc1f995392434bfdf2ff2cfba6c5151b5a6883974f17b00fe341d7115be0174fe653a2edd55f1e55fbec217e8803e87140331cc7c01cf2cd46ded4a2563dfc9735d03cc7f53ce3f5184c66478c9a3aeefca38f096a1538202f8dd82d370a1aca76c247fbc979fcb963bd3120faf407f6e5e132220d220dc653f6d43368bf651456ed00cc8ffa859bb314f567dea906d90b36ba6d3225011a301fcb5118f4ae25bd3c7a20b49101fa328a7a57f247f02f4b30fe3c56a7a41462d512aaf685e770d0791a81c407e6ee08feb05f33f0e4ae8b55a48d997bf1f9e5a0580feab1d8df2137d307c7063f054fa8f72830f4b0ba0bfc51e8ef736be5c330b9eeee3bc08fae8c1b7897600bfaf8c31d7237a320c615cb51197221350ee95f900f8160d8aeb363c38b45fa18473f3dcda021aed510ad0efa607adf305dd38f9f3513c8ac645a2a90b7f81f3739e77382bf24ec959adde6d5a91a9c9c3a4ff9b01ce8fc8ac05383593226b14844cc2e9c2abfb7f5004007ed4f4e54e37a89208117024a2a14eaefe803c3e01cc772292d160ea77a8151b7efd5337e5fccf0d123510f0379c8f6f0f65b575940ed7c80870df56df98e14403f565185e53d65aabf966a53894e151ee289b93a11ee85f8e0329cdf79d6754b86e8f33972c1203fa7595007fe2f8b4838203aebf03f372c95fe6db299a3af20960ffd698e3663f10f7ab0a4a08f63f787838f6a7a301cfe7b7e262c901c968cb3237a44d1bb4248cc3e62bc0cf6d2ca27c2ae174fa060bd1f8318e2473c36bee0ca84fc4b0cb44bd2e0fde81d81434c2b928cb92f837807e8e33f7781db3d26201b5c4e9ef027dea67af2b04f0335b7866caf0f59ca2074d83c4ebbf2a3af33d9881fa9ecabf8749cb2477276f86e4c379aafa20b02106f85ffffdbb5e9537e5663dde5faa88e8214d0dbd9f03d6c746c920a9c6648c126153d622742cf37773c82e201f77ab38b6c4ada292c284dd8e171a75a0d6f29c02e0ff24ff838e6cdc0b49c8af2a78a4bd55b596c42f407fd34831f21ecb15013f5d4b4cb29bc5dcad53a514f08fd9f6fbf72136c3d79529dff09234aeaebe697601eb234e2d9cc2beebd8c8aab4a659f9621342d65614d8dfda25aa3eb753eafc50a8553c99e9b6dab4e602003f5f751192a420ca0e2512892ea6a7eb223379e706bcbfa47e31988f9278f9a8bd4432c2c2808beede1cd87f061f7e978f0ccdbe76ebc38712f72bdd3e195300fb97696fe6fdebbd21cfb70cd22cdd57b6b82f15a2407d8c2eff24959b9ba5c27fc9843a35d11de3f02d1798aff065927f68730feee3613d0a5bf0c567ac62b452407fe291dcfd7a4cb9ac4f169d68999c88404496e20af8bb1420542fcedf0c7618ccbfba4686b3127e5a3906d66f0e4f0125c4f7b64e93ee5bf96dbb8194cd135f40df1311b6a233dd25b5fb55ae38fdd78fe9a25aab1cf04720480b5c8a3f1b2df9a9059af2bdbb9c6211de05ce8f9b07e9119a1435c7bbba3f37cacf84358864b0c07c9f2ee37826e477625b94e6a35e968fd24b058b38407d7149f7cd662b55be469b417677fa7dd7c991096c4f88686c19dad59f8f767689e2e2a401e3491c830d40fdbd243a4f7f083218fa833732ec92dd26b9475e09f857e0f014f4eb9472b8d68cb8896c9529d90acd89007fcdd9348c0d798de21715e49ab6a225587c917e1840dfc42b6924ea54fac1f2a19accbc7938e97c7e6b0a58bf83a7340e3f5806acada8dcb4275ff23fe5c09503fce490061afb7793a1a0089163328db0e965c8712b80bfe57019ad3794d1e8500ba38a31085ea134847c00f2530f7ad46f2e4aa398f275598cbf0eaf5abfc1a682f58b7fb73af2b401196f921bf55cdf41a74a880cc06f522d75bfc256b8bc848ae6c6942ff6749eb6a8007d6624d730824632eed235ee38a8d273819e60400ce0efc65f568af4ba47dddba3c83c710eb771cd708281fe80ad31072aee2be295e0a2d8f35d44dee7fe7a3cc05fc6f7976075eae1c9682e53f04575232b575b050501e42728119216d3665e6961c4f512921b8230ff02f5ebd963f1e6ac6982cb4c69b3bc56a4534625d00be0a7d36f06ab91f5dc56a5afaf7728beae90661c5c03f98a8a035fede8d55cc43e9e88041adacd44ca77d600bfe4349a57bcc2297bf42ebba0df8ec7a7f70a2e0bf08ff8f7c152ddd46bb7f472dda7bc94500e75b3ea03cf074a86dcbf7f8346d9636655294c23b091afea1ed0afdcd0661fe39abadfb1d8766ef6e7fb2fcbd60800fe928da2e7f0afe149ad16d4088d8584ed304d8684c0fec2dff9b59adc9de2cd0c9f118ece479461f9e8980dd47f3c69b6cb1b4167d9d33cda4f78ecfc43d3f8407fea9fa6dacc632444cf38e35724e0eda462629f01f857197f9ffa41e76573734bdac13aea310fd6d2e600eb83233734c167d99fac1202c6f3a214ad05b9e911cc5fe20be0c0458788bf2df51fd9fb62bdd9b9b000578ee7b0d6ebfe94d54137433e0e9a80fcdd3679a60f9c6f6f854ee7997af3f5f61cb88123191842fcdd9cc0dfaf8090809ceba07a49f8ad496a94c309c71fad01e8bfea021e26971389441198c4ef366cafbf75b77103f35972e55dea846414d5b68b9118ca26ab5ad851c981fec71edf311396392222599d146e50f4a0400b020b981ff6bd94f4154afc6aad2677a4b9bec14f9820db0df0d7ff2e2588a0aad67fee7bfb4db6b6d99d4a67b00c9c8fb8dd6cef8f0468787e18cc55d0934490e353f402fc68e9d090ed60abaed6e354c56d13dfe11192ee77c0bf66409ca61b7e277d7ce2a492fdfce7737d81310c909fb8f6baa2cb428fb9e19af9878f7c219e06a33b0a7cbfea8494e03529683cd39c30df5da3459af66264805f404d3c239f5afc4308391d6a2aa4dc8560f306c07308010638df4a8a4bcc2382bfe11dea70f6d119ea807e76df116f6236e284f6876d62dd7fc3bef7c71c3aa0ef5f411ed0d2dc84773f65f75954d916067ea7ce00ea2be51e35621ecbd93f2b627eabf27835c46b9c3b809fa33c657163d84a8a87ff00c17c441f73da9df008ac0f539a9f3d26d75921df9aefd87c4baf9296ad72017de5f79ef1d317b7078f14df6f59bab9fbcc3eea8b00ff6b1c51609cc4d01880044d791c5c271dd392120ecc3fadc73c75316b984ee22e3292f1294297e9266703fc3bc47bdf8555eaa80ff29c223fb833dbd19222ab01f5198185aa32618f21b0b2801cb3c1df397dc9ab0ee0c75e1fbc266a67dfa8eb3d480bff56b8b75111bb01f98f24f61eecac21889f1487b19f46e830d1c4c2a881fa13748470ccc27be09e0093a2eba08a31fd936015c8d7b5bd0dd3d62c9cefece452f6075910237b8c1d02f397df57eae7933245f979eafdd26f7c3a4a6efbda0c9cafe1847059bbc5107f6d892c2b356aeb3d19555180fcb299038b65c28b315edb7b56ba3c49b313cb005ee0f3537db81b2c8b99ef0d086a5eb6949460fc55d6817c669ec1e540cef66111b7d7c04c1f877c2eef8b21e04fea62fb132d95f6a32d43793367e850e1bacf5314787e32bac25ace21dfdfb6cda8a755d319ad895aa101feb0ed1d4a9809b1fbea1e8aecd4293a6a0683572c70255086ddc59d4cfd6f29da3891c816c43145c2d02b60be1e84d3faa60efb4334a1e84dceca1977a0f36227f0fd395388c48ff3e65b9ea60afa8b5a7ef72ff82102ebf380e3755916afa47f8043a269b2d2cf459a6914982f11ccfe429553835ef9c6743427a2431cb9ff4915f04f0a54a2a9328563e4093108c762cfe3333656ee01f8bcbfdf9c1e8a221452f6d3ae7825299ac56629b03d2136a2b094f00576d63fa420b48f5af27f2a738006f27d21edb984d66a6a3489b9372cab53e546ccac72003f7d04f942d8310277829885799a12d1e898fc960ff85febdaea03526130e7de682a9562b4dcc7bc028701fea36a3ca7daae119d722f718be6c405a7d682970cc8ffb31b9e4ec0ef99761ddf94db890a582e05eb5101fadc98eb4108033644e7f9cf6a2474a31d77d5554e20bf4ffe19f9a7298aab0cd386f5b6f6bde3403c1f1550dfce6f6aa9fd924a55ebef90f04db3958d5cad1f01fcbfca559a6d52fcf571037678eba0c07be96d6118e0075ea4451f5433ef5e1eb6e2147d07d5ba1cff0502fe3acf461be78d5285bec6697ef68f4ff4d2629fe180fef53bcc1f53a17f10d4758bcc2b4bba3247d228fbc0f7d3b94f43d6e72412efc00cf26d21ba6a9b7f8103f0b7cdfbe67a37b5e4eb786a4f94fa6c91390b0444c05f500b2da70ff1e845e839ad53f26f96e43d623637907fb5846441f1dc7e42805d93c09e416d56147cc104f6e754c2df20b6da7a497e5ab9889fee858f479961c0f92e631f12f835c642d870d196e6c73b51aaeaa05e20bfa3fb5158ffc0c427b772d49ff6238ec9df608df573e0fc3edbe0ae22e209e98611d0f4ff173b717e6000f0bf6c98f5381e565f395bd6edd469959a4283fd7981f95fb59597bdd0aeeb255e79e833d49dc582d2ce21c0efef2a6ac0dcfd790e2e7db7efde423011a9ab381ed0af79e8d114323d8e44749fc325b528e02ea18aa481f385504deccd0d5a273fc99693af019119d73ce08f0ce0bf40d9c8715c8be5b5102e3d4de06b2d19ea8d04fcfff2cf7317c93f6a76b3ec04db84f5d86acba2c0f9422d1f4e4307946095e69e8279b34947b111624c017c18f4d95ad0ade9152107930083f8f9a128b5b00be81f8987cc7313a00b49747d842ed382cff08de3bf00fe351e194e2d5e752204fb9a701dc95cac8c449f5660ff4d94c7dc1aa7326c5dbfa6a48de8b5a84db6c303bf6fe39c82296c1d143666e265295af2f393a30a381f26a60bad659a031267170a991fab13039d328805c02fe98ae8ab709e3be656f4f06c1571fd21283015c0fcc7fbb393c227bedabb9072d469f2038c6fef19d501fe35ff4144c3c22839add709bd3ffe89cc6db27f1ea89ff8b22cd56655bc9d515a422d27e8a1b1fb13d6403ea7cb60d8fd9531959750452c5751a5246a866f10987f15926d66ab6fcde8c2220a01fff7c04efd243d1898dff5f796acf9df5ba73e2f4cbce88d9117d5055a1ae07fbaee0a5faa2ea98636935be542dbe851ac38cf01f0fb9e5b49a779b52c8b223cf43fc1cfd295ad78fa80bef57548a500caddd3a48ef66f88d29fb68d295265c05fd078fc4e0fb577cb00d5b708c2766535534a0a9c8f1ccb2407c7aaeec966fd2be33573c766fd37aa19e0dfcafa2cf64989bfd3624ef99f31a53504a524f57be0fb6169e1697cb218c9ac3e948691463de224ccc701f0e1805116068378935f3e15c7a8572063bfde03f8fda3b833e764d09d999b0baafc28dd468b5427f480f9af8dff3e2fa6121cad0eae5592270dfb64183e0b00fe01d92e37b24f1f772fdba3c80d9e7b141dbe6dac03dfbf4d8e6ab9795c557c9ff977e78a5fb13ab3351bc01f38212beeb1e5bc2b6e7970fc15fc65d214a3a01ee0af30037070320e2a0e0de6ee6fce766ce51251e9f8fefb7a36c49bc1e80acd61b24b5e519df38e18940b2f80afd0f74c689b333c36bf416f3c79f527ef67ff6600fc9991f09628e58b0efacbad64fd81186330f6198d007f9f19be7881129a0bb5785e56ace88dc94c01fb02ccdff8a527266179c48f19ef7fc3921a06754c4dab07e47f2ef4dc8cac3c559ff0331c9fb88a73ad4e6d2a01ffc727d19f5f601bdbb778ae25848ab7a683d9904e81fef426a533f7f0445de69deae660bc63dd8efcc40cc08f9254b752ec6a7d4f4c95b9ce79914ee9f130a302f25717fa968bf25912b602492edc41a1af34d834bf80db828be7f9475a75c550119f0c77632f9636be9e0f03f817222a8a508bf6125d38842997502c5c51bed014e04746d4a01620b4675b053915bc12ddf2fe2a854f02f9da2ea2fe1fba78c830516f66eed82850284761d3c0fe143c666033dafdb82decf5970c0a665ef047bc09803f1c16a877c363443ea41ef428dd8bde4860224400f8f8a7b3f8b4746ec25f0d8adf8b9f8b6d8eb8a63e02fc34ed130ce79ca76b18d29194f0e247b43cffbfd7007fb9bc36acdf9b2f1d5890814bfb4344b8ead4a317d0df741f14aede992bafa26a4d2b0ce33d9b53640a01f3d7be9d3c438d4f8e96d616b238b551c01370d102f206c4b567c2845ebd940eb65f4572b06d6e7d014c01e01f6bb70ed484b060f87b8fc27edbfb3c624ab9b601f087cec9e97b744f86539ddd2143d521c3bc659f620000f8c0e1e92ef02c8cc63511becddbc2572982aa0bd44768198d2b02ea405c03e66073a7f723d0269d58807f7c585d9b446e28b603bdbe6a9417dffd7a68001f981f290df76d96aaa213f9e0c71bc70ddb358937fa2e307fc2ebd1315fad3b17229dd611c5c4621fb94e161fd89f93aa3d142b8cd001363c66ddec04bd1910e5ae00be16c38356c428e09611366872aeb6dd11fdbea206ccbf13d8fd3b2564bb304092893f542933c0148f4b0de00b5a9932b4807efdae035a51c4c9d4dd73cd1a5b201f8a0b5dbec55d62f096b6711e9f4a347d74be4509f48737d9b1fd35d0e31d616d1bce7a6c7e9d2fe65240fd6674fe988492e2713a016f3428af366af36d290ac88fb80b08c3a4bb403f97dc4ec661405d7e37b22703f83345da8892cfd112a36ff70ddbd9f374af17c849007e8a31efa38a3a86110b26eddb369b31ef2c7dc301fce1a22b6401d9bfef91952ef203316b6e82fb91bf00fca319b71316fea35639070bab4b997125b1648e0cc03fea8b20a4fcb5e45578fad5705c946cb660717a0df813545c0448ee5eb52a9ec96b59826aacd353c9e2007ffe737269ee6b090d63e4e3bb11e7caa1db4b5d75c09f3d5885cc94cac04b768fed7e4c222f4e8ede140ef8fb50dbb75011e2849a3f2a7dda16f993943154a80be447fab4a4df7052c66898559613a7738d6ce8ae2c007f0696d84747cb7be4c6d3b3bc8f1cb54208cfebb5c0fe524539680fc48a88fa60e8925d327b1647b4c10be06fa442e18fe54fce7f9cc9b7b1ba2049ae9ab99080f98d1b98adbb904e180ab4509432d89f3e107f19f9073cff90849e6a8c3ac8ad6161b3c7bd79bafbac0e36e07cc26f86e581f8368631d050c0b53bf95eb945a807c85f2ccf3f2d63b1f9be7c29c19022ed08197b07e7049c4f5fe023d2f36c3eb893ec8fe93a9d10f9c7f9f403eb1b6eaf3661bc53376fe7628d4fd6fc63f06ebc2f507f9e22bd19da974f4af02a7cbd39a1b5c40daf8480f387766cb0f85bd6117fa7fd3fe74b4ce4666e3f63e0fe36b3e76274b15939ed8499accf736f284e2f98f980be8b277c53bc2c370e7b856b3dc181f3c823186a0be853da8cb1a56a9a9734718eb98e6352a36293d39c40ff904ae931b7336d25b3418cbd88125bd0fc0d1a07d83f541cafe7a9bc41133bb8a2a979fe93f5ecf2dd407f439022e7711cc5326300ab49119b6bbd2af08202cc47f983188ef3b62aba77600ab72db2b2ab7de5950d3c7f6be685ab7308b43eb1d77f34b43f870404fec900fce56281fb65ce173bd350df3ca5b4f7a43ca782f280ffa81d3ab19196ac806c7ff12f057f9c84d6585c07a03fe576bce1956a7360fb1157952dba4d17a14e8c01fc86fc8e869fda246d4bdb901defe7f5e352c8f16b80ff6db3b92f61312a1f0d1c69f22491ebb41e5aea03f4a956921fd48c1eee9f7d3e8e52d225cea3725ead02fe8941a4ea9c4cf2bd52f247cf790ef26ba2e25624009fc7ad79a26cf9e0a7378e6fc1c2c765d7352c4c00f8a0d09df81be4d091f3fb264352b866fd16bbb4f7807ed9b4a3a5f89d80acdb64fc939f002b126e24f11da03f8933e042b7e6c9598b3da775b2934f0b1709f601e74fda9a0cc6e61f4ae8a04b5b499124b55f01f63f00fe3ae4efc1b3add1cc9677de8f0995a65a36b6ea04e0f95f563a4c645a8fb3c6e0f01c2ddb5ddd90b46b01f84462b391b7980383e9d2e68e99541d2be82cff00b83f052d03819afb5a4f2c334148d06a6ae11baedc2fa0ff910d5e6780777f6cd35d2a7886386dafb7a7a104e69b7ccde02b46f956e5be7af7801fefbb7e5a670203acaf146d7cb832846cf7c0593519b712b2dc48660e003f63f2aadc0cd88ca62c2a63e1b7c698c0bc170b047e3fb3360bc6dd537d999245c1610e87828635eb71e0fcdb1e6a4f4e49d81856ff3ccfc3cb665a4bb9e50df00fd25312d26287fce73a81590eb3539fa89df5c17c58679d5cacc17afd18e3102e7c3e92be1165a210c07f6a209ea97ec2b21a92fc6e9d6ac1e2d7f346ab4a0cacef7a3bd78b6fa3cb52c862f61fbd96df1a90c201ffff285c026dd35aacf02744778cb0ce513c3a0f51409f428c610b56ee765b8cf81a793cf7674fa425c30fc87f0cf408ef41dc61e3954b69401105b14222718900e7b763c5fade456d6fb50a04ad4e23d9bdff5ee807e0f3173d5a357fce65a5531a8584b8dcc09d2e227c05fcd72db233c99d9069880ad6e813f1ccd9d7417db700ff103d10fdbe68752172b276c340c0691ed704da1c38ff66761afc6602232e827ea8a076dc3522a0e73302eb9bb99baa56f3d432de9587cb0a219440ea0a4203988ff29d259b65e94b52b987a14029ff2bbece8ac510e03fc3776d7b319cdf7b231fb646c9ddcd3fae51c605f8ff206255921ac5d36ab8b0d27072f9f3661f7a59003ff8a726053b5cfdc6417ff557d1d1a80fd7a71e02f8b529df083737bbc050fe24775fdb1c3af7f6c638409f55e0aa74a4fed2e273adf11d1f4ae3f3ee99283f301f7130fdd57f3d1201f397c3d1fb3faae7842eee1680be9ac73997a90251d0f4298ff83637f8c5579fd405f0bfed361684b43948d0d82135b0119bfc4ae3270901ea77debc81774674d90fc793b4ccd220f3ea6a15f07e4c2a8577eb736e34e9f641cfdabc3b0fa4ebff54817c97f827b9b640b94bf55cca9688e61a7501ae5633c01f566cb57f67dc73d5ec8f0dcb7f5d113ad8967903f89f939e6196347c918c05a2e0de7135dc8fda9dc902fed96abc05e35846d9dc9849cb655c4f18af575418c0df5cd7ac19429ff0bef5629bdbaa486656499c821258dfd504561f350db5e0b1a5f2cacb10171160903901ff7617191cfb5bdcc7d20caa6a36bb8e123c1ed26600ffc71d1a307510ce5ac240d1a31a45fb50fdf1f300f04923795bf6dcbe1a0f4faed892c0205fec83f306901ff6158c2b732818973caa3ca286f5aebb3dbb7200f845d28fdf7d02ee3ff38eb89f84ee49fe64c4432d00f851bd55b905852d1843d5dda22165fc35e5419813c03fcfec79c71916bf10827ae454abb4b37c66e0eb0be09b5a45aae45c4ce38d47b1bbab4c1e4a9456d94bc0bfdbdab743c3bc8868c4996d2c88ff23946e595b18e0bff4865b2885a20ee7e2619ad9d33c134fae235600fd98551c57211263368512eef1435044b5c2d92751a03f15f42d08f4e861eb414ae87648bebad5792d3206f8c5d81af51a347167782492ba5a93a3f7092293e1c0f7bf458d265ca5dd181825506fe4346f622934ee00ee1f3391be4c29e7d830956b970b0dbcfc82c8fab102d0bfcb60d383ce51bb1ca3646511a286af064557d080d7139b36beb7c657f9aeba35a3e77fae534eb15605fcede88f862cb02ed76d15dafb02379398055e6c0bc0fe8d9f7ca7e2cd3927f8ca6542a94ca77f85e3390ce44363cb4e787f6c4f53151668e5b9349d114d9d4802fa728683c4d7afafa297e518b891be4347eeaa3cc9407fae3e32e12b6c4636f1597357438e0f9719266b00c83f8fff6c4cef21d1c1b1604b60c0f0deb8639fb502f6371c9cef05130b7e529d680359c37eec78590711c0bfd654e36d3573b935e1f0ff88b44eb5623fc9836705fab3f146334fa51342acb9eb6e585e4fef1ff3ef80fee6e4b0ebf8742edaf7f5e806620ee50932f1e41538ffdeea78e98f7e8c99f761dd696620b16869d54900fad1f9c9390ba122d436a61dc345105e1bcea8150330bf01411c6f782453937c4f10d115dd0216b1c06115c04f4cf6507025f8918270c808080cd46bb629503e807f2133beba3fe72cf966619590bddae61386265d27b07e07914d59b777509e3d7aa04302837506b9057a00fe47c0219e3a89b9512d678cbf8a9e93a2c7d5ba02f037d832b0efe2e9f277b7237fde5bd2762d43448c03e6dfdca4a61b3971c5a4b9e1ca5e96951012f76a0d01fe198cb53537d204f6dd2ce3e2b3a8a6afdb98b33240fe8480af84c3ceb5704995fe0e4dd26ad257c0c008d89fbc43c3e5f4d249a80e7d7ebf24b91334518596807cece18fefe3fa42a75c2badc3e19d7354dbe1d643c07cf3ad1c815e575c3de50c9f088e397c7873cb8ffc803fb39d97222866b7f5fa1abfa0abf0333519dedb22f0f976e2115ba2c3023bf3e25c295338ee161f129701ff018c5966f07bbc1bb4dcf8282c7c7bbf28d94072f0fece5b5aa357e20ff37b013b333f4944bbd2dabe02fa42d47dd483b3a5e79c3c6f953a05e71277383e0e90cf5b9d176f1e43bae52cfa0523a95bbf6deeee1608e04f9b9b2dd7ef826d72eaa9210c6a29136c17d8b7c0f91d7bbd988b7c3852a926b5073fb22bfa6fe0580fd00f2454abf391c803823f750636fb5f0545b4a70503f3e58cfaee50bb5b88aa9cd46577c32e5a0d5b6c6001fd76ac434af27214b253cdd03cd9cd1fc73a9f7818e02f3fdbaf0e95361f86354275e0615cd73034c183f7bfb8badb580d32f0f8c5cf70414d5a0a324c7ea600f48f11aea4f16518c6127d9e221b239cbe00f75f7c40ff5683ad5c687ab3dcba30b2186d62f627aaca4b0da81fb8d464a63efdb8cae45be961c865546ab25277403ea7c40b3e451947dad17abbcecd421c1a4ab3ce1fe067f4e63b374b2bc309acdc65dfdec451fe16893f03fc1afe0adc77a9d826ca70cee28a9a22bcbbfba83460fecacc0f3e35535f0fdb70f48dda954be466cbb20460fef5af40624e8811714b0adae75f5c9d5db826ed13003f3691c1df33042dd87c3bb63f804d21fccd8dd50ad8ffb2e9ea0c71a1df38542ecb117a064fdbb96b3e8703feefa05cca29e27edead05ac03a24362d4edc76b00df53b587c4f3ed94ecdf6895692fc33975bbabdc03fe7bba0e43d13f631e5b580b2b10c55fcb043d33bb80fb1103ba22ea1fa8a323ffd11aeaccc0d9c28acfba00f8695eae8caf5627684671bfeb49567430ce0da51b980fb664056dea06752b829bb6e115c7e74e291a230df01fc2696e74cfc8998825e7ef04bf0e3bd5e6a56600f585dabf9005e71eba79f7d4cc26ed5d87e2fe4320901fc354aa620f2e70231a553c34cd6235859ac8b006ee2f133213b4dbebd9e43ad5c064549343cabfafdd00fc4d10c929a68a1446757d312a7f05b8c5d157589081fe2eab5cfa330e41a6fbc7acceef9d2887727b8e0c40ff23cb3c66bc3a14769d95bc5cd34e55c52420cd0ee0df80086fb854073a1d9c2bc4dcf3e4de9b5d7e09c01f1c6c5989f9ef9aaf9ab19b0b4f59f094147acc13cc8ff46033b421af4ebcc0e2976e7d45d2d0634607f26524cd3522cfa9ae68e924db5efcc6387f94bf9302fcd48b54dc7343aa5e74eef819ba0675cd66f2c73920bfe0e141f535c44a052765ee3374ab6b786bfd700770bed54ebc170eaff4ce8db9f960aa17927c6efe820cccf7494fd71a5b319bc4a64d686ab1bcdc52a9f55700fa5f17bd8e4eb6e1f27bfb4f0f1c5633a8a3284d4e40fdc21f2eaa1963bc82b4c10fa40b361cb26df9970cf8c7e34fe36b78ead5e9d12fdbf065c30e1ea8afad007eb49db653667b8276eb8d6171b4f01dce6abbc91240f0c9775399bbb05595cffd52d717593051b1915a020876e9530d983e4236f2a694efbff327d1563c7bbf01cf77a3abba9d65c8c9d1cb9e319a53f731eb47bb97cf7f5fe74e6de544c95f5effac3f77d1d7b486bf75fc08d407391b0e6207684fd99d4c59d26bcc5badf77918c0fa4b3239fdea1acebf26085b7bf4d8b0b64ecc7203eccff00709c47fea97987051e58e57f7353bd6e246407f7f446cdadcbc8be6f023c7633f20b369567bf614f0bfc4add74e90f414fb244651f142f2328bd4fc5a05de3f36305dad3f2f4c14dda5a52c368f3b033d5306e493a4c585635b953ec5fdc6262695c91038f91ccf0e7cfee6b604d392519f3cd79d3287be1c871b34db35c05fb418faeda8d3f1c48ba4a674533c2de7b90bff04f4237ced7e38bc733cca77e7354f7df70dab4a7eb4807e536470c6d45bcfc277879fd61e90edbfd28a8505e49b753748fffefa8b8b70b6e1c215c38e52e6c4f504e87312f023ab70a87da7a15abf3e7676894c2fef6d02f89bcffaeeeba119711153e4935f6083be75a22526905f9c59333bd55a379acf1f6a80989151ae535ad102d6d7476dc99fe635d2e7f6ea86c2de43612f7e786240beefaa1451a1d626bd604976c560ecef7922a44931806f0ff88b74d493e0704429533989b8ad64cef254017c1d8bedb3bc724fc784692b34ce24e7a2a67feb06f0dfc7193e4af6fddb3ff21e26720634a6e5a991fc81f3835e4ea1f717853932bc733cb629b5bb862e762ee06fa92f7f77328f737d7efd112942bb68ad39e0ef4720dfd9a37996bfc95c107510c59db3078363bf915e02e88f16c5a8de365c720151c3d79baef04683c983d0803ebc8087a4741a94a90e5d3affbe83fac111b2150ae84ffd4231b69f392c98d992d03ffa2ac13b43f73703e7b305b46ce887d7fbd35ea313b5fa59eac9f66223201f2e6a5b318cdb253a5f282cb820437efa6f446a1eb87f8c7a53eb46f19c747d52e7d9a47beb040b8d1113f04f7e96a2bf1f516f63a68c13fe3693541f780bed01f06bbf75f8933f458586f02aa93ed15287b0519f4801cf9f9895f79fcf6d6398d2d848e49b2e2394cc4216c06f2bd7adbf9aef5d58b3c4b5190cd51bf3d45ab203f37559039d148a881fa5ee654721e969dabb545b70007f203ec76f79f8512a9e038ba1ea2452f9a73afb3c607f6e624068f3b8337bb0fc39886a41fb3da36e3a0bf85779c8c6a6bebc8f3ee454a2f6d6a94212ceddff0cdcef9a21fb5cb3e01f3bff75d34715f74b58d99f854d60feac31fe037dc517eb301dcf03771d0e854af7d1af803f33e597048205f54285ddd9dc38be95577452fb77409ff31a175298cb824b4488ab1563ad4c7f5695e906eaf713c92aabcc0d5bbc9d4781e54ef0b7bc769d55c01fca453ed816b5c319511754af944ff272adb76002f85fa0ef29b07a953577be6433f1d5dba7f514e41803fd51f72811fd36e667eba91f588ac68ef0dc5f476081bf9f26c98ce164aac8f380e49aed2dfc2f17f9501be89f51bbfafcbdc2d2a927d80e9a0bc663ddb2cb0c80f9e869a11fbc875a7e3754e1f5d2a0c54f87d3659902febdba5b5fcf134645199f9f67dfd40a5ee11ca49d80fa460241b0a25a602117ba7dd6df73c11b9fd0f606f4e7fd4c412f2d76619f8f61b921aee18598d0affb81e7dbe0a04c0de31436fe4c4d8f13049d75e2bbe500e8bbe2335fa1105934c2f1258a7e58901238afbaeb03fe1af470111484ab13c2cf748793305c48d982285340bec41971fb13edaa58891abac6a5dfb1fc5b7cdf0490ff8b4f4f7dde2789fbf61933383b48d722795c3b01585f85532125d8d09535fa467ce8b8036ac1d52784c0fce467f15b536ed4c7518754afa8d0a3fc7e994244407f7bfeb1f4f36ab77e76dd6f79efc9d328da3b007cbe5f0867dd6446070b6a19f24586f78e32c430bb80fdbb08adedf6e819f084daf516d6eaa5143975c906e01bf850e30fa817cfb263bc5f02d5ba83b1eda10580fc32353f1f85bbd0b1d5390465eee47baa34139f7ec03f338c942b8112218bfdd2ef3760c8f7c737c34c10e07f9abea0f1659c529de0e10833417185b29261e102fd1745c0f1af8441a912353609298781f76bdcf3fb807e72249446dae9641945bf97959c1bfa8a76b00ede1fc722e9aff1bc9caa09cf134ac0fccd58fc8de201f0076d9128c2b2ce2286bd4ea4297b0ee685d21fdb03feae3478b50722ace49c7952770a8dee7e5c078512603ec931defbfb25d998c2d8b622a6943d72643ef950e0fc58f4573d83298d7d433aa80d1a72392244f5e901fcb15a2573505dccc3ec5f9c3198bb35e2bc72ba8d017ca45b8e12644cd82a2d1487ec9a9571a2d1a74002ccd7cadf324eb8e94da41fb52248f47aee93ec7cc102f22fb90742c20d764bc5830a8f4c1b830f44e37f7681f555c395ae02bb2b21f5f7afc666650c8b79801812f0fbb77b5379107ef3849c37c3c0ed1b48eaa2fc0e03f41f525db64b895c47993a84c4cc5126644435d3a8003f7a939ed0f24afc8ec629d3d4d9dd834868ffcf6600c0ef40444a13fbdd3a5a199bc808554575f611dd01f78b4c938b6220fc4ca0f59c0df93d18b22adf250807e023facd3346951529ac0f4c517de8f52e1ab745ef007c958b56ce0d33b39a28e7f6ba871586f1531bd10de02fbe9c773e05621bab3c2a0df8a40f8a78ef7fe0059e4f960af2da3b680487419cd3475a8639fbb7d875803fefb249fbc34d85e767274bcfc596b4345cd2dc04acbf01fe32988f5eceab3ebe886dc1a5908a2d3c28807ef93ff6de022aab6deb1f7ee846ba914640ba43404a52babbbb434240ba4550524240520401e94e69a5435201496969e11bf79e7bdfd77d3cfa1ccea3dfbdfff3bac67038d8f3597bafbdf66fce35d7aca52a451d2eab765e466a8a601639404b4494c55e07c0f703d848d1754ef73729f7e7b8d154cd7af9eb00fbfb0ba82c27fb00795c16633d4c93f7e6a6473e7000f96cedbc731ed1a0b49263d5288fc3e45ba4c7d80ab09fa3dc38fc1c9378b3661ef1e890cdcea50185ed3e20ff880519be584d7e4a3d7b2d64a56db76b7c904601f0fd90ed248f353969fa440d0962a4960fde939dc800ec4787d1a7d6de4d4415b54801f6b775efab39c36e03ecebf989fc9831424969f73a90c8cdd4c4784ac8f501f63fa7a4ade4e77c9dd86b573904cba76f75958b0800ec03bca2d3915056a0a62c57dc2bd6826eee3958e680f7e3cfe458d22c3f7167a9bed61b1ac15507f3a01560ffbb5e8c6a4cb929b5c0fc92e3bedcc828218b121540bfbbbfa7038d684e68be78aa163eadb2e9a08a5206b04f4139bf8c55e69297b3639237197d062dfd39ee1a20ff65335af235ab99ea3432b5744e86d9899abb912720bfc0a868c2c6b2ede4fd24f1a146e68466a0c004650b303e00fee1f2628b2381816025fd8d527c2e0766c0fa263cd556be55727bbd5b601e79c79a3a71a7db13903f4d0e0a6415c512f276997a758428d79761b0640dd8bfddaf8091c9f657a97111791c807b46222bf1c913503fc288899fe8896e075c7ecd28131a8af0d1c10c19e0fc2ee5a72b4f930434136bad1e2c68adbda34e673107e80f38ee9baa3d251f9eada6a675374ea9a63d103603f89f93873e0753417bd23fd06540d0db78a85b2c8303a8af057f8e93dea42f8775031bedb18ea98fb5242a2d203e634ea283135a1fdf78df6a39cc392c2aaf4f3e1c90df7eef7ec9f9d00a06bcdfde7268a19a7aa915d45540fc61ccadf9706beb0834f98878cefb9f0b274467d201fe83f5a2dacffeba0403e34fc5dedcb70ce15def9c00e0739e75c793562c237e2edaedcce54ae053e91c12c0f7fd6013a618460817e9f44189c36ce189e5f0356540fe40f1ea23dab82e5ba9adfe48a80ef4e378baa72980fd2dc65519119d9cf3a2d6d220c6f9bbb494cf50927980f1f96d6556fbdb874b562e992b98a2d52b296200fdab5292faa698d5c1697033341367091a89fe2809207e90665f05bd3581f1d6deb085615ac407f5f74d5b80fc2f3bc66782740e31e5a15b73d766e967a5b4a23b01f1cb94fcab64b42c11772f664e637470b273e45e7802e28ba917f7045416ea0be636b04c7530941be24d0201f14d5937a1dca80515194342681c35484e7113d81200f6e39c0ff5506a630cb319edb227c19817a94d793b00ff525690d2dee6b8130365303b4dff33bbd1e23a0c407df4ca678a0d6f0a7b9def164cc7f4a8227e7c6add05f05f2e1a25d72d94e8183069c8b49380eec9bc97c204c44f544d984a4ed608ef2eca1528303abf3e10cbf701acdfa3cd0c7c2706d1e4851ede8bf4ede3f1b5fa5a80fc9fe74b5e8f0b02c5dea39cd7dbd355a70573bc7203e43f50f7c840493f297a653c68c9d14e7487a4e04214503f3a90bc17f1c944af51f6b9f9b4ca2d67ef71f34d80fd7ba5e2112fac23df71e09b184b348274b5cf9a1480f84b712fd748936462f64edd6b0d7c66a46537375301febbcd70e84f2f7d0aac7042e824a0295966e1d39f02f0b7a5a59aa293995bf16c1824a7789d9ff734830d601f7bbbe5a6bee2c1bba654c33dbf03fd22c28cc115b0bff24fb7f46865c09e9ec14b96417c08c7f8cef72dc0ff53f8d45c4a91606640c7d9f9b45a5be70d55b302c07f8b8065daa9ecaf18a743647bd4d13262b0adef0c90ff070ee1b0fe7a1ce831edc766f5d8146e24cf5400f171cc4bdde803675d645e2f9f4c349d10c45e2ba704f8bf784decfd7bd056cad465c937de8aaf2488daa201fc235b56f7442ae8b02aac8b3eebcdae7aa04cf0a300eeff425f98266d0c9bc523cee47a5babeb59675b03200190dfdcc47635f4207e62d1398a9b85992637b900c05f2432c215413ecbfaf750b9d8251a235603765f00e4c7d884f6d55b73b148932cd525b8a7d885ce2400f31f2894f0f8f68af4e3c09bfd65b85ab4c87bcc07d980faf9a73473cbf58f65df5a6ad257b5d9aeac215c9b07c497bf0c7a3ded1a0a5bd50d3d4e25f1a2ad8a74ce03807fbd89c0536103c39b9cbc4e5d68ace7672c38fc80fc406e8a6996bab61c4bbc92b91aa96b9a2aebf68ef3c0f5ddceb741e79820596657c1aaea8ed6aad41e20fe0a5dc32d9106b1b489962a8d10c335f762478102c0ff7b4b2e2d8617ba0bd7ca19950cad7b3de2f63200f18bc481de3e84a7b15e124c87079fb50bf8fb9e01ebe3b4486f7272d8694bd29c287b6862aa0e4733f701d647e1aaa2dbd4a54e46a354f92ea489f973e2f5da00fb4a69273ea7165a96cc663a06edbb14e9a682f700f101524a26976749f74423bca551dc2bd6413730da0aa82f6fefe49d3ce39395e7ad92ce1a0be590d33f810cd08f3b0263b0b1bc2b8ba6eb05f644f7e505981f1803f20be2055b22e18d6a77df6675c66094f0041db85903fc3f49d7d2d86f6f8c68c9cce0bad5c4bb90897c02d6c77dc820cbb3f530e5c9ba988a73cf40c976141214c0bfd020b8811bbf8668c64996d2ef65d8625a8eb40ac85f6baf32c51d9816785335692d9251e9c46c7f6404d02f135567798ed59fab189d88bbdd70f74c938cf307c4ff9422bd3fd62ec8580bf13bca24d02f826fb61d05c89780f45af6b0dbd8ab7692cfab1958a14f28dede07e887e8b1aa71f75105ac4d3a5b9a718ba11febc503cf07b3125660fdc0e16710a55aaa6f6873043da52e07888f193c8fec607662186cce43ab9a606f290c47b502f82f1ae8073b641cd26e8f2aa4a21e7b7164cfbd9505e88fdc674e9a4442046ae2ee9f6c60f62beafcd6b800fa59eb80f173769ef7a475661d6f763bf7b1b6cd1e03f627762f612a4c33f453f14fcb189d10d5af22b61503e20734dcee36c8c3b27a0f8c52e428b7366ddf3dc903d8271745282ca913e92510ec486d0563e653304f0400f8da8b397cad7e354ed5e2ac2d021e978bb08a610b90ffc6cf43235e77c455d2e4c4534b39272f36c2110bc0bfd375f76d3cc5a2a752959fd8ee5d378f7f58510da88ff3742b7e54472e3c5852c868ecfeade69a05951c40fc63c4959abc63cd3274453b150a64e29e6b619a9e80fc01d5de4c44c737b9fd8ad16661d30608e1ef0a6e00f667cd4c78e1558d433493b76b9ee7dfeb1c187b5500907f0330aa6656947861b867834b240d53e95ce93700f1ad226239fa4855338bddca62ee12d6cbb99ff63001fa794c2ccab6a22af61e978c269c1a69db05ee2d47407e397b859014038a1f7c26d4fe60f35270aaff290f607ef237688b4877640686ce266aa345a67d321f7703f82bdf5423f2faf1e6a074e9567ac41462c5cb479f01fa19d281c59e80deced1ecceac4860c2e7e79846ba00fb1617fc3e4abcb3c47bf32172ce10bc9b9883c1f100ff2c21ec54b682b46ab228bc0b12a544ddd3ca964ec0f90ffe9b243563041a13c2248fbc5fed2a4f5b9a7e00ec4fbb79e7dee76776d02e2ce328a78fdae8bfdc6800e07f42ccad32cc5b90f7497b4ec7bd8f99d1ef321701fb4fcc2ae108caea8290fd2bdef31a952efe73a18d80fa271a06965dacc5050cbd4850192a9cb29eb34f0c00f3a32d8bf1098dbc10667269511babc3c3e909d51dc0fae9b1e6938ec785f8c2da002f0aff55c6bb830b0a40fc6b6da8d168d54c4a403a51fa42a46019ae92a82420ff982d8676dd21b8a8c73434db2cd022efd18b517440fe1343115d15a76182efbc7c2961e54aa754c57d2ec0f93a58adf814329b4b78a7ca4e4dfadbb18995d9b800f9c66d11973b403c364142e06a5b10bdae615ae80a38ffe3d369805122e2b00894c8ae89d316cf536db15a80fcce2df294c9e356c2cb0d9233c57c70ddcf8a2803a0ffae4377ad08f47761c3db337da62319221ffb9802c88f81bad7183cd90335acf4b29cd99dcc4687613a1310dfb32193be5014806929d3c1b619a592aae6edcf0c05d02fbb853669bcef3179750fa904dce733e526c703f8ef2b253a07d863192d0b13b7a41d0bdb1dce62b700f88b74ba86499a1f612a46a8e9634a784d2391e516203ef5445f084708254ae3ddb82b5b62c9138cd6a94140fc5b536cde886c6ad372faf55536b559ef948c1c13c0fb2b5c78114455bbb1930886cfbcc4485a09d23802f8a71aab0917d9ce6a03c51e580d40c72719150503f7ff9bc723a9618d45ccf7f2b2add724635c23753600e73fc433e73f7f6c76f6fcf8b5461f42a2b39571672520febf135990e93954f9be55e4f245efc02226a9d80b407c277281ddb94058047d4a64afab5d29da6670372a607cf8fe764c05896ed2f654d3cea37363d826e46980f5e9432d0787cbaa83e4d5814427794174f714f202807f77663679a3892923a37caa2ba5b93e392d16f71100df2162924268afc67ad4909f361a18f9f149799003ea276bb18c8e3aa6144d6525d99c8dd1c7efeba34f02eca7da7e18877d2aa1ae193cc5f14f4c2a6026048c01f6dbdeed9b4d2feba443cdad27faa4c6e7b95d260c00fcd18ef48ab7778e0515df53a20b2d3ed4a5dd9f10907f3e777bc4117d9fb100bb6af1508ef15ddc7df83140fc180be730d75bdd98bd9ba2a1710b4a8da80f490b01fe27cc94e6869b11a98c049650f6730f3a24cf29e100fb1f697576ac5068fa7daf7b860c7add269c1c941880fa6437296b3a6b5e588eb31ad17f94a1db38e4f1ed0514704ff6a2f12da195974c49aed93fa185352cf34303c84710688cd12ff6c996bcf847393a7ea795b5980780f32ba29c554d5ab41e9d6e3df8bc8389fe90fb7eca05e0fb3188c12ec2db5b6f625f1fe1c2c27fd189b2380ab01fba533a8ab1b23ebd5efdfaa4f6f8d9043bd9e61620be1764ca9f842bf2f829f1838ec0dda1f6467e176480fd5773e5099ed0ca1d56530297e22011d5c9e1eb4c00fe967b1e7b34359f14f036eae03c8cfda979c2610f20be9c4575425ddd0165cd7ef9ec105b9feea1947521e0f9689541b2d8f8e84db3f583830edc567b460f5200cfaf65bc784c9571e28ad762625117556fe6b4e00ed04f24e5654c482fe45787e39b8793643d8f71ef6403f20383364dbbaf0f0ddfa626cf6463fbc0bd4a2d660998ffcf1677c6589b10aff344f3715ea10a56ade2ca07acffcbded1d405f8ea4d72c48756138a68f758721e02e287371936a60d8b736b9e1195e729268d9521b5e103fccfbc2f19efb4c77ae7b36e2f6d585909b99842c100d637cdc9001af516d1b141c5ee557649836aa2933700ff727b0a7fdca7cead169032f4eba205715d95bc14407c82230831068f3c2f7c78c5e01aae36067aaa6711c0be085568c66d8d77919b21ff4ed40135c55bfdd40fe01f6621ea4b7ca488d688fb2c57666061a2ab4add13e07f67ab0a2fb6e6c57822c5b396dc7f337286a7981910df6bc678d72b996bdacbc7bbd6485b187fefe53b0540fe37f306acb5098dbb5585e748b389465be2161213803f3fc39024a1d610b5a6331351ea5fb75aed390c07d84f1c30d17c2bcc7a2c8cdf13b69ce666bd7e71e721207ef5b1269fd1ad1b30ec262c91282e3c290eaba89200fbe0ada6eba5497d2721e2879d7979864abac11f3901f689396551d1c084e74d5581e83856bb8fdd5a8e0f81e7a3231f53eec3bcc4d13bac5ef2956e081c918b03c4670ed82604647a9acfbd5a137f7278a86c29d73b0c887f20744d5bc93561a06373f8eccdead08e0062a400ac0fafa4f6493c25e94a74367a3f5ffd10234a79a503101fb896f606b5b2b9a5185d71ffda5098a33cf2b42a20befd3d6975a8db95e57753639f87906e39176935b301f2c3e6c67218838d3569b74dfa4688238e7965dd1f02f6270c1727f4320e71b2b6eab8ea51fa8923b8381600ffa307568a7e6a21179d2a272f89e69c50d82cd60180ff8504eb656a4ab5605ec5b122473553c1bd1c60009e5fed141d51b9f78410e59471cde2be200bfc2d65c0faeeb32a48ea993f8082bfc732ad6e7cb3c6061f98bf71efa3a62d4995a8c46240dd0c4c40c26c712b1c20be2cdbd8e9f891756b2116722e9d2701c36bac2160fefb4efe13313b55be953dd6c2cfcc8c374aea5dae02ed13eb4393eb2bea7af6fe85c2e90b7714cf4c7901fa011be7eadb6a3d0bae91235498f7f341ce77ac4a00f623dfe71fd14547a5def74bb79b3d8a83fba498bcf5f14b7ac1d6e1858f2701cdba3241f76278898328be0a207ed92598006bf239b51c5ebe003c811c29a58f1b0d30fec598157377cd837efc75557a5cb77d19d5a104203eaaee7407b9e480d435760b5ff0091bf267cd085f80fc231bd5b62783f5d5644111be478894bd43204105f00ffb16d6a6d9a4b3de6ca594b70c36c02d177f711b609f99bb52195a4600159d91ac37b3d78ede9adebf0bc88fa97d66acd8c38cbf5e7367750f0bf704e164731360dfebd42cd12e73d510f8109ff5362cdebc16ee4e1b203e0984d46613c8a8843b9a36b4171f6cc26c3f2ce7037d8cc61b8ace75f28808040a246e21f8c7bf1a34ea27eb166ca0efb79bdf2743830c6d0eec0f05120c8b75d3dbc4583f3304992c996a540f95320adbea9ef7a2cc7ebf3f1498fb43f9812ed1e24a109f813452b1114ab618a51f4f46442d45034cd799b036677319334b30af250289a4e784503e46777df78623cc68601ec90b860e0582acc15b7a3a5a9ad9fff5feca6677cccc6cff727f68c8f00105301d34d8de4563b16cadf2d30be30d247c53b951815e7019fa0f6dff6207306d1ec2ef6b3adfdbb1fb6df2953b570f30be37fdb53e5b00eb1388a86d0bfd8d3cfa90a0714fb9d41e5e8c29153cf59f7de5d99dc205461879b5a819158d58d31b9a99fc7d1897a143ca7fc0f9a7ea3ee06c8674fec134443a11353340f9b0cd3f683ff51d4698b2c10c32030c1d160c1d8c8c842207737b30730c370fe1f86e5e860eeefbfc050c6142363e184330fdc92f8348d13f90d17f197f7f6a0d8521878c0e3d0fe1fc83207b3e5819fbfbfe586af6562eee468eea1c60d7803fc3bff70421e33f10e0014b0f45bd90375699163e6e9211cce37ac385e09a82e96f08e1fc5cff43cc41ed5f1e6a3f47c701d79021942f1910f22f1464fca904f02f1641b5a2698660fbc259b8bd508fd55ba1ee3a5b02f37c1408df7f1ec2f787747ea12f23ff2e25ffff6d528d0f029440a67fdb7fd857b4bf69d14d55de4aa9965d260d3c02e17f5ca9255aa11149cb680fd83e7e53cf6d863e1af40bbc227a33482b46abbcba5d102b1f10cea0c5801bea5c406ddfbb04159978d84a27e04668f7ed99b87b70176125fb2ac2526250715683dd3b14d19c419b70bdd53ab11c94a2b815c5c0cfdbb9a653df7e53913a1fe2d6a86fdf9ffb9997fe6efa77a01cc31f9b33d50c7d8ca5e4178834111f48d9825483251e50a4de0b2148e02f2304ff601100032278304216098c90440503e22b80f7c3eb32f9d439587e1e62f75ad02b3ed12c27eb800d422108667cd0e2d677b000209c76d8bfadf788b1c1bd82591dab168e7eda98760732390a4e51f87fbd4181d344c40be05201b662ad3cbd0f6fd6b36bb2aec34d76edd7d7521eafc57f535770724e115a62856480222898a4e192c3670d7069f50e063a2994b9cbed6a7cb8f6886d5e1c75383ade04d7d68d612dccccfada5f1d5d0f5eaea0c04dd8a07b64aeadeed713d1422da8814bf283662aac129e8e2de018dde321deededf68f57b7523a787fe1fbef87ef87846c4991355a18b317c7237e3c4eef1ed1a8e37fab734427156cf80a440314896e7f67118bc6345aeddfab2d801850e844112b1b17193e20f08a825fe32847ef8e8a7b6e5ef1b9653901b6f7e9d9a46b06f3a05d61b067decb7bdb70ba715934c43f68a68ce3781d0248c2106f0d3a859e9daf9ee5102fb7ffc2f7df0fdf52702633c3f7fdeea1178c471736cdac1319150b7cab33ce2016da5d3cc8f06d954ca9fb9437019d6ef5bd984deca479bde336cfc09654d17650296d30f400b55eb2714a9c2bc71a1f7dcebd56af8dddfa15cd93758d3a5a256776dbf78621513f0adf045b5e8fcb12a6e056345f1c75e53194688e3d92fc85efbf1fbe4df4749a78532922d1100a0c5fb977e7b238e9bcfa5667fa2c6aa20346c8f02d9b898642fd1294f4c1a27c5a9571632958bf8c7976f17aaf29ee2cf2b5485faa7b8ecf6f0a1c900430e8a75a29b5c565f06244ab0ba16e6228f545a9df7fc975e347e19ba2d92671ee703a351eef2cac608a4a8bdb59e1fd2f7cfffdf07d660f9bc990f1a4cdd3a693ed500f29e2e403dc37f791142aa49fa56221c377a226d39ef72076d914a9430926684529fe6eea45b404e8a0d407e5fae306cc31594eb1b554f582c3db0a165c1fcd5074a5791ec65abfd83482ca56b6bd13527cf2a3f0fdb0dff741cb95215221c3b6fee360d73074b783d65ff8fefbe11b7d3ce27d861f9a49fc9b74363c2d7681dd0046f59f29bf95e71f2cf6cf59253af5a66f0f669020147655069cf75e19a51cd9f39224175ec03249b3ffa4bd32d7868df9ba826ab3f0cc6a6b86521aef9377e9d289e06e7c2124fbcb66e863d8c310716e399d125020e2716d20f3e49b1ad8c541aaf209c8ec8bb05f41db92d6b4017d52de2f05be9e34f793e58d2a8ca26de80c798b65abe6b1851ebcd65db876fbbb16d8da9f3c719bd6302dc8bef3c5fe39dc2fdb9b04b68dc01e6e99f632ac3b3411f3c3cae8a69397306ffe111d5c0c0518d3173418d8c1feb7f30dcc4fbe3fe26d3357a3db462e367fb13f9795a3b90b3f0b8b929dbab6b585b31d9b9dc96d2b3e093923f65ba662d64ab72d9514d94dbc34b925d9e43deced64954cb89438a44c7ea05c014387d43e0f4e6c89b27a70981a1b719a71729b721af37199f19af2f2b11a9bb0f19ab271189b9898f3f099f07199f3191999f1b1f1707098b31b7318737073b31a9b1b73711873739b41641503c19bddb1e36763fd69f810c143d95ed4adbced8efe5232b553ea7168f5c5cc69f00db51b9d92acfa30be3ed62e59f52b29413bec561419c6b87a3dddaf825ca7a9ae78136f2490baf4d707c31383177d30e3bf177d30ed908b3e98afe8260f4c845fe34b91d6de3d575a83a7609ff57c1bdb973ae5abf8f074620066a7477438d852a1b290cc6e09869798aaedf66544dfbd4643c9b6a10e2e7a19a4f34f911f3d077a34d0ffbb451ff4fc2fd1f783449fb61c9bf32dce3be616225262e2da4e726c5c6eee1e26da6e52ea464ab7c4d48c6c94642c655d9df8d839399cff7be65724f329dcc80d9d55ec838fe436162fd76283f89e5e931c79fa26c7b77314512ec88e73fb135414f4b1de73acdb0eaa29f82c198ad1326f3e38d867a390bfe83599202301cfdaf0f8ff666de8c3c3407ae35735f0b090b336dc570e2d4f533c7e7e0fa991f4f1c32c6fff378a9bd396f3c60f254f2b87234d57c3e650849d056ecef29cf1d67e64f980db1ab57719d6ce4661dccee485962e993fa837aecb99bea51424fddfcdda70e4ffd7595bdadedaccc4d5cc1472ad864fdc55caed1697b91ba7319bbc869b83aad16d4b19251551454f6733794533655713456dde3b2e4e2ece623f6efc90468edefcf95a8f9111b789a919371727171b9b310fab391b17bbb931bb091f2f2b0f3b2f2f2f1b372ba7311b271fb7192bab111f37178fb9313b2fab993987b9399711273737e77f54ebf901ef6f6c66cac6c669cac1c1cbcac56766ca66ccc9cb6162cec5c6666e6cc663cc6dc2cbc6cb6bc661c2c6cdcdc567ce656a64cec7ca6d6c64ccc9cac3c669ccc6cef1dfadf509770ad49fe5f3ece48a57d1c20c62bb8a441ea874b6beda425b0f0d54d561ce9775b9c9ceec82ba1d2ed7541aefead45eb47f95524ee60594839e620d6fdad34af05adf6828aee2e1741e0814e4ce15070a8656455904f9351fc9f905a11c991a568b63c22600627b7ab9c5fa02e2cad89caed149480a6e48bba4c93e05b737801e0985ca7cd5f2681a1484b2de0a0ac27cf714e4d7fc8fab4fa94191aebfbf0acdd22b3a75f4d555446e8faaafafa2cec6dcfbfa2ac2ee9e01f5574fdb9012c2fe7a0cc71a25affa80578f61bd4271e51e3f6e8ef6fbed3aca3159620dac699ba6aa2f9897fdf692f1af1b46231e095512a2787b30a9b99c3ef63f64f5eb7b769d02e5830f08c66a49748800cc4619c032e70efaad8aebc1d8b742408ee9e88da9828fa764bef31d62f042e49420b30f6597fe658989dbd81e5d082b0266fe90fff292fdaff905d3df0fb2251ddc8a090361202e0cb8404c9c0194daabb4c13b1e75b75127e6d430abe834b7b52f21527020144910f78774fcffe1fe3fbdfd537e31b7d3a7aa7e25d5e8f77d9059bfba7a3d72e73aea575719cdfbc3dabf9275993a798e8b5ffd165951c1b91c70f5181ecd2f081ebddbb5061ef60704d641a84701996ea42546a774c8a65bf079ac10764fdfb5a8e92e3408ad53e01c0fd4de7c83ac10ea82e022a03f8040208c7ffdffef7fff6effb83ef6e58f09a7798b8b8d61fcc450c991ed93d1b43f4941e1772d942beaf3d3527538dfa31fa34ecfe871dd2882b3efeeed6f09f59de70b6e06c1bdb336fcc09b183a844dd763bdffeef67502adbd465392de806a334e91aa369541d5d79408c3f822bc19ed4d81c18bd0e16448f1d7231f45223071a30577f66f24ce3091d453970b4672773509c91551b30cb6d0e26b796538ca8a452c12c9d64767e73dbb564aabb3ea685a52235478323bca85223ad8eb4636b55068e598bff292ccaeffaf7b9feec3fce30540200a402515d4122ef954a504640f376731b1b9b6bb8f8669917ebf8ab4602c9f14a8fb7526f3f43d7f3ad8531573fd3da0dad14c90a31fb5693d9b2eeda29bdf28be41c59a4ccfb717e01ed2dbb570fd1030f82ffc8240a13705820ff6f0966b636bc972db4e7d4690d424d76827c83bddd31ed843d91ea1061cb931d4f52815fb8e7fce1d990c3a68230c0381aa55cccc3c968db58ad841f358cdced037c81ce91662f5de79aaddea0a5561cc1a73527fe5c809e51706edf98c642aea2d53924fc280363fadbe55f68198769b4a992471b9affab10dceea001d5c90136297cae3d56a1928b9edb39e7dabf9ebf98e7b340c93324c7b73b4cbc1733765a7c982310e16fbbad83d1d91debf7b5db32f97884402097e41d52a20101cc0d7e1f35c0a8d7c8f72680b23422bdbacafc2f59153fbeff1cba0abfa8a8612b50ccf366ee6117d2359687022e03483768592bc3eb9574c049aa296260eb881f15842a2df85834985e425077f0ccdf4aff5e0972cfc250bc1cbc263e828bf400a11831ae8a05231585908d507489d4b80b3d36fbb14555d6f9be10e37ff38b05d86130495e9f1fe2fefe9402090b9adef6708e11ee42886072e81c69ebe57e52d1fac806b6ba20b8d7cab18ea700fa7df589d4bcdab711c47e280a621707a2f84fdbfd702a4222c45d8179588aa8f526d94a76f7218a096938437bbadb373055b0b3c8f99f688dc79f1b2bab51b734e1a1e47923a862ce930f5619a6b33d6e8e39ee1d8b20e128220df3da73cb7fe2dd7bbfa5d8b82579e4aaf323cb112357b82f39ce45a9235021d949eafd7ae10b15d0bbccbb5a9e221e1cf72bad82244083c89e62f7b5fe1df032396a18faa4261eefb66bff203055e11bd19484724559dada36d6a7f9975d58c5f0a7531cc2bc69fcfabd1bde780925850410e425d1faa632474e5112ea63076be64f92d22efeaedd1a128d7ece9244ad40ef9ea89118966e82345bf7fbbe3aac591f4f300759cb1d8662b3fb9a61fbcb612cb72cb1c5e9672902ffedef328ae089880b9e53f1be5880bcc154cbbd9d58372fecec86fda77c435a95413d2d6627ebbe5bf3c0b5fdfd288ade2e45d7af6f1a4e95af3cd6871c44a139e8aef4ecb6ce7cd66e8dd50648b74972b31ffe3b2c8c8cc48c8317976ab41da5d4f9dd52de22227508a9e46e575a689cbb58e6e5caaf06f19b37ed4e57f65db032f7fd3ac745135aa4efed5d58f2a1736525f1bb1d20c3810bfdec29d0ef87fbd857b7dd6ee41f7d56f41f8b282d95ffd767de2ee24c957bf65a5c5282cffea2a135257f7d7bf65d0dec235fc7abce57905db5f9bedbc09254bbe1ac32807880d68e2fba77171bd15f4355286cc1d9db39ea247519c5cb3915418902c34d83efc1e5b27bf1f7f710cb5e11778f5eec71aa845661e7f5cc83413e0820a8ebde4b07d8d12820378e94fedb86d9382f6d16e5b5dca94002e8f101c1dd0e824cfd596e8ae7e9b2eb1ea8fa24e7f99f7ffaa11793ff81ef9e2e2e202428108b6560af450281cef27942118c17f4a72f2e340bf1f28d1e0b775b486cbfee9324552f20b442c140ea4b873bf06e9476417c240b68903675984073c7fb05c96e1a1e55d358da12a71b1638ff6047a25e39fbcc9fc159d0971bb7c74a65bcb23ddd882c317f39e76da6417be64d01e6ddfb4d032c23d57f786822cfb8f1fd685ec29b4ff705414b79460e36a0c09f927f1d6067373661eb237d2518757ee060591a5e424a4f0e90f690df6787b08e4e43715cddacc17fbd363c46b1128fda8ecbfebe4efd66196b5aca908482230653c474147c783bff0fdf7c3b77891e715c733713fe98efabb07bd434f85b363bf594ae047441f87a79fe797e494fa806c6bd60750b1e1a2948f5f9fe812eb7ac877605b9bc70892c8877d5e25d7e67cbb8483da6fde7d45b3bd1e65145e50f728b885506beed6de8fc2f7a7c31650e1eeb0d31b09f248c53b45812ee52f057fe1fbef87effc77d88c1f5f1dd637aeb67dd056738716ea1198f95667ef0fb68aef8320c3b7e9754cf768dcd6e29d6367576b27cda5b6213bd63682b6eebd94fa249dc27d46fd17fd0f63e6f2d5e44894498596cb64def1c8df3e9ea546b856a3ca90f8a62008c2e87a84c1ff55aa10daff0b3c4b80e0cb7ac612456707d453ca503fe5103d136c25e1b5ab3f97e960607fcf7415a7e7b54952e60a83d969ad598a931583f40a0c3f99e9a0fedf663a702e7d285811d9bdf0cb08b67bba77e4ea718ecd53c202bd33a5517bf0dbf010beff8c6b1890e0c8dc9637ec9b6f27b42b727f2be6fb8f47dcf2fd89132c92ffa063dcfaaaec8c89ce7ee531126f02c1ed68554a27d919928c32030c2fae181d25c67c8cc9b176477baa0346cd53e7c7bd09cd6c694684c8ca2e28a08b4eae1fb528a68444592e21529de298517f70a53628a7d38c8cffc59f7f3ffe04c77f97574c80fc79591c99dbf2467cf7f6743edf747120bd8adf4296a7b9fb33f953aa3d4ab6041d5bfdc216697d642087e0e31df3840f6a31c8e6758329e37d0894b6ce78ccc1afd4e8f0797636c3c43094076e3e66f7ffd0dd8b274d28a7fc9675019294d0e61ebfc02ba9af823e5764081dc3d8fa0552f0efd5c098d2b0dc05075b80f1dd9128ded10b3ae67964cc4ba7f4e8a5320c6ec4c0eff68665e705e7d180d0e3012e6f037062f437bd4ac2af7740f65c4c016ecf16e65b6fde7e6279f07a3ea8a2f1e50cc8e1d2ef0c5e14a04148ffab4d5646a368a32f79e1db362668f7379eb39ddffda26fb7427fe6fe4057b813e9dbdf14da83352e5e1342fd1e9c360ac211f7185f2f0d093ae7bd1af68024be0e5d674ff6fd5f16acc750bf95750bbccaf4b206aa1def900e6031d6707f27c96fb7784b31c6c83934cd15673eb07ff84bba8ed7938ff5d0b0d8b961efd8de8a55d35b1711e5821183a990f00cf8fe609742110f84c8d998a5eed0c74f29ddd7db8d34a98cd89c3ed82358ac72a32d52bcc56ec1f1d9e8906533181b56b82a7065e37de7b1c67c4d98c4bdfe535a4525b8f65a48b3df45a6c2b5921f759920311f2e4d32dcbe52aaac6463fecaf0b8cadf07698b9e6ab6dc8280bb73423c42db75347a6b139ebde6352af7ac397502163922c38966ca83e83f91ab83f585e1f95759bbdf97b563a0a9b848e96fd65ae2e53bd669ceda51beb61afacb30f2f7338c4ce4871d464888df2341ddc7e4ebd4f0c983131cffa6fa75454b60e52a648c6dabfd66f61e6fdf4c95cef1ca76c51522493a0144b8622dcce95d5e12d44fdcaf187dcb4256eb2fd44314ca47a1abd9f836b2af766a3e744f2d175ab72361b09dfc517b9c59bd176ac6cf734ba8b0683e987e78c4c251bef3ec17beff7ef87ebe45835e24b2aedfc1576d2f1b437084805a2bfeadce3c016f5fa7c54086ef358d8a39b93cd04cd132231fc6dbf72e92d0466a8b83373625d2aa59ae9478c370deb9b335a0badb8061256f8b91f7caa97673155529709a2b8b69096b89248ee447958d69dde25aae7f2c9a5e6a9b67fdbeac527e191e93f217beff7ef8664ebf9fd9eb01b75b83b6ae3fe95018dc1cf3f89ba9470c7059fe54c48a10e19bd359849c2e0e271636cfe333a6a644e89c7e223c06de0d27d1c889e158f5055742a1dc635aee8a8e407ae79e6ede63c5665ecdb3d30b9bf325cb8f288b06a5cd3f4a7efb76f9c3dea1b837345b84e087cb41795d7b5287f017beff7ef81e9e6a176ccad56f3055bc7fed7a17c32b911af96fda01e9b32336bc90219a42114b67eba507f3179c7e011afbfebdba9fad23a827291311b4f2c7a0b1d69e6d1d28e83cd3618b91da1af8802e72f1e14da7e2d5688b38291ddc228e752b23fc27b13f4a7e8bb1ac53d81395dab8cfb270a462f295b24a94fd72bcff0df15dc555865526b1c3ca4bc0452cd05077969c52aff04d7cff00c7fb11566ea8f864f12ceb401159d3f12edad39e8016fa4826fabce4a9bae2b9c908613bbe84146113a3588284de3ba6b1b7d8604e9987ad23355012fbc33da111297e14bed7469836376ef013942c1d9c2497424b30a313befc85efbf1fbeb573754e8da7a0c4ce6cd29fc6d32f3f09654b7afbadce570576e8c49ef941846f86ba449857cf0a286672325e48236d6e159b4c26cac407af7cb8ff2224fca9ec8509b54ab34184e9bdf378b4eefcaa30778a7c66a359eecfa1bceeb41dd1cfb3c221b2d1431fddf6430844c7b2ab16dfd87a080eafe0fc35a7cdd047437e41709fde8182e0e1f6edaadb33e012bf6524c2ccf39f62754a55f8a30f44ef45eaafa9d2e9efb6227a90f625e10f025f452ff3c26764ec09120f5218ae65d4908e5b92dc249f4f83fbbe65f2baf63fc392611e8582aabfc2ca241e3607a71f25cb184affd963e6e3303bdf4030e761c1db7dfb6ece955c8c79732993e24a22c56913a8642fda46c0e411c0e9401fc3fccbd44b7e1c5803d34e1d6cd306660e6e42f695413088205448100f61b82b1484b577802786edb037d468f05af43bef6aea72443e144e14aac6ff6e7784ebdb108aa40c08570d001d9c47f42f300cd465c87f347f7f5d1e7ad829c59fcf43387fe0daf53f9e93ae4b05d5ff59fec299629258cd3aa699c064e009d919a7dd3d8fd403f7fecdd0c7c898ffcbd4c8b03caf70c0a50ffce470282453d571b4f0cb8c812f75a727e57e1f77fa421d83191eceed452145bbcb00f7eb46fa1a0c301320030ea2a947f2f1c7cb8c1185d1245585177bff599a074fcf01adca40e245f0771f811104f80e4a8d2179124443c3c34f38f93b1a7ba3d2f3cac0c5f374815bd1bf4f46b0fcfd91913ffb1dbfa64b4c43c8e0102e20b06078056c71b6ffaa23177ff8f7fb4f1eb9f867f0035ebff8cf1db9f8a7f02f77154205ec3f7ee4e21f63cef4f250fb6bcf87b4fd571db978f9498307282affd78f5c0427ff7ec4918b187f720de0be977ca70483f759ade104de8f066d33f4315cec6f4a211d5a740d5c50ab5f37dc55c4869046a7ab56216c744d9fd0d956c6969c1f9bd3beb6ce0c1fc7e40f0e1084ec91c09ccf2d8532998158637c1a32b51b7009471f5fb1ae9b7f1fe670f29001095c7c097484866c1a220fcae127a92a226c6b4ecbc324152879be37f724156ec9457e48dc1dc72bc47c9b8210a7c2969f77205f9f301cab110697876800a5513da7ada064064f12bbcfd143c05fe639169b1fa171d51ca460ae39f890fd513d1374a4482ec1334a65ee27d27521f1be9b3c4cb81225c5d4ec2d3ad4373fc538c13c4979ed78a73d94f86e00071b87a0abbe8d780b7cf3a267b56c99003fbe5bc07ac7a712aeabb745cfa963267d2a3cdec5d2e92b29e409d1af9fabc649229ea6d5f73d78e139cb14ce42cce11fee93dd2a476e547c7b5cbb44af2eebd437d53699863120290186ecace2a32cbbce4252d3f260b825e15d5d734e6484e45e8c84a0ed6c3949824d03d88d4292c4f124e795cfdacbfe3b062ce9a922cdce29165ed0f272e9657557587ac34298a46f10bd471fee6baf49eca06879067d678435d2adfbd0feb0ff55a962850ddd6d12ebeef84452f1742a3b64e4e2e854b404d139a2e5b58d9d2bc642d543ae5275a49a024f3aaf267a5fcb512bc27f168b15bd69166a89e089b578d6bdef9c5b6e4f4ff2e2309dbfce5679629db1b7960547784b114ecfa314cb638770213b5f7c3c8f41a2ca479197a2bc6529bde07db4bd821b721247ae5f87595f5ab0497c4034830d49bf6ad613be735ad95c41e9698f0873c3992e7f12fdb0b33ee7c4fad895502654cf9bb6d43726d3f43dab3867fc8c8c251adc100e325bd0a479ace7fcdf51542391642104f78d493ebcea2919dbd218b909db66e325e1afd6316de433dd899d001a62d7f7594832551e9e5c8f5ac05a8839bc6e93e12195afd7bbdaf96c939370d295f87553777b52d1dc4ab4a5744a0cea382cdcbc3b2a27c23c6bd8aa5a3a5ac0598696ec2b747638db593a317f9c2c7d474225bdfe25dab1a5e0731174792ed54f06c43fc53609699560c0f6f52f54a900c37fe0cb975837c8e75d6631f80b638422224e33bdcfc9e0956f9de47d2286e9fffc68a7bdb3d0b9ff5602e9fda77cdc2c10cc1fce3217d9b8e353e7801a525ca10f31955c9e6333f77e27946162fd0289f396fff1af0626e824e9a9f0973f60251e32db7ed962add93b15663de121ba90d90ce981f910469a82dd7d8020d79e611a3273ee7e3c3b547db8e9bd52789b5888889767eb5fe9c5d023f43c09aa747edbd1e102ad9fc2e3ed7bd44adea37a7d7a47d75d028d784d3e0d5078182977d9132bb7fe8cbf69ae5a245fc5af8e21321142ed59a40c1fb5073d688ab36e9f2245e604193588ac48f6feba9a9e5ec5f38e75eb5d2ffc16283479d644bb6044d223a27cfd722f7da6b387c7f64f52c7df5a2854ba2191fcf4f903f7fedf9f5f7003b8ccfcff057c8abc234394105f748ed4b6bf06f782c5509b28211a7524a434add2b0e494c3f6631fc963a245bee371e821da415bdc904e8e87303ba7f7a0cd6e9cf797f0f0a0b22a1143ac290d85c22353135cbd89f5efda0f7effb8c63a3f7c3e388ffac5b563d81dbf2f4ef3e17aa42e07e1d6094c41a8ebdde0b81319c2bd2f84b6f579c86e0f5c11c09d267ff107edbbf3f7a82afd32ccd58bad768158c85bf7da10938cb8ac2aa891667ce5fbdf872d17c2ad8f8865855cf560fcf6752fdc5c2129e8bdab8bbd0dba09670af3ac2cfc1c4c8283dde28f924969a81503b31863b3cd1b8611ab42fc988f29ca293f3ec243d74c572923fea9b2e5bb3f8042f5e1f996c6b1bb6d19ce56d4b01cf4937d1bd7ff1833a66f3140a90d7a0833a81baf8c8cce1e8d7783d032ffff9e1f10e928fcb7e687e81c7b5078b345d6eb3f323f50fb7f7a7ec094c4b98c3b0caf8c9b1a89cec3b519fa185ec92f1091763790fcbd610dbcf87f41f636e0feb8ea2f7cf9fa0d8e6d69ef76bd7f94f9189ec7700ec2eff02b72e3a7b7cb476e14f210a39dbaf5cdda7454bcbf01f5d67a78f29ad3b73e90f3f009c2893c64911b81accabe39fa2963faa2087cd9cf33f49fc9db53d40e8a635005ab2ec808bfdd809ed766a35dbc4ab09d75ca83b14105f56436634ca54bdfdb68e20695d219cff28f8a4c8a977aa05f2a2a30d4aebbc46cca3aea73ece12df00bdf7f3f7c5754978b4d78bf69b774628de8bab72edaba2140f44d25ff0744debde93267aa820b7cfe1a8a89e7621174e1a7c896b5a44b40d0b07934e0ca4a05cfbbf9e00afcf921cf83fe02439a31db4751a99170250e52b869d2aa2348e5b9b7212b09f28f750a96fa5320f68bfb5faf53962f6f69729c3ae7a1f7ef07cf2f763684989d495c8069ff4c48f614ff674232749f1f5c201d5af43fff685ef70b4239921ec8d6252cd0fdf2297751b63a4ac470a2c61b74cf370feb4d86a2679ba14743510ca774d66efe56b93018656e4ce8b72a9030823a5b219cc0cbc730cffd0219a62e6a60b24b73ee797e797339810beb851d4152ee49061c1aee7d522a8ca02480b6b1fdaa32d45839bc1a2dab2ad2055d78f3430ae99774ad3dbdd7ade7293725d84ed86a0fcc1893eb44c27bdcb0df7350f1adb90e3f4cd5d37b02ad631eb66ba24dd9af9b7211f3582b0b5c3883ff56201f5e6e7c21ebc047a3cf2b3b4a17b5d79b66b6ace9c78667c8603eb8bc6e80503b8654ea80bbbd11743df14b2f65e629926708fcad2f676bfbf5c3274821e453d88464efdc5ff7fdedbe90dd22e0c9e3eca2f14fc5f968347ed470a68c18cb088d11a22c3b132b2ca7d01c27d9111c522a0f1d876c974291bbaec926162c624fd558e8ed4f762abfd92b9d1b39c826a15b6373c479c5aa830817ca9d633375de83b748de7a913b5464d99f23962d674dbc62fd5184667f1aeaaecffdbbb5743634b1ca2698504a95cba9931fe1528b02c12cb9df61d721bbd1e5bd3b23953e433a989f1ef8dc7ffb82145032530d8d471ae6a04401b983128f80f9a1b2a1762960b64860da981fbfbf75954038d6d54bf619972a7ab0eb56e0242b83884c78e01963e5a7a7d833735deaef82e9d967344cf64f3a0ecb15c0b16bb1795b4b332e0dc9b98704b59d089610fbac0ce1006ee866233d552fd79ce7d42f76fdc5ae90b0ab9fad94e893e127bae638dac8d04fa53a475b1b8322a48c9061e24926374f2b0f9447e645cf48225f16452655c708c314753ec70f534329e5f3247a48e0a037b243aca46b82c3acd363b4379f24aadaa9d16a63d9da7db73b8f702627ca4f3c06ab4fc5662990eaa2d9fef33149e9e329c44759dbe27981acad1c4a242162638ab0067f9d5d892412d3aeb6690b1567977e86da28e8c9beca02b0bd8950c10594b4be3d4fcc84e32d8f78b89a9350effd255dda22c31f718d407802a7560a47aeb6cd9d35b595fcb49b06d7bf5eedca936403542418621b2bebe29c93a40cf36d174bbf732970ece2afe824514dbe85359b791cbe1f938fb5c5036a0b6dbb5a11f791097e70cb9299f617bbfe625748d85537dda344a1606fcae0029d8222f473fb8024d4954ac9342fc1b72a701eef53113d83057aef7681161674c27165e8ce0b26ef2284d7adc6592a0ebe40f18b465a22a99d9ca13a5ae09a4bafaa7dcb93f9fc4dfc18aa5c9054ead527a7448ccbede10a3ec12fafa4418d8b066932e7c6a317925511271b8e098e2ee86ba1dd189b94bbf9d7d9155bbb8020ab12461ee723e90ecf8abafbed6518c0e913c28a1f35ee5ed4abb9b178a6d8581b38f5b3a800a2383c48275354a2d4b63aaec9678a678ebd33ba32288a177852df43b660efed6cbfb27b9d4c60cc8590c906e9e8b31506e98aab770c1b383fd28d7e7c4c1b413546d19ee6279fc65cdf6c9d355ba6c5452c588c0d2ab42b7d4cf9c5aebfd81512764d767dedb4daf8a6f1634a7bb9b354de7bf7e44cadc4974264ef303d2893bb03f7913719839eede2464cd81ef56f87f04d8ddf3a7c3b3fb189f5d4b612f7c319512ec9fdaba65c343157a84c9a705e4fdda6c76732d72f5455ea234c92b1a6bad3a4717a779a4cdba251e381a58682e148d6cd3edec3021889a3868e6969cdd3cc37c56f49fe32bb6234e1e58a55b6a7c689b594c8b78954c6ef92000e2fb29a8fb2e96a596592a21dc19537a05b14a06906042e5b4aaec83b65b4536935765e3b87c7f4ca8e67a439eff3dc1d39ba4e6aa1b95b6c77bf98b22c76f71ae27d0e54ac943451d7521f70165bffde2386122b23f4b63b6d1e8fcd6bcb5ee344550bc5e9f2627fce89a1d2ae1f12fdc5aebfd8151276b53327a6732f16103f1c52491089686bbfdd9fcd0222f7f56a6197daa7a57c762a11d0d2a0e62ce96393fda0515af90592a6a602abc4038178bab6ea3b0c1e8692c42bb43d4c9b617343b4d4fef7cdc62310b9d1dafc347a5c0b999a64b4b8a168731528f84f2e285f33913e61a3bfde3a1986ff8807a35a0223cd9e113ef31cb7190f2cbbdafa0552e849fdb91a7bd55ffef1576aec452353603e860a9b7527ea0f512dbd6ee5f330e629e161cddc3b17cedb6dbb06e2e0e8e0ec7f60d80950638f77d6094bc14bdc0b6acc5322e7eaf4ea8534d66f252590420b41d52f541b75ec37a8c79a9eeff42dae1df8e5533d4afa5bd5d823d40fab909217a837f8e6c04d884d1ca219f4bf339fb5097eea173fcfd8cd31793dc4f5e3b7c7a774c81677200399c11c7c8dbdb5541f79c7f677f15947fd191d1a03b254c87e267ff58d7e0b1ef977382eeca246c5dbe75fd22f1dd77d0d0304d91203ab07ee06f02984b263b657c8a6f834eac2d9c53141bd6c78205090b1cfa835989ee0fcb5747ff4ba0c650c5d36340de211b7d39a451aa882cc9493dbbf7583a185018f1d8c444111a76be3ded372176d0870513f66687f98ad70147790fdacc0324f8905c4110cbbc3b4cda744fbadc4809f38b4a3e4859814f187c169e2b4e8a48b44e9e6428e9fb0795225edec61d1c22b1d6954a41b1aede1388afe0343335f31f2213d2c54e667b3b6851ab4ce699ce4f60f6fcc0c7676d93b0bbce05de0ef79e924f37012e63f30b4c559d9504e4ddc4ea27cb1352991fa694b5a9ebadafdb18863a869b337721e818aab2e6a13276bcfbe3534e6a1112cbf9b2266a6b78a5b653fba6d7eae3350f8416c30ca1237e7cae0202b1709c361758e9d8d1fd0247ee6be90b688ae7a44be800df310613a185d2eddf03fc0a17f94f55ac1d1c252be5c21a046bd5653eef3e8c2d64bc2e1bb4eb94f21021f9225aa7f3b2cef2a2171b6b5b107e0c681fb044f3dbb3f6b5d35b621f878a050ef7daf81b7f918b5cf2f90f8c9610d6abb38d97112846f02486f5a7d3c62a5e6dc84bdcb3f6b80ac2308e3103949f5251d61a3edae5e7872b9757251389c6ba2a0e3b2d98d2fe93a647aef6c9959a07a04a81088a8891499646d01aa89dc4bfaa96732be8de222cc95ebb8c2b51f34e40111c0b91ce69baabdbdd78f2d41b26eeab24f5cd475bf4ad5759f0d7fe9f5f41eb685fc4aaa591dbef397b4c9d75aaf9487236ccdee62da85bd431dad78d550f325bdc378a4151901797f2aaaac2f91ee459078cb0ce0d8cd2944ec4e6a543e5175cf94737194b78febb5e401eb8880a08177b546f7b967f6924a88c5189be2f61660a7f6a89b48cbbdfb597e1ced9dd1fdf962efa7146f007e7beeb2bb375e6f7076ecd9ab5d93aac5733aeae004f45fa39087bea2f3ee032ed208874c802c198a0602204830dcc7c5d06cd3a0e4698fd49e5e9deee695c8a7d7bea48f2ac42b0adb4c59f4bbbc3ab82389a0698db9870d58b31be4dd409fc90b083d3277a28a38b3f99b599abea4af249acbabefb5f1de7a26de681cc05f8c62850bc84a51ba8bce55caeb30b698b1f026355830ef28ba14505022819ff20546570b8f0195fe5bae07a3f18d78b880f9d36f70205798b8458225ec55ee2947d1815a850388ada0edbf0bd725445f65879f45048faf9c720b1301100396575e5af3e9932b11b347696b6d62235643c8c4c697f4eee1c87e874726cba38f6391b83a442aa55e5e01e06b91c37e48bea73daa681cda139d38ce4a884ebc05c06fb9ad290b2d74f19e0a940455eb5dc4214bf1fe00598764bfd444967337598187ba5848bab5f6463b601f514ac78ebc3df98ee2fe1af9198b88ac70ae2c2ea0fc76d74a058f3f6b27967f19770ef52c9e724f98d0de977487c2393acd3b5b461d58d82f0e2e5c7d385a4a012a55a9c596fa23913524f49aa475e4ce667bebde71d52fe9e7996b59b0fcaa8f88613f070e23b97b2a28bd07a45ef5b20cee912733caf6e1396e1e1ba6f0b14fb3034eda1b9a467924c2bebf1ac4f0fa229657f525898fc897670283244051863c0e32f63ac7560221fe3379a5d036007c7390af4b6265caac5b77f3500cdd1c7f388d0bdc99a458b45f4b1e9876d8d4a30f3b083c313849087af825ddace79062190d266ce77a4a17f93bf15b5e29ac80f149eadfc228dd64671b93548be400ad57a59a8f02626015e25efb49576432059406f074b7e5269a9eaa02fc1027b01e7e43648481d74d31ed9316a18cc2e700ec01120c525f12192ca4e45880d1bf1265922f6020b0fe25ddf34e4f0155c353dc9b5e01d227a3711f57555501319ca9b0adc30bc41746ef7c8583d4b333fc32609c01090c9f25c68d9f87df20846bc3745cf0ed66196ce690fa92be309d870e7da7ad6c7c23279c6f6fd463e4f63ae0fb1abe43d3509ef3094f2d9498ba295ae810b515490118bfbd032a654a82def95b8bb112b32c38bb52df832fe948ae1da15049348f555909f5ed45329c17e8da35bea4fb8cdbcff79f23e797e2a9ce524bd69946f21b527f493f0db323cd23def1bef622a024e599bd4957b305a0aae43e6db13626374766ccdb72430f869dbdf0174700b769c5ee484f2f912fadee7968ddc783c2fa07529f00cf6f55f6c8677ab8029b8de62c4d76554266c02902800fe9c69da3008e4fa3876e2186cffa58edee6f3002f059e5c041c46e83c5f12cba731bd43a2b27ccbe0358e9fb5e0d9d370951bdde220a9161b06ef129ac2a4f062843854ffaf784e87d250eac5507efeda7625b6502d28b55535cebbc2b8fde22f739eee3bc1dc2078d2100e2aa6e65fb42c3cbeb299e8cbaa44cccfa0ef9d6f600eac7e439b815b060d938d59787fbb367452c40557603aa21b5e1e1d72ce990542b7f960b7a834edf77435004e0b666f2e4d618fe18bae0039a3137178ecfec791904d8064617519b86c6f6d3663b6d23effadb564a46ef01f0559cec26a2806d23e5ddb49a9bafd229c2c9ad05c82abc50eeca32ca47344c9428e79b0de3a9bea84a79f725fd362d1c31a7169b3abe6e1efb1d26c3bc0cdcd1892fe9b1b61fc833df94c568a4a036889095e826852f01328638de3de0ccd8ce78a4b9f1d19beb8d9145771409e0fb09cf7ee4bcc61be397ad35abaa7226482195d40d28a5aebe908d99f6dabfc4a4e8f15101c771c4714e332fe0feef93a90f57cb2a3d569defc3afc639c232cb0362d4679c76a5dac7833ef80486298e88a3b6b681de347e496fc22ebf726f33697148a65663ee6a80fbe1a949c097f4f7a795f5613708b9b39af41a6f5db5bad1e9270138e29d12cfe543862b81a2cca9e008b671a36811d500a05ae106db103c546ee3057954c69209514bf145c2b35d007ea72730263b90ef308660b8df083d327cf1740a6086f8f0565f7d72fbd66d11e663a133a8f9dcd56360a9fd846b2577781f4727304aa2148cab6c1a10bef6a8fa929effe431033e96f9bb0fc35e0ab045551d0944498063b086e8b7aa1b451f7ad523f6d539bcec7fc8736a70fe25dd86ff41c033e8400e3d5dc4cff02c3615afdef000aa459fcc40dd9450ffecf42413676ad570b811eb2801103517fd62d381be6004fbca7c1a5734bd5114b3dcf61a809e7a7f98a446e07ed1473a7a861aaf94f26aa42380a57b65a6dc82ab6300e66d6938332f8952d4ab3880fc0ba956fb4411827c755af57dcb63b6402d722b3a807eb26abf5c25182b50495830bb153329c1fb42d45ce54bfa1ca15324cf5681a268a94e6bb090eb0323740440798d3c574b810c135b1fe9bb24a7c1eb3c7e6b52dd465fd24de03fe330cff610e42a8c4f9d8aea230a4eafba7c49bf23fa8a7cc2755b2541fedc6f19abf8c51bece7fb5fd283e9eedb3e7b19e7e2ceec536092857985a1560da0ff3a11a327fb09493485846ced8d93892746106302f4b31464ea97cc1f7975a1e78d4e5733ef72cca688167c49a7c17f7e9e9de4a9ab535ceba961a43dcff8207511a0bf53ca24755ef10c323126f3c7c95dcb1b79530d3851a5ccd23567ff29168c5ef8adbb5b4d93e857b7a301eb33ea7b03cd2bada824d10be2c104b7a8ea589494dd01fa2da9f549f4f8bc0466acb22fb2fe6477aff8ad08e863f8fbff9b3704efc72fecc20ea177005c4992e6d631f1dc4d460296cfb4254df6a0ae56ffd4bcdcf419a1bb273429e5ac3b3d608e2c01174b0b3c5d175ce91e708592fcc9038372ad3ceef65a19d84fb4d4f4a54eca3efaf6b32f6e59f7542783199f3884a659b09631231367071797bfdedfd6c8d3cc99e32ff78730df0198b7d5607b178dc5b2b5ca4f2f8c3790f04de546057ac165e8e0f2b6c0d1bf6a57446ffe64ef5bd0e68cfa3b88c608c21702635e05546b76a6797903ef346bfa6524726f9f3a83f39686b5cd9ffdd898989858aa143d76edca43e16ca27e1947410c6697a1837bb74b1732bb42bb0ae1f701d7c202fd90a321c3d0f7c6f89b0ca1fb72dfaaf994be094e60f9be315f7b36a7c083060ac9eb0c718338971e7914cde94896c8eef77e626e4b0fce75e2072e381ddc3006d0ce3fce16356e85b3af53d54e364ce91ee1d01e0678e6dcb015706a863e86d608b150f3bc1fd6f13fcb12b4782b9d3f40d5db1e395d5cd4bbc0a736b5bd49899926ea499ec8f3fd8766977ef9d7e52aa9fc361cc8bc7c5097cacfb9e4f824cc4a479dbeff591957c03c32e347fac82e377e73ec0d93108eef8fdf7800d2f13743bff91f4d272821d93bf777f983bb359fab21f27a418564fcb3be2a69bfd91f5444ad90327583cf998bf340ecb0abd508419d07e97e579382177eaff85bd88a46d61f84ad703eafbbee321e26492f2ea182487267b0f5ec39c01854264c95c9aaae8ec3111d162986f79928eb4c02e0b3657a5b876317d5d2733b81a63b35ef9ae7ad1bcf331dd534614986843fcca49b88054a0489d6bf2866d637f445149aa22a77c8810157ebcbbf29a5178650f5be8043a9c811bb0d2e7f5e2f19871eef9bd4e8a40d1e0fbda8c89fbc70fe0a5bf99b87ad18d0ac36382432ecd81606c2103b9e222d54b82a9044c0863db8e7c9d2a620d6a428d247c6f4a054caa33fc9423f60322b6bda6437620c9ffeb9a093e67c311314319e1822a246045904fb993032be2fb9a99cdea8f99a73e292616017aec991a0282f673c7569d603cb9606e2b587705a4b48f6e9e7e622b316581f023e15a1e4ca9380f3a3a396fa05924bbbfce35f0d6af62dd68c8941ff5b35bea2256c680fcbfc625dc50a0a1f7385f54bf3bf58cfd5566a9dad7f0ce1aa02f8eb786681719903d53d686bb7ea41fdb5f83691f4b36f76d523461fe985b903e6fee08a445cb176b37334736636f330b134b2b730bbec376560f560fd93ed2f61e652154a66770a171861e4d5a2665434624d6f6866f2f7615c867e504e60f0266237b679e1e1bdc642c75be507f93d90001eee89c3cb9f2b3391567e5f4315dc3b5cb606dd77df418f68fe62ae9e10c2951d169cc20a66bf0bc660010fae0e2db8e457086bacc2dd84d0c90cbaccf7bd7ce56870061938106474d80c087904c2e7436cd0f83d3e2e558315eeaecbb773e7b4df45a3e6234441867fa00cf90fd46015fd434c5ec3f8c3729850fb10d736bf5c0d5caab36f2f6ad07a143a1332c47fcbf907997ea33ecb8faa2dffa7f9eb673758c8f81b48ffa3d981d060f25f55e3f772eb3fd56b8d37cd8033effe03357e212d4d09e921235090ea88bfafe14bffb6ffb0af687fd3a29baabc95522dbb4c5afe0f8f8d6dbcadc54d746ba87c60eb5cf5db56916304a7ffddb42098fe63d3d28f16f86128d43df14935b6c9a1a2a564c04d5a9dfe7049f9a3ecd0177e7e19213f72d342cec669b4465df96c256be40d9f1be9ffc7de5b4065b92efdc30fddd2dd2948777777488774b774498b9202028274494b0b82024a87d29292d2080848487ceb9cb3ffefcb75ce56debd39fbfbdeffb7ceb3366b2f19eefbb9ef2be69af9cd6f66ee59e8e2febcd6072d12fc832af7caff382dff3e04fedf548eecefcf7d597cf04ec9a4afe1af755a608b10b6a90130ac5ea0c2819b8767dc714ff0f16b29da8f0bc218afffc83b5ea7d4aebb3f16fbe7fa43f7ccef1f6c25733d724656e59c552afef418fd8fd6f8b553715d6383df8b50c0084060282030c810e8564836c297d69f4462a1d321d02610281775ba6cb38ac8f47b9825d97467742e95e386fd51f4babf3bd6b0bdddb1c537b21198727bfec83177dd8cfd0bc07cff1de31f5995d7ada83fde1206bcff1eacc9dd12ed90da932262cd607ee51cfb2f97faea144a0ac7d31d3436846cb97eac4487cdddf02cceaf7ef79ba402dec184fc91e7f91346ddcf7729cda4697f84e5fe1f990f89c3db87e8783930f5fda572cdfdb9386aa81894e250bffb54b02c4401cad72e98ec6fa7f6567fc858a1e52b659b0efd6f76001c1a9ebc287b65abe9ef6a500a7bc5f9c65fe9d65f45e85aa14723f0c634a28e2b5142b15483c251f205dd2141ad1b41a1c45f6bf29f36127c861041ac1c98ceff61fff50b05ad271ebbfcdd1be41899881d75edfa070d7da0f92fa0a13f3a4c371f9e57d7195d7e0faba0fbac9877a3eee95539fe7ccb8719a4c46e92f01f9d2cef1dda1cce9d011a911152c31c3b8118bff217cdcab7e502b73f08b201711303178ff7c7163b8f480d9f274a3e41a5c6538d07f2d7f597dd5f55a631f98a10e8d771e7172d6e1ea90034acfc17d20a17f88b748b87a877b7b88d1634a55d001af1fb0397efc513d0ca2b7c9fbf1dc36494f6dad70334e7c1854364ef9ae6fc119451bad970258bc773a380c1c26d452e90be923276605c280df5e5c993aaf670e0fdee178f9113c91b93b43ef413f36598d055f065fa7c55ae3d0b675f58cac4cae8e52ee64e31537ae2a702d0f01a3e9212d7c97bd163385adf5713154c65326aff7855be6880346aeae8a5cdefdb9f877c19816d6062a67d557e7472e9b28be2f8d85d2c6e3a25bce451188f334093cad7a942b38c1441e6e3897d75918a4b96e35407d2a09bd87ff4e4df336ab2a1dc347845d363949201f4c0ebd6e09ba78679e2d0135e55eedc89c1d089980a8cbfa822975eba9eee87d06aa5574d8ff7d274ab9e0009d17af024eea23a2ba8dd2f90cc1d9972c649f06f0134b20654a830a2d6974b9d8ac9114dcd4ae1bb8c1f8faeca0547ea4f3217c573464b83079fd5c0889a6c970128f9aae577ca002f6e83344d8134b730190326e953e068882229844f4475370f53fe845938eebda936bd04cc1f8100fd176aa77d2513b22e248bef6543cef56019ecd4b95c44153b21abe28a2f95fcfe793e4c4e6680756dfbbd2db415f36271a35721f27ec239012f7609b0ffc4a87ebc691134803b1fcbb27fbd47491689c4bd7a55de2253c6a047b6fd68ef21797fcfb17c94fefe01a028a007dc3e918b6514a72c570a6157e597753e1d071086ee6c92ac0abd507f229128a67dc108f60da945daabf2e0a4d8f52e07b448472e047b68e6a8f2b65133a0dbd98824454e5d8d95a1ca3d54dbd3019e30aa7c34204a904754d020856aff5175f6b836ef643d89da20b8e4aabc5d8ac4964260fd79fce9fe1318ff362a2d3f5fc0c33b5d601528f8fed5b2ef0872a2bdb4f1a38f5c1c78fe871728efea5d605e5f787e9c111660711fcb7f5774556e96ea8e331d875dba9c17c3753fa83c6ed4dd5bfcaadcb957ce5cebc10986610cfd57f5a065cc86f17740b6e23d54a30e7be6586c2b95b61a470426ff883a3ae0f93711e209d159e62d34b5e6d6028aa85717d0ac80fde9bb3751b4be0f6bbf52b5f4741846825bf163b10c6025dd8b68c5a2bcfdc2f7f9c8592e2ac5a32a385ba07e86a9c8a0841602cdd6bb2ff758182ad85f1acda1036922aef96f6a66567244d22b857c70852b0c579cd10e81f97f3117a88cfd54fe58e3ddf2bc8764e669c044ff5579ca404a185af9d3f862afed26cecfa56ff30ddf585f951b67ee1311207dd210c77927e328a69899b1ec0dbc5f5f87aa8d46aeecb28d8f78fdbbc9bd7b2b1d94c0f8145544bf4a2d961c24cfb05f7c04af8aa2719712e05e582b051605bfd85a7980813f0063e19167881b04d0303fae052b7be9407d3d38ef1dc433e6cc2296abc405f4c75ed9e5189600b4de7c9d0625fdf21e57af5bdd55b9bb8ded7488b074ed7ded880b88a8c3e04a6512d049eef88999b5a81dee0444e42235f55377d53d0c5e403f0c199b65523cbfa52e9fab3b38db9cb4e284ca9c7455ae2a1ae6951d47b572faa85ab5af40f260732f1a483340b2834359be4cc52bf69735a9bfe45577bd5708e85fe65729bdd0db2777510c4d2e194fdf078d94ed02e72bc902fc32839df3a76c3c37a71065ca3c47df0040ff784fbac22bf62c98a4ba2921275d922e290cb82c5d95fb0f1eb043556e4a1d857c7b7ef49d2180b35804d0afe125d6d4872f36431d57aa6f4f328f05669ed9755e953f8b59ad877870e5e168e7acabda1357bfb5ef43ba2a47c3f708d7a017ec3924cd642625af412f0b8c0522f3f5b9dd132d6f3ae3529138fad9aa0308952c6e01a9aeeb53eb6f1a055f3fe8d2a65ac1ade29d67f30a05e697b25521ef3b497d19239eb350cb8ed7372cfb18e07cc7bf63391ae8a0c544522fe30f29a54a9966f302c6cf479942d1cc8bae9efc3e7579e57dd4f6f946dec9abf2a54d52b3ad313d94289e876cf98717506ebe49807e65651cd610faa8daf1caece4b56b819c5f7347e903c0d92b6f44e726331f271893c9932bb00a9c19ca04ec4cf72a6c8220eb3b2df9a51a7d82771faf2ce3f9bc03ec17c901c47bf8c52f7789e7f46b0ae88319acc8004749c29dc72caa877a5c2d750a26378a3b19968d28f7aa3c31b81ed77829cc2a95f15b63733db5780bb33380407225679acbe3e045269d4bc1642ab2a6b6f4ebab03fa615d315f7c6f39ae4e9af82014f6580fe1b125e0ba98c7e4d8de6f93e87cc45d7142f2d8840109bf0348934908b54067e7e9ff10faeded4b24e72c150cf83220cd4b13621f291e235af9f5bbf201e194427c998601c02c0998b6d9d2d6afdb7179ed1b319c614c96855f028450487d3c457bd11725ac5698f4c33fb516c84ed10269260f223cee697f1d74d9fe3ae0b6798958eb5f05577b55de4525af93db53f5485565a9e1f6b8f43b8b5134201b5cbf75e04d26fc68925941beea50ada99874f867a044779a49099cee9adba51431335250df6a2eb23a34d00a2164dafffc76e5c959004d9353b5f95bdb6dbd3cc055ba7f50de53a13e89adba8fa5e394c0fb38ca6c1770edf6cbcf083552140e34c35fb9d7a1be2dcfc749079a53d1c6497a7ffad61aed60bab7c3f59efb39f58f51c0154ae512f1ff2295274c75c855a245ef436a70a108d86fcd789fad95b5c97e6c2d576d2206fbcca17e3c020215eb39ae9b2bbea7a87dfc873f8a197bbac2cb44d101fb4975eb62aedd7ec412772315fbc58b710c7961c0be4b876e77fef4e53189bc8c97d62d8a6762db746d80fd35dace3d232bdc5ae204e352110f8948a463371c03ce5fcfb1e984878d275809419b6346318fc54fd959aeca87072bdab74ffa48fc864ee8dfa2ebbb32bcc001fc079d54aa379dfbab6d44e8cb187a044632de36bbfbbff59ee951ffdbcf7f7acffcdd675339b112097f52355b9f9f6d2add02898d75890c3ef971f7b7941dd44eeedd2fbaf314b1f1462f7d9c9eaaee9c55a17cf562c7b10cb95d322f505aef08a0d67f45ef19278508c2f5171dc5e647c11665453eead36ba1a7b2e38a6a9da80374b129ed0a50888144aaca54f15f9e6c7b74cc39094b24c5be8f1f0c0a83ac9adf8bbb73bf8ff82f1ebfebdeffbaf1bdee01fec8f8ff89f5292e8f61a0478e4efcae2db5fec3526ee73416ef65c5d48cf881a23211d9a653ef9c6d45a78084518afbb9f5c8c5adc1cb9eaa30758b019da629b522e47bfc0b970237ee3df30f8ca147fd2fc018a0912adc1cb8a278a94ab4655c0e5be48ecade0094f58b73a4e4ea3b3c6d66bbdf8a9b5b87dc93f5dedb023e945e4058d68457b4b4cde0d3686a7c0c0be96e6ec086223499f038f91106333241b084d75f365af116b0d1d7f7f1153b5aea5f50f36d6030f41fc5b4ee34002c0d99e93b524a2d8289744fa19ea818a20e55ec9803691b4664cc14293b4b9d552d8b82634dc1bbe934ae00c6701b6bf66e5e0f72ccfd07551b05e8af9cfb891fef008071bc7fd3de13fe6efa7cca319295425f0581717ec0465333f7be33237aaf76a1ad29526df791a2fd73c0066a2f0b5e9c7d133d8865c858e1fe193191ad0ccb01d0f1e8a50ab1fd5c6f69efaa6375e39144cf4fd702a9469fec3fc0d7e4ab2e482a2b0f3bbf9c90cc696202cec00b62573b0b9aba3c43e4e02698e64b622c49679eabf2ed6e17214c0d49ca27dc427ad6878a5a9a67930046e0fcb5ce2c77b7284272c250d3acf3558010b53990ca24b83f8e86b5f7247acaffed6b21a82c16ba578e40c5a2d191a233e1b711253a7e1e73ca410fbefb90f603fc6b59a737f03f9c657a0e4acc3fdecd0a571b33eeeb023002936a877c28d5a0de7bc9afc3365de6dee81c0018d48420553d969f68626ac487d5e266725533fa22c0868bf944960e65cc2d5bdec77ff661153976839f0d48b52d1eaa50e376763eecb5ba4799007b6ea01f1d06e80b6732ef4bb6d6558dee05db7403e320ec3efa7900b20cd325a83c54752d0e36873e4cf5ec22d8971006f459d670cbab073851e95cde2efcce141fd0ece13a80520150e9c4f5f577304269f62570d09589f3a6883580f3cc977c13a224f2d084b10df98e313151ca9bbc2560ff15d48a47faa7b8986616895128f34756ae54f903efdf148ab2ef9ef14de194efbcab1da1fa13397cd8d58c0408afdad74ef6fdb6c7e6f754a905eefe60acc16f0152f5c7e12d69fb3f4fab6a5be70f8de6c45370c6b201018d3ce6833cbd0ae99e2262059ab812aef0dab05ec0c64b9424e4789a8e4c14c61b39f15e734f82ed901b987fa4024829568c972fef59443c054a22794dd004d09caf1517a1ec61e9527cd652e6b7fabe473bea9e60290a62323adf45185785cec235fd28f2cba1b6e00920175dd6a5db40057716d7991249f3e5fbd1f0cb6a2780e99779375922b0e0ce5bb7f81c245c9fc0f6b50524c0063ab455f7d272fb4e1b76a9108c15f5bc0b41f11ee063209ebd3fee0c5e59e3c1a4a587fa84a1f8e2b92a6063199358d57e6991c2a489605e1babc25140b1f80ae8dfb4f18c69cf4a0753c8ab670745c67744e88546001f83bf8959fcfefa4b69c257f72eedf7d89c0e13b8815e681cb34107cba5a46e84fb2699bd317e86158a5fb0aeca3d69dc21be5a4c26726e63af9f937fa82b1e8c03300a3b0148daa33b8aac2b15bc26c8aa4fe1313ae080e38649a8172a30c4dbde9dd63d49d5434f63be35184885dd9e5018b5ce542fb7c29bcf7d267fe678b73870f7aa3c09138740226aa8552e16457f574a7c3d409c00b0b70cc2a46ff974a2f12e763f75768daacd889a1205d2f27a1e496d737ad306eab18f32553e8ae03046fb0160acaafdd9757ef1c6c171b409793b9982cf67bac580fd217ff0a89cfcfe78f7f3e73bc7823bf87ba59b715a0046de153a373293b43129185b8d499e3687d9ec5f7e55be62186b7be724534786cb26b021178ddfd79bc80660017cc42db1d31cefe02fc75eb0f8f44e761ccd0de87587424d72ba2c2ad9201216b60503ef0911ec3f05c235df3e308bf8493a43d6d678c276b7b831e744928054ee538c9ad8c4708f55aa54e3c32fcdee9d54592211000681e73f2a750893623c89cc6e52737e10196b3c74556ebfd2a19422ac87ed7bbb4896ab8d56ea117105303fa23af11c8adb8e9ba89f06681949c62567a44f001fbb71fa959a6b4d359705a15ea41da3e5b7b6efec00c6a981a7f146c442de786412c2f9e085b2f1da692290ca3e8bb79430a04a25fd354411ba3d2aa833b9fe25d01e84df9b0c8702f7f157a76799a4fc743288fb7443800f9c8a501f86097936376bf5f228886bdba5cdf10818bf4f43db2d9c310d4f8e899e464b10985fbe6f70074a2d6cd1ac07c8cb5170cfeab6c32fd37c688d34200230a4596959261421f94f9792320b2b476e2e962330c0fd1b7673b70d3109d18d5a371fd5ecbe9fe6bd47ad01ec2f267266a9f844536935fe20b9c7887e4fe98600164c3b55ecdbd947e302e2c598b13aac4e2a6121d5407e13c708836a7b9fb8f4f47ce6f46d4bb50e94f15000c3a1e5e8bb847bb1de08c93e8d8838b713510e7303529dc59fa7e6b411a9ee4f48b49695ae273e5717a300ec177fbb79ae17cdc93dafcb3b438e1337438846998084d2e1cf3db7a02648c5e6c8a6559eb51d0e31aa5200189cada6098a452aecbd3aaa187213d4c1ea1753340086b5a8152ed39bf17eb0f25392c63482f5b426ae1570be8defbf5c9e65d7921dee9513518aa634fa62781b589f15d5bd877155e4b9ec7623dbc2d0956f1fc17f0152cd99c758e9a573d735fae947ce9d1e362c85bd4007522a1b8843bda7f5f5096f07ec6f651220480dc33702f33fdc2454be7c7780fe47505eeff81e55677a6d38601f2d87ca2cf75c4ebc67a747ca6452b57db199c603b094a2cd72dc50189969b73308d3a8df96d93127f70018e7831f2ecdb3b41ea824acd0382df7515170521400fdbc1c1fed522cf45a81347ea658879f7c34f3420260f058a791e4a97e91329d48c0d559457f9eb0f1e31428d5912f5892ad5f6e8aa4f0dcca0be3104a3d5cb60cc04006110b96397d06f075a29fd63b1296f01c563901a57a10c55a55c8782ec402dee2ff20c8c2b1733c9105626c25d3fe871931a75f8de61f0510e6b39a604bda0118be4eff43e50c5f99be9891f432a1149a56546a27e07ce5cf7716170d19af223a4877fcc19924423e269a05380b78456e05abaf8f5fabfb7d7aa6b14787ffe60510a3e91cbf47db5fa715e75f8c336684ab34841541260fd8376505e12a6f1f9e44848eb12c57adcdabcdf513419f40bbfe56e5d054a911dae29fdb3e8df54a1f2c8f9b6e88185f160920132194c6c928ddd045fdff7bdba7eb883c16c7334680df22d577f248bdacda6e33c83f63acc1c2b9d7e101b0ee30c97e887ce6255e389a3a8db0471232c9c2caa2b8210ef04b1ffb8f3cdb9ff9f256e813d8a12b7d59dbff5fe8cbfaf19aeb619121f0a83718b41ba65b5c0bac5dc75cfa439dbc7faf52e0d57ffc89bead7f881df727fab616df903d272ee76b49568a4f6622b856ede38d2a94bd6ef015132182b2c9d1dad2f8c3c1eb06154ea1a026a7a586edc928ed408277397e5a912fe97c3048c5c351ec6c471c2448fe525ceee70b404a975a333963e3c94faf44c627ac545568bd21b0766d5fd2df5d33ff3edebbc91fe15d09876e71c0895724b7f79025405bf0b948c7553fb9e6fed7a55389fde9f1f96dfcff3591fa4a37ba0f11d094d566b34190bfa741071d2bfda31fddcddab3fee3b91e66ff221f9a92fc0b42cd9200d3ddc94bacf56f1783ae106d202f667eb28262eff56d427626a16787e872e11893ca9180c7c47190108afc8544230cf6e3238fd3c3ec470d3ea5fce868c9ce5ac6ccfecd7338e4c68fc46beec2efccbeb020dc97b0915a79a6709d22e4c7c6f772c8a3d112eeeeec7171c817fa4a09bf852ad8429f4b47df48e25d3e7cc38dfe9f7ce8ffdbef7bb35b8450366a7f3776159dc4d69f6e9b4537e5b7e3fece5ac892232e426dd47d1e1cff806f24a564951eb136a31aafeb8e1ee3b026abf95007366dc7c3f5487afba724e8c49e9f1325c85a38d6325f318eee2e5b101de84e1b0d3c7b475355b81ef8c59274e13e0f2e5695e2321942a49a3b736bf64a61099a49609a9761ddae4b9164110ee49a93e2b83a02119b37893bc72d94527baa21df62b9ecf6afaff048baa1f262042bd3bf7598149debe016f3f05891cedf459862e360f9cde663ff091df81f418f6ed3bf20e841832187628a239fa913a18cb750194796232e0538457da325ed8ff6e745c94261b3104c2476151e0e02f5477f881d53b99164ee6676350ca706c0f4266f6301c48b886d3f023379b35c3c215a0fc4d571e2d50403a04c7ed94eb1514c62f367f61dfd58cecf84f86eb9fea5007162ea3bb7c5d7e8849d368e979df369d693ea1840fd30ffa89768826a8fe698ab5c1322b282c552bb29800a178958f63483ddecc94a5e176cdc7341fc351d8e8b57e5b895ccb0741e1f5c8f124334b72e62edfc729903aeca3fdbc02e7749111dcded39f81b1012b04668a7014e73f80f6b2ccf81456a75e538399f86a744b6d56c800a4e8413d8be35e0cc87121d11c2a560ab44c4450f106fb644d857cc5733957cba37044fe83ca4ea246480a092b58b14f61cc6806a2ff4b9510e73a638fcad0ca053cbb8631b9c5ad48ff395acee0684d3c84aa7b8710034b92f510337e3613e326941cd311a1365b690670a04653cdc5468d52058f7926d7aa86f13c757fa542402c41c8b109da1697adf10475cf6626a6eefda2c5714a082b512d1d72c5d6e1de659f8e05308b357362d4419081ae90d0c9cc91f0527452e430b052522bd27f6b404887186ce9c85e6ea2c3ade2d4a71885603be0813a60031b45c3640f338fa1c9fc5f000c69192ce650b3e19a853c4c3bc805ec37f8a5de05e6c07bb3363eceae30b149d91fe821076f155f5b66f7f3c7e0b91d3b2594716608b7599c6b3ac53130e162d6d51054b3dad99f8209e7c558e4132f9d0f536d4f3f7727ea71b9c9f9ae68ca500e2c9670b321221fd458aa08dfce10e36341cfa867420358ec76fedebb0e89a8e441a69cfd9848d34b24503509fd850265b5296c4ece15835debe1ab22c5cbac136a00450c25ea38749b0b5d07b166474b4bb2b9bc85e00f52d43d9c6a23ba523717df6c86d96ad42b24809bb0013e3dd21451bebbc8973f29738492b8c37ac349353c011df967a32b457d2c6e6728bd0a8f7c35e6594061f401cd10eeaa8e8ba9011657899d167537ee6101e950a1cf1122749e747fbd94785bcf1f61f7998097ce82280fa996e23fada3d8547e528795cd5fbe5f3e939b78d81a05affebf3c8d7fe5fb511506857ec52821cb0a0e9006268bfa9753327cdfa4706c945d4cdc8865ae1847cc00176866e31ab92690c3f66731729b04e20f4815919bf2a9f5397831ca7514e5baafb5b8720bf7839205901cccf2a65f7e12d8c7772287053b0d6fd1514144d93c0fe716225c078bdb338cbe8041d842932d85fe69901046deee3be5fb8dba7c9d576e88da093f7218b94961d98dff19a2fd81e8f31be25b0c513d9285649c999806973fd21853ad60a9999d81b082c8b96fde817fe130009025139207b8a5f750bb939c7a13d35b8a2498217203e36ee74bc246a0987ebf6218afd44f820a30cce05a8efba8e9e56388faf98e5c2ec34d27dcfd67235a510203e77b77ea65072461cca9d301ce4558a9a81228701404f6c248efb7cd58dbd048df6bc3c52dc8921c4ba607dd2a77dda70d5815406fa0de78dc846da0df76281923cdf95dc8d5a3b16532663e342f0472993a1451f01f5cb75fc1a5fde5ddafe2efd5918d11347f1d99c081410142f2bba952953264d126928032bc429d74cdd9c0f64317df9f623bd88c40cf549a4d0da87e090c850813a00349d84412066c882be78d7298a32841d31e92df31500e52fca1f953b240db96fe733caf5c2175e0a46be04bedf232f62b516ae73b4e6e91d647e7ed588ef75dc80af6d4e9b57069fc4bd992bd425d169503dc65ea30ba44e6aea5fd6bec853cbacc1b3829b92adb009445701f453ed83274e426f3903c676de0f8fcc69d020b49f00fbfb763a49839186148b0ac7bc024d0dca52e307ab3cc01e972a54618e30befda15475e433615144d8311a201fbb38485e0ed4c05df5dc459a0a67c086d9770488d14bac9fe882ee1feb8d64072fb33b2961530bb40341012a91d5d4787529c1f7f17a6f28c43ab186a7fd00fd1d4dc14f56b3b2fc8e8c6ce6b51836c4d8b2f90980150c63b9d5737d5c408de611d9e415665c852d450412139ad88a9172a39527ef99e8e0bfe44141ebc0ad04ea338ff6db4ec8b0d4ef11e33c5da1a9eee524d26e034815ba14ec6f88a9aafda23d3b159bb3228f645eb302fa67e955ed4050c585e396a9af24546d0ef38191c62d8030f39e30cb622fb027d2c4d7dc1d5bf398c4f22b603f70d5d5425ca70d1bb8c24e4790de527fc77e6c0a90bc9cc8687da0a6040ec33cef288a43e10d17fa9e9c5e953f9fd3e78521b03db4bd9b436d725e118d2a1a029893c9f54bc41229444332b1119b6f5c18e18ce1e4002cc6db418b4d4587769eee50932bf62b7439bb340c60bfe9397b9e853b182492eab9a69b937765c31a3d05820a8271f859cebae4567c14f35dabfc6383a481872e003093e1599d9a2df2c335b829f6c331010566462940fc53225ff1b6b8b4191ff35b69b73edf167eb4bc0278cc3d7c911c29ef30c7fc30733ee6dd7f23f2cd3a18b03fa937996d0a1c990a4939df3f468a9aa2323026041203bed69e967877497fc9ef6a8e0b37dc2782231e0700d0cf12415eb7a44cbf9fc0d5b2509fcd40ab2fbe0040f999bb0c9de4dce68759e1a423f573798a0c5b88800f099f0bf3267be80e29d9508d4a4722cb00e2e20970be374036b19832b59fe9cddf53a33e62888a10b4048887648f4bbf7db43048666f6546e433161d9bf49e04820a9f8a7ca1363cc4c5166919c876031b65f283c781f593dcaa6db60719d15b445b43d07dd54fdcb7d40c10df5b62c4b712111f9b3e5dd735493e5d78706a940dd8f71f4574bfb138ed93e3a5b850f3a5da52a9d6d803f6b1223b4b849abc4ae87b69a92789cf1c3284e79c0062b8923883980266060bdb594367b6dd8bb374524f20287b0897ed6c5c71348863680af74c726756f892edc35539df474f3e0b74d4fd656a7a12b623a154a42906a03ef9c34ac62d18add7d5b49ac58534758d1edf67dc81a0ccd74b8b2f2bbe3d6c88b325635a1c2c580a6834e3d02728f1bf053df4f81a51c20686aa8d6e86df5e97590a7f8d8b86781dace18e8c23407d22498083b8d14e836977f4ca07f5695f4a01164e1b218a1cb3e1e840634476401cf5bdf381bd7416229cdf43d0a810e17f302af5233bbca0afea81664316a07056fe1620979ffa4d84e02edf1a2a6b42d4a4c191b61f1121c31d52911d417be63fe07623de0c9581ba213e0e7dcdf530d7e1eb413783f761af258646b063dcc256b11351d6e931cd9f659cbf2f151f86de8370472f6d2d6406a167d65d7771dab7db01bafaf994b46fdaa09b9eb3f844da50906b919dd41d7493c7be37242e8abf6a5eeba0c187abfa167c54bff30135a2e28b7cd3f8e80315d4e41e8538574449230471d597419599f9c7ee9e5e79ac97fc5dedf5c65215e90564692c3031410d2437fc7ea90adfc908f26826450aae8c8ffa94034e7b4e514c5a43ab50c56a45be64fd031e773a47b39559cb2490de1b11b8b46df40854965b4e1d558751edf64e0dd7eaddf4fd6da699e953fa1aa847cfdf102b6ee53477903dc7888ba6d33f0b82c3d9e4dd96e37ad06efe388ec37336019937c145cbf06264e4409f8622d642e3d12be761e6ad9bbebfcc12662094eaba9ee78b714477863b08835da5dd21d0b7a502d1bf079d3e9b79807ff42efe48b75bc1ba1986799eec32ee84e8a5a32cfe14b14d72adbd3274a7fe4ddf3fe7899fbb36492beb4926538aac5a54f42df87cce43bf578bd025c1ce5e2c584cf6a9dc468381efe1acc886ba63ab6a0972145e60d64c8af7e6aca64594bca7a9bde9f72fe559b1dcfa6227406d4650ad7b1ffab6fced7c8942aeb72913a18e336f5a22677539c3bc29dcf8158bca2de8aa683c751fafa339de4a6a20ae7c8a89052b472978d3f177d11b7be95cdef2c8535c2e12a7e98df6364593a9f8fa766c2ce3f65d477f79b25b8ff1e4c64ce69a47a65fd7ad056da8f1e223189d25dab4e389a3bae4258f9cdcf0fb01a20f0d11060ce7d7539633becc6e9ec24bd3acb64e80487b9b8d7b011dc32275a76c297c3adb957bb53f16203a681de4b4746a7dd4dae630a6e6cbf9bca6eaa10cf8443a3d7690934b3fda3ec3138bbd9cf8b789940d00d12402f578d6283db64a4cf8dcc94ef6eec1387e0160f3274139f194d7cee3e2737f1849bd1358a082f706d0ef490a33ee72785cb5c3deed7ae1822a0e6dbe6b804fff6c7df8913b1fffba5d105c611d89174a8bd723203e9566652116861149fc4e207d406d7c5ce42e524c3d208f3efda82e5a33b694a5a7d16d12fbcc40b813200aa775dc7e82bd7dd1a564c8e079164a34a8e10e0d9c9fe9631027e24b6fa3725ddb9d092226094dc30cc0a77a21c8992e5a71fbed9cff2be60c080ae2d9e36400736ac9fd1e75526e3223f4836407d6edd6ab535a1cc0a7eb52e86d68cd133b0c1f44ecd320b518d61277056cb28f58b35c9ff402e02f9f1d1b313e0d9d1c3b9a047c9a21d4505149b80538018b66b7682d98e0836058c0673bc025db3c254e8f984de8a61120a87abd134d0060a287e581bc216b857e97b929362ab73f66f57b130336f7f77591ea4796315f16d7434e321879a3a8923270ffc8fafc6ff053eac173b2c899a29ff566d622b6f6a7c3ee10f4f949f851e8b1b4389f538fd6cfba03dc457b45743c8572f1e62772f51596a929a4acc39f594f84a94d8aaa5c77d07e763d13e26e9709e9a2c8cf7aeb7d0e15f66557c7f37cf713ab04da7d9e85bb7fc71cbdf3f70b5dabdc75e13cf8fe36a4ff27efcfafe83ca2deccf6e427412d28986c362c86cde6ba9ff525d013754427f2e08dfb59f8b78301cdc407aa2db0a0f4f7e5f1babd2c655801d63fabf221f8b8ec95798dd746f54fe4fef214a115afe9c27f167d579aa9af63d99122fb59451ff656e30a9e5d19f5da9fc8d1243ab67f6cc49a304120add0238fa02c50cf85f08db4ff619aab9905b5429fc0b1ff373d098ee69fe94939536ff7050543ba82151d435e64aa1d94496130de4c514389dd3494ffbffb037b0d1300862626e812f966111ee8ff937a583e2def6380febca099217a3697e7861313b6571a01b4761c934baac2226259daf7536355f3643fc5667f3bfee7a3bf504ff01e5ae95d73fd72fec976cb0dc35fc004a06d6fa00aa9c8ec2a22a9bc2b9d3355fe226031f5ef94ffcb8766e366d3f087bff0fa0f8d298a1e80c90cc47fe2e1f7269c4521a478d03239be9f19c79cfefbbffc377fb15e83648f837baf3774e902239a33642d421225fcdfbd01017489959c5dfef4e4a3fd93779087aed81158a7999035c80d15502bf40954ca43835bdce1e2aaff45b2830a537fa20decc0579172960f25567d24e48df152059fb209f1e0995e7367a71b7133c4605edcf0fb21370361203837fc4eaa6bc61e9e28e8faa9a90a0a8565860bc51e9b6a84cad75f869af9d505560eec05370cfe43dd505d2dc32eebbfbcfa8bebaa7cfde11611ffc40810708da77eca54af99ddc7f7929e83a2cdd3befca7e751829c631fa789d4ecaf15aea6c64d9655eb0934735018fcad6d8946681a7ffe21b69b4dc8b5851857201008c66fffff3f3fff150d864020c0194738c3575161061324894a81ec948aa67f2807850f375fe5715244eea5f4f5a9b5d8488dcd185bca1c9ad0a543e0378751c8e2ac1d046ec13efc05cac3dc7d29b8f476fe514c7361e47a4f6d56184a0f7b5123faf21e554d5a030c1ec32e4325c9a433a4af245ecc869cab5d0c533f4c429b156c87f588247b88a947d6383276323ef5425209192f59a94b443eeeaf1434222039da98d456f25a63864e6e411349da3b1da605667ab32c049a4ce613eb31e6573a893dcfc275aef9f378420cccdf5e0002a1040a2ca05672aba4ab27237b7bdc97949c7bef97384287f4cf6562db30564f8bb583ba5279fbcbf2867a5f3d615c04b820cb49102b27ca40626ccde840f652be191efe11fe9fda38751f1d3eb711f5fd8ee817843c85a940f1cc85ae13c3e721b25c458b2b46f24140346ab9a7c7696dfef328742eaad05c4d343b7f3049dc55f9297dc9ed033d454f58765f7a72c11f4c8b54a6cff432991fae3066a3c6207d58c0eeb618ca7cb1acc9242bf926ca835f6d41eaaee635c3198c8520256f6e667879f9755438123227f5d49f94ec7bdbddce07368a270701c7b13752faff21e4fddf7fdf9bdd2244c46f36dbc0740a0d7efb70710d49ed3ee2787baec0c3d50011e471e8e05cab2f5c1f3dcbbb04105dad1b3aaacf3b9965b30458bea89c791123d3c42cf8a496e093ac4bb43762ca5ac7ca6b0b6cb82a922997e50c453e0de9173b78cedca96653b825877822bce8f87cecfd92cf1bcc2e5b429d4b448950e226297f7e942977813b7ed74077bfd8aef32f98b7d596fb49e51ecb68efbed14b2bdd970682b776ccf6978d7eb54cbe52fa8978d1512e4ed1d440c6a884d3cc3c71956e9068fedc2dafb78975cf2b83ecc35f1a797da5bf24d8a39abc680cc485d6e37f3e58e1d9dd49fd895f22ff19d275e679b06a458fbb623946c1f3f34159e3afe5c38736e25f503487dd6f4b6ffe18177040ffcf76fdcf76bdc9769d2e19c60aa3993b284075ebffe2820f67987cd125ca42dd8bea692578ba7d2cca74e1ceff0ef9b5e8d4b786fdf1d8415aec08a2be04c1dba8f2d16333c71ee182247a97b2a5a90c30040c8b2723b769fcec43e68eb40afb6e97a07b3c7f6566b6e4872df2bc44406a6dcf9c85a35532e1620f3d7a8577424eb67507b5727444d6bdf2cf6fd784842099303956d14c3846e4c7ad96d648101d001994c37036427565376744efe6862a9a51527fda2100d807091bb2682c520fd47124bbb6e4deb3c6b441bf2aebd5e6c068cabfeb956ee2e24e9c300963339fa878c6b74073b4c63e2e79ebc575db159a51ca246aaccd40ad48fe492e52df687764b8856fd8c3a186c54cf872d36aecff6cd7ff6cd79b6cd7b83bca1852022fa153493de15f46d9476f616cd545f3a6488ebe4567ebb0bcf073e42f0cef3d83dd7569126c2874f158630a84b970ee81cb0ce03a8ffd186fc24522aca44cfd69cd254de4f8099a6113635ee4439b61b7cd58e255ceb1857c29db702b3a98e645f63058fa1d2159cdc930ab480e08b76559025964be10aa61159bcb35863cf4091ce63f105c3259cc4638d80606bcbe6bdee9ba58fc350e16ec35b950b0f3ff9b10deaa435407ae2e6a992aaa4cc320dd0034cb24edf35ffdbd8df23dd6bff689ae43c0a129fe62c421e886d7df61f3b6e03535e3e2e2323535e5e6e2b0e43065b3b2e0e6b232e5e2f8dbafd9cdf82df8b94dcd38b9cdffd4f804dd1091b92e574f82cd9b9783df828787cd92dd9c8dc392cb928b9d9f978bdb92879dd782878bd38a8f9fd7949b938f8797d3dcdc8a9d8d9bdd928d9fd3ccccca949bcf8ccd949d83cdec460be097bd9f6c24e232affefb53a994428926cb1df431d5fc3bb5e28d93fa3586bfba1ec1600017f154c20c52a8d6e766fcc0d26684139dbcc69b0b916e9996204618111bb3d01ee6748d95af056ad22743e77ccae274b44c6af9ddbb3d0783e38508616411c2c4eb1056366fbebfcd350f071b9b290f379bb9192f27b715af250f370f9f052727279b15bb9519272f1f27171b1f078f251707373b1f9f05a7051b1f3b273bbb290f8fd95fbce7ffd163f5d60046281726c9bf26013ef0bd45413e12233055ac215cd88694ed4ad063f7cbfbe12d99b4421f5706854289d5862a298a35e43b969276fc11cb60a7b8d7be4fcd0d2af9c46878f81599de5d0383eb4221dd37d30222aaad5027b01e11b84a698189bb9030c1b45e4823acc57b5dcd077f247e027c7ebb5782e78e8c117796647323550cb2b29e5d4bc3196555e1d99b9722e17e973bbbfd7fe478b11490435d7ee4fb2498dff7ad57ef772a626155a55fc79092afa1cd415db3fdc19ecc7fc864c37dfb3ea4afd1ea9af1bb26b8072d76ddf85e73fd7554be6bdeff5aaade35eaf5ba1a94d751fd20388328afc9e8c2f7bc9b945127e6b4305fd1ebeeeaff81dd8d7343ed70e3eb6ffafcff1f5fff977f7ed18f9aa83645406ee631d3640ad98758a8992e9bb838ea5fdd0a7757d1f804eaee43e4f5a3e656c8df93ba1ba1a4b4d4180a6f68235cdb3e4b5c52535e55c5f8ae969a9a929eb18ab8a6bcb6b4b1a6aaa2b4caffcc88b9a18d056c3216a905f7a3315168ac10817ecc5622ccb16803c0c63f45cb4f7e98b75bdfdb4bc269bf9b3c4b33fdacfe17fae9ef63790233f2508a47c9a0f2b75185691fa2ae9db8d9a8feabea308f3317fd802f47fadaef427d039e9283ecb0d8ea3c9936da99ac23cfbaafecb3428d44aa8f4c407e7648595ae4f3fec69fdffb1fcf7af5b31f0bfbc94ad0d4ec24e72c41a0eef452ba8d88eb66de38f4759679f6cd542ff43ce47ff707e62fbe3faa86a999990f858ea98383a5fb9fb89edbd6c5ca4d809555ddd1dcc74bd3cc9d538287e7ae932c8fa6b9a3b386a9adb792b605bb24af9e178fac0f87b3b4f2fdfbf77d1d95d5fff78caf789a160736f29c28969dd0503e5df25bb4fa7c4b3e7cfe27316a6e5c4abee43151ec3d215fe768038d4639651bfc56ed38f7de3ab61c59c14d8a3abde251d33d6bb98e250b7d0253131486b2f90e128672f981a71126bf27c883ff66ba10dae4af5d3480c18610ad2795a7bb6c2e6468ad6dfe4a7f03028fdc0e8d08878400050b0df3273b7e9adcc460138b4d19fca53c215aed97f214b05df7bfca3f1eff5a3ef24bad21960eeff34b7956a8de8de4259ebf8c708ad5d9aa5d07c361181cef70abb6ad7cdbd5263abfddd00395162bd3a8c4f9214daa6089f5ebe5c89704fc497141039e131828918f9328c8d49b6b6fcb23f5fd5fc793e7bd26893841441409faf88071d25b3bceed75cea233429d78c1013cf90686c5d2ec9c405482daa22a91188ad681e07a0e53543c5ab30a8dbe92454d582366710ce9646fd9750c0f58a6c49e9a1c9c44af8ef815cd9c8332e823f8d08ea772d24fca5e51d40ace329e41179c54770d1d854a54be4df9386efb493411592a09f97c8eee0c56966404415c61e2c3a657b5b22f6dd7126a636e7d261e8ccefa8f111efc342ca38ec48df73bbaab6d8552ea8a950485aa9fb1e8c33c1507b85293de9688ad4a4b29ea3f6ac66c853eae89807e576e73fe0412063d4e641506bdf479b021d36a7e2a1b421604216b8590429b40c8a0fef61fca6ce297d9fe9d24d49437296810c8a942cb40d857e4bf0b2920a45034dc59d8d010141eef87acb00cb7b40d7b59f6cc5f6f263479faa0fe7603b26c08d9fcdfefa49a2ab0e7871e32754cc89e71ea8c1c4bcc8320feb7bba0307e54ebbba54c9fe54b6a5ddf8c157ff656c75a1b22000521f8ed212150ea5bfdadd027b07b41a16fe594433f0e6c35c22e97bbc6dcb93aae71ee099f43d8686b3f96548d28e9268bde39fc7c1d952d5bf8fda6cb7c0d2e556851992a3decfbb01624b1e31c0298532dd14a368d06dcfc172df52c46c7f3aa1a0b8b2c8d649db9230a8bfa87e9c8ebc7efe5c5c81c863dac84b450eeb86d8d0540a78b3a1f7941573a25691df755a898e709df330adf76bc6625c09a2fad0edfd074cbbe993a058d0cd5533d9b705f871089e5c2cfc3e2c15a232221ab37f1cf5c9c1d7ea92e9c5ddc6d7f797b4b77a0a6ac9a3025adfc638e07ddd8c696636527780d6eae397f44fe278e8beb4a6d8164a7df49eafbf5e5113f1f5fbfee0c79c9ebf8bd81404dd5ec06dbf84eca289eb6a10c0596ccca0f3cf09b72373d2f81f99a502cd6a26def1f26f8b6e76c6187585b59f0ec2f764dc4113a1d54b9a6f2a594318b9bcf5113e60ad55e7ad206e38b51ff1899e11976e135a19eaca9e99e77954f491177fad0d00827d45495f5f511c3f81d9dae32989a949ba61ad9df17b99c686da4095b5f8d3191ac5cead7e39131c742533543923f80b2a0ce452776f6981af482bfa3ff915f5887cc61bfe8927ec6ac98619dfbf1a4439ebe23c98da1452a2d8c508136e806a9fab84a0aa97fdabf24102b87e12b8c7fac484aae394c25b5c626433acb358a58c347540278cb81b61db8ac96e3edf667ffb8250946f8df7efe15ad14d58ae77f77e69752eebc5e3b74a42e7fa83e4df0ab2784175d54fb9b6af68e80c18c8ed176b5f8afe010ac45be52faeacd8c25a83fd4816433ea31bfd6f794f6e079ce78bf7db1c376a72f3afffc37679541f9e3efd31235a7321b9b31943efdea7e560ecc372c0409117fccadb53236216c91b48415c52c776760d76a948e29435922e2b272d4a1c7ed0d7e886c7acb4bcb9cdec7ac01afadeb1bc7bc744b3f967dffbaad3efc6062f10b3ac9bf71f7fe89dd0775c3e007d44dc4d78fff752e3484c231acca8c25d1e674b3630d5ef3188f98613107d32c60881485772a9b6b95e71af8f35a0ef235726a607ca0777cd03ea50a709f87f9c389cc563c4583d34abec9fafa1fac4f71ea51e84b9b2752c15c3a534e9c978f838616455a3423a41fcb93d8c73e683848b57a186a75c61fc9f0bd309945f9458aececbb68f4162c5bd49d59d6f5bd2e589f9b6a4f5981c3212dbd420eccb1dbe82724efb70525d39ba76c4a3fbc13cfe112c02f4297967e3798793b5ad0a4dfdb9e6d506f80ca9468ccc5238f5767cb76f060b06685e45a0d7902fd5b449a9c4fa6111a36854aede2868b1ef0d00c3c4e35f865d30776e033b2bb061f61ea946b00152ffead5d85c9ba6383f0d2cbaeb9fe1a5ce7da9ca79fcaa165b7129143a5b87ebde8182e6fa814ff07115f6e335e6e2b536e0e4e7e2e5e7e73730b4b2b361e7e0b5336735e0b363e3e76766e5e530b6e0ede9f90b77f6bebd329f51754b86b2dad88aa7d0c97f0266d486c6b298770fedb13201b350986c3d89318854039932afff90e43fb128a1170ff0433c1d15e07830b740f6c32967a3885b2ce7d0068c4c4e0bc55e434a5e8ff7253a60e4b3a8578f702f0789b5cb376ce0aca382070c5af5eed50f662297e032ad4a4b60479609f343dabc9fa91fd661345bf94ea36c0392693e88626d337d35d884a5df8264f5bf404951ba82073891d9a80d926446ac816deb0dcdf61448d3b0a54888ae440d511fa60e7e766ee58467c87f959b65033b05ec5355c9fcc76556ff0bc5e9bc8d11411cc9425072a6c2d24f973e6697c6d1e667281708ce0c7f1fbebe15d951f659edca7531a5b28bcf774bb1231a1492ac70e284b1f916dbdea0d5578cba96ba4d35babe299723b92ff553995ac500c12466162bbc4e2a86d5805e551150750c1c438b1021b514acc5359accff864e8b0b4c29c06a80088371f0dbdfc2a0ad940203ef0f0a262962b4d1f181f11d5e9d6fb19b3ba89c164e70a4c26f065f8fc40228ed3e3fbeacfb3f7e2cedfc67e9c53b883988387058c1f366274a29d8014c907f810a626959c843e6849a048eb34d284c2193cffd19b33daa5011ad3e19a2023c5ab72c653f6f7f22b2c0a58d9ad9f3ebdadf079903d04e0d876b8eeec4dce72492ad5e8314ac6f695111529405b81baa170cb209a8f44385e3cec253b75816fb2d501c4c5e25ead8e25ad0b8efe6454f468c62bd41d2e6280d536d87872f2ae8f3c268e23e0aec5637a72d25a6ba0029bb7bcb41abaeeb4b166c1f81d9c0284efb75186004410d34a739a431a260571242d9abadf4859c286f03be032563f1adc8beb707ce8659cb8b8b8731f734906c8f7982fd9a6ab80367f19c64b7b9f8711b2e3f2591ce0dc7b8563add2cb6f2eb6e719a7635992aba0c221006d9bfaa033f3eec18dd04b65311ae04bddf35d27c905724c75756116dea04736964f10e497989753eef6d603ef67d755e72b2b825aa842880aafbb69575140a105b4456af61916082aa687d08ad80a502b47960ef55301df2f1c198837965eb8d6bdc918be88aba2a478790954c8db08c8dda7bbed06b1147f2e205ae6eae248ab04b6b6fd6acb58a096c9e1d2e048666c8df0797f8d0cc8e7b5237c9c6e4fc94acadb62bfd7a30fd18eed2603c60fd69fe7539f70e9244119cfe3574e4dd6c6fe1e3157e5f7586eb3b377ffc87d9d33b376416e79e97e206e7f55dedff77402bbd9a39fcbf94922efcab4dcf8442450c172f75c946d479f675ff1c9aa10b7b61df5d6103550c12ec262208486151fc15c7d3a0cda68e6d1681125f07e0976cf619a02acdfa68b4c6d90bca74e78bfc50284e0bddfabaf28858ff47faee843a6d9178a3b3362042077e41f7df12701f99738b3814ca2de92e8281dd180cb84e28d5cf64c49924e62d8fae34996e4f6c4b36020ba97bc66fbeed9a31fe488691dd512523fb02ac6a7800a9fdfb3eaa4f704ccc2d0e943df481fe36dd847806d06efb7479fcab4271b7fd46447b422a88ca2d6d703dade780c0d4faf3529a8231ddeefa141d94209a356035a2fcbeb224e11f1466eb122477a1eaaf68f351be201897aa1a41d97d3366f9ed32d5b361e0e2520fcf02b0018341c64958f0a481c8be5cc5949895d63798e5769000a01f9e99ce9a7f0ed6e22728e386ae917d22cbbe70009de45293a5cc7b2b6f56e3f732ba3287bf4cb3baf38aecaa3198bc9fce9e4a0e2551c131ec8ed0886627f01f43beaa80b57ec6cda6ba7cb0c24c8ee96305617039094a91943e53c174999f6b92e99e95b63c7cba087af813698a51c532a19c9eee19ecec4b8862f65e01426a001566eba86842456e7ba95d5a940091f4cc986837e2e60c4f3d6a2e6a76ddb74964ca6118a127d8791d6b1bc7f55be64a9f754b85381ee4d5f508da37136ff00fd1850418f8924897bcac1348a510c56248b8921f069aa3ed0d6890d0ae2e48f0ff36e780d06a13593982588c0cce0aa7cf935ee5a74fe6d4e0223589a9d5613544f8d6780fe388253bf3ce95b3f7528c9736b2b4a7e8c1c1a00b48ff047bd57a8337c5673d111808a2a1dafd361d204b4befe704282c2d3de9081ca058f4b51b71afc3e621a8098a46105a6328908b590fc768a2ea3445f8e4dc101f3cb8985c691fcfcdb9347ebf4dd527eaf0bc3a09b018a0aaf84205bec420ea1b5e7db37117bd67229ab3240db3904dc2651214fd8079ef9a558af1e10d2c3353100f64fc587c5d290077609af9d75613d33bff030226c003991942c0b68ad3904388d56f9c2116e25672783704039e32f650884444e45905c5b8421483dea00cf820b1db0bf68092eaba3b956d36993cf139d678543512480f5f3216d7feac343883f853be527817c133cbf03358017b810c37abfad68ff88e6645b56830fb5231ae733d0ba76508fcbe6cb1796330c34f5b03c259d577446f240db1f539de9859d43069d49c1205ee90fa9cf3ff03102ad9f11b46a4fca1e985cd8d4da2fbefd9ac44ef8910f688be760ced7c32eefc2466a2bf452e3dd055c9c6b39102886d95181dfb28952d93e1af6b67ebf420cb7160d9c1f17867b2bd6dcc5e25cb2d6781b75534f3f61ed010cbd7be6526449979b6cf5813e6ce1291ef0f7d91e0015eec6b8adea927f3419b062f6d2202b5ff23f0b1238b92a87c33f0c11170bc675c46a2dc087e589507cf10ed02f6484cd186d71946f66ef91e712be1b51e7438b04d2f8115a71ca93eb1df96515fdfc5e7f9bdea4f2c700fca1ce30b4be8ed84fe9c8a6e68f9cdba8062a9cc0d6eb660c01823df553ac2a9a76c1ee6187a28ce4e6408ef53a865cff85d45368bf805cad0acec9d5442e7ca0b53b6f23afbb7579b502e75401cf088d51dce5401b60df0c8e2bba61ec2a927efc945cb4634d6bfa21133f15387f943267bb57420edb76550691b0f1cd9cfbdd80f17b7f7c07eb52abd3dc22a9a134cdfd53eaeae30c00a49213b23bc34765d6677736a96a5bd0dc0c3e5ea20420e2c6efd8395af2c577aa7752e843c28265b24a8014d0bda112216cff81b2401bbc0cc2cbcc6f1290a0b0bfd09db945903efff57b394b57f7b71a4f6ffab0fd8eb806c09d1881e44f60f6d132d769e1e4f6a828050a3000db01ee1d11fe59b7c067ce96aa88e69cfe286dfe00a02020838790a58aaf79f36d74cfcd619cdb2d5ab462c076177923e0c7a6b51dd01d537d5ba264d571a67313e8040de1cbf3258d94d6591f1f8e1281ad1b4f3b7703ccbd8ff194957d2e4dbe918901511a3f8432341a7cbe5e95b38873e41134f394d5234c6ae6a0779de9f80902ee0027c7c3815beb0d071b8db4437d1f4daa1e39a8035dceb0e668fd53badb949622aae9b227904ee6c29501f6660eb601d5f7c47bb10c3ba7e1c632f7968cc3f100c89ed8b6ec5638f7f7840df51ce703caaeef94640ec0714f98cc634a84a448e9d81f3355c7399213c83d0120f69daacc8ccc42da286c5a369d7a3c1cd36121b70146d919bccb036444777732b72cf451a8f01a1f066ba0d3394c8f5771f9b7a61ed3ca4fe9fe240f5dcd5f5f00904a74b5c186d296f6bca69e97f36bad2ce507702bba57e542a170d60a65fa89ce3fb409896e55e58ea6b402ee28bba45d6df463a1d446346f1515983055dd179fc1f242a3fe0f6ef9123e41feca2221b2870c457bc800e4c4753f19c257f801a1a8a6ffe69125fa7d8b4a7d19e80237a2306b6247157e1735ea95626f33fde67cba2da02e4f835204390f864f33f3b01ee2d6983cc4149c070a22178e31b80e9068759295f2e2d81cbe7471a59b068ee3b7c12530275eb3772e5ebaf6f4917617d0a8c502c72d96c4010a4d7e7c9800ccada1ddf25d146ae351607d6c5560a81bc42653d743075e62ee9390500a1e02c7c97ba83042adb5cb6953a5adb19e0aa1ad89a924c0dd3f9b9d9973c8943a6e3b83cfd66a16b2d67d3102147f3af81cb2bfe68fa0450e37f3c1e70bdfd2bbaa83f757e5f9f19fdd9060a6c78da7dce1ab93bcb0739089802e9df61d1138489be2cced9e17f41cfa91aed614df01792f199d6d7beb5d2f3197e2ca670ce4b8bca25580b9fcf6fe05c49e38aebaf1016ab9b440013e53961a30bf3062e451177568c51b1895b7494472b42539f08082eac2b9b624b46ec46d28ab7cb45f9f623023c74703e599f42b07371e318bdacacd717f368646f85ea12509303c64a6fa632b93125d55b47206c39f07a60c747101faa1e063e4b89be30acc14e6f8dda14857510ade5100141ae9d5ff30efb8a551085f8c9a155f478dcb1c0e1c27cb35ec4fa9e7e5e2789dab5a9847b97333ab9581f9f160f03b29358c5da7e6a98944df98fe0235aa0fb8fbadd30b65988907074551d9eedeae86615247c2c0fce096ce4827acb4aa4cdd61e928cbf72d2a81ab054a82ecdad706b4e2be40879d3dcff2fb868cb10031070afee3e097772634ace92042e08bb4a5700a8c9c9380e266aaf277a7555493b84ba71f8a2546c4c6cac650375f9517b504daf0121253e1adbeb456e69b78a11158021cc75b94a7d3c4ac23a5254a85470a2e48b77b24c38082d80a399ca450cfe28be1f61ab676996e13525255035d16cd2ae79e2eda2b3fe02898f1d65bfb444574d7cdfbaabcfc6c6f8f33875eecf5313e25ce82b015cd792cf955f9d42d49478d0d564ce66fdda736231322334aed40e32eccf31edd6df95ef1e34bd38728af96b9085fe001e521de75d1e9671809f00db30606753cb6669f904b01cca10826ba29a3d08d7c5c47f7dc9103b2a087d4b9807e9bcb3235bffbfdfd8052051d164206b71206bc2a70ff20422241ddc8a38066a44efff93d3c8fcf31c10081054533fb492fd172a571b7e7c3dde208fe7b035980fed262921795099c3646e2f15efafa9e903cf9dd2150f03951098755634e19d6529d2140da488e88b04b04e842fcf2c214298a877049afefb2bf106ea474834e0030d7d1448667633e33083d109671fe4e56d8b0a9c6ca7055fe144736ccad2e5150521b43ed4748db73d8990d008e69a7af4e60fef14a7e1acb667b1b8b944a9be018989fa78bb46fe5d975ee2870571b9ceedd0bbca43000e044eeda6fe20582a4c3f7ea32fb4b626cdf89e3800d1f244f85a8b6a2e1d0e57ba112b49b0fb40af2abdaafca870c284d334c2698490c4510c95ad6ed04a9ac0102d4b9b2c97c1f996b7b12d4047684d863ffbe87f4f180396e17f18071d6e1bea72fdd8bcde775234534749b80b99ae6f2c68260b8de8c1e1d21d42d20dbef5e2f509cef561946ad086ae3509e74dc488a93e03cbae636503cd806827e0bfe73124332ac743a5b65480a551717a05fbee86de17a7b0d320c901f58bf3957ebd99a1402cee7ddeaed8320de74abc7339f1f14f4b953842bb903e189e9d8d0cc1c53eafb8b051791ef68763e0bafa6010d354479cd32066810d4458887b6ce55ca58ef36ad02e77bd37c840ca567e8bde97be9b3a91965de2c777a80f94bb3e80ff4dd467e127309293380046fbf3183f971555e03addcf8b97a98c480d983cc170f53f1c72b6d607e61f7719a0fcdfc949f28ed121e4b9fdcba07cf049c1fab968cc80f4366869f720897e98d6cebb895c302e5e15a07a56fd1bbd106df7be05d209b8b1c93444b0fd8a7e54dee0a4fb94a06e1bd988e9c7c8665dad51c8114c439af989d81da357c824b34f52211a6fd1985e700eb8738555ee5e296be778c4c68e66b78c57a0b416680318289176e2f76db4503efb23882e34756d9e6b036d03cb0c0547303ba8647c2a3b819b7e8ae667955d52720d5e6ede6fad93b3bed52fecd68c4dd99fddd05111ba0cb706329a3d83397c6d76fdf4d746dbc3c269279be0ee87762a62dce379f7da42de753053e11b4f16c19ba02e5f91aa8283ca345901eae7f83ba73c7f8cc22eaed30e0ced8924ee5f764b709b771b1599f90d0b7877ff8042474390942466a965edfc1bf7c9ea014df37da1fdf0fe8e7d7dfec92655b03ee1867cff92f26ba47680f3d06f6a753137390bcf3bec3d984a8b15c135a5641b11410ef4fa6bdf5192dd1b257f0cc7a2e312ddb40ea821fd85f0621de4c14ddd0e4e53cec14644fe1b29e7c41f4b82aef39bc0b356bc71697c76bdec846ad9eaa9376ec0e7d027bf41b1bfcffa44d35c22edf9c100efb2f6c01893bfbc84ddbaabd8564993856ec6cb1258b3e5cd921938cb4d2d5dfbf19b275e5e345a487a4d0ad793244d4ad3f0c48f9f9bdff9510de20047955ace2be8c678a46573623696db9b6ab7b4db41a72b368f64d09e1b090ffe59fbf9a108e28ef646769ee6e69f127afff6f42b89e8f2dbfb686b38597b9b4d65d7e695d89fbba56761277ad397834eebabb4828ca3a2aba487bdd75b1d3e531fdf73dff4dd91cff86545676530b4b7e3e5e2b361e7e3e332e0e530b2e732b530e2e6e4e737e0e4b6e735e0b7e0b1e364e760e7e534b0e362e0b4b6e4b2b7e4e730e4b532e2b534b2b73cb9bcd9fa5a7a3001f1737e75fb43ec4d9898d0dce66ac5e94d060d48a69d30b4758c5300b7176eb4efb6c2b1e56e41ed90c538d8988f1d2b6bff46bd92457b27afa487b7ef745e07bcec499876f1b1c9dfe1ff6de02aaaa6dfb1f3fe7d00d920a0252d2d20dd2d2d2dd25dd0d12d2a220084a37a2528a202920291d12028aa40888208d08fcc7bbdef7bdeeabb22f1ebdeffddeffee31700ccfe7ec75d65e7baeb9d69c6bcecf24058f5640c2fba3562e12e2df502bb71fe47e447408323c9ae157d7ca05cb020211da5f5e2b37ef3883f503b5728be08b068188f1abe7d63e642c15d30a7ad36e75271c86109d1931b5e932c2c5c973eed0475a55c751327d6d215475886cc22954bbd89db0b8b3e3cc655dbe9e5c098e511896009cb572c194d277050079554fe77929c8dd38def8f0cc8fbfa054c58f532b17ba11ff00f53e442b151fe5c10ab36cf2e8d56b73d1da700d20407ec15aff05b5748ff5fbc71e5f1c6ffc06d82ed42b1c6a56ed10b6c3f607c5a97978c7cd1eb0052f6dc67494e5de3e3f2795f41e4c84ab24aa2d70be2f387248f8f52e5d8fca5cc8975f9ed852ae8c98e1151140b7650e7ab3d911b4fcc617ce4826b00513c5d3c4cbdcc5d1e9871724f7284233f5c5ebd822f8c3556aafb52b27842dfc6cebb642cb037d75b7b48cefb2dfe615daddade3ff2493c697c4b56742715ee264a3a8af5f7a96d7a8b2757be05f099fdb0e43235172c984fcdfd0c36669426d9be0dc4b800d1d0c0315ae690183b37f99c7b9ff78859c3f0f27c8ac008c0fceedd5087b65f530dfc48f0b12ef78ecc36e1428ff70fb4257b6305afa62e07cfe638d3f58106480be87422dc1ae654a44b05f962c6607711311ca71d406d8fd60cbfab7daff97e8ab0406a3f23a079f616baf8249fe998dfea5e386a2c14de6279ee5ac9a27aa91185e9ad27d8073ff2e0af99fbec0c830205f65e86339e4677375b38e85d64db7d7e0f1559e6dd2fa6e42144336cda92d66f836f8962aefec729a6a71c7d70f981c5fa055b7de1c0cca76ae653fa3ed2481661da5ead2babf4d7e4e3e85af0ac1df07955187b8629caefcc23973ede79f244dae8c85806ff077d43e67c48662bc1e16f9062781281bc30e3313c127d60ff90493c1c51093d3028347b5884cb21c0bdeead307f4775adafb91ca48a61fe84ae43c4528f1a83872d90d7c9afb1b71026273d9379a2b77786bb621f970286a64e59dccfbf0bbf779706b8f5406045d6777e2c21078ade7286d42a01c0e2495211c2c63f6e125c2dbe6d462ea1f274c139994a27cef115e9b39c1f11aadaaad43897372f1f1d99bcdd968f48ddca76fd1679e7d40b3e7a9da5da089f8a220b7d368666df02071b830abee36c353e14baedc44f867cc53bb8a6d7408d53d69899c70121de359ef1fb9afd14ee5fd3a314544ec3f909892955f543829757e6681c48f1a2d74a6a8b20fe5ab22e0c74c4c418673f9158b5bbb213f2fd2369164cd4c7d868d2cd6d8f3c063a487649db44c415d4564785ecf816320ae6346b7dc4220bfb245f63549043a9a5a67977cb865db326200d6e1ffcf135390e173e54128eb9895c41f946b462b377293529860df3fdf4b2ebf5a7a263e3e2db8ca6aa86d174eabea3f9d9802269f62ad7a4c78868792b8187ade853dca8bc3b18c8b658869e379913362c9ad98429556dc851ea1b2aacf120ca4e2310fdd3c885f5d4565cd99efe7ab44473461a809813731a577a30bf5553a57c8ede617dd88c82cafc78446c993445fb46f12abe0318cb0c49c9a458855246b8ab89baf77b8a81f5727ba4a6c94fe4c9c96dc9433129545c00b7c67bda319763ec76c49fce0df9aa75292816d2cf7887b88653cb6c0f66d91ffdab70c4520e5b224e67188fee140873550bc8062c2b72986411a7786d178169f161edee3b3627c3ae98c103ac0af4b314cd238a644eaaf7b7503befd0d28d7deb12ea9a5a927fdc3420907f962ae5853aec2bd6bb6dfb575ff3d5c475f67907ef10609d9c4ccc5d1d5f587efc7b371b777b27061b5f032b33271b864c16aff976fbd5e4d1f13e77bf4ac05cfec3904b9c07ad18775b03c5154b712c9b1445d3dfa645c7f87806efb8a779e909da073036c17b13ff08f13a16669b6cc17ad0327b238f3246ed421cb3e54f7cb5984b13c29d73c614966ecbc43a4788deb227ccb0050cd73b9fa433ef28eb886de397bee021ecbd6b4ceb5f1a3ee3edf9dfe14ce6d021c2ff4b78b91cd8bed2f5ebf7e99c6c3c33ba17ea6c3be597520925d3c30732784d1e23838989577dc5224a254ee10389da720cf0f434559a606301c3c1628b6e3e6e119b6ff2018592d49db3b258c5bfd339f11acfd13ec138f37ddd2b77aac25b2ddb39ecfcb382a15c33346e7bbd31b20bfd6015cf27d281602313bfad4f3bcefd5bbefcd9ece0542bad16bec18ccb75f9713a58dca654014b837b4bf35d8df1442cae127f55edf1a7a3091fd3c09e3fb098e70b0219084fb3d92b89dd6f087870d0151a33df5c50f2fa6887dfb3ddbbcdf6716f4146d2798da38f2e0c0d28982f267ae6c611d6c75bb018f552a5f580e6a2058c9dfe8e404d44e70c618e3486134a5992d93e19c605b8f0e081a3287f7373f6091229c2fcc98df96a12228e634baec3bd0fb5170a061c7edbeec3ee2c73ddeffa6cd0acdb3d22e295f024e89ba27bebc374b342d4a71bdbefbe5dfc5054edd0cef8560f5e38eebbfb4b6fd6b6e54fc3b10e4ffa646ee4f6006fc4ab4ffcc0c78438956d27d7cbf96794ce1391feea429dea242150a9bed33a2779bcf506ef7081de1c2fa2a1064b327a3952042ffbd75f87ec136937e3ca6887a0a7cbe6b304e6e505256380345fee317c27ff8f7ff08f45071b537e3d5d1b8c0c165a5ae24afa2a5c0c9a72c21a5c52ba1a1aaa9cba523e6e1ac2d67a57ac153824bf1d27fcff889052a5da75aaf97f13fd7237de0ca9d527f55dd62c016255a04cf0eeffd0189e24b8ecb6efa8331e5c895de555d6fde3ecb52142e7d59eecb97f1c9c37bf256e664e25f383742640f0c4ebc15189c775ab80a91e6e14b8b5c08347095a43e2da8e559672239898941d38aa748f6d58dba6623ee3d9117e347476dd03923acf934b3b94c11c7bf4aa616d79aeb3e17bd278846f31e63fd8ddd6d6f0c5ac64df8f672a0c482a2f02955d0bd3ebc44f9bffa1285cf25fae3b604b25b153193bcec51149d50b156e22d30e33ac46375dc58cde8e9b5d19804b1ca3e0e949caade898bd39d9762a8385d9935e29498049ea8cf289093afbc57ee4c9bbd1e758df98d228c4cf5306e59d2b118f27a221e59f0cdbbe233eaeffa53f8b90360a47533892d4198c3fe64ef78d5aef9bd0ef1686839b0bd101f72739ddbe0ce8a8d0ed303036c95fe152128516d59395ba102912879f8598297a71adfcc78680b094dcaf129a9866fda8b46c9e76fd45d4e2f4aeabb149a644797aacfa2a8d963e4fdb607657a39dc39fd2c6474d22cf6b516d8f3d10f2355bed4a7cfbb465f4080919d41f8fac32cccd10bd99b695225b3ed92698aca640cdd2321e94499d4f1b011278277c53539dccf8a62f8856c83a734061630c575a9bc7990e372f12e707d7474be82d8b217c6ae1635f2e88c5d1b15971e371a71f393e2b589543ae342ff67911ba20d777b339860891e8f68162e6acdf53a87fbbc52abeab8c1eb19b724bc24de6d29d6338adfedb01b5dd2a9fdc67239676e9faeaac11971c794d821c575947eaae5f1d3dbd286e79adfdf46ba798e73cd6650fc77ea29b487fffafb9a7a4a9763cbcf7044f169e924fbc35a66915bcaaee2df35efa16f50cf67eceb9834c07661ff2e297db1ba0a4633b9fbf8864b9a2536196c76f6a49ba1c965f3f3913e6a61ae5d1f90c8d104ebf3a2d04f607cd9d0710b1efcb960c2b72eb08208f0e9315046baffe70b3e5c9e5adc232584f2b6b5d2f0205e400d31baaf5d5e4815205e13d47e38a5a12de88d05f92486b6759a03397777a884e2741795f6f98cbed9178325e476c6db335d7fc135ab150ec56e3191e4c8fdec9b95d3ac94dc6cd26e85c73d887d1195a401b6f27f0eac5c96a07cb06a8b9006d840387b981e339399c1bfef0b04a927f1db2593163950caf99c571fd1601fc9cde86879b08b6ae8080cc6f1960cd92fcf14810d86436dbbc4162090cfccbd78533990c006d85004c2b44bbe81a81df0e3c170e8650ea5a6e6af3e0dc8d866bcf1f5a7897bc8b35f35ac5b103d4244f9f597c374f3bf6ed8bfca23d6fceb4f1b5cca71bffad4abbb563df1eb76af6b763a7df5a91ffe3216dfd7ed1a7feabef6f5a775e4f798819feea80686206d4e41425082a1855f9f98b6c9147156afa41d6619e0583fcbc48bccd89ef80e8d12acc4e06d3c63f30619a4611771eddfe68fdef5c05f14036f4567fe047b54293005b9f6f49d4d2ba11c2669251a63f6598ce724d92d87489e54859f9e9c941ff709e9e4902e22451a3a8ee973e04d1abe85526cfb5632ddfcf9cd76d2599b1352709a3effc4c0ffdaebe7c5c06b897b9a3a5b727a5bb8a97b99f03bf1f0ba697979f288498b39f38b2959f3b9c948396adb72715bc8b8a83bffbcfeff17c4c0f3f05bb0f370739b7299f29b9b9a9b9999985a9a70b1b3b1f3b0f17172985a7059f270b1739a70b073f1b271f35bf2f1999a7170b0999a99b2b373f0989b9b71c3778464e1612fc0cef6eb4cc734a834868a037d2a6c5647d3eb46e18d0ab66a5b61fc86973691a242175de489710d3543c850185ed2e53aa6e5946d6f51c87662e72a78ee3f2334240c91a27c0a1622b30b7d17187c9aa1ad0a3acbca1b4408dfeb820292959f627e6814a19c3970c4a0926f9cb24beea85f02706331972584ac2045ce74ad89eb67896cc8bbdb9d3f7f9cb1d15dd4724fc8a8a5d5ea0a8fac25dd45a74e4b0624231e8b0b906765e7aeef1164ec3ccb5a8ac2b465c779feaf8d6ab2eca3e0bf726207af93f4336df7bfbd7b88b37f43c6432fc8fdffed190f202a0c21f3388e083099fc818c8763858efe40c6c35df80c248898732ac38773e7f47ad124ded106d356debb7155e3e00575c5f8c5e11c348afd07c9f6c405374ede9f18ee0cb4729f4ba8726c6a92a13c872a1e5667994fb1ba351b42fa4b57a82304e0490a8f0e32e69187da182487dbf0cc8fbfd041a6efa4361cf7fcf52fcd6fb046033e09d70b32291f0a9cd6a0aea5a9519608f6052ba975f267fefe71c7ef5fefe7b7d81f6f21353a544830225371307eaf4fa5e49cb909482a0fb0569cb3233af5c9e903ec8ae4a0f47d469f9c02c348ad065857205230bd8cfa6f96dc6763e7d69d3dc8d7068cf2031fe53f7f0a13da53e236fefd53e291104860c340f850f9dd189f9da410849be19010a8cab2d26fd6670529e45f7f5fdb39073e03eeb73a2e657175510574bd9f3c3752dd257dd4509989a99cfd6cd012cd18079f5b70fbbac9dcabafd0de1a281f0a6b344ea02e22dde73a91357ae4f0a3ee307d693a916128fc22d389aee9d68767390adc9985949777f54ede135b1e51885ae02191272d2dbc7f96c545f6f16975d3ea6161b298db2e94012e0f8f633aad360593715e987e2912dae0cb329b6279e59a58fb3fa6d37fb5e9f413eb49a9babbababc8d8782b4babcab35f70541093e290e4d5f0d095d76597e4f476b715f7527257d3e435f1d4bcf03f653a7171719b9a735b9a719a99f19a9b9870f3b3b3b1b1995ab05958989bf0b1f1f0f1f0b173f25ab2995bf0f1735ab09b995af09a9b9b9ab1f3739b5ab0b19bc169f6fc72d34967e4aea95d4a97c0902d1da5ee7e8053c712d4e7c34517ee5b646783af50d89e3778744771acbe74c5569df2ba78f6c78dcdf52b2a21a23889b985a815ca0f950cc1eb6da18e8491dd14b37dacf19bdf927237b80ab5596846f0588cdcc75df19005701be17cf35876d696d662e6264e6e162e9a1cc71e5814c08e0f2cc4e9b8215010c83cea71c687d8142f2c35bbdc6a2d7ff07d3c398ac015f2a943f8c6ef787941ce3651697d9a48fa2115d9d5e850e324695d9570b093b1e0f593c4f0595d6031094881864cbac6ec962672304dc7c52856ca5adf133ee4275307353c0755ce714aef49c03706c70bc7010bb35b7f3dfb8284fa7caaeae2d079a5278b7c54a71fb21e39821c8411c791a11f310bc0b49789bd898ba3ed0fdf8f636fe166626fe26acb62ea626d7ec9e297ae1ef08682825a05c74c28fc0b3aec6f0f0585f7197f762828d81881cf0150118233141492b957d18d1c989645b185f14977f3ec66cc3091775361629fe8e897efe95b037bbc50d0bffced3f5f4785821e47fe8ea79f3e6f0940dae78047ffd9593bd8ffcaad75036c2d1cdd4a5b898b4dffdf279f99c88fa08c5d77d4ae5d4d69f44daba0f313bce37b73cd5e82ba0f0a1ddbbb9db1d600db2551f9c3b54822d9a88d8a08df2207b64692803d0788bb96360804bf02820783e0212038886b83360c0407d968d0822808da4810fc2a081e05825f03c1af83e02007f6b420d959b420641bb4b120781c080e123b411b0f822780e020697fb4b74170106e6ada24103c19040709aca54d05c1d3407090d81eda0c101cc4c5420b528d92361b04cf01c173417010b2225a9042f7b4f9203888eb9ef61e087e1f042f00c1414a67d182786069410c45da1210fc0108fe100407a13ba27d048283ad3fe520f86310bc0204af04c1ab40f06a10bc0604af05c19f80e07520783d080ee208a105c917a405315268415879689b41f016101c24028eb60d047f068283b8b8693b40f04e10bc0b04ef06c14108a768418e9669fb407010323e5a906ad0b4cf41f041107c08041f06c1474070902318da51101ca46237ed3808fe12047f05824f80e0af4170102702ed14083e0d82cf80e0b320f81c08fe060407a14da17d0b822f80e08b20f81208fe0e045f06c1df83e02b2038483d595a100229da35107c1d04072168a005c9a1a105c9b1a005093da0dd01c141684d683f82e07b20f827107c1f0407291d4b0b72d04207e21ea0037122d2819cf1d2811c71d281f83fe840082ee840685be84058dbe8409ce0746820383a088e01828310a0d06181e0d820380e088e0b82e381e027407010b2533a10e7231d48b8251d11080ee220a3037190d18184e6d09d02c14122b3e8404e50e94e83e0e4203805080ee2e0a63b03825381e0d420380d084e0b82832830bab320383d08ce00823382e04c2038083d201d0b08ce0a829f03c141820be8d8417090b3713a10e6733a2e9005a8b919f126608fdc2ddabc459c70936f36404ac0d22cae1eb7440fec301fecb05ad28a500270d85b72196d2bbccc30191962c7cf74f982b0ed871e10398782acf33f7c414179e33a028371627fcf94da85c50606c35efa06d35b8557c142fe0e565c544cb89e0f4e565cb04376e001de715871894a7968d0e8bd8e089b4a5f2a0e2200e3738237b415801fba0a32726be52f5bde86f894f7937aeff757008ed77fa0b43fe438e37fd9f37ea01074c17b3375fba3940bfaf89495040fd8f834ecc296037f0bd6fc9b689affc30239791c813d1e4df3bf6e0f3b42a04e66b39a2dcdfd6281048c0f18bbfa8fd01c1fe7fe1f1078e871de6f1d4d0e0d228bbedf1373c122ab692b91ed91ecbb60e30f7b11de08439ad7c6450881f9b43287c022ae27844055962f35bc090ca65643c96c82400a1ebf61b2633b0b855e30b8c089697c160631eb09c6b157cfb7b463aced0bd35dc7f511fd2d4e386439490d054037ff6fb6b99f4f377f90e7ba92663854e6c44e78a53d155788af810307cec9084a37ef6465e1ed6d6dfda393558c92ef511b64d13d2c84f574291bec21de92d2c9b11ef4f5516b96c7c3e78512af6190c63c6fd5a8c6bb033d5577dd3f466feb594874f2be1bb9c866edfd212c08df5fc8bc560e0c46a4cb0dc6afb8f6756ab3d523696dce3d977cecee8dd0c9d9b62761169fa4c018071ba0bb88eee1840ac901375721219edcf1902a44f3266d75ff1f9eb8bfb79558521e415ef208fd504522d9eaa38ea7656b129f4c60911eae1b7d82742322c8490f2240f02d04643067237c6e04f1fbd479766c51910a5f3c22090a7a83286cc00864de81c842eec31fd6cb84754d55d913209e540888a57e4430faefe30b723f581215c8f383c58283969501638b019bab047d18d5e474a11fbc6a14315fbcd6c0aba0d75ed53dc65c248073a701f7fdf0f6ff3f7cff2fbf1a6043e1c232cf928d7f2736f84ccd1ed8001b0e3f8f4978d5b4f465f05cbc44f0faf94fff5aae421bbeac7af7efe0ed5f93b6725b5667a7a7454e2c99d7e59c54b6628f8770a63b2a2e42eb4e8055d6477aa1a86a09afa253da1feac5b24c3f76619d3bc227fe75da8a4a5d58bed4a981e7cf33b8045aea3aafa5e7831598f9276d05029faa82f7fa7919ff26b6521c9ca617f878782e703848bbca9b2b28d9383a889bfb28b23bb8f0d98a3b2adbba4879f37ad8dbbbff4432b4ff82b4154e6e733e0b5e733e4e1313131e2e1e730e0b0b6e4e36734e4b0e366e6e0e0e4b760b0e5e0e3e6e0e367e0b5e7e333e133e2e6e763e0b4e4b330b537e3e3e73f8982e7f79da4ae18eadc1ce49093f4112bce095ba770c68eeb27269623798ef1cd6c4874616de656a475490b964b7cfda661122ada6ef5da3383f22db4cb916a45d83cba270c8f757c8e288bf2af889f813781ebf5a84ffccf358c819995b9c19f9d6b5e87059e6dd0e63e186e444de9030b7cd795e87b6b3e88dc7517d430d8cfc1f8da2290bbcfc66199a4712aa0d75b8febb551f6cf2ffefaa4fd1c2cd44d1c4d5166ed567a1cac52e63e5cdc1aba426cfc3a9a92ce3eee921a9c123e1a1c6636faacdabaea365a9e0eda265ebe362a1f2f78d1f4035622abd66ec6d2c4b21a1d77fe56d9a1d42552e02721c8e390ba61a78daf3b8ace799f2dd39a0a4f7f70398526fe23bf9e0c4b80a89ce14323bd63cb2df2925d7182bec2e919dd9aa16bcb43db14fa864185048d2843767831d5be8fb170a624255fee07c874a6a2833dcf9c52e2da89884baec4525232d5975194955312d23253175594d2923f58bf2524a7fc37a04d8afb14a4eb96d0f9d879db822d085d7700a6fe89a1e20c629178379358b0f26fb6072abd6b426efa5b44a88ec775bc66bf2869854b434c07affcf6d1d722bc9ef0e78fd20ede44d1aed0b5d3a4fb1ae9492f1bd9eced6bf7c64a81bc5e383c0df68d41045ade931c53e17d7a1e72dac9464f5183ffa101345048c0d08fadb03fc1ed7fffb037ce9b3f9b747feaff96c4cdfbd919ed097253b99d86d316539e817a6f6a0aef17558e1bdb5d9b2b74a5b8088a043e639cc38673d31677111c975fb38426437be895fedb3d16557d1d156e1fde1d5bb40edb4effb99385bec5a024199389a43519a3623a4517c668d9bf956aab52fae0a68ccdcf114630d4f1cbb672176c5a7ebc1a85453cf987c49be43e5544a27edfdbfb07a431ffe710c02cd55ac0b5e86730af083e0cebf485de25c388cd81d3f1ceafb4f2e7a96767ced703e8731b871ba0bd30e4739b52f7b1d02f9e3084bb2913e0890507eaa284a43eef64015bb314bdcc1b49bdc705265d6711c4befbf717d5fd43f7707be0973bc3cd063f66f99fba142e1d1ef8e050bce1301c8afebffc99a5ad37d87a3fb6fba096fff1b76a1d73faf2655d090995174404cec3193cb6149d32325c7191026516b156f2d57bd49ccf73845c511d96616aa31c7c17f404387383e575682f319001b5d7266b56ae9003bb97b1cba9e9772a10f935298b6e11843086c17913482c067f8e96d8675b910e468cfc0dffea94244ac62e385a3501af40651d830137c7e54b08aef70fb59bfef40f494155f337395fb51554ae0923fb0cf0512500eea6707b2f55e1ab17e3f3eaa8894ba128ea6f154842cf17d39480962b00350d052016026a6f1aff6231fff997feafdffed9718140a85408875e2acf2fe5c6da38b6b7f20df05de17d0f099703163eba7102e02e51d2cc55c2e881c6dd95c4cb0269b3de702caeb36d9bbf7968fd3f72b25b29297afa2cc3c497882a5b9ae2aa67abe1af0fbc73ab026c2d952c03e62ba1061c7d1a97b65c035df4fef1ce9d2f83b08177711ec027fe3c442309766cb7c91348465d4e8cf33a53355caf4bc85b07332747bf639cec2e8d345081e9ab7a0359cbb31c0ff9498a59105e58a3a698ab64388591eaf4ae45ffe7ec22936a91b72cdbd4e3877ba387f2a3273dcf1fc9f2aa0f6b308fafe7079be064918859735037237dca93a16ae6738aa8f9f65ac83fe8b49c32f97d159109a247139bd792e3773c9efe1fe39eb2f5f6d6813c5475ee4feae8521da4c1cb2cb720c78ee0a47b13afc16d1f01b89bc90ebd7110dc7e62262552568f82740e5bb8be5af0f50d92554090c46bbef134c75f54615a1e49522cd4e389d1d28f04d21049041450631d8d0405e1a26888ac501d9119c003cbf780b498378caf380bbfbc446932d432a39b7cf3d865345801153fd53b81f7ec7d8b10bf7638f5c9dce0cc4324be84d6727d2e1105cbbc2acf9bd9b7f46e17e76cc58cc4c06212defa47a9617eccebe6f88dd55c7b71dd6793f8d6d996811d52bf8bf0d35ec3ba3bc49f49a3cfc7ee2ea3b5913da09b488fdf01d46d9e9897d51d29f3452948c7b667d8d0d8f1ba65b6ff95eb82eb13dea6dfa77cbf771c9affe91efe3cb37b286eef2d9aed869dc6b0e67c5632f1c8c99f51f9d9b4d990f4f4572310dbebbc406ca57587b041e088ab2b1b55622da3e601d930e351751eb67b4b8842ba154459864a482b246f08c9d7ac07590d6e9ac1f0a31a3946fca5ecc07f1dcd19f25dff215771658438d22d383dfd371515d5ff07b12a4f48ffefedf93ef1d710c24b1d7ab3c27c79f973ae553255798707f973b8921fdce0927bd1b70e96f47737ca9a184122cbf0bede5fb58f2dd8a3b8dd52e324e4158d213760fb80b232125157e92d0eb2957deedf3e945d741737db8d25e042d6d3ce7c5b597b43f874af69346aa44cf42f5ded9395447d24539cc02c5573c88a789ff91efff3df90efff80607dd3759be5f7f8318cfa0e455d8157fa75fb93f91addc1a54725d6db1d0721e10229120f4db580fc2185bbcaaf349ffd5da82959837fbb494e8d2aa007a9f0b44ce837f0453f7234fc0bd558a6dc88a2c8ad9e6bd9fa5bf995c25ed1874fd3844dbae467450ce23ae4a2fdff947befff7e4bbe3ca098f790eeb8276fe092981079af40b58ad73bf52be5b60e6f42967765129153335200ba942f9fba9f4afbd1cacfd5d391e0724fb3fe568ebbdef9c7deed227952abb4ac199399348567465ec05c81d6dfca0baaa67223f4b7fdb72299e6a552d8055d67125725017adaa344f8efd23dfff7bf2ad5b975da0de482845d55f78ed408683333251e6bb0e587ad44c2f754eb86a5a8b45917d5055b966047524301279fc8156d0512c77b205fb440f22f1784108b2c6630d03743c63ae1aa99b9dafcfc6580b39901a2a8cdff051afd863f0ea6945535df959fa9bf80d0d0f753a9510f45e0df6c54f1b6ec8fac385ffc8f73ffe1378f5b76d949d5b61c183ac0bf62e46b21f22d4f05f463218f8fb3b56341c4249ee313b6acd353b2e6ba03c2a63ba58e51e4dc7fdbecd29de627d40b7bce6a22ba204c2043cfabb01b68bd01f188c483f1f8cdf285785d0ac3b0b3d32bdccd28e1d84081134081401be490176cc061a593d8b38ab0b080a013bd7387eee2d1484ece9e8f073a8e498fb774142b317059c2e13bf70c2d0546a9380d5de83b760fad1e1f751fcdf8fe44263327f6c9308764c21feed776a3e860b497d6280f20af35dab89c9a79b23ed10ac2c0874e317d0091ca1706445e5fdbe8b92c7089ba4630022011372b7922524db7dc66718692355c4e80be549508f9ed43b2a81c1b0d69321e8437d245f471467d254776bbc65c80fac66d976a5e10fce7f99ea73547bc80810e3dfdb6c410d41efcf53f8bacd2e1b1a655dff8f837d8f1c8992838414f456fb8e5e987057561b7611990383294687ab1069620798000445b57616fdf8740fca1c72f9f2b78b0d6658b4ceb41f077f16478a8d2a2f5b1ae6bf851d5ced58f31283046ca104abdd4709df62af0a7867dfe213607dddcd5662ea152ea5e9197a0a3f58c0d2de8ffbf7b086df527aab533d74fb397554b1b326193e169c61b1b4f713fc1d4307133a18312708012cd99104acb0920e3e80333262d5d69654bbd59d869fe3b9d125599793170104946797a6d8fa12865d84d92f12304f6cd9aa8d9321813dff97780ab7cf8d8513ef5334acf3a7a8accbcf2d6fb0a3c2a9d7c0d60d6338d79506383783c7922fb0f13bbefcab828daf18fa4cfbac26a4f365b5d05882098a2f55194d4f3793353dfa9a73f99dea138aef0d34ee384315f1cdb8b655dc1bb6bd79f2b0a00232c51b8ca86a881b9d5998cfe1dd88ef627605069396ca5761364b52ec26c2399c80ccbfb0c3106ed4752d68c33893f95a4b94aebd9619806f4c3a1de1c9ddd3636d5d0f8da547dfd89d78c6dd0be0dbbef8284af9959c264e476b3103fed093e0c64f4c807a01e6f578739967f549f6f50c432818cd03e844fa003c4c8f3037d6395db836ef4b7fb2ff602ade3fe5df0210a72d7f9a621f8588b12eaef1a62e43412d840c1740a0bce11b678e0972fd600f1daffe36e827e2e48a00404676c430f7540b73ba4728ce92664aabe6e50c765c009f251b798cb0996fe0052c89a8f7e949fea1c92b48001ea8ae756d59aba1697a8bc45de299e4a8d291339701db403b76ebda8961f3d1a47c0e42fca0d31a06b1f4003e587c3e59894a396a6acd4325eee8fd4e7c9bd715009a019eb153774e042a0d5bc9c56b182e12321668580212c3b683d03c77b319321b91a20344929985dafb9e02b6106bc2daa28bf2e12f7dabf2fc0bc93b72392a2e03b247b090596ad358e92e3221bf5a55972ef554ace702448ba5ed519d4942707e11e41f41ef34bce3b185780878ff7c66656bdc05a1fcb4a526c8d64f5fb47aac9d07d0fb88e4eda5e38ea610baf1100445a2d525d027500354f2e98f01fc56ad7b4e725166a8765b3586fa45b700f516821b772a0d045634255090f71b1fdbb3581e305b7e893f20dfd13b2030bbc4f9b48ac236ce84c1441c17908891e84ccfc27fdeae74e19a03457bb4afddb5973c00be3e0f0722431f7fc1648673b28e2fc2b38728467401b5ae651b258ccc09b55402154fc82a206ce60f750c03964382ec77c9cda273d9b43711cdd74d269f869d5f05f041b9a1995db4487ce72ff94c82127b0071ee230545c097f8d502d3c5a4751ba1f5f3fc2703d19b4ea811dd00e44ac53cc25be46945507e8bf21423274ea58e560609a08eb5de30ed3f1f9555280f9a79fce196ffc7f6da32403d0c1da2d37c0f5dade357021c8c24b6587569b488007c713145a41fa4e3584b5ae54be2e9b8dac852bc501cbfc4cf6ce22c1ae29fb294f7116b7b3bf5ec5494782d203662e70e69ed8d871630b24c3f8153035485531d75807a0ff61de91b8e043e2981ee9c550ae549b93b6e6b00fd42674475f0b4b4dab09e966eb88dde61d9b12109c0c717d45d25ffe4ca84749c2e3d3d7e52a0db84161a20742a65e4b9c88aa6a37250c1c700bf75bf76fe612f40357fccf9c5f7f1bcb7f651e9c3986590db2e326ed402f80243723c6aa3cffa52d8468b75b7fbd49dda34a2f1fa933e741d99643352b4ba3620c6783ab9198315f0fb3d04dada830196bd062897e7cc32d2ced0dfb205845f6387c74cde9f09267d83fae6a4a2a4a387df0b2d001fa87afa01ef9034a2f2bdc8ea5ab7281b0cbac7170166e2c20899b5206eccb8b6e9d456e2158ade320a3e009fbdb8d6981351dcadb7d3c518f3317931a8a2b72e02c69f8642b0517ab202392c1f1d1fdffd6d8bded90c80fcdd692a2710ba93fbf122ba66bc353762b0a6cd24e0fedb85ad42ec6c97d96e5273953e5ac9685ecd325403f87eea6b7706bc05c7ac0b6111c825e40fe8202800be403691950b08347e57a04e77fd1b67127bc3126e01f8bcf593d133ba036f63b1dcc0d29539d91c63315703885df237e0589f51b9e8d76352b718e5d5b92a979ba3f5251ed0d843868f7c594475c5aa21da4d9b3616bf1ac0077f7d41d5f34416aef6872e6cc51765cfbda4fbcb0071072f10ea9ae8b003903b3f604adae91d7a9d7d3b04f049bf654f7550ab3b1f3397d72fe4e48736a2b1f514902b4b96f6b1a5ec9adf70a989209128161a03424413e0fd61747e5adfaf36768a6d6df4194e391930ddaf03a897229475ff63de8200d2363f15fec1b86b8bf7d80680ef7183518a1433247db0a9cdfbc4698ca27af5712b40328ce0a44af60b4ea3d2d7d5a1bc3883822ef5983d80a24a0dabe812458259a333f3a5842895abea3ad95a8a80f9438ccf330f21d8f46758bd7d8b6b6c25035948f04bbc5b3566bff9c69dfc3a8e66776b9eb293c86882003e64c4bae51e45f72c5e55a5f218fd48c26a43515f40bd12c793bd6ac5383ed19ba18dc9091208cec2769d003ef9d9f3294a68b7eb2d85effb12ddea4fbedf7eaf07307f9f30a9472bad6c1461b84b10e7f66109becc4703d4dbe855752b1ec78eea53edba442b5b12d52578e91c603bff104f80d2d3d704ef7c9f236c6c493c2fcdde02c047b9fa4e7fed66ad07ee26a63b9618c3f3410f1b5640c061b2322e9663e408bbe61dd87c180b3dd28917e7017ccc9e5ee89c6f06a3eecc49dec0bb4c99f1ca63100ad84e4af9270f61dcbe90db49fe788fa892abdac5661fa03fce8f1b579a08f997e24d89d63b2ab1ac3cf48d06e83f2883e26991a5cde850bcac69b97b3b0898767280f79fd6cbb6fe01714979d36693cc22b38da541c00ec017bb2f2ae46241cb6f15b5c510d13a18a4afce940ba867427fb9c6f5b61f9d8af5fba819c733d928e5b3d58033c7c69dcd3a0c992e87733d4a552ae91f9c733ebe00f0a13e7d47b6fa4803f3a3f74adf1bbecd4a340b4c1d403d9a158e821339775ff7252626d83dec1fdcbb46f401c017c81131777af5118b6bab65c4c5c7d3ae9b469d8900fd584a5a44a8e439a1d3b446a0aa67fe92fe92031a20ed343766a971cd9fa598bf0e4d380a55dedf8dcf1fc0a2e520d25f47a03d4a0b9d13cd79f66ec030a1bf06a09f1fe6e3e1f8d96b69398893afed68872dbf3a5036fc12b7d1d8be696cbff0dad3ec15dad3c8d455e99b1d00bee4b35713ced97e88cc277f49b18b8bb2aeec867f16c0979cbe51b9d1bfaf40c490bf8fc5898af3082da60110ad2f4bcc46d62ff331cee2428cb9c779e7acbe5a5be0f821c987765a132b8e480c51b326378442f0ae00d67f7afe0b8e6baec86542344d383ed1b364e3bb1e006ba27c4673cde50e5f852471df29c1b7f66406a229807a49928ac6110fb668771aa171a6824212103cee49009ff9593d6e8ec456bed30b11191f29c4b8170f6b52007cba6b0f1cd7fd56f3a6a50749b907a36fa253ec3f04ec7f9db2870b20776a58d13fa15fa85ab848da7685dff54b3c6f8ce5dab81384a3f23653f192e6ab9818f1513dd84edfe7f0647a2acdca66fb7abb36381daba0d9f55c9cbcbc1c6c5c3f6c41f37b9f1414ad717029797d89cdd2ea55acc384a0a6835f2576fdb4711d72f7b2d3670eb9df535a2b25f1106f0116994e1e89ae2bf1a5ecce67e9a52e08bf93754d93cf01f5deff92266ffcd16408c5a55b60dcf0df6372540b596d200ca33704a81272fb10edfca7623331d8390681f2981fe3eff08a8076e937a63c846bcd3689777f0e53deef6ded64c5065609fbb95e9a44ce78a7a12df626aef022835aee55564d81fc8b3788ac8e6e1e5ea6bc425bf8ce66e060ca43d82793778d04a919040753deefe30b72fffff34c7935e5716f736cf8a0258c5508c2a7f4b49d7cbb958fa131fee34c79f0f6ff3f7cff2fbf1a60cfc33096f262f5437ecb1a0fc1247f621fd8f0dbca441e1218cc2bce5cd94c29dde70ea20dc57f74653ab377cad86c8bece8da870858a2c7718dde7838d416a59feecedf31af399841c44f4e7ffa53c38e7c60f0192dfaaf0f2e50b6ee8577a8a55c549c794e6267e32accda9f72a4239c3465a3e25f8d91f1d07edd98f992a1e575d225b9e79bf31f64d5575be9d87c8f3ca98a967376fbecd5bcf7ea1778354971720c9b7db14fa6969945a0a59519e855754903bc22ba0d5d678b84736d2469944894ee6b208bb503aa98b65ce30fbea22e5adf4faaa070ca35e58585ec9cc19778f69a81dd5dccc16b7521d5ed036557ba4d140a2e7f899fb85bac41c1b49672f3d320d712a2ce0bd5ea3080d7ecf62af749c43aaa83e6e89ed6fb31665b2b110300ab8143663fc07744cac7206b78304cb67d627dae17b0fa3f6f3d3dfb9e5987df352df0d9867ff528bb290b60578a23b941fd9c2fd1bb2c776fbdddd20b5fac17016075258c66b2216727181224aaa2abcedc5e245e7407301210ab8cd2be271ea430d69cc15da6c7ac56c44400f4df366c905cac69aace5ca1556998564aa0e40127a0ca8327b6e1e66b2c55f7048c2175d3dbeaf37814a6802a4af5657a5ba2126746bd9e4fb84761218f8dc70c0122a6ce1af23c9328737a26e145c516cc9ad99edc8302a842f5a19cd856aa73d269a9fde6f3d5d42714d23bc300abd8efeded673d1932ec618fce7d4a7b565cea489f09f07a85e5ccf10dbabaf986328fbf3a351f50dc48870ca8b293d3d337791f3d2472422563fae3cdb91b9cf5774dbec4e77b3efa9bb25d440bb85a9b7e4a49a0af50f11da08a288a979693c59b4b36cc19180938b332eeeeafae01565c98bb9f1e82e93d19f7ea7ef5ab6dccccd851b500afa409f5801313270ad634521b5605c2bbf25732b58015c1f28278648142bd46776bc7595dadf7a76463ad0155fc96afbb0b3e6f1f6aaa8aaba017d025628c7857012803cec12eecc4a63dc76355b4ee45451f3c50454b0a48da34568f270f7e723aac8ff72a1aaf147f07719c0d200d0faf659550c9b1d597f3a6523e05fdf38246b32480d727c3332abfef320b86b262a5c6631363c67bfe4280f62fde62339d8644751ac5d5dff2ea848eb7e77500aa1087ca0c949419b9ac755b4b5f72345694698e260064b46431f9a6ed6d428515d6364b6676121262c22f000e8e2fbff7b81031bf99a74127431adf7c5336b12e1690635eef9f1bdae737aaaf693c1b7345e231120ddd2d40e082b0b7f29b822734aa97f668ee9f5aec1121b53d04ac5ebe7507f98af88ebab7736077e37d167c37ef230174e7cb1b49c1af50deb2e36e5eefcbd7bbefc79ed80ed8112232a45ec8dc7885847eb9a11ab68fbd752fdb1bb01527b971e939997f854189ec9d0ddcb94b8d0a6e9800fdd8e34885bb6213dad3dbc8b92b7d0d552184861b509d22b2f0d49c71a06f7447a3456e76f373adedc63380f959affc82b6ffc136d26a2361fac5b3452ea1afca015e654a44736c06eee711d3a9ecb7b3c613c5dc489f02acdab7eeaf0baec7c40534c8a989e4e5590a331bd49901f49f5169c5e5ae3637b3a0a867fcf8e7ba144ab73c0056a1563d26a25c68eca5acd1a80559140b5dff5300abfcc2494d53fa9e139bb31163625e3915347beecd802a60d4e6b42e8d5151f7b76f1c7a993d314f5430d30044e24d68cc8f5d7c736eed313b6aca992b320b982d56805397abf8c40658dc2233c99a63ce726db95a496b9200b693a2c345e7fc08b7bb0b2d815b254be1171c6cce03aa4464362c31d1f3a0d73d9c61cbb99a152b104e610da8227a9daf58707e7f419413bfb72d79f010d6b4e10460a4515a2278b66fd10cf39337274a273e73e1d1d9606015b041c6a22da66c671469268c8194079a170a0401d4670e2c62ae1d0979af3f0589dfb8c7fb565f24370970eaf3d27741ce6ef8c9d20c642d33592e3b1b330c1f30ffed045ff59879dc9e7fa4a82e2f20951ca94eea0d38e4c5fb582521508a74d1f015edf30db5eee9c6b3d800e3994de1c2984ff4ab80616d190d370db599b3c938802a50f1fdb82bd8018c4a785a4382260bc41de3f93480f551f931d5e2a0ed43ce7464538788b15865c3eb42805345c2dd98fef7182e572464339ee862597a60669a00debf8e38c78e86697bb683683f3d835858ccb9465380d729f73c54940c72df9a60a3168542bbe0c5f5d7d1809d9ca3467d0532514c33daeb37aa136536cca4f9bb80437a0cbd9707b56c6df3114d446e77b2763994cada0169cebc3e2ec50dbd94d5960c35ef8b9b4f86a979d801aa74799169f5774b7d94ceb23ca7b27237322f804905b0feb6ac87311a89166584ecda76e321859de7b58804789de553ee97385e563f0549d2b440f12e21d37d751fa05f06f89cf8d4366dabec9e8b8ff4656ebe09b7d80578cd5591e51a55ebc2d8a335516b3f5e6e5dc72c2001786ddf5e57468f6e62f472b84f78b63ed5275716f123c0a255bd74a8df9af248a9175143de4aba2eed767b24607cb35f157b19de0bab51c50f19142b43531991e400e8773e24d367922337d911c2735ad22bfddb67733a0111ffdd5cc5ddae48220635b8ea15cbf5cf7d1f0e0d01f4c3cd76b90dbed6b0e1aea2ad295b151c03621119c0f8a51a84444c751ffa457ae36f4a4dc35003e91b00eba7e421bb414da7c22bf435feb21e016a0726c3658057592bc6428513c3d89c41aaadca6b4f65fae1db69e0fea4d353c673714ef3d1538c893dea2cd9dab2db80402112eff46de667017923faa6022f445322973c12005eaf43ed8777cd3a556ed6a46b175cbbbb9dc8728112b03e52b3a30d185def1a3dcf2b44492881f4f06d2014e0750b2bbafed68cbcc973ab6a5834bc77a1e08d5f2fa08aa0ca36d59a9d61449ea0fa13d628e27cfcd69b270155b49027ae0be03c1f94164be6bb108fb77465c8281350e5ac1f762ea2c46eff441101f138a2622d890ca21b60fcfb93abf374972f5f94e1c2e5ab47612252d0c0009caae66e2e6d13276ae16651e78d1dbc57a4592e13b2073cdf64ed4b8865cb0ace06bb2f2591a3074dae01a0ff32dcc885763e94632f3fb5dc41e22d7840a1bf0be8bf0882c5535537ae1ccf40c2d00d510988fdf34040ffd456c5e9797b503a8caad2d7db4e0d234ca82f00aa70e61fbce02afe784d43c39b863493a22850bb2319a01fe7eb7de3c66c712db81a03630b084abd1e8f3002f66fa4065a250fb27ad484eab2e8b8e8c4cd2774f0005556ad48fa7ce5e81f5644a4ce9c7f26a390941116bbf2d99cc922fa05e68cf7b4f9f4b6e8d4e3c71a9c4ce4ed3a0957120af400d3cd9b5002b15efdc46b1c433d7c7734ea870f0a004110264a88ac14aeabe57157a1c9f2a7377bd4df34039cb85702a6cc7ced386d292c1ff1525e8e9e410caa001ca2949e665cbcc565d990b6bf4fedb03315addd08012c8781843bd5a813f3312cec257e853e42dd83411a007166c5a7b8fd749b35d83b395452a5dc30ceb6d3075094b9cfecf190495e028207921f5305a6d80e35bf3de09050360082d517366275d366dcd1484ab1242b6d1650b4f1307722240c21b92f665b303474888a9a22e534e0904b33357542be63dbc5bd4ec68d2353aa24afd80660eecc4508ea134a3c285f57fff492297094c47eff02a0e8ff5cb425beda90ebc0cbf958352744fd92ecebe700cfd793309d237c47ead1c3b00cfb07294c95e254c600713ec99c172487add2701ac74e1372c297fe79b82bc0b03f784ae0116d51d5cabddfd47499fd1eb1d1780fa028f1c3f148f35647f9f843071d6493597382d5ed9380a2af06831907a59d5406d90f6fcaf8b5c9745e7ffd1260ce06637479de4c241ebbc4aec8adc91ef366eb992960390f9c3b1573fde9da24023b2d2e62c66899c9401a200ef852170a1971127ebbe5130a4b46b3b9cac9c53640acb41c417b8b012b71b697c67e9e870b5372b13fb2e497387735571e16c628dfdefaade867b3e235cab4c380e5d6f0a9d49d89806a42649ba9107df691fbb16db580e52c39c860fcc91c71fe0a6bfeb28dbfaad0662606a0a87cdf62282efd24e7ac45639635521841f6f5a74d515fe2b8443b8583cf782ef4f2f13b947bcb8551851a02b603c17197952510649cb3472cb1b622b2a2c488c56500cef67b3b8606adf37552a3d263d34a67e9f6d1d400f2a7e2a0c6f201d2c77a5df076fa499726f1d9a0748003b2113914d7718c11aa325d5c362554c59b57e90a08828a510875d9fc60eb31acddbde9cf07893780e600ccfd0bdb3771715b35c5ac1c966bd1b79c96dd273701cb516c25c3fc0da5ecc29a35142bed8c2d7e7c0a0640d142a68da9965ba6ab0cfd947de48ebcd092076172d15fe28aa2fda7e4ad6b7c2aac7ddf9faf4118fad4ec0090ffd94e89849a170ab91033a1deb452c3e26def1540908e16461b64d26d4581286ef396125b6570ea2934c07247d4bf7cf27d4d8ea09a183e825e1f647fa1c30a1084905c1b9089e1fc519d82127f241f76417dc9af11104466dfbb154a25ea8bd916d2d494392c721d65fa3460bb787f3fe715c78db2cc4c93d99e1e8eba8bfa24c0a2d185518e01af58fc2b37b758a4531fc6f6bcf209f704e8cf2c584176792cb50993fcfac6e3334c8b8e6f0145cb778408437670c21c66d63c64ed577dae7575d602e6a719f52b2b4b8dd63708840f05e6b7d6b125f1212e80ed5402515dc7a2af6645c67d62cbb8a6598b8318c076bf2de9956911f4a6928b1991b52d226724a77c8218008f36806451e6dca4907bd699cb6e4a79cfbdb108b05cbff27a7b7031230c6d74fb740ad2fd69f401098063f1629afa52ade61bf7fe1b22ecf8dea11466c4290077d380754bbcd57808bbb1eeb60fd2846640885a3540ff27ad669d789afc2e56cd4f983bd2524dfca5f618607dcbc1ef95cb10a6750a38c35bcabcb923beb97f011045ee56c472be9e96aee1c163b53499aa76d5bb0bf7018cae86d5e20d79cd6f3025e5e9544453c29c6c2bc601dbc9b45abbb66d71d58efb0be1acef126f0d88c7e200b65b1df14f78a48a1c0b6b4d5275164f9e4133c33601f03df3bd74fdb4b09a34884b85d85e6784cf9b6b250fa8c051bb32c76012a26bfebedfe6bd2ed1e9e7f7992300f31b4a8d19c8cbc6a3ddd2d1fede7852873dff13ab00c05cc4c1675d178874e45629954861e0235a615d05acdf1f1d6ecef35fa59260dccc30d1697a00b3c47c080ca27057682ce93de152cf8bb62e191f81a818c30558bfcd9893ebce5ee0668228f1e331becd5c2653385f0058ff9c3a6ddddc467563058c4a825ab4744f1f4802b64bd6d728210671b6ad41743dcd48e78b42a7ca6180201f429e8ec91ec60e2db1aa58ee5e7aeab1b5882c40108df7f5252422d96b566e1e5731d0bb34fb22166e01f60f4cf8d3d3d4661930d95761c58361195477aa1901c41489d9b3744cbd637c0ae996c9ac57b308d16a5f038ace33477b394a4562c49dc9bbcd5770952a697eff29403ed1f062631d939a062aadf24e176e9d823d992005e86f096bdd03ec9566435753d63b95adccc1b34f5a52bec4dd434f194d204e25deb96d2032a2cdb9c0d8fc4c181044b1e7d14261945e588a8d7ef8bedf640a21351a102472f9797c86f96c99f9c35cecce94cb051a1d5daf01faabecc4ac36aa7a8845baf85dcea21216e224b75d80bbeed35b1ed8e5d1c33774581896485d68b74b23e6014196bc75e727f47b0d99bb29d17d45763ccaddae15da0082842e9517531ebacfe572b95c4c1b88e7a4652900e8df6dc4cbbd8808220bb1fe7c6df76d7b0abd22c40141140e83ba458a75d3e5c982c3e71fdf8e3dd590630770970e9ebf3a6f29e9c035526bc3ecf47ebfd04b420fb0dda514526c8f08a39a4a4bc4a670bf71d8d878f500102438c622726e0a6f9892e0125e08269e2a5d4ad7274090a3b773b46e4acac158b56be66e69228d946b2390b1f8cec7247e65aa815b25f2034f0e1cae3dd97b12014830d457f47411251d72b5713be71fd3b24745842a097097751f2e07ce259de396e8d1542bf5429966ebb50604c934ede4f991bf0fa273298e517fbe30c9e14d219ffa253e7dcf724e6d27ca8e4b2e9dc9ab98d7ef6acb43803ba8f68a088e99e84c6d80c298b647e9b92a85069dab5fe2621ef47ccc736b14cd2134ac9d0739196f3975016ce25da60dfa55c142f33e5d8e6f6cf3293b63de5d04c4ac339f76ec4633da26920ad394e2787cc32b5b650ca01f2c6711526294852b9948d84e9cad7f872123590130d7ed8a6c4eb2f276a8f7499cbfeace6ac6b6936b0f38aef0efc859617889d4acf73e3ba82ecc58a5cd4e071064f431d2aed147203e19569cd7862fff8212612c6e14b60bd50e3f416e27a3db0009462deb0c669554fd7b6b21a869282b2be81cb712c2df5c0b216b27f403b9a93ac4f9349f5d028174c599cb42dfafb5f1fb7036c05602ff3da4b92c41f9d1e08795bb3095ffbbe51b94e948b795ed899fc2989793b5f8462a95a7589306e28e1361706c824324bb28b03e83bca46371281fbf7fb6471ae096762c783f1c61f2035749449fc5bcdfb546ea9b675b961fe572d0456b1f5d751b4927e6e8fe9beec2dbff06d8ee0995c060940ffdc1942597ab4efc0c5a4164f8840201a4d3c82042830612f68109a29a7000cf67876c9ad7ff582f0c932155e3b1af4437510285219caae79fb4f75f7e1d3fed1d89407f9e8a22d7996330acc57e89d0ca6f40fabb69ef57dba81123dfc2d541b1f95e914291679b1a8b4659123be7b21ee287cc411fa5d4a654c65f895f6f229020786af8f169de4be25599936fa560358f63622fed973e42ace0f3f244ceeb31c6fb59b4257d6ed77bc6ea27f2330fe4544e334b7aee5a35dbfe23dfff7bf2fd4081ff21fadd9b3509dbf127596c77ca3af68abe47cd08258c9646c387af128d188d4bae6d89ecc0dc30b51b410346059a69ab1bada73e5f923b69e1bb39aad72e2e9e4f70a7295097c8b5b6703d6f7c08a45c856d5c4ab85283bb9065c9757dfbfecf92efd3d90eaec9dbb1a709f55e3bcd2bc90aab5351e5fd23dfff7bf25de91ac3d79e6d8f6f8c1f19c09c7b9a20695cd0e47b37ff0cda92830fc248279a4bfc568df4d154efeaef8abd4fad63a11dbc39bf5a8a8d30a6bd8a3ed3e71a77867cee655fc91e6547aa0f3b0aa9e1352d1bb2f833ddd93bc421843f8b96271f79c2ef5cece0b3330a113dc2114161357dd59effc8f7ff9e7cbfca5ad4bdb271af54579107df5ce43e24a6d8f8c2f76e66431a1a3a2ded04977cb3cea7eca2daf4f61691adb14f543de7d843a7d10aa7f7cd2eee9eec8e688850c4aea2394d8287afbffd3c55186d4596ed8cb0c04033564e28a6bbf662a370f8e4cf92efa2a917d2c64b36f345210c6446ac5736b5235f58ffddf26df521f0538f158f55f48d90556eb674867a74ca847fe4fbe7ca77cd2d6c27facbc997447be55ee8613c7e85902b517f7413bb38f0c8f74b5c414ec7b2a91a8a5bd7e3821e608e5c5e1a2d6f482f54915b5c24c491c478c18bb4bc449fd031d216794aee35fede03c55ce5534172cd91837302dedac6ef7f1a6d20c3c3c7716d8d6d2ae3ea5261627aba640f742627fed1dfff7bf21d7ff9a0d2713a84b0d1edaee6c113aed755d7d0987ee5fee4308c8f2cde809aadcfae5775cc9294ddd48f778922238e3596c5c157856a768151ece239cbf4e4bd7cc35352d88e013dd29f424f98307924cbd1df8754b5345bc345abd611188c13671fb25f9e29f21b7112029668c823183973a5a45eeaea14c8dd47926309df63956d80edc212c3a93c4e8aa88a7e51423244684610ac86d10fd76c83fcfe73f0b9b6205876d696d662e6264e6e162e9a1cc7173b806bcc61e714c9d42c4763e44bac539730bcded6ecdee73e0efe03aae5c72f2afaf93a923838d486227b203fb0428db34d545a9f26927e484576353ad438495a57251c6c5a1291ce896beb628f47c3581823d7e7b059cf98b6f26bd286f5abbf70db46cbc1008c0162dfeeda9b6b7fc8d4693626b7f98ba120fed41fae077c9c1239ff2fd603164e9e854ef14f0921de3a5cd34bc36dfdb05211b33d945426501c1d214b2621872e61821d59a33743e78f40b5f724e89a10e62bad4b2f321873e77764991e620c8397ec5e090ca6d2c00d1648f890dbd41b28013a5b77d6c2f5e30c38c6e93c83491fb354cee211743382dc032862caefc7639c868373f6a363d98707ef59f3cd537cc0aac87ddfb9adcf8f9a28b542ff5d5caf711f95ee1558fe1c01589f60bb08ecd060cc70bd7ffd5521d06cdc7eed3e6d9be4b9b082a9d4c53fbd12e4271737109d9c54a0892617e91876be65811f9035e3f0ba343daa4dbf5e328659c3596dbcfb762c75e971c4008cbaeb9b1c85475d3822dadf55b79d140bb6db49bd5f9ed39e508740342096764c5f64c8cbe446f106605dd9217257d93bb1d3a5c581c4974b67ed4c51c1a40375130b936f21e8583244624d26b218c288098bdb9850cfb1949734e059beb46a295d8c3e03d6bf0808640602f9da6ec11a9e95374d4088388ed6ff066e2cff90b3b9bf405cdb8d84659eb949d22d74a4d808028134412010b93ffdfd5f48e21e3d5a1ebba4eb149191ad36349a723906a5f2dfdad7d28e71180229835c61fee12f59da31b67cd9179273bed626f72b960ffee456ec8f1a34c288df94bb75c449171e34210f615a9134ed4fda5df45b8fbd7c1cf9f88cf301144530759fd9c37ec6ae59b773e422a3b5d22314e2540db01dc5409460ec13f69592ef56e2a0f0695ae85ec38e4260c8e96e8b6fd4bb2fd8c23cd4f824ba179c050d45288dfd887b22f648663a28eb0c2e6c47f573d9e310bca99c6fb4b9f416079642060b634abfea943fc7ee87bcc57c64ad37c1e40e48036c30ec9de776ae28e48bf2fc810db0a170a80d03c3b5075f7d2ca992700165f24f1f0f87c3a86d50c48ab78235bc9f049bd19f0b81aa2c3ff9cc6a4034631c7cf656f2d7dd7daa39ebbcc41679761b6db84cda21d019494afabb2e2de6bed7454e4507b40db05de4cec0609cdce160eabaf7558834040ef38af0bd26b0f2668870ee881010ff51bd7f97ea05ec7b539e4960d07a96675bd508e9c3e054bdcc4582976aefded42fc77d065065ffebaaf75b8f8d4b1978b41afdd946f87164fb9b1e2458e24e4afcb77aba1392096a8ab10831b467a6e891238af474f36dad531c98899fc42a2b2a10a0d6fce441a8c20e1a7b01f605c49d94f86f4dc99d904cc3dbb169694cf875fb93b84fd9aec5487e2238e9cd5fe078022582eba66691625866efa803651662a32ee35c207bdf8dabee5816d4c28e36c9a8d8ed4963e34c2aa199e2fdaa0ca7041a306cb2095168b3f323d3066acec696ade83de97a21ba43edd49479405e652053fba8eb717f78ce3c8e01a7be7be4820b3b97b75a246c336842c07f3bfe5b323095c9831a9f2598957306f9d2337a5996aaeb1d22941e5dbcdcf728733a940df8986a87e30922b288cdc9870606921f2d53dfbe757ee2a0d1b0612725fe5baf702724b3604ccde332f662ec25e30fa14bb3c40d133aad05f81786e7258dc6ececf74e4d64d274e02b4d36f6ce7de4acd32dc3aa84ea5192a0594d6cf75f5f9cc2da207cb893b917ab45b35fac38f349daa0a5616e30ed19bb453c6dd3b5db94056fb8abee7ad165720bb4d94522a6645276e0c79fc14b25aa4de70ca86f3c9cdf9bc9bfe674e995ff8e4efcb72adcee8883bd42844cb6e4d3eff7a36665f90d2407a9acdbb4b64cd91ccb4f77cd2a6ae731e7ef66290fc77f6b0e37eca23f0f3f95c7fdbcaf01124cda2e5385deccdb4aa00b6641c2e359018fd943a7511fc18a3c4e1ff8533f74a45cefe2499fa961b42022509c1551066488bdffc6f5fda6cfd5a78c3e039b96b7e09b7468345e49bbcbc7e9e3b1a2d24eb97bf06eed00078c410a5f8fb7cdb664fd627973bd82200ba33708b10eea33f82c79547a310d8b077fe7337ef59622739fc2ab1ce1dc8383cc154410ba6ba44938fb277a1cfc58f3e42f8cffbf5676f8fa07c6ad04aa8b0017580182e3970f073b8040a0840f3f820efdaf8d3f04bedf3f767cf3090d076b574f13274d4eb09ffe2bf3f7aa10f5cff436cec589fba0bf5b6099597e4f413249e8871446680edf2117e8f8307d53e6a01b3fab523df4179f40a1c3a95f32e19cbf50f8e6e76d00934b11b4114b3b0cdf1fe9927b8966acc15b9a679fe6407e1f03cee79f84f3f9e11d5fd871f4dff1f6499fafb08490ee2fff8ffb17d700a9957ba891788d3753e87b4b7eb6d0c27611bdc211f0ae5dd77436ff8dfb93fc945b15a279ae42ea3c7cd31d6a7c9c6ed8db9eb93bc6697be669f58a9ddc1a6ee4b50f6c80b338e31531894999f7ea0e38f591b5ad7a311f3e3d0939aa3d4b3bbe1138d5a158d3d6016f89f91d6b418ac34695e847049472a9111e08bea5abbb38179d976e2c1aa9ab919dba1df53ea7670496d94995469859a673b02d39d4efa6d0e078fe4c3de94fb4a5e155073f70b8048507061f7f18d874a55ccd6dc28ff0d0ae4f700f1b7a194b6d951077ef4a06b93d4778ec726e21cc4007beed24d8f811af7ff9bf3b775d1d49d6833f74b1dc0cf5b8296298ff1a9a058f7cfd05f914b33db4ae5d8ae044b0e417b2cdc21d6799594d2dc543a08b5970efbbc43c2f6ca5d14cb349c461dced35b137e05311b085d0a781659dc04aab32142f23d5131607673c8c189794642de9ed148297843a6e8b5ece0310b94eab86f9b914122c32b79c70d34224fcc6fe0d1c459916499c32db3b4e0f3837c76d46d65287f4ab2e1ede151eaa25033d1cdc51fecc374cb91b5c29896698ffe84bf85b195e872057036ca7221ce1ba7f6e6b20e4b796cf2ef955e6eae99a3b1c47991d8f57f6af4de657d8632528ab7b8efea4362fbc2f1af08f3221369c5c1961dd172790bc8cad16d0f099a6e4d6c42fa029f1b25f63e75c43f66033d72aecd3e5be733f601b90e67ada1f9fe8311f21c282c1a2e9f662bc35656955ee973845c20e64e59217ef9ce622c3d85c0069610233c0c97bf9ff63efbbc3a2bab6bee945418a14a528a27490198661661090de7bef32c00cbd7744a483481541aa4a534429d20414a9224d8a208802820a5204a483a07c4f6ef2be977d93383124efcd776fcef3e48fb8661fced967edbd57f9addfea1fcc0d1a733ec962909221a55c64132fb50a80f1de9b8d1e26d0638fee385f62fd74c920e680d735a04c36871f662777898626b3e1b2f98d0b7768f34e930281515305b7ac69bf236c2d1e1267755fe15355065501be1cefa733d9ed1e523a4fd234dbc66019eacf33c581f773ae13602c4e4ba56cdc0c397e9d36c8eac39d20804681eabd6f7b6b032ae30651f31ddf03bb2e8833f24099b4b0db1b7e8793f444f2a912dc51253ed9bd8b6d40ad653dfa32e57b3da961a2ed85c19c0327089e04a2812ab06b164eac7ea75bc56f15d9a9f94f7505c526dd038046b6e4f662bc3bdee8c2d8953bc7389e16197c8a00b8c0dbc725211ccdf49cc1615faf3e9b13392dc1dd039491be492348505255d4b27f2cc0ce8366789db0680fb0ae2d1f7586b8f28a971a52dd6e0bbdfd7071eaadd2c7bd729daec42b17cfaa89a47d41bb131b565f5889b3040a118664c2ee559496f5bfb4cdd3c3d71d9b2e807a01ac655af76658e3189997b752da1e89c85c72a929230302cb09bcb5675df3544eb4535f94b079f1126133721178fe15f980da979dc969e4e783ab447b76728e596b0165dc2539b9066216d985b2b7b40fe562dccabc733975013b2adbaa479e9eeb9c6562e10e47ac3af501ae5e20e6b46a9c572a68924623ee77a561970afb2c5e102c935e4b465cf97424a08617be933a91c66b2ae87014807aad9a7367eb8b759736b42e691bdb17179388b36bef95e79e83bf1afab8f5982fbcdcb86d516a74fac01c4043d1679361d39f7127b5828a7c3858c8ec1ae1cb2d0009fc2622981206d1bf4f7b6b4a267673f6e47511b0d747fecad9d85db22103e657c53a3e45e6fedc4b9ac0e1d13330cb2994b1f4a430472f4286a4d210550f0358b5629b4c8b9df377b955855c9e4737b56b716e1101343835cd9d911393360142f3ec77a664536e1d94ca05f2af3a7a71a332c645ca4c02d4218acafa61ad53c7015bbdc2327567743aa3515e3588d8f4f0dda6785125c0173daca37a526b8d867c2ef3d38167359e13bd2de340acc9cb3177ec2de1337746d7d57e830166f32baf7a00e3e4c4cce6517a366dcbb082a8e57b47370473d69a001a0a36e2dc4275a253b7590d84baef4aaa289d5e1007f673a557c3a88f977af0126594be0e492cc65f24e0017c1d02d1d5236b5cb970c7260623246ce088428d23800c4af0146e4d838495b67ff5e69ca13b12b8e9c80df482a1f6a0cbd432afd3a7c7aadc1f50bb6a37886e0768441607343f185c4ac722f0d46b85b50e6ea0889700563eea99730be873ba2107d2bc9c6c3c6e13eb1fac0758c156daaef72c3fde8896b3a1e21b0da98c44954a02344f899f45e35f5f10bf7f1f517ecaa6f785c8d0f9298086a6ca146998cd9cbb5e1be2d818611d66315d930e0079751419cf6490b689163ab6061313dddaa1bac80cb42e1c8c3e6bd0d7609f9478c2fd945e9e05b9cf394580a7fd764cce94ac4cb7353fbddf0764b2f1a738af2c8026e595f995a02ff62a97d3bcefcabb36c0f9ae2d5c3abe572efe02dff9221dbfea1855219d8ca3276bf0682eb0bf78db7440c4b226d45f2fbc673c7ffe45e8b18c47003e83fad0b3cd85b8c20bac731cea02f0f4c31b5de14041a8f7a8e059fa038a7227611eb5e71eef0c2c35eb0371fbce81ca068a722976a163efa63f4627280d79dd05c63b3cbe95ba76e3784317b1a3b6c3e382459a3427e07c3cd7032be95b0b4b7ae4783f98a5e0b4288cfe2a00d6657ec271f66ed1b14a229992633a5ef98d52d784011a9ee456f9d8a742e64fd96a48f4f0c96f72130c1700fba7ec5b890df8bd2ae23b82133aab5bbdeb39e9eec0f3bdde7654117e4ba31f5b41ff4efd4c992a6196094043a39cf695a96dfb6be0cda05ddade21e7e4b1a04480a645b7f0cec4f635cfd0d48162d7fe4f47e0fde3ce80f375843f828afee154ff03f3814b65baa570cd0273607f34dbb91a26dc9a74d371366e97a6f662725b5e37b07fc90fdb8710b0a86c553b6fc7dc9de0d56f232a064a35c79cb2ce6896d46ece9528e2cbbbde97c3c4bf050a85ca0c27d78318ebab3d8f79bba233ea24469fac03acb051ac7847359332b55e0f779c43c95dacd96a4403ac769f13164e8fea2e690b18041a3ff7fee2ac1dfb0ca0a1f0a84bdaf08a78783c5090bcd89ecc9ffb505a20e05cb257d954d6a9eb9b208204c7a76be4d64eba450220802c0c67139fbef7646178c376a9d238fde7e0ee84bdf287016476f99f63db2c76e229b33b530f091c8c077a421c0f97162a376db8fbae4af1e8c71b0f5eadfb5401e3a5630ebfdf4ee04493e6a3d6390605de527a1901fbdf2552b7e034d1f3e99ea752055f2e45ec0a73c502bdb4a037a1b40185a3c5633a4798045424235d3fad02ac81ce6f5891bc93277617c67b542ccf74730e3d6803581daf10e762a1b48e9aba66056ed6c14ea95a4339004dc8da3db3171102f2d79bae11d8d4e6433c5882aa1fed952be4c35fcde3619d5fcd5f132d8a71fdc0937b05c8e70cac4c3224196de7dc36a7faa44e79e97ce97024c0fa2778f304b25cf50c24d96bdc9fba6f43c023d103a0a1322e292c18234ec8515eb8931d6b1357396d5e06342a387372a298d0d0b033e2faab7375ca547ec7338700e76d3764ded5d479779774bc97caff75f7a88fc6198abd72ce9103bede414b1c78cf0a92acdd6889042cc480fdeb305559b7f4f3f9fc464e3d29b6c76f6b49d14de7f7ca83177389468688aafa270cfc9ebeab8b284fab01daed2e9faed03eeb830f7d6439b533a45734d25ec00ad8af573e3d24749857b477ccba54fe3200414077abf8c45e39c799fb744d855f14d34f7e4a737a277e6a60e00e108b0a5ff6e17be7ecd52d4d5bcd321815cf7fe5c86d80b5f428fb7409374389f086bed6723289b8876992f1cc5ef98b70e486ee2ce331b86cfa4c5de6848c6eae07e00fd18af3247e8aebcf6add8d28e3be715a738ba44bf64f746718152d2134b6f7f81f20f343deca534f99a59200add538ee1ad24c04ce71af44d0853bd54ba7b92d4280e38e7959b45444d31aad792551c181f539ecd6f875c09c8a1e769caff2348f5b583cbf4af26cb68ec624178080fa9f21acd08a9c2153adcd13a45e3815ebe0de06b466129d1f22ba5d9d72a429a166fe7ce39dac9dda0d60ba3de16726a56362c284440d5f2eced05273185c065a0f151ad7f8aa7a2296d632527b3e655fb38eee1d0148924d0e1eee4bdb32e92bf315ede59e2b0f951cd801586f968ff7d02e3e91237d42c5f5a139177ff39e7cb8f25e39f71dd32be9b385b2773eae0d430f3185ae966802f2d72d2104c699e3bbee3caaef579eb4bb37399625ee953f317a592bf75ce98b751d4af38e404a235d321b102bb9bb70d46175f4e2930334932a014b3d462f8a3b00bc5c07e731ca721a82cf0b23e568df2b82fcac592dc071168b697ac71a5c85be02b508cd1bab6cd9297bdc041c47743a12b3a7274c32b5e1173a14e6044f5cca0648e8bdd78b43aea5dba5cdf0210bfc15fa733e7c7d0898a310a9f75d3ab5dbc7ab049edff6b84be6b49e771d6cbd148d3f2b62526b689260c63b417df29945ea0cc04a35722e21e145fefcbbd34c3619af58168f1f4ef460048e832bf404be7e07bf4c49f8bda3a7a5db89683e0a40985f3c6a956f36bfbd3d33aa0f3f3171b8de33d69301d0bfb1436e9760e786bba8d8e1064d6aa3da284fc09ddbbaefcf697c9ed3cd2d408bab3adb7258ef461cf0fe4a7c2a214153d78e269767f0c81524336acdbc3d07cc6f05cc347dce5088e7a6fa8756be328a270f81d3102f4d827ed22578e6c688f2d96df72e756868b10dd03a95dc284d1a4f3a3a89e4b604aaedf10ad305ad1740eb4fa3a4e6a89cea1ed30fdef6475407222a37b71701d6b7405f9bcaaac0d1ac042c7fdfd161a2b8e78c4b00c9ef7d7aeb2f2b774ba5571f7a66b36adfd7a1330f1e045253fc0241678c2c774c8f8de9179f2bbd7b24d014c0428518c7553e73dd7c76e019daca6655923f9ff3440770dcade8b56cdb1245a6cd4884de62a87e0bcdd90570490fb0e96b9f37a9562a426ecb30844ad9df1d2701de7f96c81f53a072ed2ac7517b6cb9332f42402d0c384e33348f5c969194f43aef46b3a955e596193302055841e93b91f18376cee5a817e71cd8c3cef1f6befa0a90c04baf09913f7d19798c4ea5a5663df0c2e777f834c078439bd7e3fc87470f39dcbc83cce888f76f188802f697ad4777859466925f28072d2fb609d24a55886f02ac9987ab891c529fbe7472bbd1ac6e2abe3639fdea12e02e147b0c77914acd254e6bf5170ba529080dd209019dd0657374db2753a5c4569e1e2955cde94596c64f03f367bccddb2d984fb462b03122e6fe48f881eb14135065e2c1b71da57bc6278a555bf37a76f0f90763bdeb405b29c2ce5277db6745b6be311f6e605bdb7bb59b9680f5216cd0c538a150806e5f8df108eaf73996c0750f30e7d5961eb58cb6b9a3428c02deef761ffab082bc09845b6ef61f7861daeee312f1fce15a4910d5195eb517400676d7ca9ee77933276553d89a0f7d9d876b8c7b3be0ee6e1b895c3da4bad92616da9ba8b1b594a225290bb48e7c2e177f415db6af93bc6b2aba3d8fcf8189250330f7e74f5d9cbc70b92ef8ab83d6950307490a23540200d6cc9b968757bb496919b9ac2402caba44084f7ca6b2d92b97d95992ba295f2c6aa20d7566c3fbf89edcf01010aebadb5fd750fb55a1ab6ab6a61c015f7856895205529393032751f777c30b1769f0c5ae6446c97f460902c7794d44c51324c646496274bc8682622544512f1c08970470178cf6bdbefc42f92d3a3c4b676ef7f84dd09d3a15bb9cc2bbeda340929e7ebbf600f5a9115b7b607fb23432ecf20ef2896f9eb89c77c27eedc3938b6b80bbfe7e7da79c983157ecc145d779311dae778e39198039b2e23e7cd2e4b3780f6b674dd9a90a7ae24732ec00eb638ab39eea83c88731f576e8cf6865a5b54aae48a0c9c4f90007a9f86027deaec8cff70ab7ed8b1297a980f3531633306dc2cbde59c02419dad4171d951d7001787f8a6599d9f6519705210147c1ebf215a68aa98b40139b0b1bac55d5929415a7a628b008bdcf63aabdf55de0f96a7b0ba16102e19ff6996f1e3d1b755d320920d197db4a38b5764576e1ade7cad72e2e8e52f5ab4b80bbbfce76ffa1624c2c79ea09ee8bbbcde1143a078a0177f8a502a9807e0cd9d30ce4dcdd817b266f6c022e0275e9ba8434d7e59c8cc74a436eb6c6530ca8a13b2e02acc394d6c147cbfdcc631eb67945bfbbf8fe4be5070f20758cd8700eb53c6b41e3dbf07077e7d4e793cf55dd80f9db4a8e2f6f7d99821279fcac77f46c4c7291c517607f42a6aae899c569abb42d5c1e471623850a291581f07c82fc06f319959687ca6e3deb18d1fc90a46b7440b249d829318790543b6df464de7d5625c22cdf9283c0f9db02a7e43b2b90e35d43b352eb77dfef2a95b734104e59666e3b9172ca5cad39f9b60847e5167ca8ed3a90aad0c4875d4aaef37f27d9782a7f3ad60d4f0ce57076af7c63f1c2a035d961d58b1f9bd9aebb481b05f31203e19caf32987b1376f304a7866113355aaa944bb19d00497f1d8d9c171e7cf025c757752afc5d3c87f78db7017741f0aa7fed71f26ef5a2071dd1a2fdd3d35f286380f3afa1d188bb4a4482b45285e9520c57e750e6fb3a80f599fa157337da8866d1cb3246976fd3671ce34003a4ee611ed49faeb4e105dc1d620896f70daa7338c409b096ef50dbb39d5cd07cfb6eb5de3b3b93459376b11a08b71362c622f28e369fb2414d4dc2b186de395e2a803bc2267bbde61e4ad7dca4b42b2de7c5047c2dff04e08edac4b889bd981a7350218f4b475196903c0f1d00ce8fed3bbae20fbf0ca97f210e5314b1befdb6439c0408373cd82a7e132860e616a9e1efb01de4dbc7fc421b7097b3bc4dba8ce6cefa5b1f51fe58fdb590614adf0760bd44b4e96f1fa8cc4c80e17542663e0e1d25cf6f03dcd5e3299f4496c96e4daa600f151e17e02e6de866dc24d822618c9015513529fd27e9220951dfc98a97fb032510ff2c4b786d6618dae2a5f12a6a95eff655ad1b01e8fcb584e12d412447606a03cf62e4edec0bf4125b7cd5783e17a50e2b9587fd7a711ade8f8f0b68d671a8d2e7ad1e878426bc08b7c391b49fafe34d7f0726e27764710970802e70351c2466c3fb6b5f847ff2fdc9d4309e6835b487c3ef1c0fb773c57a880a0a6a39618c0d74841474ddfc3c0de0b29ac6de9abe06b6f69af28a0eae6ecaaa2a7043151f4b77776b776979615bc33f105fb2bff5812fb94fd00f2e948334c4d71a82810a0961b0426861b8254c0406472244e030345c584844188546406096300c1682b142a0d030181405c5c0919670a8080a8eb046c12068ab7dcd0f09c6db49140af9dde3ff80f7c7c2d170281a85864010084be40fef2924226489b012466085b0c2c2586b61a830048240a3305814c4126e09b5168659624460482c162582b112f9b7be3f4e1483cd202583415d7a45bedf5131c2b3be022649926a7db5ce2fc248fb5e1d63eaa8b4d49c23335731c45e862ede61aceb653e6652e724f43c37729dc582ddbd82ee33ee12e72d62b7ffedde5b436c1d7f97c46a7f1f8d000f13c7af799ce3e179bcfe949270621d957e25caba48f69b232b5b8b429cfab2e4fb5b347f2cc7ebc84bfe72df0ba72567b53d63735b994b580f647e13d0caf62e0847196081fd3ea14d385fc90ae38871b7b273b772c4fc9ef134f65e4eae18f7d3185f2b5bb4b30de6b4d39f08edfaa53297ef915f34f5567d44b785cd880a0dc856a2e8606c6620fd9eeffbafd709aa33b8f47b7c7fef4f40463a7f12c8005689163bc24544069d96ce5caa95e5e89910a7aefd23df11d7fd69a163556b9ed7d7bbed6472bcb2fb3f28baa817ef678e70af019c2a84cb30fa065234110fcfeadb86195b0d5470c1aa713208efd981878e3cd61b6f2a19b286956fe0a9c2570d7f69b27f5109d906ebea7d7f69ea71a9ec8fa89eab7d74dfd8748936fec71eff9fad97e8fdfeed71a29fa1566d39adeb0e0dab0765903c62bdb5662b46d64358443f3f857e9f776ee4717cd5281bf1b3568a84dbc2b379cd1fea8f14687d8f3dae4f5fc3cca62e602ccce35e565cb80d2912a709fb6bdbe344787fdbe37f903daeee6a672923658c547110d24328c8cbd8cbc35451eaf23067693f5b3994889e951154de1de6a7a98f44a3feb8e7df2faaf80fb03785911811181a6265692d04118259c384e02894101689c6a221c2502cc64a0886b2c25ac2d09648341a86855a8b8820e05838042a0c434161706bd45fdbdebc60431aa20fd5aee7fd38320ef7905a5219eae7328d80613ef6f511f45eaae09662a616b66e2926fccae9b8bcc1bd3dc946399dd638bc7cebbde358edd4403ae16f40cd6a0585123429871d685da8fe395503a12245f359c85865d6d854a0a17f8c81d18b5c835fbb17f760a603b13bff0ff7d409da4313f1f3bb1e62223c583938f32026f18552d586b8bf7ba7e937cb1d485efaf6366c11d145113daba45ec35b270d3d61c05d4344540341ec03978e9fc010b1d4816303c0a1a1b4cb38c6e3a85b20fcdd8465441d5c573445bd49712cd05ffdfb446d15e5b76b6d71fc950f6438ee0f9c00819c59d3cbf0af6df90cb1e44774b85732c64c70b0dce1e330ee08d8f67942e1aa1ba1dbe70aa5fbfe77fe43c7ffd52f297c7c7c3cbce1cd8ef267ffcaf1625b7bbbbca26dbf36c08fd090fabe3f011a92b7ceed6f1626abe8cac446aba03d3e3967c8d8b7576e6e517621468986fd0be5f98b46931383571eb401a93bd6a4358b0c46ee91dda7777caa1e2208140a4c814fcbc45716582adef082f0f01905cd832ea4c455e300f9daab20b90dd2adc7bae7ad5973efcd1ce57c9ecc0e94a98c6c446f0b929cac101cadc3b0dbd25d8c961b072853d9a74e531a6e9f684b21fd7a24b8e95539d5b9edab7be5de443943ec3d705a0352c42cc57bbfcbb0430d40e89f9c720c5f102a0b5d18666083dfa30fbd61930354ae579234d79437bb87f14f3d50ef721fe9bee1dc0d402b8a254e5fced757a72089e83aaced6b6fdb785d0d40aaa91a615e9fdc759658b935c350d2a59a93e62a0ca4e60e12e6b585a76c39fadc317a25a167813e248405a037f0bbb51b49b18a210477959e4c14b5fa08ddfb0c54bc25de3963c5d04cc4f744d71e950079ee446cd2032049d14753ecb3eec2145477f856cedf18a4e95a57025263a2b7b36eb1df2f1e5a303912d7c849a7105d3e063cdf005d812be11799a4648a231633371382e9cd9480fedf6f940a45eb8da71c7d7b965dbbbc42fac84d5b81d466d8553f32728f569f89e5ba3c173b9ad31af45980fc4006b3c5196775be1aa7a6ddfa7ef39487796e00523824dce5d41aa3dfb2b000c5e4829ce568791519d0d2c2586f51da7b7bf38b1f6cab2fed682e7b8c122fd084c87a235b5e9cf313dbedc26e11fbbbef8dc7636781868a8df07b3b81131a7e4aeb65f68a0fbd853e1abd0690c83e3702ea92deb656855798076cad7f212b0b4e05fefee329bec4da3b527d1f97aebce5678d90a0a62a0252c75cc58be28fc5894f250e1f1ce69fd34e5b79cc0e2049c385b5a6485da7a9c6a94f55e70ed8d26b98390395068f1951dcd792ab88db3962f33c2490fc0669ef81f85ad8daf6a268f77509a1ab934e53ac9366c6341f34f6ca77cc94c752bbf8ec9ae4cff507aacb5e76e51400a0036637c8d72f518f25d29df595d7be3b6b215dab0be8478ee6d3830cc2b75ca2da20ad0fd53ff0445c3a0df4d72fbe51bf3ac7c4842fc577ddec9db055c3973bfec0f7a567efd3a80b67b941e8caab4e517e439675e8229040711971fbe86fc48ecca8eafdb2a0491764a4f10e28f2e488aef7590c852222b5bec8786f373257df57061a7ef10cbeb897b2d052808ebffa26e76970f8eb13c140a5c41946cb345e46b553ce4d36071af196976bd2e6807e451ffc4fdc23dcf2bea838422308b3d0b6346de6011aba3ebdc0f3eaa04c7ad9975607d7bacfc75723a24a80d60bd95d23788f3f35cfd11d9278c93bbb4a4a773511402a2eecdade0a43abdbb11e1c0b3c5f2376e8c10e1b1047212f7214697ba57cbda190a9566055df8b8c551c48fdbf0b2bc23018dc48dca45fb97071e5e66393b36a4043dce392478a6cd787336975bbe5027679224f23ef03d09127bc34d2de32a487bd5594d5b409ef04f6d61e00a031687e3eeab9e1c3eec1efb28e650633e4b188a780fdcb21d0cafb74d4349ce98fe186d537c2da30fc80b9c41da054861d9bdb5d965971866ab726b9347402d0982d9fa3903ab797bd1e493d6cfd1b97d493fa2d81f5476e3c6553ea7e0eae101cffa1fb8c1932d92a11b0e79b047449f219b0ab9ce17738aa4f42391c09a040ead289dd6ff190cc9887f484ae23de8b42e435f95920bc6e671d421d9f47f150b964a65b3cdc8edde4f96140bfdce96b0a9ea5172dfad0cdc1de6618c793528400e654fef583599196b927535be79a42fac59c9f3e907bba57ce1061957199defdd9725402997607a7bf2a1331d03058f8dd978f96ef0f6d2ea19c7344ef3e108d94b001f4a3b4a341a47e9b48b9c3c5242fa3c0e8e310ef02006d0caecc34d7ca681f308d7951f37c693451a7ce0e28b4b6da3954c09859982a26a078bcb5e9583f221b0b4067bcf3b807a7772a634542e6b59b960b1a72ecb40177806be879373989bbc4cc7509625daedaf36a5cab4043c13bdc67cbd567ef65dcb0d50a81dd399c1f66f400581ff5771fb67c8a7209821d939a4d882a317959c6fd6aafbcaefe9d5eb84759c78985000a0a6a7e3c36666d20cee66ae4f4ded963e3b20bc2770493f8cc95a0ec08805426f5bceaf87cb07234a47660a1903cd225b9af0980aed072f6902b0c26ab67d7bf5c36af154d6163ee049cb7514d520af62386f9c99f037cab19c876edac2681869b77c57a6ee2bfe3b0236aeeaf7bf9b8b5863efd0650e923932411794afa725ee35dc6a3ab6ae42c7e9ca4000f9b862ae2da45397387a2f265baeadd433d67238f024867c9636bf71162da030241b7f9f9e92a8b634b3800fb492a53b2beb7dfe7ec70b82dfdc3ded1657b7eb0d265d72340250ced8b1d7f71337580f493ba72762510277dfd8239e479c7b3c6b845c2b7148745eaceb9b101eed274969573b4d5b9335e537437d0d647d7af5dae00a05d26a32b7d552305c7751403ecfb3ddbaeb95ccd062afd7cd257d2fbe11ed5b746ced90cc9a4b734ae1e07f47fd74506654fc7d42b691a424c24aeafe29510029c2f0f6f3d841c9a965e7acb4320ab8dae97c7dfe103ecc39b495eb96c813182de8785fda2735d175e3da5051a6287b72ac9e999c454f45c65e8d3c99c7ca7741c7e6daf1c663c1f56fce6d6a1c0b94609b5b1f682c48a5da0d2a0d1d795506a99c484e91a89776b4912eb49916300b443c76a95fa09c1ae7ee835b9afaf855e40cfc1fa016eeff381ed2ca6f3c7c894c9e88fe777cb7910b3dc04faef3f7f9174b1cc57b0fbfee6414bc57ebfa727ef02c851bc167e585ac565c7c42c1d6f2b2f122996d6e7a6c0facbac82b38d7d15d6fc885195d3c9fae0307ee1215049e6659be9961dc897935312d9495907531450ab042ae1de07eb17099dcf58a2f13dc841232226aea6dc0e40234d304d2f239cc79f511231cf5138e78bb2f41a028c0a3cb14614f8a77298a51e129c0e98df08f23e6406eccf310f9e791e35915b6edda11cbbf496fef2f5470f00685dc9f29b566a573dcb97bee36df04ae2db87f35f02d02434f3f1a5b71fc8bd21d14b687523ee0f5f02a38808b6f0f7f480fc3fedc469a0a4ab28ab2d65f0bdbd38ff4f3b715ad00ca129b3028dc20d6cd11271bdf4cecb73b5bf1e5df9b176fbc7d2f09fe6f4e7a5e13ffa4a031e8454344e6333eb15a24f2fe77efb8933b381e69e33c6d61adf9c20e197530df85b445e91f4aae917933ee1852d3870e3d51059371bea06e2989c5f8fb0ff742fc91167de7a5e47b45d36f1f324e3fc6bea4b0ad6c1949fcedb681a95d7ef6ce370af8900e5f9be5275fc0486488a1b3874034704176c50fa5d190afac7cdec70765c342c077e77fce8a7f9c5313e687f19105c090642c93f39be48d77bb0f61867f892ef43358a976ff4681e701b7e32fe9ef8d57ee367fb1dbfdfe7ff378fffd3af06822dfcf2a030235b41bc30a3e0e35135f87909a26a502259e11bbe9f128e0d8e6ab4e5db2eb0d9eb1a20229fc425bc22c84850e33c6cc27991db6ef0f62be942e6c76f2f683ba60d705ddd10babed25f18e9badd7d8fd780bf5bdcd3a54cb96adb6a2eb682a62a6c04d35ec0ce79e7ca4b7d2d9fb6b39ffdc85d5f26c6074c21a26ca20b877a48be322764deef7e4474c6c7ca8897efb13484e933f2d4c56e4595c461d5e3feef9f70441e2d83454d88366ea51a6d473d617d4c6d3e3d3bc57185eb93e85afb4345d6730b9c5a493697268a5f574a690510dc9d662fc08b3ba12c8f20dc3a720dfad1e1fe5ddea466c6744dcc64c2c5b82bf0aca3a7bc94c35e9fbe41a447185b3ac27fd116b9b1fdc4240b5be5a86147ea7f77030b219a62290adb697c3af5853ffee9d14c8db22ffc2fe6484f9fb99555d324305c0d7ce3d8a20333c54f63672bd950f1618f7299a2e618093f4f0b22ebf187fdb20cbebcb2fe3c704ff67d53d392a3c9e644a4f80189a349dffa24a426cfe899244e997e1a91cb1bac789b4a79e72956c4a0449c97b46bad5b37f756423bd41ec849ffc4a4fd27e5a4abf3d916efec2a5d7c84869dbd95f7c02e6b7dd924807a73ecb2591883877ba9536e92c11b3da57cbaa7e8613e6bbdd76cdf9393ceb974e6195b794489695798930a599ffce76895d6ff9f72d2bf40dc25fd7b4f1cf2574e625f23ff6f57bf391f8f2bc300b55ea4cb126930de13c5a2e63c5c21f2031a2a866c066847478ce73e73d2964ed27ebe5803297567ac8c8c97b3978aad8e83be1dc2c80ee32a2bef8e7256c0e838f823756c35317e0eff5139691124140e4142add0d630144cd81a2b821486a251d66821244c04256c8d144120852ce170ac080a8a8463852c51d61088b5084208861286a05010cb7dbd3f19c6db4914290c87fd59199dee1efddbbb074d9496a378d36fa770261806387bbe38c1ddaf7d6e685629ef25ad158f0b3f2271f955b4f79b07026d4df5ecb38eb39a542ee665e425d7efdd598afb0d39699f487cb6cc367f8dea506ae2845096d013d5d6c115d315df634ba6fbf5d43a767c8eeb6e62649a39d81c1f1a76e69bdd890ecef577fd8cf0895ba8edaf41f874c57b51de0c7e43e6510d7bec013523fbfaea9d13f76fedd49548849fdf5dfcd4d540d0f3bffe48584a5ac0ad7fb4e83213feb145575710f10faff28fff6920188ce45e62fae2a969bfb767435303c1d6c1c4a050e6db1f7ef8afe660d82ab340d5fe960aaead18178722ae950c268f0bf0aecb4bce486bb287ee9aa0a6785ebd79781bf23d725c0f906c31cd5522d2aec6db2c8a7a34f1d5d78a3aeaf5af8ecc886c72cdf982ab131a60fdf72405dfc9b5da446bd8089d0d3c708cc35f26dee31b8f46e1a6878fc6712e917c8f8e7dd7fbfddf5c16fb1cbfcf720152403fd1d0cacf13d7f3b686ad671b24e364c9aaac1095fb7c3e1cce190e6c071e89ecdbe3bd33df73dcd18cbec8843d1593f5382375b2c039bfdb607d5a779fef806b0d077d8fb9f54ba701fe0fab90e7874ff9e873633a9e4851be750ad479b20719c3bb5cffcfda920c8f57c403540bf8077e498f7f696d4c9f94c96678ce78e3a81bcdb060860e9a59a6013899b08e82bebfc11c002fdac53d11687cb679398be2ef5963dfbf867111a3e26c70f25d001c5c73f67d7b20fe31f2eb8af7f6b74608be8b27fa77e8ffaf1f423f69dcf78c0fe74f9030693fab6d5fa65bbf40c1ba7bb227e4ee3ecd59e27d5a165be7d9755cbcdcad30eca2ec96180cd68f9d9f5dcac9c5cbd953c9594f47965d941d721a0211868940a01021041205872120c8fffd8d869727f023181209850841844544e04228767e766d0c16e3ee8e7664176567e767977744db78b08b42f9d9959c3d3136ee769e7e4ace581776d1f3ec2a183fa51fee0365e767d7b1b371467b7ab9fff048d6f2aa503f456b4b3b55036d77550308d6dbd2c90d21e7ac22037184fa68c3e150752d2d3b94ad3256445d53cacb4d59c909e922a48bb576c2387b38ea19a8bbd92af24939baf97a2b18e909fbc03d5c5484bde07e0a7edac69a8a6a824a7a0e4e062a0a68576b1f4b3f754f791f4514c40ae9a7278bf13246223435eced74047d647dadb5dcd0ce280d7b473e37277fac8a9fbe33c2de8ecf455e85cf082a6bec8f3684bbc8b940b14a6e8ac2aeceba68883f56dad21f26e52bec833046b9c391b6eada0618653e2f45236d0d452b38ca006ee76ea06bad2f84f6d4d585d9fa281bf209eaf379c1659c0de1281f17795d55636f0563154b292f79257b7505356d65576f1f1534c6d0106d692fc8877641ab186230fe8e86ee7a6a1e5a2242083d8c92bc813c12e521a58180a2d00e0898a3b5908a9dab979695364a5fcf1fa1a1ebee055114715591d346a37dc4c5d92f5cf8835dad5fdadb7031acfe7efbe78f385f71ba724463e72c6dffa8f9f9a5bdfd3730d07e2b73812b59f2b38b6365b581600bdf30920ecf33827e1cefdf9368d2d1d3d45435fade34d3ff71a289a9e8b29ef2b5e735500b812b5fdf7a2a0fa655677f238afd8fe9fca97a8ff0d46c28377bcbdfd57b7f57effdc14afd77f5dedfd57bbf3085ffe5d57bffec40af2bfb9b3ad07f3b294ccaa5d940b045a015144a427839f4d80bb31a02597b6f5a209830e2b2aa6696c45fe753795a9fb6969867c49273699f1f5112ef3ffac28504c0fb599590d333fa9baf177b5df1acf2647542fa35ad12f3987eede66149dbb7d43fed2faefd526f9de700593f2fca69ea109691244f2a6f8dab29ac3393acf3895af84ecab1f3622fc996e92e74dc5fe424e5ef2ebbf6cc4f070b6d08a786b2b1692316d57b71d7f6ffa3d6aaa533ec406fa5e7cfaba2260f92abf9b4a54cc0e8ebe79fd2c49e0878c2cffa2bb73a8738df999b30d5f0c33db5ff37b98bf7bc5a365b3618c0f8ff52eef22d4ae3e87d431d9d2d168df30125256a6c18d1dc2801bc747cdf058383adb3b90d5b843141a18896b81ac2b07f55fcb44854fdaba2b180a43357796d895130f2f8ebaffe64c5c7ffff5bf171413cf0c36283760feccfb4c517da77f8f7b7ac48bd072a86f3b51083a7865fc59642ee672a43e40b7ffde4d6e09b63baf9676c95fb7a307cef72d9e5db317fe007965272094cb823504dad9dd173ada3efdc389570f006e9fb618f709363e387b260a72d5719495fb814d126e889ed2029536d654f15bdbce6662b2312f7028b7acd701c57426c8b202628f404aca38620ecbe0c91ca3ebda0fd7a51008454cda3e8015ff3a8c825ec7cefa732ba30fc6cdfb7fb0918631d2f42f717f0c40b739561c0d55cccf9acb8dcd6a19da1b1a7c14b2c146cd24f8e1d978898fa4843912f76a14440ea34ae39a2dbe7f86f5d218dd164c552e5a637c3516fced4cd406cb79274c30e99871c5688ca090a49ff6029c2798d42245921fee9fa17a638b9465bc547c35a51f6cc6b199816b38eaed16096b496378c274d15c8861fbda98d290f31ae54fdf4faf411cc605a87f2bc847749a15b34552d4f976e77ad59fa65cb495db770571f210f0d22ccc599a14fb74fd73fc3a18f3f4b92e263ff1a49d2d492caa86325e50776b564d26d3f1bf9605bd3908a414526d49edcc9f24d44a538a229e38f5b568d17adbbdb476af0295516b70fcfd0617fcd697cf6bcc2bcbfe24a068e5b725d3696450564c5697b9e9dffc829a66031c2c4f4cd7710dc72fdf19644ec9ba1876f31fffc96defd84a4dc930273518a2fd0a2b56351b22c8f8c7086913a8242a9fc277f4c060f4412886dabc32d7eece41fc63814f68f46fe5184949e9e169d41ffdae03f0a3ffe52aba06bc3bffc7b7f8444a3e6054dfc505a8da0f08379673cf1821a16824259b8b00537dd046c711ae027b6cf126c1123fe95058ff8d41f8070fad90ef3afac1b39b44da550349ef9606fee0ecb203c6af315a987a6a3c291b7e48d852601b3dfc0bdfc1ce1b4e4d97bfefad5b0b466d64b8262b2766d15c97ceeb8423a38e47fb36efcb9179992b33dc6ca1363fd3bc7ff13e1a4ed6fe3ed0d1791b391d5b476b1b3d457369471d1b672f215f25447f91b7a20841118195f3f791b194361f41fe84aedf304ff03104e9656108c953006692d048762acac45b048241a03c15843ad6030ac9010d212690d43422010340c868662d0282c0c0315418a20603088882502f1ef65dd00e64f113a5aa5c99f67f6787eedb2020f1bfa5915318ed6a314ef7119847451f6f3d0a55be5a38eb5d7fdee63933d5829ea90c2e9f821fc1feb04b4e0224e2424db8c228f5b4aa81b65e37814eaefeb575eaeb67de79f3d111c3c8e39d8ff1b58e2f0698242093f6686b22449d7e0133152760bee532980456772e9ca46f628c967c8fc1d5904817db2809de2a35f1b18fdf424d1a57f21febcacacbd7bdf80a0d78f7a6857a93bcbf2f6985edef78c3717af5958126fa73ad663f06afd63b3e1e880d802aef13f9b941e2db5a17dae84ef6bf2faaa6f72934100bd20955511cfd4a6f248210338b9eb382a891f76ee26a6ace570c3fd93adbece86f4e3ede7a8a03db7868753535e4492327d518ac5c30bf3815fc50b27d03df8fe87f37b0b7f25288cc1f86043989167884e0dfefbd38860fa7dced681ef09939e79ce1c2288651f91452dd4ddf012e1370e47367fcf220e855fda199caf1a8f570d101ad860d6adbbc973781fa16c7c4cdaf2ce37e5319ab2df7ea213df4ec53305247e4bfcafe9d0dfb1ad05e1d6065ccc3c2d3a81f8b63c47b40eb348da446791ad0a1b197c13e0407bc5c711f75db39ebb2a9a78b054e83c4d347f37bd7398119970ee9b1b3cb55e70c316a16350e8092a680da13587e0795c671ee07cb83225bbfa1324dcbb9c50ee763d6eb28c5a842cf4db269110129747b74f8f0fc7994ae0f99bbceab3ddcb78ce708110afc277e34d926a376cd7bbc7c32a1f978fe2b97cf73bff067ce5bf2f3047f0d0d248bee99b9ac7c97ee5dbf738f9f8db61fedbfb8b17e3184f0b4f81ec2f228ecb4fc0a393afc5360eaa65510f2bb23314b9102d2868f9fbfcded7d9227afe3fce1873fbbd1aa2963fc01193c4e588e135f95dba9f8058a092eb96e6b0c294fbb79d969628e8a69faab8d996d29cd1fa3d8ed867cabc9488dc4f551d1d2c30874f29a3a75ea756edd311c3453e15b43f478d70fcbfdc11fbed8e165ad3ca4355d9480f0a35d09653f5d045c82abbc8cabbcb7a18a015a515659de59c512a1837116179597fa1ff2847cb1a658db4b2820b5bc1503024dad21a2a04c3c06018084cc80a8e8222b15642302b28126a8d4220211024022d8cb2825a5bc384d022c230619435fadfea68e18c9c478ab60607d95e0e67212dd5797a327681e8a6bb0b6f3a2fc34181f363dddcf521467766dca8ec2a2e3b974af1b9fbcc7d515c99de75ac7cfab5fbcc59491da2c05a5c9173822dc2a17f0d3111fe013bdbcfcb72ade2adce76332ab2d69effaa354b7242682220335cecd97a65c0057bfd5046c4addea9ce5594bc69e039a7c97b3c946c3cdfb3b3bd7fa013739aaf3534775afd96ffd8f337eb6ddd6b7fed1013c17ffbcef6c711bb6a6bfaca781be963e51d3c3ced1de0d66e30b434026908b5167132f4b246594a6969b9c958c355ec9da4fcfe3af32b15a53e0f273ea35cb70073992479ab6a3394e340c3f2b6b2bfb8a135504578d8c4f951c17b8df212b3d87159e64457d3e005614da509d68d82afb424f41ea20486388bc4361d8242295bdaaaad5bc692f271fc98f25725f7d1e48fde1502fd4367332fa8bbb64c24e76c3ebbf9c4a057e5e48120ab2d22cf48c29816fbd402bc305e4887e87eb91b7eba1785f872c9ba0df339d82c75e984c885399f35cf81535a39d5af4f9fe07098762cfa1ef34a474822e33d9dc707053c96b104efa99ca64c66896f9c6c090c115152fbb34f7142b7be71b0e4926fd63b567f4f3ce1bb36b19fe6779fe6db5f9ebb015efdf1d4d86c7b8edfd36e7b29d43d4c4b21d7fc772cd27f3b77c37e9fffdf3cfe4fbf70065674435be60dbb982a259534f163d72b79fd7b56bf69f1115f3f45ddb045b40c787a7f0aa9005b82cd91380a79a9ebcde396d2eb87fade120d19ea5fba75804a5651ba08e5c87bb1f7d91d22bd8df727bd2ee0e9a3426f7c8f3df4f854ee292201d3803aeb3345b66f6d253686720afedaf6d07f7dca8d421b6d69e9f7fb6905fe690f19220d3c3db0aad22a1ece4efa323ede5afe5228949ca2918397aa1346d7c1cdd05ed3c143df0f0d339613f98ff20451680c0a2264250c83a0e11004c2ca1283820aa3a022306188250a09131211c1c08420102b11214b04146e8db2424045205034c21269650d83c1207f6d4fb0f891eec167a8ba7c9e76b7f71068b5a99a2e63f696e2868b7773cdf5d6254d1e09f22b5ae8d39b060e16fde6d7bf2468b7f3204e64553266c89353be2d6a4b36c39532db5409fa61b7ab96a5214a01d0419d22325d2157cba06e5cdc720ae21f953cb25472712903c1167162d03fb02f5c43b435c4614d41edc4c7c8ea221ebb1db38b8072d7af1d824e0f4ebaa76339bbedb32f0dd188868788ef6f7ec0dcc2a24699726fa22523c7713d31e294cdf956c81cf6db4f4cac81e30fe070d808716d80d1c772d7b9fd752eb2718cc77dbd8be2cda3ad188c5e6655906522b8b3197ab860db3eb877a626d19cec2b4f1ea31503943435ab193e572a14fb6ec43d4d521dbbcbbcd1755b8eeba55b649ebb48b49f88b21d74f241d4a313c27c8eef8f4525576359ed1ff9159255ec46701cc01af5071f8b4c3b7c835bc672dc86532160467c0edbc23c74db59fe5d6be7897ea1d9830f5e51bd36ecf1a5bd2e1537cd7d053a2d4a319110288db1e111c20efaad735ab2ab251677fac684a02e9f8c8eb9535f352180c8668e61a2b841a536d02a92c6d6ea2417ab3e84687dcc69d97ed7873ef6a8c6c383291e123a0b240279ba34c5f4a4bda3c62ff36a88131b753a2690b76a8d3a1e6730b0c4ea5db50c3ff3a0840ce9f950ea53becd4bd7514648758079e788dae49d83b1d7749acea72f0933bf97d530b9eb15df64b77121fddaa80ddf72da8b39cbd822565fe65d92d1b38baa9e9623d41a0277ed893f3a5fbbc7bac25245ad4e726ae554bbc69db345cfae8c634882160c2e5f90b1538abe6f5c26714f9229cb34f6d66bac98eb4b169733b6f6526b2bf88f094f723112349b3ef93a4a79ca33233632d4badb3bfe48b6e50a97d194ffdde89a6bb306340b2c16a51696e69b1a2d26cae3344f291413349989c55a4ddf1d2e3c957b4e2f9d4386fbd316417452be148d989e2bcd35ea87e5189844f204edf46b9fad37ca64760a4f8eabd62b1de731186c2828d31fb8565fb6c3f2368ba503d21e40dc54d6272d6671e53299c4d4c183cfd126de07d3040769ab1fc33e0b5f1bd8449e535dbc4f1379a3134ac7062d33b3e84087b7f7858f750ab9b358a4e7326914f214a90a549556f5322d68dfcc74decdc974e02c343e948cbadbe5862f9a29d7c9a3172124306412d215b436ceb3b452cb73aeb888b6d82ca96390f977edef0dfbdce0716ea07bffe77780df709571e34445dad7a9dffe9e43ea773c23fe3d3ce3ee0cae62ea6e3d42eaac96a2debcbcb4cfefba6282030d09668cf9a583f6e32b7c801f1f72cd750fa961a597984aa882fb0d8e5e044e5082cd6b91f8eaf78b14f07e842522cb27aac34c0349f97efda675fcadacbf2a3c2c4d3edbf882099c78d7a4606fd7a1b45b9167ad2fb76bec0634457e75fcd663631da16f1a08b6086982428942ab43593e15d41012fd2ba6be72fb6b6daa2256a32f2fab294773b8b28f4783779fe6ca7f38a61e97eb8e4f24a5b27269efbfe0aa0afb7e943a17c0954de0e078aef19980995f117fc9e19626b763704bf96f2b06f29be9497cf5fa5f45cc683e57b8a82f9128f567c6ef8e65fb7e4885666852fb3c7dc03b9bec6660693d4d5c2759eacc33e1769169d2171e8229794af385989fc17e40834b259fe19ee63b7aa7ae9089220bf5af90e4d1df0061fa27bfddff14ccfe39aea8c009839e326599e2dcb1cb72a27904776b1eb1d9b54b7a3ce4ce3233c4e7357b5fc5d5f589ae9bf5dec99a2d894c8e4315dfe38a125fd374626c24e09f4f37400e556b4e9c4e7b7ee56f57f4bf2434afab8975903350b07481c1bd75edd4d14e464246feca7246beb2ae3eb25a22f2cad60e56da685f7fa4ab83fb7f942b8a846091686b212b6b61618815d64a042a82c058c2310828146d89824211082c020b8158c1d1565622186b4ba415d60a2284c620503084b0301206ff6bbba2496ecd6ae473afa35df13e7c3638406d535a2de61e7ba17d27b695e3cb27c28eeaa3972ea1b98fbaa44eb23f66e377271eb13839fe164d24ccc7d020dfa79eed1dfddbea00ff51b08a1f99f79b0a568f7d73c110d2dc68d8c2bf1714ca6b9053839f9758bc3082e37eccba3a14720724db8b75e21e9f33fcb2d010a893094149915fedd03fb0a42894f05ee89a23bcc3b6cea53086459b7fbc3185dbb7b185f302d6ef05f24ddc160d25ae4c13b0c95de13b75108665b312c3a346e93425b19c157c9dbe4f1dacc1375afdc44578fd30b9a9d9bb07a56208a21bc4fe97f6f9e1ada5c34e1fdafb0f0ea2ac4752524d02696513a1e55efc1f574cccdeec95a7ae61a4a3036c83733565e34befe7b306474ce00839d16eff6f64d7352894883e26f430d6a8da3a48931258b57eca6878acad9c91c6d511d6525e0e3e1345ba5fcf0519c8bebb465a0884851de7d4dccf3b519c7c3b89e8a319ca1f3f59be3ad440b0853f1c859fe839f5f07ad03f8f5cfc16de7b62f7be679a167ee1fa1ef9ecd69d04f463c7825b6f8653dfccb9b09c2b78b3f2ab7bdd4f8ffb7be53ffdeadb3f20e3d2fc9ee707ee9c70f3fc8878d6755c9a058cb9cca8756bf5a3cfd99238a653f31467b88ee9c4ecd75a6f20188c14241c5a1fa3c00bd575880845afdf0bc3d79adff891ec8570a9db6937f04a28a3db7c28b750630dbe6cc7c987aa318fc29f500bac0699eb4aa8699e0e5c9799e06cc84f5ee3d92c1aa9bfcaed7ef2d7f79b1f6bbff65e035f24279856c8201697753906e2f061f7edda4bf7b91a7141af2d715952a07dfe4bd7ef9e811fe77c8b382628945dc5a286382ca448bf739fc7322eb3c7627f662518c0d3bbe96ff1ec7a5f6a65ad8cc56e2ade5c15d6236a9f3a888b87ee07bb33e4d7a494631457eff67c7b8ab08ebcb84c170b461e3832f9f89707bc78ec165e059af358ff1881e13751e49abe737aeb988de0bd3f66e43afe06c99b20c8cea1499f050b7c5b7d41337c7fef8847aa129d7fefd5d4f6be6d106a2586fe28ee5797430ba219bd0a89e38e1f6450f8b77e9f71495f734b095cdf071735b6858429da9464d5fa724c66ed87b87071b52eb519b6d2f5feafe44bb3979aca86003388a640a03145fa21f3e29dd0944f024c8e0d4e2dfefbfd3e16f905ed437fc0f7e9b8b276e3b10386367be1449e732f79f76bf5b22fffdeef63d29032d380ebfbe0aa11b2688f09be7556a920be3e6a3bae93995823e6cc738f92f1d3171cfac47a3d92c4ecf6fe98ea125b17f22e5b406187a2e998ed66c43477cacc7ebf0f83855b72fbaba9dff54dfe5125cfb621f3df51258fda6f3cf83754c93b7dd91c7a6d63f4fafda7b4015b5269856335c1e6ec966142d098be92728b6e35655c47d2fec67ff30aa17b30a93d71d4e5541e3a8fd7c3bb96cc5a6e12494054de7f94f8a16f283a4900c2808544a5891b9c6b4e5d3f22796b867d9023d310bfa363ae6ae2a3853ce10566e721f679a5cc53752a87f48defc5f7230ef73310dfa5462bcb65a525753acd430e48dbfa1b96c4482486040bab3267478759856c76545d7518eed8f1c06aa0a038106a9b2a41a1c7105fab65754ce0a67b052d952d982b881585bee72d018da65f2e9052eafdeabac63bd853d1440cc123d822a5090a65f6adfee1bf1a52a2cfa9b967f7fe0cc2fc1cf3a9bcd1deb0f37594fd4b5fe977d90d79fbb4af70b960e37ff100f33eb99f71225871f1eaddc4c323accbce3f3fbfb3a17b652160faae1ab3041312b1f853e08260607f4110a6a07d7edff17d6e7038c32851be192c078a0878e81ef733a470392cd677087b487fbdb93379e002d48852ab97f6b6aef8f9d98a450c4d129dff9718297f8e376624d80fe52fb0d356745155c3b8f37884750df71e2ad0617636153d9d8e205ec2a3ab13757f4ac750ad7ee3db1c4ad87aa2b9cff9ddafff02e8cfc7c748dd321bcd727c61e622bac4f0493dd9bbd9df1a4c6c1886d8af7eaa6f6125c213ee8f56e5dd44cbd5e3c5c5b946076f6debfc74f852b4eeebed5353fef2fa798d5bb5e4029bf3466727ed1bbfa3b61a1cb68ef1ae06270e1005783be2ebe82952a07b91eb92d34bd541792dbe70c889c7332fef1a7d65697f1f88576ed4438fc58550fe617ed109e1e774c7aaa45ba87306a5ef240de892217e8a10697e83771a9f72e39859d43ee777bffa09c839f1d62b9e90f92813f66c5f11d63e2ed4ca45f94d230dffd9a3dcdffffeffb87017b3fed8f9ddb7fa4fe8fc9e26327573aa4770b932eef4cbe9a844f4d97c2c50ec57233f9b8249322d7573b0f37d96713293ad590be8acbaea4b77ce4d14134b7eb4879b3b22cb622933ae6daf3c8a3ae4e90d7373f988541e54ca61fa6d676a6120da2050a5ba2588d0dd3549a8ddccfe6847da68490e702157967e928c3ffb7124eb0d29fe528df691ae0e0bc6bd729b2f1b45fdea96a39ee74764c31b9bc9f58ff7019dc3339e4a42132a7609249692d38592d01f62f378017d7b257d20edb4d167e24b06ee3437b6eb3a738df0e6f6ca758d52d9ab34c6f03a2b4f1c99345435e65c380e743edf7eb17b734a63f28bce915bf7b2ee597dbe5a6b5db757be3271a7a79f60e0b3d6b573f427ef238fd4952e00b8d5a35dbec9cabcbbac56970d5d868ae829ba95f0938100e75308ca66e6fc93c1aa63588643a75e45b84603d9dce84ed801cf839fcf735a71267b7db875d00c2e06f0eec4a43a1ce7e11fb8bc3e35347ada4fe812958d254041fcc2e2a4f44b5799c5154b0f9fca8828c1d49b3580d398de789bbb61336edbc0d6c0d3adafe8655e5deb65c01f3b5e435f7ed34a4d4c702df156209a332dc408e89c7bed248f9b8319716df4f86813e99a904ac076e03af07cf92d0ce65284e1fe37881a4ad93bbf20e9c8ddf6ca938b15f16419397d586fe99f1b3be71a1c7bf8f120a07ff9e4dcd5876e0845d95ed093a23c1fa0a4fa1588518aaf5e0a743e47ccd55fc9d5f9f98466ec78d11b80a757f8a2610455a40a923d5ed2783330c62b72e1f632f0fd9f859879439bebe52e4a42bc4f7899bc1774049c89a8e9f99c087ccf2cd4b2f55ce3f8c6a7606fb4d25ef94546c51bf16ccf9f7171f4b2358abd9f63e38e04eedf57da69cceae6e4ac7a76ea26b251cbfbb9001c28b66031458604e1d5c9de6fbf7b8b36518a77b1c504e87caca7fdd6adacb4aae9ad35d5a87e61b9c5c5f943c0faa9e5da112d360ebb8a383a1034c89da841d63205747e953a381d15e9327623ceec2162e78884839bb3359052096e2d5244b77b9bbbb68af08596d59f6eb410053a6fa761456f495557af43da3ec5deae7bceffb032072035f53e3d4641d9f23cd9b72bb9f87ef028c4c43b0170d8fcc21d48b3830d8b4de60f377da557bd5aed520a90130c7974851ce19bf49a54b3711de1d236bbde45a8bd573e5c70415a0e33eaa8c5cdc48221378ff7b08d4202cf77af65856783cfc81143357547529046da3c09689e64a84d7d3cf6eadaf6648b620fdba99af58a1d29804a435d82d6fe229fb1d820268326223b45e4f536f4f45ef9bd95f62c8faf58e555c885e35101c11ef25b9cc0fe71025ffb4c556b9254fc28b1c1f0d5f6b11d592460af9f446706b01c3d9aed21128c8aa1ac20ae151a03008e9e73b24d67da2e9113de8cf998d2691df3c40d0f48809c586f564e4c1428dbe63a1ba579803670832d6d0b78bec75b35471e51ed5ea597b8588b0abc8be85aa901f4cfe176456e458e5e51b2ac46df870ff749923f00195c64f2d5529712d3d5b8e7e1f0f6e01bd40d995540ec8380f0624f924ae517cd789acb4e32e2c26aa7e381ecd4a7ae949084a0e5c3687e734d2b8d850a2a9f4e607d8a906c701bafb15c9b0f5f6e165c78b3f1c92410b0573187b4f5768e9b2d96bc7e2637f4400e3e122e0154041248d2ccbcd72c172b3c9462c4d7e29350670915db2b0f1c9e2c7c5ed141e96886f73e2344efd5d1b068b0d78dce92109b396548f2a51869b6271a1af43e9200ad75579ed4d4ebfe538691b34782f2f512fc43947ce2f7ca2fc8c8bc23bfbafe6978ac84acd9e9256d25693ca05f972a788c6dca2b1f6e06ce4247d6588292367ce3f6ca13bcab2f9bd697d91c9f7e7af145e704dce1202180f0ffd084e16a344c546841140e9b9e7b7465a57517f0b16f4211334ef48897641f25fb16360ecc24e8b201e4f69dafd7a8cc4b18f245ae9e8b3a715c6fccafd11120903804ef7e785f5be2d31bc5f2d7859f8c5e68394b03e69e43d89205ffc7ea54c69000aff042b4c3b04236e02f33a543999d42dac46e868dd9d2936fd5bb7c79059c7f1288f36e2e9dcfae64ec9267c8b032b0d0f91f07328ff708d6b974c5038ad26fec78be893c76972e6d9606d87fc6c614ae2f59ce329f329c7b4a4ed8af269e0e14532dd844db30d7a8c56d2c910e2a223ddf473c1704c814ce985da1972369a06368f2a5a94f4a28751b180adb2b2f2df40a634ba279f838dd3c4fb938d6f07a10d81b20022aa6c0a9bf3215bee87455644a5477c6d8e82bf8fdb92ada4f9e5525cdc687b3ae4473b7b2ea37ee95cf5578f43e7559c9f0d7f67cfbfc75fbd49107d3a05c7223efa8699baf8210990c511572951d71156099b5458a965930de1928bd19e910e6e257fef4280ad8df30379c193f74c6db4515130a07a0e20ae7b2b385f7ca1f1d52cd8f981e88be3b60faa07bb02f8940dd0ab0cffaa51e717b0b2317d7910623ef3f448b67ad9000b1e7f03e53ca0ea3971a7ac92f3fcad5d1669ed57702ecbf71296f7c8249a49dd606f28e0756e279b8840d37e0ef85595fdb16f1a26f60ee4fdd2e0acc3bdc4395b5573e359226fbd68fe5ca492f5966f73b75b75e889c073ac3b3bd7f20e3ebaecc58a789cc384488d2373b3f019c4feda774b6e203486223b9df3c5d98598cc81fcb0762eb262fcf6123113379642e45712c7a33caa9520701fbf7339ce52d93b3e087eddc2bf4705bcea9f9b008407f9cd2e56d261442b939187bdfcd7d5cce2a206b1ddb2b67cd90ae762d51531f4d8bcae1b82e2f22baf108b0e7b372974882fd79294e2fed723ca0600be4e9aed507ecb77e2773eaf6e73bdc4f37670494306b3305c980bfcb2aceae1837c4ccd1d150401c4cb33e240acfdd0414b47d2e29b8966d86d269dbc6d07966a467a30d28ecea92cbbd88188a22b7a816304391f12ed099c9bfdb2b973dccc5d314a77da155463d7df34473107d6e00108ee80f947d56145377623ca9c4b656e84bfcc6bc3d005b9b9d85c8df52c2e71618bb6371c15c22f5c392c48bbdf2f70f9c14fa694a3f7f26124fbf6cd915e2ade2f1e82794689e4628cbd79ebf51a27f4408f0bb51a2b850a0b850a4b8b2d027f424df9d41b3beb99cd65da4f6357c4041ad40e5db817ee4afc66178724e31adf3e39882fcbc863f7182a54e8430b1074eb4b86a5e13b411efb5dd0a25c3174a695db37e6ec532de92e0ca4cbddaed70b56a65bbfbc2aa4943e149fb4025a20503e611bb8156392319aada57bf01254aaef5cf45412efbef4fb4e3aaea2501febeecc9c7a5855c87525e45b839dac611d07591ae22ffdc44e1cf7b157cbfd2ee7be3f8fffcfafe5e0584af72f50ce732a43e881bad1f1620b85a7acc44f19b77b8cdbd1f48a854680444edf44b8e4aecc996170f69c96d55a88f9a92bb85c0ea471b77de68372294a21f2521d7882557971c735ef3eab61e7cd0f7d29afc70484dca0d3d2a8d4e09963f68a6eab83e3a94f97658a549e4493c5149229a10ccfcf27faddf7ff7e2f8f3f57be869b88cb2e555a8e78ecc84820277091ac2fe6b094682e616e80a1e9febbe0e96b6549f03f3ae072d99e0f52257556924fc29829d2b4abba4be6a7575100730c37d20ae6a9929596e378d9bf2ade6af3d30e7650e7cd57681bb5efa7a728d2fe63cf31f3453e694a4b7de9fa6b5da3cc7c93118e51e83c7e9c3f5f7fefd9fa7df8fcc14bc6f3533742e5b1ed066e32bfcbc2aa37de39b5f49a0723f0a2ee55cb1631bcdd3c59b277c45414cf17c5fe4ab0f9f19641fa8386f6de542842e7b1cdbd924e329eb657caf5f47146e6851bb78d6bf4fe8cd7166fe9eedd90795c466fbd1ef06822d2aad7f12e552fd114615f1feb24db8a84e48702c1a721c0a4d013cff1ae3c6250daf81d90c8968b7a8cb2ba9c3a6afdffe7d68fde72d6a12ad8a7eaa54e940a21e7a1636899d69fa4f83bfbaa80fb9418fbbfafe3ff6de022aab7cdd1fe7a5a55b1aa4a5bbbbbb5b41899706e914786950101010909456e90651909090100424240411906e5081ff3a67ce3d87ef3d2a33c3ccbdf77f7eb3d71ad72c9e777ff7dedf7ce2f37c9e0b8196a582fbdb449855a9c761444fc349427298c79f3bf8e298ae5f8feee3da7d711d4f4caed0fa3a428dcb3b32518d3bfdbcfd10afba073161288e0d74cee2574f586faefe518796d728faa65e2c64437070bd49484b9d45edb1e4f05ff3fb3f6f7e4f60a43377ea46b24dddd0532c154d53e7ed55d3bb90b7e09cf93d35a51736fff58e9233a963ea389bdf1187e6b28d76384d6759d7faf5f6859e6324c80184f11abf73dd42c5f16aaac9e1bc0e946fccee4b822d7985a62ab251fc1f65746cfaac780fa5bb2c0fbffe64bb54b5d4a046dd2ff2d7fcfecf9bdf50b3d728b83b366576c518b25f36200d956c793f2c6976f5d14a9d27f785404152a843b3b16333f13cd17dcee41344a3ba34e555c3c35c625b6e3c84f8d5e6e9b7f58c11b2174bf39b7922290b8cd6393ef6951e5d21a460ecaa10eef4222216dafaa3e6f7f4d124da975bab938f9fdeebe4f7efdf59957979f3aff9fd9f37bf33bb286f52f921f4d9084fe88932f1bc147781fdb086160b3b7702c5b30b81bea4764a7977746a1850de4f8b26d019a3bcda8ef06531d4af71a71cc6a9d4eb2673c1f9b4d12c0c5ff51c36d121db300693da9e79a4b856aa612925ef7d4b382df7ce1f35bfa772964854b9dac8d74968df6c951c3d27707a66f9d7fcfecf9bdfec1ecda6d9140f8fe2e5033e715ed6df2c8b37c0fdd1cd4c58be3656f417d3bf0f9328fbe21c7a70ccd22dbf320bac30be0c08cda78d47a048cfa057123f0e21b2e430dd3320f7184328d571ee19314092b4a115822b164765dfd9e7e7d1e4eeb9a051fdffef48c511ea68442077acdb9a3cfc1e792c6d8979ca9bbf94aaffc0455946f0dce59bd45b0d675d4f873033829e3a1ae31f72c379b3a3f5cc575fa84c86948e48c81db3940c24c7e1d67ccbe2e3e6914af56d7f5d8dfcbe3828cd25f382ec80270aeb4f8d203db11e7ad5989babc557e8057a74d3a24cc998b73f330d601bfc51878ec1259611a5d2446ecace85072165f60315f214557fcdefffbcf98df5eece5c360cd3226920938bc8885b643b9855ffcf348aadae67e8e184e5103d5f5b57d74ef08e2cfe943d409325db822d500e8dd4766ad064af38e8a2a76d3068821778b8f07a9856c639acf6d27e19ae94ccc6172b0fba3fcae913545ec628c3dd89caf874ec3072e69d574e43f4d8fff4fcfe2b52f1e7cf6fe2e2b728d46cce4f2467afa76e5bf4a8aff5f9e9fff409ea55e61799df0f049e33e361ed5d1a17497f9e73ad38f8cbad45772722f6e7e3de6c37c2969e3ce4453299213eaae8c23e50e5c174ec1ad1da0ee858488947161bcb782295b3277231a56a24bcd07034f75f7c25a1784a1d775be00f8760a1487b1fe04291910a71eadbb391927f1460c0bc1e4eaa932d45f33dcbe9aa6fd4d4d324069ab4dbcaa4001bd477f2d8a57fcb7b7fa3e27e20179bc6cc98dd40f1ce865c927a36e3a701122b0796fb2dbde19086a5ed328c1066831cd866f8c30cbef4ecbf3374e7fd8a82ba90bff3db162efe7bbd60a23a7e1eb9ce0605029ad6ea40f8d9270fc8539e9d572ff897cc9a5bb03f21b3a6630e86da6c5db7ff7e8a7265efb58dedbba3774026d073eb09a6ac1cba604c42dec249260797775702af9c95f7638ef735f65a70ce4a2520f963f637b2395603c846e2b6bc09599af2bd968f4f775c72206d0d02935fcecaf39799d717dcf9534b4e719e7c290de49cd4541d3c2b5f0ffc1265b6796309df7829b4c62a92d3837c1c402607b51d4775ac90d50c7dc3cc22cefad879056d08a832f589d3a3b6b03d09b39de098a7f3d552dd43392600199adf6d67894561d4be19de8af262da41e9c96cabf159b9837c4bba09a9c0c006941bc515fb3068ed9ec20360a0939a7449130ae5b5e5bce3120ca1aa0e90110068525102fbbc4f5024b27f5f975c58510ffb81e225adb372d22267c1a3d42ce56867c7d7d4a78bdc56f7ddc7cfcaf52c9f7dbd216a45c84b81274b78b856324a60df7f568e2bb424e93bff04ef7e5be93a43f5714019350980ec26825d3bb23433fda84ce83da1bc1af896b25625e7ac9c6aa727d6d9b5f0b3795b1b65c7e95deb1b6123004167ef352e737dd1398b1dffb8e715ea86a4ebf753e9cecac54f93adeec42cca1037562dbdc619df83e7860199682d8f7674208fa43d4ad02873af74f5d0a7df5d063207e271f9344ce453e01917f84cc3ed1038a61ee203d9d7bad9419e87a83ce27ab541fa01b043af75593ca07d73ddafcb8f4c8eebadba59f9349c188aa2f17da680a54f094128b885de9810509c741c86129bced60df4cf02153e35f7e4ab4dadd1870b2225703a0f6bd080e7ab2b9f20348a895cb6873cb697bcae8d64e96302f0448a25c16758d7cb613370aafa248465261ee781408cab717c64911b4f634a2d596bcb47a99b6c3f8d0099012fc2478cbe067e70a12c8879b06346aee982c40db03f857cba97e5351485a4c1a299bfb61045a66abe21031cf17b1b58cac6e557449be7bd365a3ebc9ede2203003f9f5257fd8f0a4bdf7be312e3c410b41d2bad2400991165d56a44519d4295a31e4bd5133a87ba12b73a81cc1b43e3924397db25095f93726583b67df0c214b953cfca9d98a8677d1ecf09c09b1a5223bd17d07bc7bc00646e14616a66669af7d11ab62396a21697a37e50e2a7392b9f7ca8c0ffea32975db63bd995f9fc75061489470035f7a40bd5296e7b9d70194da4af6b98e2248ca01dc86c39f422839e94bfb90f87703b42eca56275291e3690d9535f1da526d93b8d154f6f34affaf8d3de661e3100f4321cb0b208ce28938abe3e424660a358824b2e0664568c94a4332ea8ebdc3f79494b8436f41c37344c07d8dfb64595fc8d94eb3b7982580322cdad426a9ad600561b74069e1e7b6c5d531e08a746205b7caee436119039546fdb443ec4c0209679bbebfa64d5d8d0e64114b07ff02cd9f74f87f4a68e852d77c7d6508b38c74e0031a5ba1b3bca482ac362378d7864b3abeb1b55067700f562cd40feedbd3bc304148d4b13e94df07c28f049edc0fedc5b89cc66442fc24b127863fa986ff6db6c2660e129b2dfb552c2c2b5afd7c19f3bee9040969ad3f13b2bef9aea48fce433bef56a8b63dac9de4907ef9315a0c0432adc283f350b79d6181f1d2b3987bb14f42c009975b69f999f89ba2b878fbc53fffa02498c9f75a10f00646658d207ca760dbadb2e779edcaade4172b8aa04a8683778f7fd0bfabaee5e6af593d3923525d31dba0164e6300c3cf996577630571723c8c5e27b5013b0cdd37956fe70bb2f157593cf4243733f278adf93f292a33c909936a522b34325744bdbe1d9ed48cd2025849b632c00f2bd3005dbe6529ac8a97026ee565cea1dd646a24820332e4181a4a5de830e3e89e920c37975d5fae1420ed55939f780396d451dedebaba754654e5870e82919ee4059da2b9a92d8777c26ade60ad608bbe185c8cce82480f529e1a5ab3ed8ac2c61453d40fe0145a1046719af19f0f9052dabf04b1130381ae14fcd18e045641e130199dcb9f8f74697a775675fb4bcd2e38d2c7f9338e604643e71d6bd5842461fc7cf3288f8a24b5c2bd62b3a0c9c7f01f8132f5d4bccca698f3f1257bfd7b4b1451a05cee706feb7da83be38ede6451454f91f0ada849c7800daeaace1833a6327b2dc2d9f8fadc81b4f87dea2f30008ec8a2bb44f2f69b677af644e7d788dd317db2ae1097c3f3ac778e263e5686fae7c72baac6f9b06740b3200d50bf94dde6fdb36054d4f111eb8a971c8078adda805321f656686b1a293454929c3995788dc39431291a200a602c4e8e7366a48d03b1bacfeed625f57e31fe3bf05321b1b2fd55d9aef67c0d099d7db7abbf32d607ea40ac83cbd3d5c2e2973b7b32921274b4d112bd972dddd24edac3c9b9fbabfea44559ba9bb2b8b51c8f3262cef1a303e4fa61ed2265dcf89f517d24e8f7ff985bb7d62e0cd5979f968ed29c5c2679529d20083cca6b22743235d1667e51f347abedcc43ba8d6fbb882a29f7bfb2e711a15909905096a2370b829f9f19e11d43d62b325305b200b38ff7cb62a12c2b76c7bd5ef1193f61e2d05275a7702aab74cfaf88d5ef29b6674288d2e94e17ae576216bc0fa31c6d14ac91bec7d2a282c979acb74c2a1790d13c8fc601b8e7c616c9af82ae83d4afc47321376c74e3460ffe0ec27ea677dfbf0598e47b1c147b5e324e9684240f73e501bae889338697c46d3f14ebcd140d9f3840f280b2dee757cdb5de4531bd4601667cf81f324aed913c83ce4717c335554b35bce830e7d948b6278e3762614c88cfc6487f50a8d506df539a91145c0f0bd6133b67e60fc03f5ebc9ba27dd4b2e376a31dc5584b64b041702a95983389bb696d62889665948667ea1d7d0efd0de036aab63dc6f47a57c46a92925068f43581af1f48aae10e0e6955c609e0b0ddf9f4d9c31ef599c8d4a0a544de33a2b8feec0e179e62d1c72f93d62174fdbb0fbfa95a780e99508798c8bc73dc4e99ece71a32c95f01525b12b9059ad446a55e65c34f8410a377638234e598f521811389f28de53ed4cbfb99a322b7e33412ba4a99a2b621e005c62d82940ba912c46f03e58497da61fb355c82604326f3bdc602a24abd74f2112f4c9b585abab48d0b67ef823f8e1087f4ed1f189e17f9581846fa11a83609c63ba9c471203776321dc5261a21c99bc9edba0bd38414327bb6c6b8304e345d0fe97d0a129f1d28b793720b03fd2d720a4162e258b5ec123de8b2e17dacc9711df3dbef1c31fffa3bb7e7ee555fcc9ee1164330bd75b6e6ebffb7e6c3b0f4767a82b3bd4dbc2c6ccc91afa1b6e3d8973929bb6f8b9d57ef5bc8aedb0f32cedf3dee20de6c9da74c98b8d28ee15dac6f1e793d70f09180e827df2451d445c5a7a6021d8068f438f6bb2c5e10f3561ffe523f9f732612e668d7154aa15b38b89f9958c8c0cddf9d98c3f4f1dc39696fc1f2468ffefb5c22c343fc528a21858c920cc112515b9cfe60ac6193ae83e7ef65a36bea7b706fd27d3e1df09dafdbd9ec04421cb3e7be9075fe45cd1263fd8c8f09fb7a8cf91ff45d0fee75e7f1c41bba6b1878a9da7a3968b8cb69720af97afa6b78c93879cba17afacbdbda785aea599a1b293818b2e0f9f9bb2f91fe8d3bda0bfec8fa815c6cfc7cdc56b61c9cbc5cfc9c96fc623c0270815e2e7b1e4e3e332e711303733b3b4807209720a585a9873f20af0f2f009f1715941a142dc66bc96505eb38b75c69f4ed08ec3a647ef70dbf7a65ccb75011587c2464c37c68c5ead5d5bcc8daf297d4f93fd6f0fbaafdf5df7dac55a88445b490a659184d16dd18dc43d8cbe3f1f3f939ac5716e6d59f823642d580802b64108d325cd0664d939aa37cbbf655071a746d2793a4565dd44a4688b9cf2fb0df697742ff8d1d9178b239cbba96c43feb6ef89fecd82cf944879b6a68f6555ffb0f83efbd56123bb07d2e73ac4a73b25e149e0e0e048ffd60af4992257be0ff62115835a1286eb8bcbe35bfe261008daf7a243df23db36ed0deb335c9662976e9342a6488b5e74ec46ba029ebb1c0ebf629900d74144e1bfd458ac9790820591c91ffffabc38d66fa108ffc7f89c47ef7adef8cefe9643ebbc1efcb76bbaf3c7bd89554a4568df1e7ab1e90bff9bf8567fc7fa81fceeeffb9f89f37df1a3d1b9e5e16a01a511a691855ad9aa3a98399ad1b0d24839def270725772d2d391a511a6e1e265e7e7e2e513fae725f8cf9f6878b8ffeb379c3c42dc3cbcdc3c820282bc9cfc34ac34da502ba8abab99038d300d0d2b8dbc8399b51b8d30172b8d92933bd4dad5d6dd47c9c9ea168db01f8d0ad447e9efadd0b0d2e8d85a3b99b97bb8feed9df46564e5e59ccc3ddc647d145564ec5c6dd454f47d55ad78e56c04a1b7a45554ed0d747d38f47965a4b91414b8a45d6c9d655c78940d6ce4cd3c2dad3964dc6de5797d1c95ac3c3d78acbc54b40c34ac0c6da4157dcc0c5ca4a59c9d0d95e5a5f4ec65acbcf46e197b5972f05a4a599aab6ae8c9caf32be95819cb711969eb68b370f13a7939cb71d82bcaf1f01b6ab8737b0b6971280871ca9abb72a86af31a7a58f8dc62f1d1f6d412d052866a0968a95bf2e9b8c8d979eafaca2bca73f2b35898f1ea2b39f071aaf89a5919e93ac9f1fbca5bd979d8e81b39eaebfb42e5dc042cac75f9a04eb2024a3a7a360af22c864ade42b2369c329c76b6ca5c8a8e3c3296dcd63a3adef6c69c3cbc66b29e1e2a023ce672babeca56568ef20a96c6506f0b1743c75b9cea9e1cf2b6563cfc960656daf21e66ba325c161e5e767686ca664ad63a4e8e5ed6dcb2dade1c2e0aaa36fa2aae665e626234fefeff8c6546a015f9adebc0e042bdf812e1c2e075d13fc2c15ae047c2b13ce9218e4c218aa415217a89a7a110adf59bff20b7b374ff134270988aa26f3aeab4d306eb6bfbb8ebecc54ebdb356cfca8f9702df305bb1a164487d2272c3a3aa75c266065c14214a8fdedc8f0cd4df2bf7b415a0d66fe7a859045c3ccdfe1fd3076d13c6ef1e3492b860be60451f9e20015cf87cd3f62f0ba9e439fb5a9e16b5f31c2bfa7903b040a9083293fba248badb5e88eac6715dc175c2b4002c968d7bbf42fdee0ef5ee1e07dca799c634b4a72e801de57c7a29d4f5739169cdb7f6a37cb2ac6f73231f4ccecae70e9049af6ca2dd661f1f8b1e13d0fe7815070e6036cf29a2786ba3683441c2fbf4fad5ad207fbf1d32805c84a7a73427c4e9f0f321b43314b3200c3da0b9006032c6c428440a15d2b099db7727b9553af89a72860f08f15df27ce6d9eb6d4263bdcdd8c18d95d5a03b51059c4535958a52140574613ede5acc5806d9ad698b7440084d25783714a3545d7f5b31738efc4e084173a2e2d0597970deda759df005b4709dd7dc69b2a66ab08d24c005151d2c4a10f57ebbcc7f0dab77f290ea28e183ccb5b3f22f4f1f8e89472a653814373eb165e5ba6a15ec470cb84099cd543addb0e21a745ee471a570f43d0c310176df54b44f5ef147fe4f275f373d5cc2891f935a9a7c7b56de2ee1e134385ddeaea752c41bec56a5ebd5e90ed4b7a1bfdcfcacc7dabfa39fe743dcb4f56b0ae50156c005544d7e603b2a90a0df48ca457d40fe543ae77937703af635a066b55df91630f8ce6b43fc01e5bd65226f200436c0a3fcb849e42d6eb5c5169471337f51d2d707080194dff93a93c6e9cfc5e918c5cc8ab4b177a2ff0a2057628862bc4a3a608f40a49759fcf2ebb1f15d053a40bd524e3beee214be82925358c39d1dd5560e6b2001c87fd09c138aba74d074943e415fb921c994953fb0bc7a56fed8a9a77978d9a4d8aa6e0dbbd175e2553e260d408ef43e005f2e8785f57d7f0abcabffcb118b41494720845365b342b2103ef3a9c283b9b5d44375069d92152097bcd70dd7f21539bbe05bcdcaea400f6f68f9b708a0ff6a1f291cf7076b6e2626692cd9286d7cf8ba6b0eacbf2c96019920ee259729fbf5b0dbae433e6c8fb080022e47ba4c98316441fa11c5e812b78bfc95e978570172bfade2f85c63edc2ebd7ec260faeeb5a9a33de4b02427cf89c4e97cb8f27f1b95f946f4c4e0c7722950802da0792668265b3ca6955d2adfa50a4b28d775806084049693a999dd4cd46e4c759d7347932901be850b52a019f0086747574596b8c3f6791686a3822c6d7fb4fc78110c00cc6f1adaf75581da511d9c762d725a63e453103065130b2be845b4791508a6403b527ef6d82e32c3a40fbdecd3054f812e94ca5a85038807e2f624ead5c10583f6c0718991cdf8232b10aac936dcdee1779ae7df30642e4fb7e2b750461c5ce149dbee8df58ea9afdd10172b0399fdd6ef712b85413f83ad83c71b77930290b4016ec0ad73cbb19da629738385edde3ba3384f815ac1df1cc4f542f1b4e3cd5c4c54061262747e2e6de2780fcb3714d8665ccc55cd876258d6e1fda2bf4781c06a4dd68508f3dc5781439c3496e529a7dab408397c01358bf44dc3046a188542f52a15c789fdbf85e7bae92c967e551d31c792bbc9216c9622477b376a92e5fb64c28034294d70ed6847b736bdc10acdcbe72f685d63ccf075cf8a743dab050a84353fb78498b17d1d438d46d12387f5275c860af69c86acabd7676eebe5ea194cf4904c83f256fa251abb4bec15ae49b3c0ea6a12e4732f9507356be2fbe20ba24e83f04e9f073404a884cb2cb8901424c3979b65b97c2cb70ed9c59428df59c638d1e9d809c2e8c9cb4d6812a43a449c778699d18f5268f0a81fd57dee3aec000765b38bacd74e8fdce05d9b8c39918a07f374f31b685e982db94adc7797998321c693701f2327dcaf9f1431445fa0888f044db3d936a738464a07089797fff8251c746bcf523f996fa926ee1203c062044fe95b33b9666b6cac06dc2f494e5c627b56496a084b3f2cb935b33f8fbbd1dba28aee5c21a167e8364c200f9631e79e5b375e11d543be2e25d66ed7bda88b0712084622e8e28cad9b1b380fc81c84f27055dd5dff703a0bfc0a361c14e623258129fc991c2670f556c80dcaf7006d935bbc7e892d754cb9e8fd73b1ec61a5629017ef48dc2967b1faa8219541e368c92e235852b89d981ebf3bae0a6d115bd933b1835edad3cbb6e6f683f03f5ca9e2876fa2911a422708fbf221fcce0502a4cb102f697f97b2ea29dc703643eee644193d76ac79d721d8c8010f5b76a1bab1e721ca189d001fc1229f45c6bbf08e07cff94a7df53ba649d4785cb1df98abcc70af70b1062a56d45bc1d30c35d32b199a2ef5dfc5c8afbf9fec7b3f247d97c5153efd3d1658519af355b296d8837ce00101dc35e137ab496902bf1cd522ee37828f5da6232405a73dfe9ee6b47d763f8045ac191a06fcb0b4af6884088474d560af68c8a720be5b849c07ab9b74dfa9e2ef07e2afaf5a9eab29edac4b0fde984b7fe0fa5a88201f49ad04376e6c0549737d864ea1152f3664bc8f068c0fd79663e8ec37d638439f4777184ef95ba748ed102eb3b3a20061f278bd4b22d728faf07df78f8b678e9e2593957149fc3f697a65b8ab3692f233d5f85a2f2c601e4c6bbc20c265bb79cbd5849a5bf34064c8db42a91580310a6e52a089c4e1815474f58d7d4009d52f2126a3e00e10aa8db097b8e34992cf7225e828fc521f8f329fe59f9e687322af4c0bd9bb72803095bde1aa73c8f5500f4d72bfe54953b07afa6cdb9bce052b38edf3b2d1401fa731586667852c51bb13ab8c588f93bc45729eeb301213cd4bee3a29cdd09817cbabe6039e757e842f953409c413d6c662168b7d336e0268c848f5d421e19411b20770d5329dfe11c1320969ed226288f889f297ae09208ec7f379d52f5deed71d1587fbc146791eef82243ed121062658286661e1caf98fa589c12a999e5ce6d130388c10752a6d0435fec28c50f2338b5ba38dbe2157b409cecc4d9df3e7afe8e737bcea76ea23103ae6bb34d0004281f3df3d53003718f8d684862e2c4c9246bc22355f8d108c8ab666d0b49b8104cd9f910d207f27f3373fc5ae087fecb28fabbbb8d71fd3eac057284e81141a8fa303061f31753a901d1b2cd5037e0777bc8fed1d6bb4bdc7955a364911157b3a407e4b487343566be51c66c602ce2d3ef96060d9c535105f102d5b121714411767c17f3e09d176bfb49d882f045abc373f227e7f41fdaef0e7bfca37fcfb91f76b1b0c8795187f3b29d10cecb963a2fd64af006bd9192216ccbfb991ac6d88c1e6e1d93e1a6f16f7001115cd08574e1fb2ffafeffcbf7ffe9570bfce10d582844120e2e1485130eae5ecbca8105179e337e69fd7bee6f34fa9284adef089079e0e0fe26a2bcc9c85a3250e341e76be5803bf999c03c35a7bde508be1716c2ae82de00df8ef7441438156b854b1df8f8f9471db744a21a65e9073e88e134fed8dffeef7ffadeabfc96f62578b971371503aa85b76717dd1a21d4849fa0a43f0ab021da3c5cd3fd09241d658e2264d88de03c9feb394189ff5974c477dac7fba978172ebb7bf1fa3f2d6fea9ffcfa151c9cc58fc3c2f003b6d0700444b8758b970bb0efa50e7c6fe8717171f174d72b75bd6a63eb4dc7af1e083f400afef123104f562a356e7470fcfd11df9b99bff611e04f7e29b9363af2ff46c935898b85787e55c9b5ba93d01ad34bfd8a6e291ea4089ddc4f85470c6e47bc237ccba218bbf7f9358af1798e60b40bdeffb32bb8799fb0162b729d7046f3fefd0ab261a9263b62eb5ce11575ffadf224777e4d56e14797e3739f90f96ebc900cd5ee1592da6a7945db3a507cb7c3c1dfa048c782888c18d6a6386119f309b291e1d6a0fe6d3a3d0c19cd73b6d0187999f05569e9cb39ec81d563bc7ee69d640e5bc99829346c7c2503e9a12cd5aa2813dfd0c5fd47dde7e4121d216eff176085ac5bf14f02ab242b191df677284b3d1470e590cb51ebc739321dcd47c07b75ef86bb06e28addb1e8818c3de2e849cd825b1bf65838c24fe26eff0e56a1be2a877f4da0d3be6c47a3a6bd5955848dd9a7e8776b7de7696dbfc8ff02ab5cecc2d0363337f7a13630737080baff8efbff0556d1f1d6b757d0b25734e4b3d5d1f3f4723732b4d7d7e5355773d5b6b5f5b0814adbc8bade52745280f2cb7aeafe8187dc0537dd3f00ac6229c46bc9cdc9cd69c12928c82fc82d20c4c305855a70f3f2f3f0f170715a0a095a5a4205ccacf8f82ca0e6500b0b0101011e0b0b4e6e4b6e7e210ba8a539df85beff4f07ab7824a588494b0b10cf7f3c4e8e4acd1583322dc49b66d92a27731343aabaf9e3cc9bc5f5030a5678700c88ad9cfce4d69dd8971dbffa11050a3eeb157f2018ea7d5efee42f814293ca3f2150e8fb9881f561b6958acdc349a715896582606e7d86b3f229f780bbf0e5b8af44caee3d1069975b3bbe9601e48a4893a3325f95c040285ca8f72cdb32a15dce440672214e5be5d13a4c627435d65f3c8270af252a6e2702463e91f4cd26d3d6ba4c2f73b30291a025171a333300914a8ae449a56ac7a96e841f7a799d546c7d74880170e4d994add47dbb3f6474653747b64272b6982dd90cd8ceb3aee78ff0ebc4897dcd6ddf19a1bb9ba6ef3c0d0412f9af50aeb5c9c9a7e14a56a328b23ffd60fdf53df07dad3e2ec4d5ec3b53b97b376372d9645a592ae9002a85db57858a5161e40af3454241dc249863246b5f01477020798739f9cbfc4cc2c8969c448fe1451925692057c612169b2bf6ae1967efcbf51c836df5b0519716c091cef0f903536ce075af4af169e241c137c97b2b5b1d67e5baa15b713b536890b9e11059b9862be82ad0352017727fc7d5d5ff44c58974b1c3a73d3f6a8065441470b47d0b769e7c3bdd728078e7b0fe4db2485feef559607c765f35da52393615dbedf93eaa60b3636d783af519087464c69879a653b311166e5f2f30e116ce110e05aa4c41f5d3959bf60fdea5c8429eb8c94ab6e4cbc600b96e4d928b217673bbc5374630297123e12dc21c60809e9a5c21988a98ef42212bd9caead71f661dc4300c1cb7410acee17c478c661c5fa79a891b89e60a87740058acb57bb32866274fe9645ec93d2e41126be5caeb002c76efc9bbaa5135a5d461b1378f86fc6dab226f93b3018ee423ab1cde785ed4b7bccd33b9699a310c7b4b4020ac22c9b565ccb1a130fc5168e2879400aa8aa831c009f3a5a778962d5c42ae27b8e9168512d1cc269a381068bb359e1d682fdef17cb96c3a7d5644c64d54ea26a02ff4bb3e27a035f057820a636f2b0ac4320971dacb01819eb6fb52a4d3195b412706d482a20cdd9db78a815caa4aa4ed279b142161f856d275be69728b32cfd280f3bc5849fbdd5775e4c71e93256fd3034c51ebf227815c12c7e200fb0e898ab03a53d58277684d3716504d810ad38b4314d42e12c3a2b3b8ba1d5c57f60417303a815c42383a3fd8b5b9af6bb89aa11b32ed99ca1e90bef4b3e259fa26589cba8bf18002575c345b547b54622f307f1ce8dc37455bd3896e0fadf95f0e7bb079a5cf0ca81297b866fb26093669b8eb548bd0f3d9dbf97029c10e7014f32ad9b8f513b7da0e0ee3edc0159b98d0730055dab0aedf89edd2285dc7cd152660b67a269089157c07f07bdd81375c296c8321bdbe7e2796726f4339f308a8824240a4b676153af881d5e5dafd9d0696c169fc4ac0d1fbf0155e1f5acc4ce3f64124da3e973f76c7ea6b0f2050abbeec94fc54e5be47b6427061d1ac34276c02700259da9fc8520c85a03ccecbf986cbea83fd598804189f37c402bba2b06c4433c75a2ceda0a688eb6223c0fe309af72ca80c8d60a6d7a0b95078a6ff4b834e2f90abd1d2d94faa96b02c996ceb4178fa410dd75f2e12a84287f0d82c69ca2fb8ec5dc455abfc3df7fb6dfd13c0fe7a43ddd5527e54d51e9659619be171cf9ff4da0e10a838e5140fa3c0f1818938c7c223374ad267e01801087d9da6be3b01f8de5eaa342cc481f1971d21d12b4020e78a6f8ac46dcb28d2d2e15a56e1f87dd8651f56a00a559ad947f8dac08f8f67373fd13593362c7e541105d49198b0d87a239de3fc14a674fc3917c41503b756607eb13c9fdf17213f745d2c822d88b4a70bd10bd000e37f50e885cf92f34d34b85f17975bbf546268c304d0c78bd7f7b2469c48e55efbd6cfec1b1e29247c8307e6ff665083c8f3f9a21b616c637d6c81b5484675e48440a08fbdbbb5ca10c3a49095f5f0d1a358bf9de91400130dc551b2b84cb2c6e78b02a539f98cebd40cad01d479f667b9fd3b35bd53f909c43b879b5fc7f71e65f59c95533038f46e4abfc4c6282f503360f640b7798202a8b0e68b9a6921a945b3095244e33d9a8725afbb48a381f9f774b58f272e6e435c1b775cc6f05e14a11601902b7e8bf89271282ca6bfb0c921fc744381803016e5f159b95bdd2955614feed7fe271ec129a2d2d3954771401543af454d34b69cb44b1cc56b836f8550ace099b000975631d5a76079861534ec75c9ae72f8f90f145fac805c666afc139ab8510a41d36c0bcf99a414263d761b607c5083a9a3677b70de55a4debc666e6ce2c2a649db7d56ceecc0cc2f41547aa8b7c0cd911bde689ca2900c24ccf890e24c342649542e70cf515572ecf2d68cc901408dbe1bdafeb86e8d32af78553f7d66d755bd54370754d027ced334f46f52aa9d20c1f00a9e31a3e9b27600fa87b62f4e2941b6efa82f85d9afb5786c1baeefc01fd83feb8b197293f35af3321333784ed75ed8898a02e72f816f78e7ec714afba3c33ca8426a2c3d638c0a800468eecca8d4422b22e3191137259e6b2a497d7104cc8f503ad5a353c4d4d32bb189e66cef37435f199e02ebaf82b9ea49c3d3edd78f0d7937663d27af7ab27b00b962d40223a56d6832e6e523979a11b13f38320a170240a38fe95e7e8fc542dcd7bf30b6412339cde0758980f619a299264967359fd6f0a8f4a4ebe8910b676900817ec1fc434426a1937409668b9ef14ca41736f22b00d0c2c54c4551934efaedb5f21b11393b92246e2ecd009022aaba8fd9baad514ea47df5c504f371cc80d70100f4418e101b5ddbfce489514fc716e6ccf721981a09581f7591e1b3ea9f175f5fa31c838fd72bab881c6a04d61fedc44c8c20650f5576d2cc72cad1b74084e60e0088960aed1d92ed8299e319d3eeb2bc926e358d0e00f46b4f24efab89b20b871dc7ef13455b6bebd55a970fceca97ed29aa97df3c18c0b8f7c193acc378b566d309b00cc38d6ac488d902f1313c5e0fb2e32b7e5ce87404e627abc850f4c6e6e7ca3be377c327705756a38c920020015bebc06b9b6d4b5ab7cee95dc785e6781b77732057d70d319a3e138587fe2a1a439e3b4971974d7115707e8c71e9b014111d54f407f49a6dca3fc157e105d43bb86f62dfa64691fd6f25491273f0a15eae7de5edb6fa8b396370f34f30676268fa6d3b565e2856143dcf5e4da20c5b3274023cdcb8fd5f14982b2d970774070f42eb8e7254505600ea09fb302f661581249677f6a5d5811ea15a8f46db80eeac9e1cb33bd508a77eed412b3464056168dd68038afe5562cc15bc4d72375db926b53036b2e35738ea06c495117899d9b44f7539457815824d792fd9bc791607e04e0ec88634043aaf3536609b1d355e1e8c7e8448016c57337b399277d204d3cd4bb311d71d0a12cd8a08006a8d70011e41e421a704991b57792cef4bdb1411cee702e6c86b01c4b0217ada294e3fe7ad9aebb6c98b70d567e534f0911d97f65d19adf077a1f34891665a3272c0718974fce63e254b57fd87f558a4b0b4cfaa322228407cc7f2aa157b78553a72a4a6ba37e52e4ce500730248ddd48cc729d7b3e825987828f82ccdaa4740fbc35de0fdcccb456a5e7526bfe926955811bae6c429d0ed06e05af45a847994b437190b12540b4d34dd22dfe19300df1f6b56ae8aa9b76c7ef7e80be75b565314f45e61c05c7c648745b44ac43a40f1ecee5ca48a0256823c4a21a0ee8ae5495c525f6d2fc753588d6ee14a6fd591078ef3f49531f706cda3e53bafeba475b54bf32a5f3c02ccad313dcbe247d419f2aa91dd83332be316ba271bc0fcc31f295a3d1a8b7fe9fa423b6a6d4a55036dba16380ea617b0b688387027a95c4e6432d4bd2570df63eb00fed989fab974a3f147ef75e3f3dc7d38bcaef78801a97254335d5bfdd925b38da11555cab7bd180d9ce581f5f5dc46ea523497515b9a54fcd55e2bf42652066ce0b8ad7008bd938f517b02419fbd3ee0ee4351dd1d05e09e887611a956adb61b4995b63ef32697cc8e5e4506b266e2637da9bbfb0d6cf00c058d318acb7a4e269481f119b9bd7badf85982c7d53d96b9a49114f9cd9976e0f9280c70aff72cde76f2905e13272a4f33a293d003d4fd69a11aad838c6b1858656b77fc903b1edc6d8e01fa47fb5301bfbf8dd1e71c59eba8ddeb3d4aad5ad200ae6c2f4f2e72f1c194c6a78f631bd717ca9cf32670c2cfca9f460e2bc4ccb7ce9a3d7b7aa96c0525cbbee83580dbd2129f607f5432d892daf2d27725fcad3465643f807bb582d8d3729b967e40e976d7796eb84b6b12100dacef0226d6295dbda67c3a1c925be20964845bf488c0fc1b78f61eb9a492d64c6038ff15d471fed8cd28c4ebac7c0857db41cf647d1c1ed632f9cefcda76546509805b3edc2194dce60b9e224d76263675eff99c3a920f30879112924d1b36bf95bc6aba7f87be724de39ac30210523148d616e5c784ee9e708662e20d9d449e2ed501fb2f915df495dc2f0cae33a61daee96547a9941d4940d60c4516e4feb3f2c94afd13adbd5cb68fa4c28a6b00358f27ec9dc5ab840ee960bfbbd7668cdf8684ee510311d2dc6a232c27f80a24dc176b823634702557e4cc81a28ae1f79c9ba5e1a9f1774cfdf727cd275ad1c83e03b8a54fcba2195374d6eb07ea7747541a57df74a8c403ee0cead25621fb274af897da536088e3b77c24bcb0017358289377280edf524edc60144f40b19516514b0448adc7ea97923c21c930b431bb99ea0c5535d22647eb3d2b2fd3cb251896d7ef2ca7cc1b0a752358d0807600d109ac4b2b348dd4bece290f0225221586da5b2f090502e68eb4b5dcb4ef6a50c1fdc1b8ded55a1a8ba74e00ee575087817ae06395088f9cbf496630aea47a9f1270fe5c2119bbdb247c59cebf8adf0243b39628b497127027dce79108ba61f8f4c93d09f4e311ce7aba5ba1d2002e4b0f5b583c47cbf120793d432c0afb0e14abff3e68ce75be6f6d913343659350bf4d2c75d03e9e130bb8ab44b226bd3f291d39a1585f3f993727bbf3d14f0e50f7ade3b2b087df5948d76ba14ab15d4f5ebbc6d30eb4df463c9fbf8d31a848649e56b8e118a8bbb278701f30a7f787bbdc074e2551ac1fa1c52c67dcd38b4173382b37fcc08c494a16f35289d8235ef03604898db012787e855e43ee129300afea4bd3b001444fa1614546b068fa827334a1d8637fe31697f2410d04b5b0895a805a85daf7b1cbf18cb22a5ccb8b8f283a1dcd74b66aa0ba972749ab17fd044b310cce748bfdc5c7c223262086beaf416b5d5c4a3095c4457f332418dfaf60e723408d30347fb9276c3a3ace7c20fd6e546b40009e752c707e15bb0faaa0ebb068ec22b4545e6d68c44d1904a97f746e090584e2ec26b2db0fb5e776e5f530cd1d02ee4ccc2efca81c09375eb5c88c7c671f865791e44b00b54509859dfc099b4e051a6b6ea58111950fcdec47c01c1cd47a36d8aee7a5906fb5ad96bb40befa1aa710c88a935b4c5cd30edb7d8f8cbc97655f1f2d757297007087658b85ced6f619cc154d12aadd7019097214140f01d4f5871977455f1558e13e34d1ef69180d258fd00362eeabb78e31633a1fd00adc145dfa3a475dda5ff30d304773e79c59e1bd25d5becd57bd4a426dabd494e3008a166f7721cd730dbfba3baf5fdfa8cdc0af19c4a40a503fd1bd16d51b237b75b36114668cfc01d97b412d12587f54161582cde29795da75e1cbab10135f6e9b1502b87e5473cee64a2e077ae5c7b94c700ff6b91506c7016a36ec840f3911bedc71cece3057e614a4c541fb22b068f7d7bea76c576a0abaf9cc5693bf986a9942b4005c63dfabdd10e3de07ef7d07e4f8995fa6bd8fa24e05ceaf4086d412c22c45965313493763fa6acf2c844fc0feccd5a96429e9916826c01c738a23c5299f3efe08c0053f8e5052926ee299aa27d05819788a587723c80e30b729a3e32a296e93ee953e7ee0916f9ae225bac00ae006d11cee2587eb318d85c7bdc897de808f6fa19202cd698f4223de0a885c192f656585cb78637cc024707e11a03319d42955087cc6532cd7bb82fdf59025a0e9ac5c31ec738ff63b7acec01299f163bf044d4df68eaf67e5e3f9474145b643b3f6324d6ce5fb298cd58ce8c0fe31cbce805554db1e283bd551eede147133e4ca02703e9522922a9d861f5d9fe1532ac661f0c4f064a7058b72cfc4562d7e0dcc3092f5aabe77d42e0ab77803067f04c1fd2731430304f19abf7bf005236ad9e7ddcfc3c5c32fc06dc16569c1c90be5b61412b2e414e2b7b4b2e4e3e483427904b82c04790478052d05b8b92c84ccb9a0fc9cdcfc9c825ce642423ce67cfcbc4202e7b52f6826c4cd07e5e5b4e4b3e0b330b312e2e532e78742fff6571e7e6e7e6e0b4121213e211e73a810d44a50889bdbd28a971f6ac12768c1c5c5cfcb67c629d4027f04cf05fb2fbc67033c5df103cdb10b1a7af017eb54c879ede76128f5bef839f2429ceb87ad27b769f6903846fc408cc52a36b9923e60f6d3c2b03c95b30a3fc7a7b05eba409832f4db2d6fe86ff93eecb84e1397489aa17c5d69a9cf828c814a3084fcff7ecb796427bf850ca505fe5007762671f2df494ec4a521c9ec4b373307706ae6a473bc22a369e40a7e36e452d18a2dbfb48abed20a17863e332afe9d56536af7aefa527cf6350a70594a2e2b1297a8dafd3950d6157ba7e5175cd4cb93ff37705132ff13b828d2803b46a608aa4c243ae45a3e72453ada8403bd5a0e6e938c4e1553fbd935e73115a15df0fe9fe2a206a80ab04b71dcdd6627c61c5625cbc72f258d2724284c0b77486088894eb94c13a10864dc9854b9cbcd13d3ecb92e8767e31f7f579f1125b8752d5fb741d6bd89cc7142e852540c7d6f6db5dc82e07a397db82cb4869989ba9aec6d71b2bc9521334699c89b83db90e18991857782f8bba48e0513bad3986cae2e9401a1b77d7c1e9f878b4218fa172eaab801a1fde2b8a87f475b73055a987d99916ba8a99452cebfd6b8d3390a7dfffa74b5571349b9889eaf2b2cbe8f5fc1fb26b5d7460e86d33e0389cc6fc14549734bdf6c5ff070dddc42af71a76d24bb81ad98f27f1b17053ffb172eea0fc245590adab978b919189b1b42a5cca4e5750d04bddc14b9bcb47d151415b864f58cf8b5cc3c95dd050ded8d0d94ffeff4af1479f43a2fdcf881fdb7e72f2794a2ef7f3b85dac679c588310dedb21242bbb09dafa8b3599e92dabfd0546392fa5831f38222ccf1c96c4411cd2573884d83b1610bf9afc2fda8eafe098e729c94d80ee81edaf03a242963e2d9ec088c45057044e0bd8bbb828bad7f738d5244b5c05f528dd6c71670a4b5e6d4a46bf6293ca2b514c7edadef2e26330c01cec0741ffa46f9fae5f82f26345f6128f4cf2be55e0309861f5f2e4c5274eb5accbdf6e2ea1ab9efa9a9170e181249c38cdca4e65a71b3eb7bd5ac88a6ea9c6ff2b901434b1d91bd60f3c497f78a786e0e520a6ea6733ab01f7097303e5eaab65f471b207d814678a79b8e2d1370246a5e69e88a8ab23f16df8c8d8337b32c123dc66b030c091d62ca82e7b0b2d3fbfe5568cf0a4f064b0580e49fa10704228f23c5bbc6116374c789aae4281988809989375236945be73f691579704f3660e07dc26c2b60a8245cb6206e6576abb351a44ca3c68a2efa94960f1882a11c5adb5f29e74a6f5d3d650e455b875ec377001258354360f78e56e4c6c782eede50a7bb54cd219c3873568e78e5b954a9378aa374e04df8963bd69bc7be44c0f763bf6bff70fa9cc3336e356e92cb9c14b3e09918e0c8a8a70957eb20ef0f30ad984ee89bca21bcaa2c0ee06276122021eb61b0e2b782c97224adcbf94411e286c0066f1de99168ed1285f97a6ae939bc90c92d4c23c0902251ca94195ed1800e3ed1cab2204db9cb11fc0970d449ee3dd1d5978eef9621d93e89bd072749cc80277f562e03c133c3aecc63dba7bbeda86159574027b901041af879e003d6a88cf554eed3897117c9e25bdeb705080668ad863b5eb4f8a9b88f2b3fff6cbff1ed91e602c0012bdaefab80992f1b2d177d2ff3a5b2715386df34c55979635f884a6c501a29df33629831f58aa26b8001906a71e7b3968613af234ef63e7d39e445cad7c6ad19c0d1f074978bad2506ee5e4ec247afc71eb750399cdd81f5eb279adac71b59221e8dad5c5b552ecb98d9990c101cc892bdc63e568a8423a679d97afffd53b2cdc911202efcb1eacda28c0c233fce9231161c3d4ac651ff1d4cc0d173d76724a275a4196b374f77da224b79fde91e808bf9e88156c6ef5be55192b72284782892e599b60eec3facfe7a37949ff71918bfa27492afaa94e425930170476326922f0542c66b7887bd74eef47666325f83073884ad3e3f799339bf99f978f449dfa7e6e4af81a259c071fd86ba842435ab1c114b01ebc8ff680039530017e87fd7e8a8595bcb26f4b5fc5192c2c29d2a5bb5b5ff16374fe1b3efb82417b6d70f798ddefc1acf0e0624d7e5329d7cd1a77f8bb092795f63522053a894ce1fc02dba8e7bf7f43c7ce814114bd371f8dede85fd201058bfccb00771444fad0231646e992a4ddef8727c87102060b031c61b5e56b3763b0ea2b20c1baf6288c2c305d48deb30fa49bc838eeb6e6bf8f75711e211d417da801a02778447d2d0bb8cdee6b267636e3ea358170db603028118928b54fa4f3c5bb375a2bbafe59117e363db03813a8bdb8bcce8ef36a72be8756b18421b35c2bc0c40474734999ae98bd7ef877dc3f7df6158f2cd4c07028ea44dde01cd8e6eee4bf72489c495eb4889b5d2ee01b8153fa765923b52541e35621e9eca3366fc335c91807e1f16275ed0785bdf378155cbd0ee796d9bee2a0d90003d5a7ec22b819090d9a343dc4cb5351a7258fc0a4830e73819a4735559bb81d1e2af9cb15079efd3bb500077672815f73e56362dda0ee701c935dc775a08f418c0fbcd5cfa08d96b6a34ba2f43d3a329d56d347fcf1f08a4c0123d8290d112529750a271e8dad8f910beae0181aacaf745f25158965adda67cb4c798e8d6cab85f8140009e24749fe8a38b9d6a53af56a66954a47aec0c00525ef6a24f7ec6493fc43324c88272fbcaaeaff555c011e9baa4970c4fabccb5961c46352ac117a4ef36e30bc4e5e9ba46624d1ef4609a324bf5f5b63d5a23ed05d69fa733c7e8888a599308c76e01a24f1145497901b0beb3c49a2c7467925e285fc27c3171b56e0599c604581f29ea89da4f9a6d718a8b2f1d8e27941bf122801cf1ac1c8a86a7ea65b753de3f2a57eff6b8cd54af021004ad5838bcb9f2dc5bb6616c43d681279fe89a822218085e69648ca57e98e491d5ff151165d16f832c14c0cd696e6e8d3d59d8f027a936412ce97c2879dd11fc7e4faf23229741b811ebae23065b34379a9b63f3ef01dc47370f3b668a6123cdbe6fd593c7fd8ef71ebe006ce0185d497716a76a67dc3bb46653da0c6314312800ee2eab665a02e5e1db78b51a3ed2927b93cf6b25440047bf869ad1e31888805b8bcb37e4b7b0dc8f2f455381f1ffa4ca2f4cfc522fa4719bfc39d39d006a22351d8020e39364879eaa9a1743c7420301e3b29dbe9fcca0f3597924e57afbf4cb36e4daf1c9d5729129e1b608084040e07c6330a0d62b737da1358358ac76217d883c17709433d4f0b0adb2d70dbe9124f3769cc4a3dc476f0602659f5f2e75466bfa22904e0fa493989b848c24eb02eb6b717f606aa6d45be04d78e6160bf33255afda20a09fad07a770eebd0c76db22b74f8b386de28d24090254fcea702129ee1bafea57d3487c10b8a7e00f3ff301b8c1eee2fbc7bcf25fae1d1d0cf9a3a65e1b78549d07f838aeaa47f6ede06a5569e234ce0f8a8c1c6e154c02f679680932b7bdc52b4109f9d89dafa8ad5bce32c800ee474767d261ecb3324159fdc9f44cb779755aa6f6d3b3f264a912f6e7df866f9064a8a1e6e7bd1ff77cc38d0a047af0478de834a6aaa3d84226a2d3e762b007219ec0f967e1b51b7337f036d3acbee368a40122298d2fd0bf56b8e39fbb702da509516a253416633fb7242300815ef72b9e0a97d7164fa1349b47efa7543a18a6df0104061fb8cdf59ff86f88706f2ea03f4965d935372b06f4ef453821d1700e2c85cca3222fbc4b48b1454889c0fe591eeedfe97ddc09f3d426372f65aa0c73c60dc10602d585095bce46289cb8643c9ff513a3f11936fb0182818503e9c5fb19f4c9abd569c70b0192a963d51840206df863d44854fff003f172d36d9eee6d36aa3b730010c6a86582efe6edab75a9be276108b9cf59ee650e80b822da6c24946f45f5dcadcfa7f03afddd58072471e18f2006e19788d55db3ffc904d00091d5d3bc5a704177f97949a21029195d250df51b3a7a9a9aaa4637d4a57495f4e56ee86aa8c8a9ff2a43efa2de39c03bc62efbc1fd6044021e2f58b817b785147724fa1a50447403be1a5b1ce93041bb0557a6f0b0c5dea1ea88e6c74dffd29d2091f33fbaf64fca8d5b1ee76af7d09888dc65294cd4cabafd60fbfe425709cb8b177ef32554a8c89af3944c477b0a033166f36b9b02a4faf3bfc507146ed7a0cb8a6df26a2d5292e41dfa3381d6d1bbf0ffb77d407f1139ff6144ce3a5c6e722a8272ce3ac6da5cc65c960a9aaa02eacaeab6d27cae7cdcaa827a76e642fa3c16ae76fa5a9e5cee7fdcfbff1fc88d331312b2829a5b710a729959f09a9b5b7259f272720a5a7271715af1f359f25a5808580a9a9b99f1f10bf2f3085972ffedd742e6bc42e6dc961682568250a10b7dff9f9e1b77bbeab361bac40bc93decacbcd1a9b1b601fa6f73cac672a88c976d4db3f72b29d0970cd8e3d73b5ebb366c608ad006d4dc0daecb9cf118afc1f1ce8ad75165b23d9fc8f9bca0d1d06dcc43ceb75bcbd053547cd5058d98778de63f2c518dcb611c571a2e09d772a80a0ba5e8837ea7b99b25d24f9f578e7d467f6f2562a2054bc680974dfdd90b22edbc4c83df828512a4757cfafb3f797cddb83cffe06261a139d5477efa5f1038643b32d265c88d7f7cd6bfea1fba58fdb4fe21e91bf684aae63bd1df935dbdcd8f1160ac37557463f11300fcfe5fa87f98fe3df60d0278f63506a7efb26fd0c560427ec0bef137d1f7dee03f857d031dc36793e7276a0dca3c3aa6ba98fe7983f41fc5bec18cfae39ffe9c7d0322f141ea039784f49fc8be811c6b41cdcb3418f207b36fc01f2169c142a87737fef65f0392ac3c67f6d8ca00194356b4db9176d17beb51a78ed95b34b964effc869e54abdd3aa8990eeebbd889041e884a949743a2782d4c49836061241472b8358329dd3fbc55a243ab5f812efd82a1e20bd473f9659a707a73fecaebcfd718fe3ec2577a1cdbb587a2b8a461d987a1ccd0df224767b548d711c4df7d92e12dd0b3cfa0f326f934ec42ea6017dec3737e327bb1ef878f4459a3f5f92dfbe279df781e13ff79ede3714dd7eeb967eef7dbcae4783c7abba8784bbdf477f7d1af9ae3e74ea173b44a08edbffd05090e6e050e6e040eee2e1c9cc64fac160937458b9653cabf6d43106e29b720a54ad578cb09822fd4928459df1c8704bed7d952df3d65e110d949fdd5cefd96eced2ff656bfa95882e4e56b827dcaff3afa9030899424b8ca5bccbebb18a8ed55661bce5b26ffbe09240efe904bed2fe8ce6fda8f7f0d74c7098bf9b2a8d7f1a33be874b3fcd647210b5b118af3aa247315214e376cca38f343ceb99fe082f7ffec0a26500b7c1eaa797d52867b80a6a045f7f8d23ef363d3cbbd06ccdbfdbce602eb848e57cc4311c414584e5f8ce22324da656e691aac61092cf36af458cc094e2d5c27173799c21c75241b3aee88c30889b89795f08d6a7d643f278c8378ff24893b4749acf821dc9ed70c2af693b7fb06f1d54f57678dfcd69f4975cdbbee3930a7fb289c03dd39b4878550d70fd75bb230559de38ff248faf1be6be4635fdd1aff4339218c9ec58167f30882010ba1b24c6880206633088aff91faef77ae2ec025cec2f630dadf65b063dbac9b208d947a1575e3f2cfe72edb8fe8b8242aadee27a97cfb895bccfb16f62c3ef6f17993e308b20a0b21df2f6c807c64170822bc98b10f01a0f42af54ea3aaf0f5debecfb96f707ab2ca7d23d1bff95be4e75dc69f0d3c92b29ae80d7a23a29ac88ed068331ec6ffeee1622aa99d2ef9c94ec2f4b4e6704745fdb77cffbf5d0ce43fa537fd35a5f22e6aaefc32dcfdc57fc27063a634b2d772119b5d39de97cb5dcaf9a8d23ebcf55be4ffa3c34ddf878c61fb9387d1bfbe62c158f6fc82c35df5bf3ddcf087fae1ee018ec13b6d21d8d2922114ad70f5b27b6d86af7e7e57deea4f1d9e9b07082df047f05bb05fdaac7ed100ff311be92d509e6300935ed369d461e41e9d694abdcf69b2e0f0aee705d5880baae53f5739ad1cd8c4cee9caf3f626ea700c744296fc7a5b882137126640c3728645edf5a9b5dd45d4c1d5b2bb0718ce175389cf53a32e03e8808222b75bc43b215bbd6c09619e09e2a685339047e7b4ffd3c964a3666a789ea373226eb016ad2bfa3295c4c8f11727b83889a0f287f3b4668cc872aa87e934befa1af82a840a4d4d19370d6adbde45b1e04d88acad70954d3d9ad489a08033b9264e7e41476b57c3bed5a9a4cc24dc6efb67b96403782b7cabc2670bbcac3e89908f7b3cde527e726b2e6c6e525f4ae3f16dd29df15f8fad7d3c0a324537b089094e76ef6e1ffc1515f310bd231070a363f45d2cff39f7112df354d317cfb9f5e6055550e0f2bb0cf39089857f9d315a9f86d4436cfab037ebdfccb56fe5b0bcd1b68709ba13990d4d38aaefce99fbc217991b7feb7b024f5ca9e937039c1fecfd7ddba43951db834251c30e4e7c5d1e33eccd3d21bf1530aa9fb1835d4cd74612c8852a040747c042d794a7b492371b90362374d7a388ec7f378801b96010037211f1f9fd7f5e100f8e5a51efbe634ea6cf27c623821df84df15bf8c2e3e9890341989dfdd1ba108494c63f77efa105fa077ec307f35daa30df71e86d24f1a9d2444c24bd0717995fbf627e4ab14677bd852faea19beb9f274a1a6d25627acc83307f18c42e72176ef99e2acc0b53bb6ad9bbe36093815501a903b3362c2e5a461a3ada21c633d05f7b9ad69674c1f927e599e9313f91a67ebd321b4799f99dffbe9a58d4221f1bf63d44e6b6cb3954b415f615c1796ed0a19adb64c6d7c8cbb4b67b5a632503e7cb6ea1d21029470be871fd8a208fe62fe72da3be4dbd6ce7e604fe7947ff8f0d21f4f937e3fa9bdfd9ce9e26fe2f6c67b36f0c540e775bfd2e893c5587d0db4b77b433bb5f703b13bde876d6126ce2f13ad321cfc7ae3ab2a7a5f0d00f05d6cef99633d5fa76c7d3ab2c028fe4b1a96a1d12b2659643f80a356b6a933b563172964a3eefe7c2f3475458a1d7ff3fbe9d895e743bebe365dc54dabaabdefbad516ce05927b9d38c8043d7e5f7513a5da8f454096c08ff3fdfcece9b9f52f3eb6d77a3fc3adfc488d1892a93d0bc24b1fda4d857e6ac701266632c5a705c87d83e8ce9307c3799f4e4284797b178837f60e992cb49dad1073b57425f4cbacf1755a5e8ba3c1de9f5623b78571bdc9d457aea842ccc6fa4917822d76f118f2993363828bf362431b7c2be0e5ba52962268b4e408eb8f70e56a25da4c60e4d6e6ee5463f7f3b1b09c7331ea454590ac1d8e20821bda91a0ad15a0ffedb96441c8186cbed8915291e82bab41342c873bd0111b18153e002531a124714beb7744e58e082a5532f5c78e3c7177248d5a022aaf8efde729151178311ceab6cbb887a4efb806b440683a5a8ec9ae5ba48543ac66cbcedcac3fbe91ee7dc7f5e695fea8b6d19e75a67172e2cf2dbbff90fbdfffffa25058140e0e08e0c29473c7dfebbff1c21781115f9a20f68e98185603355861ed7648bc38fc04219b5ddddfffe4f241c5330fe838d6aa296c3bbb050e4fbf592f5213f39a468120fa594b0c61b7e386190e884f32363e9dcbfe70dfeae8f09b2fb8fffbdea7b26f47c180e0b891f2fad0fa4817daf54f8f75a97fc95917964ccab9e24f9f36cc2526f19a296563001880b8ae018760cab71008ae0047a087b37d1110437ec4a064f9f42087bc2700304317234a9f2626ebf73c91900d5535f5ff9738171b7809657eb93116985bacbddae409e80fd1c5277cf8450099fabe595e2e7271c5a054cc9bf5bb58583c35188c1fa99630f57afe0c5f461092c84bac4a6bee0c3d5ea7a04023808649711e7d7763e03040ec2ca88f36b0795021191118711a7118f86868626e66e9e5cd21d8f6c67357dfbedaeefb58ef470790b331c8593ba5bdbe1e01f9bf211fc63580879665503fc793e9c9f683682ef3eabdafcb16bfe77c443b22f30b4bfc6237dde2b0308addf8ae03adff201dfffbcfef91d08348096e17b53f04f7eff73e3614710adb0a5e17c9c5fb2f0ff02b7ff77703b29db5ae050ed60da6baf95872a31d912af4f9ad97fd2f6dfbbf2ff88df3168e84553c4fb0753ca58aac8effcd83316b8a298feb70d751d2186b6e678375ceaf602ff3bbd779f982aa83c55a14a94dd1588b78f63fad221b3b2977e5550f4d665162bb584bcdc1bfb11cc96d73e6dc89324ca7b14eca72290ff65a85fc850fff4ad8780ba4864768807d1f39bc1573d59ab58bfbed3cf21b82514b1927095a8ffe986fabeff0c3ccbe3aa264c3d6bebdb455889b4471c47b89288ad1a4c452a0625fcf8cc446c2b6fef193ba3f4e8bacc4128f45a577b74b2bb387b42152239cbdf54ab5dd4ef985487aa529e8a95d99aee6fe53016fbe2f6d2d58da1a8bae6f62bbe944b09f3d1b4962a16737d03b3995bafa2aa5f9998b177a3d30e56b05b4dea7ce20fe2d2db26fbd57e47b28c8c7ad94ba685404cf37cfce6bfae2b5f496f5aec9327fdd2e43f7274ea65774e760002dcdf0cf833469e6d811f89c0c24d35d280c19de13982b5c08f462258c90d79b3c3817fff7f05233d679f9147f1ffb1f71e5051254be3f8bd776618d2903328a86444c928a08282a82888a09840328a18401441d44172ce3923410912244b18099291019138484609029291f03feeee6f9fb36f61961dfdbef77fdff659cf1eade9beddd5d595baaa7a8e1013f9e785c25fd9c75b0f5cd5a2c31ea203983779a170dd2cd331509c91e8306cfc9f2c81a9687b8105a0008cf77df1b99b003ed9856c3a12795d68c55fb81c88e69cac23637876cf402ca97402ddcca6928c8a5044b8dd1fcecd29d6a979c016506e77df98fe3ea5c21b297aceba02c7dc675cd3db08b9c2beff8b9f103785b83197811c40b35fa3cc9fe3d0deae3022655e01984e312c73895e977c5499f1ce8e5d27e6cfcd609a882432c3c30e7bf0cae65f97d9c6121472e9119da2afe8cbbbbbc7672e69e395170999333aec667bd5eee96945ef8ccc846d764e7d529b7f82eeff95d6fb85ffb1278dd8ef3ee495aff8a560359f28fe0728ecc02caaa0ed779ccdfdf75c1dedd4b0994b464552d5120670daf196557d83079b8784d9758a2ca13c7e318f0b500e3c7b1f1052516e7cff97db6c81b76d209f54779f97e6515e43593452a49b472e4a485aaf2eb675995ce81a9c0a7d77157958797b819dce4e7d0731510f6cfa4bddc6532708c18923274abc57c5378c3e258b29076e44b2335dab92851d58537e7111344c09fac053a008dcdaf29a09ab7d30a2e0f4b1b1449c84ddf2758f08a86d4cd9ffcbce46025e02a65c02e8c710e7840018c65f47313fe43ecb963471fdcddab99bb51666d5cffffaf4c9491154ac5e524932dbc5a62fcbac9caf387de3c9bcd69f45e9efd6f1a941a73f33cea8566761ad9b51ba29edfd946def13e9be8369d31328d57922cd27bc16f7d2ae0567b79def73845cf115ca6c0765e510f88f347f7aa6530676c354cf7ae034b47c0de5cec7cad4d36c05de490344146b237194e36ff4f456fcdb6a00d4dfa77386f324f64dd313047e4564c81929a9c259a38cadac614b690404d7005acbc6373910e991831347637042f9f504708c20b407b4c4799760ba449af0c056f677eb1e5342da378c8b38f826351cffda190188fbfe965d2874676f5ebb63a577fb9c38d1671cb4aecf27ed209287e2c99221bfc336e4e39f8407263e73b2f432da229c180d89f4c812c28fd09fd21c38fb379cf37fcfa54e648313e91d962774bed609b4ad78dcffc6fc627ea48b704bf20100005ffa7d78af1fa48165a8f34ef48f102677d3cff96a7fe4ae5e2194d94441e4fa3144ae9fe8db01627508a74007bc8c6281ce8685fab4d9cf2635bbb2cb769e8dcf3aaefaa76eee92ab26ecf02bc309daa8d263c41c90259819da9eebd99d0298e15f30c76e12658ec1c5f6114a442332518d90ea6df997ccb1438d33c04d49e1277793077acbe44f455f9d6fec75c8297989fb29e6188a48f8df6d2a27b4d2c6eb4307f437fc452609eadd8b3d673759da65f96b074fff3ceecdda5ef5367a66e3f9ed38dc3d4219f2b3cd317f8530875b25f605fec2fcf3d04bc6ec9c338ee37fdbb66c71413c399eb3eb0696eed7785b091634065a82cdfdee062f807dfcd15e63424e1f42e3fb64b656b95f8ebabbbf76e4dcbb68a6fddbf9b7ad10f2c36f8a85e3a7597fb2d758974817c00637ddd02100b80a00aa682e5e13cf863fc3fbbebd2bfdfc16679644faaabbfdf93b2b4bf985369601e756b305e69e1f04b8de1797deffd35d8c410e619a375ee64b003cadce1b2bd6994bd775b96025ac8237a67ec745895d4b357f3a1a7c96d9b89d28c41cd738bc158d841065fdb1195b94e96c859d12a2fcc797ef9d2c6258320e77b1b78d3d4e59cb5cce8424b8f7318468f717073bd040feca4cc070e1430e5364c78968e0a4e4ecf93ffbdc060ef63003ed1c0cb444a98eb647d65db3e7cc5e2fa0547c9276ae8e48858648839a90c14a42e0e091e1cdcf53e3a4b97229ef3394b4e55ad4e17ad6a481bb7c444a4b02f383144defd1e1b1f4ee5bb3a7b4fd77175be5ec3947578810e8d6e79d268e3980f2c07f75232431817fbb0788a6a2316cd617a7403dcf6c9a2dc1ead7dbebeb6dd439fb4c84fbda35a250a8803d9183d6b4b2b90b5ad566d198224cd7f358ce8a3c8dd87f3499bd9039eb201d935bdc94d4b6be3e292c65b01d07fb646d76994c47f8b2aad5b34a39c5ae0bcaec3f085332d9d7a364dd3430ef0bef8846fb6444169e2834fb87befffbe8bbe0e1ad44ac75ac7262d3d7b5af2fefa4db1feddd709f507d1414b9393d0f88a1efdc86834acf750b2f391e799b54eaa6cce4c6bbceacf6d697739525f4fe89979e1799c0d9b16d364a2f041fe62a9fd8fb3478c9c729d7dcb60aa27a5f22e6ff21e13ec70fc21429956468855b46f93ee8faacf1cac0c27ad53deaff69fafe3b22fc1ffade1a7d53b59e7b79d4a12f045eb37dd0a2f05d0f4f42dad94d77a9eb632331f4cd25ac5f8cac7d75f58ca2055d67a3e3b6b15b3220a7746b7d3857ff0b3e669097a5d2a831b5de047c0913aabee537b623a8adc628c36574b8d47338b2ba96afe947f16f69946560eea3ab32ebf2ebe377f3b27663ef1bb1fcc3bffffbe83b9bc1f5c3f9280fc3a38faeb05d4fca540a4dd15cdea8b3904b67589955132331f45d1bec86ea08d6a5a93c7bf85060f7a3bd46039e39d609a5b7f4be8e57eee77ef79a27cb663ad784ab1d27a7dd822caca360e29911c03dd2813ff7f017ed8cca162286be31d012c9774605c98f302a888c9bc7bfa531bf99074fd2a52a78d3cd1678e7d42175217e04f73f87eebf50a8b4b9f5c7a05106816fa344992e88c97e79b27bc382aa6e55bbe0ae1f899aa0c26dfae2cfdc153cc7ecdd25ee70b71a6999193986678ca304b82e4567362bf37e3a5bef4236975d6bf0e2fe81b29ddedd2e61f125569967baf4e51b6c6a9897e6267f945011d570006cb9bf0cdd7c745edd4e26237cfbc186e67fe8fbbf8fbe5f4514f579287a625dd5baf6df1e9d5f55d6faba2109c105940e830bb789a2eff1474105de1f137030dda8d383ec4d3c1c819a12cd1a7b5bdafa8abcee0b529718bd28dee512bba27e829d4de0fa8cf89ec03334422ed8c29ba7ade6d8071542dd3988132a64df0915b21f215460c4113da19c6a12bcef0b7a3f0a3d257f8f2daa20daadb9f4bc773bdff1da7f0ee5ff6da1a3a1784c72fc3c7142a79a975c0bfde1de5bc6d9c6f70ad7d2e73f88df77a7886931e9fd28c73a80fbdcb0fdf9c045e551eb1294aaaf3b5b30f562b2ba78c11b531348f66ec9e4e4b3a0d61f25742c8c33c623ca4e312c3e35270f5f7f58830b5490ff87befffbe83bedeb4199f301cb49254d776b690bfbcd35b52737da27b0ef7341b1f97e2ea2e8fb7085bf896bb1e21dcb27891aeb417c394b4c3a323338cd636c85346b3745156a0ff095089739560696665cfcc0997222cd74d45f7ebfd72e43d3e53db581c58c613fcc1365d02f6a28ac9ececa8d296b69f351988caa20fb87befffbe83b5d98f255f5cd9dfd07e58ae82f2f25cfd5234d368c7fd03844c2ef474f547e9002e4f5ae5341a7c3b55feae6cedd7dc7d427af5590bf6d0b5753355ce77be215ac210d73e0b6ed7cc32c5c2b6b2fbcb4eb4e55a6dfa998968395cc48dfd8aaf6b95222952a30136d8fe84bb16749552a00e3696c75e289b3b4f1efbd3ff47e8ecfab65b4cd5c5e3894ba43924945fa4a0681f109619350bc19b16f76833fb27fa0ee47be74a99a5382e532fb8bfad6ee1bd0b87411db1f032d4efd2bbf70d0ac32112fc42ff3ca552d85dd6795cef81ec4d564d18a3f3eff90fcef2ef90075093b67d241898de00271dc6cf3bb3719591935f8e10fc95c84bce921ef85b1b73bf283c371056ec3af6edd937f26d8f25b4a2565853ddf49ee7fcfd2fca5ed7c7707464d7ba3e7d37cb64c95fb534251911868f1141a694f4577235f717cd20f248e2ac1af1870097ed719e651611af20c70900dab050ae086e5e7351ffd6d72fe6d2c39c185e9f01571f6b383d1de594aa496399711ad67b699430fd2c68c65df38beda7c78fcf07423996394832e363e76fb6d4aac6ae777b21f503bb9c9a27c989c7d0589e3aff1997fdbaf075b65db9b949544007f04687b930cefdff04ba03f818d27f47c1cc14c2722c3f709d6dd620846cb77a9f3f2bde09cad967c375c71b83f4b87740bec8ce8ba52c4f62776feffcbfd7f7ac3404b0831b47d4810dae125a0905180e03e70c65b9238cf08bcfd1b3f84a300162d93fe1cb98b36b94d7da98cc573db04cc726e270605aa8c58329cf06df16166b5bd59d9d613fff263e0b9576454a4e1fedbbcf7a6bdbd2eb17d7479a9e28a750508f68c8558bb4a6b79143ac9a4dd7ae33371ac903a457acc607059a45ea66031f393e5aa6b40500ca975d7dc10f9d8487fdd99994c6057eb74eab113fa244de53e7300ec128762b26e44ab19ab0486bad6ee94e9dab2cf7aec13a959c3febb3d67ac92dc8f8b1638995c11b546e65f54618c7cab0b77a2a4a9101d6fd53932d3928e842983f037d6fe16c915e728c95bedc391b748a2a86342d1ad6594e6025d6a5719a81b8f9325177820e8ced41f7aba7ae94139d5d851e46e5f9d1ef66ab26bccac4306ed0300c9271e5abd2fa3db8ff15d12cf14412f2dbfd805eb61aa9e3b162db9e39072ecc085bd2ca891bb6f5ec30370373e2dbd31ba38455ba997d85b6e79fe347702bd85513f889c62e06ebdf830f1c9f44a788df004678c892069d53e717ac584b7676f92afcd541d795987487ec94c5ae40bd5e8a047b7c3168f69dd5f51bb8d220d6c337ff4c91524dfb77f8ff1fbf28e6d1cc22981a2fc77c67053088af46caf13be327bea016bfaaea35670523b05ddc37e4257a3abfc8557461826e23f39f6bb74fa8892a19e16ef01284db34e97bff0bb311328fe35c472e5355d181387beb1edd70e8bf315ad0f47040e6b488728e45b01e9bb42a2d592ae985f114cef45c0c92a0331a6068f904cb22120954e656fa1b6feb8d987150ce3ab37aa8851131a55ef13373a3347c63c2f615905ac52ebafcb4a2aa88fdaed18aed3bcfd956550e21a77f55e56963b15d0e28be90b00b56b64217af07e49aba02749dde311b2d032c3a6a754ddede8ac638509e1edbdf3ea90155bc6887967a62a4a93c6464428b0e2325970d9a9cbd726831747442b02419ad0c473ef3b8fe9ea8c694459698bcf734e21101a8e3674cecb76425e1e4a8de97b16c54bea1a9f0fd5f205ead8cea1672f9d45ab7d36b7eb5623efa7c89a30940468e9221ea47234ec36a84c616bf7683815312a65b682107dcf19814cd0380731b5527ca62b9e835d9870d25195d065c5369daf1b64863d8fba3ef2e0b6a0d6c1134e00dd2517a7d797488f2b69c7e9dd3c72d2c8298e8d6e4df94edaf57599973c36bdcff76b1a2ad25395f8c8b57c2edab142fb440fe552b84a552322f7a2c6c125ecba8a32686cc64bce6469e900e03d9041c10d31a8bc344e14c3aad87f516ceeed676ff9f5212238580a1210257c78711118c1e4fd59bbd63cc9a92eabf18d7690cadd352bc317b66143240d38be9e3ebe2bd83563bea16e239edb7a00deddf3b1e5bbfca5ef994e7d20fb40d5ccadfef642b582080be06069c96408008684c51da10c1d77f33bac83cb5a870c12b65fa432bd1237b5fa7693524a1767104d739bc4045c7cb6686cf576134d857a6f63f68edff1b3e884b63fc069fb93cad7dd97e470ade85418ba59d1dd861278186dfdca1677b1acd9f2932c72973359be6cbe1f729773b5d11453fd62dd2fc90ff915a27a79f344fab30929b1f91bfc3be27a958eb0010d81fef17b0905c6d2fc4f2e66e387708c34f4616a323d9b75ce6ad221fb0f5a0cb8111c710d9d954fd6debea9160db39346ff27edccc6d1e6e4152e4c0e633b3735dfd7b733fd272dc688883303e6c699ad11722bfe4f2e268a183223f1b2224c66ea4ee2b7b167fd62edd96b52f31505443a377427a17a735f26114a1e05e2c71b9c7955f633a1745e7c1b12fd7bb9268efb3bffbd1ac8336fa7a1ab576a6db56ff40e76283a5f02cca573365dd2f1c7f27f52263e5cfc7fa1fa1c795b08eb4bec7a08fd1da73df7224d53992fe04afff89b2d569f3b42a4ab4de119798409c5117fb30c7ee9c8695f239bfd5fc932904c0b1317d07025586bf34b4d7f391c75f4a361de9ec68a3409e7ba783517e71bcb09e9c7a22760cb60c7b5ffe365e28ffc6ddfc46febd375bfd089b1e0b0b4b8ac601a2a0c0db1a996dd6222d1425e889a3e6b27774598c0fcfed3abcf11a24f85d31dec48067aa3f2801187bd2399f73b2baf3cada165df4e3b7b4ccc708937e88a28ed6dbfb19dea59fa5a539a3a0e16ed958ae3de38fda6d152b62fcff3e59f5f22b64cfc9a013662e6a0c0f69ee6f72c958d3a41324509356909ad4fd21a0f830fb69d37b8776980ec8bc531099bacf4fed79357eb48075befeeee3c75102a66ed32f13055ff6baf5efc5e2aee8f7ecdb0db8c5e928c995fd657ef5cf0913ec92e612a63b2e9782755beb13384faef63fef6a0b0b89a64389d00f355b3acaf61176451d3f7c44d294b6f87749f19e89f0e8e7e5546dc39c13f0712771e01cbd26d771c13f9f62ad30acff75ff0d8d4e52dab5adb4a241fa5fdc383c27b6e6c6d9fff4f3d28bcd56876d93042612aff790f0a135ae38f7e5098108e08d338411222f4a0f0c6953cc1ed0030a5ce75f2c4625725f7555691b8077bd9e68aaa49f6dccafb533c2a3e2c833df9991445882264a1c7e25ba1f8c3737c73d44cb1b0dcfa946345f5710ca7296976fc9ad0ba6136e49f8cb6e983c2d0e4efd74ff1aa12221bed36454373b64e4bb65f385e31ff255897b34b48a2557b3485037221d8d491225ed612288055bc3b12fb8500a2085d0fc410cb7a72d9c3ecd43d8e6b5f64c9ad477dede4a8b9d7d6c8fbba0f5b13099765e790b0fdb9ceeb5bc6f9fb4df4dfb1489f7091fd2c71a65c738d79fe107d5af6eaa160615dda6b6c9c7855c796a71d9e37cba8c1acaa32e35bdd325a0c34580954efa034fedbfd7fdb31e24413b1b5434133bc0dc4b108b58a0a1fd5f4b0a5306d6cbbe2557ff5e127eac3bfd008992b7ef1fbade38a602512426c9ec8a428829582f0e0b7fdedeedd6e0b4d743e64e85ea3b66e5be6bc66f6b7bffc97f67a03d6f06bad5d84f2ffcf6bedd26641730c019b6cee80b1b207973c217afea7d6eeaf2acddb11a45ac1d7433fb1d62e02a05c3f107819f3836bedd63b81051fbfa453da0b2c00e825d86f857761f199098ff154346b6af1076a1ce707d4acb80391c5eda673d5eba178ccc4ac5aa425af04b4dcd171894ed65029cf5513af1253f4b67dee13ace617a686b02c719750504bab4a2eb3881bab95fc2d41535641de6bcebeef5df45caf3f99295338a1e33c182a6d17496073ed5ef58e1d39722ece2e6c27bf5cf942233509b5ca3381fc0825b54fb10353db73577eae1a4b309c8fd0f07a5011fb4b9b337bba38929132652f7b0a1b745cdbb71149f1f0a050dbc47fc6fd755ce28678b2cfb5d0f272b56ec4bde44126ebdb67071623565312777942bc65e7b0a7a5ae4bd2edc8b2256bdc79f3a1586d9285bd3b92d657dc5c4c45ee66d1761dae200d9a19766ecb5c5d76ab188190532526392a2bc1e5e81b8d7bd68eb5bf9361c8e3b1eee4a270a4cf6b2ccc893c6d542571c6f20a56538551e1ab887b3b676471fedb5ad32e02feb425c80c6dcfd5852b80fe4a25ad3c3c71bad5aa52c8ec676c8fdc2ee8c04ef2b36a70a85b2b3dd338c3f8b65eddec4e17dfcd4cdc7c4cce3b427022bd0e7895b4f6f598d3a9d928da80efad9512b6777f5a3f4ea7feab866c8501f294a69f5f3c75689fd4994397ed4de4ac1f9f3ac52bfbb72a6911aa6df2b32a69f565d091272c5d50ddbee12fae8c5fd29d44136ba9333cb1ba58b8b85f534cd1c5cd07542857f75f52aedce8c78b994e4a9ec58a4c35df682e3fde703095406995bb41446aa042f84cb4bcda0747ab9dfe80543eceb7b4d537c4e842cd2fd5b2117e1e1bd44b8796e0a2687bf658a66f7f0ae0dca941a7db8974321359ee86d02905892c9703288abb6b8f7eff0f0119a4c98056043d326372f7f1b00e378f21af4d5350480ef51f25826b2ac2682ec1b7f2fdab2f8f9e17ff6a9144d530ebd83b5855ec64b4a24484c617df7bef443191e31359cb0b84e79f673944249de1d181bfd994d98bcc363b12499338831613d5c1ccd10062f690446a86103b16faf37d33fcf1d531b6485f3c2e526b5ba1afade20e032dd1d3a2ed4927fcedb92a450be8e13f20f390d0336c04dc3a848a4e2309f0790a024c851a6ffec1ee98ac9da93e4adefbbb6bae1c66b8c699dee2fa73556518fc8f992f395fd70a438e19ab61e323cbe24e77e46005d404893c94c466a7fd87374291cb205c4165066f1faf4ea3571aaf4a5df5f27198921489122825e70a24cecffbfbcba17f8b8e8ccdf61178f684402d389a3dd63f11c10a51777df6d4684367c22c611a6edda24223c61ccf6f2ea45cd2f077bc55fd08dca328200dbe411f7d5675b94c9fff691295ed87fd4c98a915c9bd0d982871ad64f88fca4c3b7b90f65ef067df1e46a3e8b133b4a54b7e2839a57fcee77fdff92474feb69e1dc847430c1d199beddbf4560ba698b7e14bb7b570def07ddb9e9efd99e733e37a97cf74d64d7e72cba1a327231eee1e61ba5c5638dbbe1690622b1a72884eefeef96b6ce001958e0173a9c551414471de2b6a26de17374b6d23a4f3cf459c0afa51e7b3859f143c247cf9c43d79b515aff64b313b8ea5c5fc733eff399f5b3d9f5ba52363b37d7d9b0e2f5d716b4347c5bd81ac84426e929f793e73f25f54a393b5949e29f879b922dde55c789a9badb31605a305bade7e92adbf62a2f8cc60c242d2e5a3a77edeb5dbc312a968e7a342eeb2f6a5a704cc5fcf5555ffa8f3998da81b71097f6d793a5f2de9bcb0f3c09a425fe33fe7f3bfef7c123a7f5bd76ff1cfe756e9c8d86cdfc046b0bf5219045439fe33afcb140ea166667a62de190cc34814d1cce72b7c2f1422295da93f0977589fc5d5b34f704af90dc8bdf3c1753d37572b58f379a3712a898bd48e5a747b71f1d392ca74fb1f5559646426ebd1c3db83a88e156ff29efb892eecd45e887fcee73fe773abe773ab74b4d9f9e4570e5f5dae2270451173f867668a2af0a55e4dcade57066fd9717a6f4355a57145cef91d5c165d01326f9b8d60b9e9f2f78753c8b52cfa0b464ead891d1558e35f4c385e6824eec7b660375c9b7ed48392b81a8f9b66e45b9fd093f4bcaa74412da07b5b86208fd0a5630c698433f2bf4b64edf6f8b189ace1d3550039b58cac0226dc72171d9752578d6601d97ef9a709fabb460e0904d66a9cb02c14d2c344768a7f3072bba55d5caed2829d57d4f9dac71ffedab0b3c60d0431d1fbd2c5f7b8b14e9a76cf67272fc6b5bb020d4176b7be565c39f6f4cb820c374d31ed7d5c53bada24e93bf11b474c2e4b7f49d25d998d8dce2845eea4a52b5404608e08f645feb381e606fecbe3c6081d83e1eaf8f39431753b9faed765b62627329a674bf13e8e92960b676248d61b3b7d7fa7b7c0a89fb00befc275a957ad2bae8d209cc74292ee93cd48b26ea6c5e7c78aedb45fa813ae501dd4e05353a55bdbdefd8254a619c23e6d67d0b3949fcc01d5c4eca9dfe6c67b32cfe4662b769a7356940e0308f4c1ecde584147d7220d72e6de42b7b2bd6e947b4b3245505a3bdbe51bf2cfae1ced28b05ae67c80493e8c1b0c60e8d790284a473b1b573db2e5192d647451bd0490a47eeeef8bfa045ed1e7fb6ab1eb2d25f3ed825a160f83dd3086de8a39f2b63bdaa7768ad8c8ee59e63432ae118a9c1c2ff50d193fa1aac737c491fd31e1f1f98b9600f24a2fe349cc7d5b4b6bc16bc5dd7b1a583f4ff6a97cb1f87af2a122d0b55b2349e38eeca5d360735d8ccc411fdf107359ea84073a56d12f30d3add3d4fec5de05140039176f6acade78c68f4fbe523bc41eb440aea40ced71fcf20e1df0ac71e735cb068ea3e7128e26aca479164837969d968712de3e4ef4c93ba7b05f675c1775cc78fe3e40f1f4da3993a5654e2e0dbec38a54f6b58e7ed7ab6c5ab8f8cfd1dcd97e328e9d32fb112897538c256bd69978d85bb5b8ebf57106a9f829756dadeb2514b3810d0bf740cad9d26c8ab37992b3ab06bcbcee1a31e117589e8995e94fea931c53a37a2098a80b303d6759459a76973ebcafc93570e5d8bd0a98c413e3c8bbb93dad90c73d0e5a801a4cd206a0dcaa06956bea4dc59fc203b62d9d4c1dbda8b61f65e27b8369e4fca93df556274a8e087e4999699663bef652a24be97e6132d3d4f4b6d8050f9a91a24480e6f2a1d30d27e33bb8b6ed3fba83af45f0b67f66866d53cfd17337e4ddbfea771f1263af7fd148da3f21577fce3d917287bb9673a2ac2d83fe1cef191f741c10576b0fd0c90d77501dbd4f6f2f7b441e48f476d12e7884524d2cbfe7d0ff3e6d2f67ba09efa194327aeb4627b23d476f7c4e7c612e94b153ade4a18840fdb470ac75f6f693170163333e3d26ebeac9bf9cc88a2198c88ac7b47f5e22eb64b5f50f4a6495ab0cbd07997ebd95bf628df171a1d50a6d0cd5da787d17e754ca79dc36813f5b34be21b30982d627e4d473bfbb9d808bfec235ede3737715c0b933bb8de201103dc5521a6957595d17b29d454fbb7cd2ea609cdb6c49c515c9af07dbbb367f5990d7023ce26a3fbbcb3f52d237c27d37d7b583cad36c02aa8fc856baed9d840c039ae308d5342170fb0011acab086e9c11f957342b629fbc44ff2fab4ef2c469af8411b4a13fddb2805948e5b8e5669ddf3bc0085d159278af154add55136a7317bff160623af6926a8ff1425ecfcc4ed1cb179ba2b86e17a399989f0dac3b4a26e8526ae68fea6821d6d42eeb79f395ddfbea95ded5e6189841afaefcc84dcd37909efe587eb88c9cb8aea0c6a0dd2a4f9fdf5019e5f1b63baed7d92c3cde044c0a98462544c5905cb858529842372babe856512570f48b2cad574467657b74769c0767fb013941de0c5cc76e7686b1709848cbd9d3fd7eb77b1627680d9ff662a543b31b3ed1d658672cd49571302c0703bc6f495c6763ddf6115adf8dac268d4732f64fa2fb1dca353c5c72a3064d2a76a53a3ebb79defadcdefc1d9dbafad4a59593c723aa5c8e6cbb1997219b1dfda19cfa7e079d2082bc777e8006a7e189de916945765e3f566bd82e2c9dff6e63f760e1037254abb9e9c7307beb3992edd0dcbdacdcfe458f1b29899f2e3bf1609e3e7df55672a425d72f8ef94876f20d1ed3178fee4639b06b6e63e6babfdb462ddc22735782372759c7d0a0b801c97145d7c040e79c60b2beb58abcb6558576551f6f2d0d0156ce536771862ef1326c982558b393a2d4c94b19803d7b4d6a01ac02bb2bbb9d38aafcf74a36fbd6970b0b4c0e9f0e0a4a3a7639c0ea2b49dc27f220a725f359e640466e8569b77a234445abf95332f2131f5d9e3eb9d9b2f1d8bf4ef5fb76abd5fa5eb0c879fa0e7b90ff786b8379d48e074486df11cac623f02813c14a4050ef7fb8c106fbc9e3539ed1d3d7b7e6d2d2333333b2fc1bfd25afdd36be23b377affa8d73578dae9d3ca67ad55c515dc146d2fca495b9b1de5d831b77ccc58e18a99b1eb1105352ba2d76eaec1dad3b276cfe73f0abd07768ce609738ade9090defc067e4f777e4d1183c39b6eb3855a075a276c7cd639edb19dc1cb7bf324abd7974d4e213599b94784d189ce6c8c78bd7592ac3f845b88e12f2772eaaa07f4dbbfea335b6d57426be77d401bf265adf60b1e7f05f203ed11aa89e11fb9344ebc6c8ff85446b6fee190e8cca586bc44c60b8e25d219d19f0fafd3ffe668b89d6ca442a140aa367344ebb08a72b3ac10e2f3fd56cdfa91016f539939973f8d4798704c301b35dd72854046e2d0fd714cdaace2dec0c3c7db03b21e86b4ad5c283155adcaef1958bffc713ad958963ed00d779d1ce10a69510e975a5d4c7af4e9ec8f3789f7904eb7ce2169fdc8304b9caf16202f3fb4f4fb426449f0ac8b93aab359645b62fcc56aa92871ac36dc05751596e674ec3b87d91ef3feb2f097f9e1aaa4da6437fbd3615f960963fade3fd83aff36f44f8d253aef4963937bb109b686d1c8708537f20ca538f41f71565a7683204ec955367921a7b27605990df8cc952a6be7d1bbd5612c0386fced0bfe4d9db15539d8b501a63e0126a11125a9b306027e8ae5a02b1687b4a49c0fe38ffd302b0e28f0cedcfbc77c459390433360931b52ddd115f68bd903dba6c48218fe25ce5399859c8aa0fb16da5ffc7e773a171b39e5ee7f5cc4e1f75c2d8d20272a55be90fe19ea1d7c94a426654e34c9ce15eac8becb42ec41c7ace607e34f0173695f67727640108bff4d0f20991a61521a4cb8b894bee179530969690dc2765a02f2a69b85f5c7abfb4949e81a89491a188be9eb498bebe9184b49eb181be88a491a1be98be91a894b1b4e83e29236371312942fe4c792929113d513d7d036351a97d06927ac6fa460662a2e27a06d2d222e2fb2544f524f40c44f444c4f6198a1a89894b4b8889eae91948184aee37d63714dd6f2ca5ff7f271f4daba3ea4125211af9afca4713dce43a61f37c34f0d0645e2a2aaa8ee864b18d3f41e29d0283d0393344a7bcfd41762fc132d1f65cb393f662220d05b0f81af4ddfdc4695b04dd4f445ebde195c4457a5c507c7a7ed040eeb2c93983bc8ba300097925448a009170320806fcbdf00a42daeea62571e54d9f496c0abfceabbb39dce4215170734a92cde1959b560897bf3f664814fc2169f5a6f0c755ab84784a47ff29a6f1a2bb326697458c232fefa0a63b71e7b66f49fd5ed238fb57c18cef9a1c8befd4ecae29251b511bcc3fb090c783bcaafd6e75fa8ee37a96efbdf8d79c3ea7e93a52578c664119b9e75ca3ba8b2c8e31662d4197c85b758c0ea7f3afdda00e5909d94fa67b2e9726ef6969436fd79c9e88926ed35b78c9fa89e8de0e566642320a6e7528e2812c0fa758bec7eb93fb84c777b75e7d327780ed74e0d93bc2ab197aa2a887511d7d8898eb29cfe8f63da30e3166f5bfe6ab326a70505c7dacd3d195e2f4fc05d4b951b8eab06d39efed261ed52299ca02c4c75a8a53404c738af9d5a08bb24211219ed7eef1e5bb1976bf5e28cb4c584366f235b915517ff2d033b00b1b30c2404b90961329bdebc15ec09e3d69c47ef721af0248b18cdf0e4f11552f714a52626b6e69899690a92ca9f3884acaf2d97c65f8d5a43fff49dbb8efafd321ce50d99a38d8e2fc68ad66df1132740e13a9be003f6ffe24d836d151e4e6f337102476fef88535c0f3ce48b6d5e39e00f02f2d50f1ec69814422b54042db0c2a1cd13caea67a45e3ece9d3272f5c5155d03c7e4ee98aa69a8a92eaff88bd8dc7a1f728f6592eb41e82e89ec8d4d362d8685b3d2ee1a5a714b1da7034e13231f91d51f38657337c9206ef6ccc017f4327065a42b63b83a5175583ea987fc72db262c864cfae1f39f77f6f3b1044fa122022710f7d370f5044d8f1c380e0a72ba9e93e461197e5a30adad40884744412c08f872681efbf21ce6c4410e96bf9c3a13e4861ae54e3a2679574d88f06631d3375816b6c2bf03f1911b395fd2514f2d3913cee94abed349f94939951b177598399269750b52aa3bf4f7f3c91aa0e649e04b447c392eda923c4f11f08a0be98f3f4fa476bb13612a30bec5430f9bddaf927b848c7526866ca318f624582fe27ad1a427bc0eb6be01debaf6525c772fde971eebe80ec14b3cecd478c8825920710db48ef58ea5998e8591afdcdfea83b776f1b5918dfb2b8b1e7beb5cd8ff7b712808767fefda5ff3b0d6ffd0c139c3e21f9cab0759af9618d90db8e6a7315665f0391d69982aa94604ac63da85ce38c3bdd75a3881992797ae4781b43890a72b2405ac5cec3783b15e4d11c982dfcf84b0f60bf74da74f62615c9172ac11be6fc26cab1fbe6880dab2510ba47489dc03b07bfd5f0fd814302f73e085a073ca2c440adcee46e93474cbb01072bc900c011d2a41804d0984577b40333656b5dbe7d57b3eb665132405684c2867b41813526779dd3fdd3d2767feee5facda854c0557dcfa8179bd10e88b93ec0810481e6caaf8841046faac1420af47f5eb61c36b05ba3b4944fcf33024f7c6d3555ff8fadfafc53815284ec88a7fefe8a780959efe21dca429b93b750f22f1886a9bc08228ce1d4e9531b4a3a0a0b13ea393591bf81e13ebfeffe7d896cc6f9188da4498ed4d02f954dc806456e97ff9b7818585e89fd98a9a450fa28245da3e940e58f146f78eda8974fe0819ff964de63cd138af81a92b4dd956a33bd8c9beb1ae3b6a7462fdc570c6d3bb1a9f54f0a1228da07db439c7a00c7fc48f366c8efb00df9f827e18189cf9c2cbd8cb6082746c3adc07fe8fe126a7f6dff098998c11f50768448131254c4a8785e20720e78bceeda51c4ed9a439143fd134522674a69e8ae50c66d529f1af46172952c2306cf70ba841d04e677f84f190d38fba789eee0ecff76258bbfbcb7c63fc6c4fdb55df550afe90852a20762ad9fb8090f0aa55b70846e8a779225954d4747c67211b97cf4161cec7f0041a75dc4a58688f310000c5bc5c91ffb8f2218513779aca4f2619707146e99ca7de8f924f5d7bb130cc4e22252fee1c5379e6d0911bb158a885eb87becccd1501fbe97acd65bcaca2684abbf814b42e12044664f13927f8422f97ef6f1c6df5f9a8923552ce6aab9072e263cb89fa30cef3f076de864041d83d9ebb771f810e3de3436cbf3dc0aff20443faf4e7645144b65b063eb5e940af77afade2922bb4c08014bd0ee5f42d90a206e574ed5c332aadc37002125b55837feedc1779b9f6f11df1b5a0110648768ba42509ca3ea960a4e0e75aeda287473a72588930809f13faec77dddeb286a77cbd4ec592b7504ef710f724572eb23711790dc07e8544ec7f8a0748b2732699ffba55618bef08a38343a029ed14f9d73d7d53da97fe133cfdcbcca34c93bd899da45b39372d12c0a313b64bcc4196c99bad2a55fe9855ab33c5572ccb73f23ba7fa70e47f59db1333702bce648ee6846020b9faeab172d21076f98e5187e386b76a9e8a66d4b4615453c798c4fdca36adf210fcb342ea34acb2455769217c1f5b734aa63d8ce6ac0f3db9c4382f24517c7abf5490232d35b5e24208f3877c664639bd722eaeed34bc4d0999da0ece000d5ee91291e50b7302c116b537f8d5ee6ae2b28150a02eadf6b4add7c87e0b7536fceabe2b85a64aa5e57c25aa47e41185b7feba84dc3cd4e3fcfbef0c20752d559c78a3075ed5567b7a12673772cca7ae5413a5d96d7ab787452a9d5f6a66b5a244868153bad97442852a13cb84c142ecdb8da5a5644f34db23098baf4193cd6bea4708867b94bf55edb1d568f3626e393b264afef2b6c2b67abd10ce1dfd3fdcae441f017cb775add91a427137d781864c6e84ab59e434fccf8eca9a0666758e6871e5f00b0a73e3e60cfe72a80c6fceba118bf9c1ff0500ce0ad8b819620b15fc674b8e01d7aae00e2d61a5fe527ced347307271f13b99f0ebb183e9e0792858c551341df5ceea3bbcd2d53fba68239c0ef7e2e59608c95f53b7d6ba73a997f23375da0b973803a333de5b6558e92e4d4623b61e65bbfcf92a275ec68bf17a9d17fd1efef1ddb4b3278358a41aabce116389b3a62f85c7367db6553855a91bcf250b2fadcd732dd7e0a93d22303dcb992a41b1f4e42f4eed89c88bdcc2a342a3c6e4a3cac1a177a3d39cefcaf9aff1de083d7affc4a9d2d300cf4de39e8bc59f8b05bae29dacc342f83f08095c1c3f682dcad3207b639b93e0e7460eef8fd61f8fd3dc98427b74bfa42a3e64646f706f79f8c6dc14c32e0db621b5bbf616a51f2d1993f41e1b8529adc52ce8dc3a187a845e45a4445dc9644547e3e84e3a7682de96dbff728d18c64707e23d6f151ba8ebb18b24ee96c543f5d0d6a74fe24fdb93fee608fbe5ae13943f1e7d964a7825e21d5bcfecead982334ed1a41b4b673215767213af210cd4eccc9f32246980fefd5a0c8d815a9ce8442286e40107d28c57f20ea4985d26e86f144d8bfed70d2d9cc965aae0fb01bfb2afd7b4edcf6fe5ea9c79386fdc79f2aab9ef2891ee2f42b2d130f7201a45a4fc2432731adf7d09ded68fedd93b5c103ed0d1725f610d1badb50f0efcef36bcf5b95798c8fa0e7bfbdd06d061c7ebbadbd3c8524f11e71e5c82fcd0f64c03ba0e17d0d2e802c801d58373d88a25f8372e2436b7982144ea8630edc3724617095e1be3edd8edaa35be47b451349416713498d8f48ebb721de3dfc3935af7d5146acba3c896f506bca7da7b2e4e56e76e05ee9bf57ec7f117872e5d0fc873e632d5387b5919fe692bf3fb8323adb95e6fb332683776a55dd94ca1bb27fbb5998b88febfceafe316cc50ffbe9c929719acf2c12753419e589119c417dd034117181f332c1e5bde47f5b2a743f28af2d064fea4c1b8de8de7b3b98ed3c5ebaf855a9486903b60be34e5071adafbd4cb63e9cf753c0878fcf87a26cfe7d00f8bd54eb44a4785d782a21f1dac781bf0291858d52d3d75aabefc7d0f8c93576870c224bc5a744ca2fa562c66091c47db6f073005e0e01e693b46e2c42e80577780507a84f8baa8fd03f58643dcd763ad45fadc2f150979ba6fe544d98eed5abd8e496b8dd24fe5d17a72fbed74c13c2b9e9be24fdda11b6cc436398583adc9c88de1f46f96a24cf8b7b2fe7fb75f93360d185f5f5f5fffb9163e002c811acedb3f8685ba63007bf69a63ff8475fcdba5494dbcf56e1f9894e97200d9d851feeb6149190d1bcff237542e51d6a3edd94f7117505628722e8510395d3c37ec81e23b0776f259c44e9f51e7884ebfd44d76fe7ad8f7f0bd05dd550be490c4b3f59b19eb4f21852fba7bf1e08f288ac140ed11adf7133bae081808ef0a4ec5af5dc565e7ea617504ce3f3c653f3f6f262a1fe5f5351bcf48b3663ca87a577c643558d5a43a9c87c46854eafdf7f00906533786a95a0cb7a4f6307fc5f385e027d7b2be8707334d6bac8235efcc5dfa79a0486acc0183dc07dfc36b698e5f519df43c9d7a95ecb072e5f0f1d0b90cbc38c50b27464e274a9233c81972f29960ada6f9b1698a7816078f9f48460263a8ceb2391bbf932a92ba3b7502cfcc0f98ed8ea3f04b0cad70b977def4a1450b8f15def848cde8619a6ce6b73d94821af25a0d2e9a0f16f0e2606ddcb12c50deb8c1c45b05d9682f655b13d5bb784289c309475d86c61e207f014bb6497531949fb0c5eb7fbe46e95481e287e38efbc56a2795c3e386a89e6a7f0f6fb45310394a5dc89c6f743e55547dda7f46a00fcffd7a50389a15b9d348bcfdd385d77cd12421f2d517f09486e3881e9ee8057bcf4472b29ac6cffee240d873bcdbf71773cd63b6a5b10dcbab5a6e7efa5748141acee34509ccdb3659beb2dfffd8dbc5e2a29051cc7e0ed6437815076ec4db06d728875f2d9ba096cd39fc653e51651b5e9cebf193b44d05248f304c275eb1f50c9569659a0ee1d1b79d1f5456235be3f754fee467f92fc8f046ae64bce2852bcad496b954f7b39dd71e9c7fc71a6022cc5c89972051e15a2e20ed79f2d29bbd63ecfd7686376e44c4e0a5c93f1ba00b8cca21dd477f2e75bee6e9edc1b173b278c549bbf4530feacb4a48e4c546395eaaeb5724eb2cb0fa1ede6c2bf7c9a4ad5fe7d1ae2c9c918ad989cbeed7f1d84d42a36c227747a0ed75c776971827334596a11c3cfabb79e4f3d965519a609e91fb5a50884dd8c5fc0778c56d6662d3e79f94bcc4ee9c7ff22825524f6cefd2033c3b337ccf07725baeeababcace50bd71c9daaeab42fe13d97155e1d9ed79742eba475d00e261ce9b760bbdb0f0f7ee5625811f4da36829d1a5a0dd8cfcdd82dea85f71aadd4e7d74d03f58b4d06dccec722c2ecb3954d4b78bf879b9c1c25796075d446fcd83a8f722bb2de6034d5f47b386edfcd7eb91d8a3d874722071b06dafb753479f0e290c14e59e0cb6e8d4b293d56333e37aa3dc38783f1dedeba918d5eb2e2302d3efba6a1d27f37dd115cd481c77842daac87ef369baedecad0e3d6d9c98b2a34c3854578df2f92a5111aa291cababf1e3d4495060f4a4ba2c5533a539f46df4c18d277d43af80a37ffb1584491066f7d8b89af66b982bdbb129fec3ffac5273d957282fadcf77005b162a53abe3024822909e46c7d98deb574112f89f6f1e30e3fb4ff4da773be1f8dc499e68f289ec357220a47a72f1ba7358d3b30e497ed12c84738f604e33de2b6dbe375fbad66afb0a0a747cb7a871ab2f8aab0e5dfc3958fd1e7e7a13eca9f3b10b9c75c5855fa02b336def9969a690df75df87a350a3061121e6f248fb3bd17fe3d3cb029e19260d4c5b0733cd5625dc6a710bdf1105e7127ea2ae73cf36bd72493ca3318d102b7faea522af094b02693c911ed771f1af79be1469011b225b6f1dbf04cd52b71ca8c5d5294ed4577d23da9bd38f5458e06e25db14de89c38f5e024cbb9e8d6db8dd76c1ecd7ec89ac6abd1d1f33557eaf94947de156631c458954e1b87814733defc57ef7a3a72c60193c3d1b970c6de0296724f3cfef9d5e14db3e3ccfeca61aa3415951d8a2f8ff8d1e2f5cff2823780eaa3f3d77a855bead4508d633e2378863c68b60c9ca6b9e8997b32fdf20959d982b48fa5785aff03110d1abb4b7bcf2c1efaa0d9bfc26fc459780b2f3b26ea389af6104029918ae551156cab72c43cd1c61b3fae7eef4edd46d1c47d824f9161a9dea4abeb0cd8efe19e9f965bf3fb8d8b6f6b8f59efcac285efc51dc6fbfe9e936d322f02e9e760a4860b5731de9d5fe6253f7c0f57b9edb5d38ed33902e190b394d6eb2986e13e3ff83dbc4084c72387e641b4e6d783d5c52c54ed2ff524f1d4951b71cee2235acc62804ad09bf581dd687f47002f22e8a37f612ff7d154bfa7ae80f693ab26b26de21c789196cb27b74fa27d2c13cde8c5a286174ebcbf2f1980777ed6c9e9eeab585eb1d0e796ec7958167ae10eb61b4f9dfc6073f89c81781d87503cabd5b3019ae3fedb63f1f49bc35cfd0566d311cdb19992628542ab172bdc76e029d9b7d84f65b44870f859f3e9d30b2b8251c3970496be87374804bf39e00b9da28f2999e95ace443ae5b61fff1e3efb4104251b7baee1a0c68b6c05e9a2068e2af13bdfc32b0594dcc275f35534a88f20e4ac68246e9a299ec4e3bf9347ccbc22b565581f71044b6fa3fa6cde988177bd7ade95d907ddf3c45ee3f0bd89e74b6a0292cda678fa5fb3bd938fe76c62407634156f3eef09a6d33a8278c1de5ee263b77c2fd3b75796453025901d583f70b8044ffed46b898c6967a15eb374d4f1e475deb93690e181271f6dac95ce0da70d1b93d6a87d88243b0e19ed65e0c1f363aa701adb24addd31360b3b25f0d043bcdfe23a1e7f535a41ed551bed61ce3ad1e16aa3951020474a8b778d3073796ab552fc11491c79acf5171353a6cb6e2478f4f752d6a8a6b8fe616a3dbdd1e3f9879287de6449e35d4f3bbf6f39e2da86ade89ec87bef6991de9540a76f87772d520d09f049cd4763c42c10887722587dfe68bc6ba20aadeca7a3991c2e698cb7ed1a9887b8fb381bf18cfa9627a556cf4b25571c463f73c6993ca72f70e1c25bdf749063ea3a0f567becbd43c3f8b2c1b3af2462fedfc3c594ef7e489be6fc5a762f4e145606bcbbf15696fa7b78c6f111123a9d3aa3f10ef7e47314537d364c761ff1f873817fad0a003da3cdd4b689e51842374e19e1f16fc5cfe8e685b61b97dfa75718bc73f33c21d7b28e271faf7c281f576a5b912f5113bd9cb12a71a433ea261e7fc93def63ffd04800abff9e41494ffb7005dfdefd788fd51c8db61cf97a9e29e24202f090b1fcb170f8e154bc483a2ad6628f6a497e798afbbcbe1c968ae3cb9e7c7846ae13a2f2e8fdce107392e798992f0f694e7f3181f6fccbc76f77f307f8f8512bbdbf3cf4f96d44760f9ff83d6c0f37f6a42169de4f9e53d0c6d4a2ed691ea7ff968f00c7fe2b1d155e21e97fee2491de5202f57d77138a0584930324944418ea443e6e43e8a61d46c0c424f4bc29fef589b15f80fd9ecee2ccbc1b17ebb4745d604669b8ce3ffa2dfed836c5af7fdee4569055477f769d3465dfab465d5a4ef6ac3c87129eb68f9bef9fe802812109252c290886b10a5b1e7e6a6ff5505c26ccead05de5b7a5dabda4f46c39130c2461fb2c81fb82adef96196a8d0e60472203d5c969f70939cbb45cf65c29927cbbe8805930e7f8b937d91b118094644d5a65cfe6bb1bd2ce835124ceef4c7082427f4a3314f95b71c9fde5f34d68d003f6e362088517411535dbfd20c37db795bc09e5c301ac447ac44042f8c740ef9da174ff853bf2bfdf2539d01daf74c7b885f2360cea340eead4c69036b9d3f8b992d7540b8e8dc0bd51eb884a309ccc05b54cb60a412624c1c3aa8844f14e70915cc01535ca24988e789aa6199fa609c7bad37c09e191ed4f035d5195901d4831540b8d9739803929b066771a371c520125e186d2df3951e600b6b8d35c9e8332a13598c4a123d073e81d0691e189dc9e96097ecc00dfb9d37c1dcb013bdeb2f05d1bae85c26228735360c92b30b232d1d7e22190ddb745b20d36c396a790adee34b1631088fc64c3fd023c1032ac1a3aac5a87cb03eb7179e0cbd7bcb5d5825407760394c950032e0fac8b2175b38315c18221803e3c8612e3898cf04416f7c33333418f0cb02d145c19cb0123c886c9f8c97605d9c3db43c1b1e454b8bf2b393fb2722f900736e2f2c0b7b83cd0658ee1eda04ed3a0ce2a95638d3f349e9c0af74495ae4147413586560ca20997076207753a42c17271d00705b8a20413d23433104c89699ac0e7320770b2cc019c2a7300a7cb1c4066ca436f40371408d17987900272d2d4b487cb409f10d217ea0215e2b0723b981bca1d6560f6aa8eaead1e2aaca746cc76c0faa949aeb6c3df4061c3aae1c3aa11c3aa45757423b550f3a00ea23e86742239152e6f1a0f0a9c6eaf8746fb9163fdc8da6af2c52ad2edd1a03da4f8a5cc010c1a02bd87c09441b03314ec0a053bdfb2ec02dc70e0783f7281fc23984145e206832305b1b83cb019970776d443a95fa00057f2ba6a7298f708fccd3a940967de76231f6cc1e581eea1bc25fdf0cfc9a9f09415d82ae46e07c3ac41400cc89b02d993323025a569163af117d7d1450eabbe72e277c781ee2e20d432a83353e6007a4f41f1894cdc81aee4ee75fc545ea827763020187c8f41bc1bd421591a8356c772c0e63dbea877b83cb0159707267822eb3c917929b0fa74703a038cc582f343609bee48002890ba029be847baa1949f800b79502f697728182ad1aefb750af9b91f194926ef8eb270031b62486710b85030690c4a1d04b3c621f83ae8e40301eea848fa3271a1573eb0241d68650ae954e30f7d847ba0903c0ed0fef7df3e3ea8335be600a67d81269353e1c8deec1d996e3ca867699acc2c00c9fb411d105c846023e0301900d8774155109cd4cc15abb7485e0efa4c416bead054722a5c50447bd4a60e0395d4d1450dabda831f6b2104491b2e0f2ced87878c415d6f5996619fc020577246a00d83309884b25ff322e91e034b7a75d58273b23e23705fa3311b79aac618526f941cf83c4d53ac1d83200194dbbfed112e0f041686c0cc48c80fd55705ce96832efa1a0c8c8ce4f929b067535010c4e38ea2dedd07232917ef810ec680113194259ec8103bdad86592ea747039035c9d42d663a07638a2040cb6877ba04a183b717960172e0f94f108e54df744ce953980f3650ee0d05bb02005d652068e64807158d0770aaa1d0267db20771cd203350976e3f2c0b529a497113a0c5a1862f9540ba5831f2269da0675da07753a067528e040a5782b869a23e7352f2fd93310f4449d8495d6d161eae83a300825800f7c8000b4c801d22ae928fa352a525bdf1052ff61302b125a1b021f17538c8fc06b8b54466b211c2e0ff4a47808880af1221fddc9fb0477000d3beb217792b5b11c707d2c07fc18002e9117300300f5225cf60d58b906231bab857a7079e0075c1e882029a1205f05935da16812be40307f77610a0cd60fab107f5d47173dac5a017a86f24ef623835dc93f0cb030faa0fc51db99608f17aaa1b23abae434cd9434cdd434cd4e0ca2ab1e6a6a87a09c5e08f62900641b55cc81c5cd831e286e5801b89b74a1cc015c2c7300e1b363d01b71e71a7fc8a5c61f9a4e4e85a3c773c02c371e4d3f234f141ae4c2f4c3653e527ef98597c034ab4005a8bb1e9a494e85db8de7805e280f3b188d1dd88541f4e2f2c03e5c1ee8dbc2cebc54e600a22111571118cd1af8042c04ab2192e53207f0753f7c3639157e6cae0326f791817ebc16b24cfd553280a8ba21909a6cae0d9a4b4e850b7d004f83c80f10a44ed38fcb03077079e0d76544e7a00e1932b813f29c80bc60601af458d1758e81bc6b50a77b500737a80303ebab050f8edb00e575741575744799610838bc175a8f0733de20c81183b83c70e8db40650ee0db18d2b27e38d54c084fccb0aa2f8a2f76587507348ccb0347707920389f9c0a0f8de007c26001a8091be0990e084220044790b961f560ee583d0fac1e29322d4d336e58d51305323781dd18842756cf0babe78dd5f3c1eaf962f5fcb07afe583dcafa21106125e68562f287d85f401fbfcda0191680d50bc4ea0561f582b17a3d833a2158bddd5ea89c8f700a7ebe4fb83cb0120cc5ea8561f5c2b17a1158bd48ac9e37ca11fc2517144e4ab352e6002201a03810f2b483ad83c81df424e08710ae11300b5ee4c4ff7458350aab178dd58bc1eac562f5e2b07a349575742fd23433c0895a681497073ec5eac563f512b07a8958bd24acde33acde73ac1e9c329a7e0d92547684c2c164acde87419dde419d14ac5e2a56cf679d1c485b817985f2d65793fb85907ae0fe9f2007411082befd0f0ec260000c0261000904a3df267c16cc03ed41a47f08e973683400cca27b49971d040343a053c6b0e73aaf52606e99e0fa14f2c9780ee88a03fd5039413012108741b48281a840783a780086ab873c71a0287a1a19e24a9e1b049b18817fb6f142917ad4f1e33e73d17aa3f282604e502c48461e0efb7d2e70e89b0e08c240380a84e0701808437ea2f49940c4f9214080393f08160a79a358a0d532079094fa4d1d5d551d5d03069ab4f14135540b522482fc9f6ba186d7a47410fcdabf341304008b1f5675adf187dc6afc219216d0b211327a03fea2ce904300393b0807e0700042918f824d31a4308a57bed08b1518c9642d840611c209c3aa0bc9a9f0eeb72c004842eb269cbf332191e9f51ae404925d4dd60908f97f19bea4540034d58fec8316bf89755412828c8c6fec1be3fcbe6d738333143bf107a13e93700c20806f7d7ff9f38be504e324034000260b827d833a3044ffa0cec0a00ecfe0a00e368614ee81438e0580e92b306f54851d4c48ad07834076b88211b0a1419df7186a1f5418949ea69991a69999a69938acbad30b0786c17d50db113012fed74b24cbe44bc9a9705f54e61b44371c82a66a21e4af5bcf02001012494e8162e29160804348361a7aa69dd7114c70da9d7c7b590438799922119f90563b9063a02800402700f6c33652cc5336f053240090a2f382d47e3c076c645c1c62591a62e9461cbc16ea4a7e88aa7780a5ba8e6e959c3f0eecee431d852018fc5bf3252321a3e086e0cd31a4e5fd7087f11c70da66094e4d4e85eb43f5f4a13ef4a17afb507d7d28381c8190a083c177b3eea2e4602d220b73252f06c56aeae8b2d234a76ba1e56f0c2e014c1a56ad028b5260c0e99d2c5f6cb2f7d21f0b03196aebe8383f6010453eb051e45a9903b85ee6005689578bd788d78ad7897f4d4e85270bc2035014d4a4ad648121a4f5e22ca8b008feb6ccedc1a8ee3d3e28761a642f061114424adf203e1e00faa2c6db613336bde02082ab2586b451fcad7893784335792db2a71e4ad5019423613d9fb9a6fb91e9d550b31c5ba51d4cdb17e587621007049e0dab0e0fea8c0ceaf84e20a4f33fc11152c7699d41bf1088aeb19a9ca4040e18f6611000044130188c9c0d4902e3507762088767acc010b4c53e300ab634acde1e4a603b0cc804bfd4428c0ca49e38241d0ece94a6832e7700bff4235943114f41d28f833a254a644038c8574c06cb5c814d42177ae0cf875599c77179e0042e0f7c2617880aa67dc49f06bdd0f170010ba60458484250be289652b8515d1d5d3f06e1cb09822d7250da5b5835181c420a0c88d8953b804fca1d408b7b4942e27609e0fd591b3f941faa0f5c86bf27fb8ccb033f0dead881fe28612fe878bacedb6a727a70ce866ca6162af181a1e84d05bec2fd38ebebe826717960492114e64fed389e03cedb60e0025cb3b5d00bac9e7db903e850ee008647f003779787584a9cf8bd4379bd71e0e8a0ced8a0ceca3701130b1e59aa221d1fd4617c062ed844215ea6691afb73823b90a4196022c2e91bddbd262d160a4265a76936d4d135d6d1bdada39b4116a7c0b0e2e7594251ec0318840413854f28af821f6aa61f6991a1539c0eda4d239dc77340097f943f4ab712f40de59d18d47119cf014503508f1ff885f28684902678224b52608be9e09369e41ed7f11c702200f436ca5a81bd5c8121bcec60656bd0fca53776301160ae168a8ca10c2f2379554f5de50862ea76559d7e0edadbc12041c77207d0a9dc01b49f4642d645d6abc9a9f0b5e454f8a20d2c7958b5a98e2e6558351c1274866ac04c9d30fea27aea2fee8233ee82b3ee8273ee82f3ee820bee822448a0e0137cdf89d7f0f95ae813abc334b259bc457c3d3915fe390064cf49d37c010d5e2ff285682f02be23f0376bb07698e33432b07017db422d140e7a432d307eff505eff00e85d19f8228716243d00ccd092207d8640e9360c75b82b7914221805008318840bc4e9538a98f2279bf2279bf5a31d6ea1885f668829db1e51c58c7138f0798c2ea1963aea051ce37060ee2bf3cb7846c72589b030a8ba028a1da26eaaa2748907c7d0b08060c4f00425c6e14076306d4137ddab7ab6d54f8c6b9f76c578920cb750c4946d7f324e87eeac20c3381cc81b87329ac85ce241dc27d9297f32cc5beea8168ade24ba805aeaaa298ae09293188703c9af286b47a1297fb2e1168af05e382690742d9c6aba1996b44aed120f663491a5c4437171704cc1d9f8e7541fbfc2d01872742fc5700bc59b2fb7a62ac0c1457072f8fa73bf9de81c16cc5beec00cf2a558944b3cd89b4497d8b6afe29749fffa5fbb97c0943fd97224fdb70f8dee98fa4cd75e44dae74bfe6e048e599aa49ccf2175b6637e96842a0e8425fa413165db63cab68f26239c9eae816bfddf06e87a8dc8ac227309814566d22ebc61fe9c431d51ea0f86bfa62a72a444e7b0f44c53f44e534466d2a26348d1311498b7dc6fbedc7ad7f26d3231983a547711595c0b2bdad506f3967b3e42ad7a99a1d30e56940d4e77528d39b1e2ec60dfbee00d3539c17edd1f4c502694ecc0361fa1867138f0ffd162ad316de5d95df7619b6b5f1b3006874cc26e6698ee64a6ab69bbab48bb89a7c956d5465b75b51f4aa56a2a55955a55aab4e263a57e32c975720938d8c610e310e238d780cd633c3c623b78cc8d717c03816048887186870960dec4360f83214c75b88c1b656657d148838e9065fd1fe77fceeffc7ee758c729e97508e43883b5aea1aca78466103ea2d690746819652915cd202ca57aba2fbb332f0f70e466026de34ef16765ccb080b5aca0eb0ebc7c51ca522aed7d2ce8fc63dd2a66d10a6906a9fdfa3f36cda87120bbc9230d71e4d715b3628b5668d10af56528cd20b70cb8ee10282ca50a702465c3bb46652f26383cfe3a3752865586b2bb6ee48ef8c04daf867c16426cb4e20e2b590e8bd73c321e014dbd1277a4c0d15c303b9cdfba98b3348d5c5f47ef4effe4f0940032b9265d787d8a66902dbfc0c711661fe6744138323964878bd9e1e2780089eb215eea03e1a68dd4efe10dedf866140bde2fbc3cfea1b64f90f212f7d4229a418c260c8ea61984492bca473e6aaf10e83865ac4ab2bf7e5eedc2aeeee1daca9fcc36caea56315dafc01d29185d3955e314c766f07a4ea9e394231ef080d1903d5f939d43055e0d79a94576a3b1303af1f1f04ece3c97e76ccfa79aa4bb21341362c05ef0f3da895f3c1b2cb4f88b161aa19c284e191d536cdec3584f09a4cd0cb00c264b1f5c8278d2268c3661bbb749d641b2c3c5809fa8e428a3c3c531ed6f92db788d531c0f4082bf183ccebfad2f24867098614b5bbf221e4002b58a759782a55423a94f99875277a4c0362267878b33e1eb78244cdbf5889a59c46e0cc9584ad5a9c726c2c57d067c7bb417b1a4ceb066f2a94b16bc543ada72927700a0fba08dc2bc1ad268c2324fbc97fce5ee1d295f244f7c026b4439b794f5822178bcc4ab89c53dec7b8d6610db8a24c491dd3e62f31e1c68533f41f6eecc8a6906d958c2866a44b515ffa99efe6438245ab321337a314ba95e9a24cf0672afafa3f7af7ec0da4577ee4251051b4f42110490839bb27039c9a415a1f963fe2ad432f35f940df76a488bbf881e155f0909aaaa4edfdc1297b59558fc45eab69738c529cbbc92cb0b05daf52c084cb4907febd6bad2b00247f3f54533c8ce5e36ffa1f56a0154df989ca554cb3b59cb3b59d307d8135ad0a52bded388e032f6b1940fb57155febc0a63295555d569f58cb02698cfda45e6a0b46f26cbdb858438e08acd5af80f1695a82d591b919386716955d5e9f5b464c48803491cd22bcd208d0694d190ba5ec1234ec2473fda24e7cb200312dee07b4a057e980d68c7f27b2ca5eaabf9154ba912bacf80e0f8bc6432b8664318ff07d690f4fed62fa30cf076f8f6eff8f439274eb0946aac3d8f6690ae51d9f69759aca7246d134e7809af861c7d9833654279fe8957132e6b16bfeb8eff9f3af5d8ca575f224d316cca84f29c323e26e72f4bcda0c01e7dd5a25efd59da84393524bd8cb066b2753e079c3e0ca05d3465427baf295341659327cb1cfa67a8de351b944a30591a4b5ce88b42c6f46568d043b4d8f03906ce79da0a0456b524b61bde379a30b301dddc15b0769176f15f022ff31bda7100fc93899f863872691a3118fe9a1d2eaedc8654e8d7446e4f5e3c40d4bb67c5167f513c80f08ad8a52be689ae8523f77c3934834c9950f5cd75743d9e6f1cfd8bf23689699c643d2547ecd2e347f9ffb1c485c65578b19996770e4318af9bd11606ad67b0264f166b17f141e3d7d30ce2182c740c169aeb2e0693a5167f9131fce18d551154791d76a94546470bd76c88c55f64aebbc8522a7f15faac1f7bda8a8538727c0385bbf99d2ca5aae7940fa65ce0250f5e5f0f5ad9d18c8e2dfd15e4282d6f5c9553f6d3968393356e114b93fd7388c5029acebb4447c107a7555a11ca1e497dca1e42af577f36c4915e0df996886420d4e347f987389a0b2eadca5b22c2fdeba727bc046b17d18bd29d991cab157fd02883977e4b1d3d7eb4ffdbb7dfea28f5fb88da7dc9d23400247405cb802a5e4df4fb5175f7318bbf68acec0b34162b50771fab784858efcb4c9eacb957b9fcddbc55faa46183a89e537af5c7bf748a79de8f5713ee48813b5230ee0731f73781e075cf62cb61f1c64b0228c484594c588f1f1d1dc88907909916648e96670aca923af3bdcf7ecb780742a993dc402e4ba9346d0aab1577f5903ccf7fd7c6bf054a1f9bdfedc80169d7158746b1e9c1536d35d94613f6669566ecd2aa9c6f02d4dd026af3e7502ab409fbeac9acf8ad852ca572b6e76760f6670c4aa29e5376d42ade04251fab5bbb92d6cb7f8c4e7cdcd091f5d6b61d17668ee1f38993c028934b671b0de8f71e7fc95798f07ddca9c7c24cfef39a5f3c2c979a7d58f30dfcad65650d4da8a3f367354eb1ae57f06a0d5eff7a4af22e81ff6146d9245e1716193cf6d0470ccefc1dcd20d75d529ea862dadfec9b14fb2685452b74dc954c6d624d1e69ec31e6b92b05613ca4dbc58dbc60b2d4bc0fddaf452b0cbe42efcfc8584fc9c0a36c88bc06547db7516e7064436d6ebc168ed2726b439e3a0d87ea3825af66ac5dc40be78bd43ff00781c00e4e0a59bb68de96d7befba304c0e22f622955481b41fafde8c881ac721b01aabf071a32d4288b8e293270be5a217fba255f0d4b5fc40ac0417fd1ed473f1fe480fc414bb42ea8e77bb3987a5a6a4c91ef78ffca4d82273bd62ebad422f371c020c6143939f71d14bf7be1417f1f2de43f03a26b9de2ad67aa4ed3af9d56a9bb1f9da3e5a9b03879ab138d07109316a47dc447b857e4eaf2ff85e5d1e0e74f83ca917268ce291b6e499df9def27bdb02c8c1729ea31962b3984059bb68d28654efe16fb2d79fb7176558b845dcef47a1582995c993e5d590eb0bf88f07fc23ce8e160ea561008b252e543bc53ca70477b0f9fd5cd653b2e225606e5b5f91c352fb4f61245c90061efda56e4fe2dd91d754c80f9a8eab0f84b7760133ae0aa8d98c5d1b42463871b2398f3f34e525525ec8b0ddf0fe9bcb78cb74893fb62d3b047d7a628e96d3eba04a3b9bc2ef3ac3aff4dd144df9f071069bd18b0756d1ab7b386b171d799b3a939a2582c9d2ae1bb97c53e0d590218ebcf5ea13fdaec268d0a2dcc6cf2cfea2dda482d7cb917240dab243f0a7dc1a77677df7cb9dd00977a420982c9dd18be718cc9d40c719acdd8e641a3cdedefdf1ea04deb5a5002d4ba033cfc86f46726bd267cd7517670da2f60dc5417a16e607be2b8f7b7eebea7e2f3c5550d1a4e0af31c5e55dabe8e58eac2b06185aa297ffbb367c44f5c696f3cc93f3e3ee73d6e7e7e2ee73e6ba8bdce059cb16884f476bdeab34444ebd0119ee1838dfb35f83dc6cd1429f1e0d7e0e606f820965b257d05d96cdda45aca704a070178f0f9d8f252e004585a0e776752a4623b2816d913b527065082e4c8e9d1b7e7d6a9c399ce8f852350e64cfd1728bbf68724d0acdd9fe1d988eeb39e563d3293e53131cb95f796cc447ac741554c6f08c4a7aad27ea3965ed62f69daad31bf7cfcf4dfdbab38c044ad85945e7431f35448fbdba076c9e19d9ded15833e95e911b9de2da15597817316fd6fdbf3eceed20a987a869011b70295682bf6ab7234722eabd264d347cb4668316be8fcd77fb88a61876d4800490f8f30f8d29b26e4ac20bb59581d636e525f6ea28642f8d5fd5666ff74a6a9d62f5c3cf9ef5636e1fd1db7aace285923e6c84fbd3ffca52aa2b5598d58a5badf8639d0222e72f62f7f36766048f1d181f26470f19b9268cb6108dfbefb19e9231131a1d538cf8887bd7d0ab072777e6731af473e2b74a76cd86445ee498358a88b1e801a7ec33e047f246a95acba5b723bf0f71e4d755f07ca7157aafba29898ec11a9aa0b57a319cdfc9fd4def9c1cb6d84509dd67cd1bbdd004b6687eeb74618e1ef276e4f7fd26f49b79bcac064b85f0278fb3f8ca585f85b270f9b084eeb33719b9d94ddcf4c035d1898f75bd82d6bd235fcd7517dbe7e55e0da9730b5f6dfc215e0dfcb6ec10344e15f4f589747b9200478e99d06498986f393190806163bd3b7bd206420f4a67fce681006639b3581393a5b7e4fe26e9fa02ce4f7a4b5fe4744fc178bf7cfd34eb29311bd010476e6affa0eb15e8cb509652c50347ada43a2a69f4c0917ccfc752aaa569a461308fa79f60b274f4b0f574f400af4073a015c62c87bf05b0e26bf7f3369cff164c965e9904c5a9718aab9d62aba5f04995241554da9b01effa32d4de775c17ff809f2a0782c4780205f703b539fa32998e535e7162ced051b8eceebc6498786980c1ed75455a60aebb4833c8ccf6896b0998856e2d4ae3b794d4c0bf7fb181b24d73624bea0c133b3ef280685c91bd6583e95c660cfa887a4ea9ab10a5d8bf376ce7d5a4cf8667440f9fc3f858e939671df89f58e2c25761b2bff193c53dcc357f1cea6c8e96d7a6707ea7452b5c9a46863d79167f91262eb31bdef77144fbd37f9c48a08b0954d72be07fdb53771fa34dbfab4848bc5d7065cd02f13a79344ab4a53e5df5bef728ad600fb5c9a21536ba6010a25ee5b85790bffde607fefd5f000000ffff512c4122 diff --git a/kona/crates/protocol/derive/testdata/raw_batcher_tx.hex b/kona/crates/protocol/derive/testdata/raw_batcher_tx.hex new file mode 100644 index 0000000000000..0c036ea529b85 Binary files /dev/null and b/kona/crates/protocol/derive/testdata/raw_batcher_tx.hex differ diff --git a/kona/crates/protocol/genesis/Cargo.toml b/kona/crates/protocol/genesis/Cargo.toml new file mode 100644 index 0000000000000..c41f2e7db4e68 --- /dev/null +++ b/kona/crates/protocol/genesis/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "kona-genesis" +version = "0.4.5" +description = "Optimism genesis types" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Alloy +alloy-sol-types.workspace = true +alloy-primitives.workspace = true +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-hardforks.workspace = true +alloy-op-hardforks.workspace = true +alloy-chains.workspace = true +alloy-genesis.workspace = true + +# Misc +thiserror.workspace = true +derive_more = { workspace = true, features = ["from", "try_from"] } + +# `revm` feature +op-revm = { workspace = true, optional = true } + +# `arbitrary` feature +arbitrary = { workspace = true, features = ["derive"], optional = true } + +# `serde` feature +serde = { workspace = true, optional = true } +serde_repr = { workspace = true, optional = true } + +# `tabled` feature +tabled = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +toml = { workspace = true, features = ["parse", "serde"] } +rand = { workspace = true, features = ["thread_rng"] } +serde_json.workspace = true +arbitrary = { workspace = true, features = ["derive"] } +alloy-primitives = { workspace = true, features = ["rand", "arbitrary"] } + +[features] +default = [] +revm = [ "dep:op-revm" ] +tabled = [ "dep:tabled", "std" ] +std = [ + "alloy-chains/std", + "alloy-consensus/std", + "alloy-eips/std", + "alloy-genesis/std", + "alloy-primitives/std", + "alloy-sol-types/std", + "derive_more/std", + "op-revm?/std", + "serde?/std", + "thiserror/std", +] +arbitrary = [ + "alloy-chains/arbitrary", + "alloy-consensus/arbitrary", + "alloy-eips/arbitrary", + "alloy-primitives/arbitrary", + "alloy-primitives/rand", + "alloy-sol-types/arbitrary", + "dep:arbitrary", + "std", +] +serde = [ + "alloy-chains/serde", + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-hardforks/serde", + "alloy-op-hardforks/serde", + "alloy-primitives/serde", + "dep:serde", + "dep:serde_repr", + "op-revm?/serde", +] diff --git a/kona/crates/protocol/genesis/README.md b/kona/crates/protocol/genesis/README.md new file mode 100644 index 0000000000000..82f84e83f8a1b --- /dev/null +++ b/kona/crates/protocol/genesis/README.md @@ -0,0 +1,30 @@ +## `kona-genesis` + +CI +kona-genesis crate +MIT License +Docs + + +Genesis types for Optimism. + +### Usage + +_By default, `kona-genesis` enables both `std` and `serde` features._ + +If you're working in a `no_std` environment (like [`kona`][kona]), disable default features like so. + +```toml +[dependencies] +kona-genesis = { version = "x.y.z", default-features = false, features = ["serde"] } +``` + +#### Rollup Config + +`kona-genesis` exports a `RollupConfig`, the primary genesis type for Optimism Consensus. + + + + +[alloy-genesis]: https://github.com/alloy-rs +[kona]: https://github.com/op-rs/kona/blob/main/Cargo.toml#L137 diff --git a/kona/crates/protocol/genesis/src/chain/addresses.rs b/kona/crates/protocol/genesis/src/chain/addresses.rs new file mode 100644 index 0000000000000..74d3ab7900522 --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/addresses.rs @@ -0,0 +1,218 @@ +//! Address Types + +use alloy_primitives::Address; + +/// The set of network-specific contracts for a given chain. +/// +/// See: +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))] +pub struct AddressList { + /// The address manager + pub address_manager: Option

, + /// L1 Cross Domain Messenger proxy address + pub l1_cross_domain_messenger_proxy: Option
, + /// L1 ERC721 Bridge proxy address + #[cfg_attr(feature = "serde", serde(alias = "L1ERC721BridgeProxy"))] + pub l1_erc721_bridge_proxy: Option
, + /// L1 Standard Bridge proxy address + pub l1_standard_bridge_proxy: Option
, + /// L2 Output Oracle Proxy address + pub l2_output_oracle_proxy: Option
, + /// Optimism Mintable ERC20 Factory Proxy address + #[cfg_attr(feature = "serde", serde(alias = "OptimismMintableERC20FactoryProxy"))] + pub optimism_mintable_erc20_factory_proxy: Option
, + /// Optimism Portal Proxy address + pub optimism_portal_proxy: Option
, + /// System Config Proxy address + pub system_config_proxy: Option
, + /// Proxy Admin address + pub proxy_admin: Option
, + /// The superchain config address + pub superchain_config: Option
, + + // Fault Proof Contract Addresses + /// Anchor State Registry Proxy address + pub anchor_state_registry_proxy: Option
, + /// Delayed WETH Proxy address + #[cfg_attr(feature = "serde", serde(alias = "DelayedWETHProxy"))] + pub delayed_weth_proxy: Option
, + /// Dispute Game Factory Proxy address + pub dispute_game_factory_proxy: Option
, + /// Fault Dispute Game Proxy address + pub fault_dispute_game: Option
, + /// MIPS Proxy address + #[cfg_attr(feature = "serde", serde(alias = "MIPS"))] + pub mips: Option
, + /// Permissioned Dispute Game Proxy address + pub permissioned_dispute_game: Option
, + /// Preimage Oracle Proxy address + pub preimage_oracle: Option
, + /// The data availability challenge contract address + #[cfg_attr(feature = "serde", serde(alias = "DAChallengeAddress"))] + pub data_availability_challenge: Option
, +} + +impl AddressList { + /// Sets zeroed addresses to [`Option::None`]. + pub fn zero_proof_addresses(&mut self) { + if self.anchor_state_registry_proxy == Some(Address::ZERO) { + self.anchor_state_registry_proxy = None; + } + if self.delayed_weth_proxy == Some(Address::ZERO) { + self.delayed_weth_proxy = None; + } + if self.dispute_game_factory_proxy == Some(Address::ZERO) { + self.dispute_game_factory_proxy = None; + } + if self.fault_dispute_game == Some(Address::ZERO) { + self.fault_dispute_game = None; + } + if self.mips == Some(Address::ZERO) { + self.mips = None; + } + if self.permissioned_dispute_game == Some(Address::ZERO) { + self.permissioned_dispute_game = None; + } + if self.preimage_oracle == Some(Address::ZERO) { + self.preimage_oracle = None; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + + #[test] + fn zero_proof_addresses() { + let mut addresses = AddressList { + anchor_state_registry_proxy: Some(Address::ZERO), + delayed_weth_proxy: Some(Address::ZERO), + dispute_game_factory_proxy: Some(Address::ZERO), + fault_dispute_game: Some(Address::ZERO), + mips: Some(Address::ZERO), + permissioned_dispute_game: Some(Address::ZERO), + preimage_oracle: Some(Address::ZERO), + ..Default::default() + }; + + addresses.zero_proof_addresses(); + + assert_eq!(addresses.anchor_state_registry_proxy, None); + assert_eq!(addresses.delayed_weth_proxy, None); + assert_eq!(addresses.dispute_game_factory_proxy, None); + assert_eq!(addresses.fault_dispute_game, None); + assert_eq!(addresses.mips, None); + assert_eq!(addresses.permissioned_dispute_game, None); + assert_eq!(addresses.preimage_oracle, None); + } + + #[test] + fn test_addresses_deserialize() { + let raw: &str = r#" + { + "AddressManager": "0x8efb6b5c4767b09dc9aa6af4eaa89f749522bae2", + "L1CrossDomainMessengerProxy": "0x866e82a600a1414e583f7f13623f1ac5d58b0afa", + "L1Erc721BridgeProxy": "0x608d94945a64503e642e6370ec598e519a2c1e53", + "L1StandardBridgeProxy": "0x3154cf16ccdb4c6d922629664174b904d80f2c35", + "L2OutputOracleProxy": "0x56315b90c40730925ec5485cf004d835058518a0", + "OptimismMintableErc20FactoryProxy": "0x05cc379ebd9b30bba19c6fa282ab29218ec61d84", + "OptimismPortalProxy": "0x49048044d57e1c92a77f79988d21fa8faf74e97e", + "SystemConfigProxy": "0x73a79fab69143498ed3712e519a88a918e1f4072", + "ProxyAdmin": "0x0475cbcaebd9ce8afa5025828d5b98dfb67e059e", + "AnchorStateRegistryProxy": "0xdb9091e48b1c42992a1213e6916184f9ebdbfedf", + "DelayedWethProxy": "0xa2f2ac6f5af72e494a227d79db20473cf7a1ffe8", + "DisputeGameFactoryProxy": "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e", + "FaultDisputeGame": "0xcd3c0194db74c23807d4b90a5181e1b28cf7007c", + "Mips": "0x16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4", + "PermissionedDisputeGame": "0x19009debf8954b610f207d5925eede827805986e", + "PreimageOracle": "0x9c065e11870b891d214bc2da7ef1f9ddfa1be277" + } + "#; + + let addresses = AddressList { + address_manager: Some(address!("8EfB6B5c4767B09Dc9AA6Af4eAA89F749522BaE2")), + l1_cross_domain_messenger_proxy: Some(address!( + "866E82a600A1414e583f7F13623F1aC5d58b0Afa" + )), + l1_erc721_bridge_proxy: Some(address!("608d94945A64503E642E6370Ec598e519a2C1E53")), + l1_standard_bridge_proxy: Some(address!("3154Cf16ccdb4C6d922629664174b904d80F2C35")), + l2_output_oracle_proxy: Some(address!("56315b90c40730925ec5485cf004d835058518A0")), + optimism_mintable_erc20_factory_proxy: Some(address!( + "05cc379EBD9B30BbA19C6fA282AB29218EC61D84" + )), + optimism_portal_proxy: Some(address!("49048044D57e1C92A77f79988d21Fa8fAF74E97e")), + system_config_proxy: Some(address!("73a79Fab69143498Ed3712e519A88a918e1f4072")), + proxy_admin: Some(address!("0475cBCAebd9CE8AfA5025828d5b98DFb67E059E")), + superchain_config: None, + anchor_state_registry_proxy: Some(address!("db9091e48b1c42992a1213e6916184f9ebdbfedf")), + delayed_weth_proxy: Some(address!("a2f2ac6f5af72e494a227d79db20473cf7a1ffe8")), + dispute_game_factory_proxy: Some(address!("43edb88c4b80fdd2adff2412a7bebf9df42cb40e")), + fault_dispute_game: Some(address!("cd3c0194db74c23807d4b90a5181e1b28cf7007c")), + mips: Some(address!("16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4")), + permissioned_dispute_game: Some(address!("19009debf8954b610f207d5925eede827805986e")), + preimage_oracle: Some(address!("9c065e11870b891d214bc2da7ef1f9ddfa1be277")), + data_availability_challenge: None, + }; + + let deserialized: AddressList = serde_json::from_str(raw).unwrap(); + assert_eq!(addresses, deserialized); + } + + #[test] + fn test_addresses_unknown_field_json() { + let raw: &str = r#" + { + "AddressManager": "0x8efb6b5c4767b09dc9aa6af4eaa89f749522bae2", + "L1CrossDomainMessengerProxy": "0x866e82a600a1414e583f7f13623f1ac5d58b0afa", + "L1Erc721BridgeProxy": "0x608d94945a64503e642e6370ec598e519a2c1e53", + "L1StandardBridgeProxy": "0x3154cf16ccdb4c6d922629664174b904d80f2c35", + "L2OutputOracleProxy": "0x56315b90c40730925ec5485cf004d835058518a0", + "OptimismMintableErc20FactoryProxy": "0x05cc379ebd9b30bba19c6fa282ab29218ec61d84", + "OptimismPortalProxy": "0x49048044d57e1c92a77f79988d21fa8faf74e97e", + "SystemConfigProxy": "0x73a79fab69143498ed3712e519a88a918e1f4072", + "ProxyAdmin": "0x0475cbcaebd9ce8afa5025828d5b98dfb67e059e", + "AnchorStateRegistryProxy": "0xdb9091e48b1c42992a1213e6916184f9ebdbfedf", + "DelayedWethProxy": "0xa2f2ac6f5af72e494a227d79db20473cf7a1ffe8", + "DisputeGameFactoryProxy": "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e", + "FaultDisputeGame": "0xcd3c0194db74c23807d4b90a5181e1b28cf7007c", + "Mips": "0x16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4", + "PermissionedDisputeGame": "0x19009debf8954b610f207d5925eede827805986e", + "PreimageOracle": "0x9c065e11870b891d214bc2da7ef1f9ddfa1be277", + "unknown_field": "unknown" + } + "#; + + let addresses = AddressList { + address_manager: Some(address!("8EfB6B5c4767B09Dc9AA6Af4eAA89F749522BaE2")), + l1_cross_domain_messenger_proxy: Some(address!( + "866E82a600A1414e583f7F13623F1aC5d58b0Afa" + )), + l1_erc721_bridge_proxy: Some(address!("608d94945A64503E642E6370Ec598e519a2C1E53")), + l1_standard_bridge_proxy: Some(address!("3154Cf16ccdb4C6d922629664174b904d80F2C35")), + l2_output_oracle_proxy: Some(address!("56315b90c40730925ec5485cf004d835058518A0")), + optimism_mintable_erc20_factory_proxy: Some(address!( + "05cc379EBD9B30BbA19C6fA282AB29218EC61D84" + )), + optimism_portal_proxy: Some(address!("49048044D57e1C92A77f79988d21Fa8fAF74E97e")), + system_config_proxy: Some(address!("73a79Fab69143498Ed3712e519A88a918e1f4072")), + proxy_admin: Some(address!("0475cBCAebd9CE8AfA5025828d5b98DFb67E059E")), + superchain_config: None, + anchor_state_registry_proxy: Some(address!("db9091e48b1c42992a1213e6916184f9ebdbfedf")), + delayed_weth_proxy: Some(address!("a2f2ac6f5af72e494a227d79db20473cf7a1ffe8")), + dispute_game_factory_proxy: Some(address!("43edb88c4b80fdd2adff2412a7bebf9df42cb40e")), + fault_dispute_game: Some(address!("cd3c0194db74c23807d4b90a5181e1b28cf7007c")), + mips: Some(address!("16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4")), + permissioned_dispute_game: Some(address!("19009debf8954b610f207d5925eede827805986e")), + preimage_oracle: Some(address!("9c065e11870b891d214bc2da7ef1f9ddfa1be277")), + data_availability_challenge: None, + }; + + let deserialized: AddressList = serde_json::from_str(raw).unwrap(); + assert_eq!(addresses, deserialized); + } +} diff --git a/kona/crates/protocol/genesis/src/chain/altda.rs b/kona/crates/protocol/genesis/src/chain/altda.rs new file mode 100644 index 0000000000000..42dfcd869b13e --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/altda.rs @@ -0,0 +1,69 @@ +//! Contains the AltDA config type. + +use alloc::string::String; +use alloy_primitives::Address; + +/// AltDA configuration. +/// +/// See: +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct AltDAConfig { + /// AltDA challenge address + #[cfg_attr(feature = "serde", serde(alias = "da_challenge_contract_address"))] + pub da_challenge_address: Option
, + /// AltDA challenge window time (in seconds) + pub da_challenge_window: Option, + /// AltDA resolution window time (in seconds) + pub da_resolve_window: Option, + /// AltDA commitment type + pub da_commitment_type: Option, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use alloc::string::ToString; + use alloy_primitives::address; + + #[test] + fn test_altda_deserialize_json() { + let raw: &str = r#" + { + "da_challenge_address": "0x12c6a7db25b20347ca6f5d47e56d5e8219871c6d", + "da_challenge_window": 1, + "da_resolve_window": 1, + "da_commitment_type": "KeccakCommitment" + } + "#; + + let altda = AltDAConfig { + da_challenge_address: Some(address!("12c6a7db25b20347ca6f5d47e56d5e8219871c6d")), + da_challenge_window: Some(1), + da_resolve_window: Some(1), + da_commitment_type: Some("KeccakCommitment".to_string()), + }; + + let deserialized: AltDAConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(altda, deserialized); + } + + #[test] + fn test_altda_unknown_fields_json() { + let raw: &str = r#" + { + "da_challenge_address": "0x12c6a7db25b20347ca6f5d47e56d5e8219871c6d", + "da_challenge_window": 1, + "da_resolve_window": 1, + "da_commitment_type": "KeccakCommitment", + "unknown_field": "unknown" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } +} diff --git a/kona/crates/protocol/genesis/src/chain/config.rs b/kona/crates/protocol/genesis/src/chain/config.rs new file mode 100644 index 0000000000000..a58a9dbaf1fe3 --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/config.rs @@ -0,0 +1,357 @@ +//! Contains the chain config type. + +use alloc::string::String; +use alloy_chains::Chain; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_primitives::Address; + +use crate::{ + AddressList, AltDAConfig, BaseFeeConfig, ChainGenesis, GRANITE_CHANNEL_TIMEOUT, HardForkConfig, + Roles, RollupConfig, SuperchainLevel, base_fee_params, base_fee_params_canyon, + params::base_fee_config, rollup::DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, +}; + +/// L1 chain configuration from the `alloy-genesis` crate. +pub type L1ChainConfig = alloy_genesis::ChainConfig; + +/// Defines core blockchain settings per block. +/// +/// Tailors unique settings for each network based on +/// its genesis block and superchain configuration. +/// +/// This struct bridges the interface between the [`ChainConfig`][ccr] +/// defined in the [`superchain-registry`][scr] and the [`ChainConfig`][ccg] +/// defined in [`op-geth`][opg]. +/// +/// [opg]: https://github.com/ethereum-optimism/op-geth +/// [scr]: https://github.com/ethereum-optimism/superchain-registry +/// [ccg]: https://github.com/ethereum-optimism/op-geth/blob/optimism/params/config.go#L342 +/// [ccr]: https://github.com/ethereum-optimism/superchain-registry/blob/main/ops/internal/config/superchain.go#L70 +#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ChainConfig { + /// Chain name (e.g. "Base") + #[cfg_attr(feature = "serde", serde(rename = "Name", alias = "name"))] + pub name: String, + /// L1 chain ID + #[cfg_attr(feature = "serde", serde(skip))] + pub l1_chain_id: u64, + /// Chain public RPC endpoint + #[cfg_attr(feature = "serde", serde(rename = "PublicRPC", alias = "public_rpc"))] + pub public_rpc: String, + /// Chain sequencer RPC endpoint + #[cfg_attr(feature = "serde", serde(rename = "SequencerRPC", alias = "sequencer_rpc"))] + pub sequencer_rpc: String, + /// Chain explorer HTTP endpoint + #[cfg_attr(feature = "serde", serde(rename = "Explorer", alias = "explorer"))] + pub explorer: String, + /// Level of integration with the superchain. + #[cfg_attr(feature = "serde", serde(rename = "SuperchainLevel", alias = "superchain_level"))] + pub superchain_level: SuperchainLevel, + /// Whether the chain is governed by optimism. + #[cfg_attr( + feature = "serde", + serde(rename = "GovernedByOptimism", alias = "governed_by_optimism") + )] + #[cfg_attr(feature = "serde", serde(default))] + pub governed_by_optimism: bool, + /// Time of when a given chain is opted in to the Superchain. + /// If set, hardforks times after the superchain time + /// will be inherited from the superchain-wide config. + #[cfg_attr(feature = "serde", serde(rename = "SuperchainTime", alias = "superchain_time"))] + pub superchain_time: Option, + /// Data availability type. + #[cfg_attr( + feature = "serde", + serde(rename = "DataAvailabilityType", alias = "data_availability_type") + )] + pub data_availability_type: String, + /// Chain ID + #[cfg_attr(feature = "serde", serde(rename = "l2_chain_id", alias = "chain_id"))] + pub chain_id: u64, + /// Chain-specific batch inbox address + #[cfg_attr( + feature = "serde", + serde(rename = "batch_inbox_address", alias = "batch_inbox_addr") + )] + #[cfg_attr(feature = "serde", serde(default))] + pub batch_inbox_addr: Address, + /// The block time in seconds. + #[cfg_attr(feature = "serde", serde(rename = "block_time"))] + pub block_time: u64, + /// The sequencer window size in seconds. + #[cfg_attr(feature = "serde", serde(rename = "seq_window_size"))] + pub seq_window_size: u64, + /// The maximum sequencer drift in seconds. + #[cfg_attr(feature = "serde", serde(rename = "max_sequencer_drift"))] + pub max_sequencer_drift: u64, + /// Gas paying token metadata. Not consumed by downstream OPStack components. + #[cfg_attr(feature = "serde", serde(rename = "GasPayingToken", alias = "gas_paying_token"))] + pub gas_paying_token: Option
, + /// Hardfork Config. These values may override the superchain-wide defaults. + #[cfg_attr(feature = "serde", serde(rename = "hardfork_configuration", alias = "hardforks"))] + pub hardfork_config: HardForkConfig, + /// Optimism configuration + #[cfg_attr(feature = "serde", serde(rename = "optimism"))] + pub optimism: Option, + /// Alternative DA configuration + #[cfg_attr(feature = "serde", serde(rename = "alt_da"))] + pub alt_da: Option, + /// Chain-specific genesis information + pub genesis: ChainGenesis, + /// Roles + #[cfg_attr(feature = "serde", serde(rename = "Roles", alias = "roles"))] + pub roles: Option, + /// Addresses + #[cfg_attr(feature = "serde", serde(rename = "Addresses", alias = "addresses"))] + pub addresses: Option, +} + +impl ChainConfig { + /// Returns the base fee params for the chain. + pub fn base_fee_params(&self) -> BaseFeeParams { + self.optimism + .as_ref() + .map(|op| op.as_base_fee_params()) + .unwrap_or_else(|| base_fee_params(self.chain_id)) + } + + /// Returns the canyon base fee params for the chain. + pub fn canyon_base_fee_params(&self) -> BaseFeeParams { + self.optimism + .as_ref() + .map(|op| op.as_canyon_base_fee_params()) + .unwrap_or_else(|| base_fee_params_canyon(self.chain_id)) + } + + /// Returns the base fee config for the chain. + pub fn base_fee_config(&self) -> BaseFeeConfig { + self.optimism.as_ref().map(|op| *op).unwrap_or_else(|| base_fee_config(self.chain_id)) + } + + /// Loads the rollup config for the OP-Stack chain given the chain config and address list. + #[deprecated(since = "0.2.1", note = "please use `as_rollup_config` instead")] + pub fn load_op_stack_rollup_config(&self) -> RollupConfig { + self.as_rollup_config() + } + + /// Loads the rollup config for the OP-Stack chain given the chain config and address list. + pub fn as_rollup_config(&self) -> RollupConfig { + RollupConfig { + genesis: self.genesis, + l1_chain_id: self.l1_chain_id, + l2_chain_id: Chain::from(self.chain_id), + block_time: self.block_time, + seq_window_size: self.seq_window_size, + max_sequencer_drift: self.max_sequencer_drift, + hardforks: self.hardfork_config, + batch_inbox_address: self.batch_inbox_addr, + deposit_contract_address: self + .addresses + .as_ref() + .and_then(|a| a.optimism_portal_proxy) + .unwrap_or_default(), + l1_system_config_address: self + .addresses + .as_ref() + .and_then(|a| a.system_config_proxy) + .unwrap_or_default(), + protocol_versions_address: self + .addresses + .as_ref() + .and_then(|a| a.address_manager) + .unwrap_or_default(), + superchain_config_address: None, + blobs_enabled_l1_timestamp: None, + da_challenge_address: self + .alt_da + .as_ref() + .and_then(|alt_da| alt_da.da_challenge_address), + + // The below chain parameters can be different per OP-Stack chain, + // but since none of the superchain chains differ, it's not represented in the + // superchain-registry yet. This restriction on superchain-chains may change in the + // future. Test/Alt configurations can still load custom rollup-configs when + // necessary. + channel_timeout: 300, + granite_channel_timeout: GRANITE_CHANNEL_TIMEOUT, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + chain_op_config: self.base_fee_config(), + alt_da_config: self.alt_da.clone(), + } + } +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + + #[test] + fn test_chain_config_json() { + let raw: &str = r#" + { + "Name": "Base", + "PublicRPC": "https://mainnet.base.org", + "SequencerRPC": "https://mainnet-sequencer.base.org", + "Explorer": "https://explorer.base.org", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 8453, + "batch_inbox_address": "0xff00000000000000000000000000000000008453", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17481768, + "hash": "0x5c13d307623a926cd31415036c8b7fa14572f9dac64528e857a470511fc30771" + }, + "l2": { + "number": 0, + "hash": "0xf712aa9241cc24369b143cf6dce85f0902a9731e70d66818a3a5845b296c73dd" + }, + "l2_time": 1686789347, + "system_config": { + "batcherAddress": "0x5050f69a9786f081509234f1a7f4684b5e5b76c9", + "overhead": "0xbc", + "scalar": "0xa6fe0", + "gasLimit": 30000000 + } + }, + "Roles": { + "SystemConfigOwner": "0x14536667cd30e52c0b458baaccb9fada7046e056", + "ProxyAdminOwner": "0x7bb41c3008b3f03fe483b28b8db90e19cf07595c", + "Guardian": "0x09f7150d8c019bef34450d6920f6b3608cefdaf2", + "Challenger": "0x6f8c5ba3f59ea3e76300e3becdc231d656017824", + "Proposer": "0x642229f238fb9de03374be34b0ed8d9de80752c5", + "UnsafeBlockSigner": "0xaf6e19be0f9ce7f8afd49a1824851023a8249e8a", + "BatchSubmitter": "0x5050f69a9786f081509234f1a7f4684b5e5b76c9" + }, + "Addresses": { + "AddressManager": "0x8efb6b5c4767b09dc9aa6af4eaa89f749522bae2", + "L1CrossDomainMessengerProxy": "0x866e82a600a1414e583f7f13623f1ac5d58b0afa", + "L1Erc721BridgeProxy": "0x608d94945a64503e642e6370ec598e519a2c1e53", + "L1StandardBridgeProxy": "0x3154cf16ccdb4c6d922629664174b904d80f2c35", + "L2OutputOracleProxy": "0x56315b90c40730925ec5485cf004d835058518a0", + "OptimismMintableErc20FactoryProxy": "0x05cc379ebd9b30bba19c6fa282ab29218ec61d84", + "OptimismPortalProxy": "0x49048044d57e1c92a77f79988d21fa8faf74e97e", + "SystemConfigProxy": "0x73a79fab69143498ed3712e519a88a918e1f4072", + "ProxyAdmin": "0x0475cbcaebd9ce8afa5025828d5b98dfb67e059e", + "AnchorStateRegistryProxy": "0xdb9091e48b1c42992a1213e6916184f9ebdbfedf", + "DelayedWethProxy": "0xa2f2ac6f5af72e494a227d79db20473cf7a1ffe8", + "DisputeGameFactoryProxy": "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e", + "FaultDisputeGame": "0xcd3c0194db74c23807d4b90a5181e1b28cf7007c", + "Mips": "0x16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4", + "PermissionedDisputeGame": "0x19009debf8954b610f207d5925eede827805986e", + "PreimageOracle": "0x9c065e11870b891d214bc2da7ef1f9ddfa1be277" + } + } + "#; + + let deserialized: ChainConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(deserialized.name, "Base"); + } + + #[test] + fn test_chain_config_unknown_field_json() { + let raw: &str = r#" + { + "Name": "Base", + "PublicRPC": "https://mainnet.base.org", + "SequencerRPC": "https://mainnet-sequencer.base.org", + "Explorer": "https://explorer.base.org", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 8453, + "batch_inbox_address": "0xff00000000000000000000000000000000008453", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601 + }, + "optimism": { + "eip1559Elasticity": "0x6", + "eip1559Denominator": "0x32", + "eip1559DenominatorCanyon": "0xfa" + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17481768, + "hash": "0x5c13d307623a926cd31415036c8b7fa14572f9dac64528e857a470511fc30771" + }, + "l2": { + "number": 0, + "hash": "0xf712aa9241cc24369b143cf6dce85f0902a9731e70d66818a3a5845b296c73dd" + }, + "l2_time": 1686789347, + "system_config": { + "batcherAddress": "0x5050f69a9786f081509234f1a7f4684b5e5b76c9", + "overhead": "0xbc", + "scalar": "0xa6fe0", + "gasLimit": 30000000 + } + }, + "Roles": { + "SystemConfigOwner": "0x14536667cd30e52c0b458baaccb9fada7046e056", + "ProxyAdminOwner": "0x7bb41c3008b3f03fe483b28b8db90e19cf07595c", + "Guardian": "0x09f7150d8c019bef34450d6920f6b3608cefdaf2", + "Challenger": "0x6f8c5ba3f59ea3e76300e3becdc231d656017824", + "Proposer": "0x642229f238fb9de03374be34b0ed8d9de80752c5", + "UnsafeBlockSigner": "0xaf6e19be0f9ce7f8afd49a1824851023a8249e8a", + "BatchSubmitter": "0x5050f69a9786f081509234f1a7f4684b5e5b76c9" + }, + "Addresses": { + "AddressManager": "0x8efb6b5c4767b09dc9aa6af4eaa89f749522bae2", + "L1CrossDomainMessengerProxy": "0x866e82a600a1414e583f7f13623f1ac5d58b0afa", + "L1Erc721BridgeProxy": "0x608d94945a64503e642e6370ec598e519a2c1e53", + "L1StandardBridgeProxy": "0x3154cf16ccdb4c6d922629664174b904d80f2c35", + "L2OutputOracleProxy": "0x56315b90c40730925ec5485cf004d835058518a0", + "OptimismMintableErc20FactoryProxy": "0x05cc379ebd9b30bba19c6fa282ab29218ec61d84", + "OptimismPortalProxy": "0x49048044d57e1c92a77f79988d21fa8faf74e97e", + "SystemConfigProxy": "0x73a79fab69143498ed3712e519a88a918e1f4072", + "ProxyAdmin": "0x0475cbcaebd9ce8afa5025828d5b98dfb67e059e", + "AnchorStateRegistryProxy": "0xdb9091e48b1c42992a1213e6916184f9ebdbfedf", + "DelayedWethProxy": "0xa2f2ac6f5af72e494a227d79db20473cf7a1ffe8", + "DisputeGameFactoryProxy": "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e", + "FaultDisputeGame": "0xcd3c0194db74c23807d4b90a5181e1b28cf7007c", + "Mips": "0x16e83ce5ce29bf90ad9da06d2fe6a15d5f344ce4", + "PermissionedDisputeGame": "0x19009debf8954b610f207d5925eede827805986e", + "PreimageOracle": "0x9c065e11870b891d214bc2da7ef1f9ddfa1be277" + }, + "unknown_field": "unknown" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } +} diff --git a/kona/crates/protocol/genesis/src/chain/hardfork.rs b/kona/crates/protocol/genesis/src/chain/hardfork.rs new file mode 100644 index 0000000000000..b63de5eccce59 --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/hardfork.rs @@ -0,0 +1,239 @@ +//! Contains the hardfork configuration for the chain. + +use alloc::string::{String, ToString}; +use core::fmt::Display; + +/// Hardfork configuration. +/// +/// See: +#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct HardForkConfig { + /// `regolith_time` sets the activation time of the Regolith network-upgrade: + /// a pre-mainnet Bedrock change that addresses findings of the Sherlock contest related to + /// deposit attributes. "Regolith" is the loose deposited rock that sits on top of Bedrock. + /// Active if regolith_time != None && L2 block timestamp >= Some(regolith_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub regolith_time: Option, + /// `canyon_time` sets the activation time of the Canyon network upgrade. + /// Active if `canyon_time` != None && L2 block timestamp >= Some(canyon_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub canyon_time: Option, + /// `delta_time` sets the activation time of the Delta network upgrade. + /// Active if `delta_time` != None && L2 block timestamp >= Some(delta_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub delta_time: Option, + /// `ecotone_time` sets the activation time of the Ecotone network upgrade. + /// Active if `ecotone_time` != None && L2 block timestamp >= Some(ecotone_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub ecotone_time: Option, + /// `fjord_time` sets the activation time of the Fjord network upgrade. + /// Active if `fjord_time` != None && L2 block timestamp >= Some(fjord_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub fjord_time: Option, + /// `granite_time` sets the activation time for the Granite network upgrade. + /// Active if `granite_time` != None && L2 block timestamp >= Some(granite_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub granite_time: Option, + /// `holocene_time` sets the activation time for the Holocene network upgrade. + /// Active if `holocene_time` != None && L2 block timestamp >= Some(holocene_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub holocene_time: Option, + /// `pectra_blob_schedule_time` sets the activation time for the activation of the Pectra blob + /// fee schedule for the L1 block info transaction. This is an optional fork, only present + /// on OP Stack sepolia chains that observed the L1 Pectra network upgrade with `op-node` + /// <=v1.11.1 sequencing the network. + /// + /// Active if `pectra_blob_schedule_time` != None && L2 block timestamp >= + /// Some(pectra_blob_schedule_time), inactive otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub pectra_blob_schedule_time: Option, + /// `isthmus_time` sets the activation time for the Isthmus network upgrade. + /// Active if `isthmus_time` != None && L2 block timestamp >= Some(isthmus_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub isthmus_time: Option, + /// `jovian_time` sets the activation time for the Jovian network upgrade. + /// Active if `jovian_time` != None && L2 block timestamp >= Some(jovian_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub jovian_time: Option, + /// `interop_time` sets the activation time for the Interop network upgrade. + /// Active if `interop_time` != None && L2 block timestamp >= Some(interop_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub interop_time: Option, +} + +impl Display for HardForkConfig { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[inline(always)] + fn fmt_time(t: Option) -> String { + t.map(|t| t.to_string()).unwrap_or_else(|| "Not scheduled".to_string()) + } + + writeln!(f, "🍴 Scheduled Hardforks:")?; + for (name, time) in self.iter() { + writeln!(f, "-> {} Activation Time: {}", name, fmt_time(time))?; + } + Ok(()) + } +} + +impl HardForkConfig { + /// Returns an iterator of hardfork names -> their activation times (if scheduled.) + pub fn iter(&self) -> impl Iterator)> { + [ + ("Regolith", self.regolith_time), + ("Canyon", self.canyon_time), + ("Delta", self.delta_time), + ("Ecotone", self.ecotone_time), + ("Fjord", self.fjord_time), + ("Granite", self.granite_time), + ("Holocene", self.holocene_time), + ("Pectra Blob Schedule", self.pectra_blob_schedule_time), + ("Isthmus", self.isthmus_time), + ("Jovian", self.jovian_time), + ("Interop", self.interop_time), + ] + .into_iter() + } +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + + #[test] + fn test_hardforks_deserialize_json() { + let raw: &str = r#" + { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time":1732633200 + } + "#; + + let hardforks = HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }; + + let deserialized: HardForkConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(hardforks, deserialized); + } + + #[test] + fn test_hardforks_deserialize_new_field_fail_json() { + let raw: &str = r#" + { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "new_field": 0 + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + fn test_hardforks_deserialize_toml() { + let raw: &str = r#" + canyon_time = 1699981200 # Tue 14 Nov 2023 17:00:00 UTC + delta_time = 1703203200 # Fri 22 Dec 2023 00:00:00 UTC + ecotone_time = 1708534800 # Wed 21 Feb 2024 17:00:00 UTC + fjord_time = 1716998400 # Wed 29 May 2024 16:00:00 UTC + granite_time = 1723478400 # Mon Aug 12 16:00:00 UTC 2024 + holocene_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + "#; + + let hardforks = HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }; + + let deserialized: HardForkConfig = toml::from_str(raw).unwrap(); + assert_eq!(hardforks, deserialized); + } + + #[test] + fn test_hardforks_deserialize_new_field_fail_toml() { + let raw: &str = r#" + canyon_time = 1699981200 # Tue 14 Nov 2023 17:00:00 UTC + delta_time = 1703203200 # Fri 22 Dec 2023 00:00:00 UTC + ecotone_time = 1708534800 # Wed 21 Feb 2024 17:00:00 UTC + fjord_time = 1716998400 # Wed 29 May 2024 16:00:00 UTC + granite_time = 1723478400 # Mon Aug 12 16:00:00 UTC 2024 + holocene_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + new_field_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + "#; + toml::from_str::(raw).unwrap_err(); + } + + #[test] + fn test_hardforks_iter() { + let hardforks = HardForkConfig { + regolith_time: Some(1), + canyon_time: Some(2), + delta_time: Some(3), + ecotone_time: Some(4), + fjord_time: Some(5), + granite_time: Some(6), + holocene_time: Some(7), + pectra_blob_schedule_time: Some(8), + isthmus_time: Some(9), + jovian_time: Some(10), + interop_time: Some(11), + }; + + let mut iter = hardforks.iter(); + assert_eq!(iter.next(), Some(("Regolith", Some(1)))); + assert_eq!(iter.next(), Some(("Canyon", Some(2)))); + assert_eq!(iter.next(), Some(("Delta", Some(3)))); + assert_eq!(iter.next(), Some(("Ecotone", Some(4)))); + assert_eq!(iter.next(), Some(("Fjord", Some(5)))); + assert_eq!(iter.next(), Some(("Granite", Some(6)))); + assert_eq!(iter.next(), Some(("Holocene", Some(7)))); + assert_eq!(iter.next(), Some(("Pectra Blob Schedule", Some(8)))); + assert_eq!(iter.next(), Some(("Isthmus", Some(9)))); + assert_eq!(iter.next(), Some(("Jovian", Some(10)))); + assert_eq!(iter.next(), Some(("Interop", Some(11)))); + assert_eq!(iter.next(), None); + } +} diff --git a/kona/crates/protocol/genesis/src/chain/mod.rs b/kona/crates/protocol/genesis/src/chain/mod.rs new file mode 100644 index 0000000000000..decdd5e4ac63f --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/mod.rs @@ -0,0 +1,28 @@ +//! Module containing the chain config. + +/// OP Mainnet chain ID. +pub const OP_MAINNET_CHAIN_ID: u64 = 10; + +/// OP Sepolia chain ID. +pub const OP_SEPOLIA_CHAIN_ID: u64 = 11155420; + +/// Base Mainnet chain ID. +pub const BASE_MAINNET_CHAIN_ID: u64 = 8453; + +/// Base Sepolia chain ID. +pub const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; + +mod addresses; +pub use addresses::AddressList; + +mod config; +pub use config::{ChainConfig, L1ChainConfig}; + +mod altda; +pub use altda::AltDAConfig; + +mod hardfork; +pub use hardfork::HardForkConfig; + +mod roles; +pub use roles::Roles; diff --git a/kona/crates/protocol/genesis/src/chain/roles.rs b/kona/crates/protocol/genesis/src/chain/roles.rs new file mode 100644 index 0000000000000..529a20db1ac7f --- /dev/null +++ b/kona/crates/protocol/genesis/src/chain/roles.rs @@ -0,0 +1,82 @@ +//! OP Stack component roles. + +use alloy_primitives::Address; + +/// Roles in the OP Stack. +/// +/// See: +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Roles { + /// The system config owner + pub system_config_owner: Option
, + /// The owner of the Proxy Admin + pub proxy_admin_owner: Option
, + /// The guardian address + pub guardian: Option
, + /// The challenger's address + pub challenger: Option
, + /// The proposer's address + pub proposer: Option
, + /// Unsafe block signer. + pub unsafe_block_signer: Option
, + /// The batch submitter's address + pub batch_submitter: Option
, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + + #[test] + fn test_roles_serde() { + let json_roles: &str = r#"{ + "SystemConfigOwner": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "ProxyAdminOwner": "0x4377BB0F0103992b31eC12b4d796a8687B8dC8E9", + "Guardian": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "Challenger": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "Proposer": "0x95014c45078354Ff839f14192228108Eac82E00A", + "UnsafeBlockSigner": "0xa95B83e39AA78B00F12fe431865B563793D97AF5", + "BatchSubmitter": "0x19CC7073150D9f5888f09E0e9016d2a39667df14" + }"#; + + let expected: Roles = Roles { + system_config_owner: Some( + "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2".parse().unwrap(), + ), + proxy_admin_owner: Some("0x4377BB0F0103992b31eC12b4d796a8687B8dC8E9".parse().unwrap()), + guardian: Some("0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2".parse().unwrap()), + challenger: Some("0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2".parse().unwrap()), + proposer: Some("0x95014c45078354Ff839f14192228108Eac82E00A".parse().unwrap()), + unsafe_block_signer: Some( + "0xa95B83e39AA78B00F12fe431865B563793D97AF5".parse().unwrap(), + ), + batch_submitter: Some("0x19CC7073150D9f5888f09E0e9016d2a39667df14".parse().unwrap()), + }; + + let deserialized: Roles = serde_json::from_str(json_roles).unwrap(); + + assert_eq!(expected, deserialized); + } + + #[test] + fn test_roles_unknown_field_json() { + let json_roles: &str = r#"{ + "SystemConfigOwner": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "ProxyAdminOwner": "0x4377BB0F0103992b31eC12b4d796a8687B8dC8E9", + "Guardian": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "Challenger": "0x8c20c40180751d93E939DDDee3517AE0d1EBeAd2", + "Proposer": "0x95014c45078354Ff839f14192228108Eac82E00A", + "UnsafeBlockSigner": "0xa95B83e39AA78B00F12fe431865B563793D97AF5", + "BatchSubmitter": "0x19CC7073150D9f5888f09E0e9016d2a39667df14", + "UnknownField": "unknown" + }"#; + + let err = serde_json::from_str::(json_roles).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } +} diff --git a/kona/crates/protocol/genesis/src/genesis.rs b/kona/crates/protocol/genesis/src/genesis.rs new file mode 100644 index 0000000000000..3db5a64910514 --- /dev/null +++ b/kona/crates/protocol/genesis/src/genesis.rs @@ -0,0 +1,120 @@ +//! Genesis types. + +use alloy_eips::eip1898::BlockNumHash; + +use crate::SystemConfig; + +/// Chain genesis information. +#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct ChainGenesis { + /// L1 genesis block + pub l1: BlockNumHash, + /// L2 genesis block + pub l2: BlockNumHash, + /// Timestamp of the L2 genesis block + pub l2_time: u64, + /// Optional System configuration + pub system_config: Option, +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for ChainGenesis { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let system_config = Option::::arbitrary(u)?; + let l1_num_hash = BlockNumHash { + number: u64::arbitrary(u)?, + hash: alloy_primitives::B256::arbitrary(u)?, + }; + let l2_num_hash = BlockNumHash { + number: u64::arbitrary(u)?, + hash: alloy_primitives::B256::arbitrary(u)?, + }; + Ok(Self { l1: l1_num_hash, l2: l2_num_hash, l2_time: u.arbitrary()?, system_config }) + } +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use alloy_primitives::{address, b256, uint}; + + const fn ref_genesis() -> ChainGenesis { + ChainGenesis { + l1: BlockNumHash { + hash: b256!("438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108"), + number: 17422590, + }, + l2: BlockNumHash { + hash: b256!("dbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3"), + number: 105235063, + }, + l2_time: 1686068903, + system_config: Some(SystemConfig { + batcher_address: address!("6887246668a3b87F54DeB3b94Ba47a6f63F32985"), + overhead: uint!(0xbc_U256), + scalar: uint!(0xa6fe0_U256), + gas_limit: 30000000, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: Some(10), + }), + } + } + + #[test] + fn test_genesis_serde() { + let genesis_str = r#"{ + "l1": { + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108", + "number": 17422590 + }, + "l2": { + "hash": "0xdbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3", + "number": 105235063 + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddress": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "daFootprintGasScalar": 10 + } + }"#; + let genesis: ChainGenesis = serde_json::from_str(genesis_str).unwrap(); + assert_eq!(genesis, ref_genesis()); + } + + #[test] + fn test_genesis_unknown_field_json() { + let raw: &str = r#"{ + "l1": { + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108", + "number": 17422590 + }, + "l2": { + "hash": "0xdbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3", + "number": 105235063 + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddress": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000 + }, + "unknown_field": "unknown" + }"#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } +} diff --git a/kona/crates/protocol/genesis/src/lib.rs b/kona/crates/protocol/genesis/src/lib.rs new file mode 100644 index 0000000000000..ad10131f19534 --- /dev/null +++ b/kona/crates/protocol/genesis/src/lib.rs @@ -0,0 +1,66 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod params; +pub use params::{ + BASE_MAINNET_BASE_FEE_CONFIG, BASE_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, + BASE_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + BASE_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, BASE_SEPOLIA_BASE_FEE_CONFIG, + BASE_SEPOLIA_BASE_FEE_PARAMS, BASE_SEPOLIA_BASE_FEE_PARAMS_CANYON, + BASE_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, + BASE_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + BASE_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, BaseFeeConfig, OP_MAINNET_BASE_FEE_CONFIG, + OP_MAINNET_BASE_FEE_PARAMS, OP_MAINNET_BASE_FEE_PARAMS_CANYON, + OP_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, + OP_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, OP_SEPOLIA_BASE_FEE_CONFIG, + OP_SEPOLIA_BASE_FEE_PARAMS, OP_SEPOLIA_BASE_FEE_PARAMS_CANYON, + OP_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, + OP_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + OP_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, base_fee_config, base_fee_params, + base_fee_params_canyon, +}; + +mod superchain; +pub use superchain::{ + Chain, ChainList, FaultProofs, Superchain, SuperchainConfig, SuperchainL1Info, SuperchainLevel, + SuperchainParent, Superchains, +}; + +mod updates; +pub use updates::{ + BatcherUpdate, DaFootprintGasScalarUpdate, Eip1559Update, GasConfigUpdate, GasLimitUpdate, + MinBaseFeeUpdate, OperatorFeeUpdate, UnsafeBlockSignerUpdate, +}; + +mod system; +pub use system::{ + BatcherUpdateError, CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC, + DaFootprintGasScalarUpdateError, EIP1559UpdateError, GasConfigUpdateError, GasLimitUpdateError, + LogProcessingError, MinBaseFeeUpdateError, OperatorFeeUpdateError, SystemConfig, + SystemConfigLog, SystemConfigUpdate, SystemConfigUpdateError, SystemConfigUpdateKind, + UnsafeBlockSignerUpdateError, +}; + +mod chain; +pub use chain::{ + AddressList, AltDAConfig, BASE_MAINNET_CHAIN_ID, BASE_SEPOLIA_CHAIN_ID, ChainConfig, + HardForkConfig, L1ChainConfig, OP_MAINNET_CHAIN_ID, OP_SEPOLIA_CHAIN_ID, Roles, +}; + +mod genesis; +pub use genesis::ChainGenesis; + +mod rollup; +pub use rollup::{ + DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, FJORD_MAX_SEQUENCER_DRIFT, GRANITE_CHANNEL_TIMEOUT, + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK, MAX_RLP_BYTES_PER_CHANNEL_FJORD, RollupConfig, +}; diff --git a/kona/crates/protocol/genesis/src/params.rs b/kona/crates/protocol/genesis/src/params.rs new file mode 100644 index 0000000000000..16bb6485744e2 --- /dev/null +++ b/kona/crates/protocol/genesis/src/params.rs @@ -0,0 +1,253 @@ +//! Module containing fee parameters. + +use alloy_eips::eip1559::BaseFeeParams; + +use crate::{ + BASE_MAINNET_CHAIN_ID, BASE_SEPOLIA_CHAIN_ID, OP_MAINNET_CHAIN_ID, OP_SEPOLIA_CHAIN_ID, +}; + +/// Base fee max change denominator for Optimism Mainnet as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const OP_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 50; + +/// Base fee max change denominator for Optimism Mainnet as defined in the Optimism Canyon +/// hardfork. +pub const OP_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON: u64 = 250; + +/// Base fee max change denominator for Optimism Mainnet as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER: u64 = 6; + +/// Base fee max change denominator for Optimism Sepolia as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const OP_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 50; + +/// Base fee max change denominator for Optimism Sepolia as defined in the Optimism Canyon +/// hardfork. +pub const OP_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON: u64 = 250; + +/// Base fee max change denominator for Optimism Sepolia as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const OP_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER: u64 = 6; + +/// Base fee max change denominator for Base Sepolia as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const BASE_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER: u64 = 10; + +/// Base fee max change denominator for Base Sepolia as defined in the Optimism Canyon +/// hardfork. +pub const BASE_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 50; + +/// Base fee max change denominator for Base Sepolia as defined in the Optimism Canyon +/// hardfork. +pub const BASE_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON: u64 = 250; + +/// Base fee max change denominator for Base Mainnet as defined in the Optimism +/// [transaction costs](https://docs.optimism.io/app-developers/transactions/fees) doc. +pub const BASE_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER: u64 = 6; + +/// Base fee max change denominator for Base Mainnet as defined in the Optimism Canyon +/// hardfork. +pub const BASE_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 50; + +/// Base fee max change denominator for Base Mainnet as defined in the Optimism Canyon +/// hardfork. +pub const BASE_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON: u64 = 250; + +/// Get the base fee parameters for Optimism Sepolia. +pub const OP_SEPOLIA_BASE_FEE_PARAMS: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR as u128, + elasticity_multiplier: OP_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Get the base fee parameters for Base Sepolia. +pub const BASE_SEPOLIA_BASE_FEE_PARAMS: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR as u128, + elasticity_multiplier: BASE_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Get the base fee parameters for Optimism Mainnet. +pub const OP_MAINNET_BASE_FEE_PARAMS: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR as u128, + elasticity_multiplier: OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Get the base fee parameters for Optimism Sepolia. +pub const OP_SEPOLIA_BASE_FEE_PARAMS_CANYON: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON as u128, + elasticity_multiplier: OP_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Get the base fee parameters for Base Sepolia. +pub const BASE_SEPOLIA_BASE_FEE_PARAMS_CANYON: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON as u128, + elasticity_multiplier: BASE_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Get the base fee parameters for Optimism Mainnet. +pub const OP_MAINNET_BASE_FEE_PARAMS_CANYON: BaseFeeParams = BaseFeeParams { + max_change_denominator: OP_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON as u128, + elasticity_multiplier: OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER as u128, +}; + +/// Returns the [`BaseFeeParams`] for the given chain id. +pub const fn base_fee_params(chain_id: u64) -> BaseFeeParams { + match chain_id { + OP_MAINNET_CHAIN_ID => OP_MAINNET_BASE_FEE_PARAMS, + OP_SEPOLIA_CHAIN_ID => OP_SEPOLIA_BASE_FEE_PARAMS, + BASE_MAINNET_CHAIN_ID => OP_MAINNET_BASE_FEE_PARAMS, + BASE_SEPOLIA_CHAIN_ID => BASE_SEPOLIA_BASE_FEE_PARAMS, + _ => OP_MAINNET_BASE_FEE_PARAMS, + } +} + +/// Returns the [`BaseFeeParams`] for the given chain id, for canyon hardfork. +pub const fn base_fee_params_canyon(chain_id: u64) -> BaseFeeParams { + match chain_id { + OP_MAINNET_CHAIN_ID => OP_MAINNET_BASE_FEE_PARAMS_CANYON, + OP_SEPOLIA_CHAIN_ID => OP_SEPOLIA_BASE_FEE_PARAMS_CANYON, + BASE_MAINNET_CHAIN_ID => OP_MAINNET_BASE_FEE_PARAMS_CANYON, + BASE_SEPOLIA_CHAIN_ID => BASE_SEPOLIA_BASE_FEE_PARAMS_CANYON, + _ => OP_MAINNET_BASE_FEE_PARAMS_CANYON, + } +} + +/// Returns the [`BaseFeeConfig`] for the given chain id. +pub const fn base_fee_config(chain_id: u64) -> BaseFeeConfig { + match chain_id { + OP_MAINNET_CHAIN_ID => OP_MAINNET_BASE_FEE_CONFIG, + OP_SEPOLIA_CHAIN_ID => OP_SEPOLIA_BASE_FEE_CONFIG, + BASE_MAINNET_CHAIN_ID => BASE_MAINNET_BASE_FEE_CONFIG, + BASE_SEPOLIA_CHAIN_ID => BASE_SEPOLIA_BASE_FEE_CONFIG, + _ => OP_MAINNET_BASE_FEE_CONFIG, + } +} + +/// Get the base fee parameters for Optimism Sepolia. +pub const OP_SEPOLIA_BASE_FEE_CONFIG: BaseFeeConfig = BaseFeeConfig { + eip1559_elasticity: OP_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, + eip1559_denominator: OP_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + eip1559_denominator_canyon: OP_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, +}; + +/// Get the base fee parameters for Base Sepolia. +pub const BASE_SEPOLIA_BASE_FEE_CONFIG: BaseFeeConfig = BaseFeeConfig { + eip1559_elasticity: BASE_SEPOLIA_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, + eip1559_denominator: BASE_SEPOLIA_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + eip1559_denominator_canyon: BASE_SEPOLIA_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, +}; + +/// Get the base fee parameters for Optimism Mainnet. +pub const OP_MAINNET_BASE_FEE_CONFIG: BaseFeeConfig = BaseFeeConfig { + eip1559_elasticity: OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, + eip1559_denominator: OP_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + eip1559_denominator_canyon: OP_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, +}; + +/// Get the base fee parameters for Base Mainnet. +pub const BASE_MAINNET_BASE_FEE_CONFIG: BaseFeeConfig = BaseFeeConfig { + eip1559_elasticity: BASE_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, + eip1559_denominator: BASE_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + eip1559_denominator_canyon: BASE_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, +}; + +/// Optimism Base Fee Config. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BaseFeeConfig { + /// EIP 1559 Elasticity Parameter + #[cfg_attr( + feature = "serde", + serde(rename = "eip1559Elasticity", alias = "eip1559_elasticity") + )] + pub eip1559_elasticity: u64, + /// EIP 1559 Denominator + #[cfg_attr( + feature = "serde", + serde(rename = "eip1559Denominator", alias = "eip1559_denominator") + )] + pub eip1559_denominator: u64, + /// EIP 1559 Denominator for the Canyon hardfork + #[cfg_attr( + feature = "serde", + serde(rename = "eip1559DenominatorCanyon", alias = "eip1559_denominator_canyon") + )] + pub eip1559_denominator_canyon: u64, +} + +impl BaseFeeConfig { + /// Get the base fee parameters for Optimism Mainnet + pub const fn optimism() -> Self { + Self { + eip1559_elasticity: OP_MAINNET_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, + eip1559_denominator: OP_MAINNET_EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + eip1559_denominator_canyon: OP_MAINNET_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_CANYON, + } + } + + /// Returns the inner [BaseFeeParams]. + pub const fn as_base_fee_params(&self) -> BaseFeeParams { + BaseFeeParams { + max_change_denominator: self.eip1559_denominator as u128, + elasticity_multiplier: self.eip1559_elasticity as u128, + } + } + + /// Returns the [BaseFeeParams] for the canyon hardfork. + pub const fn as_canyon_base_fee_params(&self) -> BaseFeeParams { + BaseFeeParams { + max_change_denominator: self.eip1559_denominator_canyon as u128, + elasticity_multiplier: self.eip1559_elasticity as u128, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base_fee_params_from_chain_id() { + assert_eq!(base_fee_params(OP_MAINNET_CHAIN_ID), OP_MAINNET_BASE_FEE_PARAMS); + assert_eq!(base_fee_params(OP_SEPOLIA_CHAIN_ID), OP_SEPOLIA_BASE_FEE_PARAMS); + assert_eq!(base_fee_params(BASE_MAINNET_CHAIN_ID), OP_MAINNET_BASE_FEE_PARAMS); + assert_eq!(base_fee_params(BASE_SEPOLIA_CHAIN_ID), BASE_SEPOLIA_BASE_FEE_PARAMS); + assert_eq!(base_fee_params(0), OP_MAINNET_BASE_FEE_PARAMS); + } + + #[test] + fn test_base_fee_params_canyon_from_chain_id() { + assert_eq!(base_fee_params_canyon(OP_MAINNET_CHAIN_ID), OP_MAINNET_BASE_FEE_PARAMS_CANYON); + assert_eq!(base_fee_params_canyon(OP_SEPOLIA_CHAIN_ID), OP_SEPOLIA_BASE_FEE_PARAMS_CANYON); + assert_eq!( + base_fee_params_canyon(BASE_MAINNET_CHAIN_ID), + OP_MAINNET_BASE_FEE_PARAMS_CANYON + ); + assert_eq!( + base_fee_params_canyon(BASE_SEPOLIA_CHAIN_ID), + BASE_SEPOLIA_BASE_FEE_PARAMS_CANYON + ); + assert_eq!(base_fee_params_canyon(0), OP_MAINNET_BASE_FEE_PARAMS_CANYON); + } + + #[test] + #[cfg(feature = "serde")] + fn test_base_fee_config_ser() { + let config = OP_MAINNET_BASE_FEE_CONFIG; + let raw_str = serde_json::to_string(&config).unwrap(); + assert_eq!( + raw_str, + r#"{"eip1559Elasticity":6,"eip1559Denominator":50,"eip1559DenominatorCanyon":250}"# + ); + } + + #[test] + #[cfg(feature = "serde")] + fn test_base_fee_config_deser() { + let raw_str: &'static str = + r#"{"eip1559Elasticity":6,"eip1559Denominator":50,"eip1559DenominatorCanyon":250}"#; + let config: BaseFeeConfig = serde_json::from_str(raw_str).unwrap(); + assert_eq!(config, OP_MAINNET_BASE_FEE_CONFIG); + } +} diff --git a/kona/crates/protocol/genesis/src/rollup.rs b/kona/crates/protocol/genesis/src/rollup.rs new file mode 100644 index 0000000000000..a6048d51e5dec --- /dev/null +++ b/kona/crates/protocol/genesis/src/rollup.rs @@ -0,0 +1,950 @@ +//! Rollup Config Types + +use crate::{AltDAConfig, BaseFeeConfig, ChainGenesis, HardForkConfig, OP_MAINNET_BASE_FEE_CONFIG}; +use alloy_chains::Chain; +use alloy_hardforks::{EthereumHardfork, EthereumHardforks, ForkCondition}; +use alloy_op_hardforks::{OpHardfork, OpHardforks}; +use alloy_primitives::Address; + +/// The max rlp bytes per channel for the Bedrock hardfork. +pub const MAX_RLP_BYTES_PER_CHANNEL_BEDROCK: u64 = 10_000_000; + +/// The max rlp bytes per channel for the Fjord hardfork. +pub const MAX_RLP_BYTES_PER_CHANNEL_FJORD: u64 = 100_000_000; + +/// The max sequencer drift when the Fjord hardfork is active. +pub const FJORD_MAX_SEQUENCER_DRIFT: u64 = 1800; + +/// The channel timeout once the Granite hardfork is active. +pub const GRANITE_CHANNEL_TIMEOUT: u64 = 50; + +/// The default interop message expiry window. (1 hour, in seconds) +pub const DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW: u64 = 60 * 60; + +#[cfg(feature = "serde")] +const fn default_granite_channel_timeout() -> u64 { + GRANITE_CHANNEL_TIMEOUT +} + +#[cfg(feature = "serde")] +const fn default_interop_message_expiry_window() -> u64 { + DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW +} + +/// The Rollup configuration. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct RollupConfig { + /// The genesis state of the rollup. + pub genesis: ChainGenesis, + /// The block time of the L2, in seconds. + pub block_time: u64, + /// Sequencer batches may not be more than MaxSequencerDrift seconds after + /// the L1 timestamp of the sequencing window end. + /// + /// Note: When L1 has many 1 second consecutive blocks, and L2 grows at fixed 2 seconds, + /// the L2 time may still grow beyond this difference. + /// + /// Note: After the Fjord hardfork, this value becomes a constant of `1800`. + pub max_sequencer_drift: u64, + /// The sequencer window size. + pub seq_window_size: u64, + /// Number of L1 blocks between when a channel can be opened and when it can be closed. + pub channel_timeout: u64, + /// The channel timeout after the Granite hardfork. + #[cfg_attr(feature = "serde", serde(default = "default_granite_channel_timeout"))] + pub granite_channel_timeout: u64, + /// The L1 chain ID + pub l1_chain_id: u64, + /// The L2 chain ID + pub l2_chain_id: Chain, + /// Hardfork timestamps. + #[cfg_attr(feature = "serde", serde(flatten))] + pub hardforks: HardForkConfig, + /// `batch_inbox_address` is the L1 address that batches are sent to. + pub batch_inbox_address: Address, + /// `deposit_contract_address` is the L1 address that deposits are sent to. + pub deposit_contract_address: Address, + /// `l1_system_config_address` is the L1 address that the system config is stored at. + pub l1_system_config_address: Address, + /// `protocol_versions_address` is the L1 address that the protocol versions are stored at. + pub protocol_versions_address: Address, + /// The superchain config address. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub superchain_config_address: Option
, + /// `blobs_enabled_l1_timestamp` is the timestamp to start reading blobs as a batch data + /// source. Optional. + #[cfg_attr( + feature = "serde", + serde(rename = "blobs_data", skip_serializing_if = "Option::is_none") + )] + pub blobs_enabled_l1_timestamp: Option, + /// `da_challenge_address` is the L1 address that the data availability challenge contract is + /// stored at. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub da_challenge_address: Option
, + /// `interop_message_expiry_window` is the maximum time (in seconds) that an initiating message + /// can be referenced on a remote chain before it expires. + #[cfg_attr(feature = "serde", serde(default = "default_interop_message_expiry_window"))] + pub interop_message_expiry_window: u64, + /// `alt_da_config` is the chain-specific DA config for the rollup. + #[cfg_attr(feature = "serde", serde(rename = "alt_da"))] + pub alt_da_config: Option, + /// `chain_op_config` is the chain-specific EIP1559 config for the rollup. + #[cfg_attr(feature = "serde", serde(default = "BaseFeeConfig::optimism"))] + pub chain_op_config: BaseFeeConfig, +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for RollupConfig { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + use crate::{ + BASE_SEPOLIA_BASE_FEE_CONFIG, OP_MAINNET_BASE_FEE_CONFIG, OP_SEPOLIA_BASE_FEE_CONFIG, + }; + let chain_op_config = match u32::arbitrary(u)? % 3 { + 0 => OP_MAINNET_BASE_FEE_CONFIG, + 1 => OP_SEPOLIA_BASE_FEE_CONFIG, + _ => BASE_SEPOLIA_BASE_FEE_CONFIG, + }; + + Ok(Self { + genesis: ChainGenesis::arbitrary(u)?, + block_time: u.arbitrary()?, + max_sequencer_drift: u.arbitrary()?, + seq_window_size: u.arbitrary()?, + channel_timeout: u.arbitrary()?, + granite_channel_timeout: u.arbitrary()?, + l1_chain_id: u.arbitrary()?, + l2_chain_id: u.arbitrary()?, + hardforks: HardForkConfig::arbitrary(u)?, + batch_inbox_address: Address::arbitrary(u)?, + deposit_contract_address: Address::arbitrary(u)?, + l1_system_config_address: Address::arbitrary(u)?, + protocol_versions_address: Address::arbitrary(u)?, + superchain_config_address: Option::
::arbitrary(u)?, + blobs_enabled_l1_timestamp: Option::::arbitrary(u)?, + da_challenge_address: Option::
::arbitrary(u)?, + interop_message_expiry_window: u.arbitrary()?, + chain_op_config, + alt_da_config: Option::::arbitrary(u)?, + }) + } +} + +// Need to manually implement Default because [`BaseFeeParams`] has no Default impl. +impl Default for RollupConfig { + fn default() -> Self { + Self { + genesis: ChainGenesis::default(), + block_time: 0, + max_sequencer_drift: 0, + seq_window_size: 0, + channel_timeout: 0, + granite_channel_timeout: GRANITE_CHANNEL_TIMEOUT, + l1_chain_id: 0, + l2_chain_id: Chain::from_id(0), + hardforks: HardForkConfig::default(), + batch_inbox_address: Address::ZERO, + deposit_contract_address: Address::ZERO, + l1_system_config_address: Address::ZERO, + protocol_versions_address: Address::ZERO, + superchain_config_address: None, + blobs_enabled_l1_timestamp: None, + da_challenge_address: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + alt_da_config: None, + chain_op_config: OP_MAINNET_BASE_FEE_CONFIG, + } + } +} + +#[cfg(feature = "revm")] +impl RollupConfig { + /// Returns the active [`op_revm::OpSpecId`] for the executor. + /// + /// ## Takes + /// - `timestamp`: The timestamp of the executing block. + /// + /// ## Returns + /// The active [`op_revm::OpSpecId`] for the executor. + pub fn spec_id(&self, timestamp: u64) -> op_revm::OpSpecId { + if self.is_interop_active(timestamp) { + op_revm::OpSpecId::INTEROP + } else if self.is_jovian_active(timestamp) { + op_revm::OpSpecId::JOVIAN + } else if self.is_isthmus_active(timestamp) { + op_revm::OpSpecId::ISTHMUS + } else if self.is_holocene_active(timestamp) { + op_revm::OpSpecId::HOLOCENE + } else if self.is_fjord_active(timestamp) { + op_revm::OpSpecId::FJORD + } else if self.is_ecotone_active(timestamp) { + op_revm::OpSpecId::ECOTONE + } else if self.is_canyon_active(timestamp) { + op_revm::OpSpecId::CANYON + } else if self.is_regolith_active(timestamp) { + op_revm::OpSpecId::REGOLITH + } else { + op_revm::OpSpecId::BEDROCK + } + } +} + +impl RollupConfig { + /// Returns true if Regolith is active at the given timestamp. + pub fn is_regolith_active(&self, timestamp: u64) -> bool { + self.hardforks.regolith_time.is_some_and(|t| timestamp >= t) || + self.is_canyon_active(timestamp) + } + + /// Returns true if the timestamp marks the first Regolith block. + pub fn is_first_regolith_block(&self, timestamp: u64) -> bool { + self.is_regolith_active(timestamp) && + !self.is_regolith_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Canyon is active at the given timestamp. + pub fn is_canyon_active(&self, timestamp: u64) -> bool { + self.hardforks.canyon_time.is_some_and(|t| timestamp >= t) || + self.is_delta_active(timestamp) + } + + /// Returns true if the timestamp marks the first Canyon block. + pub fn is_first_canyon_block(&self, timestamp: u64) -> bool { + self.is_canyon_active(timestamp) && + !self.is_canyon_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Delta is active at the given timestamp. + pub fn is_delta_active(&self, timestamp: u64) -> bool { + self.hardforks.delta_time.is_some_and(|t| timestamp >= t) || + self.is_ecotone_active(timestamp) + } + + /// Returns true if the timestamp marks the first Delta block. + pub fn is_first_delta_block(&self, timestamp: u64) -> bool { + self.is_delta_active(timestamp) && + !self.is_delta_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Ecotone is active at the given timestamp. + pub fn is_ecotone_active(&self, timestamp: u64) -> bool { + self.hardforks.ecotone_time.is_some_and(|t| timestamp >= t) || + self.is_fjord_active(timestamp) + } + + /// Returns true if the timestamp marks the first Ecotone block. + pub fn is_first_ecotone_block(&self, timestamp: u64) -> bool { + self.is_ecotone_active(timestamp) && + !self.is_ecotone_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Fjord is active at the given timestamp. + pub fn is_fjord_active(&self, timestamp: u64) -> bool { + self.hardforks.fjord_time.is_some_and(|t| timestamp >= t) || + self.is_granite_active(timestamp) + } + + /// Returns true if the timestamp marks the first Fjord block. + pub fn is_first_fjord_block(&self, timestamp: u64) -> bool { + self.is_fjord_active(timestamp) && + !self.is_fjord_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Granite is active at the given timestamp. + pub fn is_granite_active(&self, timestamp: u64) -> bool { + self.hardforks.granite_time.is_some_and(|t| timestamp >= t) || + self.is_holocene_active(timestamp) + } + + /// Returns true if the timestamp marks the first Granite block. + pub fn is_first_granite_block(&self, timestamp: u64) -> bool { + self.is_granite_active(timestamp) && + !self.is_granite_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Holocene is active at the given timestamp. + pub fn is_holocene_active(&self, timestamp: u64) -> bool { + self.hardforks.holocene_time.is_some_and(|t| timestamp >= t) || + self.is_isthmus_active(timestamp) + } + + /// Returns true if the timestamp marks the first Holocene block. + pub fn is_first_holocene_block(&self, timestamp: u64) -> bool { + self.is_holocene_active(timestamp) && + !self.is_holocene_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if the pectra blob schedule is active at the given timestamp. + pub fn is_pectra_blob_schedule_active(&self, timestamp: u64) -> bool { + self.hardforks.pectra_blob_schedule_time.is_some_and(|t| timestamp >= t) + } + + /// Returns true if the timestamp marks the first pectra blob schedule block. + pub fn is_first_pectra_blob_schedule_block(&self, timestamp: u64) -> bool { + self.is_pectra_blob_schedule_active(timestamp) && + !self.is_pectra_blob_schedule_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Isthmus is active at the given timestamp. + pub fn is_isthmus_active(&self, timestamp: u64) -> bool { + self.hardforks.isthmus_time.is_some_and(|t| timestamp >= t) || + self.is_jovian_active(timestamp) + } + + /// Returns true if the timestamp marks the first Isthmus block. + pub fn is_first_isthmus_block(&self, timestamp: u64) -> bool { + self.is_isthmus_active(timestamp) && + !self.is_isthmus_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Jovian is active at the given timestamp. + pub fn is_jovian_active(&self, timestamp: u64) -> bool { + self.hardforks.jovian_time.is_some_and(|t| timestamp >= t) || + self.is_interop_active(timestamp) + } + + /// Returns true if the timestamp marks the first Jovian block. + pub fn is_first_jovian_block(&self, timestamp: u64) -> bool { + self.is_jovian_active(timestamp) && + !self.is_jovian_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if Interop is active at the given timestamp. + pub fn is_interop_active(&self, timestamp: u64) -> bool { + self.hardforks.interop_time.is_some_and(|t| timestamp >= t) + } + + /// Returns true if the timestamp marks the first Interop block. + pub fn is_first_interop_block(&self, timestamp: u64) -> bool { + self.is_interop_active(timestamp) && + !self.is_interop_active(timestamp.saturating_sub(self.block_time)) + } + + /// Returns true if a DA Challenge proxy Address is provided in the rollup config and the + /// address is not zero. + pub fn is_alt_da_enabled(&self) -> bool { + self.da_challenge_address.is_some_and(|addr| !addr.is_zero()) + } + + /// Returns the max sequencer drift for the given timestamp. + pub fn max_sequencer_drift(&self, timestamp: u64) -> u64 { + if self.is_fjord_active(timestamp) { + FJORD_MAX_SEQUENCER_DRIFT + } else { + self.max_sequencer_drift + } + } + + /// Returns the max rlp bytes per channel for the given timestamp. + pub fn max_rlp_bytes_per_channel(&self, timestamp: u64) -> u64 { + if self.is_fjord_active(timestamp) { + MAX_RLP_BYTES_PER_CHANNEL_FJORD + } else { + MAX_RLP_BYTES_PER_CHANNEL_BEDROCK + } + } + + /// Returns the channel timeout for the given timestamp. + pub fn channel_timeout(&self, timestamp: u64) -> u64 { + if self.is_granite_active(timestamp) { + self.granite_channel_timeout + } else { + self.channel_timeout + } + } + + /// Returns the [HardForkConfig] using [RollupConfig] timestamps. + #[deprecated(since = "0.1.0", note = "Use the `hardforks` field instead.")] + pub const fn hardfork_config(&self) -> HardForkConfig { + self.hardforks + } + + /// Computes a block number from a timestamp, relative to the L2 genesis time and the block + /// time. + /// + /// This function assumes that the timestamp is aligned with the block time, and uses floor + /// division in its computation. + pub const fn block_number_from_timestamp(&self, timestamp: u64) -> u64 { + timestamp.saturating_sub(self.genesis.l2_time).saturating_div(self.block_time) + } + + /// Checks the scalar value in Ecotone. + pub fn check_ecotone_l1_system_config_scalar(scalar: [u8; 32]) -> Result<(), &'static str> { + let version_byte = scalar[0]; + match version_byte { + 0 => { + if scalar[1..28] != [0; 27] { + return Err("Bedrock scalar padding not empty"); + } + Ok(()) + } + 1 => { + if scalar[1..24] != [0; 23] { + return Err("Invalid version 1 scalar padding"); + } + Ok(()) + } + _ => { + // ignore the event if it's an unknown scalar format + Err("Unrecognized scalar version") + } + } + } +} + +impl EthereumHardforks for RollupConfig { + fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition { + if fork <= EthereumHardfork::Berlin { + // We assume that OP chains were launched with all forks before Berlin activated. + ForkCondition::Block(0) + } else if fork <= EthereumHardfork::Paris { + // Bedrock activates all hardforks up to Paris. + self.op_fork_activation(OpHardfork::Bedrock) + } else if fork <= EthereumHardfork::Shanghai { + // Canyon activates Shanghai hardfork. + self.op_fork_activation(OpHardfork::Canyon) + } else if fork <= EthereumHardfork::Cancun { + // Ecotone activates Cancun hardfork. + self.op_fork_activation(OpHardfork::Ecotone) + } else if fork <= EthereumHardfork::Prague { + // Isthmus activates Prague hardfork. + self.op_fork_activation(OpHardfork::Isthmus) + } else { + ForkCondition::Never + } + } +} + +impl OpHardforks for RollupConfig { + fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition { + match fork { + OpHardfork::Bedrock => ForkCondition::Block(0), + OpHardfork::Regolith => self + .hardforks + .regolith_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Canyon)), + OpHardfork::Canyon => self + .hardforks + .canyon_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Ecotone)), + OpHardfork::Ecotone => self + .hardforks + .ecotone_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Fjord)), + OpHardfork::Fjord => self + .hardforks + .fjord_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Granite)), + OpHardfork::Granite => self + .hardforks + .granite_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Holocene)), + OpHardfork::Holocene => self + .hardforks + .holocene_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Isthmus)), + OpHardfork::Isthmus => self + .hardforks + .isthmus_time + .map(ForkCondition::Timestamp) + .unwrap_or(self.op_fork_activation(OpHardfork::Jovian)), + OpHardfork::Jovian => self + .hardforks + .jovian_time + .map(ForkCondition::Timestamp) + .unwrap_or(ForkCondition::Never), + OpHardfork::Interop => self + .hardforks + .interop_time + .map(ForkCondition::Timestamp) + .unwrap_or(ForkCondition::Never), + _ => ForkCondition::Never, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "serde")] + use alloy_eips::BlockNumHash; + use alloy_primitives::address; + #[cfg(feature = "serde")] + use alloy_primitives::{U256, b256}; + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_rollup_config() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + RollupConfig::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + } + + #[test] + #[cfg(feature = "revm")] + fn test_revm_spec_id() { + // By default, the spec ID should be BEDROCK. + let mut config = RollupConfig { + hardforks: HardForkConfig { regolith_time: Some(10), ..Default::default() }, + ..Default::default() + }; + assert_eq!(config.spec_id(0), op_revm::OpSpecId::BEDROCK); + assert_eq!(config.spec_id(10), op_revm::OpSpecId::REGOLITH); + config.hardforks.canyon_time = Some(20); + assert_eq!(config.spec_id(20), op_revm::OpSpecId::CANYON); + config.hardforks.ecotone_time = Some(30); + assert_eq!(config.spec_id(30), op_revm::OpSpecId::ECOTONE); + config.hardforks.fjord_time = Some(40); + assert_eq!(config.spec_id(40), op_revm::OpSpecId::FJORD); + config.hardforks.holocene_time = Some(50); + assert_eq!(config.spec_id(50), op_revm::OpSpecId::HOLOCENE); + config.hardforks.isthmus_time = Some(60); + assert_eq!(config.spec_id(60), op_revm::OpSpecId::ISTHMUS); + } + + #[test] + fn test_regolith_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_regolith_active(0)); + config.hardforks.regolith_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(!config.is_regolith_active(9)); + } + + #[test] + fn test_canyon_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_canyon_active(0)); + config.hardforks.canyon_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(!config.is_canyon_active(9)); + } + + #[test] + fn test_delta_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_delta_active(0)); + config.hardforks.delta_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(!config.is_delta_active(9)); + } + + #[test] + fn test_ecotone_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_ecotone_active(0)); + config.hardforks.ecotone_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(!config.is_ecotone_active(9)); + } + + #[test] + fn test_fjord_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_fjord_active(0)); + config.hardforks.fjord_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(!config.is_fjord_active(9)); + } + + #[test] + fn test_granite_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_granite_active(0)); + config.hardforks.granite_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(config.is_granite_active(10)); + assert!(!config.is_granite_active(9)); + } + + #[test] + fn test_holocene_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_holocene_active(0)); + config.hardforks.holocene_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(config.is_granite_active(10)); + assert!(config.is_holocene_active(10)); + assert!(!config.is_holocene_active(9)); + } + + #[test] + fn test_pectra_blob_schedule_active() { + let mut config = RollupConfig::default(); + config.hardforks.pectra_blob_schedule_time = Some(10); + // Pectra blob schedule is a unique fork, not included in the hierarchical ordering. Its + // activation does not imply the activation of any other forks. + assert!(!config.is_regolith_active(10)); + assert!(!config.is_canyon_active(10)); + assert!(!config.is_delta_active(10)); + assert!(!config.is_ecotone_active(10)); + assert!(!config.is_fjord_active(10)); + assert!(!config.is_granite_active(10)); + assert!(!config.is_holocene_active(0)); + assert!(config.is_pectra_blob_schedule_active(10)); + assert!(!config.is_pectra_blob_schedule_active(9)); + } + + #[test] + fn test_isthmus_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_isthmus_active(0)); + config.hardforks.isthmus_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(config.is_granite_active(10)); + assert!(config.is_holocene_active(10)); + assert!(!config.is_pectra_blob_schedule_active(10)); + assert!(config.is_isthmus_active(10)); + assert!(!config.is_isthmus_active(9)); + } + + #[test] + fn test_jovian_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_interop_active(0)); + config.hardforks.jovian_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(config.is_granite_active(10)); + assert!(config.is_holocene_active(10)); + assert!(!config.is_pectra_blob_schedule_active(10)); + assert!(config.is_isthmus_active(10)); + assert!(config.is_jovian_active(10)); + assert!(!config.is_jovian_active(9)); + } + + #[test] + fn test_interop_active() { + let mut config = RollupConfig::default(); + assert!(!config.is_interop_active(0)); + config.hardforks.interop_time = Some(10); + assert!(config.is_regolith_active(10)); + assert!(config.is_canyon_active(10)); + assert!(config.is_delta_active(10)); + assert!(config.is_ecotone_active(10)); + assert!(config.is_fjord_active(10)); + assert!(config.is_granite_active(10)); + assert!(config.is_holocene_active(10)); + assert!(!config.is_pectra_blob_schedule_active(10)); + assert!(config.is_isthmus_active(10)); + assert!(config.is_interop_active(10)); + assert!(!config.is_interop_active(9)); + } + + #[test] + fn test_is_first_fork_block() { + let cfg = RollupConfig { + hardforks: HardForkConfig { + regolith_time: Some(10), + canyon_time: Some(20), + delta_time: Some(30), + ecotone_time: Some(40), + fjord_time: Some(50), + granite_time: Some(60), + holocene_time: Some(70), + pectra_blob_schedule_time: Some(80), + isthmus_time: Some(90), + jovian_time: Some(100), + interop_time: Some(110), + }, + block_time: 2, + ..Default::default() + }; + + // Regolith + assert!(!cfg.is_first_regolith_block(8)); + assert!(cfg.is_first_regolith_block(10)); + assert!(!cfg.is_first_regolith_block(12)); + + // Canyon + assert!(!cfg.is_first_canyon_block(18)); + assert!(cfg.is_first_canyon_block(20)); + assert!(!cfg.is_first_canyon_block(22)); + + // Delta + assert!(!cfg.is_first_delta_block(28)); + assert!(cfg.is_first_delta_block(30)); + assert!(!cfg.is_first_delta_block(32)); + + // Ecotone + assert!(!cfg.is_first_ecotone_block(38)); + assert!(cfg.is_first_ecotone_block(40)); + assert!(!cfg.is_first_ecotone_block(42)); + + // Fjord + assert!(!cfg.is_first_fjord_block(48)); + assert!(cfg.is_first_fjord_block(50)); + assert!(!cfg.is_first_fjord_block(52)); + + // Granite + assert!(!cfg.is_first_granite_block(58)); + assert!(cfg.is_first_granite_block(60)); + assert!(!cfg.is_first_granite_block(62)); + + // Holocene + assert!(!cfg.is_first_holocene_block(68)); + assert!(cfg.is_first_holocene_block(70)); + assert!(!cfg.is_first_holocene_block(72)); + + // Pectra blob schedule + assert!(!cfg.is_first_pectra_blob_schedule_block(78)); + assert!(cfg.is_first_pectra_blob_schedule_block(80)); + assert!(!cfg.is_first_pectra_blob_schedule_block(82)); + + // Isthmus + assert!(!cfg.is_first_isthmus_block(88)); + assert!(cfg.is_first_isthmus_block(90)); + assert!(!cfg.is_first_isthmus_block(92)); + + // Jovian + assert!(!cfg.is_first_jovian_block(98)); + assert!(cfg.is_first_jovian_block(100)); + assert!(!cfg.is_first_jovian_block(102)); + + // Interop + assert!(!cfg.is_first_interop_block(108)); + assert!(cfg.is_first_interop_block(110)); + assert!(!cfg.is_first_interop_block(112)); + } + + #[test] + fn test_alt_da_enabled() { + let mut config = RollupConfig::default(); + assert!(!config.is_alt_da_enabled()); + config.da_challenge_address = Some(Address::ZERO); + assert!(!config.is_alt_da_enabled()); + config.da_challenge_address = Some(address!("0000000000000000000000000000000000000001")); + assert!(config.is_alt_da_enabled()); + } + + #[test] + fn test_granite_channel_timeout() { + let mut config = RollupConfig { + channel_timeout: 100, + hardforks: HardForkConfig { granite_time: Some(10), ..Default::default() }, + ..Default::default() + }; + assert_eq!(config.channel_timeout(0), 100); + assert_eq!(config.channel_timeout(10), GRANITE_CHANNEL_TIMEOUT); + config.hardforks.granite_time = None; + assert_eq!(config.channel_timeout(10), 100); + } + + #[test] + fn test_max_sequencer_drift() { + let mut config = RollupConfig { max_sequencer_drift: 100, ..Default::default() }; + assert_eq!(config.max_sequencer_drift(0), 100); + config.hardforks.fjord_time = Some(10); + assert_eq!(config.max_sequencer_drift(0), 100); + assert_eq!(config.max_sequencer_drift(10), FJORD_MAX_SEQUENCER_DRIFT); + } + + #[test] + #[cfg(feature = "serde")] + fn test_deserialize_reference_rollup_config() { + use crate::{OP_MAINNET_BASE_FEE_CONFIG, SystemConfig}; + + let raw: &str = r#" + { + "genesis": { + "l1": { + "hash": "0x481724ee99b1f4cb71d826e2ec5a37265f460e9b112315665c977f4050b0af54", + "number": 10 + }, + "l2": { + "hash": "0x88aedfbf7dea6bfa2c4ff315784ad1a7f145d8f650969359c003bbed68c87631", + "number": 0 + }, + "l2_time": 1725557164, + "system_config": { + "batcherAddr": "0xc81f87a644b41e49b3221f41251f15c6cb00ce03", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": 1234, + "blobBaseFeeScalar": 5678, + "eip1559Denominator": 10, + "eip1559Elasticity": 20, + "operatorFeeScalar": 30, + "operatorFeeConstant": 40, + "minBaseFee": 50, + "daFootprintGasScalar": 10 + } + }, + "block_time": 2, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 3151908, + "l2_chain_id": 1337, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "batch_inbox_address": "0xff00000000000000000000000000000000042069", + "deposit_contract_address": "0x08073dc48dde578137b8af042bcbc1c2491f1eb2", + "l1_system_config_address": "0x94ee52a9d8edd72a85dea7fae3ba6d75e4bf1710", + "protocol_versions_address": "0x0000000000000000000000000000000000000000", + "chain_op_config": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null + } + "#; + + let expected = RollupConfig { + genesis: ChainGenesis { + l1: BlockNumHash { + hash: b256!("481724ee99b1f4cb71d826e2ec5a37265f460e9b112315665c977f4050b0af54"), + number: 10, + }, + l2: BlockNumHash { + hash: b256!("88aedfbf7dea6bfa2c4ff315784ad1a7f145d8f650969359c003bbed68c87631"), + number: 0, + }, + l2_time: 1725557164, + system_config: Some(SystemConfig { + batcher_address: address!("c81f87a644b41e49b3221f41251f15c6cb00ce03"), + overhead: U256::ZERO, + scalar: U256::from(0xf4240), + gas_limit: 30_000_000, + base_fee_scalar: Some(1234), + blob_base_fee_scalar: Some(5678), + eip1559_denominator: Some(10), + eip1559_elasticity: Some(20), + operator_fee_scalar: Some(30), + operator_fee_constant: Some(40), + min_base_fee: Some(50), + da_footprint_gas_scalar: Some(10), + }), + }, + block_time: 2, + max_sequencer_drift: 600, + seq_window_size: 3600, + channel_timeout: 300, + granite_channel_timeout: GRANITE_CHANNEL_TIMEOUT, + l1_chain_id: 3151908, + l2_chain_id: Chain::from_id(1337), + hardforks: HardForkConfig { + regolith_time: Some(0), + canyon_time: Some(0), + delta_time: Some(0), + ecotone_time: Some(0), + fjord_time: Some(0), + ..Default::default() + }, + batch_inbox_address: address!("ff00000000000000000000000000000000042069"), + deposit_contract_address: address!("08073dc48dde578137b8af042bcbc1c2491f1eb2"), + l1_system_config_address: address!("94ee52a9d8edd72a85dea7fae3ba6d75e4bf1710"), + protocol_versions_address: Address::ZERO, + superchain_config_address: None, + blobs_enabled_l1_timestamp: None, + da_challenge_address: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + chain_op_config: OP_MAINNET_BASE_FEE_CONFIG, + alt_da_config: None, + }; + + let deserialized: RollupConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn test_rollup_config_unknown_field() { + let raw: &str = r#" + { + "genesis": { + "l1": { + "hash": "0x481724ee99b1f4cb71d826e2ec5a37265f460e9b112315665c977f4050b0af54", + "number": 10 + }, + "l2": { + "hash": "0x88aedfbf7dea6bfa2c4ff315784ad1a7f145d8f650969359c003bbed68c87631", + "number": 0 + }, + "l2_time": 1725557164, + "system_config": { + "batcherAddr": "0xc81f87a644b41e49b3221f41251f15c6cb00ce03", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000 + } + }, + "block_time": 2, + "max_sequencer_drift": 600, + "seq_window_size": 3600, + "channel_timeout": 300, + "l1_chain_id": 3151908, + "l2_chain_id": 1337, + "regolith_time": 0, + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "batch_inbox_address": "0xff00000000000000000000000000000000042069", + "deposit_contract_address": "0x08073dc48dde578137b8af042bcbc1c2491f1eb2", + "l1_system_config_address": "0x94ee52a9d8edd72a85dea7fae3ba6d75e4bf1710", + "protocol_versions_address": "0x0000000000000000000000000000000000000000", + "chain_op_config": { + "eip1559_elasticity": 100, + "eip1559_denominator": 100, + "eip1559_denominator_canyon": 100 + }, + "unknown_field": "unknown" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + fn test_compute_block_number_from_time() { + let cfg = RollupConfig { + genesis: ChainGenesis { l2_time: 10, ..Default::default() }, + block_time: 2, + ..Default::default() + }; + + assert_eq!(cfg.block_number_from_timestamp(20), 5); + assert_eq!(cfg.block_number_from_timestamp(30), 10); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/chain.rs b/kona/crates/protocol/genesis/src/superchain/chain.rs new file mode 100644 index 0000000000000..861af4dcd069e --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/chain.rs @@ -0,0 +1,113 @@ +//! Contains the `Superchain` type. + +use crate::{ChainConfig, SuperchainConfig}; +use alloc::{string::String, vec::Vec}; + +/// A superchain configuration. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Superchain { + /// Superchain identifier, without capitalization or display changes. + pub name: String, + /// Superchain configuration file contents. + pub config: SuperchainConfig, + /// Chain IDs of chains that are part of this superchain. + pub chains: Vec, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use crate::{HardForkConfig, SuperchainConfig, SuperchainL1Info}; + use alloc::{string::ToString, vec}; + + #[test] + fn test_deny_unknown_fields_superchain() { + let raw: &str = r#" + { + "name": "Mainnet", + "config": { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + } + }, + "chains": [], + "other": "test" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + fn test_superchain_serde() { + let raw: &str = r#" + { + "name": "Mainnet", + "config": { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + } + }, + "chains": [] + } + "#; + + let superchain = Superchain { + name: "Mainnet".to_string(), + config: SuperchainConfig { + name: "Mainnet".to_string(), + l1: SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }, + protocol_versions_addr: None, + superchain_config_addr: None, + op_contracts_manager_proxy_addr: None, + }, + chains: vec![], + }; + + let deserialized: Superchain = serde_json::from_str(raw).unwrap(); + assert_eq!(superchain, deserialized); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/chain_list.rs b/kona/crates/protocol/genesis/src/superchain/chain_list.rs new file mode 100644 index 0000000000000..5ec99d04900fd --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/chain_list.rs @@ -0,0 +1,135 @@ +//! List of OP Stack chains. + +use alloc::{string::String, vec::Vec}; +use alloy_chains::Chain as AlloyChain; + +/// List of Chains. +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] +pub struct ChainList { + /// List of Chains. + pub chains: Vec, +} + +impl ChainList { + /// Fetch a [Chain] by its identifier. + pub fn get_chain_by_ident(&self, identifier: &str) -> Option<&Chain> { + self.chains.iter().find(|c| c.identifier.eq_ignore_ascii_case(identifier)) + } + + /// Returns all available [Chain] identifiers. + pub fn chain_idents(&self) -> Vec { + self.chains.iter().map(|c| c.identifier.clone()).collect() + } + + /// Fetch a [Chain] by its chain id. + pub fn get_chain_by_id(&self, chain_id: u64) -> Option<&Chain> { + self.chains.iter().find(|c| c.chain_id == chain_id) + } + + /// Fetch a [Chain] by the corresponding [AlloyChain] + pub fn get_chain_by_alloy_ident(&self, chain: &AlloyChain) -> Option<&Chain> { + self.get_chain_by_id(chain.id()) + } + + /// Returns the number of chains. + pub const fn len(&self) -> usize { + self.chains.len() + } + + /// Returns true if the list is empty. + pub const fn is_empty(&self) -> bool { + self.chains.is_empty() + } +} + +/// A Chain Definition. +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +#[cfg_attr(feature = "tabled", derive(tabled::Tabled))] +pub struct Chain { + /// The name of the chain. + pub name: String, + /// Chain identifier. + pub identifier: String, + /// Chain ID. + pub chain_id: u64, + /// List of RPC Endpoints. + #[cfg_attr(feature = "tabled", tabled(skip))] + pub rpc: Vec, + /// List of Explorer Endpoints. + #[cfg_attr(feature = "tabled", tabled(skip))] + pub explorers: Vec, + /// The Superchain Level. + pub superchain_level: u64, + /// Governed by Optimism flag. + #[cfg_attr(feature = "tabled", tabled(skip))] + pub governed_by_optimism: Option, + /// The data availability type. + pub data_availability_type: String, + /// The Superchain Parent. + #[cfg_attr(feature = "tabled", tabled(skip))] + pub parent: SuperchainParent, + /// The gas paying token. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + #[cfg_attr(feature = "tabled", tabled(skip))] + pub gas_paying_token: Option, + /// Fault Proofs information. + #[cfg_attr(feature = "tabled", tabled(skip))] + pub fault_proofs: Option, +} + +/// A Chain Parent +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SuperchainParent { + /// The parent type. + pub r#type: String, + /// The chain identifier. + pub chain: String, +} + +impl SuperchainParent { + /// Returns the chain id for the parent. + pub fn chain_id(&self) -> u64 { + match self.chain.as_ref() { + "mainnet" => 1, + "sepolia" => 11155111, + "sepolia-dev-0" => 11155421, + _ => 10, + } + } +} + +/// Fault Proofs information. +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct FaultProofs { + /// The status of fault proofs. + pub status: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_chain_list_file() { + let chain_list = include_str!("../../../registry/etc/chainList.json"); + let chains: Vec = serde_json::from_str(chain_list).unwrap(); + let base_chain = chains.iter().find(|c| c.name == "Base").unwrap(); + assert_eq!(base_chain.chain_id, 8453); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/chains.rs b/kona/crates/protocol/genesis/src/superchain/chains.rs new file mode 100644 index 0000000000000..1166748ca4a44 --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/chains.rs @@ -0,0 +1,121 @@ +//! Contains the `Superchains` type. + +use alloc::vec::Vec; + +use crate::Superchain; + +/// A list of Hydrated Superchain Configs. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Superchains { + /// A list of superchain configs. + pub superchains: Vec, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use crate::{HardForkConfig, SuperchainConfig, SuperchainL1Info}; + use alloc::{string::ToString, vec}; + + #[test] + fn test_deny_unknown_fields_superchains() { + let raw: &str = r#" + { + "superchains": [ + { + "name": "Mainnet", + "config": { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + } + }, + "chains": [] + } + ], + "unknown_field": "unknown" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + fn test_superchains_serde() { + let raw: &str = r#" + { + "superchains": [ + { + "name": "Mainnet", + "config": { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + } + }, + "chains": [] + } + ] + } + "#; + + let superchains = Superchains { + superchains: vec![Superchain { + name: "Mainnet".to_string(), + config: SuperchainConfig { + name: "Mainnet".to_string(), + l1: SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }, + protocol_versions_addr: None, + superchain_config_addr: None, + op_contracts_manager_proxy_addr: None, + }, + chains: vec![], + }], + }; + + let deserialized: Superchains = serde_json::from_str(raw).unwrap(); + assert_eq!(deserialized, superchains); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/config.rs b/kona/crates/protocol/genesis/src/superchain/config.rs new file mode 100644 index 0000000000000..518e79d4401ed --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/config.rs @@ -0,0 +1,194 @@ +//! Contains the `SuperchainConfig` type. + +use crate::{HardForkConfig, SuperchainL1Info}; +use alloc::string::String; +use alloy_primitives::Address; + +/// A superchain configuration file format +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SuperchainConfig { + /// Superchain name (e.g. "Mainnet") + pub name: String, + /// Superchain L1 anchor information + pub l1: SuperchainL1Info, + /// Default hardforks timestamps. + pub hardforks: HardForkConfig, + /// Optional addresses for the superchain-wide default protocol versions contract. + #[cfg_attr(feature = "serde", serde(alias = "protocolVersionsAddr"))] + pub protocol_versions_addr: Option
, + /// Optional address for the superchain-wide default superchain config contract. + #[cfg_attr(feature = "serde", serde(alias = "superchainConfigAddr"))] + pub superchain_config_addr: Option
, + /// The op contracts manager proxy address. + #[cfg_attr(feature = "serde", serde(alias = "OPContractsManagerProxyAddr"))] + pub op_contracts_manager_proxy_addr: Option
, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use crate::{HardForkConfig, SuperchainL1Info}; + use alloc::string::ToString; + + #[test] + fn test_superchain_deserialize() { + let raw: &str = r#" + name = "Mainnet" + [l1] + chainId = 10 + publicRPC = "https://mainnet.rpc" + explorer = "https://mainnet.explorer" + [hardforks] + canyon_time = 1699981200 # Tue 14 Nov 2023 17:00:00 UTC + delta_time = 1703203200 # Fri 22 Dec 2023 00:00:00 UTC + ecotone_time = 1708534800 # Wed 21 Feb 2024 17:00:00 UTC + fjord_time = 1716998400 # Wed 29 May 2024 16:00:00 UTC + granite_time = 1723478400 # Mon Aug 12 16:00:00 UTC 2024 + holocene_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + "#; + + let superchain = SuperchainConfig { + name: "Mainnet".to_string(), + l1: SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }, + protocol_versions_addr: None, + superchain_config_addr: None, + op_contracts_manager_proxy_addr: None, + }; + let deserialized = toml::from_str::(raw).unwrap(); + assert_eq!(superchain, deserialized); + } + + #[test] + fn test_superchain_deserialize_new_hardfork_field_fail() { + let raw: &str = r#" + name = "Mainnet" + [l1] + chainId = 10 + publicRPC = "https://mainnet.rpc" + explorer = "https://mainnet.explorer" + [hardforks] + canyon_time = 1699981200 # Tue 14 Nov 2023 17:00:00 UTC + delta_time = 1703203200 # Fri 22 Dec 2023 00:00:00 UTC + ecotone_time = 1708534800 # Wed 21 Feb 2024 17:00:00 UTC + fjord_time = 1716998400 # Wed 29 May 2024 16:00:00 UTC + granite_time = 1723478400 # Mon Aug 12 16:00:00 UTC 2024 + holocene_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + new_field_time = 1732633200 # Tue Nov 26 15:00:00 UTC 2024 + "#; + toml::from_str::(raw).unwrap_err(); + } + + #[test] + fn test_deny_unknown_fields_sc_cfg() { + let raw: &str = r#" + { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + }, + "unknown_field": "unknown" + } + "#; + + let deserialized = serde_json::from_str::(raw).unwrap(); + let config = SuperchainConfig { + name: "Mainnet".to_string(), + l1: SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + ..Default::default() + }, + ..Default::default() + }; + assert_eq!(config, deserialized); + } + + #[test] + fn test_sc_cfg_serde() { + let raw: &str = r#" + { + "name": "Mainnet", + "l1": { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200 + } + } + "#; + + let config = SuperchainConfig { + name: "Mainnet".to_string(), + l1: SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: None, + isthmus_time: None, + jovian_time: None, + interop_time: None, + }, + protocol_versions_addr: None, + superchain_config_addr: None, + op_contracts_manager_proxy_addr: None, + }; + + let deserialized: SuperchainConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(config, deserialized); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/info.rs b/kona/crates/protocol/genesis/src/superchain/info.rs new file mode 100644 index 0000000000000..87b810e66329b --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/info.rs @@ -0,0 +1,60 @@ +//! Contains the superchain L1 information. + +use alloc::string::String; + +/// Superchain L1 anchor information +#[derive(Debug, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SuperchainL1Info { + /// L1 chain ID + #[cfg_attr(feature = "serde", serde(alias = "chainId"))] + pub chain_id: u64, + /// L1 chain public RPC endpoint + #[cfg_attr(feature = "serde", serde(alias = "publicRPC"))] + pub public_rpc: String, + /// L1 chain explorer RPC endpoint + pub explorer: String, +} + +#[cfg(test)] +#[cfg(feature = "serde")] +mod tests { + use super::*; + use crate::alloc::string::ToString; + + #[test] + fn test_deny_unknown_fields_sc_l1_info() { + let raw: &str = r#" + { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer", + "unknown_field": "unknown" + } + "#; + + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + fn test_sc_l1_info_serde() { + let raw: &str = r#" + { + "chainId": 10, + "publicRPC": "https://mainnet.rpc", + "explorer": "https://mainnet.explorer" + } + "#; + + let sc_l1_info = SuperchainL1Info { + chain_id: 10, + public_rpc: "https://mainnet.rpc".to_string(), + explorer: "https://mainnet.explorer".to_string(), + }; + + let sc_l1_info_serde = serde_json::from_str::(raw).unwrap(); + assert_eq!(sc_l1_info, sc_l1_info_serde); + } +} diff --git a/kona/crates/protocol/genesis/src/superchain/level.rs b/kona/crates/protocol/genesis/src/superchain/level.rs new file mode 100644 index 0000000000000..ea6bb66edc8bc --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/level.rs @@ -0,0 +1,20 @@ +//! Contains the superchain level. + +/// Level of integration with the superchain. +/// +/// See: +#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] +#[repr(u8)] +pub enum SuperchainLevel { + /// Frontier chains are chains with customizations beyond the + /// standard OP Stack configuration and are considered "advanced". + Frontier = 0, + /// A candidate for a standard chain. + #[default] + StandardCandidate = 1, + /// Standard chains don't have any customizations beyond the + /// standard OP Stack configuration and are considered "vanilla". + Standard = 2, +} diff --git a/kona/crates/protocol/genesis/src/superchain/mod.rs b/kona/crates/protocol/genesis/src/superchain/mod.rs new file mode 100644 index 0000000000000..139456b26414c --- /dev/null +++ b/kona/crates/protocol/genesis/src/superchain/mod.rs @@ -0,0 +1,19 @@ +//! Contains superchain-specific types. + +mod level; +pub use level::SuperchainLevel; + +mod chain; +pub use chain::Superchain; + +mod chains; +pub use chains::Superchains; + +mod config; +pub use config::SuperchainConfig; + +mod info; +pub use info::SuperchainL1Info; + +mod chain_list; +pub use chain_list::{Chain, ChainList, FaultProofs, SuperchainParent}; diff --git a/kona/crates/protocol/genesis/src/system/config.rs b/kona/crates/protocol/genesis/src/system/config.rs new file mode 100644 index 0000000000000..bbf94cb955935 --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/config.rs @@ -0,0 +1,584 @@ +//! Contains the [`SystemConfig`] type. + +use crate::{ + CONFIG_UPDATE_TOPIC, RollupConfig, SystemConfigLog, SystemConfigUpdateError, + SystemConfigUpdateKind, +}; +use alloy_consensus::{Eip658Value, Receipt}; +use alloy_primitives::{Address, B64, Log, U256}; + +/// System configuration. +#[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SystemConfig { + /// Batcher address + #[cfg_attr(feature = "serde", serde(rename = "batcherAddr"))] + pub batcher_address: Address, + /// Fee overhead value + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_u256_full"))] + pub overhead: U256, + /// Fee scalar value + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_u256_full"))] + pub scalar: U256, + /// Gas limit value + pub gas_limit: u64, + /// Base fee scalar value + pub base_fee_scalar: Option, + /// Blob base fee scalar value + pub blob_base_fee_scalar: Option, + /// EIP-1559 denominator + pub eip1559_denominator: Option, + /// EIP-1559 elasticity + pub eip1559_elasticity: Option, + /// The operator fee scalar (isthmus hardfork) + pub operator_fee_scalar: Option, + /// The operator fee constant (isthmus hardfork) + pub operator_fee_constant: Option, + /// Min base fee (jovian hardfork) + /// Note: according to the [spec](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/system-config.md#initialization), as long as the MinBaseFee is not + /// explicitly set, the default value (`0`) will be systematically applied. + pub min_base_fee: Option, + /// DA footprint gas scalar (Jovian hardfork) + /// Note: according to the [spec](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/system-config.md#initialization), as long as the DAFootprintGasScalar is not + /// explicitly set, the default value (`400`) will be systematically applied. + pub da_footprint_gas_scalar: Option, +} + +/// Custom EIP-1559 parameter decoding is needed here for holocene encoding. +/// +/// This is used by the Optimism monorepo [here][here]. +/// +/// [here]: https://github.com/ethereum-optimism/optimism/blob/cf28bffc7d880292794f53bb76bfc4df7898307b/op-service/eth/types.go#L519 +#[cfg(feature = "serde")] +impl<'a> serde::Deserialize<'a> for SystemConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'a>, + { + use alloy_primitives::B256; + // An alias struct that is identical to `SystemConfig`. + // We use the alias to decode the eip1559 params as their u32 values. + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + #[serde(deny_unknown_fields)] + struct SystemConfigAlias { + #[serde(rename = "batcherAddress", alias = "batcherAddr")] + batcher_address: Address, + overhead: U256, + scalar: U256, + gas_limit: u64, + base_fee_scalar: Option, + blob_base_fee_scalar: Option, + eip1559_params: Option, + eip1559_denominator: Option, + eip1559_elasticity: Option, + operator_fee_params: Option, + operator_fee_scalar: Option, + operator_fee_constant: Option, + min_base_fee: Option, + da_footprint_gas_scalar: Option, + } + + let mut alias = SystemConfigAlias::deserialize(deserializer)?; + if let Some(params) = alias.eip1559_params { + alias.eip1559_denominator = + Some(u32::from_be_bytes(params.as_slice().get(0..4).unwrap().try_into().unwrap())); + alias.eip1559_elasticity = + Some(u32::from_be_bytes(params.as_slice().get(4..8).unwrap().try_into().unwrap())); + } + if let Some(params) = alias.operator_fee_params { + alias.operator_fee_scalar = Some(u32::from_be_bytes( + params.as_slice().get(20..24).unwrap().try_into().unwrap(), + )); + alias.operator_fee_constant = Some(u64::from_be_bytes( + params.as_slice().get(24..32).unwrap().try_into().unwrap(), + )); + } + + Ok(Self { + batcher_address: alias.batcher_address, + overhead: alias.overhead, + scalar: alias.scalar, + gas_limit: alias.gas_limit, + base_fee_scalar: alias.base_fee_scalar, + blob_base_fee_scalar: alias.blob_base_fee_scalar, + eip1559_denominator: alias.eip1559_denominator, + eip1559_elasticity: alias.eip1559_elasticity, + operator_fee_scalar: alias.operator_fee_scalar, + operator_fee_constant: alias.operator_fee_constant, + min_base_fee: alias.min_base_fee, + da_footprint_gas_scalar: alias.da_footprint_gas_scalar, + }) + } +} + +impl SystemConfig { + /// Filters all L1 receipts to find config updates and applies the config updates. + /// + /// Returns `true` if any config updates were applied, `false` otherwise. + pub fn update_with_receipts( + &mut self, + receipts: &[Receipt], + l1_system_config_address: Address, + ecotone_active: bool, + ) -> Result { + let mut updated = false; + for receipt in receipts { + if Eip658Value::Eip658(false) == receipt.status { + continue; + } + + receipt.logs.iter().try_for_each(|log| { + let topics = log.topics(); + if log.address == l1_system_config_address && + !topics.is_empty() && + topics[0] == CONFIG_UPDATE_TOPIC + { + // Safety: Error is bubbled up by the trailing `?` + self.process_config_update_log(log, ecotone_active)?; + updated = true; + } + Ok::<(), SystemConfigUpdateError>(()) + })?; + } + Ok(updated) + } + + /// Returns the eip1559 parameters from a [SystemConfig] encoded as a [B64]. + pub fn eip_1559_params( + &self, + rollup_config: &RollupConfig, + parent_timestamp: u64, + next_timestamp: u64, + ) -> Option { + let is_holocene = rollup_config.is_holocene_active(next_timestamp); + + // For the first holocene block, a zero'd out B64 is returned to signal the + // execution layer to use the canyon base fee parameters. Else, the system + // config's eip1559 parameters are encoded as a B64. + if is_holocene && !rollup_config.is_holocene_active(parent_timestamp) { + Some(B64::ZERO) + } else { + is_holocene.then_some(B64::from_slice( + &[ + self.eip1559_denominator.unwrap_or_default().to_be_bytes(), + self.eip1559_elasticity.unwrap_or_default().to_be_bytes(), + ] + .concat(), + )) + } + } + + /// Decodes an EVM log entry emitted by the system config contract and applies it as a + /// [SystemConfig] change. + /// + /// Parse log data for: + /// + /// ```text + /// event ConfigUpdate( + /// uint256 indexed version, + /// UpdateType indexed updateType, + /// bytes data + /// ); + /// ``` + fn process_config_update_log( + &mut self, + log: &Log, + ecotone_active: bool, + ) -> Result { + // Construct the system config log from the log. + let log = SystemConfigLog::new(log.clone(), ecotone_active); + + // Construct the update type from the log. + let update = log.build()?; + + // Apply the update to the system config. + update.apply(self); + + // Return the update type. + Ok(update.kind()) + } +} + +/// Compatibility helper function to serialize a [`U256`] as a [`B256`]. +/// +/// [`B256`]: alloy_primitives::B256 +#[cfg(feature = "serde")] +fn serialize_u256_full(ts: &U256, ser: S) -> Result +where + S: serde::Serializer, +{ + use serde::Serialize; + + alloy_primitives::B256::from(ts.to_be_bytes::<32>()).serialize(ser) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, HardForkConfig}; + use alloc::vec; + use alloy_primitives::{B256, LogData, address, b256, hex}; + + #[test] + #[cfg(feature = "serde")] + fn test_system_config_da_footprint_gas_scalar() { + let raw = r#"{ + "batcherAddress": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "eip1559Params": "0x000000ab000000cd", + "daFootprintGasScalar": 10 + }"#; + let system_config: SystemConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(system_config.da_footprint_gas_scalar, Some(10), "da_footprint_gas_scalar"); + } + + #[test] + #[cfg(feature = "serde")] + fn test_system_config_eip1559_params() { + let raw = r#"{ + "batcherAddress": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "eip1559Params": "0x000000ab000000cd" + }"#; + let system_config: SystemConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(system_config.eip1559_denominator, Some(0xab_u32), "eip1559_denominator"); + assert_eq!(system_config.eip1559_elasticity, Some(0xcd_u32), "eip1559_elasticity"); + } + + #[test] + #[cfg(feature = "serde")] + fn test_system_config_serde() { + let raw = r#"{ + "batcherAddr": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000 + }"#; + let expected = SystemConfig { + batcher_address: address!("6887246668a3b87F54DeB3b94Ba47a6f63F32985"), + overhead: U256::from(0xbc), + scalar: U256::from(0xa6fe0), + gas_limit: 30000000, + ..Default::default() + }; + + let deserialized: SystemConfig = serde_json::from_str(raw).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + #[cfg(feature = "serde")] + fn test_system_config_unknown_field() { + let raw = r#"{ + "batcherAddr": "0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "unknown": 0 + }"#; + let err = serde_json::from_str::(raw).unwrap_err(); + assert_eq!(err.classify(), serde_json::error::Category::Data); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_system_config() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + SystemConfig::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + } + + #[test] + fn test_eip_1559_params_from_system_config_none() { + let rollup_config = RollupConfig::default(); + let sys_config = SystemConfig::default(); + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 0), None); + } + + #[test] + fn test_eip_1559_params_from_system_config_some() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let sys_config = SystemConfig { + eip1559_denominator: Some(1), + eip1559_elasticity: None, + ..Default::default() + }; + let expected = Some(B64::from_slice(&[1u32.to_be_bytes(), 0u32.to_be_bytes()].concat())); + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 0), expected); + } + + #[test] + fn test_eip_1559_params_from_system_config() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let sys_config = SystemConfig { + eip1559_denominator: Some(1), + eip1559_elasticity: Some(2), + ..Default::default() + }; + let expected = Some(B64::from_slice(&[1u32.to_be_bytes(), 2u32.to_be_bytes()].concat())); + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 0), expected); + } + + #[test] + fn test_default_eip_1559_params_from_system_config() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let sys_config = SystemConfig { + eip1559_denominator: None, + eip1559_elasticity: None, + ..Default::default() + }; + let expected = Some(B64::ZERO); + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 0), expected); + } + + #[test] + fn test_default_eip_1559_params_from_system_config_pre_holocene() { + let rollup_config = RollupConfig::default(); + let sys_config = SystemConfig { + eip1559_denominator: Some(1), + eip1559_elasticity: Some(2), + ..Default::default() + }; + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 0), None); + } + + #[test] + fn test_default_eip_1559_params_first_block_holocene() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(2), ..Default::default() }, + ..Default::default() + }; + let sys_config = SystemConfig { + eip1559_denominator: Some(1), + eip1559_elasticity: Some(2), + ..Default::default() + }; + assert_eq!(sys_config.eip_1559_params(&rollup_config, 0, 2), Some(B64::ZERO)); + } + + #[test] + fn test_system_config_update_with_receipts_unchanged() { + let mut system_config = SystemConfig::default(); + let receipts = vec![]; + let l1_system_config_address = Address::ZERO; + let ecotone_active = false; + + let updated = system_config + .update_with_receipts(&receipts, l1_system_config_address, ecotone_active) + .unwrap(); + assert!(!updated); + + assert_eq!(system_config, SystemConfig::default()); + } + + #[test] + fn test_system_config_update_with_receipts_batcher_address() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000000"); + let mut system_config = SystemConfig::default(); + let l1_system_config_address = Address::ZERO; + let ecotone_active = false; + + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let receipt = Receipt { + logs: vec![update_log], + status: Eip658Value::Eip658(true), + cumulative_gas_used: 0, + }; + + let updated = system_config + .update_with_receipts(&[receipt], l1_system_config_address, ecotone_active) + .unwrap(); + assert!(updated); + + assert_eq!( + system_config.batcher_address, + address!("000000000000000000000000000000000000bEEF"), + ); + } + + #[test] + fn test_system_config_update_batcher_log() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000000"); + + let mut system_config = SystemConfig::default(); + + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + // Update the batcher address. + system_config.process_config_update_log(&update_log, false).unwrap(); + + assert_eq!( + system_config.batcher_address, + address!("000000000000000000000000000000000000bEEF") + ); + } + + #[test] + fn test_system_config_update_gas_config_log() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000001"); + + let mut system_config = SystemConfig::default(); + + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + // Update the batcher address. + system_config.process_config_update_log(&update_log, false).unwrap(); + + assert_eq!(system_config.overhead, U256::from(0xbabe)); + assert_eq!(system_config.scalar, U256::from(0xbeef)); + } + + #[test] + fn test_system_config_update_gas_config_log_ecotone() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000001"); + + let mut system_config = SystemConfig::default(); + + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + // Update the gas limit. + system_config.process_config_update_log(&update_log, true).unwrap(); + + assert_eq!(system_config.overhead, U256::from(0)); + assert_eq!(system_config.scalar, U256::from(0xbeef)); + } + + #[test] + fn test_system_config_update_gas_limit_log() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000002"); + + let mut system_config = SystemConfig::default(); + + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + // Update the gas limit. + system_config.process_config_update_log(&update_log, false).unwrap(); + + assert_eq!(system_config.gas_limit, 0xbeef_u64); + } + + #[test] + fn test_system_config_update_eip1559_params_log() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000004"); + + let mut system_config = SystemConfig::default(); + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + // Update the EIP-1559 parameters. + system_config.process_config_update_log(&update_log, false).unwrap(); + + assert_eq!(system_config.eip1559_denominator, Some(0xbabe_u32)); + assert_eq!(system_config.eip1559_elasticity, Some(0xbeef_u32)); + } + + #[test] + fn test_system_config_update_operator_fee_log() { + const UPDATE_TYPE: B256 = + b256!("0000000000000000000000000000000000000000000000000000000000000005"); + + let mut system_config = SystemConfig::default(); + let update_log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + UPDATE_TYPE, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000babe000000000000beef").into() + ) + }; + + // Update the operator fee. + system_config.process_config_update_log(&update_log, false).unwrap(); + + assert_eq!(system_config.operator_fee_scalar, Some(0xbabe_u32)); + assert_eq!(system_config.operator_fee_constant, Some(0xbeef_u64)); + } +} diff --git a/kona/crates/protocol/genesis/src/system/errors.rs b/kona/crates/protocol/genesis/src/system/errors.rs new file mode 100644 index 0000000000000..c7992d6be0dd9 --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/errors.rs @@ -0,0 +1,257 @@ +//! Contains error types for system config updates. + +use alloy_primitives::B256; +use derive_more::From; + +/// An error for processing the [crate::SystemConfig] update log. +#[derive(Debug, From, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum SystemConfigUpdateError { + /// An error occurred while processing the update log. + #[error("Log processing error: {0}")] + LogProcessing(LogProcessingError), + /// A batcher update error. + #[error("Batcher update error: {0}")] + Batcher(BatcherUpdateError), + /// A gas config update error. + #[error("Gas config update error: {0}")] + GasConfig(GasConfigUpdateError), + /// A gas limit update error. + #[error("Gas limit update error: {0}")] + GasLimit(GasLimitUpdateError), + /// An EIP-1559 parameter update error. + #[error("EIP-1559 parameter update error: {0}")] + Eip1559(EIP1559UpdateError), + /// An operator fee parameter update error. + #[error("Operator fee parameter update error: {0}")] + OperatorFee(OperatorFeeUpdateError), + /// An unsafe block signer update error. + #[error("Unsafe block signer update error: {0}")] + UnsafeBlockSigner(UnsafeBlockSignerUpdateError), + /// A min base fee parameter update error. + #[error("Min base fee parameter update error: {0}")] + MinBaseFee(MinBaseFeeUpdateError), + /// A da footprint gas scalar update error. + #[error("DA footprint gas scalar update error: {0}")] + DaFootprintGasScalar(DaFootprintGasScalarUpdateError), +} + +/// An error occurred while processing the update log. +#[derive(Debug, From, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum LogProcessingError { + /// Received an incorrect number of log topics. + #[error("Invalid config update log: invalid topic length: {0}")] + InvalidTopicLen(usize), + /// The log topic is invalid. + #[error("Invalid config update log: invalid topic")] + InvalidTopic, + /// The config update log version is unsupported. + #[error("Invalid config update log: unsupported version: {0}")] + UnsupportedVersion(B256), + /// Failed to decode the update type from the config update log. + #[error("Failed to decode config update log: update type")] + UpdateTypeDecodingError, + /// An invalid system config update type. + #[error("Invalid system config update type: {0}")] + InvalidSystemConfigUpdateType(u64), +} + +/// An error for updating the batcher address on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum BatcherUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the batcher update log. + #[error("Failed to decode batcher update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the batcher update log. + #[error("Failed to decode batcher update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the batcher address argument from the batcher update log. + #[error("Failed to decode batcher update log: batcher address")] + BatcherAddressDecodingError, +} + +/// An error for updating the unsafe block signer address on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum UnsafeBlockSignerUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the update log. + #[error("Failed to decode unsafe block signer update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the update log. + #[error("Failed to decode unsafe block signer update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the unsafe block signer address argument from the update log. + #[error("Failed to decode unsafe block signer update log: unsafe block signer address")] + UnsafeBlockSignerAddressDecodingError, +} + +/// An error for updating the gas config on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum GasConfigUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the gas config update log. + #[error("Failed to decode gas config update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the gas config update log. + #[error("Failed to decode gas config update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the overhead argument from the gas config update log. + #[error("Failed to decode gas config update log: overhead")] + OverheadDecodingError, + /// Failed to decode the scalar argument from the gas config update log. + #[error("Failed to decode gas config update log: scalar")] + ScalarDecodingError, +} + +/// An error for updating the min base fee on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum MinBaseFeeUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the min base fee update log. + #[error("Failed to decode gas limit update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the min base fee update log. + #[error("Failed to decode gas limit update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the min base fee argument from the min base fee update log. + #[error("Failed to decode min base fee update log: min base fee")] + MinBaseFeeDecodingError, +} + +/// An error for updating the da footprint gas scalar on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum DaFootprintGasScalarUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the min base fee update log. + #[error("Failed to decode gas limit update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the min base fee update log. + #[error("Failed to decode gas limit update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the da footprint gas scalar argument from the da footprint gas scalar + /// update log. + #[error("Failed to decode da footprint gas scalar update log: da footprint gas scalar")] + DaFootprintGasScalarDecodingError, +} + +/// An error for updating the gas limit on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum GasLimitUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the gas limit update log. + #[error("Failed to decode gas limit update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the gas limit update log. + #[error("Failed to decode gas limit update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the gas limit argument from the gas limit update log. + #[error("Failed to decode gas limit update log: gas limit")] + GasLimitDecodingError, +} + +/// An error for updating the EIP-1559 parameters on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum EIP1559UpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the eip 1559 update log. + #[error("Failed to decode eip1559 parameter update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the eip 1559 update log. + #[error("Failed to decode eip1559 parameter update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the eip1559 params argument from the eip 1559 update log. + #[error("Failed to decode eip1559 parameter update log: eip1559 parameters")] + EIP1559DecodingError, +} + +/// An error for updating the operator fee parameters on the [crate::SystemConfig]. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum OperatorFeeUpdateError { + /// Invalid data length. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLen(usize), + /// Failed to decode the data pointer argument from the operator fee update log. + #[error("Failed to decode operator fee parameter update log: data pointer")] + PointerDecodingError, + /// The data pointer is invalid. + #[error("Invalid config update log: invalid data pointer: {0}")] + InvalidDataPointer(u64), + /// Failed to decode the data length argument from the operator fee update log. + #[error("Failed to decode operator fee parameter update log: data length")] + LengthDecodingError, + /// The data length is invalid. + #[error("Invalid config update log: invalid data length: {0}")] + InvalidDataLength(u64), + /// Failed to decode the scalar argument from the update log. + #[error("Failed to decode operator fee parameter update log: scalar")] + ScalarDecodingError, + /// Failed to decode the constant argument from the update log. + #[error("Failed to decode operator fee parameter update log: constant")] + ConstantDecodingError, +} diff --git a/kona/crates/protocol/genesis/src/system/kind.rs b/kona/crates/protocol/genesis/src/system/kind.rs new file mode 100644 index 0000000000000..5c3e5b7357cc3 --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/kind.rs @@ -0,0 +1,24 @@ +//! Contains the kind of system config update. + +/// Represents type of update to the system config. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, derive_more::TryFrom)] +#[try_from(repr)] +#[repr(u64)] +pub enum SystemConfigUpdateKind { + /// Batcher update type + Batcher = 0, + /// Gas config update type + GasConfig = 1, + /// Gas limit update type + GasLimit = 2, + /// Unsafe block signer update type + UnsafeBlockSigner = 3, + /// EIP-1559 parameters update type + Eip1559 = 4, + /// Operator fee parameter update + OperatorFee = 5, + /// Min base fee parameter update + MinBaseFee = 6, + /// DA footprint gas scalar update type + DaFootprintGasScalar = 7, +} diff --git a/kona/crates/protocol/genesis/src/system/log.rs b/kona/crates/protocol/genesis/src/system/log.rs new file mode 100644 index 0000000000000..fcf5a7db81850 --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/log.rs @@ -0,0 +1,113 @@ +//! Contains the [`SystemConfigLog`]. + +use alloy_primitives::Log; + +use crate::{ + BatcherUpdate, CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC, Eip1559Update, + GasConfigUpdate, GasLimitUpdate, LogProcessingError, OperatorFeeUpdate, SystemConfigUpdate, + SystemConfigUpdateError, SystemConfigUpdateKind, UnsafeBlockSignerUpdate, + updates::{DaFootprintGasScalarUpdate, MinBaseFeeUpdate}, +}; + +/// The system config log is an EVM log entry emitted +/// by the system contract to update the system config. +/// +/// The log data is formatted as follows: +/// ```text +/// event ConfigUpdate( +/// uint256 indexed version, +/// UpdateType indexed updateType, +/// bytes data +/// ); +/// ``` +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SystemConfigLog { + /// The log. + pub log: Log, + /// Whether ecotone is active. + pub ecotone_active: bool, +} + +impl SystemConfigLog { + /// Constructs a new system config update. + pub const fn new(log: Log, ecotone_active: bool) -> Self { + Self { log, ecotone_active } + } + + /// Validate the log topic. + pub fn validate_topic(&self) -> Result<(), LogProcessingError> { + if self.log.topics().len() < 3 { + return Err(LogProcessingError::InvalidTopicLen(self.log.topics().len())); + } + if self.log.topics()[0] != CONFIG_UPDATE_TOPIC { + return Err(LogProcessingError::InvalidTopic); + } + Ok(()) + } + + /// Validate the config update version. + pub fn validate_version(&self) -> Result<(), LogProcessingError> { + let version = self.log.topics()[1]; + if version != CONFIG_UPDATE_EVENT_VERSION_0 { + return Err(LogProcessingError::UnsupportedVersion(version)); + } + Ok(()) + } + + /// Extracts the update type from the log. + pub fn update_type(&self) -> Result { + if self.log.topics().len() < 3 { + return Err(LogProcessingError::InvalidTopicLen(self.log.topics().len()).into()); + } + let topic = self.log.topics()[2]; + let topic_bytes = <&[u8; 8]>::try_from(&topic.as_slice()[24..]) + .map_err(|_| LogProcessingError::UpdateTypeDecodingError)?; + let ty = u64::from_be_bytes(*topic_bytes); + ty.try_into().map_err(|_| { + SystemConfigUpdateError::LogProcessing( + LogProcessingError::InvalidSystemConfigUpdateType(ty), + ) + }) + } + + /// Builds the [`SystemConfigUpdate`] from the log. + pub fn build(&self) -> Result { + self.validate_topic()?; + self.validate_version()?; + match self.update_type()? { + SystemConfigUpdateKind::Batcher => { + let update = BatcherUpdate::try_from(self)?; + Ok(SystemConfigUpdate::Batcher(update)) + } + SystemConfigUpdateKind::GasConfig => { + let update = GasConfigUpdate::try_from(self)?; + Ok(SystemConfigUpdate::GasConfig(update)) + } + SystemConfigUpdateKind::GasLimit => { + let update = GasLimitUpdate::try_from(self)?; + Ok(SystemConfigUpdate::GasLimit(update)) + } + SystemConfigUpdateKind::Eip1559 => { + let update = Eip1559Update::try_from(self)?; + Ok(SystemConfigUpdate::Eip1559(update)) + } + SystemConfigUpdateKind::OperatorFee => { + let update = OperatorFeeUpdate::try_from(self)?; + Ok(SystemConfigUpdate::OperatorFee(update)) + } + SystemConfigUpdateKind::UnsafeBlockSigner => { + let update = UnsafeBlockSignerUpdate::try_from(self)?; + Ok(SystemConfigUpdate::UnsafeBlockSigner(update)) + } + SystemConfigUpdateKind::MinBaseFee => { + let update = MinBaseFeeUpdate::try_from(self)?; + Ok(SystemConfigUpdate::MinBaseFee(update)) + } + SystemConfigUpdateKind::DaFootprintGasScalar => { + let update = DaFootprintGasScalarUpdate::try_from(self)?; + Ok(SystemConfigUpdate::DaFootprintGasScalar(update)) + } + } + } +} diff --git a/kona/crates/protocol/genesis/src/system/mod.rs b/kona/crates/protocol/genesis/src/system/mod.rs new file mode 100644 index 0000000000000..eb706c60ad30d --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/mod.rs @@ -0,0 +1,29 @@ +//! Contains types related to the [`SystemConfig`]. + +use alloy_primitives::{B256, b256}; + +/// `keccak256("ConfigUpdate(uint256,uint8,bytes)")` +pub const CONFIG_UPDATE_TOPIC: B256 = + b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + +/// The initial version of the system config event log. +pub const CONFIG_UPDATE_EVENT_VERSION_0: B256 = B256::ZERO; + +mod config; +pub use config::SystemConfig; + +mod log; +pub use log::SystemConfigLog; + +mod update; +pub use update::SystemConfigUpdate; + +mod kind; +pub use kind::SystemConfigUpdateKind; + +mod errors; +pub use errors::{ + BatcherUpdateError, DaFootprintGasScalarUpdateError, EIP1559UpdateError, GasConfigUpdateError, + GasLimitUpdateError, LogProcessingError, MinBaseFeeUpdateError, OperatorFeeUpdateError, + SystemConfigUpdateError, UnsafeBlockSignerUpdateError, +}; diff --git a/kona/crates/protocol/genesis/src/system/update.rs b/kona/crates/protocol/genesis/src/system/update.rs new file mode 100644 index 0000000000000..b3a7dac74b04f --- /dev/null +++ b/kona/crates/protocol/genesis/src/system/update.rs @@ -0,0 +1,60 @@ +//! Contains the [`SystemConfigUpdate`]. + +use crate::{ + BatcherUpdate, Eip1559Update, GasConfigUpdate, GasLimitUpdate, OperatorFeeUpdate, SystemConfig, + SystemConfigUpdateKind, UnsafeBlockSignerUpdate, + updates::{DaFootprintGasScalarUpdate, MinBaseFeeUpdate}, +}; + +/// The system config update is an update +/// of type [`SystemConfigUpdateKind`]. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum SystemConfigUpdate { + /// The batcher update. + Batcher(BatcherUpdate), + /// The gas config update. + GasConfig(GasConfigUpdate), + /// The gas limit update. + GasLimit(GasLimitUpdate), + /// The unsafe block signer update. + UnsafeBlockSigner(UnsafeBlockSignerUpdate), + /// The EIP-1559 parameters update. + Eip1559(Eip1559Update), + /// The operator fee parameter update. + OperatorFee(OperatorFeeUpdate), + /// Min base fee parameter update. + MinBaseFee(MinBaseFeeUpdate), + /// DA footprint gas scalar update. + DaFootprintGasScalar(DaFootprintGasScalarUpdate), +} + +impl SystemConfigUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + match self { + Self::Batcher(update) => update.apply(config), + Self::GasConfig(update) => update.apply(config), + Self::GasLimit(update) => update.apply(config), + Self::UnsafeBlockSigner(_) => { /* Ignored in derivation */ } + Self::Eip1559(update) => update.apply(config), + Self::OperatorFee(update) => update.apply(config), + Self::MinBaseFee(update) => update.apply(config), + Self::DaFootprintGasScalar(update) => update.apply(config), + } + } + + /// Returns the update kind. + pub const fn kind(&self) -> SystemConfigUpdateKind { + match self { + Self::Batcher(_) => SystemConfigUpdateKind::Batcher, + Self::GasConfig(_) => SystemConfigUpdateKind::GasConfig, + Self::GasLimit(_) => SystemConfigUpdateKind::GasLimit, + Self::UnsafeBlockSigner(_) => SystemConfigUpdateKind::UnsafeBlockSigner, + Self::Eip1559(_) => SystemConfigUpdateKind::Eip1559, + Self::OperatorFee(_) => SystemConfigUpdateKind::OperatorFee, + Self::MinBaseFee(_) => SystemConfigUpdateKind::MinBaseFee, + Self::DaFootprintGasScalar(_) => SystemConfigUpdateKind::DaFootprintGasScalar, + } + } +} diff --git a/kona/crates/protocol/genesis/src/updates/batcher.rs b/kona/crates/protocol/genesis/src/updates/batcher.rs new file mode 100644 index 0000000000000..97164cd21279c --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/batcher.rs @@ -0,0 +1,185 @@ +//! The batcher update type. + +use alloy_primitives::{Address, LogData}; +use alloy_sol_types::{SolType, sol}; + +use crate::{ + BatcherUpdateError, SystemConfig, SystemConfigLog, + updates::common::{ValidationError, validate_update_data}, +}; + +/// The batcher update type. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BatcherUpdate { + /// The batcher address. + pub batcher_address: Address, +} + +impl BatcherUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + config.batcher_address = self.batcher_address; + } +} + +impl TryFrom<&SystemConfigLog> for BatcherUpdate { + type Error = BatcherUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + + let validated = validate_update_data(data).map_err(|e| match e { + ValidationError::InvalidDataLen(_expected, actual) => { + BatcherUpdateError::InvalidDataLen(actual) + } + ValidationError::PointerDecodingError => BatcherUpdateError::PointerDecodingError, + ValidationError::InvalidDataPointer(pointer) => { + BatcherUpdateError::InvalidDataPointer(pointer) + } + ValidationError::LengthDecodingError => BatcherUpdateError::LengthDecodingError, + ValidationError::InvalidDataLength(length) => { + BatcherUpdateError::InvalidDataLength(length) + } + })?; + + let Ok(batcher_address) = ::abi_decode_validate(validated.payload()) else { + return Err(BatcherUpdateError::BatcherAddressDecodingError); + }; + + Ok(Self { batcher_address }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{B256, Bytes, Log, LogData, address, hex}; + + #[test] + fn test_batcher_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = BatcherUpdate::try_from(&system_log).unwrap(); + assert_eq!(update.batcher_address, address!("000000000000000000000000000000000000bEEF"),); + } + + #[test] + fn test_batcher_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_batcher_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::PointerDecodingError); + } + + #[test] + fn test_batcher_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_batcher_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::LengthDecodingError); + } + + #[test] + fn test_batcher_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_batcher_update_batcher_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = BatcherUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, BatcherUpdateError::BatcherAddressDecodingError); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/common.rs b/kona/crates/protocol/genesis/src/updates/common.rs new file mode 100644 index 0000000000000..959aee4ece0d8 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/common.rs @@ -0,0 +1,83 @@ +//! Common validation utilities for SystemConfig updates. +//! +//! This module provides shared validation logic for decoding SystemConfigLog data +//! that is used across multiple update types. + +use alloy_sol_types::{SolType, sol}; + +/// The expected data length for a standard SystemConfigLog update. +pub(crate) const STANDARD_UPDATE_DATA_LEN: usize = 96; + +/// The expected pointer value for a standard SystemConfigLog update. +pub(crate) const EXPECTED_POINTER: u64 = 32; + +/// The expected data length value for a standard SystemConfigLog update. +pub(crate) const EXPECTED_DATA_LENGTH: u64 = 32; + +/// Validated SystemConfig update data. +/// +/// After validation, this struct provides access to the validated pointer, length, +/// and the payload data starting at byte offset 64. +pub(crate) struct ValidatedUpdateData<'a> { + /// The full data bytes. + data: &'a alloy_primitives::Bytes, +} + +impl<'a> ValidatedUpdateData<'a> { + /// Returns the payload slice (data starting from byte 64). + #[inline] + pub(crate) fn payload(&self) -> &[u8] { + &self.data[64..] + } +} + +/// Common validation errors for SystemConfig updates. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ValidationError { + /// Invalid data length. Contains (expected, actual). + InvalidDataLen(usize, usize), + /// Failed to decode the pointer. + PointerDecodingError, + /// Invalid pointer value. Contains the actual value. + InvalidDataPointer(u64), + /// Failed to decode the length. + LengthDecodingError, + /// Invalid data length value. Contains the actual value. + InvalidDataLength(u64), +} + +/// Validates the common structure of a SystemConfig update log data. +/// +/// This function performs the following validations: +/// 1. Checks that the data length is exactly 96 bytes +/// 2. Decodes and validates the pointer (must be 32) +/// 3. Decodes and validates the data length field (must be 32) +/// +/// # Returns +/// +/// Returns a `ValidatedUpdateData` containing the validated fields and original data, +/// or a `ValidationError` if any validation fails. +pub(crate) fn validate_update_data( + data: &alloy_primitives::Bytes, +) -> Result, ValidationError> { + // Validate total data length + if data.len() != STANDARD_UPDATE_DATA_LEN { + return Err(ValidationError::InvalidDataLen(STANDARD_UPDATE_DATA_LEN, data.len())); + } + + // Decode and validate pointer + let pointer = ::abi_decode_validate(&data[0..32]) + .map_err(|_| ValidationError::PointerDecodingError)?; + if pointer != EXPECTED_POINTER { + return Err(ValidationError::InvalidDataPointer(pointer)); + } + + // Decode and validate length + let length = ::abi_decode_validate(&data[32..64]) + .map_err(|_| ValidationError::LengthDecodingError)?; + if length != EXPECTED_DATA_LENGTH { + return Err(ValidationError::InvalidDataLength(length)); + } + + Ok(ValidatedUpdateData { data }) +} diff --git a/kona/crates/protocol/genesis/src/updates/da_footprint_gas_scalar.rs b/kona/crates/protocol/genesis/src/updates/da_footprint_gas_scalar.rs new file mode 100644 index 0000000000000..8b5b388f2410b --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/da_footprint_gas_scalar.rs @@ -0,0 +1,198 @@ +//! The da footprint gas scalar update type. + +use alloy_primitives::LogData; +use alloy_sol_types::{SolType, sol}; + +use crate::{SystemConfig, SystemConfigLog, system::DaFootprintGasScalarUpdateError}; + +/// The da footprint gas scalar update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DaFootprintGasScalarUpdate { + /// The da footprint gas scalar. + pub da_footprint_gas_scalar: u16, +} + +impl DaFootprintGasScalarUpdate { + /// The default DA footprint gas scalar + /// + pub const DEFAULT_DA_FOOTPRINT_GAS_SCALAR: u16 = 400; + + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + let mut da_footprint_gas_scalar = self.da_footprint_gas_scalar; + + // If the da footprint gas scalar is 0, use the default value + // + if da_footprint_gas_scalar == 0 { + da_footprint_gas_scalar = Self::DEFAULT_DA_FOOTPRINT_GAS_SCALAR; + }; + + config.da_footprint_gas_scalar = Some(da_footprint_gas_scalar); + } +} + +impl TryFrom<&SystemConfigLog> for DaFootprintGasScalarUpdate { + type Error = DaFootprintGasScalarUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + if data.len() != 96 { + return Err(DaFootprintGasScalarUpdateError::InvalidDataLen(data.len())); + } + + let Ok(pointer) = ::abi_decode_validate(&data[0..32]) else { + return Err(DaFootprintGasScalarUpdateError::PointerDecodingError); + }; + if pointer != 32 { + return Err(DaFootprintGasScalarUpdateError::InvalidDataPointer(pointer)); + } + + let Ok(length) = ::abi_decode_validate(&data[32..64]) else { + return Err(DaFootprintGasScalarUpdateError::LengthDecodingError); + }; + if length != 32 { + return Err(DaFootprintGasScalarUpdateError::InvalidDataLength(length)); + } + + let Ok(da_footprint_gas_scalar) = ::abi_decode_validate(&data[64..96]) else { + return Err(DaFootprintGasScalarUpdateError::DaFootprintGasScalarDecodingError); + }; + + Ok(Self { da_footprint_gas_scalar }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex}; + + #[test] + fn test_da_footprint_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap(); + + assert_eq!(update.da_footprint_gas_scalar, 0xbeef_u16); + } + + #[test] + fn test_da_footprint_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_da_footprint_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::PointerDecodingError); + } + + #[test] + fn test_da_footprint_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_da_footprint_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::LengthDecodingError); + } + + #[test] + fn test_da_footprint_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_da_footprint_update_da_footprint_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = DaFootprintGasScalarUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, DaFootprintGasScalarUpdateError::DaFootprintGasScalarDecodingError); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/eip1559.rs b/kona/crates/protocol/genesis/src/updates/eip1559.rs new file mode 100644 index 0000000000000..900e090f729b6 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/eip1559.rs @@ -0,0 +1,193 @@ +//! The EIP-1559 update type. + +use alloy_primitives::LogData; +use alloy_sol_types::{SolType, sol}; + +use crate::{ + EIP1559UpdateError, SystemConfig, SystemConfigLog, + updates::common::{ValidationError, validate_update_data}, +}; + +/// The EIP-1559 update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Eip1559Update { + /// The EIP-1559 denominator. + pub eip1559_denominator: u32, + /// The EIP-1559 elasticity multiplier. + pub eip1559_elasticity: u32, +} + +impl Eip1559Update { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + config.eip1559_denominator = Some(self.eip1559_denominator); + config.eip1559_elasticity = Some(self.eip1559_elasticity); + } +} + +impl TryFrom<&SystemConfigLog> for Eip1559Update { + type Error = EIP1559UpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + + let validated = validate_update_data(data).map_err(|e| match e { + ValidationError::InvalidDataLen(_expected, actual) => { + EIP1559UpdateError::InvalidDataLen(actual) + } + ValidationError::PointerDecodingError => EIP1559UpdateError::PointerDecodingError, + ValidationError::InvalidDataPointer(pointer) => { + EIP1559UpdateError::InvalidDataPointer(pointer) + } + ValidationError::LengthDecodingError => EIP1559UpdateError::LengthDecodingError, + ValidationError::InvalidDataLength(length) => { + EIP1559UpdateError::InvalidDataLength(length) + } + })?; + + let Ok(eip1559_params) = ::abi_decode_validate(validated.payload()) else { + return Err(EIP1559UpdateError::EIP1559DecodingError); + }; + + Ok(Self { + eip1559_denominator: (eip1559_params >> 32) as u32, + eip1559_elasticity: eip1559_params as u32, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex}; + + #[test] + fn test_eip1559_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = Eip1559Update::try_from(&system_log).unwrap(); + + assert_eq!(update.eip1559_denominator, 0xbabe_u32); + assert_eq!(update.eip1559_elasticity, 0xbeef_u32); + } + + #[test] + fn test_eip1559_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_eip1559_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::PointerDecodingError); + } + + #[test] + fn test_eip1559_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_eip1559_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::LengthDecodingError); + } + + #[test] + fn test_eip1559_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_eip1559_update_eip1559_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = Eip1559Update::try_from(&system_log).unwrap_err(); + assert_eq!(err, EIP1559UpdateError::EIP1559DecodingError); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/gas_config.rs b/kona/crates/protocol/genesis/src/updates/gas_config.rs new file mode 100644 index 0000000000000..0f019736a1803 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/gas_config.rs @@ -0,0 +1,224 @@ +//! The gas config update type. + +use alloy_primitives::{LogData, U256}; +use alloy_sol_types::{SolType, sol}; + +use crate::{GasConfigUpdateError, RollupConfig, SystemConfig, SystemConfigLog}; + +/// The gas config update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GasConfigUpdate { + /// The scalar. + pub scalar: Option, + /// The overhead. + pub overhead: Option, +} + +impl GasConfigUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + if let Some(scalar) = self.scalar { + config.scalar = scalar; + } + if let Some(overhead) = self.overhead { + config.overhead = overhead; + } + } +} + +impl TryFrom<&SystemConfigLog> for GasConfigUpdate { + type Error = GasConfigUpdateError; + + fn try_from(sys_log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &sys_log.log.data; + if data.len() != 128 { + return Err(GasConfigUpdateError::InvalidDataLen(data.len())); + } + + let Ok(pointer) = ::abi_decode_validate(&data[0..32]) else { + return Err(GasConfigUpdateError::PointerDecodingError); + }; + if pointer != 32 { + return Err(GasConfigUpdateError::InvalidDataPointer(pointer)); + } + + let Ok(length) = ::abi_decode_validate(&data[32..64]) else { + return Err(GasConfigUpdateError::LengthDecodingError); + }; + if length != 64 { + return Err(GasConfigUpdateError::InvalidDataLength(length)); + } + + let Ok(overhead) = ::abi_decode_validate(&data[64..96]) else { + return Err(GasConfigUpdateError::OverheadDecodingError); + }; + let Ok(scalar) = ::abi_decode_validate(&data[96..]) else { + return Err(GasConfigUpdateError::ScalarDecodingError); + }; + + if sys_log.ecotone_active && + RollupConfig::check_ecotone_l1_system_config_scalar(scalar.to_be_bytes()).is_err() + { + // ignore invalid scalars, retain the old system-config scalar + return Ok(Self::default()); + } + + // If ecotone is active, set the overhead to zero, otherwise set to the decoded value. + let overhead = if sys_log.ecotone_active { U256::ZERO } else { overhead }; + + Ok(Self { scalar: Some(scalar), overhead: Some(overhead) }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex, uint}; + + #[test] + fn test_gas_config_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = GasConfigUpdate::try_from(&system_log).unwrap(); + + assert_eq!(update.overhead, Some(uint!(0xbabe_U256))); + assert_eq!(update.scalar, Some(uint!(0xbeef_U256))); + } + + #[test] + fn test_gas_config_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = GasConfigUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasConfigUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_gas_config_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasConfigUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasConfigUpdateError::PointerDecodingError); + } + + #[test] + fn test_gas_config_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasConfigUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasConfigUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_gas_config_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasConfigUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasConfigUpdateError::LengthDecodingError); + } + + #[test] + fn test_gas_config_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000041000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasConfigUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasConfigUpdateError::InvalidDataLength(65)); + } + + #[test] + fn test_gas_config_update_overhead_decoding_succeeds_max_u256() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + assert!(GasConfigUpdate::try_from(&system_log).is_ok()); + } + + #[test] + fn test_gas_config_update_scalar_decoding_succeeds_max_u256() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000babeFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + assert!(GasConfigUpdate::try_from(&system_log).is_ok()); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/gas_limit.rs b/kona/crates/protocol/genesis/src/updates/gas_limit.rs new file mode 100644 index 0000000000000..a01f376a76311 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/gas_limit.rs @@ -0,0 +1,192 @@ +//! The gas limit update type. + +use alloy_primitives::{LogData, U64, U256}; +use alloy_sol_types::{SolType, sol}; + +use crate::{GasLimitUpdateError, SystemConfig, SystemConfigLog}; + +/// The gas limit update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GasLimitUpdate { + /// The gas limit. + pub gas_limit: u64, +} + +impl GasLimitUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + config.gas_limit = self.gas_limit; + } +} + +impl TryFrom<&SystemConfigLog> for GasLimitUpdate { + type Error = GasLimitUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + if data.len() != 96 { + return Err(GasLimitUpdateError::InvalidDataLen(data.len())); + } + + let Ok(pointer) = ::abi_decode_validate(&data[0..32]) else { + return Err(GasLimitUpdateError::PointerDecodingError); + }; + if pointer != 32 { + return Err(GasLimitUpdateError::InvalidDataPointer(pointer)); + } + + let Ok(length) = ::abi_decode_validate(&data[32..64]) else { + return Err(GasLimitUpdateError::LengthDecodingError); + }; + if length != 32 { + return Err(GasLimitUpdateError::InvalidDataLength(length)); + } + + let Ok(gas_limit) = ::abi_decode_validate(&data[64..]) else { + return Err(GasLimitUpdateError::GasLimitDecodingError); + }; + + // Prevent overflows here. + let max = U256::from(u64::MAX as u128); + if gas_limit > max { + return Err(GasLimitUpdateError::GasLimitDecodingError); + } + + Ok(Self { gas_limit: U64::from(gas_limit).saturating_to::() }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex}; + + #[test] + fn test_gas_limit_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = GasLimitUpdate::try_from(&system_log).unwrap(); + + assert_eq!(update.gas_limit, 0xbeef_u64); + } + + #[test] + fn test_gas_limit_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_gas_limit_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::PointerDecodingError); + } + + #[test] + fn test_gas_limit_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_gas_limit_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::LengthDecodingError); + } + + #[test] + fn test_gas_limit_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_gas_limit_update_gas_limit_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = GasLimitUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, GasLimitUpdateError::GasLimitDecodingError); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/min_base_fee.rs b/kona/crates/protocol/genesis/src/updates/min_base_fee.rs new file mode 100644 index 0000000000000..0e2da23a55a0d --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/min_base_fee.rs @@ -0,0 +1,186 @@ +//! The min base fee update type. + +use alloy_primitives::LogData; +use alloy_sol_types::{SolType, sol}; + +use crate::{SystemConfig, SystemConfigLog, system::MinBaseFeeUpdateError}; + +/// The min base fee update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MinBaseFeeUpdate { + /// The min base fee. + pub min_base_fee: u64, +} + +impl MinBaseFeeUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + config.min_base_fee = Some(self.min_base_fee); + } +} + +impl TryFrom<&SystemConfigLog> for MinBaseFeeUpdate { + type Error = MinBaseFeeUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + if data.len() != 96 { + return Err(MinBaseFeeUpdateError::InvalidDataLen(data.len())); + } + + let Ok(pointer) = ::abi_decode_validate(&data[0..32]) else { + return Err(MinBaseFeeUpdateError::PointerDecodingError); + }; + if pointer != 32 { + return Err(MinBaseFeeUpdateError::InvalidDataPointer(pointer)); + } + + let Ok(length) = ::abi_decode_validate(&data[32..64]) else { + return Err(MinBaseFeeUpdateError::LengthDecodingError); + }; + if length != 32 { + return Err(MinBaseFeeUpdateError::InvalidDataLength(length)); + } + + let Ok(min_base_fee) = ::abi_decode_validate(&data[64..96]) else { + return Err(MinBaseFeeUpdateError::MinBaseFeeDecodingError); + }; + + Ok(Self { min_base_fee }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex}; + + #[test] + fn test_min_base_fee_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = MinBaseFeeUpdate::try_from(&system_log).unwrap(); + + assert_eq!(update.min_base_fee, 0xbeef_u64); + } + + #[test] + fn test_min_base_fee_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_min_base_fee_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::PointerDecodingError); + } + + #[test] + fn test_min_base_fee_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_min_base_fee_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::LengthDecodingError); + } + + #[test] + fn test_min_base_fee_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_min_base_fee_update_min_base_fee_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = MinBaseFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, MinBaseFeeUpdateError::MinBaseFeeDecodingError); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/mod.rs b/kona/crates/protocol/genesis/src/updates/mod.rs new file mode 100644 index 0000000000000..1b2dbdbbc5633 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/mod.rs @@ -0,0 +1,27 @@ +//! Contains all updates to the [crate::SystemConfig] type. + +mod common; + +mod batcher; +pub use batcher::BatcherUpdate; + +mod signer; +pub use signer::UnsafeBlockSignerUpdate; + +mod gas_config; +pub use gas_config::GasConfigUpdate; + +mod gas_limit; +pub use gas_limit::GasLimitUpdate; + +mod eip1559; +pub use eip1559::Eip1559Update; + +mod operator_fee; +pub use operator_fee::OperatorFeeUpdate; + +mod min_base_fee; +pub use min_base_fee::MinBaseFeeUpdate; + +mod da_footprint_gas_scalar; +pub use da_footprint_gas_scalar::DaFootprintGasScalarUpdate; diff --git a/kona/crates/protocol/genesis/src/updates/operator_fee.rs b/kona/crates/protocol/genesis/src/updates/operator_fee.rs new file mode 100644 index 0000000000000..511c131e89d23 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/operator_fee.rs @@ -0,0 +1,178 @@ +//! The Operator Fee update type. + +use alloy_primitives::LogData; + +use crate::{ + OperatorFeeUpdateError, SystemConfig, SystemConfigLog, + updates::common::{ValidationError, validate_update_data}, +}; + +/// The Operator Fee update type. +#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OperatorFeeUpdate { + /// The operator fee scalar. + pub operator_fee_scalar: u32, + /// The operator fee constant. + pub operator_fee_constant: u64, +} + +impl OperatorFeeUpdate { + /// Applies the update to the [`SystemConfig`]. + pub const fn apply(&self, config: &mut SystemConfig) { + config.operator_fee_scalar = Some(self.operator_fee_scalar); + config.operator_fee_constant = Some(self.operator_fee_constant); + } +} + +impl TryFrom<&SystemConfigLog> for OperatorFeeUpdate { + type Error = OperatorFeeUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + + let validated = validate_update_data(data).map_err(|e| match e { + ValidationError::InvalidDataLen(_expected, actual) => { + OperatorFeeUpdateError::InvalidDataLen(actual) + } + ValidationError::PointerDecodingError => OperatorFeeUpdateError::PointerDecodingError, + ValidationError::InvalidDataPointer(pointer) => { + OperatorFeeUpdateError::InvalidDataPointer(pointer) + } + ValidationError::LengthDecodingError => OperatorFeeUpdateError::LengthDecodingError, + ValidationError::InvalidDataLength(length) => { + OperatorFeeUpdateError::InvalidDataLength(length) + } + })?; + + // The operator fee scalar and constant are + // packed into a single u256 as follows: + // + // | Bytes | Actual Size | Variable | + // |----------|-------------|----------| + // | 0 .. 24 | uint32 | scalar | + // | 24 .. 32 | uint64 | constant | + // |----------|-------------|----------| + + let payload = validated.payload(); + let mut be_bytes = [0u8; 4]; + be_bytes[0..4].copy_from_slice(&payload[20..24]); + let operator_fee_scalar = u32::from_be_bytes(be_bytes); + + let mut be_bytes = [0u8; 8]; + be_bytes[0..8].copy_from_slice(&payload[24..32]); + let operator_fee_constant = u64::from_be_bytes(be_bytes); + + Ok(Self { operator_fee_scalar, operator_fee_constant }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{Address, B256, Bytes, Log, LogData, hex}; + + #[test] + fn test_operator_fee_update_try_from() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![], // Topics aren't checked + hex!("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000babe000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = OperatorFeeUpdate::try_from(&system_log).unwrap(); + + assert_eq!(update.operator_fee_scalar, 0xbabe_u32); + assert_eq!(update.operator_fee_constant, 0xbeef_u64); + } + + #[test] + fn test_operator_fee_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = OperatorFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, OperatorFeeUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_operator_fee_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = OperatorFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, OperatorFeeUpdateError::PointerDecodingError); + } + + #[test] + fn test_operator_fee_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = OperatorFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, OperatorFeeUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_operator_fee_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = OperatorFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, OperatorFeeUpdateError::LengthDecodingError); + } + + #[test] + fn test_operator_fee_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = OperatorFeeUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, OperatorFeeUpdateError::InvalidDataLength(33)); + } +} diff --git a/kona/crates/protocol/genesis/src/updates/signer.rs b/kona/crates/protocol/genesis/src/updates/signer.rs new file mode 100644 index 0000000000000..bcf204c5012e8 --- /dev/null +++ b/kona/crates/protocol/genesis/src/updates/signer.rs @@ -0,0 +1,186 @@ +//! The unsafe block signer update. + +use alloy_primitives::{Address, LogData}; +use alloy_sol_types::{SolType, sol}; + +use crate::{ + SystemConfigLog, UnsafeBlockSignerUpdateError, + updates::common::{ValidationError, validate_update_data}, +}; + +/// The unsafe block signer update type. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UnsafeBlockSignerUpdate { + /// The new unsafe block signer address. + pub unsafe_block_signer: Address, +} + +impl TryFrom<&SystemConfigLog> for UnsafeBlockSignerUpdate { + type Error = UnsafeBlockSignerUpdateError; + + fn try_from(log: &SystemConfigLog) -> Result { + let LogData { data, .. } = &log.log.data; + + let validated = validate_update_data(data).map_err(|e| match e { + ValidationError::InvalidDataLen(_expected, actual) => { + UnsafeBlockSignerUpdateError::InvalidDataLen(actual) + } + ValidationError::PointerDecodingError => { + UnsafeBlockSignerUpdateError::PointerDecodingError + } + ValidationError::InvalidDataPointer(pointer) => { + UnsafeBlockSignerUpdateError::InvalidDataPointer(pointer) + } + ValidationError::LengthDecodingError => { + UnsafeBlockSignerUpdateError::LengthDecodingError + } + ValidationError::InvalidDataLength(length) => { + UnsafeBlockSignerUpdateError::InvalidDataLength(length) + } + })?; + + let Ok(unsafe_block_signer) = ::abi_decode_validate(validated.payload()) + else { + return Err(UnsafeBlockSignerUpdateError::UnsafeBlockSignerAddressDecodingError); + }; + + Ok(Self { unsafe_block_signer }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC}; + use alloc::vec; + use alloy_primitives::{B256, Bytes, Log, LogData, address, hex}; + + #[test] + fn test_signer_update_try_from() { + let update_type = B256::ZERO; + + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + update_type, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let update = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap(); + assert_eq!( + update.unsafe_block_signer, + address!("000000000000000000000000000000000000bEEF"), + ); + } + + #[test] + fn test_signer_update_invalid_data_len() { + let log = + Log { address: Address::ZERO, data: LogData::new_unchecked(vec![], Bytes::default()) }; + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::InvalidDataLen(0)); + } + + #[test] + fn test_signer_update_pointer_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::PointerDecodingError); + } + + #[test] + fn test_signer_update_invalid_pointer_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::InvalidDataPointer(33)); + } + + #[test] + fn test_signer_update_length_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("0000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::LengthDecodingError); + } + + #[test] + fn test_signer_update_invalid_data_length() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000babe0000beef").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::InvalidDataLength(33)); + } + + #[test] + fn test_signer_update_address_decoding_error() { + let log = Log { + address: Address::ZERO, + data: LogData::new_unchecked( + vec![ + CONFIG_UPDATE_TOPIC, + CONFIG_UPDATE_EVENT_VERSION_0, + B256::ZERO, + ], + hex!("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").into() + ) + }; + + let system_log = SystemConfigLog::new(log, false); + let err = UnsafeBlockSignerUpdate::try_from(&system_log).unwrap_err(); + assert_eq!(err, UnsafeBlockSignerUpdateError::UnsafeBlockSignerAddressDecodingError); + } +} diff --git a/kona/crates/protocol/hardforks/Cargo.toml b/kona/crates/protocol/hardforks/Cargo.toml new file mode 100644 index 0000000000000..4547c63b004b6 --- /dev/null +++ b/kona/crates/protocol/hardforks/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "kona-hardforks" +version = "0.4.5" +description = "Consensus hardfork types for the OP Stack" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-protocol.workspace = true + +# Alloy +alloy-eips.workspace = true +alloy-primitives = { workspace = true, features = ["rlp"] } + +# OP Alloy +op-alloy-consensus.workspace = true + +[dev-dependencies] +alloy-primitives = { workspace = true, features = ["rand", "arbitrary"] } +revm.workspace = true +op-revm.workspace = true + +[features] +default = [] +std = [ + "alloy-eips/std", + "alloy-primitives/std", + "kona-protocol/std", + "op-alloy-consensus/std", +] +k256 = [ "alloy-primitives/k256", "op-alloy-consensus/k256" ] +kzg = [ "alloy-eips/kzg", "op-alloy-consensus/kzg", "std" ] diff --git a/kona/crates/protocol/hardforks/README.md b/kona/crates/protocol/hardforks/README.md new file mode 100644 index 0000000000000..f66f0a1a116c4 --- /dev/null +++ b/kona/crates/protocol/hardforks/README.md @@ -0,0 +1,14 @@ +# `kona-hardforks` + +CI +kona-hardforks crate +MIT License +Docs + +Consensus layer hardfork types for the OP Stack including network upgrade transactions. + +### Provenance + +This code was ported [op-alloy] as part of `kona` monorepo migrations. + +[op-alloy]: https://github.com/alloy-rs/op-alloy diff --git a/kona/crates/protocol/hardforks/src/bytecode/crossl2inbox_interop.hex b/kona/crates/protocol/hardforks/src/bytecode/crossl2inbox_interop.hex new file mode 100644 index 0000000000000..30d9dc4efaa92 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/crossl2inbox_interop.hex @@ -0,0 +1 @@ +6080604052348015600e575f80fd5b506106828061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063331b637f1461004357806354fd4d5014610069578063ab4d6f75146100b2575b5f80fd5b610056610051366004610512565b6100c7565b6040519081526020015b60405180910390f35b6100a56040518060400160405280600581526020017f312e302e3100000000000000000000000000000000000000000000000000000081525081565b604051610060919061053b565b6100c56100c036600461058e565b61039e565b005b5f67ffffffffffffffff801683602001511115610110576040517fd1f79e8200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604083015163ffffffff1015610152576040517f94338eba00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606083015167ffffffffffffffff1015610198576040517f596a19a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b82516040515f916101dd91859060200160609290921b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000168252601482015260340190565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00181528282528051602091820120878201516060890151898501515f9487018590527fffffffffffffffff00000000000000000000000000000000000000000000000060c084811b8216602c8a015283901b1660348801527fffffffff0000000000000000000000000000000000000000000000000000000060e082901b16603c88015292965090949093919291016040516020818303038152906040526102ac906105bc565b90505f85826040516020016102cb929190918252602082015260400190565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0818403018152828252805160209182012060808d01519184018190529183015291505f90606001604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815291905280516020909101207effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f0300000000000000000000000000000000000000000000000000000000000000179a9950505050505050505050565b5f6103b76103b136859003850185610601565b836100c7565b90505f6103c38261043b565b509050806103fd576040517fe3c0081600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b827f5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba78560405161042d9190610622565b60405180910390a250505050565b5f805a835491505a6103e891031115939092509050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610475575f80fd5b919050565b5f60a0828403121561048a575f80fd5b60405160a0810181811067ffffffffffffffff821117156104d2577f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040529050806104e183610452565b8152602083013560208201526040830135604082015260608301356060820152608083013560808201525092915050565b5f8060c08385031215610523575f80fd5b61052d848461047a565b9460a0939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f8082840360c08112156105a0575f80fd5b60a08112156105ad575f80fd5b50919360a08501359350915050565b805160208083015191908110156105fb577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8160200360031b1b821691505b50919050565b5f60a08284031215610611575f80fd5b61061b838361047a565b9392505050565b60a0810173ffffffffffffffffffffffffffffffffffffffff61064484610452565b168252602083013560208301526040830135604083015260608301356060830152608083013560808301529291505056fea164736f6c6343000819000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_0.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_0.hex new file mode 100644 index 0000000000000..9464e23830d01 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_0.hex @@ -0,0 +1 @@ +7ef9059fa0877a6077205782ea15a6dc8699fa5ebcec5e0f4389f09cb8eda09488231346f89442100000000000000000000000000000000000008080808305b8d880b9055e608060405234801561001057600080fd5b5061053e806100206000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c80638381f58a11610097578063c598591811610066578063c598591814610229578063e591b28214610249578063e81b2c6d14610289578063f82061401461029257600080fd5b80638381f58a146101e35780638b239f73146101f75780639e8c496614610200578063b80777ea1461020957600080fd5b806354fd4d50116100d357806354fd4d50146101335780635cf249691461017c57806364ca23ef1461018557806368d5dca6146101b257600080fd5b8063015d8eb9146100fa57806309bd5a601461010f578063440a5e201461012b575b600080fd5b61010d61010836600461044c565b61029b565b005b61011860025481565b6040519081526020015b60405180910390f35b61010d6103da565b61016f6040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b60405161012291906104be565b61011860015481565b6003546101999067ffffffffffffffff1681565b60405167ffffffffffffffff9091168152602001610122565b6003546101ce9068010000000000000000900463ffffffff1681565b60405163ffffffff9091168152602001610122565b6000546101999067ffffffffffffffff1681565b61011860055481565b61011860065481565b6000546101999068010000000000000000900467ffffffffffffffff1681565b6003546101ce906c01000000000000000000000000900463ffffffff1681565b61026473deaddeaddeaddeaddeaddeaddeaddeaddead000181565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610122565b61011860045481565b61011860075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610342576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461040357633cc50b456000526004601cfd5b60043560801c60035560143560801c600055602435600155604435600755606435600255608435600455565b803567ffffffffffffffff8116811461044757600080fd5b919050565b600080600080600080600080610100898b03121561046957600080fd5b6104728961042f565b975061048060208a0161042f565b9650604089013595506060890135945061049c60808a0161042f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b818110156104eb578581018301518582016040015282016104cf565b818111156104fd576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_1.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_1.hex new file mode 100644 index 0000000000000..64eff493b7bfb --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_1.hex @@ -0,0 +1 @@ +7ef91016a0a312b4510adf943510f05fcc8f15f86995a5066bd83ce11384688ae20e6ecf42944210000000000000000000000000000000000001808080830f424080b90fd5608060405234801561001057600080fd5b50610fb5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806354fd4d5011610097578063de26c4a111610066578063de26c4a1146101da578063f45e65d8146101ed578063f8206140146101f5578063fe173b97146101cc57600080fd5b806354fd4d501461016657806368d5dca6146101af5780636ef25c3a146101cc578063c5985918146101d257600080fd5b8063313ce567116100d3578063313ce5671461012757806349948e0e1461012e5780634ef6e22414610141578063519b4bd31461015e57600080fd5b80630c18c162146100fa57806322b90ab3146101155780632e0f26251461011f575b600080fd5b6101026101fd565b6040519081526020015b60405180910390f35b61011d61031e565b005b610102600681565b6006610102565b61010261013c366004610b73565b610541565b60005461014e9060ff1681565b604051901515815260200161010c565b610102610565565b6101a26040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b60405161010c9190610c42565b6101b76105c6565b60405163ffffffff909116815260200161010c565b48610102565b6101b761064b565b6101026101e8366004610b73565b6106ac565b610102610760565b610102610853565b6000805460ff1615610296576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103199190610cb5565b905090565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663e591b2826040518163ffffffff1660e01b8152600401602060405180830381865afa15801561037d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103a19190610cce565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610481576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161028d565b60005460ff1615610514576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161028d565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805460ff161561055c57610556826108b4565b92915050565b61055682610958565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610627573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103199190610d04565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610627573d6000803e3d6000fd5b6000806106b883610ab4565b60005490915060ff16156106cc5792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa15801561072b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061074f9190610cb5565b6107599082610d59565b9392505050565b6000805460ff16156107f4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161028d565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b6000806108c083610ab4565b905060006108cc610565565b6108d461064b565b6108df906010610d71565b63ffffffff166108ef9190610d9d565b905060006108fb610853565b6109036105c6565b63ffffffff166109139190610d9d565b905060006109218284610d59565b61092b9085610d9d565b90506109396006600a610efa565b610944906010610d9d565b61094e9082610f06565b9695505050505050565b60008061096483610ab4565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109eb9190610cb5565b6109f3610565565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a52573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a769190610cb5565b610a809085610d59565b610a8a9190610d9d565b610a949190610d9d565b9050610aa26006600a610efa565b610aac9082610f06565b949350505050565b80516000908190815b81811015610b3757848181518110610ad757610ad7610f41565b01602001517fff0000000000000000000000000000000000000000000000000000000000000016600003610b1757610b10600484610d59565b9250610b25565b610b22601084610d59565b92505b80610b2f81610f70565b915050610abd565b50610aac82610440610d59565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600060208284031215610b8557600080fd5b813567ffffffffffffffff80821115610b9d57600080fd5b818401915084601f830112610bb157600080fd5b813581811115610bc357610bc3610b44565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715610c0957610c09610b44565b81604052828152876020848701011115610c2257600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b81811015610c6f57858101830151858201604001528201610c53565b81811115610c81576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b600060208284031215610cc757600080fd5b5051919050565b600060208284031215610ce057600080fd5b815173ffffffffffffffffffffffffffffffffffffffff8116811461075957600080fd5b600060208284031215610d1657600080fd5b815163ffffffff8116811461075957600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008219821115610d6c57610d6c610d2a565b500190565b600063ffffffff80831681851681830481118215151615610d9457610d94610d2a565b02949350505050565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615610dd557610dd5610d2a565b500290565b600181815b80851115610e3357817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115610e1957610e19610d2a565b80851615610e2657918102915b93841c9390800290610ddf565b509250929050565b600082610e4a57506001610556565b81610e5757506000610556565b8160018114610e6d5760028114610e7757610e93565b6001915050610556565b60ff841115610e8857610e88610d2a565b50506001821b610556565b5060208310610133831016604e8410600b8410161715610eb6575081810a610556565b610ec08383610dda565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115610ef257610ef2610d2a565b029392505050565b60006107598383610e3b565b600082610f3c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610fa157610fa1610d2a565b506001019056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_2.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_2.hex new file mode 100644 index 0000000000000..e2a9797a7e0ed --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_2.hex @@ -0,0 +1 @@ +7ef876a018acb38c5ff1c238a7460ebc1b421fa49ec4874bdf1e0a530d234104e5e67dbc940000000000000000000000000000000000000000944200000000000000000000000000000000000015808082c35080a43659cfe600000000000000000000000007dbe8500fc591d1852b76fee44d5a05e13097ff \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_3.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_3.hex new file mode 100644 index 0000000000000..e7b1432f1aec5 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_3.hex @@ -0,0 +1 @@ +7ef876a0ee4f9385eceef498af0be7ec5862229f426dec41c8d42397c7257a5117d9230a94000000000000000000000000000000000000000094420000000000000000000000000000000000000f808082c35080a43659cfe6000000000000000000000000b528d11cc114e026f138fe568744c6d45ce6da7a diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_4.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_4.hex new file mode 100644 index 0000000000000..ee623442ad2a0 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_4.hex @@ -0,0 +1 @@ +7ef857a00c1cb38e99dbc9cbfab3bb80863380b0905290b37eb3d6ab18dc01c1f3e75f9394deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000000f808083013880808422b90ab3 diff --git a/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_5.hex b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_5.hex new file mode 100644 index 0000000000000..72694b222cd56 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ecotone_tx_5.hex @@ -0,0 +1 @@ +7ef8aaa069b763c48478b9dc2f65ada09b3d92133ec592ea715ec65ad6e7f3dc519dc00c940b799c86a49deeb90402691f1041aa3af2d3c8758080808303d09080b86a60618060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/eip2935_isthmus.hex b/kona/crates/protocol/hardforks/src/bytecode/eip2935_isthmus.hex new file mode 100644 index 0000000000000..0032fc3366ef5 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/eip2935_isthmus.hex @@ -0,0 +1 @@ +60538060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500 diff --git a/kona/crates/protocol/hardforks/src/bytecode/eip4788_ecotone.hex b/kona/crates/protocol/hardforks/src/bytecode/eip4788_ecotone.hex new file mode 100644 index 0000000000000..1764b1777a820 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/eip4788_ecotone.hex @@ -0,0 +1 @@ +60618060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500 diff --git a/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_0.hex b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_0.hex new file mode 100644 index 0000000000000..0553eebd25f17 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_0.hex @@ -0,0 +1 @@ +7ef91857a086122c533fdcb89b16d8713174625e44578a89751d96c098ec19ab40a51a8ea39442100000000000000000000000000000000000028080808316201080b91816608060405234801561001057600080fd5b506117f6806100206000396000f3fe608060405234801561001057600080fd5b50600436106101365760003560e01c80636ef25c3a116100b2578063de26c4a111610081578063f45e65d811610066578063f45e65d81461025b578063f820614014610263578063fe173b971461020d57600080fd5b8063de26c4a114610235578063f1c7a58b1461024857600080fd5b80636ef25c3a1461020d5780638e98b10614610213578063960e3a231461021b578063c59859181461022d57600080fd5b806349948e0e11610109578063519b4bd3116100ee578063519b4bd31461019f57806354fd4d50146101a757806368d5dca6146101f057600080fd5b806349948e0e1461016f5780634ef6e2241461018257600080fd5b80630c18c1621461013b57806322b90ab3146101565780632e0f262514610160578063313ce56714610168575b600080fd5b61014361026b565b6040519081526020015b60405180910390f35b61015e61038c565b005b610143600681565b6006610143565b61014361017d3660046112a1565b610515565b60005461018f9060ff1681565b604051901515815260200161014d565b610143610552565b6101e36040518060400160405280600581526020017f312e332e3000000000000000000000000000000000000000000000000000000081525081565b60405161014d9190611370565b6101f86105b3565b60405163ffffffff909116815260200161014d565b48610143565b61015e610638565b60005461018f90610100900460ff1681565b6101f8610832565b6101436102433660046112a1565b610893565b6101436102563660046113e3565b61098d565b610143610a69565b610143610b5c565b6000805460ff1615610304576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061038791906113fc565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610455576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a4016102fb565b60005460ff16156104e8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f616374697665000000000000000000000000000000000000000000000000000060648201526084016102fb565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b60008054610100900460ff16156105355761052f82610bbd565b92915050565b60005460ff16156105495761052f82610bdc565b61052f82610c80565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610614573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103879190611415565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146106db576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c61670060648201526084016102fb565b60005460ff1661076d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e650000000000000060648201526084016102fb565b600054610100900460ff1615610804576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f746976650000000000000000000000000000000000000000000000000000000060648201526084016102fb565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610614573d6000803e3d6000fd5b60008054610100900460ff16156108da57620f42406108c56108b484610dd4565b516108c090604461146a565b6110f1565b6108d0906010611482565b61052f91906114bf565b60006108e583611150565b60005490915060ff16156108f95792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610958573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061097c91906113fc565b610986908261146a565b9392505050565b60008054610100900460ff16610a25576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f72640000000000000000000060648201526084016102fb565b6000610a3283604461146a565b90506000610a4160ff836114bf565b610a4b908361146a565b610a5690601061146a565b9050610a61816111e0565b949350505050565b6000805460ff1615610afd576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f656361746564000000000000000000000000000000000000000000000000000060648201526084016102fb565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600061052f610bcb83610dd4565b51610bd790604461146a565b6111e0565b600080610be883611150565b90506000610bf4610552565b610bfc610832565b610c079060106114fa565b63ffffffff16610c179190611482565b90506000610c23610b5c565b610c2b6105b3565b63ffffffff16610c3b9190611482565b90506000610c49828461146a565b610c539085611482565b9050610c616006600a611646565b610c6c906010611482565b610c7690826114bf565b9695505050505050565b600080610c8c83611150565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015610cef573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d1391906113fc565b610d1b610552565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d7a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d9e91906113fc565b610da8908561146a565b610db29190611482565b610dbc9190611482565b9050610dca6006600a611646565b610a6190826114bf565b6060610f63565b818153600101919050565b600082840393505b838110156109865782810151828201511860001a1590930292600101610dee565b825b60208210610e5b578251610e26601f83610ddb565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090910190602101610e11565b8115610986578251610e706001840383610ddb565b520160010192915050565b60006001830392505b6101078210610ebc57610eae8360ff16610ea960fd610ea98760081c60e00189610ddb565b610ddb565b935061010682039150610e84565b60078210610ee957610ee28360ff16610ea960078503610ea98760081c60e00189610ddb565b9050610986565b610a618360ff16610ea98560081c8560051b0187610ddb565b610f5b828203610f3f610f2f84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b81811015611096576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b90911890915284019081830390848410610feb5750611026565b600184019350611fff8211611020578251600081901a600182901a60081b1760029190911a60101b1781036110205750611026565b50610f8f565b838310611034575050611096565b600183039250858311156110525761104f8787888603610e0f565b96505b611066600985016003850160038501610de6565b9150611073878284610e7b565b96505061108b8461108686848601610f02565b610f02565b915050809350610f83565b50506110a88383848851850103610e0f565b925050506040519150618000820180820391508183526020830160005b838110156110dd5782810151828201526020016110c5565b506000920191825250602001604052919050565b60008061110183620cc394611482565b61112b907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611652565b905061113b6064620f42406116c6565b81121561052f576109866064620f42406116c6565b80516000908190815b818110156111d35784818151811061117357611173611782565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036111b3576111ac60048461146a565b92506111c1565b6111be60108461146a565b92505b806111cb816117b1565b915050611159565b50610a618261044061146a565b6000806111ec836110f1565b905060006111f8610b5c565b6112006105b3565b63ffffffff166112109190611482565b611218610552565b611220610832565b61122b9060106114fa565b63ffffffff1661123b9190611482565b611245919061146a565b905061125360066002611482565b61125e90600a611646565b6112688284611482565b610a6191906114bf565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156112b357600080fd5b813567ffffffffffffffff808211156112cb57600080fd5b818401915084601f8301126112df57600080fd5b8135818111156112f1576112f1611272565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561133757611337611272565b8160405282815287602084870101111561135057600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b8181101561139d57858101830151858201604001528201611381565b818111156113af576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b6000602082840312156113f557600080fd5b5035919050565b60006020828403121561140e57600080fd5b5051919050565b60006020828403121561142757600080fd5b815163ffffffff8116811461098657600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000821982111561147d5761147d61143b565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04831182151516156114ba576114ba61143b565b500290565b6000826114f5577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b600063ffffffff8083168185168183048111821515161561151d5761151d61143b565b02949350505050565b600181815b8085111561157f57817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156115655761156561143b565b8085161561157257918102915b93841c939080029061152b565b509250929050565b6000826115965750600161052f565b816115a35750600061052f565b81600181146115b957600281146115c3576115df565b600191505061052f565b60ff8411156115d4576115d461143b565b50506001821b61052f565b5060208310610133831016604e8410600b8410161715611602575081810a61052f565b61160c8383611526565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482111561163e5761163e61143b565b029392505050565b60006109868383611587565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0384138115161561168c5761168c61143b565b827f80000000000000000000000000000000000000000000000000000000000000000384128116156116c0576116c061143b565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6000841360008413858304851182821616156117075761170761143b565b7f800000000000000000000000000000000000000000000000000000000000000060008712868205881281841616156117425761174261143b565b6000871292508782058712848416161561175e5761175e61143b565b878505871281841616156117745761177461143b565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036117e2576117e261143b565b506001019056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_1.hex b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_1.hex new file mode 100644 index 0000000000000..08ea992d576bc --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_1.hex @@ -0,0 +1 @@ +7ef876a01e6bb0c28bfab3dc9b36ffb0f721f00d6937f33577606325692db0965a7d58c694000000000000000000000000000000000000000094420000000000000000000000000000000000000f808082c35080a43659cfe6000000000000000000000000a919894851548179a0750865e7974da599c0fac7 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_2.hex b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_2.hex new file mode 100644 index 0000000000000..7813d0de719c6 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/fjord_tx_2.hex @@ -0,0 +1 @@ +7ef857a0bac7bb0d5961cad209a345408b0280a0d4686b1b20665e1b0f9cdafd73b19b6b94deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000000f808083015f9080848e98b106 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/gpo_ecotone.hex b/kona/crates/protocol/hardforks/src/bytecode/gpo_ecotone.hex new file mode 100644 index 0000000000000..4fc96a0ed6def --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/gpo_ecotone.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b50610fb5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806354fd4d5011610097578063de26c4a111610066578063de26c4a1146101da578063f45e65d8146101ed578063f8206140146101f5578063fe173b97146101cc57600080fd5b806354fd4d501461016657806368d5dca6146101af5780636ef25c3a146101cc578063c5985918146101d257600080fd5b8063313ce567116100d3578063313ce5671461012757806349948e0e1461012e5780634ef6e22414610141578063519b4bd31461015e57600080fd5b80630c18c162146100fa57806322b90ab3146101155780632e0f26251461011f575b600080fd5b6101026101fd565b6040519081526020015b60405180910390f35b61011d61031e565b005b610102600681565b6006610102565b61010261013c366004610b73565b610541565b60005461014e9060ff1681565b604051901515815260200161010c565b610102610565565b6101a26040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b60405161010c9190610c42565b6101b76105c6565b60405163ffffffff909116815260200161010c565b48610102565b6101b761064b565b6101026101e8366004610b73565b6106ac565b610102610760565b610102610853565b6000805460ff1615610296576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103199190610cb5565b905090565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663e591b2826040518163ffffffff1660e01b8152600401602060405180830381865afa15801561037d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103a19190610cce565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610481576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161028d565b60005460ff1615610514576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161028d565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805460ff161561055c57610556826108b4565b92915050565b61055682610958565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610627573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103199190610d04565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610627573d6000803e3d6000fd5b6000806106b883610ab4565b60005490915060ff16156106cc5792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa15801561072b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061074f9190610cb5565b6107599082610d59565b9392505050565b6000805460ff16156107f4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161028d565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156102f5573d6000803e3d6000fd5b6000806108c083610ab4565b905060006108cc610565565b6108d461064b565b6108df906010610d71565b63ffffffff166108ef9190610d9d565b905060006108fb610853565b6109036105c6565b63ffffffff166109139190610d9d565b905060006109218284610d59565b61092b9085610d9d565b90506109396006600a610efa565b610944906010610d9d565b61094e9082610f06565b9695505050505050565b60008061096483610ab4565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109eb9190610cb5565b6109f3610565565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a52573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a769190610cb5565b610a809085610d59565b610a8a9190610d9d565b610a949190610d9d565b9050610aa26006600a610efa565b610aac9082610f06565b949350505050565b80516000908190815b81811015610b3757848181518110610ad757610ad7610f41565b01602001517fff0000000000000000000000000000000000000000000000000000000000000016600003610b1757610b10600484610d59565b9250610b25565b610b22601084610d59565b92505b80610b2f81610f70565b915050610abd565b50610aac82610440610d59565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600060208284031215610b8557600080fd5b813567ffffffffffffffff80821115610b9d57600080fd5b818401915084601f830112610bb157600080fd5b813581811115610bc357610bc3610b44565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715610c0957610c09610b44565b81604052828152876020848701011115610c2257600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b81811015610c6f57858101830151858201604001528201610c53565b81811115610c81576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b600060208284031215610cc757600080fd5b5051919050565b600060208284031215610ce057600080fd5b815173ffffffffffffffffffffffffffffffffffffffff8116811461075957600080fd5b600060208284031215610d1657600080fd5b815163ffffffff8116811461075957600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60008219821115610d6c57610d6c610d2a565b500190565b600063ffffffff80831681851681830481118215151615610d9457610d94610d2a565b02949350505050565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615610dd557610dd5610d2a565b500290565b600181815b80851115610e3357817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115610e1957610e19610d2a565b80851615610e2657918102915b93841c9390800290610ddf565b509250929050565b600082610e4a57506001610556565b81610e5757506000610556565b8160018114610e6d5760028114610e7757610e93565b6001915050610556565b60ff841115610e8857610e88610d2a565b50506001821b610556565b5060208310610133831016604e8410600b8410161715610eb6575081810a610556565b610ec08383610dda565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115610ef257610ef2610d2a565b029392505050565b60006107598383610e3b565b600082610f3c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610fa157610fa1610d2a565b506001019056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/gpo_fjord.hex b/kona/crates/protocol/hardforks/src/bytecode/gpo_fjord.hex new file mode 100644 index 0000000000000..42f73850c37ea --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/gpo_fjord.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506117f6806100206000396000f3fe608060405234801561001057600080fd5b50600436106101365760003560e01c80636ef25c3a116100b2578063de26c4a111610081578063f45e65d811610066578063f45e65d81461025b578063f820614014610263578063fe173b971461020d57600080fd5b8063de26c4a114610235578063f1c7a58b1461024857600080fd5b80636ef25c3a1461020d5780638e98b10614610213578063960e3a231461021b578063c59859181461022d57600080fd5b806349948e0e11610109578063519b4bd3116100ee578063519b4bd31461019f57806354fd4d50146101a757806368d5dca6146101f057600080fd5b806349948e0e1461016f5780634ef6e2241461018257600080fd5b80630c18c1621461013b57806322b90ab3146101565780632e0f262514610160578063313ce56714610168575b600080fd5b61014361026b565b6040519081526020015b60405180910390f35b61015e61038c565b005b610143600681565b6006610143565b61014361017d3660046112a1565b610515565b60005461018f9060ff1681565b604051901515815260200161014d565b610143610552565b6101e36040518060400160405280600581526020017f312e332e3000000000000000000000000000000000000000000000000000000081525081565b60405161014d9190611370565b6101f86105b3565b60405163ffffffff909116815260200161014d565b48610143565b61015e610638565b60005461018f90610100900460ff1681565b6101f8610832565b6101436102433660046112a1565b610893565b6101436102563660046113e3565b61098d565b610143610a69565b610143610b5c565b6000805460ff1615610304576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061038791906113fc565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610455576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a4016102fb565b60005460ff16156104e8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f616374697665000000000000000000000000000000000000000000000000000060648201526084016102fb565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b60008054610100900460ff16156105355761052f82610bbd565b92915050565b60005460ff16156105495761052f82610bdc565b61052f82610c80565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610614573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103879190611415565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146106db576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c61670060648201526084016102fb565b60005460ff1661076d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e650000000000000060648201526084016102fb565b600054610100900460ff1615610804576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f746976650000000000000000000000000000000000000000000000000000000060648201526084016102fb565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610614573d6000803e3d6000fd5b60008054610100900460ff16156108da57620f42406108c56108b484610dd4565b516108c090604461146a565b6110f1565b6108d0906010611482565b61052f91906114bf565b60006108e583611150565b60005490915060ff16156108f95792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610958573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061097c91906113fc565b610986908261146a565b9392505050565b60008054610100900460ff16610a25576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f72640000000000000000000060648201526084016102fb565b6000610a3283604461146a565b90506000610a4160ff836114bf565b610a4b908361146a565b610a5690601061146a565b9050610a61816111e0565b949350505050565b6000805460ff1615610afd576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f656361746564000000000000000000000000000000000000000000000000000060648201526084016102fb565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa158015610363573d6000803e3d6000fd5b600061052f610bcb83610dd4565b51610bd790604461146a565b6111e0565b600080610be883611150565b90506000610bf4610552565b610bfc610832565b610c079060106114fa565b63ffffffff16610c179190611482565b90506000610c23610b5c565b610c2b6105b3565b63ffffffff16610c3b9190611482565b90506000610c49828461146a565b610c539085611482565b9050610c616006600a611646565b610c6c906010611482565b610c7690826114bf565b9695505050505050565b600080610c8c83611150565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015610cef573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d1391906113fc565b610d1b610552565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d7a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d9e91906113fc565b610da8908561146a565b610db29190611482565b610dbc9190611482565b9050610dca6006600a611646565b610a6190826114bf565b6060610f63565b818153600101919050565b600082840393505b838110156109865782810151828201511860001a1590930292600101610dee565b825b60208210610e5b578251610e26601f83610ddb565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090910190602101610e11565b8115610986578251610e706001840383610ddb565b520160010192915050565b60006001830392505b6101078210610ebc57610eae8360ff16610ea960fd610ea98760081c60e00189610ddb565b610ddb565b935061010682039150610e84565b60078210610ee957610ee28360ff16610ea960078503610ea98760081c60e00189610ddb565b9050610986565b610a618360ff16610ea98560081c8560051b0187610ddb565b610f5b828203610f3f610f2f84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b81811015611096576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b90911890915284019081830390848410610feb5750611026565b600184019350611fff8211611020578251600081901a600182901a60081b1760029190911a60101b1781036110205750611026565b50610f8f565b838310611034575050611096565b600183039250858311156110525761104f8787888603610e0f565b96505b611066600985016003850160038501610de6565b9150611073878284610e7b565b96505061108b8461108686848601610f02565b610f02565b915050809350610f83565b50506110a88383848851850103610e0f565b925050506040519150618000820180820391508183526020830160005b838110156110dd5782810151828201526020016110c5565b506000920191825250602001604052919050565b60008061110183620cc394611482565b61112b907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611652565b905061113b6064620f42406116c6565b81121561052f576109866064620f42406116c6565b80516000908190815b818110156111d35784818151811061117357611173611782565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036111b3576111ac60048461146a565b92506111c1565b6111be60108461146a565b92505b806111cb816117b1565b915050611159565b50610a618261044061146a565b6000806111ec836110f1565b905060006111f8610b5c565b6112006105b3565b63ffffffff166112109190611482565b611218610552565b611220610832565b61122b9060106114fa565b63ffffffff1661123b9190611482565b611245919061146a565b905061125360066002611482565b61125e90600a611646565b6112688284611482565b610a6191906114bf565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156112b357600080fd5b813567ffffffffffffffff808211156112cb57600080fd5b818401915084601f8301126112df57600080fd5b8135818111156112f1576112f1611272565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561133757611337611272565b8160405282815287602084870101111561135057600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b8181101561139d57858101830151858201604001528201611381565b818111156113af576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b6000602082840312156113f557600080fd5b5035919050565b60006020828403121561140e57600080fd5b5051919050565b60006020828403121561142757600080fd5b815163ffffffff8116811461098657600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000821982111561147d5761147d61143b565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04831182151516156114ba576114ba61143b565b500290565b6000826114f5577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b600063ffffffff8083168185168183048111821515161561151d5761151d61143b565b02949350505050565b600181815b8085111561157f57817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156115655761156561143b565b8085161561157257918102915b93841c939080029061152b565b509250929050565b6000826115965750600161052f565b816115a35750600061052f565b81600181146115b957600281146115c3576115df565b600191505061052f565b60ff8411156115d4576115d461143b565b50506001821b61052f565b5060208310610133831016604e8410600b8410161715611602575081810a61052f565b61160c8383611526565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482111561163e5761163e61143b565b029392505050565b60006109868383611587565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0384138115161561168c5761168c61143b565b827f80000000000000000000000000000000000000000000000000000000000000000384128116156116c0576116c061143b565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6000841360008413858304851182821616156117075761170761143b565b7f800000000000000000000000000000000000000000000000000000000000000060008712868205881281841616156117425761174261143b565b6000871292508782058712848416161561175e5761175e61143b565b878505871281841616156117745761177461143b565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036117e2576117e261143b565b506001019056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/gpo_isthmus.hex b/kona/crates/protocol/hardforks/src/bytecode/gpo_isthmus.hex new file mode 100644 index 0000000000000..a4479285e2b6e --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/gpo_isthmus.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b50611c3c806100206000396000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806368d5dca6116100d8578063c59859181161008c578063f45e65d811610066578063f45e65d8146102ca578063f8206140146102d2578063fe173b971461026957600080fd5b8063c59859181461029c578063de26c4a1146102a4578063f1c7a58b146102b757600080fd5b80638e98b106116100bd5780638e98b1061461026f578063960e3a2314610277578063b54501bc1461028957600080fd5b806368d5dca61461024c5780636ef25c3a1461026957600080fd5b8063313ce5671161012f5780634ef6e224116101145780634ef6e224146101de578063519b4bd3146101fb57806354fd4d501461020357600080fd5b8063313ce567146101c457806349948e0e146101cb57600080fd5b8063275aedd211610160578063275aedd2146101a1578063291b0383146101b45780632e0f2625146101bc57600080fd5b80630c18c1621461017c57806322b90ab314610197575b600080fd5b6101846102da565b6040519081526020015b60405180910390f35b61019f6103fb565b005b6101846101af36600461168e565b610584565b61019f61070f565b610184600681565b6006610184565b6101846101d93660046116d6565b610937565b6000546101eb9060ff1681565b604051901515815260200161018e565b61018461096e565b61023f6040518060400160405280600581526020017f312e342e3000000000000000000000000000000000000000000000000000000081525081565b60405161018e91906117a5565b6102546109cf565b60405163ffffffff909116815260200161018e565b48610184565b61019f610a54565b6000546101eb90610100900460ff1681565b6000546101eb9062010000900460ff1681565b610254610c4e565b6101846102b23660046116d6565b610caf565b6101846102c536600461168e565b610da9565b610184610e85565b610184610f78565b6000805460ff1615610373576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611818565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104c4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b60005460ff1615610557576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805462010000900460ff1661059d57506000919050565b610709620f42406106668473420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16634d5d9a2a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610607573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062b9190611831565b63ffffffff167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821583830293840490921491909117011790565b6106709190611886565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166316d3bc7f6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106cf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106f391906118c1565b67ffffffffffffffff1681019081106000031790565b92915050565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146107d8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973497374686d757320666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b600054610100900460ff1661086f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20497374686d75732063616e206f6e6c7960448201527f2062652061637469766174656420616674657220466a6f726400000000000000606482015260840161036a565b60005462010000900460ff1615610908576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a20497374686d757320616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff1662010000179055565b60008054610100900460ff16156109515761070982610fd9565b60005460ff16156109655761070982610ff8565b6107098261109c565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611831565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610af7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c616700606482015260840161036a565b60005460ff16610b89576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e6500000000000000606482015260840161036a565b600054610100900460ff1615610c20576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f7469766500000000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b60008054610100900460ff1615610cf657620f4240610ce1610cd0846111f0565b51610cdc9060446118eb565b61150d565b610cec906010611903565b6107099190611886565b6000610d018361156c565b60005490915060ff1615610d155792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d74573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d989190611818565b610da290826118eb565b9392505050565b60008054610100900460ff16610e41576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f726400000000000000000000606482015260840161036a565b6000610e4e8360446118eb565b90506000610e5d60ff83611886565b610e6790836118eb565b610e729060106118eb565b9050610e7d816115fc565b949350505050565b6000805460ff1615610f19576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161036a565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b6000610709610fe7836111f0565b51610ff39060446118eb565b6115fc565b6000806110048361156c565b9050600061101061096e565b611018610c4e565b611023906010611940565b63ffffffff166110339190611903565b9050600061103f610f78565b6110476109cf565b63ffffffff166110579190611903565b9050600061106582846118eb565b61106f9085611903565b905061107d6006600a611a8c565b611088906010611903565b6110929082611886565b9695505050505050565b6000806110a88361156c565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa15801561110b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061112f9190611818565b61113761096e565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015611196573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111ba9190611818565b6111c490856118eb565b6111ce9190611903565b6111d89190611903565b90506111e66006600a611a8c565b610e7d9082611886565b606061137f565b818153600101919050565b600082840393505b83811015610da25782810151828201511860001a159093029260010161120a565b825b60208210611277578251611242601f836111f7565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09091019060210161122d565b8115610da257825161128c60018403836111f7565b520160010192915050565b60006001830392505b61010782106112d8576112ca8360ff166112c560fd6112c58760081c60e001896111f7565b6111f7565b9350610106820391506112a0565b60078210611305576112fe8360ff166112c5600785036112c58760081c60e001896111f7565b9050610da2565b610e7d8360ff166112c58560081c8560051b01876111f7565b61137782820361135b61134b84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b818110156114b2576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b909118909152840190818303908484106114075750611442565b600184019350611fff821161143c578251600081901a600182901a60081b1760029190911a60101b17810361143c5750611442565b506113ab565b8383106114505750506114b2565b6001830392508583111561146e5761146b878788860361122b565b96505b611482600985016003850160038501611202565b915061148f878284611297565b9650506114a7846114a28684860161131e565b61131e565b91505080935061139f565b50506114c4838384885185010361122b565b925050506040519150618000820180820391508183526020830160005b838110156114f95782810151828201526020016114e1565b506000920191825250602001604052919050565b60008061151d83620cc394611903565b611547907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611a98565b90506115576064620f4240611b0c565b81121561070957610da26064620f4240611b0c565b80516000908190815b818110156115ef5784818151811061158f5761158f611bc8565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036115cf576115c86004846118eb565b92506115dd565b6115da6010846118eb565b92505b806115e781611bf7565b915050611575565b50610e7d826104406118eb565b6000806116088361150d565b90506000611614610f78565b61161c6109cf565b63ffffffff1661162c9190611903565b61163461096e565b61163c610c4e565b611647906010611940565b63ffffffff166116579190611903565b61166191906118eb565b905061166f60066002611903565b61167a90600a611a8c565b6116848284611903565b610e7d9190611886565b6000602082840312156116a057600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156116e857600080fd5b813567ffffffffffffffff8082111561170057600080fd5b818401915084601f83011261171457600080fd5b813581811115611726576117266116a7565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561176c5761176c6116a7565b8160405282815287602084870101111561178557600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b818110156117d2578581018301518582016040015282016117b6565b818111156117e4576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b60006020828403121561182a57600080fd5b5051919050565b60006020828403121561184357600080fd5b815163ffffffff81168114610da257600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000826118bc577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b6000602082840312156118d357600080fd5b815167ffffffffffffffff81168114610da257600080fd5b600082198211156118fe576118fe611857565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561193b5761193b611857565b500290565b600063ffffffff8083168185168183048111821515161561196357611963611857565b02949350505050565b600181815b808511156119c557817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156119ab576119ab611857565b808516156119b857918102915b93841c9390800290611971565b509250929050565b6000826119dc57506001610709565b816119e957506000610709565b81600181146119ff5760028114611a0957611a25565b6001915050610709565b60ff841115611a1a57611a1a611857565b50506001821b610709565b5060208310610133831016604e8410600b8410161715611a48575081810a610709565b611a52838361196c565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611a8457611a84611857565b029392505050565b6000610da283836119cd565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03841381151615611ad257611ad2611857565b827f8000000000000000000000000000000000000000000000000000000000000000038412811615611b0657611b06611857565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600084136000841385830485118282161615611b4d57611b4d611857565b7f80000000000000000000000000000000000000000000000000000000000000006000871286820588128184161615611b8857611b88611857565b60008712925087820587128484161615611ba457611ba4611857565b87850587128184161615611bba57611bba611857565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611c2857611c28611857565b506001019056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/interop_tx_0.hex b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_0.hex new file mode 100644 index 0000000000000..88e1a698cfd1b --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_0.hex @@ -0,0 +1 @@ +7ef906dfa06e5e214f73143df8fe6f6054a3ed7eb472d373376458a9c8aecdf23475beb616944220000000000000000000000000000000000000808080830668a080b9069e6080604052348015600e575f80fd5b506106828061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063331b637f1461004357806354fd4d5014610069578063ab4d6f75146100b2575b5f80fd5b610056610051366004610512565b6100c7565b6040519081526020015b60405180910390f35b6100a56040518060400160405280600581526020017f312e302e3100000000000000000000000000000000000000000000000000000081525081565b604051610060919061053b565b6100c56100c036600461058e565b61039e565b005b5f67ffffffffffffffff801683602001511115610110576040517fd1f79e8200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604083015163ffffffff1015610152576040517f94338eba00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606083015167ffffffffffffffff1015610198576040517f596a19a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b82516040515f916101dd91859060200160609290921b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000168252601482015260340190565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00181528282528051602091820120878201516060890151898501515f9487018590527fffffffffffffffff00000000000000000000000000000000000000000000000060c084811b8216602c8a015283901b1660348801527fffffffff0000000000000000000000000000000000000000000000000000000060e082901b16603c88015292965090949093919291016040516020818303038152906040526102ac906105bc565b90505f85826040516020016102cb929190918252602082015260400190565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0818403018152828252805160209182012060808d01519184018190529183015291505f90606001604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815291905280516020909101207effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f0300000000000000000000000000000000000000000000000000000000000000179a9950505050505050505050565b5f6103b76103b136859003850185610601565b836100c7565b90505f6103c38261043b565b509050806103fd576040517fe3c0081600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b827f5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba78560405161042d9190610622565b60405180910390a250505050565b5f805a835491505a6103e891031115939092509050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610475575f80fd5b919050565b5f60a0828403121561048a575f80fd5b60405160a0810181811067ffffffffffffffff821117156104d2577f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6040529050806104e183610452565b8152602083013560208201526040830135604082015260608301356060820152608083013560808201525092915050565b5f8060c08385031215610523575f80fd5b61052d848461047a565b9460a0939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f8082840360c08112156105a0575f80fd5b60a08112156105ad575f80fd5b50919360a08501359350915050565b805160208083015191908110156105fb577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8160200360031b1b821691505b50919050565b5f60a08284031215610611575f80fd5b61061b838361047a565b9392505050565b60a0810173ffffffffffffffffffffffffffffffffffffffff61064484610452565b168252602083013560208301526040830135604083015260608301356060830152608083013560808301529291505056fea164736f6c6343000819000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/interop_tx_1.hex b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_1.hex new file mode 100644 index 0000000000000..37ce5829c078a --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_1.hex @@ -0,0 +1 @@ +7ef876a088c6b48354c367125a59792a93a7b60ad7cd66e516157dbba16558c68a46d3cb940000000000000000000000000000000000000000944200000000000000000000000000000000000022808082c35080a43659cfe6000000000000000000000000691300f512e48b463c2617b34eef1a9f82ee7dbf \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/interop_tx_2.hex b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_2.hex new file mode 100644 index 0000000000000..4eacc6940f8f8 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_2.hex @@ -0,0 +1 @@ +7ef911efa0f5484697c7a9a791db32a3bf0763bf2ba686c77ae7d4c0a5ee8c222a92a8dcc29442200000000000000000000000000000000000018080808310c8e080b911ae6080604052348015600e575f80fd5b506111928061001c5f395ff3fe6080604052600436106100b8575f3560e01c80637056f41f116100715780638d1d298f1161004c5780638d1d298f14610253578063b1b1b20914610266578063ecc7042814610294575f80fd5b80637056f41f146101b65780637936cbee146101d557806382e3702d14610215575f80fd5b806352617f3c116100a157806352617f3c1461011c57806354fd4d50146101425780636b0c3c5e14610197575f80fd5b806324794462146100bc57806338ffde18146100e3575b5f80fd5b3480156100c7575f80fd5b506100d06102c8565b6040519081526020015b60405180910390f35b3480156100ee575f80fd5b506100f7610347565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100da565b348015610127575f80fd5b5061012f5f81565b60405161ffff90911681526020016100da565b34801561014d575f80fd5b5061018a6040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b6040516100da9190610ca9565b3480156101a2575f80fd5b506100d06101b1366004610d2b565b6103c6565b3480156101c1575f80fd5b506100d06101d0366004610da2565b6104b2565b3480156101e0575f80fd5b506101e96106e5565b6040805173ffffffffffffffffffffffffffffffffffffffff90931683526020830191909152016100da565b348015610220575f80fd5b5061024361022f366004610dfa565b60026020525f908152604090205460ff1681565b60405190151581526020016100da565b61018a610261366004610e11565b610789565b348015610271575f80fd5b50610243610280366004610dfa565b5f6020819052908152604090205460ff1681565b34801561029f575f80fd5b506001547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff166100d0565b5f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c610321576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b507f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75c90565b5f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c6103a0576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b507fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35c90565b5f61040a874688888888888080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250610b0c92505050565b5f8181526002602052604090205490915060ff16610454576040517f6eca2e4b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b858473ffffffffffffffffffffffffffffffffffffffff16887f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3208887876040516104a093929190610e67565b60405180910390a49695505050505050565b5f4685036104ec576040517f8ed9a95d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffbdffffffffffffffffffffffffffffffffffffdd73ffffffffffffffffffffffffffffffffffffffff85160161055b576040517f4faa250900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f6105856001547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b90506105ca864683338989898080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250610b0c92505050565b5f81815260026020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600190811790915580549294507dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff909216919061063583610ed0565b91906101000a8154817dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02191690837dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16021790555050808573ffffffffffffffffffffffffffffffffffffffff16877f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3203388886040516106d493929190610e67565b60405180910390a450949350505050565b5f807ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c61073f576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50507fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35c907f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75c90565b60607ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c156107e4576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60017ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5d73420000000000000000000000000000000000002361082a6020860186610f31565b73ffffffffffffffffffffffffffffffffffffffff1614610877576040517f7987c15700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73420000000000000000000000000000000000002273ffffffffffffffffffffffffffffffffffffffff1663ab4d6f758585856040516108b8929190610f4c565b6040519081900381207fffffffff0000000000000000000000000000000000000000000000000000000060e085901b1682526108f79291600401610f5b565b5f604051808303815f87803b15801561090e575f80fd5b505af1158015610920573d5f803e3d5ffd5b505050505f805f805f6109338888610b4a565b94509450945094509450468514610976576040517f31ac221100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60808901355f61098a878387878a88610b0c565b5f8181526020819052604090205490915060ff16156109d5576040517f9ca9480b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f81815260208190526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055610a158285610c13565b5f8673ffffffffffffffffffffffffffffffffffffffff163485604051610a3c9190610fb4565b5f6040518083038185875af1925050503d805f8114610a76576040519150601f19603f3d011682016040523d82523d5f602084013e610a7b565b606091505b509950905080610a8d57885189602001fd5b8186847fc270d73e26d2d39dee7ef92093555927e344e243415547ecc350b2b5385b68a28c80519060200120604051610ac891815260200190565b60405180910390a4610ada5f80610c13565b50505050505050505f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5d9392505050565b5f868686868686604051602001610b2896959493929190610fca565b6040516020818303038152906040528051906020012090509695505050505050565b5f808080606081610b5e602082898b611020565b810190610b6b9190610dfa565b90507f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3208114610bc6576040517fdf1eb58600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610bd460806020898b611020565b810190610be19190611047565b91975095509350610bf5876080818b611020565b810190610c0291906110a9565b969995985093965092949392505050565b817f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75d807fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35d5050565b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f610cbb6020830184610c5d565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff81168114610ce3575f80fd5b50565b5f8083601f840112610cf6575f80fd5b50813567ffffffffffffffff811115610d0d575f80fd5b602083019150836020828501011115610d24575f80fd5b9250929050565b5f805f805f8060a08789031215610d40575f80fd5b86359550602087013594506040870135610d5981610cc2565b93506060870135610d6981610cc2565b9250608087013567ffffffffffffffff811115610d84575f80fd5b610d9089828a01610ce6565b979a9699509497509295939492505050565b5f805f8060608587031215610db5575f80fd5b843593506020850135610dc781610cc2565b9250604085013567ffffffffffffffff811115610de2575f80fd5b610dee87828801610ce6565b95989497509550505050565b5f60208284031215610e0a575f80fd5b5035919050565b5f805f83850360c0811215610e24575f80fd5b60a0811215610e31575f80fd5b5083925060a084013567ffffffffffffffff811115610e4e575f80fd5b610e5a86828701610ce6565b9497909650939450505050565b73ffffffffffffffffffffffffffffffffffffffff8416815260406020820152816040820152818360608301375f818301606090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016010192915050565b5f7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff808316818103610f27577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6001019392505050565b5f60208284031215610f41575f80fd5b8135610cbb81610cc2565b818382375f9101908152919050565b60c081018335610f6a81610cc2565b73ffffffffffffffffffffffffffffffffffffffff1682526020848101359083015260408085013590830152606080850135908301526080938401359382019390935260a0015290565b5f82518060208501845e5f920191825250919050565b8681528560208201528460408201525f73ffffffffffffffffffffffffffffffffffffffff808616606084015280851660808401525060c060a083015261101460c0830184610c5d565b98975050505050505050565b5f808585111561102e575f80fd5b8386111561103a575f80fd5b5050820193919092039150565b5f805f60608486031215611059575f80fd5b83359250602084013561106b81610cc2565b929592945050506040919091013590565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f80604083850312156110ba575f80fd5b82356110c581610cc2565b9150602083013567ffffffffffffffff808211156110e1575f80fd5b818501915085601f8301126110f4575f80fd5b8135818111156111065761110661107c565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561114c5761114c61107c565b81604052828152886020848701011115611164575f80fd5b826020860160208301375f602084830101528095505050505050925092905056fea164736f6c6343000819000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/interop_tx_3.hex b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_3.hex new file mode 100644 index 0000000000000..c40d0530f48b0 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/interop_tx_3.hex @@ -0,0 +1 @@ +7ef876a0e54b4d06bbcc857f41ae00e89d820339ac5ce0034aac722c817b2873e03a7e68940000000000000000000000000000000000000000944200000000000000000000000000000000000023808082c35080a43659cfe60000000000000000000000000d0edd0ebd0e94d218670a8de867eb5c4d37cadd diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_0.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_0.hex new file mode 100644 index 0000000000000..001d15e755fe0 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_0.hex @@ -0,0 +1 @@ +7ef9070fa03b2d0821ca2411ad5cd3595804d1213d15737188ae4cbd58aa19c821a6c211bf94421000000000000000000000000000000000000380808083067c2880b906ce608060405234801561001057600080fd5b506106ae806100206000396000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c806364ca23ef116100d8578063b80777ea1161008c578063e591b28211610066578063e591b282146103b0578063e81b2c6d146103d2578063f8206140146103db57600080fd5b8063b80777ea14610337578063c598591814610357578063d84447151461037757600080fd5b80638381f58a116100bd5780638381f58a146103115780638b239f73146103255780639e8c49661461032e57600080fd5b806364ca23ef146102e157806368d5dca6146102f557600080fd5b80634397dfef1161013a57806354fd4d501161011457806354fd4d501461025d578063550fcdc91461029f5780635cf24969146102d857600080fd5b80634397dfef146101fc578063440a5e20146102245780634d5d9a2a1461022c57600080fd5b806309bd5a601161016b57806309bd5a60146101a457806316d3bc7f146101c057806321326849146101ed57600080fd5b8063015d8eb914610187578063098999be1461019c575b600080fd5b61019a6101953660046105bc565b6103e4565b005b61019a610523565b6101ad60025481565b6040519081526020015b60405180910390f35b6008546101d49067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101b7565b604051600081526020016101b7565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101b7565b61019a61052d565b6008546102489068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101b7565b60408051808201909152600581527f312e362e3000000000000000000000000000000000000000000000000000000060208201525b6040516101b7919061062e565b60408051808201909152600381527f45544800000000000000000000000000000000000000000000000000000000006020820152610292565b6101ad60015481565b6003546101d49067ffffffffffffffff1681565b6003546102489068010000000000000000900463ffffffff1681565b6000546101d49067ffffffffffffffff1681565b6101ad60055481565b6101ad60065481565b6000546101d49068010000000000000000900467ffffffffffffffff1681565b600354610248906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f45746865720000000000000000000000000000000000000000000000000000006020820152610292565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101b7565b6101ad60045481565b6101ad60075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461048b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61052b610535565b565b61052b610548565b61053d610548565b60a43560a01c600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead000133811461057257633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff811681146105b757600080fd5b919050565b600080600080600080600080610100898b0312156105d957600080fd5b6105e28961059f565b97506105f060208a0161059f565b9650604089013595506060890135945061060c60808a0161059f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b8181101561065b5785810183015185820160400152820161063f565b8181111561066d576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_1.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_1.hex new file mode 100644 index 0000000000000..722b6c156a731 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_1.hex @@ -0,0 +1 @@ +7ef91c9da0fc70b48424763fa3fab9844253b4f8d508f91eb1f7cb11a247c9baec0afb80359442100000000000000000000000000000000000048080808318cba880b91c5c608060405234801561001057600080fd5b50611c3c806100206000396000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806368d5dca6116100d8578063c59859181161008c578063f45e65d811610066578063f45e65d8146102ca578063f8206140146102d2578063fe173b971461026957600080fd5b8063c59859181461029c578063de26c4a1146102a4578063f1c7a58b146102b757600080fd5b80638e98b106116100bd5780638e98b1061461026f578063960e3a2314610277578063b54501bc1461028957600080fd5b806368d5dca61461024c5780636ef25c3a1461026957600080fd5b8063313ce5671161012f5780634ef6e224116101145780634ef6e224146101de578063519b4bd3146101fb57806354fd4d501461020357600080fd5b8063313ce567146101c457806349948e0e146101cb57600080fd5b8063275aedd211610160578063275aedd2146101a1578063291b0383146101b45780632e0f2625146101bc57600080fd5b80630c18c1621461017c57806322b90ab314610197575b600080fd5b6101846102da565b6040519081526020015b60405180910390f35b61019f6103fb565b005b6101846101af36600461168e565b610584565b61019f61070f565b610184600681565b6006610184565b6101846101d93660046116d6565b610937565b6000546101eb9060ff1681565b604051901515815260200161018e565b61018461096e565b61023f6040518060400160405280600581526020017f312e342e3000000000000000000000000000000000000000000000000000000081525081565b60405161018e91906117a5565b6102546109cf565b60405163ffffffff909116815260200161018e565b48610184565b61019f610a54565b6000546101eb90610100900460ff1681565b6000546101eb9062010000900460ff1681565b610254610c4e565b6101846102b23660046116d6565b610caf565b6101846102c536600461168e565b610da9565b610184610e85565b610184610f78565b6000805460ff1615610373576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611818565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104c4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b60005460ff1615610557576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805462010000900460ff1661059d57506000919050565b610709620f42406106668473420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16634d5d9a2a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610607573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062b9190611831565b63ffffffff167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821583830293840490921491909117011790565b6106709190611886565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166316d3bc7f6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106cf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106f391906118c1565b67ffffffffffffffff1681019081106000031790565b92915050565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146107d8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973497374686d757320666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b600054610100900460ff1661086f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20497374686d75732063616e206f6e6c7960448201527f2062652061637469766174656420616674657220466a6f726400000000000000606482015260840161036a565b60005462010000900460ff1615610908576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a20497374686d757320616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff1662010000179055565b60008054610100900460ff16156109515761070982610fd9565b60005460ff16156109655761070982610ff8565b6107098261109c565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611831565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610af7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c616700606482015260840161036a565b60005460ff16610b89576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e6500000000000000606482015260840161036a565b600054610100900460ff1615610c20576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f7469766500000000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b60008054610100900460ff1615610cf657620f4240610ce1610cd0846111f0565b51610cdc9060446118eb565b61150d565b610cec906010611903565b6107099190611886565b6000610d018361156c565b60005490915060ff1615610d155792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d74573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d989190611818565b610da290826118eb565b9392505050565b60008054610100900460ff16610e41576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f726400000000000000000000606482015260840161036a565b6000610e4e8360446118eb565b90506000610e5d60ff83611886565b610e6790836118eb565b610e729060106118eb565b9050610e7d816115fc565b949350505050565b6000805460ff1615610f19576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161036a565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b6000610709610fe7836111f0565b51610ff39060446118eb565b6115fc565b6000806110048361156c565b9050600061101061096e565b611018610c4e565b611023906010611940565b63ffffffff166110339190611903565b9050600061103f610f78565b6110476109cf565b63ffffffff166110579190611903565b9050600061106582846118eb565b61106f9085611903565b905061107d6006600a611a8c565b611088906010611903565b6110929082611886565b9695505050505050565b6000806110a88361156c565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa15801561110b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061112f9190611818565b61113761096e565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015611196573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111ba9190611818565b6111c490856118eb565b6111ce9190611903565b6111d89190611903565b90506111e66006600a611a8c565b610e7d9082611886565b606061137f565b818153600101919050565b600082840393505b83811015610da25782810151828201511860001a159093029260010161120a565b825b60208210611277578251611242601f836111f7565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09091019060210161122d565b8115610da257825161128c60018403836111f7565b520160010192915050565b60006001830392505b61010782106112d8576112ca8360ff166112c560fd6112c58760081c60e001896111f7565b6111f7565b9350610106820391506112a0565b60078210611305576112fe8360ff166112c5600785036112c58760081c60e001896111f7565b9050610da2565b610e7d8360ff166112c58560081c8560051b01876111f7565b61137782820361135b61134b84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b818110156114b2576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b909118909152840190818303908484106114075750611442565b600184019350611fff821161143c578251600081901a600182901a60081b1760029190911a60101b17810361143c5750611442565b506113ab565b8383106114505750506114b2565b6001830392508583111561146e5761146b878788860361122b565b96505b611482600985016003850160038501611202565b915061148f878284611297565b9650506114a7846114a28684860161131e565b61131e565b91505080935061139f565b50506114c4838384885185010361122b565b925050506040519150618000820180820391508183526020830160005b838110156114f95782810151828201526020016114e1565b506000920191825250602001604052919050565b60008061151d83620cc394611903565b611547907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611a98565b90506115576064620f4240611b0c565b81121561070957610da26064620f4240611b0c565b80516000908190815b818110156115ef5784818151811061158f5761158f611bc8565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036115cf576115c86004846118eb565b92506115dd565b6115da6010846118eb565b92505b806115e781611bf7565b915050611575565b50610e7d826104406118eb565b6000806116088361150d565b90506000611614610f78565b61161c6109cf565b63ffffffff1661162c9190611903565b61163461096e565b61163c610c4e565b611647906010611940565b63ffffffff166116579190611903565b61166191906118eb565b905061166f60066002611903565b61167a90600a611a8c565b6116848284611903565b610e7d9190611886565b6000602082840312156116a057600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156116e857600080fd5b813567ffffffffffffffff8082111561170057600080fd5b818401915084601f83011261171457600080fd5b813581811115611726576117266116a7565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561176c5761176c6116a7565b8160405282815287602084870101111561178557600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b818110156117d2578581018301518582016040015282016117b6565b818111156117e4576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b60006020828403121561182a57600080fd5b5051919050565b60006020828403121561184357600080fd5b815163ffffffff81168114610da257600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000826118bc577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b6000602082840312156118d357600080fd5b815167ffffffffffffffff81168114610da257600080fd5b600082198211156118fe576118fe611857565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561193b5761193b611857565b500290565b600063ffffffff8083168185168183048111821515161561196357611963611857565b02949350505050565b600181815b808511156119c557817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156119ab576119ab611857565b808516156119b857918102915b93841c9390800290611971565b509250929050565b6000826119dc57506001610709565b816119e957506000610709565b81600181146119ff5760028114611a0957611a25565b6001915050610709565b60ff841115611a1a57611a1a611857565b50506001821b610709565b5060208310610133831016604e8410600b8410161715611a48575081810a610709565b611a52838361196c565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611a8457611a84611857565b029392505050565b6000610da283836119cd565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03841381151615611ad257611ad2611857565b827f8000000000000000000000000000000000000000000000000000000000000000038412811615611b0657611b06611857565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600084136000841385830485118282161615611b4d57611b4d611857565b7f80000000000000000000000000000000000000000000000000000000000000006000871286820588128184161615611b8857611b88611857565b60008712925087820587128484161615611ba457611ba4611857565b87850587128184161615611bba57611bba611857565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611c2857611c28611857565b506001019056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_2.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_2.hex new file mode 100644 index 0000000000000..1eacb78dc6f54 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_2.hex @@ -0,0 +1 @@ +7ef908d7a0107a570d3db75e6110817eb024f09f3172657e920634111ce9875d08a16daa969442100000000000000000000000000000000000058080808307a12080b9089660e060405234801561001057600080fd5b5073420000000000000000000000000000000000001960a0526000608052600160c05260805160a05160c0516107ef6100a7600039600081816101b3015281816102450152818161044b015261048601526000818160b8015281816101800152818161039a01528181610429015281816104c201526105b70152600081816101ef01528181610279015261029d01526107ef6000f3fe60806040526004361061009a5760003560e01c806382356d8a1161006957806384411d651161004e57806384411d651461021d578063d0e12f9014610233578063d3e5792b1461026757600080fd5b806382356d8a146101a45780638312f149146101e057600080fd5b80630d9019e1146100a65780633ccfd60b1461010457806354fd4d501461011b57806366d003ac1461017157600080fd5b366100a157005b600080fd5b3480156100b257600080fd5b506100da7f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561011057600080fd5b5061011961029b565b005b34801561012757600080fd5b506101646040518060400160405280600581526020017f312e302e3000000000000000000000000000000000000000000000000000000081525081565b6040516100fb9190610671565b34801561017d57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006100da565b3480156101b057600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040516100fb919061074e565b3480156101ec57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040519081526020016100fb565b34801561022957600080fd5b5061020f60005481565b34801561023f57600080fd5b506101d37f000000000000000000000000000000000000000000000000000000000000000081565b34801561027357600080fd5b5061020f7f000000000000000000000000000000000000000000000000000000000000000081565b7f0000000000000000000000000000000000000000000000000000000000000000471015610376576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f4665655661756c743a207769746864726177616c20616d6f756e74206d75737460448201527f2062652067726561746572207468616e206d696e696d756d207769746864726160648201527f77616c20616d6f756e7400000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b60004790508060008082825461038c9190610762565b9091555050604080518281527f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166020820152338183015290517fc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba9181900360600190a17f38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee817f0000000000000000000000000000000000000000000000000000000000000000337f000000000000000000000000000000000000000000000000000000000000000060405161047a94939291906107a1565b60405180910390a160017f000000000000000000000000000000000000000000000000000000000000000060018111156104b6576104b66106e4565b0361057a5760006104e77f000000000000000000000000000000000000000000000000000000000000000083610649565b905080610576576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4665655661756c743a206661696c656420746f2073656e642045544820746f2060448201527f4c322066656520726563697069656e7400000000000000000000000000000000606482015260840161036d565b5050565b6040517fc2b3e5ac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600482015262061a80602482015260606044820152600060648201527342000000000000000000000000000000000000169063c2b3e5ac9083906084016000604051808303818588803b15801561062d57600080fd5b505af1158015610641573d6000803e3d6000fd5b505050505050565b6000610656835a8461065d565b9392505050565b6000806000806000858888f1949350505050565b600060208083528351808285015260005b8181101561069e57858101830151858201604001528201610682565b818111156106b0576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b6002811061074a577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b9052565b6020810161075c8284610713565b92915050565b6000821982111561079c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b500190565b84815273ffffffffffffffffffffffffffffffffffffffff848116602083015283166040820152608081016107d96060830184610713565b9594505050505056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_3.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_3.hex new file mode 100644 index 0000000000000..a85a46a691cca --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_3.hex @@ -0,0 +1 @@ +7ef876a0ebe8b5cb10ca47e0d8bda8f5355f2d66711a54ddeb0ef1d30e29418c9bf17a0e940000000000000000000000000000000000000000944200000000000000000000000000000000000015808082c35080a43659cfe6000000000000000000000000ff256497d61dcd71a9e9ff43967c13fde1f72d12 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_4.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_4.hex new file mode 100644 index 0000000000000..6421a0c88d44b --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_4.hex @@ -0,0 +1 @@ +7ef876a0ecf2d9161d26c54eda6b7bfdd9142719b1e1199a6e5641468d1bf705bc531ab094000000000000000000000000000000000000000094420000000000000000000000000000000000000f808082c35080a43659cfe600000000000000000000000093e57a196454cb919193fa9946f14943cf733845 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_5.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_5.hex new file mode 100644 index 0000000000000..a18856f126ed4 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_5.hex @@ -0,0 +1 @@ +7ef876a0ad74e1adb877ccbe176b8fa1cc559388a16e090ddbe8b512f5b37d07d887a92794000000000000000000000000000000000000000094420000000000000000000000000000000000001b808082c35080a43659cfe60000000000000000000000004fa2be8cd41504037f1838bce3bcc93bc68ff537 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_6.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_6.hex new file mode 100644 index 0000000000000..061a6b727d960 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_6.hex @@ -0,0 +1 @@ +7ef857a03ddf4b1302548dd92939826e970f260ba36167f4c25f18390a5e8b194b29531994deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000000f808083015f908084291b0383 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_7.hex b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_7.hex new file mode 100644 index 0000000000000..40d4c1152a273 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/isthmus_tx_7.hex @@ -0,0 +1 @@ +7ef89ca0bfb734dae514c5974ddf803e54c1bc43d5cdb4a48ae27e1d9b875a5a150b553a943462413af4609098e1e27a490f554f260213d6858080808303d09080b85c60538060095f395ff33373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500 \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/jovian-gas-price-oracle-deployment.hex b/kona/crates/protocol/hardforks/src/bytecode/jovian-gas-price-oracle-deployment.hex new file mode 100644 index 0000000000000..c3227a9762b75 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/jovian-gas-price-oracle-deployment.hex @@ -0,0 +1 @@ +0x608060405234801561001057600080fd5b50611ea8806100206000396000f3fe608060405234801561001057600080fd5b506004361061018d5760003560e01c806368d5dca6116100e3578063c59859181161008c578063f45e65d811610066578063f45e65d8146102fc578063f820614014610304578063fe173b971461029357600080fd5b8063c5985918146102ce578063de26c4a1146102d6578063f1c7a58b146102e957600080fd5b8063960e3a23116100bd578063960e3a23146102a1578063b3d72079146102b3578063b54501bc146102bb57600080fd5b806368d5dca6146102765780636ef25c3a146102935780638e98b1061461029957600080fd5b80632e0f2625116101455780634ef6e2241161011f5780634ef6e22414610218578063519b4bd31461022557806354fd4d501461022d57600080fd5b80632e0f2625146101f6578063313ce567146101fe57806349948e0e1461020557600080fd5b806322b90ab31161017657806322b90ab3146101d1578063275aedd2146101db578063291b0383146101ee57600080fd5b80630c18c16214610192578063105d0b81146101ad575b600080fd5b61019a61030c565b6040519081526020015b60405180910390f35b6000546101c1906301000000900460ff1681565b60405190151581526020016101a4565b6101d961042d565b005b61019a6101e93660046118fa565b6105b6565b6101d9610776565b61019a600681565b600661019a565b61019a610213366004611942565b61099e565b6000546101c19060ff1681565b61019a6109db565b6102696040518060400160405280600581526020017f312e362e3000000000000000000000000000000000000000000000000000000081525081565b6040516101a49190611a11565b61027e610a3c565b60405163ffffffff90911681526020016101a4565b4861019a565b6101d9610ac1565b6000546101c190610100900460ff1681565b6101d9610cbb565b6000546101c19062010000900460ff1681565b61027e610ec2565b61019a6102e4366004611942565b610f23565b61019a6102f73660046118fa565b61101d565b61019a6110f1565b61019a6111e4565b6000805460ff16156103a5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610404573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104289190611a84565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104f6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161039c565b60005460ff1615610589576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161039c565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805462010000900460ff166105cf57506000919050565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16634d5d9a2a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610630573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106549190611a9d565b63ffffffff169050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166316d3bc7f6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106e19190611ac3565b67ffffffffffffffff169050600060039054906101000a900460ff161561072a578061070d8386611b1c565b610718906064611b1c565b6107229190611b59565b949350505050565b610722620f424083860286810485148715177fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01176107699190611b71565b8281019081106000031790565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461083f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973497374686d757320666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161039c565b600054610100900460ff166108d6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20497374686d75732063616e206f6e6c7960448201527f2062652061637469766174656420616674657220466a6f726400000000000000606482015260840161039c565b60005462010000900460ff161561096f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a20497374686d757320616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161039c565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff1662010000179055565b60008054610100900460ff16156109be576109b882611245565b92915050565b60005460ff16156109d2576109b882611264565b6109b882611308565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa158015610404573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a9d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104289190611a9d565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610b64576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c616700606482015260840161039c565b60005460ff16610bf6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e6500000000000000606482015260840161039c565b600054610100900460ff1615610c8d576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f7469766500000000000000000000000000000000000000000000000000000000606482015260840161039c565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610d6057604080517f08c379a00000000000000000000000000000000000000000000000000000000081526020600482015260248101919091527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e207365742069734a6f7669616e20666c6167606482015260840161039c565b60005462010000900460ff16610df8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603a60248201527f47617350726963654f7261636c653a204a6f7669616e2063616e206f6e6c792060448201527f62652061637469766174656420616674657220497374686d7573000000000000606482015260840161039c565b6000546301000000900460ff1615610e92576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f47617350726963654f7261636c653a204a6f7669616e20616c7265616479206160448201527f6374697665000000000000000000000000000000000000000000000000000000606482015260840161039c565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffff166301000000179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a9d573d6000803e3d6000fd5b60008054610100900460ff1615610f6a57620f4240610f55610f448461145c565b51610f50906044611b59565b611779565b610f60906010611b1c565b6109b89190611b71565b6000610f75836117d8565b60005490915060ff1615610f895792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610fe8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061100c9190611a84565b6110169082611b59565b9392505050565b60008054610100900460ff166110b5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f726400000000000000000000606482015260840161039c565b60006110c2836044611b59565b905060006110d160ff83611b71565b6110db9083611b59565b6110e6906010611b59565b905061072281611868565b6000805460ff1615611185576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161039c565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015610404573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa158015610404573d6000803e3d6000fd5b60006109b86112538361145c565b5161125f906044611b59565b611868565b600080611270836117d8565b9050600061127c6109db565b611284610ec2565b61128f906010611bac565b63ffffffff1661129f9190611b1c565b905060006112ab6111e4565b6112b3610a3c565b63ffffffff166112c39190611b1c565b905060006112d18284611b59565b6112db9085611b1c565b90506112e96006600a611cf8565b6112f4906010611b1c565b6112fe9082611b71565b9695505050505050565b600080611314836117d8565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa158015611377573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061139b9190611a84565b6113a36109db565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015611402573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114269190611a84565b6114309085611b59565b61143a9190611b1c565b6114449190611b1c565b90506114526006600a611cf8565b6107229082611b71565b60606115eb565b818153600101919050565b600082840393505b838110156110165782810151828201511860001a1590930292600101611476565b825b602082106114e35782516114ae601f83611463565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090910190602101611499565b81156110165782516114f86001840383611463565b520160010192915050565b60006001830392505b6101078210611544576115368360ff1661153160fd6115318760081c60e00189611463565b611463565b93506101068203915061150c565b600782106115715761156a8360ff16611531600785036115318760081c60e00189611463565b9050611016565b6107228360ff166115318560081c8560051b0187611463565b6115e38282036115c76115b784600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b8181101561171e576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b9091189091528401908183039084841061167357506116ae565b600184019350611fff82116116a8578251600081901a600182901a60081b1760029190911a60101b1781036116a857506116ae565b50611617565b8383106116bc57505061171e565b600183039250858311156116da576116d78787888603611497565b96505b6116ee60098501600385016003850161146e565b91506116fb878284611503565b9650506117138461170e8684860161158a565b61158a565b91505080935061160b565b50506117308383848851850103611497565b925050506040519150618000820180820391508183526020830160005b8381101561176557828101518282015260200161174d565b506000920191825250602001604052919050565b60008061178983620cc394611b1c565b6117b3907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611d04565b90506117c36064620f4240611d78565b8112156109b8576110166064620f4240611d78565b80516000908190815b8181101561185b578481815181106117fb576117fb611e34565b01602001517fff000000000000000000000000000000000000000000000000000000000000001660000361183b57611834600484611b59565b9250611849565b611846601084611b59565b92505b8061185381611e63565b9150506117e1565b5061072282610440611b59565b60008061187483611779565b905060006118806111e4565b611888610a3c565b63ffffffff166118989190611b1c565b6118a06109db565b6118a8610ec2565b6118b3906010611bac565b63ffffffff166118c39190611b1c565b6118cd9190611b59565b90506118db60066002611b1c565b6118e690600a611cf8565b6118f08284611b1c565b6107229190611b71565b60006020828403121561190c57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b60006020828403121561195457600080fd5b813567ffffffffffffffff8082111561196c57600080fd5b818401915084601f83011261198057600080fd5b81358181111561199257611992611913565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f011681019083821181831017156119d8576119d8611913565b816040528281528760208487010111156119f157600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b81811015611a3e57858101830151858201604001528201611a22565b81811115611a50576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b600060208284031215611a9657600080fd5b5051919050565b600060208284031215611aaf57600080fd5b815163ffffffff8116811461101657600080fd5b600060208284031215611ad557600080fd5b815167ffffffffffffffff8116811461101657600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615611b5457611b54611aed565b500290565b60008219821115611b6c57611b6c611aed565b500190565b600082611ba7577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b600063ffffffff80831681851681830481118215151615611bcf57611bcf611aed565b02949350505050565b600181815b80851115611c3157817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611c1757611c17611aed565b80851615611c2457918102915b93841c9390800290611bdd565b509250929050565b600082611c48575060016109b8565b81611c55575060006109b8565b8160018114611c6b5760028114611c7557611c91565b60019150506109b8565b60ff841115611c8657611c86611aed565b50506001821b6109b8565b5060208310610133831016604e8410600b8410161715611cb4575081810a6109b8565b611cbe8383611bd8565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611cf057611cf0611aed565b029392505050565b60006110168383611c39565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03841381151615611d3e57611d3e611aed565b827f8000000000000000000000000000000000000000000000000000000000000000038412811615611d7257611d72611aed565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600084136000841385830485118282161615611db957611db9611aed565b7f80000000000000000000000000000000000000000000000000000000000000006000871286820588128184161615611df457611df4611aed565b60008712925087820587128484161615611e1057611e10611aed565b87850587128184161615611e2657611e26611aed565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611e9457611e94611aed565b506001019056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/jovian-l1-block-deployment.hex b/kona/crates/protocol/hardforks/src/bytecode/jovian-l1-block-deployment.hex new file mode 100644 index 0000000000000..b8216914b5d23 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/jovian-l1-block-deployment.hex @@ -0,0 +1 @@ +0x608060405234801561001057600080fd5b50610715806100206000396000f3fe608060405234801561001057600080fd5b50600436106101985760003560e01c806364ca23ef116100e3578063c59859181161008c578063e81b2c6d11610066578063e81b2c6d146103f0578063f8206140146103f9578063fe3d57101461040257600080fd5b8063c598591814610375578063d844471514610395578063e591b282146103ce57600080fd5b80638b239f73116100bd5780638b239f73146103435780639e8c49661461034c578063b80777ea1461035557600080fd5b806364ca23ef146102ff57806368d5dca6146103135780638381f58a1461032f57600080fd5b80634397dfef1161014557806354fd4d501161011f57806354fd4d501461027b578063550fcdc9146102bd5780635cf24969146102f657600080fd5b80634397dfef1461021a578063440a5e20146102425780634d5d9a2a1461024a57600080fd5b806316d3bc7f1161017657806316d3bc7f146101d657806321326849146102035780633db6be2b1461021257600080fd5b8063015d8eb91461019d578063098999be146101b257806309bd5a60146101ba575b600080fd5b6101b06101ab366004610623565b610433565b005b6101b0610572565b6101c360025481565b6040519081526020015b60405180910390f35b6008546101ea9067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101cd565b604051600081526020016101cd565b6101b0610585565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101cd565b6101b06105af565b6008546102669068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101cd565b60408051808201909152600581527f312e372e3000000000000000000000000000000000000000000000000000000060208201525b6040516101cd9190610695565b60408051808201909152600381527f455448000000000000000000000000000000000000000000000000000000000060208201526102b0565b6101c360015481565b6003546101ea9067ffffffffffffffff1681565b6003546102669068010000000000000000900463ffffffff1681565b6000546101ea9067ffffffffffffffff1681565b6101c360055481565b6101c360065481565b6000546101ea9068010000000000000000900467ffffffffffffffff1681565b600354610266906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f457468657200000000000000000000000000000000000000000000000000000060208201526102b0565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101cd565b6101c360045481565b6101c360075481565b600854610420906c01000000000000000000000000900461ffff1681565b60405161ffff90911681526020016101cd565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104da576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61057a6105af565b60a43560a01c600855565b61058d6105af565b6dffff00000000000000000000000060b03560901c1660a43560a01c17600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead00013381146105d957633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff8116811461061e57600080fd5b919050565b600080600080600080600080610100898b03121561064057600080fd5b61064989610606565b975061065760208a01610606565b9650604089013595506060890135945061067360808a01610606565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b818110156106c2578581018301518582016040015282016106a6565b818111156106d4576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a \ No newline at end of file diff --git a/kona/crates/protocol/hardforks/src/bytecode/l1_block_ecotone.hex b/kona/crates/protocol/hardforks/src/bytecode/l1_block_ecotone.hex new file mode 100644 index 0000000000000..2baf6f7069cea --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/l1_block_ecotone.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5061053e806100206000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c80638381f58a11610097578063c598591811610066578063c598591814610229578063e591b28214610249578063e81b2c6d14610289578063f82061401461029257600080fd5b80638381f58a146101e35780638b239f73146101f75780639e8c496614610200578063b80777ea1461020957600080fd5b806354fd4d50116100d357806354fd4d50146101335780635cf249691461017c57806364ca23ef1461018557806368d5dca6146101b257600080fd5b8063015d8eb9146100fa57806309bd5a601461010f578063440a5e201461012b575b600080fd5b61010d61010836600461044c565b61029b565b005b61011860025481565b6040519081526020015b60405180910390f35b61010d6103da565b61016f6040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b60405161012291906104be565b61011860015481565b6003546101999067ffffffffffffffff1681565b60405167ffffffffffffffff9091168152602001610122565b6003546101ce9068010000000000000000900463ffffffff1681565b60405163ffffffff9091168152602001610122565b6000546101999067ffffffffffffffff1681565b61011860055481565b61011860065481565b6000546101999068010000000000000000900467ffffffffffffffff1681565b6003546101ce906c01000000000000000000000000900463ffffffff1681565b61026473deaddeaddeaddeaddeaddeaddeaddeaddead000181565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610122565b61011860045481565b61011860075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610342576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461040357633cc50b456000526004601cfd5b60043560801c60035560143560801c600055602435600155604435600755606435600255608435600455565b803567ffffffffffffffff8116811461044757600080fd5b919050565b600080600080600080600080610100898b03121561046957600080fd5b6104728961042f565b975061048060208a0161042f565b9650604089013595506060890135945061049c60808a0161042f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b818110156104eb578581018301518582016040015282016104cf565b818111156104fd576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/l1_block_isthmus.hex b/kona/crates/protocol/hardforks/src/bytecode/l1_block_isthmus.hex new file mode 100644 index 0000000000000..7facd7fc2359a --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/l1_block_isthmus.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506106ae806100206000396000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c806364ca23ef116100d8578063b80777ea1161008c578063e591b28211610066578063e591b282146103b0578063e81b2c6d146103d2578063f8206140146103db57600080fd5b8063b80777ea14610337578063c598591814610357578063d84447151461037757600080fd5b80638381f58a116100bd5780638381f58a146103115780638b239f73146103255780639e8c49661461032e57600080fd5b806364ca23ef146102e157806368d5dca6146102f557600080fd5b80634397dfef1161013a57806354fd4d501161011457806354fd4d501461025d578063550fcdc91461029f5780635cf24969146102d857600080fd5b80634397dfef146101fc578063440a5e20146102245780634d5d9a2a1461022c57600080fd5b806309bd5a601161016b57806309bd5a60146101a457806316d3bc7f146101c057806321326849146101ed57600080fd5b8063015d8eb914610187578063098999be1461019c575b600080fd5b61019a6101953660046105bc565b6103e4565b005b61019a610523565b6101ad60025481565b6040519081526020015b60405180910390f35b6008546101d49067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101b7565b604051600081526020016101b7565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101b7565b61019a61052d565b6008546102489068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101b7565b60408051808201909152600581527f312e362e3000000000000000000000000000000000000000000000000000000060208201525b6040516101b7919061062e565b60408051808201909152600381527f45544800000000000000000000000000000000000000000000000000000000006020820152610292565b6101ad60015481565b6003546101d49067ffffffffffffffff1681565b6003546102489068010000000000000000900463ffffffff1681565b6000546101d49067ffffffffffffffff1681565b6101ad60055481565b6101ad60065481565b6000546101d49068010000000000000000900467ffffffffffffffff1681565b600354610248906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f45746865720000000000000000000000000000000000000000000000000000006020820152610292565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101b7565b6101ad60045481565b6101ad60075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461048b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61052b610535565b565b61052b610548565b61053d610548565b60a43560a01c600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead000133811461057257633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff811681146105b757600080fd5b919050565b600080600080600080600080610100898b0312156105d957600080fd5b6105e28961059f565b97506105f060208a0161059f565b9650604089013595506060890135945061060c60808a0161059f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b8181101561065b5785810183015185820160400152820161063f565b8181111561066d576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/l2tol2_xdm_interop.hex b/kona/crates/protocol/hardforks/src/bytecode/l2tol2_xdm_interop.hex new file mode 100644 index 0000000000000..96e184e6c7d0c --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/l2tol2_xdm_interop.hex @@ -0,0 +1 @@ +6080604052348015600e575f80fd5b506111928061001c5f395ff3fe6080604052600436106100b8575f3560e01c80637056f41f116100715780638d1d298f1161004c5780638d1d298f14610253578063b1b1b20914610266578063ecc7042814610294575f80fd5b80637056f41f146101b65780637936cbee146101d557806382e3702d14610215575f80fd5b806352617f3c116100a157806352617f3c1461011c57806354fd4d50146101425780636b0c3c5e14610197575f80fd5b806324794462146100bc57806338ffde18146100e3575b5f80fd5b3480156100c7575f80fd5b506100d06102c8565b6040519081526020015b60405180910390f35b3480156100ee575f80fd5b506100f7610347565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100da565b348015610127575f80fd5b5061012f5f81565b60405161ffff90911681526020016100da565b34801561014d575f80fd5b5061018a6040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b6040516100da9190610ca9565b3480156101a2575f80fd5b506100d06101b1366004610d2b565b6103c6565b3480156101c1575f80fd5b506100d06101d0366004610da2565b6104b2565b3480156101e0575f80fd5b506101e96106e5565b6040805173ffffffffffffffffffffffffffffffffffffffff90931683526020830191909152016100da565b348015610220575f80fd5b5061024361022f366004610dfa565b60026020525f908152604090205460ff1681565b60405190151581526020016100da565b61018a610261366004610e11565b610789565b348015610271575f80fd5b50610243610280366004610dfa565b5f6020819052908152604090205460ff1681565b34801561029f575f80fd5b506001547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff166100d0565b5f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c610321576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b507f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75c90565b5f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c6103a0576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b507fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35c90565b5f61040a874688888888888080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250610b0c92505050565b5f8181526002602052604090205490915060ff16610454576040517f6eca2e4b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b858473ffffffffffffffffffffffffffffffffffffffff16887f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3208887876040516104a093929190610e67565b60405180910390a49695505050505050565b5f4685036104ec576040517f8ed9a95d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffbdffffffffffffffffffffffffffffffffffffdd73ffffffffffffffffffffffffffffffffffffffff85160161055b576040517f4faa250900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f6105856001547dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1690565b90506105ca864683338989898080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250610b0c92505050565b5f81815260026020526040812080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600190811790915580549294507dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff909216919061063583610ed0565b91906101000a8154817dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff02191690837dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff16021790555050808573ffffffffffffffffffffffffffffffffffffffff16877f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3203388886040516106d493929190610e67565b60405180910390a450949350505050565b5f807ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c61073f576040517fbca35af600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50507fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35c907f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75c90565b60607ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5c156107e4576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60017ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5d73420000000000000000000000000000000000002361082a6020860186610f31565b73ffffffffffffffffffffffffffffffffffffffff1614610877576040517f7987c15700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73420000000000000000000000000000000000002273ffffffffffffffffffffffffffffffffffffffff1663ab4d6f758585856040516108b8929190610f4c565b6040519081900381207fffffffff0000000000000000000000000000000000000000000000000000000060e085901b1682526108f79291600401610f5b565b5f604051808303815f87803b15801561090e575f80fd5b505af1158015610920573d5f803e3d5ffd5b505050505f805f805f6109338888610b4a565b94509450945094509450468514610976576040517f31ac221100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60808901355f61098a878387878a88610b0c565b5f8181526020819052604090205490915060ff16156109d5576040517f9ca9480b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f81815260208190526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055610a158285610c13565b5f8673ffffffffffffffffffffffffffffffffffffffff163485604051610a3c9190610fb4565b5f6040518083038185875af1925050503d805f8114610a76576040519150601f19603f3d011682016040523d82523d5f602084013e610a7b565b606091505b509950905080610a8d57885189602001fd5b8186847fc270d73e26d2d39dee7ef92093555927e344e243415547ecc350b2b5385b68a28c80519060200120604051610ac891815260200190565b60405180910390a4610ada5f80610c13565b50505050505050505f7ff13569814868ede994184d5a425471fb19e869768a33421cb701a2ba3d420c0a5d9392505050565b5f868686868686604051602001610b2896959493929190610fca565b6040516020818303038152906040528051906020012090509695505050505050565b5f808080606081610b5e602082898b611020565b810190610b6b9190610dfa565b90507f382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f3208114610bc6576040517fdf1eb58600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610bd460806020898b611020565b810190610be19190611047565b91975095509350610bf5876080818b611020565b810190610c0291906110a9565b969995985093965092949392505050565b817f711dfa3259c842fffc17d6e1f1e0fc5927756133a2345ca56b4cb8178589fee75d807fb83444d07072b122e2e72a669ce32857d892345c19856f4e7142d06a167ab3f35d5050565b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f610cbb6020830184610c5d565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff81168114610ce3575f80fd5b50565b5f8083601f840112610cf6575f80fd5b50813567ffffffffffffffff811115610d0d575f80fd5b602083019150836020828501011115610d24575f80fd5b9250929050565b5f805f805f8060a08789031215610d40575f80fd5b86359550602087013594506040870135610d5981610cc2565b93506060870135610d6981610cc2565b9250608087013567ffffffffffffffff811115610d84575f80fd5b610d9089828a01610ce6565b979a9699509497509295939492505050565b5f805f8060608587031215610db5575f80fd5b843593506020850135610dc781610cc2565b9250604085013567ffffffffffffffff811115610de2575f80fd5b610dee87828801610ce6565b95989497509550505050565b5f60208284031215610e0a575f80fd5b5035919050565b5f805f83850360c0811215610e24575f80fd5b60a0811215610e31575f80fd5b5083925060a084013567ffffffffffffffff811115610e4e575f80fd5b610e5a86828701610ce6565b9497909650939450505050565b73ffffffffffffffffffffffffffffffffffffffff8416815260406020820152816040820152818360608301375f818301606090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016010192915050565b5f7dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff808316818103610f27577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6001019392505050565b5f60208284031215610f41575f80fd5b8135610cbb81610cc2565b818382375f9101908152919050565b60c081018335610f6a81610cc2565b73ffffffffffffffffffffffffffffffffffffffff1682526020848101359083015260408085013590830152606080850135908301526080938401359382019390935260a0015290565b5f82518060208501845e5f920191825250919050565b8681528560208201528460408201525f73ffffffffffffffffffffffffffffffffffffffff808616606084015280851660808401525060c060a083015261101460c0830184610c5d565b98975050505050505050565b5f808585111561102e575f80fd5b8386111561103a575f80fd5b5050820193919092039150565b5f805f60608486031215611059575f80fd5b83359250602084013561106b81610cc2565b929592945050506040919091013590565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f80604083850312156110ba575f80fd5b82356110c581610cc2565b9150602083013567ffffffffffffffff808211156110e1575f80fd5b818501915085601f8301126110f4575f80fd5b8135818111156111065761110661107c565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561114c5761114c61107c565b81604052828152886020848701011115611164575f80fd5b826020860160208301375f602084830101528095505050505050925092905056fea164736f6c6343000819000a diff --git a/kona/crates/protocol/hardforks/src/bytecode/ofv_isthmus.hex b/kona/crates/protocol/hardforks/src/bytecode/ofv_isthmus.hex new file mode 100644 index 0000000000000..4b076f056444a --- /dev/null +++ b/kona/crates/protocol/hardforks/src/bytecode/ofv_isthmus.hex @@ -0,0 +1 @@ +60e060405234801561001057600080fd5b5073420000000000000000000000000000000000001960a0526000608052600160c05260805160a05160c0516107ef6100a7600039600081816101b3015281816102450152818161044b015261048601526000818160b8015281816101800152818161039a01528181610429015281816104c201526105b70152600081816101ef01528181610279015261029d01526107ef6000f3fe60806040526004361061009a5760003560e01c806382356d8a1161006957806384411d651161004e57806384411d651461021d578063d0e12f9014610233578063d3e5792b1461026757600080fd5b806382356d8a146101a45780638312f149146101e057600080fd5b80630d9019e1146100a65780633ccfd60b1461010457806354fd4d501461011b57806366d003ac1461017157600080fd5b366100a157005b600080fd5b3480156100b257600080fd5b506100da7f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561011057600080fd5b5061011961029b565b005b34801561012757600080fd5b506101646040518060400160405280600581526020017f312e302e3000000000000000000000000000000000000000000000000000000081525081565b6040516100fb9190610671565b34801561017d57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006100da565b3480156101b057600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040516100fb919061074e565b3480156101ec57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040519081526020016100fb565b34801561022957600080fd5b5061020f60005481565b34801561023f57600080fd5b506101d37f000000000000000000000000000000000000000000000000000000000000000081565b34801561027357600080fd5b5061020f7f000000000000000000000000000000000000000000000000000000000000000081565b7f0000000000000000000000000000000000000000000000000000000000000000471015610376576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f4665655661756c743a207769746864726177616c20616d6f756e74206d75737460448201527f2062652067726561746572207468616e206d696e696d756d207769746864726160648201527f77616c20616d6f756e7400000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b60004790508060008082825461038c9190610762565b9091555050604080518281527f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166020820152338183015290517fc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba9181900360600190a17f38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee817f0000000000000000000000000000000000000000000000000000000000000000337f000000000000000000000000000000000000000000000000000000000000000060405161047a94939291906107a1565b60405180910390a160017f000000000000000000000000000000000000000000000000000000000000000060018111156104b6576104b66106e4565b0361057a5760006104e77f000000000000000000000000000000000000000000000000000000000000000083610649565b905080610576576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4665655661756c743a206661696c656420746f2073656e642045544820746f2060448201527f4c322066656520726563697069656e7400000000000000000000000000000000606482015260840161036d565b5050565b6040517fc2b3e5ac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600482015262061a80602482015260606044820152600060648201527342000000000000000000000000000000000000169063c2b3e5ac9083906084016000604051808303818588803b15801561062d57600080fd5b505af1158015610641573d6000803e3d6000fd5b505050505050565b6000610656835a8461065d565b9392505050565b6000806000806000858888f1949350505050565b600060208083528351808285015260005b8181101561069e57858101830151858201604001528201610682565b818111156106b0576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b6002811061074a577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b9052565b6020810161075c8284610713565b92915050565b6000821982111561079c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b500190565b84815273ffffffffffffffffffffffffffffffffffffffff848116602083015283166040820152608081016107d96060830184610713565b9594505050505056fea164736f6c634300080f000a diff --git a/kona/crates/protocol/hardforks/src/ecotone.rs b/kona/crates/protocol/hardforks/src/ecotone.rs new file mode 100644 index 0000000000000..e2b255ac66bb3 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/ecotone.rs @@ -0,0 +1,307 @@ +//! Module containing a [`TxDeposit`] builder for the Ecotone network upgrade transactions. + +use alloc::{string::String, vec::Vec}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, hex}; +use kona_protocol::Predeploys; +use op_alloy_consensus::{TxDeposit, UpgradeDepositSource}; + +use crate::Hardfork; + +/// The Ecotone network upgrade transactions. +#[derive(Debug, Default, Clone, Copy)] +pub struct Ecotone; + +impl Ecotone { + /// The Gas Price Oracle Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the Gas Price Oracle Deployer Address and nonce 0. + pub const GAS_PRICE_ORACLE: Address = address!("b528d11cc114e026f138fe568744c6d45ce6da7a"); + + /// The depositor account address. + pub const DEPOSITOR_ACCOUNT: Address = address!("DeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001"); + + /// The Enable Ecotone Input Method 4Byte Signature + pub const ENABLE_ECOTONE_INPUT: [u8; 4] = hex!("22b90ab3"); + + /// L1 Block Deployer Address + pub const L1_BLOCK_DEPLOYER: Address = address!("4210000000000000000000000000000000000000"); + + /// The Gas Price Oracle Deployer Address + pub const GAS_PRICE_ORACLE_DEPLOYER: Address = + address!("4210000000000000000000000000000000000001"); + + /// The new L1 Block Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the L1 Block Deployer Address and nonce 0. + pub const NEW_L1_BLOCK: Address = address!("07dbe8500fc591d1852b76fee44d5a05e13097ff"); + + /// EIP-4788 From Address + pub const EIP4788_FROM: Address = address!("0B799C86a49DEeb90402691F1041aa3AF2d3C875"); + + /// The L1 Block Deployer Code Hash + /// See: + pub const L1_BLOCK_DEPLOYER_CODE_HASH: B256 = alloy_primitives::b256!( + "0xc88a313aa75dc4fbf0b6850d9f9ae41e04243b7008cf3eadb29256d4a71c1dfd" + ); + /// The Gas Price Oracle Code Hash + /// See: + pub const GAS_PRICE_ORACLE_CODE_HASH: B256 = alloy_primitives::b256!( + "0x8b71360ea773b4cfaf1ae6d2bd15464a4e1e2e360f786e475f63aeaed8da0ae5" + ); + + /// Returns the source hash for the deployment of the l1 block contract. + pub fn deploy_l1_block_source() -> B256 { + UpgradeDepositSource { intent: String::from("Ecotone: L1 Block Deployment") }.source_hash() + } + + /// Returns the source hash for the deployment of the gas price oracle contract. + pub fn deploy_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Ecotone: Gas Price Oracle Deployment") } + .source_hash() + } + + /// Returns the source hash for the update of the l1 block proxy. + pub fn update_l1_block_source() -> B256 { + UpgradeDepositSource { intent: String::from("Ecotone: L1 Block Proxy Update") } + .source_hash() + } + + /// Returns the source hash for the update of the gas price oracle proxy. + pub fn update_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Ecotone: Gas Price Oracle Proxy Update") } + .source_hash() + } + + /// Returns the source hash for the Ecotone Beacon Block Roots Contract deployment. + pub fn beacon_roots_source() -> B256 { + UpgradeDepositSource { + intent: String::from("Ecotone: beacon block roots contract deployment"), + } + .source_hash() + } + + /// Returns the source hash for the Ecotone Gas Price Oracle activation. + pub fn enable_ecotone_source() -> B256 { + UpgradeDepositSource { intent: String::from("Ecotone: Gas Price Oracle Set Ecotone") } + .source_hash() + } + + /// Returns the EIP-4788 creation data. + pub fn eip4788_creation_data() -> Bytes { + hex::decode(include_str!("./bytecode/eip4788_ecotone.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the raw bytecode for the L1 Block deployment. + pub fn l1_block_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/l1_block_ecotone.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the gas price oracle deployment bytecode. + pub fn ecotone_gas_price_oracle_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/gpo_ecotone.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the list of [`TxDeposit`]s for the Ecotone network upgrade. + pub fn deposits() -> impl Iterator { + ([ + // Deploy the L1 Block contract for Ecotone. + // See: + TxDeposit { + source_hash: Self::deploy_l1_block_source(), + from: Self::L1_BLOCK_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 375_000, + is_system_transaction: false, + input: Self::l1_block_deployment_bytecode(), + }, + // Deploy the Gas Price Oracle contract for Ecotone. + // See: + TxDeposit { + source_hash: Self::deploy_gas_price_oracle_source(), + from: Self::GAS_PRICE_ORACLE_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 1_000_000, + is_system_transaction: false, + input: Self::ecotone_gas_price_oracle_deployment_bytecode(), + }, + // Updates the l1 block proxy to point to the new L1 Block contract. + // See: + TxDeposit { + source_hash: Self::update_l1_block_source(), + from: Address::ZERO, + to: TxKind::Call(Predeploys::L1_BLOCK_INFO), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::NEW_L1_BLOCK), + }, + // Updates the gas price oracle proxy to point to the new Gas Price Oracle contract. + // See: + TxDeposit { + source_hash: Self::update_gas_price_oracle_source(), + from: Address::ZERO, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::GAS_PRICE_ORACLE), + }, + // Enables the Ecotone Gas Price Oracle. + // See: + TxDeposit { + source_hash: Self::enable_ecotone_source(), + from: Self::DEPOSITOR_ACCOUNT, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 80_000, + is_system_transaction: false, + input: Self::ENABLE_ECOTONE_INPUT.into(), + }, + // Deploys the beacon block roots contract. + // See: + TxDeposit { + source_hash: Self::beacon_roots_source(), + from: Self::EIP4788_FROM, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 250_000, + is_system_transaction: false, + input: Self::eip4788_creation_data(), + }, + ]) + .into_iter() + } +} + +impl Hardfork for Ecotone { + /// Constructs the Ecotone network upgrade transactions. + fn txs(&self) -> impl Iterator + '_ { + Self::deposits().map(|tx| { + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + Bytes::from(encoded) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::check_deployment_code; + + use super::*; + use alloc::vec; + + #[test] + fn test_deploy_l1_block_source() { + assert_eq!( + Ecotone::deploy_l1_block_source(), + hex!("877a6077205782ea15a6dc8699fa5ebcec5e0f4389f09cb8eda09488231346f8") + ); + } + #[test] + fn test_verify_ecotone_l1_deployment_code_hash() { + let txs = Ecotone::deposits().collect::>(); + + check_deployment_code( + txs[0].clone(), + Ecotone::NEW_L1_BLOCK, + Ecotone::L1_BLOCK_DEPLOYER_CODE_HASH, + ); + } + + #[test] + fn test_verify_ecotone_gas_price_oracle_deployment_code_hash() { + let txs = Ecotone::deposits().collect::>(); + + check_deployment_code( + txs[1].clone(), + Ecotone::GAS_PRICE_ORACLE, + Ecotone::GAS_PRICE_ORACLE_CODE_HASH, + ); + } + + #[test] + fn test_deploy_gas_price_oracle_source() { + assert_eq!( + Ecotone::deploy_gas_price_oracle_source(), + hex!("a312b4510adf943510f05fcc8f15f86995a5066bd83ce11384688ae20e6ecf42") + ); + } + + #[test] + fn test_update_l1_block_source() { + assert_eq!( + Ecotone::update_l1_block_source(), + hex!("18acb38c5ff1c238a7460ebc1b421fa49ec4874bdf1e0a530d234104e5e67dbc") + ); + } + + #[test] + fn test_update_gas_price_oracle_source() { + assert_eq!( + Ecotone::update_gas_price_oracle_source(), + hex!("ee4f9385eceef498af0be7ec5862229f426dec41c8d42397c7257a5117d9230a") + ); + } + + #[test] + fn test_enable_ecotone_source() { + assert_eq!( + Ecotone::enable_ecotone_source(), + hex!("0c1cb38e99dbc9cbfab3bb80863380b0905290b37eb3d6ab18dc01c1f3e75f93") + ); + } + + #[test] + fn test_beacon_block_roots_source() { + assert_eq!( + Ecotone::beacon_roots_source(), + hex!("69b763c48478b9dc2f65ada09b3d92133ec592ea715ec65ad6e7f3dc519dc00c") + ); + } + + #[test] + fn test_ecotone_txs_encoded() { + let ecotone_upgrade_tx = Ecotone.txs().collect::>(); + assert_eq!(ecotone_upgrade_tx.len(), 6); + + let expected_txs: Vec = vec![ + hex::decode(include_str!("./bytecode/ecotone_tx_0.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/ecotone_tx_1.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/ecotone_tx_2.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/ecotone_tx_3.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/ecotone_tx_4.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/ecotone_tx_5.hex").replace("\n", "")) + .unwrap() + .into(), + ]; + for (i, expected) in expected_txs.iter().enumerate() { + assert_eq!(ecotone_upgrade_tx[i], *expected); + } + } +} diff --git a/kona/crates/protocol/hardforks/src/fjord.rs b/kona/crates/protocol/hardforks/src/fjord.rs new file mode 100644 index 0000000000000..8068d4364b1b0 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/fjord.rs @@ -0,0 +1,183 @@ +//! Module containing a [`TxDeposit`] builder for the Fjord network upgrade transactions. + +use alloc::{string::String, vec::Vec}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, hex}; +use kona_protocol::Predeploys; +use op_alloy_consensus::{TxDeposit, UpgradeDepositSource}; + +use crate::Hardfork; + +/// The Fjord network upgrade transactions. +#[derive(Debug, Default, Clone, Copy)] +pub struct Fjord; + +impl Fjord { + /// The Gas Price Oracle Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the Gas Price Oracle Deployer Address and nonce 0. + pub const GAS_PRICE_ORACLE: Address = address!("b528d11cc114e026f138fe568744c6d45ce6da7a"); + + /// The L1 Info Depositer Address. + pub const L1_INFO_DEPOSITER: Address = address!("deaddeaddeaddeaddeaddeaddeaddeaddead0001"); + + /// Fjord Gas Price Oracle Deployer Address. + pub const GAS_PRICE_ORACLE_FJORD_DEPLOYER: Address = + address!("4210000000000000000000000000000000000002"); + + /// Fjord Gas Price Oracle address. + pub const FJORD_GAS_PRICE_ORACLE: Address = + address!("a919894851548179a0750865e7974da599c0fac7"); + + /// The Set Fjord Four Byte Method Signature. + pub const SET_FJORD_METHOD_SIGNATURE: [u8; 4] = hex!("8e98b106"); + + /// The Fjord Gas Price Oracle code hash. + /// See: + pub const GAS_PRICE_ORACLE_CODE_HASH: B256 = alloy_primitives::b256!( + "0xa88fa50a2745b15e6794247614b5298483070661adacb8d32d716434ed24c6b2" + ); + + /// Returns the source hash for the deployment of the Fjord Gas Price Oracle. + pub fn deploy_fjord_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Fjord: Gas Price Oracle Deployment") } + .source_hash() + } + + /// Returns the source hash for the update of the Fjord Gas Price Oracle. + pub fn update_fjord_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Fjord: Gas Price Oracle Proxy Update") } + .source_hash() + } + + /// [UpgradeDepositSource] for setting the Fjord Gas Price Oracle. + pub fn enable_fjord_source() -> B256 { + UpgradeDepositSource { intent: String::from("Fjord: Gas Price Oracle Set Fjord") } + .source_hash() + } + + /// Returns the fjord gas price oracle deployment bytecode. + pub fn gas_price_oracle_deployment_bytecode() -> alloy_primitives::Bytes { + hex::decode(include_str!("./bytecode/gpo_fjord.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the list of [`TxDeposit`]s for the Fjord network upgrade. + pub fn deposits() -> impl Iterator { + ([ + // Deploys the Fjord Gas Price Oracle contract. + // See: + TxDeposit { + source_hash: Self::deploy_fjord_gas_price_oracle_source(), + from: Self::GAS_PRICE_ORACLE_FJORD_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 1_450_000, + is_system_transaction: false, + input: Self::gas_price_oracle_deployment_bytecode(), + }, + // Updates the gas price Oracle proxy to point to the Fjord Gas Price Oracle. + // See: + TxDeposit { + source_hash: Self::update_fjord_gas_price_oracle_source(), + from: Address::ZERO, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::FJORD_GAS_PRICE_ORACLE), + }, + // Enables the Fjord Gas Price Oracle. + // See: + TxDeposit { + source_hash: Self::enable_fjord_source(), + from: Self::L1_INFO_DEPOSITER, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 90_000, + is_system_transaction: false, + input: Self::SET_FJORD_METHOD_SIGNATURE.into(), + }, + ]) + .into_iter() + } +} + +impl Hardfork for Fjord { + /// Constructs the Fjord network upgrade transactions. + fn txs(&self) -> impl Iterator + '_ { + Self::deposits().map(|tx| { + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + Bytes::from(encoded) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::check_deployment_code; + + use super::*; + use alloc::vec; + + #[test] + fn test_deploy_fjord_gas_price_oracle_source() { + assert_eq!( + Fjord::deploy_fjord_gas_price_oracle_source(), + hex!("86122c533fdcb89b16d8713174625e44578a89751d96c098ec19ab40a51a8ea3") + ); + } + + #[test] + fn test_update_fjord_gas_price_oracle_source() { + assert_eq!( + Fjord::update_fjord_gas_price_oracle_source(), + hex!("1e6bb0c28bfab3dc9b36ffb0f721f00d6937f33577606325692db0965a7d58c6") + ); + } + + #[test] + fn test_enable_fjord_source() { + assert_eq!( + Fjord::enable_fjord_source(), + hex!("bac7bb0d5961cad209a345408b0280a0d4686b1b20665e1b0f9cdafd73b19b6b") + ); + } + + #[test] + fn test_fjord_txs_encoded() { + let fjord_upgrade_tx = Fjord.txs().collect::>(); + assert_eq!(fjord_upgrade_tx.len(), 3); + + let expected_txs: Vec = vec![ + hex::decode(include_str!("./bytecode/fjord_tx_0.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/fjord_tx_1.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/fjord_tx_2.hex").replace("\n", "")) + .unwrap() + .into(), + ]; + for (i, expected) in expected_txs.iter().enumerate() { + assert_eq!(fjord_upgrade_tx[i], *expected); + } + } + + #[test] + fn test_verify_fjord_gas_price_oracle_deployment_code_hash() { + let txs = Fjord::deposits().collect::>(); + + check_deployment_code( + txs[0].clone(), + Fjord::FJORD_GAS_PRICE_ORACLE, + Fjord::GAS_PRICE_ORACLE_CODE_HASH, + ); + } +} diff --git a/kona/crates/protocol/hardforks/src/forks.rs b/kona/crates/protocol/hardforks/src/forks.rs new file mode 100644 index 0000000000000..aaf111dbcd098 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/forks.rs @@ -0,0 +1,84 @@ +//! Contains all hardforks represented in the [crate::Hardfork] type. + +use crate::{Ecotone, Fjord, Interop, Isthmus, Jovian}; + +/// Optimism Hardforks +/// +/// This type is used to encapsulate hardfork transactions. +/// It exposes methods that return hardfork upgrade transactions +/// as [`alloy_primitives::Bytes`]. +/// +/// # Example +/// +/// Build ecotone hardfork upgrade transaction: +/// ```rust +/// use kona_hardforks::{Hardfork, Hardforks}; +/// let ecotone_upgrade_tx = Hardforks::ECOTONE.txs(); +/// assert_eq!(ecotone_upgrade_tx.collect::>().len(), 6); +/// ``` +/// +/// Build fjord hardfork upgrade transactions: +/// ```rust +/// use kona_hardforks::{Hardfork, Hardforks}; +/// let fjord_upgrade_txs = Hardforks::FJORD.txs(); +/// assert_eq!(fjord_upgrade_txs.collect::>().len(), 3); +/// ``` +/// +/// Build isthmus hardfork upgrade transaction: +/// ```rust +/// use kona_hardforks::{Hardfork, Hardforks}; +/// let isthmus_upgrade_tx = Hardforks::ISTHMUS.txs(); +/// assert_eq!(isthmus_upgrade_tx.collect::>().len(), 8); +/// ``` +/// +/// Build interop hardfork upgrade transaction: +/// ```rust +/// use kona_hardforks::{Hardfork, Hardforks}; +/// let interop_upgrade_tx = Hardforks::INTEROP.txs(); +/// assert_eq!(interop_upgrade_tx.collect::>().len(), 4); +/// ``` +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct Hardforks; + +impl Hardforks { + /// The Ecotone hardfork upgrade transactions. + pub const ECOTONE: Ecotone = Ecotone; + + /// The Fjord hardfork upgrade transactions. + pub const FJORD: Fjord = Fjord; + + /// The Isthmus hardfork upgrade transactions. + pub const ISTHMUS: Isthmus = Isthmus; + + /// The Jovian hardfork upgrade transactions. + pub const JOVIAN: Jovian = Jovian; + + /// The Interop hardfork upgrade transactions. + pub const INTEROP: Interop = Interop; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Hardfork; + use alloc::vec::Vec; + + #[test] + fn test_hardforks() { + let ecotone_upgrade_tx = Hardforks::ECOTONE.txs(); + assert_eq!(ecotone_upgrade_tx.collect::>().len(), 6); + + let fjord_upgrade_txs = Hardforks::FJORD.txs(); + assert_eq!(fjord_upgrade_txs.collect::>().len(), 3); + + let isthmus_upgrade_tx = Hardforks::ISTHMUS.txs(); + assert_eq!(isthmus_upgrade_tx.collect::>().len(), 8); + + let jovian_upgrade_tx = Hardforks::JOVIAN.txs(); + assert_eq!(jovian_upgrade_tx.collect::>().len(), 5); + + let interop_upgrade_tx = Hardforks::INTEROP.txs(); + assert_eq!(interop_upgrade_tx.collect::>().len(), 4); + } +} diff --git a/kona/crates/protocol/hardforks/src/interop.rs b/kona/crates/protocol/hardforks/src/interop.rs new file mode 100644 index 0000000000000..23ff534215097 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/interop.rs @@ -0,0 +1,223 @@ +//! Module containing a [`TxDeposit`] builder for the Interop network upgrade transactions. +//! +//! Interop network upgrade transactions are defined in the [OP Stack Specs][specs]. +//! +//! [specs]: https://specs.optimism.io/interop/derivation.html#network-upgrade-transactions + +use alloc::string::String; +use alloy_eips::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, b256, hex}; +use kona_protocol::Predeploys; +use op_alloy_consensus::{TxDeposit, UpgradeDepositSource}; + +use crate::Hardfork; + +/// The Interop network upgrade transactions. +#[derive(Debug, Default, Clone, Copy)] +pub struct Interop; + +impl Interop { + /// The deployer of the `CrossL2Inbox` contract. + pub const CROSS_L2_INBOX_DEPLOYER: Address = + address!("0x4220000000000000000000000000000000000000"); + + /// The deployer of the `L2ToL2CrossDomainMessenger` contract. + pub const L2_TO_L2_XDM_DEPLOYER: Address = + address!("0x4220000000000000000000000000000000000001"); + + /// The deployed address of the `CrossL2Inbox` implementation contract. + pub const NEW_CROSS_L2_INBOX_IMPL: Address = + address!("0x691300f512e48B463C2617b34Eef1A9f82EE7dBf"); + + /// The code hash of the deployed `CrossL2Inbox` implementation contract. + pub const CROSS_L2_INBOX_IMPL_CODE_HASH: B256 = + b256!("0x0e7d028dd71bac22d1fb28966043c8d35c3232c78b7fb99fd1db112b5b60d9dd"); + + /// The deployment address of the `L2ToL2CrossDomainMessenger` implementation contract. + pub const NEW_L2_TO_L2_XDM_IMPL: Address = + address!("0x0D0eDd0ebd0e94d218670a8De867Eb5C4d37cadD"); + + /// The code hash of the deployed `L2ToL2CrossDomainMessenger` implementation contract. + pub const L2_TO_L2_XDM_IMPL_CODE_HASH: B256 = + b256!("0x458925c90ec70736600bef3d6529643a0e7a0a848e62626d61314c057b4a71a9"); + + /// Returns the source hash for the `CrossL2Inbox` contract deployment transaction. + pub fn deploy_cross_l2_inbox_source() -> B256 { + UpgradeDepositSource { intent: String::from("Interop: CrossL2Inbox Deployment") } + .source_hash() + } + + /// Returns the source hash for the `CrossL2Inbox` proxy upgrade transaction. + pub fn upgrade_cross_l2_inbox_proxy_source() -> B256 { + UpgradeDepositSource { intent: String::from("Interop: CrossL2Inbox Proxy Update") } + .source_hash() + } + + /// Returns the source hash for the `L2ToL2CrossDomainMessenger` deployment transaction. + pub fn deploy_l2_to_l2_xdm_source() -> B256 { + UpgradeDepositSource { + intent: String::from("Interop: L2ToL2CrossDomainMessenger Deployment"), + } + .source_hash() + } + + /// Returns the source hash for the `L2ToL2CrossDomainMessenger` proxy upgrade transaction. + pub fn upgrade_l2_to_l2_xdm_proxy_source() -> B256 { + UpgradeDepositSource { + intent: String::from("Interop: L2ToL2CrossDomainMessenger Proxy Update"), + } + .source_hash() + } + + /// Returns the `CrossL2Inbox` deployment bytecode. + pub fn cross_l2_inbox_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/crossl2inbox_interop.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the `L2ToL2CrossDomainMessenger` proxy upgrade bytecode. + pub fn l2_to_l2_xdm_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/l2tol2_xdm_interop.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the list of [`TxDeposit`]s for the network upgrade. + pub fn deposits() -> impl Iterator { + ([ + TxDeposit { + source_hash: Self::deploy_cross_l2_inbox_source(), + from: Self::CROSS_L2_INBOX_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 420_000, + is_system_transaction: false, + input: Self::cross_l2_inbox_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::upgrade_cross_l2_inbox_proxy_source(), + from: Address::ZERO, + to: TxKind::Call(Predeploys::CROSS_L2_INBOX), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::NEW_CROSS_L2_INBOX_IMPL), + }, + TxDeposit { + source_hash: Self::deploy_l2_to_l2_xdm_source(), + from: Self::L2_TO_L2_XDM_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 1_100_000, + is_system_transaction: false, + input: Self::l2_to_l2_xdm_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::upgrade_l2_to_l2_xdm_proxy_source(), + from: Address::ZERO, + to: TxKind::Call(Predeploys::L2_TO_L2_XDM), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::NEW_L2_TO_L2_XDM_IMPL), + }, + ]) + .into_iter() + } +} + +impl Hardfork for Interop { + /// Constructs the network upgrade transactions. + fn txs(&self) -> impl Iterator { + Self::deposits().map(|tx| tx.encoded_2718().into()) + } +} + +#[cfg(test)] +mod test { + use alloc::{vec, vec::Vec}; + + use super::*; + use crate::test_utils::check_deployment_code; + + #[test] + fn test_deploy_cross_l2_inbox_source() { + assert_eq!( + Interop::deploy_cross_l2_inbox_source(), + b256!("0x6e5e214f73143df8fe6f6054a3ed7eb472d373376458a9c8aecdf23475beb616") + ); + } + + #[test] + fn test_upgrade_cross_l2_inbox_proxy_source() { + assert_eq!( + Interop::upgrade_cross_l2_inbox_proxy_source(), + b256!("0x88c6b48354c367125a59792a93a7b60ad7cd66e516157dbba16558c68a46d3cb") + ); + } + + #[test] + fn test_deploy_l2_to_l2_xdm_source() { + assert_eq!( + Interop::deploy_l2_to_l2_xdm_source(), + b256!("0xf5484697c7a9a791db32a3bf0763bf2ba686c77ae7d4c0a5ee8c222a92a8dcc2") + ); + } + + #[test] + fn test_upgrade_l2_to_l2_xdm_proxy_source() { + assert_eq!( + Interop::upgrade_l2_to_l2_xdm_proxy_source(), + b256!("0xe54b4d06bbcc857f41ae00e89d820339ac5ce0034aac722c817b2873e03a7e68") + ); + } + + #[test] + fn test_deploy_cross_l2_inbox_address_and_code() { + let txs = Interop::deposits().collect::>(); + check_deployment_code( + txs[0].clone(), + Interop::NEW_CROSS_L2_INBOX_IMPL, + Interop::CROSS_L2_INBOX_IMPL_CODE_HASH, + ); + } + + #[test] + fn test_deploy_l2_to_l2_xdm_address_and_code() { + let txs = Interop::deposits().collect::>(); + check_deployment_code( + txs[2].clone(), + Interop::NEW_L2_TO_L2_XDM_IMPL, + Interop::L2_TO_L2_XDM_IMPL_CODE_HASH, + ); + } + + #[test] + fn test_interop_txs_encoded() { + let interop_upgrade_tx = Interop.txs().collect::>(); + assert_eq!(interop_upgrade_tx.len(), 4); + + let expected_txs: Vec = vec![ + hex::decode(include_str!("./bytecode/interop_tx_0.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/interop_tx_1.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/interop_tx_2.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/interop_tx_3.hex").replace("\n", "")) + .unwrap() + .into(), + ]; + for (i, expected) in expected_txs.iter().enumerate() { + assert_eq!(interop_upgrade_tx[i], *expected); + } + } +} diff --git a/kona/crates/protocol/hardforks/src/isthmus.rs b/kona/crates/protocol/hardforks/src/isthmus.rs new file mode 100644 index 0000000000000..efdfa84a06827 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/isthmus.rs @@ -0,0 +1,361 @@ +//! Module containing a [`TxDeposit`] builder for the Isthmus network upgrade transactions. +//! +//! Isthmus network upgrade transactions are defined in the [OP Stack Specs][specs]. +//! +//! [specs]: https://specs.optimism.io/protocol/isthmus/derivation.html#network-upgrade-automation-transactions + +use alloc::{string::String, vec::Vec}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, hex}; +use kona_protocol::Predeploys; +use op_alloy_consensus::{TxDeposit, UpgradeDepositSource}; + +use crate::Hardfork; + +/// The Isthmus network upgrade transactions. +#[derive(Debug, Default, Clone, Copy)] +pub struct Isthmus; + +impl Isthmus { + /// The depositor account address. + pub const DEPOSITOR_ACCOUNT: Address = address!("DeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001"); + + /// The Enable Isthmus Input Method 4Byte Signature. + /// + /// Derive this by running `cast sig "setIsthmus()"`. + pub const ENABLE_ISTHMUS_INPUT: [u8; 4] = hex!("291b0383"); + + /// EIP-2935 From Address + pub const EIP2935_FROM: Address = address!("3462413Af4609098e1E27A490f554f260213D685"); + + /// L1 Block Deployer Address + pub const L1_BLOCK_DEPLOYER: Address = address!("4210000000000000000000000000000000000003"); + + /// The Gas Price Oracle Deployer Address + pub const GAS_PRICE_ORACLE_DEPLOYER: Address = + address!("4210000000000000000000000000000000000004"); + + /// The Operator Fee Vault Deployer Address + pub const OPERATOR_FEE_VAULT_DEPLOYER: Address = + address!("4210000000000000000000000000000000000005"); + + /// The new L1 Block Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the L1 Block Deployer Address and nonce 0. + pub const NEW_L1_BLOCK: Address = address!("ff256497d61dcd71a9e9ff43967c13fde1f72d12"); + + /// The Gas Price Oracle Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the Gas Price Oracle Deployer Address and nonce 0. + pub const GAS_PRICE_ORACLE: Address = address!("93e57a196454cb919193fa9946f14943cf733845"); + + /// The Operator Fee Vault Address + /// This is computed by using go-ethereum's `crypto.CreateAddress` function, + /// with the Operator Fee Vault Deployer Address and nonce 0. + pub const OPERATOR_FEE_VAULT: Address = address!("4fa2be8cd41504037f1838bce3bcc93bc68ff537"); + + /// The Isthmus L1 Block Deployer Code Hash + /// See: + pub const L1_BLOCK_DEPLOYER_CODE_HASH: B256 = alloy_primitives::b256!( + "0x8e3fe7a416d3e5f3b7be74ddd4e7e58e516fa3f80b67c6d930e3cd7297da4a4b" + ); + + /// The Isthmus Gas Price Oracle Code Hash + /// See: + pub const GAS_PRICE_ORACLE_CODE_HASH: B256 = alloy_primitives::b256!( + "0x4d195a9d7caf9fb6d4beaf80de252c626c853afd5868c4f4f8d19c9d301c2679" + ); + /// The Isthmus Operator Fee Vault Code Hash + /// See: + pub const OPERATOR_FEE_VAULT_CODE_HASH: B256 = alloy_primitives::b256!( + "0x57dc55c9c09ca456fa728f253fe7b895d3e6aae0706104935fe87c7721001971" + ); + /// Returns the source hash for the Isthmus Gas Price Oracle activation. + pub fn enable_isthmus_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: Gas Price Oracle Set Isthmus") } + .source_hash() + } + + /// Returns the source hash for the EIP-2935 block hash history contract deployment. + pub fn block_hash_history_contract_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: EIP-2935 Contract Deployment") } + .source_hash() + } + + /// Returns the source hash for the deployment of the gas price oracle contract. + pub fn deploy_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: Gas Price Oracle Deployment") } + .source_hash() + } + + /// Returns the source hash for the deployment of the l1 block contract. + pub fn deploy_l1_block_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: L1 Block Deployment") }.source_hash() + } + + /// Returns the source hash for the deployment of the operator fee vault contract. + pub fn deploy_operator_fee_vault_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: Operator Fee Vault Deployment") } + .source_hash() + } + + /// Returns the source hash for the update of the l1 block proxy. + pub fn update_l1_block_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: L1 Block Proxy Update") } + .source_hash() + } + + /// Returns the source hash for the update of the gas price oracle proxy. + pub fn update_gas_price_oracle_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: Gas Price Oracle Proxy Update") } + .source_hash() + } + + /// Returns the source hash for the update of the operator fee vault proxy. + pub fn update_operator_fee_vault_source() -> B256 { + UpgradeDepositSource { intent: String::from("Isthmus: Operator Fee Vault Proxy Update") } + .source_hash() + } + + /// Returns the raw bytecode for the L1 Block deployment. + pub fn l1_block_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/l1_block_isthmus.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the gas price oracle deployment bytecode. + pub fn gas_price_oracle_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/gpo_isthmus.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the gas price oracle deployment bytecode. + pub fn operator_fee_vault_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/ofv_isthmus.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the EIP-2935 creation data. + pub fn eip2935_creation_data() -> Bytes { + hex::decode(include_str!("./bytecode/eip2935_isthmus.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the list of [`TxDeposit`]s for the network upgrade. + pub fn deposits() -> impl Iterator { + ([ + TxDeposit { + source_hash: Self::deploy_l1_block_source(), + from: Self::L1_BLOCK_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 425_000, + is_system_transaction: false, + input: Self::l1_block_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::deploy_gas_price_oracle_source(), + from: Self::GAS_PRICE_ORACLE_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 1_625_000, + is_system_transaction: false, + input: Self::gas_price_oracle_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::deploy_operator_fee_vault_source(), + from: Self::OPERATOR_FEE_VAULT_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 500_000, + is_system_transaction: false, + input: Self::operator_fee_vault_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::update_l1_block_source(), + from: Address::default(), + to: TxKind::Call(Predeploys::L1_BLOCK_INFO), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::NEW_L1_BLOCK), + }, + TxDeposit { + source_hash: Self::update_gas_price_oracle_source(), + from: Address::default(), + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::GAS_PRICE_ORACLE), + }, + TxDeposit { + source_hash: Self::update_operator_fee_vault_source(), + from: Address::default(), + to: TxKind::Call(Predeploys::OPERATOR_FEE_VAULT), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: super::upgrade_to_calldata(Self::OPERATOR_FEE_VAULT), + }, + TxDeposit { + source_hash: Self::enable_isthmus_source(), + from: Self::DEPOSITOR_ACCOUNT, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 90_000, + is_system_transaction: false, + input: Self::ENABLE_ISTHMUS_INPUT.into(), + }, + TxDeposit { + source_hash: Self::block_hash_history_contract_source(), + from: Self::EIP2935_FROM, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 250_000, + is_system_transaction: false, + input: Self::eip2935_creation_data(), + }, + ]) + .into_iter() + } +} + +impl Hardfork for Isthmus { + /// Constructs the network upgrade transactions. + fn txs(&self) -> impl Iterator + '_ { + Self::deposits().map(|tx| { + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + Bytes::from(encoded) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::check_deployment_code; + + use super::*; + use alloc::vec; + use alloy_primitives::b256; + + #[test] + fn test_l1_block_source_hash() { + let expected = b256!("3b2d0821ca2411ad5cd3595804d1213d15737188ae4cbd58aa19c821a6c211bf"); + assert_eq!(Isthmus::deploy_l1_block_source(), expected); + } + + #[test] + fn test_gas_price_oracle_source_hash() { + let expected = b256!("fc70b48424763fa3fab9844253b4f8d508f91eb1f7cb11a247c9baec0afb8035"); + assert_eq!(Isthmus::deploy_gas_price_oracle_source(), expected); + } + + #[test] + fn test_operator_fee_vault_source_hash() { + let expected = b256!("107a570d3db75e6110817eb024f09f3172657e920634111ce9875d08a16daa96"); + assert_eq!(Isthmus::deploy_operator_fee_vault_source(), expected); + } + + #[test] + fn test_l1_block_update_source_hash() { + let expected = b256!("ebe8b5cb10ca47e0d8bda8f5355f2d66711a54ddeb0ef1d30e29418c9bf17a0e"); + assert_eq!(Isthmus::update_l1_block_source(), expected); + } + + #[test] + fn test_gas_price_oracle_update_source_hash() { + let expected = b256!("ecf2d9161d26c54eda6b7bfdd9142719b1e1199a6e5641468d1bf705bc531ab0"); + assert_eq!(Isthmus::update_gas_price_oracle_source(), expected); + } + + #[test] + fn test_operator_fee_vault_update_source_hash() { + let expected = b256!("ad74e1adb877ccbe176b8fa1cc559388a16e090ddbe8b512f5b37d07d887a927"); + assert_eq!(Isthmus::update_operator_fee_vault_source(), expected); + } + + #[test] + fn test_enable_isthmus_source() { + let expected = b256!("3ddf4b1302548dd92939826e970f260ba36167f4c25f18390a5e8b194b295319"); + assert_eq!(Isthmus::enable_isthmus_source(), expected); + } + + #[test] + fn test_isthmus_txs_encoded() { + let isthmus_upgrade_tx = Isthmus.txs().collect::>(); + assert_eq!(isthmus_upgrade_tx.len(), 8); + + let expected_txs: Vec = vec![ + hex::decode(include_str!("./bytecode/isthmus_tx_0.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_1.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_2.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_3.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_4.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_5.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_6.hex").replace("\n", "")) + .unwrap() + .into(), + hex::decode(include_str!("./bytecode/isthmus_tx_7.hex").replace("\n", "")) + .unwrap() + .into(), + ]; + for (i, expected) in expected_txs.iter().enumerate() { + assert_eq!(isthmus_upgrade_tx[i], *expected); + } + } + #[test] + fn test_verify_isthmus_l1_block_deployment_code_hash() { + let txs = Isthmus::deposits().collect::>(); + check_deployment_code( + txs[0].clone(), + Isthmus::NEW_L1_BLOCK, + Isthmus::L1_BLOCK_DEPLOYER_CODE_HASH, + ); + } + #[test] + fn test_verify_isthmus_gas_price_oracle_deployment_code_hash() { + let txs = Isthmus::deposits().collect::>(); + + check_deployment_code( + txs[1].clone(), + Isthmus::GAS_PRICE_ORACLE, + Isthmus::GAS_PRICE_ORACLE_CODE_HASH, + ); + } + #[test] + fn test_verify_isthmus_operator_fee_vault_deployment_code_hash() { + let txs = Isthmus::deposits().collect::>(); + + check_deployment_code( + txs[2].clone(), + Isthmus::OPERATOR_FEE_VAULT, + Isthmus::OPERATOR_FEE_VAULT_CODE_HASH, + ); + } +} diff --git a/kona/crates/protocol/hardforks/src/jovian.rs b/kona/crates/protocol/hardforks/src/jovian.rs new file mode 100644 index 0000000000000..7c52fe49c84ad --- /dev/null +++ b/kona/crates/protocol/hardforks/src/jovian.rs @@ -0,0 +1,246 @@ +//! Module containing a [`TxDeposit`] builder for the Jovian network upgrade transactions. +//! +//! Jovian network upgrade transactions are defined in the [OP Stack Specs][specs]. +//! +//! [specs]: https://specs.optimism.io/protocol/jovian/derivation.html#network-upgrade-automation-transactions + +use alloc::{string::String, vec::Vec}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256, address, hex, keccak256}; +use kona_protocol::Predeploys; +use op_alloy_consensus::{TxDeposit, UpgradeDepositSource}; + +use crate::{Hardfork, upgrade_to_calldata}; + +/// The Jovian network upgrade transactions. +#[derive(Debug, Default, Clone, Copy)] +pub struct Jovian; + +impl Jovian { + /// The depositor account address. + pub const DEPOSITOR_ACCOUNT: Address = address!("DeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001"); + + /// L1 Block Deployer Address + pub const L1_BLOCK_DEPLOYER: Address = address!("4210000000000000000000000000000000000006"); + + /// Zero address + pub const ZERO_ADDRESS: Address = address!("0x0000000000000000000000000000000000000000"); + + /// The Gas Price Oracle Deployer Address + pub const GAS_PRICE_ORACLE_DEPLOYER: Address = + address!("4210000000000000000000000000000000000007"); + + /// Returns the source hash for the deployment of the l1 block contract. + pub fn deploy_l1_block_source() -> B256 { + UpgradeDepositSource { intent: String::from("Jovian: L1 Block Deployment") }.source_hash() + } + + /// Returns the source hash for the deployment of the gas price oracle contract. + pub fn l1_block_proxy_update() -> B256 { + UpgradeDepositSource { intent: String::from("Jovian: L1 Block Proxy Update") }.source_hash() + } + + /// Returns the source hash for the deployment of the operator fee vault contract. + pub fn gas_price_oracle() -> B256 { + UpgradeDepositSource { intent: String::from("Jovian: Gas Price Oracle Deployment") } + .source_hash() + } + + /// Returns the source hash for the update of the l1 block proxy. + pub fn gas_price_oracle_proxy_update() -> B256 { + UpgradeDepositSource { intent: String::from("Jovian: Gas Price Oracle Proxy Update") } + .source_hash() + } + + /// The Jovian L1 Block Address + /// This is computed by using `Address::create` function, + /// with the L1 Block Deployer Address and nonce 0. + pub fn l1_block_address() -> Address { + Self::L1_BLOCK_DEPLOYER.create(0) + } + + /// The Jovian Gas Price Oracle Address + /// This is computed by using `Address::create` function, + /// with the Gas Price Oracle Deployer Address and nonce 0. + pub fn gas_price_oracle_address() -> Address { + Self::GAS_PRICE_ORACLE_DEPLOYER.create(0) + } + + /// Returns the source hash to the enable the gas price oracle for Jovian. + pub fn gas_price_oracle_enable_jovian() -> B256 { + UpgradeDepositSource { intent: String::from("Jovian: Gas Price Oracle Set Jovian") } + .source_hash() + } + + /// Returns the raw bytecode for the L1 Block deployment. + pub fn l1_block_deployment_bytecode() -> Bytes { + hex::decode(include_str!("./bytecode/jovian-l1-block-deployment.hex").replace("\n", "")) + .expect("Expected hex byte string") + .into() + } + + /// Returns the gas price oracle deployment bytecode. + pub fn gas_price_oracle_deployment_bytecode() -> Bytes { + hex::decode( + include_str!("./bytecode/jovian-gas-price-oracle-deployment.hex").replace("\n", ""), + ) + .expect("Expected hex byte string") + .into() + } + + /// Returns the bytecode to enable the gas price oracle for Jovian. + pub fn gas_price_oracle_enable_jovian_bytecode() -> Bytes { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&keccak256("setJovian()")[..4]); + bytes.into() + } + + /// Returns the list of [`TxDeposit`]s for the network upgrade. + pub fn deposits() -> impl Iterator { + ([ + TxDeposit { + source_hash: Self::deploy_l1_block_source(), + from: Self::L1_BLOCK_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 447_315, + is_system_transaction: false, + input: Self::l1_block_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::l1_block_proxy_update(), + from: Self::ZERO_ADDRESS, + to: TxKind::Call(Predeploys::L1_BLOCK_INFO), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: upgrade_to_calldata(Self::l1_block_address()), + }, + TxDeposit { + source_hash: Self::gas_price_oracle(), + from: Self::GAS_PRICE_ORACLE_DEPLOYER, + to: TxKind::Create, + mint: 0, + value: U256::ZERO, + gas_limit: 1_750_714, + is_system_transaction: false, + input: Self::gas_price_oracle_deployment_bytecode(), + }, + TxDeposit { + source_hash: Self::gas_price_oracle_proxy_update(), + from: Self::ZERO_ADDRESS, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 50_000, + is_system_transaction: false, + input: upgrade_to_calldata(Self::gas_price_oracle_address()), + }, + TxDeposit { + source_hash: Self::gas_price_oracle_enable_jovian(), + from: Self::DEPOSITOR_ACCOUNT, + to: TxKind::Call(Predeploys::GAS_PRICE_ORACLE), + mint: 0, + value: U256::ZERO, + gas_limit: 90_000, + is_system_transaction: false, + input: Self::gas_price_oracle_enable_jovian_bytecode(), + }, + ]) + .into_iter() + } +} + +impl Hardfork for Jovian { + /// Constructs the network upgrade transactions. + fn txs(&self) -> impl Iterator + '_ { + Self::deposits().map(|tx| { + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + Bytes::from(encoded) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::check_deployment_code; + + use super::*; + use alloy_primitives::b256; + + #[test] + fn test_l1_block_source_hash() { + let expected = b256!("bb1a656f65401240fac3db12e7a79ebb954b11e62f7626eb11691539b798d3bf"); + assert_eq!(Jovian::deploy_l1_block_source(), expected); + } + + #[test] + fn test_l1_block_proxy_update_source_hash() { + let expected = b256!("f3275f829340521028f9ad5bce4ecb1c64a45d448794effa2a77674627338e76"); + assert_eq!(Jovian::l1_block_proxy_update(), expected); + } + + #[test] + fn test_gas_price_oracle_source_hash() { + let expected = b256!("239b7021a6c2cf3a918481242bbb5a9499057f24501539467536c691bb133962"); + assert_eq!(Jovian::gas_price_oracle(), expected); + } + + #[test] + fn test_upgrade_to_calldata_for_gas_price_oracle() { + assert_eq!( + **upgrade_to_calldata(Jovian::gas_price_oracle_address()), + hex!("0x3659cfe60000000000000000000000004f1db3c6abd250ba86e0928471a8f7db3afd88f1") + ); + } + + #[test] + fn test_upgrade_to_calldata_for_l1_block_proxy_update() { + assert_eq!( + **upgrade_to_calldata(Jovian::l1_block_address()), + hex!("0x3659cfe60000000000000000000000003ba4007f5c922fbb33c454b41ea7a1f11e83df2c") + ); + } + + #[test] + fn test_gas_price_oracle_proxy_update_source_hash() { + let expected = b256!("a70c60aa53b8c1c0d52b39b1e901e7d7c09f7819595cb24048a6bb1983b401ff"); + assert_eq!(Jovian::gas_price_oracle_proxy_update(), expected); + } + + #[test] + fn test_gas_price_oracle_enable_jovian_source_hash() { + let expected = b256!("e836db6a959371756f8941be3e962d000f7e12a32e49e2c9ca42ba177a92716c"); + assert_eq!(Jovian::gas_price_oracle_enable_jovian(), expected); + } + + #[test] + fn test_verify_jovian_l1_block_deployment_code_hash() { + let txs = Jovian::deposits().collect::>(); + check_deployment_code( + txs[0].clone(), + Jovian::l1_block_address(), + hex!("5f885ca815d2cf27a203123e50b8ae204fdca910b6995d90b2d7700cbb9240d1").into(), + ); + } + + #[test] + fn test_verify_set_jovian() { + let hash = &keccak256("setJovian()")[..4]; + assert_eq!(hash, hex!("0xb3d72079")) + } + + #[test] + fn test_verify_jovian_gas_price_oracle_deployment_code_hash() { + let txs = Jovian::deposits().collect::>(); + + check_deployment_code( + txs[2].clone(), + Jovian::gas_price_oracle_address(), + hex!("e9fc7c96c4db0d6078e3d359d7e8c982c350a513cb2c31121adf5e1e8a446614").into(), + ); + } +} diff --git a/kona/crates/protocol/hardforks/src/lib.rs b/kona/crates/protocol/hardforks/src/lib.rs new file mode 100644 index 0000000000000..917dc3cd9b18c --- /dev/null +++ b/kona/crates/protocol/hardforks/src/lib.rs @@ -0,0 +1,37 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod traits; +pub use traits::Hardfork; + +mod forks; +pub use forks::Hardforks; + +mod fjord; +pub use fjord::Fjord; + +mod ecotone; +pub use ecotone::Ecotone; + +mod isthmus; +pub use isthmus::Isthmus; + +mod interop; +pub use interop::Interop; + +mod jovian; +pub use jovian::Jovian; + +mod utils; +pub(crate) use utils::upgrade_to_calldata; + +#[cfg(test)] +mod test_utils; diff --git a/kona/crates/protocol/hardforks/src/test_utils.rs b/kona/crates/protocol/hardforks/src/test_utils.rs new file mode 100644 index 0000000000000..e2a222d40e2dc --- /dev/null +++ b/kona/crates/protocol/hardforks/src/test_utils.rs @@ -0,0 +1,61 @@ +//! Test utilities for the `kona-hardforks` crate. + +use alloy_eips::Encodable2718; +use alloy_primitives::{Address, B256, keccak256}; +use op_alloy_consensus::{OpTxType, TxDeposit}; +use op_revm::{DefaultOp, OpSpecId, transaction::deposit::DepositTransactionParts}; +use revm::{ + Context, ExecuteCommitEvm, MainBuilder, + context::{ + CfgEnv, + result::{ExecutionResult, Output}, + }, + database::{CacheDB, EmptyDB}, + interpreter::Host, +}; + +/// Runs an upgrade deposit transaction that deploys a contract in an in-memory EVM, and checks that +/// the contract deploys to the expected address and with the expected codehash. +pub(crate) fn check_deployment_code( + deployment_tx: TxDeposit, + expected_address: Address, + expected_code_hash: B256, +) { + let ctx = Context::op() + .with_cfg(CfgEnv::new_with_spec(OpSpecId::INTEROP)) + .modify_tx_chained(|tx| { + // Deposit + OP meta + tx.deposit = DepositTransactionParts { + source_hash: deployment_tx.source_hash, + mint: Some(deployment_tx.mint), + is_system_transaction: deployment_tx.is_system_transaction, + }; + tx.enveloped_tx = Some(deployment_tx.encoded_2718().into()); + + // Base meta + tx.base.tx_type = OpTxType::Deposit as u8; + tx.base.caller = deployment_tx.from; + tx.base.kind = deployment_tx.to; + tx.base.value = deployment_tx.value; + tx.base.gas_limit = deployment_tx.gas_limit; + tx.base.data = deployment_tx.input; + }) + .with_db(CacheDB::::default()); + let mut evm = ctx.build_mainnet(); + + let res = evm.replay_commit().expect("Failed to run deployment transaction"); + + let address = match res { + ExecutionResult::Success { output: Output::Create(_, Some(address)), .. } => { + assert_eq!(address, expected_address, "Contract deployed to an unexpected address"); + address + } + ExecutionResult::Success { output: Output::Create(_, None), .. } => { + panic!("Contract deployed to the zero address"); + } + res => panic!("Failed to deploy contract: {res:?}"), + }; + + let code = evm.load_account_code(address).expect("Account does not exist"); + assert_eq!(keccak256(code.as_ref()), expected_code_hash); +} diff --git a/kona/crates/protocol/hardforks/src/traits.rs b/kona/crates/protocol/hardforks/src/traits.rs new file mode 100644 index 0000000000000..5d59631de141a --- /dev/null +++ b/kona/crates/protocol/hardforks/src/traits.rs @@ -0,0 +1,9 @@ +//! The trait abstraction for a Hardfork. + +use alloy_primitives::Bytes; + +/// The trait abstraction for a Hardfork. +pub trait Hardfork { + /// Returns the hardfork upgrade transactions as [`Bytes`]. + fn txs(&self) -> impl Iterator + '_; +} diff --git a/kona/crates/protocol/hardforks/src/utils.rs b/kona/crates/protocol/hardforks/src/utils.rs new file mode 100644 index 0000000000000..d2a5fe3196a20 --- /dev/null +++ b/kona/crates/protocol/hardforks/src/utils.rs @@ -0,0 +1,54 @@ +//! Utilities for creating hardforks. + +use alloy_primitives::{Address, Bytes, hex}; + +/// UpgradeTo Function 4Byte Signature +pub(crate) const UPGRADE_TO_FUNC_BYTES_4: [u8; 4] = hex!("3659cfe6"); + +/// Turns the given address into calldata for the `upgradeTo` function. +pub(crate) fn upgrade_to_calldata(addr: Address) -> Bytes { + let mut v = UPGRADE_TO_FUNC_BYTES_4.to_vec(); + v.extend_from_slice(addr.into_word().as_slice()); + v.into() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Ecotone, Fjord, Isthmus}; + use alloy_primitives::keccak256; + + #[test] + fn test_upgrade_to_selector_is_valid() { + let expected_selector = &keccak256("upgradeTo(address)")[..4]; + assert_eq!(UPGRADE_TO_FUNC_BYTES_4, expected_selector); + } + + #[test] + fn test_upgrade_to_calldata_format() { + let test_addr = Address::from([0x42; 20]); + let calldata = upgrade_to_calldata(test_addr); + + assert_eq!(calldata.len(), 36); + assert_eq!(&calldata[..4], UPGRADE_TO_FUNC_BYTES_4); + assert_eq!(&calldata[4..36], test_addr.into_word().as_slice()); + } + + #[test] + fn test_ecotone_selector_is_valid() { + let expected_selector = &keccak256("setEcotone()")[..4]; + assert_eq!(Ecotone::ENABLE_ECOTONE_INPUT, expected_selector); + } + + #[test] + fn test_fjord_selector_is_valid() { + let expected_selector = &keccak256("setFjord()")[..4]; + assert_eq!(Fjord::SET_FJORD_METHOD_SIGNATURE, expected_selector); + } + + #[test] + fn test_isthmus_selector_is_valid() { + let expected_selector = &keccak256("setIsthmus()")[..4]; + assert_eq!(Isthmus::ENABLE_ISTHMUS_INPUT, expected_selector); + } +} diff --git a/kona/crates/protocol/interop/CHANGELOG.md b/kona/crates/protocol/interop/CHANGELOG.md new file mode 100644 index 0000000000000..69a1900528fa6 --- /dev/null +++ b/kona/crates/protocol/interop/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.1](https://github.com/op-rs/kona/compare/kona-interop-v0.1.0...kona-interop-v0.1.1) - 2025-01-16 + +### Other + +- update Cargo.toml dependencies diff --git a/kona/crates/protocol/interop/Cargo.toml b/kona/crates/protocol/interop/Cargo.toml new file mode 100644 index 0000000000000..bae121376dc11 --- /dev/null +++ b/kona/crates/protocol/interop/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "kona-interop" +description = "Core functionality and primitives for the Interop feature of the OP Stack." +version = "0.4.5" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-genesis.workspace = true +kona-registry.workspace = true +kona-protocol.workspace = true + +# General +thiserror.workspace = true +async-trait.workspace = true +tracing.workspace = true +derive_more = { workspace = true, features = ["from", "as_ref", "constructor"] } + +# Alloy +alloy-serde = { workspace = true, optional = true } +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-sol-types.workspace = true +alloy-consensus.workspace = true +alloy-primitives = { workspace = true, features = ["rlp"] } +op-alloy-consensus.workspace = true + +# Arbitrary +arbitrary = { version = "1.4", features = ["derive"], optional = true } + +# Serde +serde = { workspace = true, optional = true } + +[dev-dependencies] +serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } +alloy-primitives = { workspace = true, features = ["rlp", "arbitrary"] } +arbitrary = { version = "1.4", features = ["derive"] } +rand = { workspace = true, features = ["thread_rng"] } + +[features] +default = [] +std = [ + "alloy-consensus/std", + "alloy-eips/std", + "alloy-primitives/std", + "alloy-rlp/std", + "alloy-serde?/std", + "alloy-sol-types/std", + "derive_more/display", + "derive_more/std", + "kona-genesis/std", + "kona-protocol/std", + "kona-registry/std", + "op-alloy-consensus/std", + "serde?/std", + "thiserror/std", + "tracing/std", +] +arbitrary = [ + "alloy-consensus/arbitrary", + "alloy-eips/arbitrary", + "alloy-primitives/arbitrary", + "alloy-serde?/arbitrary", + "alloy-sol-types/arbitrary", + "dep:arbitrary", + "kona-genesis/arbitrary", + "kona-protocol/arbitrary", + "op-alloy-consensus/arbitrary", + "std", +] +serde = [ + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-primitives/serde", + "dep:alloy-serde", + "dep:serde", + "kona-genesis/serde", + "kona-protocol/serde", + "op-alloy-consensus/serde", +] +test-utils = [ "kona-protocol/test-utils", "std" ] + diff --git a/kona/crates/protocol/interop/README.md b/kona/crates/protocol/interop/README.md new file mode 100644 index 0000000000000..2c387a06a4736 --- /dev/null +++ b/kona/crates/protocol/interop/README.md @@ -0,0 +1,8 @@ +# `kona-interop` + +CI +Kona MPT +License +Codecov + +Core functionality and primitives for the [Interop feature](https://specs.optimism.io/interop/overview.html) of the OP Stack. diff --git a/kona/crates/protocol/interop/src/access_list.rs b/kona/crates/protocol/interop/src/access_list.rs new file mode 100644 index 0000000000000..77e63f5bb2268 --- /dev/null +++ b/kona/crates/protocol/interop/src/access_list.rs @@ -0,0 +1,28 @@ +use alloy_eips::eip2930::AccessListItem; +use alloy_primitives::B256; +use kona_protocol::Predeploys; + +/// Parses [`AccessListItem`]s to inbox entries. +/// +/// See [`parse_access_list_item_to_inbox_entries`] for more details. Return flattened iterator with +/// all inbox entries. +pub fn parse_access_list_items_to_inbox_entries<'a>( + access_list_items: impl Iterator, +) -> impl Iterator { + access_list_items.filter_map(parse_access_list_item_to_inbox_entries).flatten() +} + +/// Parse [`AccessListItem`] to inbox entries, if any. +/// Max 3 inbox entries can exist per [`AccessListItem`] that points to +/// [`Predeploys::CROSS_L2_INBOX`]. +/// +/// Returns `Vec::new()` if [`AccessListItem`] address doesn't point to +/// [`Predeploys::CROSS_L2_INBOX`]. +/// +/// See: +pub fn parse_access_list_item_to_inbox_entries( + access_list_item: &AccessListItem, +) -> Option> { + (access_list_item.address == Predeploys::CROSS_L2_INBOX) + .then(|| access_list_item.storage_keys.iter()) +} diff --git a/kona/crates/protocol/interop/src/constants.rs b/kona/crates/protocol/interop/src/constants.rs new file mode 100644 index 0000000000000..567b33d46c6c9 --- /dev/null +++ b/kona/crates/protocol/interop/src/constants.rs @@ -0,0 +1,10 @@ +//! Constants for the OP Stack interop protocol. + +/// The expiry window for relaying an initiating message (in seconds). +/// +pub const MESSAGE_EXPIRY_WINDOW: u64 = 7 * 24 * 60 * 60; + +/// The current version of the [SuperRoot] encoding format. +/// +/// [SuperRoot]: crate::SuperRoot +pub const SUPER_ROOT_VERSION: u8 = 1; diff --git a/kona/crates/protocol/interop/src/control.rs b/kona/crates/protocol/interop/src/control.rs new file mode 100644 index 0000000000000..40acb49a802e6 --- /dev/null +++ b/kona/crates/protocol/interop/src/control.rs @@ -0,0 +1,49 @@ +//! Contains the `ControlEvent`. + +use alloy_primitives::B256; +use kona_protocol::BlockInfo; + +/// Control Event +/// +/// The `ControlEvent` is an action performed by the supervisor +/// on the L2 consensus node, in this case the `kona-node`. +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum ControlEvent { + /// Invalidates a specified block. + /// + /// Based on some dependency or L1 changes, the supervisor + /// can instruct the L2 to invalidate a specific block. + InvalidateBlock(B256), + + /// The supervisor sends the next L1 block to the node. + /// Ideally sent after the node emits exhausted-l1. + ProviderL1(BlockInfo), + + /// Forces a reset to a specific local-unsafe/local-safe/finalized + /// starting point only if the blocks did exist. Resets may override + /// local-unsafe, to reset the very end of the chain. Resets may + /// override local-safe, since post-interop we need the local-safe + /// block derivation to continue. + Reset { + /// The local-unsafe block to reset to. + local_unsafe: Option, + /// The cross-unsafe block to reset to. + cross_unsafe: Option, + /// The local-safe block to reset to. + local_safe: Option, + /// The cross-safe block to reset to. + cross_safe: Option, + /// The finalized block to reset to. + finalized: Option, + }, + + /// Signal that a block can be promoted to cross-safe. + UpdateCrossSafe(BlockInfo), + + /// Signal that a block can be promoted to cross-unsafe. + UpdateCrossUnsafe(BlockInfo), + + /// Signal that a block can be marked as finalized. + UpdateFinalized(BlockInfo), +} diff --git a/kona/crates/protocol/interop/src/depset.rs b/kona/crates/protocol/interop/src/depset.rs new file mode 100644 index 0000000000000..86b821dfec034 --- /dev/null +++ b/kona/crates/protocol/interop/src/depset.rs @@ -0,0 +1,69 @@ +use crate::MESSAGE_EXPIRY_WINDOW; +use alloy_primitives::ChainId; +use kona_registry::HashMap; + +/// Configuration for a dependency of a chain +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ChainDependency {} + +/// Configuration for the dependency set +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct DependencySet { + /// Dependencies information per chain. + pub dependencies: HashMap, + + /// Override message expiry window to use for this dependency set. + pub override_message_expiry_window: Option, +} + +impl DependencySet { + /// Returns the message expiry window associated with this dependency set. + pub const fn get_message_expiry_window(&self) -> u64 { + match self.override_message_expiry_window { + Some(window) if window > 0 => window, + _ => MESSAGE_EXPIRY_WINDOW, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::ChainId; + use kona_registry::HashMap; + + const fn create_dependency_set( + dependencies: HashMap, + override_expiry: u64, + ) -> DependencySet { + DependencySet { dependencies, override_message_expiry_window: Some(override_expiry) } + } + + #[test] + fn test_get_message_expiry_window_default() { + let deps = HashMap::default(); + // override_message_expiry_window is 0, so default should be used + let ds = create_dependency_set(deps, 0); + assert_eq!( + ds.get_message_expiry_window(), + MESSAGE_EXPIRY_WINDOW, + "Should return default expiry window when override is 0" + ); + } + + #[test] + fn test_get_message_expiry_window_override() { + let deps = HashMap::default(); + let override_value = 12345; + let ds = create_dependency_set(deps, override_value); + assert_eq!( + ds.get_message_expiry_window(), + override_value, + "Should return override expiry window when it's non-zero" + ); + } +} diff --git a/kona/crates/protocol/interop/src/derived.rs b/kona/crates/protocol/interop/src/derived.rs new file mode 100644 index 0000000000000..014b9baf1d826 --- /dev/null +++ b/kona/crates/protocol/interop/src/derived.rs @@ -0,0 +1,41 @@ +//! Contains derived types for interop. + +use alloy_eips::eip1898::BlockNumHash; +use derive_more::Display; +use kona_protocol::BlockInfo; + +/// A pair of [`BlockNumHash`]s representing a derivation relationship between two blocks. +/// +/// The [`DerivedIdPair`] links a source block (L1) to a derived block (L2) where the derived block +/// is derived from the source block. +/// +/// - `source`: The [`BlockNumHash`] of the source (L1) block. +/// - `derived`: The [`BlockNumHash`] of the derived (L2) block. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct DerivedIdPair { + /// The [`BlockNumHash`] of the source (L1) block. + pub source: BlockNumHash, + /// The [`BlockNumHash`] of the derived (L2) block. + pub derived: BlockNumHash, +} + +/// A pair of [`BlockInfo`]s representing a derivation relationship between two blocks. +/// +/// The [`DerivedRefPair`] contains full block information for both the source (L1) and +/// derived (L2) blocks, where the derived block is produced from the source block. +/// +/// - `source`: The [`BlockInfo`] of the source (L1) block. +/// - `derived`: The [`BlockInfo`] of the derived (L2) block. +// See the interop control flow specification: https://github.com/ethereum-optimism/specs/blob/main/specs/interop/managed-node.md +#[derive(Debug, Clone, Copy, Display, PartialEq, Eq)] +#[display("source: {source}, derived: {derived}")] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct DerivedRefPair { + /// The [`BlockInfo`] of the source (L1) block. + pub source: BlockInfo, + /// The [`BlockInfo`] of the derived (L2) block. + pub derived: BlockInfo, +} diff --git a/kona/crates/protocol/interop/src/errors.rs b/kona/crates/protocol/interop/src/errors.rs new file mode 100644 index 0000000000000..66e61601ec5f4 --- /dev/null +++ b/kona/crates/protocol/interop/src/errors.rs @@ -0,0 +1,135 @@ +//! Error types for the `kona-interop` crate. + +use crate::InteropProvider; +use alloy_primitives::{Address, B256}; +use core::fmt::Debug; +use kona_registry::HashMap; +use thiserror::Error; + +/// An error type for the [MessageGraph] struct. +/// +/// [MessageGraph]: crate::MessageGraph +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum MessageGraphError { + /// Dependency set is impossibly empty + #[error("Dependency set is impossibly empty")] + EmptyDependencySet, + /// Missing a [RollupConfig] for a chain ID + /// + /// [RollupConfig]: kona_genesis::RollupConfig + #[error("Missing a RollupConfig for chain ID {0}")] + MissingRollupConfig(u64), + /// Interop provider error + #[error("Interop provider: {0}")] + InteropProviderError(#[from] E), + /// Remote message not found + #[error("Remote message not found on chain ID {chain_id} with message hash {message_hash}")] + RemoteMessageNotFound { + /// The remote chain ID + chain_id: u64, + /// The message hash + message_hash: B256, + }, + /// Invalid message origin + #[error("Invalid message origin. Expected {expected}, got {actual}")] + InvalidMessageOrigin { + /// The expected message origin + expected: Address, + /// The actual message origin + actual: Address, + }, + /// Invalid message payload hash + #[error("Invalid message hash. Expected {expected}, got {actual}")] + InvalidMessageHash { + /// The expected message hash + expected: B256, + /// The actual message hash + actual: B256, + }, + /// Invalid message timestamp + #[error("Invalid message timestamp. Expected {expected}, got {actual}")] + InvalidMessageTimestamp { + /// The expected timestamp + expected: u64, + /// The actual timestamp + actual: u64, + }, + /// Interop has not been activated for at least one block on the initiating message's chain. + #[error( + "Interop has not been active for at least one block on initiating message's chain. Activation time: {activation_time}, initiating message time: {initiating_message_time}" + )] + InitiatedTooEarly { + /// The timestamp of the interop activation + activation_time: u64, + /// The timestamp of the initiating message + initiating_message_time: u64, + }, + /// Message is in the future + #[error("Message is in the future. Expected timestamp to be <= {max}, got {actual}")] + MessageInFuture { + /// The expected max timestamp + max: u64, + /// The actual timestamp + actual: u64, + }, + /// Message has exceeded the expiry window. + #[error( + "Message has exceeded the expiry window. Initiating Timestamp: {initiating_timestamp}, Executing Timestamp: {executing_timestamp}" + )] + MessageExpired { + /// The timestamp of the initiating message + initiating_timestamp: u64, + /// The timestamp of the executing message + executing_timestamp: u64, + }, + /// Invalid messages were found + #[error("Invalid messages found on chains: {0:?}")] + InvalidMessages(HashMap>), +} + +/// A [Result] alias for the [MessageGraphError] type. +#[allow(type_alias_bounds)] +pub type MessageGraphResult = + core::result::Result>; + +/// An error type for the [SuperRoot] struct's serialization and deserialization. +/// +/// [SuperRoot]: crate::SuperRoot +#[derive(Debug, Clone, Error)] +pub enum SuperRootError { + /// Invalid super root version byte + #[error("Invalid super root version byte")] + InvalidVersionByte, + /// Unexpected encoded super root length + #[error("Unexpected encoded super root length")] + UnexpectedLength, + /// Slice conversion error + #[error("Slice conversion error: {0}")] + SliceConversionError(#[from] core::array::TryFromSliceError), +} + +/// A [Result] alias for the [SuperRootError] type. +pub type SuperRootResult = core::result::Result; + +/// Errors that can occur during interop validation. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum InteropValidationError { + /// Interop is not enabled on one or both chains at the required timestamp. + #[error("interop not enabled")] + InteropNotEnabled, + + /// Executing timestamp is earlier than the initiating timestamp. + #[error( + "executing timestamp is earlier than initiating timestamp, executing: {executing}, initiating: {initiating}" + )] + InvalidTimestampInvariant { + /// Executing timestamp of the message + executing: u64, + /// Initiating timestamp of the message + initiating: u64, + }, + + /// Timestamp is outside the allowed interop expiry window. + #[error("timestamp outside allowed interop window, timestamp: {0}")] + InvalidInteropTimestamp(u64), +} diff --git a/kona/crates/protocol/interop/src/event.rs b/kona/crates/protocol/interop/src/event.rs new file mode 100644 index 0000000000000..d2b3ee5e19b13 --- /dev/null +++ b/kona/crates/protocol/interop/src/event.rs @@ -0,0 +1,65 @@ +//! Contains the managed node event. + +use crate::{BlockReplacement, DerivedRefPair}; +use alloc::{format, string::String, vec::Vec}; +use derive_more::Constructor; +use kona_protocol::BlockInfo; + +/// Event sent by the node to the supervisor to share updates. +/// +/// This struct is used to communicate various events that occur within the node. +/// At least one of the fields will be `Some`, and the rest will be `None`. +/// +/// See: +#[derive(Debug, Clone, Default, PartialEq, Eq, Constructor)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ManagedEvent { + /// This is emitted when the node has determined that it needs a reset. + /// It tells the supervisor to send the interop_reset event with the + /// required parameters. + pub reset: Option, + + /// New L2 unsafe block was processed, updating local-unsafe head. + pub unsafe_block: Option, + + /// Signals that an L2 block is considered local-safe. + pub derivation_update: Option, + + /// Emitted when no more L1 Blocks are available. + /// Ready to take new L1 blocks from supervisor. + pub exhaust_l1: Option, + + /// Emitted when a block gets replaced for any reason. + pub replace_block: Option, + + /// Signals that an L2 block is now local-safe because of the given L1 traversal. + /// This would be accompanied with [`Self::derivation_update`]. + pub derivation_origin_update: Option, +} + +impl core::fmt::Display for ManagedEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut parts = Vec::new(); + if let Some(ref reset) = self.reset { + parts.push(format!("reset: {reset}")); + } + if let Some(ref block) = self.unsafe_block { + parts.push(format!("unsafe_block: {block}")); + } + if let Some(ref pair) = self.derivation_update { + parts.push(format!("derivation_update: {pair}")); + } + if let Some(ref pair) = self.exhaust_l1 { + parts.push(format!("exhaust_l1: {pair}")); + } + if let Some(ref replacement) = self.replace_block { + parts.push(format!("replace_block: {replacement}")); + } + if let Some(ref origin) = self.derivation_origin_update { + parts.push(format!("derivation_origin_update: {origin}")); + } + + if parts.is_empty() { write!(f, "none") } else { write!(f, "{}", parts.join(", ")) } + } +} diff --git a/kona/crates/protocol/interop/src/graph.rs b/kona/crates/protocol/interop/src/graph.rs new file mode 100644 index 0000000000000..5316ac6067df8 --- /dev/null +++ b/kona/crates/protocol/interop/src/graph.rs @@ -0,0 +1,636 @@ +//! Interop [`MessageGraph`]. + +use crate::{ + MESSAGE_EXPIRY_WINDOW, RawMessagePayload, + errors::{MessageGraphError, MessageGraphResult}, + message::{EnrichedExecutingMessage, extract_executing_messages}, + traits::InteropProvider, +}; +use alloc::{string::ToString, vec::Vec}; +use alloy_consensus::{Header, Sealed}; +use alloy_primitives::keccak256; +use kona_genesis::RollupConfig; +use kona_registry::{HashMap, ROLLUP_CONFIGS}; +use tracing::{info, warn}; + +/// The [`MessageGraph`] represents a set of blocks at a given timestamp and the interop +/// dependencies between them. +/// +/// This structure is used to determine whether or not any interop messages are invalid within the +/// set of blocks within the graph. An "invalid message" is one that was relayed from one chain to +/// another, but the original [`MessageIdentifier`] is not present within the graph or from a +/// dependency referenced via the [`InteropProvider`] (or otherwise is invalid, such as being older +/// than the message expiry window). +/// +/// Message validity rules: +/// +/// [`MessageIdentifier`]: crate::MessageIdentifier +#[derive(Debug)] +pub struct MessageGraph<'a, P> { + /// The edges within the graph. + /// + /// These are derived from the transactions within the blocks. + messages: Vec, + /// The data provider for the graph. Required for fetching headers, receipts and remote + /// messages within history during resolution. + provider: &'a P, + /// Backup rollup configs for each chain. + rollup_configs: &'a HashMap, +} + +impl<'a, P> MessageGraph<'a, P> +where + P: InteropProvider, +{ + /// Derives the edges from the blocks within the graph by scanning all receipts within the + /// blocks and searching for [`ExecutingMessage`]s. + /// + /// [`ExecutingMessage`]: crate::ExecutingMessage + pub async fn derive( + blocks: &HashMap>, + provider: &'a P, + rollup_configs: &'a HashMap, + ) -> MessageGraphResult { + info!( + target: "message_graph", + num_chains = blocks.len(), + "Deriving message graph", + ); + + let mut messages = Vec::with_capacity(blocks.len()); + for (chain_id, header) in blocks.iter() { + let receipts = provider.receipts_by_hash(*chain_id, header.hash()).await?; + let executing_messages = extract_executing_messages(receipts.as_slice()); + + messages.extend(executing_messages.into_iter().map(|message| { + EnrichedExecutingMessage::new(message, *chain_id, header.timestamp) + })); + } + + info!( + target: "message_graph", + num_chains = blocks.len(), + num_messages = messages.len(), + "Derived message graph successfully", + ); + Ok(Self { messages, provider, rollup_configs }) + } + + /// Checks the validity of all messages within the graph. + /// + /// _Note_: This function does not account for cascading dependency failures. When + /// [`MessageGraphError::InvalidMessages`] is returned by this function, the consumer must + /// re-execute the bad blocks with deposit transactions only per the [interop derivation + /// rules][int-block-replacement]. Once the bad blocks have been replaced, a new + /// [`MessageGraph`] should be constructed and resolution should be re-attempted. + /// This process should repeat recursively until no invalid dependencies remain, with the + /// terminal case being all blocks reduced to deposits-only. + /// + /// [int-block-replacement]: https://specs.optimism.io/interop/derivation.html#replacing-invalid-blocks + pub async fn resolve(self) -> MessageGraphResult<(), P> { + info!( + target: "message_graph", + "Checking the message graph for invalid messages" + ); + + // Create a new vector to store invalid edges + let mut invalid_messages = HashMap::default(); + + // Prune all valid messages, collecting errors for any chain whose block contains an invalid + // message. Errors are de-duplicated by chain ID in a map, since a single invalid + // message is cause for invalidating a block. + for message in self.messages.iter() { + if let Err(e) = self.check_single_dependency(message).await { + warn!( + target: "message_graph", + executing_chain_id = message.executing_chain_id, + message_hash = ?message.inner.payloadHash, + err = %e, + "Invalid ExecutingMessage found", + ); + invalid_messages.insert(message.executing_chain_id, e); + } + } + + info!( + target: "message_graph", + num_invalid_messages = invalid_messages.len(), + "Successfully reduced the message graph", + ); + + // Check if the graph is now empty. If not, there are invalid messages. + if !invalid_messages.is_empty() { + warn!( + target: "message_graph", + bad_chain_ids = %invalid_messages + .keys() + .map(ToString::to_string) + .collect::>() + .join(", "), + "Failed to reduce the message graph entirely", + ); + + // Return an error with the chain IDs of the blocks containing invalid messages. + return Err(MessageGraphError::InvalidMessages(invalid_messages)); + } + + Ok(()) + } + + /// Checks the dependency of a single [`EnrichedExecutingMessage`]. If the message's + /// dependencies are unavailable, the message is considered invalid and an [`Err`] is + /// returned. + async fn check_single_dependency( + &self, + message: &EnrichedExecutingMessage, + ) -> MessageGraphResult<(), P> { + // ChainID Invariant: The chain id of the initiating message MUST be in the dependency set + // This is enforced implicitly by the graph constructor and the provider. + + let initiating_chain_id = message.inner.identifier.chainId.saturating_to(); + let initiating_timestamp = message.inner.identifier.timestamp.saturating_to::(); + + // Attempt to fetch the rollup config for the initiating chain from the registry. If the + // rollup config is not found, fall back to the local rollup configs. + let rollup_config = ROLLUP_CONFIGS + .get(&initiating_chain_id) + .or_else(|| self.rollup_configs.get(&initiating_chain_id)) + .ok_or(MessageGraphError::MissingRollupConfig(initiating_chain_id))?; + + // Timestamp invariant: The timestamp at the time of inclusion of the initiating message + // MUST be less than or equal to the timestamp of the executing message as well as greater + // than the Interop activation block's timestamp. + if initiating_timestamp > message.executing_timestamp { + return Err(MessageGraphError::MessageInFuture { + max: message.executing_timestamp, + actual: initiating_timestamp, + }); + } else if initiating_timestamp < + rollup_config.hardforks.interop_time.unwrap_or_default() + rollup_config.block_time + { + return Err(MessageGraphError::InitiatedTooEarly { + activation_time: rollup_config.hardforks.interop_time.unwrap_or_default(), + initiating_message_time: initiating_timestamp, + }); + } + + // Message expiry invariant: The timestamp of the initiating message must be no more than + // `MESSAGE_EXPIRY_WINDOW` seconds in the past, relative to the timestamp of the executing + // message. + if initiating_timestamp < message.executing_timestamp.saturating_sub(MESSAGE_EXPIRY_WINDOW) + { + return Err(MessageGraphError::MessageExpired { + initiating_timestamp, + executing_timestamp: message.executing_timestamp, + }); + } + + // Fetch the header & receipts for the message's claimed origin block on the remote chain. + let remote_header = self + .provider + .header_by_number( + message.inner.identifier.chainId.saturating_to(), + message.inner.identifier.blockNumber.saturating_to(), + ) + .await?; + let remote_receipts = self + .provider + .receipts_by_number( + message.inner.identifier.chainId.saturating_to(), + message.inner.identifier.blockNumber.saturating_to(), + ) + .await?; + + // Find the log that matches the message's claimed log index. Note that the + // log index is global to the block, so we chain the full block's logs together + // to find it. + let remote_log = remote_receipts + .iter() + .flat_map(|receipt| receipt.logs()) + .nth(message.inner.identifier.logIndex.saturating_to()) + .ok_or(MessageGraphError::RemoteMessageNotFound { + chain_id: message.inner.identifier.chainId.to(), + message_hash: message.inner.payloadHash, + })?; + + // Validate the message's origin is correct. + if remote_log.address != message.inner.identifier.origin { + return Err(MessageGraphError::InvalidMessageOrigin { + expected: message.inner.identifier.origin, + actual: remote_log.address, + }); + } + + // Validate that the message hash is correct. + let remote_message = RawMessagePayload::from(remote_log); + let remote_message_hash = keccak256(remote_message.as_ref()); + if remote_message_hash != message.inner.payloadHash { + return Err(MessageGraphError::InvalidMessageHash { + expected: message.inner.payloadHash, + actual: remote_message_hash, + }); + } + + // Validate that the timestamp of the block header containing the log is correct. + if remote_header.timestamp != initiating_timestamp { + return Err(MessageGraphError::InvalidMessageTimestamp { + expected: initiating_timestamp, + actual: remote_header.timestamp, + }); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::{MESSAGE_EXPIRY_WINDOW, MessageGraph}; + use crate::{ + MessageGraphError, + test_util::{ExecutingMessageBuilder, SuperchainBuilder}, + }; + use alloy_primitives::{Address, hex, keccak256}; + + const MOCK_MESSAGE: [u8; 4] = hex!("deadbeef"); + const CHAIN_A_ID: u64 = 1; + const CHAIN_B_ID: u64 = 2; + + /// Returns a [`SuperchainBuilder`] with two chains (ids: `CHAIN_A_ID` and `CHAIN_B_ID`), + /// configured with interop activating at timestamp `0`, the current block at timestamp `2`, + /// and a block time of `2` seconds. + fn default_superchain() -> SuperchainBuilder { + let mut superchain = SuperchainBuilder::new(); + superchain + .chain(CHAIN_A_ID) + .with_timestamp(2) + .with_block_time(2) + .with_interop_activation_time(0); + superchain + .chain(CHAIN_B_ID) + .with_timestamp(2) + .with_block_time(2) + .with_interop_activation_time(0); + + superchain + } + + #[tokio::test] + async fn test_derive_and_resolve_simple_graph_no_cycles() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + graph.resolve().await.unwrap(); + } + + #[tokio::test] + async fn test_derive_and_resolve_simple_graph_with_cycles() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + let chain_b_time = superchain.chain(CHAIN_B_ID).header.timestamp; + + superchain + .chain(CHAIN_A_ID) + .add_initiating_message(MOCK_MESSAGE.into()) + .add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_B_ID) + .with_origin_timestamp(chain_b_time), + ); + superchain + .chain(CHAIN_B_ID) + .add_initiating_message(MOCK_MESSAGE.into()) + .add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + graph.resolve().await.unwrap(); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_message_in_future() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time + 1), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::MessageInFuture { max: 2, actual: chain_a_time + 1 } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_initiating_before_interop() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain + .chain(CHAIN_A_ID) + .with_interop_activation_time(50) + .add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InitiatedTooEarly { + activation_time: 50, + initiating_message_time: chain_a_time + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_initiating_before_interop_unaligned_activation() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + // Chain A activates @ `1s`, which is unaligned with the block time of `2s`. The first + // block, at `2s`, should be the activation block. + superchain + .chain(CHAIN_A_ID) + .with_interop_activation_time(1) + .add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InitiatedTooEarly { + activation_time: 1, + initiating_message_time: chain_a_time + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_initiating_at_interop_activation() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain + .chain(CHAIN_A_ID) + .with_interop_activation_time(chain_a_time) + .add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InitiatedTooEarly { activation_time: 2, initiating_message_time: 2 } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_message_expired() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain + .chain(CHAIN_B_ID) + .with_timestamp(chain_a_time + MESSAGE_EXPIRY_WINDOW + 1) + .add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::MessageExpired { + initiating_timestamp: chain_a_time, + executing_timestamp: chain_a_time + MESSAGE_EXPIRY_WINDOW + 1 + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_remote_message_not_found() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::RemoteMessageNotFound { + chain_id: CHAIN_A_ID, + message_hash: keccak256(MOCK_MESSAGE) + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_invalid_origin_address() { + let mut superchain = default_superchain(); + let mock_address = Address::left_padding_from(&[0xFF]); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_address(mock_address) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InvalidMessageOrigin { + expected: mock_address, + actual: Address::ZERO + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_invalid_message_hash() { + let mut superchain = default_superchain(); + let mock_message_hash = keccak256([0xBE, 0xEF]); + + let chain_a_time = superchain.chain(CHAIN_A_ID).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(mock_message_hash) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InvalidMessageHash { + expected: mock_message_hash, + actual: keccak256(MOCK_MESSAGE) + } + ); + } + + #[tokio::test] + async fn test_derive_and_resolve_graph_invalid_timestamp() { + let mut superchain = default_superchain(); + + let chain_a_time = superchain.chain(CHAIN_A_ID).with_timestamp(4).header.timestamp; + + superchain.chain(CHAIN_A_ID).add_initiating_message(MOCK_MESSAGE.into()); + superchain.chain(CHAIN_B_ID).with_timestamp(4).add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(keccak256(MOCK_MESSAGE)) + .with_origin_chain_id(CHAIN_A_ID) + .with_origin_timestamp(chain_a_time - 1), + ); + + let (headers, cfgs, provider) = superchain.build(); + + let graph = MessageGraph::derive(&headers, &provider, &cfgs).await.unwrap(); + let MessageGraphError::InvalidMessages(invalid_messages) = + graph.resolve().await.unwrap_err() + else { + panic!("Expected invalid messages") + }; + + assert_eq!(invalid_messages.len(), 1); + assert_eq!( + *invalid_messages.get(&CHAIN_B_ID).unwrap(), + MessageGraphError::InvalidMessageTimestamp { + expected: chain_a_time - 1, + actual: chain_a_time + } + ); + } +} diff --git a/kona/crates/protocol/interop/src/lib.rs b/kona/crates/protocol/interop/src/lib.rs new file mode 100644 index 0000000000000..b6ebf9995be05 --- /dev/null +++ b/kona/crates/protocol/interop/src/lib.rs @@ -0,0 +1,65 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod graph; +pub use graph::MessageGraph; + +mod event; +pub use event::ManagedEvent; + +mod control; +pub use control::ControlEvent; + +mod replacement; +pub use replacement::BlockReplacement; + +mod traits; +pub use traits::{InteropProvider, InteropValidator}; + +mod safety; +pub use safety::SafetyLevelParseError; + +mod errors; +pub use errors::{ + InteropValidationError, MessageGraphError, MessageGraphResult, SuperRootError, SuperRootResult, +}; + +mod root; +pub use root::{ChainRootInfo, OutputRootWithChain, SuperRoot, SuperRootOutput}; + +mod message; +pub use message::{ + EnrichedExecutingMessage, ExecutingDescriptor, ExecutingMessage, MessageIdentifier, + RawMessagePayload, extract_executing_messages, parse_log_to_executing_message, + parse_logs_to_executing_msgs, +}; + +mod depset; +pub use depset::{ChainDependency, DependencySet}; + +pub use op_alloy_consensus::interop::SafetyLevel; + +mod access_list; +pub use access_list::{ + parse_access_list_item_to_inbox_entries, parse_access_list_items_to_inbox_entries, +}; +mod derived; +pub use derived::{DerivedIdPair, DerivedRefPair}; + +mod constants; +pub use constants::{MESSAGE_EXPIRY_WINDOW, SUPER_ROOT_VERSION}; + +#[cfg(any(test, feature = "test-utils"))] +mod test_util; +#[cfg(any(test, feature = "test-utils"))] +pub use test_util::{ + ChainBuilder, ExecutingMessageBuilder, InteropProviderError, MockInteropProvider, + SuperchainBuilder, +}; diff --git a/kona/crates/protocol/interop/src/message.rs b/kona/crates/protocol/interop/src/message.rs new file mode 100644 index 0000000000000..0a05209adf0f3 --- /dev/null +++ b/kona/crates/protocol/interop/src/message.rs @@ -0,0 +1,241 @@ +//! Interop message primitives. +//! +//! +//! + +use alloc::{vec, vec::Vec}; +use alloy_primitives::{Bytes, ChainId, Log, keccak256}; +use alloy_sol_types::{SolEvent, sol}; +use derive_more::{AsRef, Constructor, From}; +use kona_protocol::Predeploys; +use op_alloy_consensus::OpReceiptEnvelope; + +sol! { + /// @notice The struct for a pointer to a message payload in a remote (or local) chain. + #[derive(Default, Debug, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + struct MessageIdentifier { + address origin; + uint256 blockNumber; + uint256 logIndex; + uint256 timestamp; + #[cfg_attr(feature = "serde", serde(rename = "chainID"))] + uint256 chainId; + } + + /// @notice Emitted when a cross chain message is being executed. + /// @param payloadHash Hash of message payload being executed. + /// @param identifier Encoded Identifier of the message. + /// + /// Parameter names are derived from the `op-supervisor` JSON field names. + /// See the relevant definition in the Optimism repository: + /// [Ethereum-Optimism/op-supervisor](https://github.com/ethereum-optimism/optimism/blob/4ba2eb00eafc3d7de2c8ceb6fd83913a8c0a2c0d/op-supervisor/supervisor/types/types.go#L61-L64). + #[derive(Default, Debug, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + event ExecutingMessage(bytes32 indexed payloadHash, MessageIdentifier identifier); + + /// @notice Executes a cross chain message on the destination chain. + /// @param _id Identifier of the message. + /// @param _target Target address to call. + /// @param _message Message payload to call target with. + function executeMessage( + MessageIdentifier calldata _id, + address _target, + bytes calldata _message + ) external; +} + +/// A [RawMessagePayload] is the raw payload of an initiating message. +#[derive(Debug, Clone, From, AsRef, PartialEq, Eq)] +pub struct RawMessagePayload(Bytes); + +impl From<&Log> for RawMessagePayload { + fn from(log: &Log) -> Self { + let mut data = vec![0u8; log.topics().len() * 32 + log.data.data.len()]; + for (i, topic) in log.topics().iter().enumerate() { + data[i * 32..(i + 1) * 32].copy_from_slice(topic.as_ref()); + } + data[(log.topics().len() * 32)..].copy_from_slice(log.data.data.as_ref()); + data.into() + } +} + +impl From> for RawMessagePayload { + fn from(data: Vec) -> Self { + Self(Bytes::from(data)) + } +} + +impl From for ExecutingMessage { + fn from(call: executeMessageCall) -> Self { + Self { identifier: call._id, payloadHash: keccak256(call._message.as_ref()) } + } +} + +/// An [`ExecutingDescriptor`] is a part of the payload to `supervisor_checkAccessList` +/// Spec: +#[derive(Default, Debug, PartialEq, Eq, Clone, Constructor)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExecutingDescriptor { + /// The timestamp used to enforce timestamp [invariant](https://github.com/ethereum-optimism/specs/blob/main/specs/interop/derivation.md#invariants) + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub timestamp: u64, + /// The timeout that requests verification to still hold at `timestamp+timeout` + /// (message expiry may drop previously valid messages). + #[cfg_attr( + feature = "serde", + serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + ) + )] + pub timeout: Option, + /// Chain ID of the chain that the message was executed on. + #[cfg_attr( + feature = "serde", + serde( + default, + rename = "chainID", + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + ) + )] + pub chain_id: Option, +} + +/// A wrapper type for [ExecutingMessage] containing the chain ID of the chain that the message was +/// executed on. +#[derive(Debug)] +pub struct EnrichedExecutingMessage { + /// The inner [ExecutingMessage]. + pub inner: ExecutingMessage, + /// The chain ID of the chain that the message was executed on. + pub executing_chain_id: u64, + /// The timestamp of the block that the executing message was included in. + pub executing_timestamp: u64, +} + +impl EnrichedExecutingMessage { + /// Create a new [EnrichedExecutingMessage] from an [ExecutingMessage] and a chain ID. + pub const fn new( + inner: ExecutingMessage, + executing_chain_id: u64, + executing_timestamp: u64, + ) -> Self { + Self { inner, executing_chain_id, executing_timestamp } + } +} + +/// Extracts all [ExecutingMessage] events from list of [OpReceiptEnvelope]s. +/// +/// See [`parse_log_to_executing_message`]. +/// +/// Note: filters out logs that don't contain executing message events. +pub fn extract_executing_messages(receipts: &[OpReceiptEnvelope]) -> Vec { + receipts.iter().fold(Vec::new(), |mut acc, envelope| { + let executing_messages = envelope.logs().iter().filter_map(parse_log_to_executing_message); + + acc.extend(executing_messages); + acc + }) +} + +/// Parses [`Log`]s to [`ExecutingMessage`]s. +/// +/// See [`parse_log_to_executing_message`] for more details. Return iterator maps 1-1 with input. +pub fn parse_logs_to_executing_msgs<'a>( + logs: impl Iterator, +) -> impl Iterator> { + logs.map(parse_log_to_executing_message) +} + +/// Parse [`Log`] to [`ExecutingMessage`], if any. +/// +/// Max one [`ExecutingMessage`] event can exist per log. Returns `None` if log doesn't contain +/// executing message event. +pub fn parse_log_to_executing_message(log: &Log) -> Option { + (log.address == Predeploys::CROSS_L2_INBOX && log.topics().len() == 2) + .then(|| ExecutingMessage::decode_log_data(&log.data).ok()) + .flatten() +} + +#[cfg(test)] +mod tests { + use alloy_primitives::{Address, B256, LogData, U256}; + + use super::*; + + // Test the serialization of ExecutingDescriptor + #[cfg(feature = "serde")] + #[test] + fn test_serialize_executing_descriptor() { + let descriptor = ExecutingDescriptor { + timestamp: 1234567890, + timeout: Some(3600), + chain_id: Some(1000), + }; + let serialized = serde_json::to_string(&descriptor).unwrap(); + let expected = r#"{"timestamp":"0x499602d2","timeout":"0xe10","chainID":"0x3e8"}"#; + assert_eq!(serialized, expected); + + let deserialized: ExecutingDescriptor = serde_json::from_str(&serialized).unwrap(); + assert_eq!(descriptor, deserialized); + } + + #[cfg(feature = "serde")] + #[test] + fn test_deserialize_executing_descriptor_missing_chain_id() { + let json = r#"{ + "timestamp": "0x499602d2", + "timeout": "0xe10" + }"#; + + let expected = + ExecutingDescriptor { timestamp: 1234567890, timeout: Some(3600), chain_id: None }; + + let deserialized: ExecutingDescriptor = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, expected); + } + + #[cfg(feature = "serde")] + #[test] + fn test_deserialize_executing_descriptor_missing_timeout() { + let json = r#"{ + "timestamp": "0x499602d2", + "chainID": "0x3e8" + }"#; + + let expected = + ExecutingDescriptor { timestamp: 1234567890, timeout: None, chain_id: Some(1000) }; + + let deserialized: ExecutingDescriptor = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn test_parse_logs_to_executing_msgs_iterator() { + // One valid, one invalid log + let identifier = MessageIdentifier { + origin: Address::repeat_byte(0x77), + blockNumber: U256::from(200), + logIndex: U256::from(3), + timestamp: U256::from(777777), + chainId: U256::from(12), + }; + let payload_hash = B256::repeat_byte(0x88); + let event = ExecutingMessage { payloadHash: payload_hash, identifier }; + let data = ExecutingMessage::encode_log_data(&event); + + let valid_log = Log { address: Predeploys::CROSS_L2_INBOX, data }; + let invalid_log = Log { + address: Address::repeat_byte(0x99), + data: LogData::new_unchecked([B256::ZERO, B256::ZERO].to_vec(), Bytes::default()), + }; + + let logs = vec![&valid_log, &invalid_log]; + let mut iter = parse_logs_to_executing_msgs(logs.into_iter()); + assert_eq!(iter.next().unwrap().unwrap(), event); + assert!(iter.next().unwrap().is_none()); + } +} diff --git a/kona/crates/protocol/interop/src/replacement.rs b/kona/crates/protocol/interop/src/replacement.rs new file mode 100644 index 0000000000000..905bfbb249599 --- /dev/null +++ b/kona/crates/protocol/interop/src/replacement.rs @@ -0,0 +1,24 @@ +//! Contains the block replacement type.Add commentMore actions + +use alloy_primitives::B256; +use derive_more::Display; +use kona_protocol::BlockInfo; + +/// Represents a [`BlockReplacement`] event where one block replaces another. +#[derive(Debug, Clone, Copy, Display, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[display("replacement: {replacement}, invalidated: {invalidated}")] +pub struct BlockReplacement { + /// The block that replaces the invalidated block + pub replacement: T, + /// Hash of the block being invalidated and replaced + pub invalidated: B256, +} + +impl BlockReplacement { + /// Creates a new [`BlockReplacement`]. + pub const fn new(replacement: T, invalidated: B256) -> Self { + Self { replacement, invalidated } + } +} diff --git a/kona/crates/protocol/interop/src/root.rs b/kona/crates/protocol/interop/src/root.rs new file mode 100644 index 0000000000000..6ea6217d5ab6f --- /dev/null +++ b/kona/crates/protocol/interop/src/root.rs @@ -0,0 +1,244 @@ +//! The [SuperRoot] type. +//! +//! Represents a snapshot of the state of the superchain at a given integer timestamp. + +use crate::{SUPER_ROOT_VERSION, SuperRootError, SuperRootResult}; +use alloc::vec::Vec; +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, Bytes, U256, keccak256}; +use alloy_rlp::{Buf, BufMut}; + +/// The [SuperRoot] is the snapshot of the superchain at a given timestamp. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SuperRoot { + /// The timestamp of the superchain snapshot, in seconds. + pub timestamp: u64, + /// The chain IDs and output root commitments of all chains within the dependency set. + pub output_roots: Vec, +} + +impl SuperRoot { + /// Create a new [SuperRoot] with the given timestamp and output roots. + pub fn new(timestamp: u64, mut output_roots: Vec) -> Self { + // Guarantee that the output roots are sorted by chain ID. + output_roots.sort_by_key(|r| r.chain_id); + Self { timestamp, output_roots } + } + + /// Decodes a [SuperRoot] from the given buffer. + pub fn decode(buf: &mut &[u8]) -> SuperRootResult { + if buf.is_empty() { + return Err(SuperRootError::UnexpectedLength); + } + + let version = buf[0]; + if version != SUPER_ROOT_VERSION { + return Err(SuperRootError::InvalidVersionByte); + } + buf.advance(1); + + if buf.len() < 8 { + return Err(SuperRootError::UnexpectedLength); + } + let timestamp = u64::from_be_bytes(buf[0..8].try_into()?); + buf.advance(8); + + let mut output_roots = Vec::new(); + while !buf.is_empty() { + if buf.len() < 64 { + return Err(SuperRootError::UnexpectedLength); + } + + let chain_id = U256::from_be_bytes::<32>(buf[0..32].try_into()?); + buf.advance(32); + let output_root = B256::from_slice(&buf[0..32]); + buf.advance(32); + output_roots.push(OutputRootWithChain::new(chain_id.to(), output_root)); + } + + Ok(Self { timestamp, output_roots }) + } + + /// Encode the [SuperRoot] into the given buffer. + pub fn encode(&self, out: &mut dyn BufMut) { + out.put_u8(SUPER_ROOT_VERSION); + + out.put_u64(self.timestamp); + for output_root in &self.output_roots { + out.put_slice(U256::from(output_root.chain_id).to_be_bytes::<32>().as_slice()); + out.put_slice(output_root.output_root.as_slice()); + } + } + + /// Returns the encoded length of the [SuperRoot]. + pub const fn encoded_length(&self) -> usize { + 1 + 8 + 64 * self.output_roots.len() + } + + /// Hashes the encoded [SuperRoot] using [keccak256]. + pub fn hash(&self) -> B256 { + let mut rlp_buf = Vec::with_capacity(self.encoded_length()); + self.encode(&mut rlp_buf); + keccak256(&rlp_buf) + } +} + +/// Chain Root Info +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ChainRootInfo { + /// The chain ID. + #[cfg_attr(feature = "serde", serde(rename = "chainID"))] + pub chain_id: u64, + /// The canonical output root of the latest canonical block at a particular timestamp. + pub canonical: B256, + /// The pending output root. + /// + /// This is the output root preimage for the latest block at a particular timestamp prior to + /// validation of executing messages. If the original block was valid, this will be the + /// preimage of the output root from the `canonical` array. If it was invalid, it will be + /// the output root preimage from the optimistic block deposited transaction added to the + /// deposit-only block. + pub pending: Bytes, +} + +/// The super root response type. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct SuperRootOutput { + /// The Highest L1 Block that is cross-safe among all chains. + pub cross_safe_derived_from: BlockNumHash, + /// The timestamp of the super root. + pub timestamp: u64, + /// The super root hash. + pub super_root: B256, + /// The version of the super root. + pub version: u8, + /// The chain root info for each chain in the dependency set. + /// It represents the state of the chain at or before the timestamp. + pub chains: Vec, +} + +/// A wrapper around an output root hash with the chain ID it belongs to. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OutputRootWithChain { + /// The chain ID of the output root. + pub chain_id: u64, + /// The output root hash. + pub output_root: B256, +} + +impl OutputRootWithChain { + /// Create a new [OutputRootWithChain] with the given chain ID and output root hash. + pub const fn new(chain_id: u64, output_root: B256) -> Self { + Self { chain_id, output_root } + } +} + +#[cfg(test)] +mod test { + use crate::{SUPER_ROOT_VERSION, errors::SuperRootError}; + + use super::{OutputRootWithChain, SuperRoot}; + use alloy_primitives::{B256, b256}; + + #[test] + fn test_super_root_sorts_outputs() { + let super_root = SuperRoot::new( + 10, + vec![ + (OutputRootWithChain::new(3, B256::default())), + (OutputRootWithChain::new(2, B256::default())), + (OutputRootWithChain::new(1, B256::default())), + ], + ); + + assert!(super_root.output_roots.windows(2).all(|w| w[0].chain_id <= w[1].chain_id)); + } + + #[test] + fn test_super_root_empty_buf() { + let buf: Vec = Vec::new(); + assert!(matches!( + SuperRoot::decode(&mut buf.as_slice()).unwrap_err(), + SuperRootError::UnexpectedLength + )); + } + + #[test] + fn test_super_root_invalid_version() { + let buf = vec![0xFF]; + assert!(matches!( + SuperRoot::decode(&mut buf.as_slice()).unwrap_err(), + SuperRootError::InvalidVersionByte + )); + } + + #[test] + fn test_super_root_invalid_length_at_timestamp() { + let buf = vec![SUPER_ROOT_VERSION, 0x00]; + assert!(matches!( + SuperRoot::decode(&mut buf.as_slice()).unwrap_err(), + SuperRootError::UnexpectedLength + )); + } + + #[test] + fn test_super_root_invalid_length_malformed_output_roots() { + let buf = [&[SUPER_ROOT_VERSION], 64u64.to_be_bytes().as_ref(), &[0xbe, 0xef]].concat(); + assert!(matches!( + SuperRoot::decode(&mut buf.as_slice()).unwrap_err(), + SuperRootError::UnexpectedLength + )); + } + + #[test] + fn test_static_hash_super_root() { + const EXPECTED: B256 = + b256!("0980033cbf4337f614a2401ab7efbfdc66ab647812f1c98d891d92ddfb376541"); + + let super_root = SuperRoot::new( + 10, + vec![ + (OutputRootWithChain::new(1, B256::default())), + (OutputRootWithChain::new(2, B256::default())), + ], + ); + assert_eq!(super_root.hash(), EXPECTED); + } + + #[test] + fn test_static_super_root_roundtrip() { + let super_root = SuperRoot::new( + 10, + vec![ + (OutputRootWithChain::new(1, B256::default())), + (OutputRootWithChain::new(2, B256::default())), + ], + ); + + let mut rlp_buf = Vec::with_capacity(super_root.encoded_length()); + super_root.encode(&mut rlp_buf); + assert_eq!(super_root, SuperRoot::decode(&mut rlp_buf.as_slice()).unwrap()); + } + + #[test] + fn test_arbitrary_super_root_roundtrip() { + use arbitrary::Arbitrary; + use rand::Rng; + + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + let super_root = SuperRoot::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + + let mut rlp_buf = Vec::with_capacity(super_root.encoded_length()); + super_root.encode(&mut rlp_buf); + assert_eq!(super_root, SuperRoot::decode(&mut rlp_buf.as_slice()).unwrap()); + } +} diff --git a/kona/crates/protocol/interop/src/safety.rs b/kona/crates/protocol/interop/src/safety.rs new file mode 100644 index 0000000000000..9fc617436339f --- /dev/null +++ b/kona/crates/protocol/interop/src/safety.rs @@ -0,0 +1,53 @@ +//! Message safety level for interoperability. +use alloc::string::String; +use thiserror::Error; + +/// Error when parsing SafetyLevel from string. +#[derive(Error, Debug)] +#[error("Invalid SafetyLevel, error: {0}")] +pub struct SafetyLevelParseError(pub String); + +#[cfg(test)] +mod tests { + use core::str::FromStr; + use op_alloy_consensus::interop::SafetyLevel; + + #[test] + #[cfg(feature = "serde")] + fn test_safety_level_serde() { + let level = SafetyLevel::Finalized; + let json = serde_json::to_string(&level).unwrap(); + assert_eq!(json, r#""finalized""#); + + let level: SafetyLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(level, SafetyLevel::Finalized); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_safety_level_fails() { + let json = r#""failed""#; + let level: Result = serde_json::from_str(json); + assert!(level.is_err()); + } + + #[test] + fn test_safety_level_from_str_valid() { + assert_eq!(SafetyLevel::from_str("finalized").unwrap(), SafetyLevel::Finalized); + assert_eq!(SafetyLevel::from_str("safe").unwrap(), SafetyLevel::CrossSafe); + assert_eq!(SafetyLevel::from_str("local-safe").unwrap(), SafetyLevel::LocalSafe); + assert_eq!(SafetyLevel::from_str("localsafe").unwrap(), SafetyLevel::LocalSafe); + assert_eq!(SafetyLevel::from_str("cross-unsafe").unwrap(), SafetyLevel::CrossUnsafe); + assert_eq!(SafetyLevel::from_str("crossunsafe").unwrap(), SafetyLevel::CrossUnsafe); + assert_eq!(SafetyLevel::from_str("unsafe").unwrap(), SafetyLevel::LocalUnsafe); + assert_eq!(SafetyLevel::from_str("invalid").unwrap(), SafetyLevel::Invalid); + } + + #[test] + fn test_safety_level_from_str_invalid() { + assert!(SafetyLevel::from_str("unknown").is_err()); + assert!(SafetyLevel::from_str("123").is_err()); + assert!(SafetyLevel::from_str("").is_err()); + assert!(SafetyLevel::from_str("safe ").is_err()); + } +} diff --git a/kona/crates/protocol/interop/src/test_util.rs b/kona/crates/protocol/interop/src/test_util.rs new file mode 100644 index 0000000000000..4120d1b8b92d4 --- /dev/null +++ b/kona/crates/protocol/interop/src/test_util.rs @@ -0,0 +1,231 @@ +//! Test utilities for `kona-interop`. + +#![allow(missing_docs, unreachable_pub, unused)] + +use crate::{ExecutingMessage, MessageIdentifier, traits::InteropProvider}; +use alloy_consensus::{Header, Receipt, ReceiptWithBloom, Sealed}; +use alloy_primitives::{Address, B256, Bytes, Log, LogData, U256, map::HashMap}; +use alloy_sol_types::{SolEvent, SolValue}; +use async_trait::async_trait; +use kona_genesis::RollupConfig; +use kona_protocol::Predeploys; +use op_alloy_consensus::OpReceiptEnvelope; + +#[derive(Debug, Clone, Default)] +pub struct MockInteropProvider { + pub headers: HashMap>>, + pub receipts: HashMap>>, +} + +impl MockInteropProvider { + pub const fn new( + headers: HashMap>>, + receipts: HashMap>>, + ) -> Self { + Self { headers, receipts } + } +} + +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +#[error("Mock interop provider error")] +pub struct InteropProviderError; + +#[async_trait] +impl InteropProvider for MockInteropProvider { + type Error = InteropProviderError; + + async fn header_by_number(&self, chain_id: u64, number: u64) -> Result { + Ok(self + .headers + .get(&chain_id) + .and_then(|headers| headers.get(&number)) + .unwrap() + .inner() + .clone()) + } + + async fn receipts_by_number( + &self, + chain_id: u64, + number: u64, + ) -> Result, Self::Error> { + Ok(self.receipts.get(&chain_id).and_then(|receipts| receipts.get(&number)).unwrap().clone()) + } + + async fn receipts_by_hash( + &self, + chain_id: u64, + block_hash: B256, + ) -> Result, Self::Error> { + Ok(self + .receipts + .get(&chain_id) + .and_then(|receipts| { + let headers = self.headers.get(&chain_id).unwrap(); + let number = + headers.values().find(|header| header.hash() == block_hash).unwrap().number; + receipts.get(&number) + }) + .unwrap() + .clone()) + } +} + +#[derive(Default, Debug)] +pub struct SuperchainBuilder { + chains: HashMap, +} + +impl SuperchainBuilder { + pub fn new() -> Self { + Self { chains: HashMap::default() } + } + + pub fn chain(&mut self, chain_id: u64) -> &mut ChainBuilder { + self.chains.entry(chain_id).or_default() + } + + /// Builds the scenario into the format needed for testing + pub fn build( + self, + ) -> (HashMap>, HashMap, MockInteropProvider) { + let mut headers_map = HashMap::default(); + let mut receipts_map = HashMap::default(); + let mut sealed_headers = HashMap::default(); + let mut rollup_cfgs = HashMap::default(); + + for (chain_id, chain) in self.chains { + let header = chain.header; + let header_hash = header.hash_slow(); + let sealed_header = header.seal(header_hash); + + let mut chain_headers = HashMap::default(); + chain_headers.insert(sealed_header.number, sealed_header.clone()); + headers_map.insert(chain_id, chain_headers); + + let mut chain_receipts = HashMap::default(); + chain_receipts.insert(sealed_header.number, chain.receipts); + receipts_map.insert(chain_id, chain_receipts); + + sealed_headers.insert(chain_id, sealed_header); + rollup_cfgs.insert(chain_id, chain.rollup_config); + } + + (sealed_headers, rollup_cfgs, MockInteropProvider::new(headers_map, receipts_map)) + } +} + +#[derive(Default, Debug)] +pub struct ChainBuilder { + pub rollup_config: RollupConfig, + pub header: Header, + pub receipts: Vec, +} + +impl ChainBuilder { + pub fn modify_rollup_cfg(&mut self, f: impl FnOnce(&mut RollupConfig)) -> &mut Self { + f(&mut self.rollup_config); + self + } + + pub fn with_block_time(&mut self, block_time: u64) -> &mut Self { + self.modify_rollup_cfg(|cfg| cfg.block_time = block_time) + } + + pub fn with_interop_activation_time(&mut self, activation: u64) -> &mut Self { + self.modify_rollup_cfg(|cfg| cfg.hardforks.interop_time = Some(activation)) + } + + pub fn modify_header(&mut self, f: impl FnOnce(&mut Header)) -> &mut Self { + f(&mut self.header); + self + } + + pub fn with_timestamp(&mut self, timestamp: u64) -> &mut Self { + self.modify_header(|h| h.timestamp = timestamp) + } + + pub fn add_initiating_message(&mut self, message_data: Bytes) -> &mut Self { + let receipt = OpReceiptEnvelope::Eip1559(ReceiptWithBloom { + receipt: Receipt { + logs: vec![Log { + address: Address::ZERO, + data: LogData::new(vec![], message_data).unwrap(), + }], + ..Default::default() + }, + ..Default::default() + }); + self.receipts.push(receipt); + self + } + + pub fn add_executing_message(&mut self, builder: ExecutingMessageBuilder) -> &mut Self { + let receipt = OpReceiptEnvelope::Eip1559(ReceiptWithBloom { + receipt: Receipt { + logs: vec![Log { + address: Predeploys::CROSS_L2_INBOX, + data: LogData::new( + vec![ExecutingMessage::SIGNATURE_HASH, builder.message_hash], + MessageIdentifier { + origin: builder.origin_address, + blockNumber: U256::from(builder.origin_block_number), + logIndex: U256::from(builder.origin_log_index), + timestamp: U256::from(builder.origin_timestamp), + chainId: U256::from(builder.origin_chain_id), + } + .abi_encode() + .into(), + ) + .unwrap(), + }], + ..Default::default() + }, + ..Default::default() + }); + self.receipts.push(receipt); + self + } +} + +#[derive(Default, Debug)] +pub struct ExecutingMessageBuilder { + pub message_hash: B256, + pub origin_address: Address, + pub origin_log_index: u64, + pub origin_chain_id: u64, + pub origin_block_number: u64, + pub origin_timestamp: u64, +} + +impl ExecutingMessageBuilder { + pub const fn with_message_hash(mut self, message_hash: B256) -> Self { + self.message_hash = message_hash; + self + } + + pub const fn with_origin_address(mut self, origin_address: Address) -> Self { + self.origin_address = origin_address; + self + } + + pub const fn with_origin_log_index(mut self, origin_log_index: u64) -> Self { + self.origin_log_index = origin_log_index; + self + } + + pub const fn with_origin_chain_id(mut self, origin_chain_id: u64) -> Self { + self.origin_chain_id = origin_chain_id; + self + } + + pub const fn with_origin_block_number(mut self, origin_block_number: u64) -> Self { + self.origin_block_number = origin_block_number; + self + } + + pub const fn with_origin_timestamp(mut self, origin_timestamp: u64) -> Self { + self.origin_timestamp = origin_timestamp; + self + } +} diff --git a/kona/crates/protocol/interop/src/traits.rs b/kona/crates/protocol/interop/src/traits.rs new file mode 100644 index 0000000000000..c463fca971034 --- /dev/null +++ b/kona/crates/protocol/interop/src/traits.rs @@ -0,0 +1,75 @@ +//! Traits for the `kona-interop` crate. + +use crate::InteropValidationError; +use alloc::{boxed::Box, vec::Vec}; +use alloy_consensus::Header; +use alloy_primitives::{B256, ChainId}; +use async_trait::async_trait; +use core::error::Error; +use kona_protocol::BlockInfo; +use op_alloy_consensus::OpReceiptEnvelope; + +/// Describes the interface of the interop data provider. This provider is multiplexed over several +/// chains, with each method consuming a chain ID to determine the target chain. +#[async_trait] +pub trait InteropProvider { + /// The error type for the provider. + type Error: Error; + + /// Fetch a [Header] by its number. + async fn header_by_number(&self, chain_id: u64, number: u64) -> Result; + + /// Fetch all receipts for a given block by number. + async fn receipts_by_number( + &self, + chain_id: u64, + number: u64, + ) -> Result, Self::Error>; + + /// Fetch all receipts for a given block by hash. + async fn receipts_by_hash( + &self, + chain_id: u64, + block_hash: B256, + ) -> Result, Self::Error>; +} + +/// Trait for validating interop-related timestamps and blocks. +pub trait InteropValidator: Send + Sync { + /// Validates that the provided timestamps and chain IDs are eligible for interop execution. + /// + /// # Arguments + /// * `initiating_chain_id` - The chain ID where the message was initiated + /// * `initiating_timestamp` - The timestamp when the message was initiated + /// * `executing_chain_id` - The chain ID where the message is being executed + /// * `executing_timestamp` - The timestamp when the message is being executed + /// * `timeout` - Optional timeout value to add to the execution deadline + /// + /// # Returns + /// * `Ok(())` if the timestamps are valid for interop execution + /// * `Err(InteropValidationError)` if validation fails + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + /// Returns `true` if the timestamp is strictly after the interop activation block. + /// + /// This function checks whether the provided timestamp is *after* that activation, + /// skipping the activation block itself. + /// + /// Returns `false` if `interop_time` is not configured. + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + /// Returns `true` if the block is the interop activation block for the specified chain. + /// + /// An interop activation block is defined as the block that is right after the + /// interop activation time. + /// + /// Returns `false` if `interop_time` is not configured. + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; +} diff --git a/kona/crates/protocol/protocol/Cargo.toml b/kona/crates/protocol/protocol/Cargo.toml new file mode 100644 index 0000000000000..997bf0f320312 --- /dev/null +++ b/kona/crates/protocol/protocol/Cargo.toml @@ -0,0 +1,129 @@ +[package] +name = "kona-protocol" +version = "0.4.5" +description = "Optimism protocol-specific types" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-genesis.workspace = true + +# OP Alloy +op-alloy-consensus.workspace = true +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true + +# Alloy +alloy-primitives = { workspace = true, features = ["map"] } +alloy-rlp.workspace = true +alloy-hardforks.workspace = true +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-rpc-types-engine.workspace = true + +# Misc +tracing.workspace = true +thiserror.workspace = true +async-trait.workspace = true +unsigned-varint.workspace = true +derive_more = { workspace = true, features = ["display"] } + +# Compression +brotli.workspace = true +miniz_oxide.workspace = true +alloc-no-stdlib.workspace = true + +# `arbitrary` feature +arbitrary = { workspace = true, features = ["derive"], optional = true } + +# `serde` feature +serde = { workspace = true, optional = true } +alloy-serde = { workspace = true, optional = true } + +# `test-utils` feature +spin = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["fmt"], optional = true } + +[dev-dependencies] +brotli = { workspace = true, features = ["std"] } +spin.workspace = true +rand = { workspace = true, features = ["std", "std_rng"] } +rstest.workspace = true +proptest.workspace = true +serde_json.workspace = true +alloy-sol-types.workspace = true +tokio = { workspace = true, features = ["full"] } +arbitrary = { workspace = true, features = ["derive"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } +alloy-primitives = { workspace = true, features = ["arbitrary"] } +op-alloy-consensus.workspace = true +alloy-rpc-types-eth.workspace = true +op-alloy-rpc-types.workspace = true + +kona-registry.workspace = true + +[features] +default = [] +std = [ + "alloy-consensus/std", + "alloy-eips/std", + "alloy-primitives/std", + "alloy-rlp/std", + "alloy-rpc-types-engine/std", + "alloy-rpc-types-eth/std", + "alloy-serde?/std", + "brotli/std", + "derive_more/std", + "kona-genesis/std", + "miniz_oxide/std", + "op-alloy-consensus/std", + "op-alloy-rpc-types-engine/std", + "op-alloy-rpc-types/std", + "serde?/std", + "spin?/std", + "thiserror/std", + "tracing/std", + "unsigned-varint/std", +] +test-utils = [ "dep:spin", "dep:tracing-subscriber" ] +arbitrary = [ + "alloy-consensus/arbitrary", + "alloy-eips/arbitrary", + "alloy-primitives/arbitrary", + "alloy-primitives/rand", + "alloy-rpc-types-engine/arbitrary", + "alloy-rpc-types-eth/arbitrary", + "alloy-serde?/arbitrary", + "dep:arbitrary", + "kona-genesis/arbitrary", + "op-alloy-consensus/arbitrary", + "op-alloy-rpc-types-engine/arbitrary", + "op-alloy-rpc-types/arbitrary", + "std", +] +serde = [ + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-hardforks/serde", + "alloy-primitives/serde", + "alloy-rpc-types-engine/serde", + "alloy-rpc-types-eth/serde", + "dep:alloy-serde", + "dep:serde", + "kona-genesis/serde", + "op-alloy-consensus/serde", + "op-alloy-rpc-types-engine/serde", + "op-alloy-rpc-types/serde", + "tracing-subscriber?/serde", +] diff --git a/kona/crates/protocol/protocol/README.md b/kona/crates/protocol/protocol/README.md new file mode 100644 index 0000000000000..799f7e0829a99 --- /dev/null +++ b/kona/crates/protocol/protocol/README.md @@ -0,0 +1,11 @@ +## `kona-protocol` + +CI +kona-protocol crate +MIT License +Docs + + +Core protocol types for Optimism. + +These include types, constants, and methods for derivation as well as batch-submission. diff --git a/kona/crates/protocol/protocol/examples/frames_to_batch.rs b/kona/crates/protocol/protocol/examples/frames_to_batch.rs new file mode 100644 index 0000000000000..0b7535f9d3c90 --- /dev/null +++ b/kona/crates/protocol/protocol/examples/frames_to_batch.rs @@ -0,0 +1,104 @@ +//! This example decodes raw [Frame]s and reads them into a [Channel] and into a [SingleBatch]. + +use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope}; +use alloy_eips::eip2718::{Decodable2718, Encodable2718}; +use alloy_primitives::{Address, BlockHash, Bytes, Signature, U256, hex}; +use kona_genesis::RollupConfig; +use kona_protocol::{Batch, BlockInfo, Channel, Frame, SingleBatch, decompress_brotli}; + +fn main() { + // Raw frame data taken from the `encode_channel` example. + let first_frame = hex!( + "60d54f49b71978b1b09288af847b11d200000000004d1b1301f82f0f6c3734f4821cd090ef3979d71a98e7e483b1dccdd525024c0ef16f425c7b4976a7acc0c94a0514b72c096d4dcc52f0b22dae193c70c86d0790a304a08152c8250031d091063ea000" + ); + let second_frame = hex!( + "60d54f49b71978b1b09288af847b11d2000100000046b00d00005082edde7ccf05bded2004462b5e80e1c42cd08e307f5baac723b22864cc6cd01ddde84efc7c018d7ada56c2fa8e3c5bedd494c3a7a884439d5771afcecaf196cb3801" + ); + + // Decode the raw frames. + let decoded_first = Frame::decode(&first_frame).expect("decodes frame").1; + let decoded_second = Frame::decode(&second_frame).expect("decodes frame").1; + + // Create a channel. + let id = decoded_first.id; + let open_block = BlockInfo::default(); + let mut channel = Channel::new(id, open_block); + + // Add the frames to the channel. + let l1_inclusion_block = BlockInfo::default(); + channel.add_frame(decoded_first, l1_inclusion_block).expect("adds frame"); + channel.add_frame(decoded_second, l1_inclusion_block).expect("adds frame"); + + // Get the frame data from the channel. + let frame_data = channel.frame_data().expect("some frame data"); + println!("Frame data: {}", hex::encode(&frame_data)); + + // Decompress the frame data with brotli. + let config = RollupConfig::default(); + let max = config.max_rlp_bytes_per_channel(open_block.timestamp) as usize; + let decompressed = decompress_brotli(&frame_data, max).expect("decompresses brotli"); + println!("Decompressed frame data: {}", hex::encode(&decompressed)); + + // Decode the single batch from the decompressed data. + let batch = Batch::decode(&mut decompressed.as_slice(), &config).expect("batch decodes"); + assert_eq!( + batch, + Batch::Single(SingleBatch { + parent_hash: BlockHash::ZERO, + epoch_num: 1, + epoch_hash: BlockHash::ZERO, + timestamp: 1, + transactions: example_transactions(), + }) + ); + + println!("Successfully decoded frames into a Batch"); +} + +fn example_transactions() -> Vec { + let mut transactions = Vec::new(); + + // First Transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[6]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + // Second transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[7]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + transactions +} diff --git a/kona/crates/protocol/protocol/src/attributes.rs b/kona/crates/protocol/protocol/src/attributes.rs new file mode 100644 index 0000000000000..165d6c0221e13 --- /dev/null +++ b/kona/crates/protocol/protocol/src/attributes.rs @@ -0,0 +1,228 @@ +//! Optimism Payload attributes that reference the parent L2 block. + +use crate::{BlockInfo, L2BlockInfo}; +use op_alloy_consensus::OpTxType; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// Optimism Payload Attributes with parent block reference and the L1 origin block. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OpAttributesWithParent { + /// The payload attributes. + pub attributes: OpPayloadAttributes, + /// The parent block reference. + pub parent: L2BlockInfo, + /// The L1 block that the attributes were derived from. + pub derived_from: Option, + /// Whether the current batch is the last in its span. + pub is_last_in_span: bool, +} + +impl OpAttributesWithParent { + /// Create a new [`OpAttributesWithParent`] instance. + pub const fn new( + attributes: OpPayloadAttributes, + parent: L2BlockInfo, + derived_from: Option, + is_last_in_span: bool, + ) -> Self { + Self { attributes, parent, derived_from, is_last_in_span } + } + + /// Returns the L2 block number for the payload attributes if made canonical. + /// Derived as the parent block height plus one. + pub const fn block_number(&self) -> u64 { + self.parent.block_info.number.saturating_add(1) + } + + /// Consumes `self` and returns the inner [`OpPayloadAttributes`]. + pub fn take_inner(self) -> OpPayloadAttributes { + self.attributes + } + + /// Returns the payload attributes. + pub const fn attributes(&self) -> &OpPayloadAttributes { + &self.attributes + } + + /// Returns the parent block reference. + pub const fn parent(&self) -> &L2BlockInfo { + &self.parent + } + + /// Returns the L1 origin block reference. + pub const fn derived_from(&self) -> Option<&BlockInfo> { + self.derived_from.as_ref() + } + + /// Returns whether the current batch is the last in its span. + pub const fn is_last_in_span(&self) -> bool { + self.is_last_in_span + } + + /// Returns `true` if all transactions in the payload are deposits. + pub fn is_deposits_only(&self) -> bool { + self.attributes + .transactions + .iter() + .all(|tx| tx.first().is_some_and(|tx| tx[0] == OpTxType::Deposit as u8)) + } + + /// Converts the [`OpAttributesWithParent`] into a deposits-only payload. + pub fn as_deposits_only(&self) -> Self { + let mut attributes = self.attributes.clone(); + + attributes + .transactions + .iter_mut() + .for_each(|txs| txs.retain(|tx| tx.first().cloned() == Some(OpTxType::Deposit as u8))); + + Self { + attributes, + parent: self.parent, + derived_from: self.derived_from, + is_last_in_span: self.is_last_in_span, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_op_attributes_with_parent() { + let attributes = OpPayloadAttributes::default(); + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes.clone(), parent, None, is_last_in_span); + + assert_eq!(op_attributes_with_parent.attributes(), &attributes); + assert_eq!(op_attributes_with_parent.parent(), &parent); + assert_eq!(op_attributes_with_parent.is_last_in_span(), is_last_in_span); + assert_eq!(op_attributes_with_parent.derived_from(), None); + } + + /// Test that the [`OpAttributesWithParent::as_deposits_only`] method strips out all + /// transactions that are not deposits. + #[test] + fn test_op_attributes_with_parent_as_deposits_only() { + let attributes = OpPayloadAttributes { + transactions: Some(vec![ + vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into(), + vec![OpTxType::Legacy as u8, 0x0, 0x11, 0x21].into(), + vec![OpTxType::Eip2930 as u8, 0x0, 0x12, 0x22].into(), + vec![OpTxType::Eip1559 as u8, 0x0, 0x13, 0x23].into(), + vec![OpTxType::Eip7702 as u8, 0x0, 0x14, 0x24].into(), + vec![].into(), + ]), + ..OpPayloadAttributes::default() + }; + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes, parent, None, is_last_in_span); + let deposits_only_attributes = op_attributes_with_parent.as_deposits_only(); + + assert_eq!( + deposits_only_attributes.attributes().transactions, + Some(vec![vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into()]) + ); + } + + #[test] + fn test_op_attributes_with_parent_as_deposits_multi_deposits() { + let attributes = OpPayloadAttributes { + transactions: Some(vec![ + vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into(), + vec![OpTxType::Legacy as u8, 0x0, 0x11, 0x21].into(), + vec![OpTxType::Eip2930 as u8, 0x0, 0x12, 0x22].into(), + vec![OpTxType::Deposit as u8, 0x98, 0x21, 0x31].into(), + vec![OpTxType::Eip1559 as u8, 0x0, 0x13, 0x23].into(), + vec![OpTxType::Eip7702 as u8, 0x0, 0x14, 0x24].into(), + vec![OpTxType::Deposit as u8, 0x56, 0x31, 0x41].into(), + vec![].into(), + ]), + ..OpPayloadAttributes::default() + }; + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes, parent, None, is_last_in_span); + let deposits_only_attributes = op_attributes_with_parent.as_deposits_only(); + + assert_eq!( + deposits_only_attributes.attributes().transactions, + Some(vec![ + vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into(), + vec![OpTxType::Deposit as u8, 0x98, 0x21, 0x31].into(), + vec![OpTxType::Deposit as u8, 0x56, 0x31, 0x41].into(), + ]) + ); + } + + /// Test that the [`OpAttributesWithParent::as_deposits_only`] method strips out all + /// transactions that are not deposits. + #[test] + fn test_op_attributes_with_parent_as_deposits_no_deposits() { + let attributes = OpPayloadAttributes { + transactions: Some(vec![ + vec![OpTxType::Legacy as u8, 0x0, 0x11, 0x21].into(), + vec![OpTxType::Eip2930 as u8, 0x0, 0x12, 0x22].into(), + vec![OpTxType::Eip1559 as u8, 0x0, 0x13, 0x23].into(), + vec![OpTxType::Eip7702 as u8, 0x0, 0x14, 0x24].into(), + vec![].into(), + ]), + ..OpPayloadAttributes::default() + }; + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes, parent, None, is_last_in_span); + let deposits_only_attributes = op_attributes_with_parent.as_deposits_only(); + + assert_eq!(deposits_only_attributes.attributes().transactions, Some(vec![])); + } + + #[test] + fn test_op_attributes_with_parent_as_deposits_only_deposits() { + let attributes = OpPayloadAttributes { + transactions: Some(vec![ + vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into(), + vec![OpTxType::Deposit as u8, 0x98, 0x21, 0x31].into(), + vec![OpTxType::Deposit as u8, 0x56, 0x31, 0x41].into(), + vec![].into(), + ]), + ..OpPayloadAttributes::default() + }; + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes, parent, None, is_last_in_span); + let deposits_only_attributes = op_attributes_with_parent.as_deposits_only(); + + assert_eq!( + deposits_only_attributes.attributes().transactions, + Some(vec![ + vec![OpTxType::Deposit as u8, 0x0, 0x10, 0x20].into(), + vec![OpTxType::Deposit as u8, 0x98, 0x21, 0x31].into(), + vec![OpTxType::Deposit as u8, 0x56, 0x31, 0x41].into(), + ]) + ); + } + + #[test] + fn test_op_attributes_with_parent_as_deposits_no_txs() { + let attributes = + OpPayloadAttributes { transactions: None, ..OpPayloadAttributes::default() }; + let parent = L2BlockInfo::default(); + let is_last_in_span = true; + let op_attributes_with_parent = + OpAttributesWithParent::new(attributes, parent, None, is_last_in_span); + let deposits_only_attributes = op_attributes_with_parent.as_deposits_only(); + + assert_eq!(deposits_only_attributes.attributes().transactions, None); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/bits.rs b/kona/crates/protocol/protocol/src/batch/bits.rs new file mode 100644 index 0000000000000..b25158fb47fd8 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/bits.rs @@ -0,0 +1,234 @@ +//! Module for working with span batch bits. + +use crate::SpanBatchError; +use alloc::{vec, vec::Vec}; +use alloy_primitives::bytes; +use alloy_rlp::Buf; +use core::cmp::Ordering; + +/// Type for span batch bits. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchBits(pub Vec); + +impl AsRef<[u8]> for SpanBatchBits { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl SpanBatchBits { + /// Creates a new span batch bits. + pub const fn new(inner: Vec) -> Self { + Self(inner) + } + + /// Decodes a standard span-batch bitlist from a reader. + /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 + /// bits. The encoded bitlist cannot be longer than `bit_length`. + pub fn decode(b: &mut &[u8], bit_length: usize) -> Result { + let buffer_len = bit_length / 8 + if !bit_length.is_multiple_of(8) { 1 } else { 0 }; + let bits = if b.len() < buffer_len { + let mut bits = vec![0; buffer_len]; + bits[..b.len()].copy_from_slice(b); + b.advance(b.len()); + bits + } else { + let v = b[..buffer_len].to_vec(); + b.advance(buffer_len); + v + }; + let sb_bits = Self(bits); + + if sb_bits.bit_len() > bit_length { + return Err(SpanBatchError::BitfieldTooLong); + } + + Ok(sb_bits) + } + + /// Encodes a standard span-batch bitlist. + /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 + /// bits. The encoded bitlist cannot be longer than `bit_length` + pub fn encode( + w: &mut dyn bytes::BufMut, + bit_length: usize, + bits: &Self, + ) -> Result<(), SpanBatchError> { + if bits.bit_len() > bit_length { + return Err(SpanBatchError::BitfieldTooLong); + } + + // Round up, ensure enough bytes when number of bits is not a multiple of 8. + // Alternative of (L+7)/8 is not overflow-safe. + let buf_len = bit_length / 8 + if !bit_length.is_multiple_of(8) { 1 } else { 0 }; + let mut buf = vec![0; buf_len]; + buf[buf_len - bits.0.len()..].copy_from_slice(bits.as_ref()); + w.put_slice(&buf); + Ok(()) + } + + /// Get a bit from the [`SpanBatchBits`] bitlist. + pub fn get_bit(&self, index: usize) -> Option { + let byte_index = index / 8; + let bit_index = index % 8; + + // Check if the byte index is within the bounds of the bitlist + if byte_index < self.0.len() { + // Retrieve the specific byte that contains the bit we're interested in + let byte = self.0[self.0.len() - byte_index - 1]; + + // Shift the bits of the byte to the right, based on the bit index, and + // mask it with 1 to isolate the bit we're interested in. + // If the result is not zero, the bit is set to 1, otherwise it's 0. + Some(if byte & (1 << bit_index) != 0 { 1 } else { 0 }) + } else { + // Return None if the index is out of bounds + None + } + } + + /// Sets a bit in the [`SpanBatchBits`] bitlist. + pub fn set_bit(&mut self, index: usize, value: bool) { + let byte_index = index / 8; + let bit_index = index % 8; + + // Ensure the vector is large enough to contain the bit at 'index'. + // If not, resize the vector, filling with 0s. + if byte_index >= self.0.len() { + Self::resize_from_right(&mut self.0, byte_index + 1); + } + + // Retrieve the specific byte to modify + let len = self.0.len(); + let byte = &mut self.0[len - byte_index - 1]; + + if value { + // Set the bit to 1 + *byte |= 1 << bit_index; + } else { + // Set the bit to 0 + *byte &= !(1 << bit_index); + } + } + + /// Calculates the bit length of the [`SpanBatchBits`] bitfield. + pub fn bit_len(&self) -> usize { + // Iterate over the bytes from left to right to find the first non-zero byte + for (i, &byte) in self.0.iter().enumerate() { + if byte != 0 { + // Calculate the index of the most significant bit in the byte + let msb_index = 7 - byte.leading_zeros() as usize; // 0-based index + + // Calculate the total bit length + let total_bit_length = msb_index + 1 + ((self.0.len() - i - 1) * 8); + return total_bit_length; + } + } + + // If all bytes are zero, the bitlist is considered to have a length of 0 + 0 + } + + /// Resizes an array from the right. Useful for big-endian zero extension. + fn resize_from_right(vec: &mut Vec, new_size: usize) { + let current_size = vec.len(); + match new_size.cmp(¤t_size) { + Ordering::Less => { + // Remove elements from the beginning. + let remove_count = current_size - new_size; + vec.drain(0..remove_count); + } + Ordering::Greater => { + // Calculate how many new elements to add. + let additional = new_size - current_size; + // Prepend new elements with default values. + let mut prepend_elements = vec![T::default(); additional]; + prepend_elements.append(vec); + *vec = prepend_elements; + } + Ordering::Equal => { /* If new_size == current_size, do nothing. */ } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::{collection::vec, prelude::any, proptest}; + + proptest! { + #[test] + fn test_encode_decode_roundtrip_span_bitlist(vec in vec(any::(), 0..5096)) { + let bits = SpanBatchBits(vec); + assert_eq!(SpanBatchBits::decode(&mut bits.as_ref(), bits.0.len() * 8).unwrap(), bits); + let mut encoded = Vec::new(); + SpanBatchBits::encode(&mut encoded, bits.0.len() * 8, &bits).unwrap(); + assert_eq!(encoded, bits.0); + } + + #[test] + fn test_span_bitlist_bitlen(index in 0usize..65536) { + let mut bits = SpanBatchBits::default(); + bits.set_bit(index, true); + assert_eq!(bits.0.len(), (index / 8) + 1); + assert_eq!(bits.bit_len(), index + 1); + } + + #[test] + fn test_span_bitlist_bitlen_shrink(first_index in 8usize..65536) { + let second_index = first_index.clamp(0, first_index - 8); + let mut bits = SpanBatchBits::default(); + + // Set and clear first index. + bits.set_bit(first_index, true); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), first_index + 1); + bits.set_bit(first_index, false); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), 0); + + // Set second bit. Even though the array is larger, as it was originally allocated with more words, + // the bitlength should still be lowered as the higher-order words are 0'd out. + bits.set_bit(second_index, true); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), second_index + 1); + } + } + + #[test] + fn bitlist_big_endian_zero_extended() { + let mut bits = SpanBatchBits::default(); + + bits.set_bit(1, true); + bits.set_bit(6, true); + bits.set_bit(8, true); + bits.set_bit(15, true); + assert_eq!(bits.0[0], 0b1000_0001); + assert_eq!(bits.0[1], 0b0100_0010); + assert_eq!(bits.0.len(), 2); + assert_eq!(bits.bit_len(), 16); + } + + #[test] + fn test_static_set_get_bits_span_bitlist() { + let mut bits = SpanBatchBits::default(); + assert!(bits.0.is_empty()); + + bits.set_bit(0, true); + bits.set_bit(1, true); + bits.set_bit(2, true); + bits.set_bit(4, true); + bits.set_bit(7, true); + assert_eq!(bits.0.len(), 1); + assert_eq!(bits.get_bit(0), Some(1)); + assert_eq!(bits.get_bit(1), Some(1)); + assert_eq!(bits.get_bit(2), Some(1)); + assert_eq!(bits.get_bit(3), Some(0)); + assert_eq!(bits.get_bit(4), Some(1)); + + bits.set_bit(17, true); + assert_eq!(bits.get_bit(17), Some(1)); + assert_eq!(bits.get_bit(32), None); + assert_eq!(bits.0.len(), 3); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/core.rs b/kona/crates/protocol/protocol/src/batch/core.rs new file mode 100644 index 0000000000000..7ac31bb08ac78 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/core.rs @@ -0,0 +1,142 @@ +//! Module containing the core [`Batch`] enum. + +use crate::{ + BatchDecodingError, BatchEncodingError, BatchType, RawSpanBatch, SingleBatch, SpanBatch, +}; +use alloy_primitives::bytes; +use alloy_rlp::{Buf, Decodable, Encodable}; +use kona_genesis::RollupConfig; + +/// A Batch. +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Batch { + /// A single batch + Single(SingleBatch), + /// Span Batches + Span(SpanBatch), +} + +impl core::fmt::Display for Batch { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Single(_) => write!(f, "single"), + Self::Span(_) => write!(f, "span"), + } + } +} + +impl Batch { + /// Returns the timestamp for the batch. + pub fn timestamp(&self) -> u64 { + match self { + Self::Single(sb) => sb.timestamp, + Self::Span(sb) => sb.starting_timestamp(), + } + } + + /// Attempts to decode a batch from a reader. + pub fn decode(r: &mut &[u8], cfg: &RollupConfig) -> Result { + if r.is_empty() { + return Err(BatchDecodingError::EmptyBuffer); + } + + // Read the batch type + let batch_type = BatchType::from(r[0]); + r.advance(1); + + match batch_type { + BatchType::Single => { + let single_batch = + SingleBatch::decode(r).map_err(BatchDecodingError::AlloyRlpError)?; + Ok(Self::Single(single_batch)) + } + BatchType::Span => { + let mut raw_span_batch = RawSpanBatch::decode(r)?; + let span_batch = raw_span_batch + .derive(cfg.block_time, cfg.genesis.l2_time, cfg.l2_chain_id.id()) + .map_err(BatchDecodingError::SpanBatchError)?; + Ok(Self::Span(span_batch)) + } + } + } + + /// Attempts to encode the batch to a writer. + pub fn encode(&self, out: &mut dyn bytes::BufMut) -> Result<(), BatchEncodingError> { + match self { + Self::Single(sb) => { + out.put_u8(BatchType::Single as u8); + sb.encode(out); + } + Self::Span(sb) => { + out.put_u8(BatchType::Span as u8); + let raw_span_batch = + sb.to_raw_span_batch().map_err(BatchEncodingError::SpanBatchError)?; + raw_span_batch.encode(out).map_err(BatchEncodingError::SpanBatchError)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{SpanBatchElement, SpanBatchError, SpanBatchTransactions}; + use alloc::{vec, vec::Vec}; + use alloy_consensus::{Signed, TxEip2930, TxEnvelope}; + use alloy_primitives::{Bytes, Signature, TxKind, address, hex}; + + #[test] + fn test_single_batch_encode_decode() { + let mut out = Vec::new(); + let batch = Batch::Single(SingleBatch::default()); + batch.encode(&mut out).unwrap(); + let decoded = Batch::decode(&mut out.as_slice(), &RollupConfig::default()).unwrap(); + assert_eq!(batch, decoded); + } + + #[test] + fn test_span_batch_encode_decode() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + span_batch_txs.add_txs(txs, chain_id).unwrap(); + + let mut out = Vec::new(); + let batch = Batch::Span(SpanBatch { + block_tx_counts: vec![1], + batches: vec![SpanBatchElement::default()], + txs: span_batch_txs, + ..Default::default() + }); + batch.encode(&mut out).unwrap(); + let decoded = Batch::decode(&mut out.as_slice(), &RollupConfig::default()).unwrap(); + assert_eq!(Batch::Span(SpanBatch { + batches: vec![SpanBatchElement { + transactions: vec![hex!("01f85f808080809401234567890123456789012345678901234567898080c080a0840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565a025e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1").into()], + ..Default::default() + }], + txs: SpanBatchTransactions::default(), + ..Default::default() + }), decoded); + } + + #[test] + fn test_empty_span_batch() { + let mut out = Vec::new(); + let batch = Batch::Span(SpanBatch::default()); + // Fails to even encode an empty span batch - decoding will do the same + let err = batch.encode(&mut out).unwrap_err(); + assert_eq!(BatchEncodingError::SpanBatchError(SpanBatchError::EmptySpanBatch), err); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/element.rs b/kona/crates/protocol/protocol/src/batch/element.rs new file mode 100644 index 0000000000000..f5e3cf1da3c75 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/element.rs @@ -0,0 +1,56 @@ +//! Span Batch Element + +use crate::SingleBatch; +use alloc::vec::Vec; +use alloy_primitives::Bytes; + +/// MAX_SPAN_BATCH_ELEMENTS is the maximum number of blocks, transactions in total, +/// or transaction per block allowed in a span batch. +pub const MAX_SPAN_BATCH_ELEMENTS: u64 = 10_000_000; + +/// A single batch element is similar to the [`SingleBatch`] type +/// but does not contain the parent hash and epoch hash since spans +/// do not contain this data for every block in the span. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchElement { + /// The epoch number of the L1 block + pub epoch_num: u64, + /// The timestamp of the L2 block + pub timestamp: u64, + /// The transactions in the L2 block + pub transactions: Vec, +} + +impl From for SpanBatchElement { + fn from(batch: SingleBatch) -> Self { + Self { + epoch_num: batch.epoch_num, + timestamp: batch.timestamp, + transactions: batch.transactions, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::{collection::vec, prelude::any, proptest}; + + proptest! { + #[test] + fn test_span_batch_element_from_single_batch(epoch_num in 0u64..u64::MAX, timestamp in 0u64..u64::MAX, transactions in vec(any::(), 0..100)) { + let single_batch = SingleBatch { + epoch_num, + timestamp, + transactions: transactions.clone(), + ..Default::default() + }; + + let span_batch_element: SpanBatchElement = single_batch.into(); + + assert_eq!(span_batch_element.epoch_num, epoch_num); + assert_eq!(span_batch_element.timestamp, timestamp); + assert_eq!(span_batch_element.transactions, transactions); + } + } +} diff --git a/kona/crates/protocol/protocol/src/batch/errors.rs b/kona/crates/protocol/protocol/src/batch/errors.rs new file mode 100644 index 0000000000000..33f621af5de45 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/errors.rs @@ -0,0 +1,88 @@ +//! Span Batch Errors + +/// Span Batch Errors +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum SpanBatchError { + /// The span batch is too big + #[error("The span batch is too big.")] + TooBigSpanBatchSize, + /// The bit field is too long + #[error("The bit field is too long")] + BitfieldTooLong, + /// Empty Span Batch + #[error("Empty span batch")] + EmptySpanBatch, + /// Future batch L1 origin before safe head + #[error("Future batch L1 origin before safe head")] + L1OriginBeforeSafeHead, + /// Missing L1 origin + #[error("Missing L1 origin")] + MissingL1Origin, + /// Decoding errors + #[error("Span batch decoding error: {0}")] + Decoding(#[from] SpanDecodingError), +} + +/// An error encoding a batch. +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum BatchEncodingError { + /// Error encoding an Alloy RLP + #[error("Error encoding an Alloy RLP: {0}")] + AlloyRlpError(alloy_rlp::Error), + /// Error encoding a span batch + #[error("Error encoding a span batch: {0}")] + SpanBatchError(#[from] SpanBatchError), +} + +/// An error decoding a batch. +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum BatchDecodingError { + /// Empty buffer + #[error("Empty buffer")] + EmptyBuffer, + /// Error decoding an Alloy RLP + #[error("Error decoding an Alloy RLP: {0}")] + AlloyRlpError(alloy_rlp::Error), + /// Error decoding a span batch + #[error("Error decoding a span batch: {0}")] + SpanBatchError(#[from] SpanBatchError), +} + +/// Decoding Error +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum SpanDecodingError { + /// Failed to decode relative timestamp + #[error("Failed to decode relative timestamp")] + RelativeTimestamp, + /// Failed to decode L1 origin number + #[error("Failed to decode L1 origin number")] + L1OriginNumber, + /// Failed to decode parent check + #[error("Failed to decode parent check")] + ParentCheck, + /// Failed to decode L1 origin check + #[error("Failed to decode L1 origin check")] + L1OriginCheck, + /// Failed to decode block count + #[error("Failed to decode block count")] + BlockCount, + /// Failed to decode block tx counts + #[error("Failed to decode block tx counts")] + BlockTxCounts, + /// Failed to decode transaction nonces + #[error("Failed to decode transaction nonces")] + TxNonces, + /// Mismatch in length between the transaction type and signature arrays in a span batch + /// transaction payload. + #[error("Mismatch in length between the transaction type and signature arrays")] + TypeSignatureLenMismatch, + /// Invalid transaction type + #[error("Invalid transaction type")] + InvalidTransactionType, + /// Invalid transaction data + #[error("Invalid transaction data")] + InvalidTransactionData, + /// Invalid transaction signature + #[error("Invalid transaction signature")] + InvalidTransactionSignature, +} diff --git a/kona/crates/protocol/protocol/src/batch/inclusion.rs b/kona/crates/protocol/protocol/src/batch/inclusion.rs new file mode 100644 index 0000000000000..2c6bb9f1f8266 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/inclusion.rs @@ -0,0 +1,75 @@ +//! Module containing the [`BatchWithInclusionBlock`] struct. + +use crate::{Batch, BatchValidationProvider, BatchValidity, BlockInfo, L2BlockInfo}; +use kona_genesis::RollupConfig; + +/// A batch with its inclusion block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BatchWithInclusionBlock { + /// The inclusion block + pub inclusion_block: BlockInfo, + /// The batch + pub batch: Batch, +} + +impl BatchWithInclusionBlock { + /// Creates a new batch with inclusion block. + pub const fn new(inclusion_block: BlockInfo, batch: Batch) -> Self { + Self { inclusion_block, batch } + } + + /// Validates the batch can be applied on top of the specified L2 safe head. + /// The first entry of the l1_blocks should match the origin of the l2_safe_head. + /// One or more consecutive l1_blocks should be provided. + /// In case of only a single L1 block, the decision whether a batch is valid may have to stay + /// undecided. + pub async fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + fetcher: &mut BF, + ) -> BatchValidity { + match &self.batch { + Batch::Single(single_batch) => { + single_batch.check_batch(cfg, l1_blocks, l2_safe_head, &self.inclusion_block) + } + Batch::Span(span_batch) => { + span_batch + .check_batch(cfg, l1_blocks, l2_safe_head, &self.inclusion_block, fetcher) + .await + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestBatchValidator; + use alloc::vec; + + #[tokio::test] + async fn test_single_batch_with_inclusion_block() { + let batch = + BatchWithInclusionBlock::new(BlockInfo::default(), Batch::Single(Default::default())); + let l1_blocks = vec![BlockInfo::default()]; + let l2_safe_head = L2BlockInfo::default(); + let cfg = RollupConfig::default(); + let mut validator = TestBatchValidator::default(); + let result = batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &mut validator).await; + assert_eq!(result, BatchValidity::Accept); + } + + #[tokio::test] + async fn test_span_batch_with_inclusion_block() { + let batch = + BatchWithInclusionBlock::new(BlockInfo::default(), Batch::Span(Default::default())); + let l1_blocks = vec![BlockInfo::default()]; + let l2_safe_head = L2BlockInfo::default(); + let cfg = RollupConfig::default(); + let mut validator = TestBatchValidator::default(); + let result = batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &mut validator).await; + assert_eq!(result, BatchValidity::Undecided); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/mod.rs b/kona/crates/protocol/protocol/src/batch/mod.rs new file mode 100644 index 0000000000000..45e705d43a627 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/mod.rs @@ -0,0 +1,84 @@ +//! Batch types and processing for OP Stack L2 derivation. +//! +//! This module contains comprehensive batch handling functionality for the OP Stack +//! derivation pipeline. Batches are the fundamental unit of L2 transaction data +//! that are derived from L1 data and used to construct L2 blocks. +//! +//! # Batch Types +//! +//! ## Single Batches +//! Traditional batch format containing transactions for a single L2 block. +//! Simple, straightforward format used before span batch optimization. +//! +//! ## Span Batches +//! Advanced batch format that can contain transactions for multiple L2 blocks, +//! providing significant compression and efficiency improvements. Introduced +//! to reduce L1 data costs for high-throughput L2 chains. +//! +//! # Processing Pipeline +//! +//! ```text +//! L1 Data → Frame → Channel → Batch → L2 Block Attributes +//! ``` +//! +//! # Key Components +//! +//! - **Batch Types**: [`SingleBatch`], [`SpanBatch`] for different batch formats +//! - **Batch Reading**: [`BatchReader`] for decoding batch data from channels +//! - **Validation**: [`BatchValidationProvider`] for batch validity checking +//! - **Transaction Data**: Specialized transaction formats for span batches +//! - **Error Handling**: Comprehensive error types for batch processing failures + +mod r#type; +pub use r#type::*; + +mod reader; +pub use reader::{BatchReader, DecompressionError}; + +mod tx; +pub use tx::BatchTransaction; + +mod core; +pub use core::Batch; + +mod raw; +pub use raw::RawSpanBatch; + +mod payload; +pub use payload::SpanBatchPayload; + +mod prefix; +pub use prefix::SpanBatchPrefix; + +mod inclusion; +pub use inclusion::BatchWithInclusionBlock; + +mod errors; +pub use errors::{BatchDecodingError, BatchEncodingError, SpanBatchError, SpanDecodingError}; + +mod bits; +pub use bits::SpanBatchBits; + +mod span; +pub use span::SpanBatch; + +mod transactions; +pub use transactions::SpanBatchTransactions; + +mod element; +pub use element::{MAX_SPAN_BATCH_ELEMENTS, SpanBatchElement}; + +mod validity; +pub use validity::BatchValidity; + +mod single; +pub use single::SingleBatch; + +mod tx_data; +pub use tx_data::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchEip7702TransactionData, SpanBatchLegacyTransactionData, SpanBatchTransactionData, +}; + +mod traits; +pub use traits::BatchValidationProvider; diff --git a/kona/crates/protocol/protocol/src/batch/payload.rs b/kona/crates/protocol/protocol/src/batch/payload.rs new file mode 100644 index 0000000000000..62219c9f0fb52 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/payload.rs @@ -0,0 +1,197 @@ +//! Raw Span Batch Payload + +use super::MAX_SPAN_BATCH_ELEMENTS; +use crate::{SpanBatchBits, SpanBatchError, SpanBatchTransactions, SpanDecodingError}; +use alloc::vec::Vec; +use alloy_primitives::bytes; + +/// Span Batch Payload +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPayload { + /// Number of L2 block in the span + pub block_count: u64, + /// Standard span-batch bitlist of blockCount bits. Each bit indicates if the L1 origin is + /// changed at the L2 block. + pub origin_bits: SpanBatchBits, + /// List of transaction counts for each L2 block + pub block_tx_counts: Vec, + /// Transactions encoded in SpanBatch specs + pub txs: SpanBatchTransactions, +} + +impl SpanBatchPayload { + /// Decodes a [`SpanBatchPayload`] from a reader. + pub fn decode_payload(r: &mut &[u8]) -> Result { + let mut payload = Self::default(); + payload.decode_block_count(r)?; + payload.decode_origin_bits(r)?; + payload.decode_block_tx_counts(r)?; + payload.decode_txs(r)?; + Ok(payload) + } + + /// Encodes a [`SpanBatchPayload`] into a writer. + pub fn encode_payload(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + self.encode_block_count(w); + self.encode_origin_bits(w)?; + self.encode_block_tx_counts(w); + self.encode_txs(w) + } + + /// Decodes the origin bits from a reader. + pub fn decode_origin_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.block_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.origin_bits = SpanBatchBits::decode(r, self.block_count as usize)?; + Ok(()) + } + + /// Decode a block count from a reader. + pub fn decode_block_count(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (block_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockCount))?; + // The number of transactions in a single L2 block cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if block_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + if block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + self.block_count = block_count; + *r = remaining; + Ok(()) + } + + /// Decode block transaction counts from a reader. + pub fn decode_block_tx_counts(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + // Initially allocate the vec with the block count, to reduce re-allocations in the first + // few blocks. + let mut block_tx_counts = Vec::with_capacity(self.block_count as usize); + + for _ in 0..self.block_count { + let (block_tx_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockTxCounts))?; + + // The number of transactions in a single L2 block cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + block_tx_counts.push(block_tx_count); + *r = remaining; + } + self.block_tx_counts = block_tx_counts; + Ok(()) + } + + /// Decode transactions from a reader. + pub fn decode_txs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.block_tx_counts.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + let total_block_tx_count = + self.block_tx_counts.iter().try_fold(0u64, |acc, block_tx_count| { + acc.checked_add(*block_tx_count).ok_or(SpanBatchError::TooBigSpanBatchSize) + })?; + + // The total number of transactions in a span batch cannot be greater than + // [MAX_SPAN_BATCH_ELEMENTS]. + if total_block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + self.txs.total_block_tx_count = total_block_tx_count; + self.txs.decode(r)?; + Ok(()) + } + + /// Encode the origin bits into a writer. + pub fn encode_origin_bits(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.block_count as usize, &self.origin_bits) + } + + /// Encode the block count into a writer. + pub fn encode_block_count(&self, w: &mut dyn bytes::BufMut) { + let mut u64_varint_buf = [0u8; 10]; + w.put_slice(unsigned_varint::encode::u64(self.block_count, &mut u64_varint_buf)); + } + + /// Encode the block transaction counts into a writer. + pub fn encode_block_tx_counts(&self, w: &mut dyn bytes::BufMut) { + let mut u64_varint_buf = [0u8; 10]; + for block_tx_count in &self.block_tx_counts { + u64_varint_buf.fill(0); + w.put_slice(unsigned_varint::encode::u64(*block_tx_count, &mut u64_varint_buf)); + } + } + + /// Encode the transactions into a writer. + pub fn encode_txs(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + self.txs.encode(w) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_origin_bits() { + let block_count = 10; + let encoded = vec![2; block_count / 8 + 1]; + let mut payload = + SpanBatchPayload { block_count: block_count as u64, ..Default::default() }; + payload.decode_origin_bits(&mut encoded.as_slice()).unwrap(); + assert_eq!(payload.origin_bits, SpanBatchBits::new(vec![2; block_count / 8 + 1])); + } + + #[test] + fn test_zero_block_count() { + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(0, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + let err = payload.decode_block_count(&mut encoded).unwrap_err(); + assert_eq!(err, SpanBatchError::EmptySpanBatch); + } + + #[test] + fn test_decode_block_count() { + let block_count = MAX_SPAN_BATCH_ELEMENTS; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + payload.decode_block_count(&mut encoded).unwrap(); + assert_eq!(payload.block_count, block_count); + } + + #[test] + fn test_decode_block_count_errors() { + let block_count = MAX_SPAN_BATCH_ELEMENTS + 1; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + let err = payload.decode_block_count(&mut encoded).unwrap_err(); + assert_eq!(err, SpanBatchError::TooBigSpanBatchSize); + } + + #[test] + fn test_decode_block_tx_counts() { + let block_count = 2; + let mut u64_varint_buf = [0; 10]; + let mut encoded = unsigned_varint::encode::u64(block_count, &mut u64_varint_buf); + let mut payload = SpanBatchPayload::default(); + payload.decode_block_count(&mut encoded).unwrap(); + let mut r: Vec = Vec::new(); + for _ in 0..2 { + let mut buf = [0u8; 10]; + let encoded = unsigned_varint::encode::u64(2, &mut buf); + r.append(&mut encoded.to_vec()); + } + payload.decode_block_tx_counts(&mut r.as_slice()).unwrap(); + assert_eq!(payload.block_tx_counts, vec![2, 2]); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/prefix.rs b/kona/crates/protocol/protocol/src/batch/prefix.rs new file mode 100644 index 0000000000000..a95a54995afd2 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/prefix.rs @@ -0,0 +1,96 @@ +//! Raw Span Batch Prefix + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_primitives::{FixedBytes, bytes}; + +/// Span Batch Prefix +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPrefix { + /// Relative timestamp of the first block + pub rel_timestamp: u64, + /// L1 origin number + pub l1_origin_num: u64, + /// First 20 bytes of the first block's parent hash + pub parent_check: FixedBytes<20>, + /// First 20 bytes of the last block's L1 origin hash + pub l1_origin_check: FixedBytes<20>, +} + +impl SpanBatchPrefix { + /// Decodes a [`SpanBatchPrefix`] from a reader. + pub fn decode_prefix(r: &mut &[u8]) -> Result { + let mut prefix = Self::default(); + prefix.decode_rel_timestamp(r)?; + prefix.decode_l1_origin_num(r)?; + prefix.decode_parent_check(r)?; + prefix.decode_l1_origin_check(r)?; + Ok(prefix) + } + + /// Decodes the relative timestamp from a reader. + pub fn decode_rel_timestamp(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (rel_timestamp, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::RelativeTimestamp))?; + *r = remaining; + self.rel_timestamp = rel_timestamp; + Ok(()) + } + + /// Decodes the L1 origin number from a reader. + pub fn decode_l1_origin_num(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_num, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::L1OriginNumber))?; + *r = remaining; + self.l1_origin_num = l1_origin_num; + Ok(()) + } + + /// Decodes the parent check from a reader. + pub fn decode_parent_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (parent_check, remaining) = r.split_at(20); + let parent_check = FixedBytes::<20>::from_slice(parent_check); + *r = remaining; + self.parent_check = parent_check; + Ok(()) + } + + /// Decodes the L1 origin check from a reader. + pub fn decode_l1_origin_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_check, remaining) = r.split_at(20); + let l1_origin_check = FixedBytes::<20>::from_slice(l1_origin_check); + *r = remaining; + self.l1_origin_check = l1_origin_check; + Ok(()) + } + + /// Encodes the [`SpanBatchPrefix`] into a writer. + pub fn encode_prefix(&self, w: &mut dyn bytes::BufMut) { + let mut u64_buf = [0u8; 10]; + w.put_slice(unsigned_varint::encode::u64(self.rel_timestamp, &mut u64_buf)); + w.put_slice(unsigned_varint::encode::u64(self.l1_origin_num, &mut u64_buf)); + w.put_slice(self.parent_check.as_slice()); + w.put_slice(self.l1_origin_check.as_slice()); + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec::Vec; + use alloy_primitives::address; + + #[test] + fn test_span_batch_prefix_encoding_roundtrip() { + let expected = SpanBatchPrefix { + rel_timestamp: 0xFF, + l1_origin_num: 0xEE, + parent_check: address!("beef00000000000000000000000000000000beef").into(), + l1_origin_check: address!("babe00000000000000000000000000000000babe").into(), + }; + + let mut buf = Vec::new(); + expected.encode_prefix(&mut buf); + + assert_eq!(SpanBatchPrefix::decode_prefix(&mut buf.as_slice()).unwrap(), expected); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/raw.rs b/kona/crates/protocol/protocol/src/batch/raw.rs new file mode 100644 index 0000000000000..c2e8506f02004 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/raw.rs @@ -0,0 +1,143 @@ +//! Module containing the [`RawSpanBatch`] struct. + +use alloc::{vec, vec::Vec}; +use alloy_primitives::bytes; + +use crate::{ + BatchType, SpanBatch, SpanBatchElement, SpanBatchError, SpanBatchPayload, SpanBatchPrefix, + SpanDecodingError, +}; + +/// Raw Span Batch +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawSpanBatch { + /// The span batch prefix + pub prefix: SpanBatchPrefix, + /// The span batch payload + pub payload: SpanBatchPayload, +} + +impl RawSpanBatch { + /// Returns the batch type + pub const fn get_batch_type(&self) -> BatchType { + BatchType::Span + } + + /// Encodes the [`RawSpanBatch`] into a writer. + pub fn encode(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + self.prefix.encode_prefix(w); + self.payload.encode_payload(w) + } + + /// Decodes the [`RawSpanBatch`] from a reader.] + pub fn decode(r: &mut &[u8]) -> Result { + let prefix = SpanBatchPrefix::decode_prefix(r)?; + let payload = SpanBatchPayload::decode_payload(r)?; + Ok(Self { prefix, payload }) + } + + /// Converts a [`RawSpanBatch`] into a [`SpanBatch`], which has a list of [`SpanBatchElement`]s. + /// This function does not populate the [`SpanBatch`] with chain configuration data, which + /// is required for making payload attributes. + pub fn derive( + &mut self, + block_time: u64, + genesis_time: u64, + chain_id: u64, + ) -> Result { + if self.payload.block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + + let mut block_origin_nums = vec![0u64; self.payload.block_count as usize]; + let mut l1_origin_number = self.prefix.l1_origin_num; + for i in (0..self.payload.block_count).rev() { + block_origin_nums[i as usize] = l1_origin_number; + if self + .payload + .origin_bits + .get_bit(i as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::L1OriginCheck))? == + 1 && + i > 0 + { + l1_origin_number -= 1; + } + } + + // Get all transactions in the batch. + let enveloped_txs = self.payload.txs.full_txs(chain_id)?; + + let mut tx_idx = 0; + let batches = (0..self.payload.block_count).fold(Vec::new(), |mut acc, i| { + let transactions = + (0..self.payload.block_tx_counts[i as usize]).fold(Vec::new(), |mut acc, _| { + acc.push(enveloped_txs[tx_idx].clone()); + tx_idx += 1; + acc + }); + acc.push(SpanBatchElement { + epoch_num: block_origin_nums[i as usize], + timestamp: genesis_time + self.prefix.rel_timestamp + block_time * i, + transactions: transactions.into_iter().map(|v| v.into()).collect(), + }); + acc + }); + + Ok(SpanBatch { + parent_check: self.prefix.parent_check, + l1_origin_check: self.prefix.l1_origin_check, + batches, + ..Default::default() + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_primitives::FixedBytes; + + #[test] + fn test_try_from_span_batch_empty_batches_errors() { + let span_batch = SpanBatch::default(); + let raw_span_batch = span_batch.to_raw_span_batch().unwrap_err(); + assert_eq!(raw_span_batch, SpanBatchError::EmptySpanBatch); + } + + #[test] + fn test_try_from_span_batch_succeeds() { + let parent_check = FixedBytes::from([2u8; 20]); + let l1_origin_check = FixedBytes::from([3u8; 20]); + let first = SpanBatchElement { epoch_num: 100, timestamp: 400, transactions: Vec::new() }; + let last = SpanBatchElement { epoch_num: 200, timestamp: 500, transactions: Vec::new() }; + let span_batch = SpanBatch { + batches: vec![first, last], + genesis_timestamp: 300, + parent_check, + l1_origin_check, + ..Default::default() + }; + let expected_prefix = SpanBatchPrefix { + rel_timestamp: 100, + l1_origin_num: 200, + parent_check, + l1_origin_check, + }; + let expected_payload = SpanBatchPayload { block_count: 2, ..Default::default() }; + let raw_span_batch = span_batch.to_raw_span_batch().unwrap(); + assert_eq!(raw_span_batch.prefix, expected_prefix); + assert_eq!(raw_span_batch.payload, expected_payload); + } + + #[test] + fn test_decode_encode_raw_span_batch() { + // Load in the raw span batch from the `op-node` derivation pipeline implementation. + let raw_span_batch_hex = include_bytes!("./testdata/raw_batch.hex"); + let raw_span_batch = RawSpanBatch::decode(&mut raw_span_batch_hex.as_slice()).unwrap(); + + let mut encoding_buf = Vec::new(); + raw_span_batch.encode(&mut encoding_buf).unwrap(); + assert_eq!(encoding_buf, raw_span_batch_hex); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/reader.rs b/kona/crates/protocol/protocol/src/batch/reader.rs new file mode 100644 index 0000000000000..3c06ab4ec16b0 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/reader.rs @@ -0,0 +1,165 @@ +//! Contains the [`BatchReader`] which is used to iteratively consume batches from raw data. + +use crate::{Batch, BrotliDecompressionError, decompress_brotli}; +use alloc::vec::Vec; +use alloy_primitives::Bytes; +use alloy_rlp::Decodable; +use kona_genesis::RollupConfig; +use miniz_oxide::inflate::decompress_to_vec_zlib; + +/// Error type for decompression failures. +#[derive(Debug, thiserror::Error)] +pub enum DecompressionError { + /// The data to decompress was empty. + #[error("the data to decompress was empty")] + EmptyData, + /// The compression type is not supported. + #[error("the compression type {0} is not supported")] + UnsupportedType(u8), + /// A brotli decompression error. + #[error("brotli decompression error: {0}")] + BrotliError(#[from] BrotliDecompressionError), + /// A zlib decompression error. + #[error("zlib decompression error")] + ZlibError, + /// The RLP data is too large for the configured maximum. + #[error("the RLP data is too large: {0} bytes, maximum allowed: {1} bytes")] + RlpTooLarge(usize, usize), +} + +/// Batch Reader provides a function that iteratively consumes batches from the reader. +/// The L1Inclusion block is also provided at creation time. +/// Warning: the batch reader can read every batch-type. +/// The caller of the batch-reader should filter the results. +#[derive(Debug)] +pub struct BatchReader { + /// The raw data to decode. + pub data: Option>, + /// Decompressed data. + pub decompressed: Vec, + /// The current cursor in the `decompressed` data. + pub cursor: usize, + /// The maximum RLP bytes per channel. + pub max_rlp_bytes_per_channel: usize, + /// Whether brotli decompression was used. + pub brotli_used: bool, +} + +impl BatchReader { + /// ZLIB Deflate Compression Method. + pub const ZLIB_DEFLATE_COMPRESSION_METHOD: u8 = 8; + + /// ZLIB Reserved Compression Info. + pub const ZLIB_RESERVED_COMPRESSION_METHOD: u8 = 15; + + /// Brotli Compression Channel Version. + pub const CHANNEL_VERSION_BROTLI: u8 = 1; + + /// Creates a new [`BatchReader`] from the given data and max decompressed RLP bytes per + /// channel. + pub fn new(data: T, max_rlp_bytes_per_channel: usize) -> Self + where + T: Into>, + { + Self { + data: Some(data.into()), + decompressed: Vec::new(), + cursor: 0, + max_rlp_bytes_per_channel, + brotli_used: false, + } + } + + /// Helper method to decompress the data contained in the reader. + pub fn decompress(&mut self) -> Result<(), DecompressionError> { + if let Some(data) = self.data.take() { + // Peek at the data to determine the compression type. + if data.is_empty() { + return Err(DecompressionError::EmptyData); + } + + let compression_type = data[0]; + if (compression_type & 0x0F) == Self::ZLIB_DEFLATE_COMPRESSION_METHOD || + (compression_type & 0x0F) == Self::ZLIB_RESERVED_COMPRESSION_METHOD + { + self.decompressed = + decompress_to_vec_zlib(&data).map_err(|_| DecompressionError::ZlibError)?; + + // Check the size of the decompressed channel RLP. + if self.decompressed.len() > self.max_rlp_bytes_per_channel { + return Err(DecompressionError::RlpTooLarge( + self.decompressed.len(), + self.max_rlp_bytes_per_channel, + )); + } + } else if compression_type == Self::CHANNEL_VERSION_BROTLI { + self.brotli_used = true; + self.decompressed = decompress_brotli(&data[1..], self.max_rlp_bytes_per_channel)?; + } else { + return Err(DecompressionError::UnsupportedType(compression_type)); + } + } + Ok(()) + } + + /// Pulls out the next batch from the reader. + pub fn next_batch(&mut self, cfg: &RollupConfig) -> Option { + // Ensure the data is decompressed. + self.decompress().ok()?; + + // Decompress and RLP decode the batch data, before finally decoding the batch itself. + let decompressed_reader = &mut self.decompressed.as_slice()[self.cursor..].as_ref(); + let bytes = Bytes::decode(decompressed_reader).ok()?; + let Ok(batch) = Batch::decode(&mut bytes.as_ref(), cfg) else { + return None; + }; + + // Confirm that brotli decompression was performed *after* the Fjord hardfork. + if self.brotli_used && !cfg.is_fjord_active(batch.timestamp()) { + return None; + } + + // Advance the cursor on the reader. + self.cursor = self.decompressed.len() - decompressed_reader.len(); + Some(batch) + } +} + +#[cfg(test)] +mod test { + use super::*; + use kona_genesis::{ + HardForkConfig, MAX_RLP_BYTES_PER_CHANNEL_BEDROCK, MAX_RLP_BYTES_PER_CHANNEL_FJORD, + }; + + fn new_compressed_batch_data() -> Bytes { + let file_contents = + alloc::string::String::from_utf8_lossy(include_bytes!("../../testdata/batch.hex")); + let file_contents = &(&*file_contents)[..file_contents.len() - 1]; + let data = alloy_primitives::hex::decode(file_contents).unwrap(); + data.into() + } + + #[test] + fn test_batch_reader() { + let raw = new_compressed_batch_data(); + let decompressed_len = decompress_to_vec_zlib(&raw).unwrap().len(); + let mut reader = BatchReader::new(raw, MAX_RLP_BYTES_PER_CHANNEL_BEDROCK as usize); + reader.next_batch(&RollupConfig::default()).unwrap(); + assert_eq!(reader.cursor, decompressed_len); + } + + #[test] + fn test_batch_reader_fjord() { + let raw = new_compressed_batch_data(); + let decompressed_len = decompress_to_vec_zlib(&raw).unwrap().len(); + let mut reader = BatchReader::new(raw, MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize); + reader + .next_batch(&RollupConfig { + hardforks: HardForkConfig { fjord_time: Some(0), ..Default::default() }, + ..Default::default() + }) + .unwrap(); + assert_eq!(reader.cursor, decompressed_len); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/single.rs b/kona/crates/protocol/protocol/src/batch/single.rs new file mode 100644 index 0000000000000..416c2eb04514d --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/single.rs @@ -0,0 +1,639 @@ +//! This module contains the [`SingleBatch`] type. + +use crate::{BatchValidity, BlockInfo, L2BlockInfo}; +use alloc::vec::Vec; +use alloy_eips::BlockNumHash; +use alloy_primitives::{BlockHash, Bytes}; +use alloy_rlp::{RlpDecodable, RlpEncodable}; +use kona_genesis::RollupConfig; +use op_alloy_consensus::OpTxType; +use tracing::warn; + +/// Represents a single batch: a single encoded L2 block +#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)] +pub struct SingleBatch { + /// Block hash of the previous L2 block. `B256::ZERO` if it has not been set by the Batch + /// Queue. + pub parent_hash: BlockHash, + /// The batch epoch number. Same as the first L1 block number in the epoch. + pub epoch_num: u64, + /// The block hash of the first L1 block in the epoch + pub epoch_hash: BlockHash, + /// The L2 block timestamp of this batch + pub timestamp: u64, + /// The L2 block transactions in this batch + pub transactions: Vec, +} + +impl SingleBatch { + /// Returns the [`BlockNumHash`] of the batch. + pub const fn epoch(&self) -> BlockNumHash { + BlockNumHash { number: self.epoch_num, hash: self.epoch_hash } + } + + /// Validate the batch timestamp. + pub fn check_batch_timestamp( + &self, + cfg: &RollupConfig, + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + ) -> BatchValidity { + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + if self.timestamp > next_timestamp { + if cfg.is_holocene_active(inclusion_block.timestamp) { + return BatchValidity::Drop; + } + return BatchValidity::Future; + } + if self.timestamp < next_timestamp { + if cfg.is_holocene_active(inclusion_block.timestamp) { + return BatchValidity::Past; + } + return BatchValidity::Drop; + } + BatchValidity::Accept + } + + /// Checks if the batch is valid. + /// + /// The batch format type is defined in the [OP Stack Specs][specs]. + /// + /// [specs]: https://specs.optimism.io/protocol/derivation.html#batch-format + pub fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + ) -> BatchValidity { + // Cannot have empty l1_blocks for batch validation. + if l1_blocks.is_empty() { + return BatchValidity::Undecided; + } + + let epoch = l1_blocks[0]; + + // If the batch is not accepted by the timestamp check, return the result. + let timestamp_check = self.check_batch_timestamp(cfg, l2_safe_head, inclusion_block); + if !timestamp_check.is_accept() { + return timestamp_check; + } + + // Dependent on the above timestamp check. + // If the timestamp is correct, then it must build on top of the safe head. + if self.parent_hash != l2_safe_head.block_info.hash { + return BatchValidity::Drop; + } + + // Filter out batches that were included too late. + if self.epoch_num + cfg.seq_window_size < inclusion_block.number { + return BatchValidity::Drop; + } + + // Check the L1 origin of the batch + let mut batch_origin = epoch; + if self.epoch_num < epoch.number { + return BatchValidity::Drop; + } else if self.epoch_num == epoch.number { + // Batch is sticking to the current epoch, continue. + } else if self.epoch_num == epoch.number + 1 { + // With only 1 l1Block we cannot look at the next L1 Origin. + // Note: This means that we are unable to determine validity of a batch + // without more information. In this case we should bail out until we have + // more information otherwise the eager algorithm may diverge from a non-eager + // algorithm. + if l1_blocks.len() < 2 { + return BatchValidity::Undecided; + } + batch_origin = l1_blocks[1]; + } else { + return BatchValidity::Drop; + } + + // Validate the batch epoch hash + if self.epoch_hash != batch_origin.hash { + return BatchValidity::Drop; + } + + if self.timestamp < batch_origin.timestamp { + return BatchValidity::Drop; + } + + // Check if we ran out of sequencer time drift + let max_drift = cfg.max_sequencer_drift(batch_origin.timestamp); + let max = if let Some(max) = batch_origin.timestamp.checked_add(max_drift) { + max + } else { + return BatchValidity::Drop; + }; + + let no_txs = self.transactions.is_empty(); + if self.timestamp > max && !no_txs { + // If the sequencer is ignoring the time drift rule, then drop the batch and force an + // empty batch instead, as the sequencer is not allowed to include anything + // past this point without moving to the next epoch. + return BatchValidity::Drop; + } + if self.timestamp > max && no_txs { + // If the sequencer is co-operating by producing an empty batch, + // allow the batch if it was the right thing to do to maintain the L2 time >= L1 time + // invariant. Only check batches that do not advance the epoch, to ensure + // epoch advancement regardless of time drift is allowed. + if epoch.number == batch_origin.number { + if l1_blocks.len() < 2 { + return BatchValidity::Undecided; + } + let next_origin = l1_blocks[1]; + // Check if the next L1 Origin could have been adopted + if self.timestamp >= next_origin.timestamp { + return BatchValidity::Drop; + } + } + } + + // If this is the first block in the jovian or interop hardfork, and the batch contains any + // transactions, it must be dropped. + if (cfg.is_first_jovian_block(self.timestamp) || cfg.is_first_interop_block(self.timestamp)) && + !self.transactions.is_empty() + { + warn!( + target: "single_batch", + "Sequencer included user transactions in jovian or interop transition block. Dropping batch." + ); + return BatchValidity::Drop; + } + + // We can do this check earlier, but it's intensive so we do it last for the sad-path. + for tx in self.transactions.iter() { + if tx.is_empty() { + return BatchValidity::Drop; + } + if tx.as_ref().first() == Some(&(OpTxType::Deposit as u8)) { + return BatchValidity::Drop; + } + // If isthmus is not active yet and the transaction is a 7702, drop the batch. + if !cfg.is_isthmus_active(self.timestamp) && + tx.as_ref().first() == Some(&(OpTxType::Eip7702 as u8)) + { + return BatchValidity::Drop; + } + } + + BatchValidity::Accept + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::{CollectingLayer, TraceStorage}; + + use super::*; + use alloc::vec; + use alloy_consensus::{SignableTransaction, TxEip1559, TxEip7702, TxEnvelope}; + use alloy_eips::eip2718::{Decodable2718, Encodable2718}; + use alloy_primitives::{Address, Sealed, Signature, TxKind, U256}; + use kona_genesis::HardForkConfig; + use op_alloy_consensus::{OpTxEnvelope, TxDeposit}; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + + #[test] + fn test_empty_l1_blocks() { + let cfg = RollupConfig::default(); + let l1_blocks = vec![]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let batch = SingleBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Undecided + ); + } + + #[test] + fn test_timestamp_future() { + let cfg = RollupConfig::default(); + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let batch = SingleBatch { timestamp: 2, ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Future + ); + } + + #[test] + fn test_parent_hash_mismatch() { + let cfg = RollupConfig::default(); + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { hash: BlockHash::from([0x01; 32]), ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let batch = SingleBatch { parent_hash: BlockHash::from([0x02; 32]), ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_timestamp_holocene_inactive_future() { + let cfg = RollupConfig::default(); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() }; + let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() }; + assert_eq!( + batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block), + BatchValidity::Future + ); + } + + #[test] + fn test_check_batch_timestamp_holocene_active_drop() { + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() }; + let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() }; + assert_eq!( + batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_timestamp_holocene_active_past() { + let cfg = RollupConfig { + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 2, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() }; + let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() }; + assert_eq!( + batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block), + BatchValidity::Past + ); + } + + #[test] + fn test_check_batch_timestamp_holocene_inactive_drop() { + let cfg = RollupConfig::default(); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 2, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() }; + let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() }; + assert_eq!( + batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_timestamp_accept() { + let cfg = RollupConfig::default(); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 2, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let batch = SingleBatch { timestamp: 2, ..Default::default() }; + assert_eq!( + batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block), + BatchValidity::Accept + ); + } + + #[test] + fn test_roundtrip_encoding() { + use alloy_rlp::{Decodable, Encodable}; + let batch = SingleBatch { + parent_hash: BlockHash::from([0x01; 32]), + epoch_num: 1, + epoch_hash: BlockHash::from([0x02; 32]), + timestamp: 1, + transactions: vec![Bytes::from(vec![0x01])], + }; + let mut buf = vec![]; + batch.encode(&mut buf); + let decoded = SingleBatch::decode(&mut buf.as_slice()).unwrap(); + assert_eq!(batch, decoded); + } + + #[test] + fn test_check_batch_succeeds() { + let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let batch = SingleBatch { + parent_hash: BlockHash::ZERO, + epoch_num: 1, + epoch_hash: BlockHash::ZERO, + timestamp: 1, + transactions: vec![Bytes::from(vec![0x01])], + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Accept + ); + } + + fn eip_1559_tx() -> TxEip1559 { + TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[6]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + } + } + + fn example_transactions() -> Vec { + let mut transactions = Vec::new(); + + // First Transaction in the batch. + let tx = eip_1559_tx(); + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + // Second transaction in the batch. + let mut tx = eip_1559_tx(); + tx.to = Address::left_padding_from(&[7]).into(); + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + transactions + } + + #[test] + fn test_check_batch_full_txs() { + // Use the example transaction + let transactions = example_transactions(); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Accept + ); + } + + fn eip_7702_tx() -> TxEip7702 { + TxEip7702 { + chain_id: 10u64, + nonce: 2, + gas_limit: 5, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + to: Address::left_padding_from(&[7]), + value: U256::from(7_u64), + input: vec![8].into(), + ..Default::default() + } + } + + #[test] + fn test_check_batch_drop_7702_pre_isthmus() { + // Use the example transaction + let mut transactions = example_transactions(); + + // Extend the transactions with the 7702 transaction + let eip_7702_tx = eip_7702_tx(); + let sig = Signature::test_signature(); + let tx_signed = eip_7702_tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.into()); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + // Notice: Isthmus is _not_ active yet. + let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_accept_7702_post_isthmus() { + // Use the example transaction + let mut transactions = example_transactions(); + + // Extend the transactions with the 7702 transaction + let eip_7702_tx = eip_7702_tx(); + let sig = Signature::test_signature(); + let tx_signed = eip_7702_tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.into()); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + // Notice: Isthmus is active. + let cfg = RollupConfig { + max_sequencer_drift: 1, + hardforks: HardForkConfig { isthmus_time: Some(0), ..Default::default() }, + ..Default::default() + }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Accept + ); + } + + #[test] + fn test_check_batch_drop_empty_tx() { + // An empty tx is not valid 2718 encoding. + // The batch must be dropped. + let transactions = vec![Default::default()]; + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + // Notice: Isthmus is _not_ active yet. + let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_drop_2718_deposit() { + // Add a 2718 deposit transaction to the batch. + let mut transactions = example_transactions(); + + // Extend the transactions with the 2718 deposit transaction + let tx = TxDeposit { + source_hash: Default::default(), + from: Address::left_padding_from(&[7]), + to: TxKind::Create, + mint: 0, + value: U256::from(7_u64), + gas_limit: 5, + is_system_transaction: false, + input: Default::default(), + }; + let envelope = OpTxEnvelope::Deposit(Sealed::new(tx)); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.into()); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + // Notice: Isthmus is _not_ active yet. + let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 1, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + } + + #[test] + fn test_check_batch_drop_non_empty_interop_transition() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + let subscriber = tracing_subscriber::Registry::default().with(layer); + let _guard = tracing::subscriber::set_default(subscriber); + + // Gather a few test transactions for the batch. + let transactions = example_transactions(); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + + let single_batch = + SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + + let cfg = RollupConfig { + max_sequencer_drift: 1, + block_time: 1, + hardforks: HardForkConfig { interop_time: Some(1), ..Default::default() }, + ..Default::default() + }; + let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 0, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + assert_eq!( + single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block), + BatchValidity::Drop + ); + + assert!( + trace_store + .get_by_level(Level::WARN) + .iter() + .any(|s| { s.contains("Sequencer included user transactions") }) + ) + } +} diff --git a/kona/crates/protocol/protocol/src/batch/span.rs b/kona/crates/protocol/protocol/src/batch/span.rs new file mode 100644 index 0000000000000..903edba9b138b --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/span.rs @@ -0,0 +1,2352 @@ +//! Span Batch implementation for efficient multi-block L2 transaction batching. +//! +//! Span batches are an advanced batching format that can contain transactions for multiple +//! L2 blocks in a single compressed structure. This provides significant efficiency gains +//! over single batches by amortizing overhead across multiple blocks and enabling +//! sophisticated compression techniques. + +use alloc::vec::Vec; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::FixedBytes; +use kona_genesis::RollupConfig; +use op_alloy_consensus::OpTxType; +use tracing::{info, warn}; + +use crate::{ + BatchValidationProvider, BatchValidity, BlockInfo, L2BlockInfo, RawSpanBatch, SingleBatch, + SpanBatchBits, SpanBatchElement, SpanBatchError, SpanBatchPayload, SpanBatchPrefix, + SpanBatchTransactions, +}; + +/// Container for the inputs required to build a span of L2 blocks in derived form. +/// +/// A [`SpanBatch`] represents a compressed format for multiple L2 blocks that enables +/// significant space savings compared to individual single batches. The format uses +/// differential encoding, bit packing, and shared data structures to minimize the +/// L1 footprint while maintaining all necessary information for L2 block reconstruction. +/// +/// # Compression Techniques +/// +/// ## Temporal Compression +/// - **Relative timestamps**: Store timestamps relative to genesis to reduce size +/// - **Differential encoding**: Encode changes between consecutive blocks +/// - **Epoch sharing**: Multiple blocks can share the same L1 origin +/// +/// ## Spatial Compression +/// - **Shared prefixes**: Common data shared across all blocks in span +/// - **Transaction batching**: Transactions grouped and compressed together +/// - **Bit packing**: Use minimal bits for frequently-used fields +/// +/// # Format Structure +/// +/// ```text +/// SpanBatch { +/// prefix: { +/// rel_timestamp, // Relative to genesis +/// l1_origin_num, // Final L1 block number +/// parent_check, // First 20 bytes of parent hash +/// l1_origin_check, // First 20 bytes of L1 origin hash +/// }, +/// payload: { +/// block_count, // Number of blocks in span +/// origin_bits, // Bit array indicating L1 origin changes +/// block_tx_counts, // Transaction count per block +/// txs, // Compressed transaction data +/// } +/// } +/// ``` +/// +/// # Validation and Integrity +/// +/// The span batch format includes several integrity checks: +/// - **Parent check**: Validates continuity with previous span +/// - **L1 origin check**: Ensures proper L1 origin binding +/// - **Transaction count validation**: Verifies transaction distribution +/// - **Bit field consistency**: Ensures origin bits match block count +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatch { + /// First 20 bytes of the parent hash of the first block in the span. + /// + /// This field provides a collision-resistant check to ensure the span batch + /// builds properly on the expected parent block. Using only 20 bytes saves + /// space while maintaining strong integrity guarantees. + pub parent_check: FixedBytes<20>, + /// First 20 bytes of the L1 origin hash of the last block in the span. + /// + /// This field enables validation that the span batch references the correct + /// L1 origin block, ensuring proper derivation ordering and preventing + /// replay attacks across different L1 contexts. + pub l1_origin_check: FixedBytes<20>, + /// Genesis block timestamp for relative timestamp calculations. + /// + /// All timestamps in the span batch are stored relative to this genesis + /// timestamp to minimize storage requirements. This enables efficient + /// timestamp compression while maintaining full precision. + pub genesis_timestamp: u64, + /// Chain ID for transaction validation and network identification. + /// + /// Required for proper transaction signature validation and to prevent + /// cross-chain replay attacks. All transactions in the span must be + /// valid for this chain ID. + pub chain_id: u64, + /// Ordered list of block elements contained in this span. + /// + /// Each element represents the derived data for one L2 block, including + /// timestamp, epoch information, and transaction references. The order + /// must match the intended L2 block sequence. + pub batches: Vec, + /// Cached bit array indicating L1 origin changes between consecutive blocks. + /// + /// This compressed representation allows efficient encoding of which blocks + /// in the span advance to a new L1 origin. Bit `i` is set if block `i+1` + /// has a different L1 origin than block `i`. + pub origin_bits: SpanBatchBits, + /// Cached transaction count for each block in the span. + /// + /// Pre-computed transaction counts enable efficient random access to + /// transactions for specific blocks without scanning the entire transaction + /// list. Index `i` contains the transaction count for block `i`. + pub block_tx_counts: Vec, + /// Cached compressed transaction data for all blocks in the span. + /// + /// Contains all transactions from all blocks in a compressed format that + /// enables efficient encoding and decoding. Transactions are grouped and + /// compressed using span-specific techniques. + pub txs: SpanBatchTransactions, +} + +impl SpanBatch { + /// Returns the starting timestamp for the first batch in the span. + /// + /// This is the absolute timestamp (not relative to genesis) of the first + /// block in the span batch. Used for validation and block sequencing. + /// + /// # Panics + /// Panics if the span batch contains no elements (`batches` is empty). + /// This should never happen in valid span batches as they must contain + /// at least one block. + /// + /// # Usage + /// Typically used during span batch validation to ensure proper temporal + /// ordering with respect to the parent block and L1 derivation window. + pub fn starting_timestamp(&self) -> u64 { + self.batches[0].timestamp + } + + /// Returns the final timestamp for the last batch in the span. + /// + /// This is the absolute timestamp (not relative to genesis) of the last + /// block in the span batch. Used for validation and determining the + /// span's temporal range. + /// + /// # Panics + /// Panics if the span batch contains no elements (`batches` is empty). + /// This should never happen in valid span batches as they must contain + /// at least one block. + /// + /// # Usage + /// Used during validation to ensure the span doesn't exceed maximum + /// temporal ranges and fits within L1 derivation windows. + pub fn final_timestamp(&self) -> u64 { + self.batches[self.batches.len() - 1].timestamp + } + + /// Returns the L1 epoch number for the first batch in the span. + /// + /// The epoch number corresponds to the L1 block number that serves as + /// the L1 origin for the first L2 block in this span. This establishes + /// the L1 derivation context for the span. + /// + /// # Panics + /// Panics if the span batch contains no elements (`batches` is empty). + /// This should never happen in valid span batches as they must contain + /// at least one block. + /// + /// # Usage + /// Used during validation to ensure proper L1 origin sequencing and + /// that the span begins with the expected L1 context. + pub fn starting_epoch_num(&self) -> u64 { + self.batches[0].epoch_num + } + + /// Validates that the L1 origin hash matches the span's L1 origin check. + /// + /// Compares the first 20 bytes of the provided hash against the stored + /// `l1_origin_check` field. This provides a collision-resistant validation + /// that the span batch was derived from the expected L1 context. + /// + /// # Arguments + /// * `hash` - The full 32-byte L1 origin hash to validate + /// + /// # Returns + /// * `true` - If the first 20 bytes match the span's L1 origin check + /// * `false` - If there's a mismatch, indicating invalid L1 context + /// + /// # Algorithm + /// ```text + /// l1_origin_check[0..20] == hash[0..20] + /// ``` + /// + /// Using only 20 bytes provides strong collision resistance (2^160 space) + /// while saving 12 bytes per span compared to storing full hashes. + pub fn check_origin_hash(&self, hash: FixedBytes<32>) -> bool { + self.l1_origin_check == hash[..20] + } + + /// Validates that the parent hash matches the span's parent check. + /// + /// Compares the first 20 bytes of the provided hash against the stored + /// `parent_check` field. This ensures the span batch builds on the + /// expected parent block, maintaining chain continuity. + /// + /// # Arguments + /// * `hash` - The full 32-byte parent hash to validate + /// + /// # Returns + /// * `true` - If the first 20 bytes match the span's parent check + /// * `false` - If there's a mismatch, indicating discontinuity + /// + /// # Algorithm + /// ```text + /// parent_check[0..20] == hash[0..20] + /// ``` + /// + /// This validation is critical for maintaining the integrity of the L2 + /// chain and preventing insertion of span batches in wrong locations. + pub fn check_parent_hash(&self, hash: FixedBytes<32>) -> bool { + self.parent_check == hash[..20] + } + + /// Accesses the nth element from the end of the batch list. + /// + /// This is a convenience method for accessing recent elements in the span, + /// typically used during validation or processing algorithms that need to + /// examine the latest elements in the sequence. + /// + /// # Arguments + /// * `n` - Offset from the end (0 = last element, 1 = second-to-last, etc.) + /// + /// # Returns + /// Reference to the nth element from the end of the batch list + /// + /// # Panics + /// Panics if `n >= batches.len()`, i.e., if trying to access beyond + /// the available elements. + /// + /// # Algorithm + /// ```text + /// index = batches.len() - 1 - n + /// return &batches[index] + /// ``` + fn peek(&self, n: usize) -> &SpanBatchElement { + &self.batches[self.batches.len() - 1 - n] + } + + /// Converts this span batch to its raw serializable format. + /// + /// Transforms the derived span batch into a [`RawSpanBatch`] that can be + /// serialized and transmitted over the network. This involves organizing + /// the cached data into the proper prefix and payload structure. + /// + /// # Returns + /// * `Ok(RawSpanBatch)` - Successfully converted raw span batch + /// * `Err(SpanBatchError)` - Conversion failed, typically due to empty batch + /// + /// # Errors + /// Returns [`SpanBatchError::EmptySpanBatch`] if the span contains no blocks, + /// which is invalid as span batches must contain at least one block. + /// + /// # Algorithm + /// The conversion process: + /// 1. **Validation**: Ensure the span is not empty + /// 2. **Prefix Construction**: Build prefix with temporal and origin data + /// 3. **Payload Assembly**: Package cached data into payload structure + /// 4. **Relative Timestamp Calculation**: Convert absolute to relative timestamp + /// + /// The relative timestamp is calculated as: + /// ```text + /// rel_timestamp = first_block_timestamp - genesis_timestamp + /// ``` + /// + /// This enables efficient timestamp encoding in the serialized format. + pub fn to_raw_span_batch(&self) -> Result { + if self.batches.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + // These should never error since we check for an empty batch above. + let span_start = self.batches.first().ok_or(SpanBatchError::EmptySpanBatch)?; + let span_end = self.batches.last().ok_or(SpanBatchError::EmptySpanBatch)?; + + Ok(RawSpanBatch { + prefix: SpanBatchPrefix { + rel_timestamp: span_start.timestamp - self.genesis_timestamp, + l1_origin_num: span_end.epoch_num, + parent_check: self.parent_check, + l1_origin_check: self.l1_origin_check, + }, + payload: SpanBatchPayload { + block_count: self.batches.len() as u64, + origin_bits: self.origin_bits.clone(), + block_tx_counts: self.block_tx_counts.clone(), + txs: self.txs.clone(), + }, + }) + } + + /// Converts all [`SpanBatchElement`]s after the L2 safe head to [`SingleBatch`]es. The + /// resulting [`SingleBatch`]es do not contain a parent hash, as it is populated by the + /// Batch Queue stage. + pub fn get_singular_batches( + &self, + l1_origins: &[BlockInfo], + l2_safe_head: L2BlockInfo, + ) -> Result, SpanBatchError> { + let mut single_batches = Vec::with_capacity(self.batches.len()); + let mut origin_index = 0; + for batch in &self.batches { + if batch.timestamp <= l2_safe_head.block_info.timestamp { + continue; + } + // Overlapping span batches can pass the prefix checks but then the + // first batch after the safe head has an outdated L1 origin. + if batch.epoch_num < l2_safe_head.l1_origin.number { + return Err(SpanBatchError::L1OriginBeforeSafeHead); + } + let origin_epoch_hash = l1_origins[origin_index..l1_origins.len()] + .iter() + .enumerate() + .find(|(_, origin)| origin.number == batch.epoch_num) + .map(|(i, origin)| { + origin_index = i; + origin.hash + }) + .ok_or(SpanBatchError::MissingL1Origin)?; + let single_batch = SingleBatch { + epoch_num: batch.epoch_num, + epoch_hash: origin_epoch_hash, + timestamp: batch.timestamp, + transactions: batch.transactions.clone(), + ..Default::default() + }; + single_batches.push(single_batch); + } + Ok(single_batches) + } + + /// Append a [`SingleBatch`] to the [`SpanBatch`]. Updates the L1 origin check if need be. + pub fn append_singular_batch( + &mut self, + singular_batch: SingleBatch, + seq_num: u64, + ) -> Result<(), SpanBatchError> { + // If the new element is not ordered with respect to the last element, panic. + if !self.batches.is_empty() && self.peek(0).timestamp > singular_batch.timestamp { + panic!("Batch is not ordered"); + } + + let SingleBatch { epoch_hash, parent_hash, .. } = singular_batch; + + // Always append the new batch and set the L1 origin check. + self.batches.push(singular_batch.into()); + // Always update the L1 origin check. + self.l1_origin_check = epoch_hash[..20].try_into().expect("Sub-slice cannot fail"); + + let epoch_bit = if self.batches.len() == 1 { + // If there is only one batch, initialize the parent check and set the epoch bit based + // on the sequence number. + self.parent_check = parent_hash[..20].try_into().expect("Sub-slice cannot fail"); + seq_num == 0 + } else { + // If there is more than one batch, set the epoch bit based on the last two batches. + self.peek(1).epoch_num < self.peek(0).epoch_num + }; + + // Set the respective bit in the origin bits. + self.origin_bits.set_bit(self.batches.len() - 1, epoch_bit); + + let new_txs = self.peek(0).transactions.clone(); + + // Update the block tx counts cache with the latest batch's transaction count. + self.block_tx_counts.push(new_txs.len() as u64); + + // Add the new transactions to the transaction cache. + self.txs.add_txs(new_txs, self.chain_id) + } + + /// Checks if the span batch is valid. + pub async fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BV, + ) -> BatchValidity { + let (prefix_validity, parent_block) = + self.check_batch_prefix(cfg, l1_blocks, l2_safe_head, inclusion_block, fetcher).await; + if !matches!(prefix_validity, BatchValidity::Accept) { + return prefix_validity; + } + + let starting_epoch_num = self.starting_epoch_num(); + let parent_block = parent_block.expect("parent_block must be Some"); + + let mut origin_index = 0; + let mut origin_advanced = starting_epoch_num == parent_block.l1_origin.number + 1; + for (i, batch) in self.batches.iter().enumerate() { + let batch_timestamp = batch.timestamp; + let batch_epoch = batch.epoch_num; + + if batch_timestamp <= l2_safe_head.block_info.timestamp { + continue; + } + if batch_epoch < l2_safe_head.l1_origin.number { + warn!( + target: "batch_span", + "batch L1 origin is before safe head L1 origin, batch_epoch: {}, safe_head_epoch: {:?}", + batch_epoch, + l2_safe_head.l1_origin + ); + return BatchValidity::Drop; + } + + // Find the L1 origin for the batch. + let Some((offset, l1_origin)) = + l1_blocks[origin_index..].iter().enumerate().find(|(_, b)| batch_epoch == b.number) + else { + warn!( + target: "batch_span", + "unable to find L1 origin for batch, batch_epoch: {}, batch_timestamp: {}", + batch_epoch, + batch_timestamp + ); + return BatchValidity::Drop; + }; + origin_index += offset; + + if i > 0 { + origin_advanced = false; + if batch_epoch > self.batches[i - 1].epoch_num { + origin_advanced = true; + } + } + if batch_timestamp < l1_origin.timestamp { + warn!( + target: "batch_span", + "batch timestamp is less than L1 origin timestamp, l2_timestamp: {}, l1_timestamp: {}, origin: {:?}", + batch_timestamp, + l1_origin.timestamp, + l1_origin.id() + ); + return BatchValidity::Drop; + } + + // Check if we ran out of sequencer time drift + let max_drift = cfg.max_sequencer_drift(l1_origin.timestamp); + if batch_timestamp > l1_origin.timestamp + max_drift { + if batch.transactions.is_empty() { + // If the sequencer is co-operating by producing an empty batch, + // then allow the batch if it was the right thing to do to maintain the L2 time + // >= L1 time invariant. We only check batches that do not + // advance the epoch, to ensure epoch advancement regardless of time drift is + // allowed. + if !origin_advanced { + if origin_index + 1 >= l1_blocks.len() { + info!( + target: "batch_span", + "without the next L1 origin we cannot determine yet if this empty batch that exceeds the time drift is still valid" + ); + return BatchValidity::Undecided; + } + if batch_timestamp >= l1_blocks[origin_index + 1].timestamp { + // check if the next L1 origin could have been adopted + warn!( + target: "batch_span", + "batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid" + ); + return BatchValidity::Drop; + } else { + info!( + target: "batch_span", + "continuing with empty batch before late L1 block to preserve L2 time invariant" + ); + } + } + } else { + // If the sequencer is ignoring the time drift rule, then drop the batch and + // force an empty batch instead, as the sequencer is not + // allowed to include anything past this point without moving to the next epoch. + warn!( + target: "batch_span", + "batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: {}", + l1_origin.timestamp + max_drift + ); + return BatchValidity::Drop; + } + } + + // Check that the transactions are not empty and do not contain any deposits. + for (i, tx) in batch.transactions.iter().enumerate() { + if tx.is_empty() { + warn!( + target: "batch_span", + "transaction data must not be empty, but found empty tx, tx_index: {}", + i + ); + return BatchValidity::Drop; + } + if tx.as_ref().first() == Some(&(OpTxType::Deposit as u8)) { + warn!( + target: "batch_span", + "sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: {}", + i + ); + return BatchValidity::Drop; + } + + // If isthmus is not active yet and the transaction is a 7702, drop the batch. + if !cfg.is_isthmus_active(batch.timestamp) && + tx.as_ref().first() == Some(&(OpTxType::Eip7702 as u8)) + { + warn!(target: "batch_span", "EIP-7702 transactions are not supported pre-isthmus. tx_index: {}", i); + return BatchValidity::Drop; + } + } + } + + // Check overlapped blocks + let parent_num = parent_block.block_info.number; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + if self.starting_timestamp() < next_timestamp { + for i in 0..(l2_safe_head.block_info.number - parent_num) { + let safe_block_num = parent_num + i + 1; + let safe_block_payload = match fetcher.block_by_number(safe_block_num).await { + Ok(p) => p, + Err(e) => { + warn!(target: "batch_span", "failed to fetch block number {safe_block_num}: {e}"); + return BatchValidity::Undecided; + } + }; + let safe_block = &safe_block_payload.body; + let batch_txs = &self.batches[i as usize].transactions; + // Execution payload has deposit txs but batch does not. + let deposit_count: usize = safe_block + .transactions + .iter() + .map(|tx| if tx.is_deposit() { 1 } else { 0 }) + .sum(); + if safe_block.transactions.len() - deposit_count != batch_txs.len() { + warn!( + target: "batch_span", + "overlapped block's tx count does not match, safe_block_txs: {}, batch_txs: {}", + safe_block.transactions.len(), + batch_txs.len() + ); + return BatchValidity::Drop; + } + let batch_txs_len = batch_txs.len(); + #[allow(clippy::needless_range_loop)] + for j in 0..batch_txs_len { + let mut buf = Vec::new(); + safe_block.transactions[j + deposit_count].encode_2718(&mut buf); + if buf != batch_txs[j].0 { + warn!(target: "batch_span", "overlapped block's transaction does not match"); + return BatchValidity::Drop; + } + } + let safe_block_ref = match L2BlockInfo::from_block_and_genesis( + &safe_block_payload, + &cfg.genesis, + ) { + Ok(r) => r, + Err(e) => { + warn!( + target: "batch_span", + "failed to extract L2BlockInfo from execution payload, hash: {}, err: {e}", + safe_block_payload.header.hash_slow() + ); + return BatchValidity::Drop; + } + }; + if safe_block_ref.l1_origin.number != self.batches[i as usize].epoch_num { + warn!( + "overlapped block's L1 origin number does not match {}, {}", + safe_block_ref.l1_origin.number, self.batches[i as usize].epoch_num + ); + return BatchValidity::Drop; + } + } + } + + BatchValidity::Accept + } + + /// Checks the validity of the batch's prefix. + /// + /// This function is used for post-Holocene hardfork to perform batch validation + /// as each batch is being loaded in. + pub async fn check_batch_prefix( + &self, + cfg: &RollupConfig, + l1_origins: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BF, + ) -> (BatchValidity, Option) { + if l1_origins.is_empty() { + warn!(target: "batch_span", "missing L1 block input, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + if self.batches.is_empty() { + warn!(target: "batch_span", "empty span batch, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + + let epoch = l1_origins[0]; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + + let starting_epoch_num = self.starting_epoch_num(); + let mut batch_origin = epoch; + if starting_epoch_num == batch_origin.number + 1 { + if l1_origins.len() < 2 { + info!( + target: "batch_span", + "eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", + epoch.id() + ); + return (BatchValidity::Undecided, None); + } + batch_origin = l1_origins[1]; + } + if !cfg.is_delta_active(batch_origin.timestamp) { + warn!( + target: "batch_span", + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + batch_origin.id(), + batch_origin.timestamp + ); + return (BatchValidity::Drop, None); + } + + if self.starting_timestamp() > next_timestamp { + warn!( + target: "batch_span", + "received out-of-order batch for future processing after next batch ({} > {})", + self.starting_timestamp(), + next_timestamp + ); + + // After holocene is activated, gaps are disallowed. + if cfg.is_holocene_active(inclusion_block.timestamp) { + return (BatchValidity::Drop, None); + } + return (BatchValidity::Future, None); + } + + // Drop the batch if it has no new blocks after the safe head. + if self.final_timestamp() < next_timestamp { + warn!(target: "batch_span", "span batch has no new blocks after safe head"); + return if cfg.is_holocene_active(inclusion_block.timestamp) { + (BatchValidity::Past, None) + } else { + (BatchValidity::Drop, None) + }; + } + + // Find the parent block of the span batch. + // If the span batch does not overlap the current safe chain, parent block should be the L2 + // safe head. + let mut parent_num = l2_safe_head.block_info.number; + let mut parent_block = l2_safe_head; + if self.starting_timestamp() < next_timestamp { + if self.starting_timestamp() > l2_safe_head.block_info.timestamp { + // Batch timestamp cannot be between safe head and next timestamp. + warn!(target: "batch_span", "batch has misaligned timestamp, block time is too short"); + return (BatchValidity::Drop, None); + } + if !(l2_safe_head.block_info.timestamp - self.starting_timestamp()) + .is_multiple_of(cfg.block_time) + { + warn!(target: "batch_span", "batch has misaligned timestamp, not overlapped exactly"); + return (BatchValidity::Drop, None); + } + parent_num = l2_safe_head.block_info.number - + (l2_safe_head.block_info.timestamp - self.starting_timestamp()) / cfg.block_time - + 1; + parent_block = match fetcher.l2_block_info_by_number(parent_num).await { + Ok(block) => block, + Err(e) => { + warn!(target: "batch_span", "failed to fetch L2 block number {parent_num}: {e}"); + // Unable to validate the batch for now. Retry later. + return (BatchValidity::Undecided, None); + } + }; + } + if !self.check_parent_hash(parent_block.block_info.hash) { + warn!( + target: "batch_span", + "parent block mismatch, expected: {parent_num}, received: {}. parent hash: {}, parent hash check: {}", + parent_block.block_info.number, parent_block.block_info.hash, self.parent_check, + ); + return (BatchValidity::Drop, None); + } + + // Filter out batches that were included too late. + if starting_epoch_num + cfg.seq_window_size < inclusion_block.number { + warn!(target: "batch_span", "batch was included too late, sequence window expired"); + return (BatchValidity::Drop, None); + } + + // Check the L1 origin of the batch + if starting_epoch_num > parent_block.l1_origin.number + 1 { + warn!( + target: "batch_span", + "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: {} | next epoch: {}", + starting_epoch_num, + parent_block.l1_origin.number + 1 + ); + return (BatchValidity::Drop, None); + } + + // Verify the l1 origin hash for each l1 block. + // SAFETY: `Self::batches` is not empty, so the last element is guaranteed to exist. + let end_epoch_num = self.batches.last().unwrap().epoch_num; + let mut origin_checked = false; + // l1Blocks is supplied from batch queue and its length is limited to SequencerWindowSize. + for l1_block in l1_origins { + if l1_block.number == end_epoch_num { + if !self.check_origin_hash(l1_block.hash) { + warn!( + target: "batch_span", + l1_block_number = ?l1_block.number, + l1_block_hash = ?l1_block.hash, + l1_origin_number = ?starting_epoch_num, + l1_check_hash = ?self.l1_origin_check, + "batch is for different L1 chain, epoch hash does not match", + ); + return (BatchValidity::Drop, None); + } + origin_checked = true; + break; + } + } + if !origin_checked { + info!(target: "batch_span", "need more l1 blocks to check entire origins of span batch"); + return (BatchValidity::Undecided, None); + } + + if starting_epoch_num < parent_block.l1_origin.number { + warn!(target: "batch_span", "dropped batch, epoch is too old, minimum: {:?}", parent_block.block_info.id()); + return (BatchValidity::Drop, None); + } + + (BatchValidity::Accept, Some(parent_block)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{CollectingLayer, TestBatchValidator, TraceStorage}; + use alloc::vec; + use alloy_consensus::{Header, constants::EIP1559_TX_TYPE_ID}; + use alloy_eips::BlockNumHash; + use alloy_primitives::{B256, Bytes, b256}; + use kona_genesis::{ChainGenesis, HardForkConfig}; + use op_alloy_consensus::OpBlock; + use tracing::Level; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + fn gen_l1_blocks( + start_num: u64, + count: u64, + start_timestamp: u64, + interval: u64, + ) -> Vec { + (0..count) + .map(|i| BlockInfo { + number: start_num + i, + timestamp: start_timestamp + i * interval, + hash: B256::left_padding_from(&i.to_be_bytes()), + ..Default::default() + }) + .collect() + } + + #[test] + fn test_timestamp() { + let timestamp = 10; + let first_element = SpanBatchElement { timestamp, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, Default::default()], ..Default::default() }; + assert_eq!(batch.starting_timestamp(), timestamp); + } + + #[test] + fn test_starting_timestamp() { + let timestamp = 10; + let first_element = SpanBatchElement { timestamp, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, Default::default()], ..Default::default() }; + assert_eq!(batch.starting_timestamp(), timestamp); + } + + #[test] + fn test_final_timestamp() { + let timestamp = 10; + let last_element = SpanBatchElement { timestamp, ..Default::default() }; + let batch = + SpanBatch { batches: vec![Default::default(), last_element], ..Default::default() }; + assert_eq!(batch.final_timestamp(), timestamp); + } + + #[test] + fn test_starting_epoch_num() { + let epoch_num = 10; + let first_element = SpanBatchElement { epoch_num, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, Default::default()], ..Default::default() }; + assert_eq!(batch.starting_epoch_num(), epoch_num); + } + + #[test] + fn test_peek() { + let first_element = SpanBatchElement { epoch_num: 10, ..Default::default() }; + let second_element = SpanBatchElement { epoch_num: 11, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, second_element], ..Default::default() }; + assert_eq!(batch.peek(0).epoch_num, 11); + assert_eq!(batch.peek(1).epoch_num, 10); + } + + #[test] + fn test_append_empty_singular_batch() { + let mut batch = SpanBatch::default(); + let singular_batch = SingleBatch { + epoch_num: 10, + epoch_hash: FixedBytes::from([17u8; 32]), + parent_hash: FixedBytes::from([17u8; 32]), + timestamp: 10, + transactions: vec![], + }; + assert!(batch.append_singular_batch(singular_batch, 0).is_ok()); + assert_eq!(batch.batches.len(), 1); + assert_eq!(batch.origin_bits.get_bit(0), Some(1)); + assert_eq!(batch.block_tx_counts, vec![0]); + assert_eq!(batch.txs.tx_data.len(), 0); + + // Add another empty single batch. + let singular_batch = SingleBatch { + epoch_num: 11, + epoch_hash: FixedBytes::from([17u8; 32]), + parent_hash: FixedBytes::from([17u8; 32]), + timestamp: 20, + transactions: vec![], + }; + assert!(batch.append_singular_batch(singular_batch, 1).is_ok()); + } + + #[test] + fn test_check_origin_hash() { + let l1_origin_check = FixedBytes::from([17u8; 20]); + let hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let batch = SpanBatch { l1_origin_check, ..Default::default() }; + assert!(batch.check_origin_hash(hash)); + // This hash has 19 matching bytes, the other 13 are zeros. + let invalid = b256!("1111111111111111111111111111111111111100000000000000000000000000"); + assert!(!batch.check_origin_hash(invalid)); + } + + #[test] + fn test_check_parent_hash() { + let parent_check = FixedBytes::from([17u8; 20]); + let hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let batch = SpanBatch { parent_check, ..Default::default() }; + assert!(batch.check_parent_hash(hash)); + // This hash has 19 matching bytes, the other 13 are zeros. + let invalid = b256!("1111111111111111111111111111111111111100000000000000000000000000"); + assert!(!batch.check_parent_hash(invalid)); + } + + #[tokio::test] + async fn test_check_batch_missing_l1_block_input() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("missing L1 block input, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_check_batches_is_empty() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![BlockInfo::default()]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("empty span batch, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_singular_batches_outdated_l1_origin() { + let l1_block = BlockInfo { number: 10, timestamp: 20, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.get_singular_batches(&l1_blocks, l2_safe_head), + Err(SpanBatchError::L1OriginBeforeSafeHead), + ); + } + + #[tokio::test] + async fn test_singular_batches_missing_l1_origin() { + let l1_block = BlockInfo { number: 10, timestamp: 20, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 30, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.get_singular_batches(&l1_blocks, l2_safe_head), + Err(SpanBatchError::MissingL1Origin), + ); + } + + #[tokio::test] + async fn test_eager_block_missing_origins() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let block = BlockInfo { number: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", + block.id() + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_delta_inactive() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(10), ..Default::default() }, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + block.id(), + block.timestamp + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_out_of_order() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Future + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains( + "received out-of-order batch for future processing after next batch (21 > 20)" + )); + } + + #[tokio::test] + async fn test_check_batch_no_new_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("span batch has no new blocks after safe head")); + } + + #[tokio::test] + async fn test_check_batch_overlapping_blocks_tx_count_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + max_sequencer_drift: 1000, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, + l1_origin: BlockNumHash { number: 11, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator { + op_blocks: vec![OpBlock { + header: Header { number: 9, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }], + blocks: vec![ + L2BlockInfo { + block_info: BlockInfo { number: 8, timestamp: 0, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }, + L2BlockInfo { + block_info: BlockInfo { number: 9, timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }, + L2BlockInfo { + block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, + l1_origin: BlockNumHash { number: 11, ..Default::default() }, + ..Default::default() + }, + ], + ..Default::default() + }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 10, + transactions: vec![Bytes(vec![EIP1559_TX_TYPE_ID].into())], + }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 60, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains( + "overlapped block's tx count does not match, safe_block_txs: 0, batch_txs: 1" + )); + } + + #[tokio::test] + async fn test_check_batch_overlapping_blocks_tx_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + max_sequencer_drift: 1000, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, + l1_origin: BlockNumHash { number: 11, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator { + op_blocks: vec![OpBlock { + header: Header { number: 9, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: vec![op_alloy_consensus::OpTxEnvelope::Eip1559( + alloy_consensus::Signed::new_unchecked( + alloy_consensus::TxEip1559 { + chain_id: 0, + nonce: 0, + gas_limit: 2, + max_fee_per_gas: 1, + max_priority_fee_per_gas: 1, + to: alloy_primitives::TxKind::Create, + value: alloy_primitives::U256::from(3), + ..Default::default() + }, + alloy_primitives::Signature::test_signature(), + alloy_primitives::B256::ZERO, + ), + )], + ommers: Vec::new(), + withdrawals: None, + }, + }], + blocks: vec![ + L2BlockInfo { + block_info: BlockInfo { number: 8, timestamp: 0, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }, + L2BlockInfo { + block_info: BlockInfo { number: 9, timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }, + L2BlockInfo { + block_info: BlockInfo { number: 10, timestamp: 20, ..Default::default() }, + l1_origin: BlockNumHash { number: 11, ..Default::default() }, + ..Default::default() + }, + ], + ..Default::default() + }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 10, + transactions: vec![Bytes(vec![EIP1559_TX_TYPE_ID].into())], + }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 60, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("overlapped block's transaction does not match")); + } + + #[tokio::test] + async fn test_check_batch_block_timestamp_lt_l1_origin() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block = BlockInfo { number: 10, timestamp: 20, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 19, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second, third], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "batch timestamp is less than L1 origin timestamp, l2_timestamp: 19, l1_timestamp: 20, origin: {:?}", + l1_block.id(), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_misaligned_timestamp() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 11, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, block time is too short")); + } + + #[tokio::test] + async fn test_check_batch_misaligned_without_overlap() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 8, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, not overlapped exactly")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_l2_block() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher: TestBatchValidator = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch L2 block number 40: Block not found")); + } + + #[tokio::test] + async fn test_check_batch_parent_hash_fail() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + fetcher.short_circuit = true; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice( + &b256!("1111111111111111111111111111111111111111000000000000000000000000")[..20], + ), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("parent block mismatch, expected: 40, received: 41")); + } + + #[tokio::test] + async fn test_check_sequence_window_expired() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch was included too late, sequence window expired")); + } + + #[tokio::test] + async fn test_starting_epoch_too_far_ahead() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: 10 | next epoch: 9"; + assert!(logs[0].contains(str)); + } + + #[tokio::test] + async fn test_check_batch_epoch_hash_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = "batch is for different L1 chain, epoch hash does not match".to_string(); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_need_more_l1_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 10, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("need more l1 blocks to check entire origins of span batch")); + } + + #[tokio::test] + async fn test_drop_batch_epoch_too_old() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 13, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 14, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "dropped batch, epoch is too old, minimum: {:?}", + l2_block.block_info.id(), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_exceeds_max_seq_drif() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 10, 0); + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 40, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[1].hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid")); + } + + #[tokio::test] + async fn test_continuing_with_empty_batch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default() + .with(layer) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + // Create two L1 blocks with number,timestamp: (10,10) and (11,40) so that the second batch + // in the span batch is valid even though it doesn't advance the origin, because its + // timestamp is 30 < 40. Then the third batch advances the origin to L1 block 11 + // with timestamp 40, which is also the third batch's timestamp. + let l1_blocks = gen_l1_blocks(10, 2, 10, 30); + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, transactions: vec![] }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 30, transactions: vec![] }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 40, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[1].hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + let infos = trace_store.get_by_level(Level::INFO); + assert_eq!(infos.len(), 1); + assert!(infos[0].contains( + "continuing with empty batch before late L1 block to preserve L2 time invariant" + )); + } + + #[tokio::test] + async fn test_check_batch_exceeds_sequencer_time_drift() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 10, 0); + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { + epoch_num: 11, + timestamp: 20, + transactions: vec![Default::default()], + }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: 10")); + } + + #[tokio::test] + async fn test_check_batch_empty_txs() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let l1_a = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_b = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_c = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![l1_a, l1_b, l1_c]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("transaction data must not be empty, but found empty tx")); + } + + #[tokio::test] + async fn test_check_batch_with_deposit_tx() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let filler_bytes = Bytes::copy_from_slice(&[EIP1559_TX_TYPE_ID]); + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![filler_bytes.clone()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Bytes::copy_from_slice(&[OpTxType::Deposit as u8])], + }; + let third = + SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![filler_bytes] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: 0")); + } + + #[tokio::test] + async fn test_check_batch_with_eip7702_tx() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_blocks = gen_l1_blocks(9, 3, 0, 10); + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let filler_bytes = Bytes::copy_from_slice(&[EIP1559_TX_TYPE_ID]); + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![filler_bytes.clone()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Bytes::copy_from_slice(&[alloy_consensus::TxType::Eip7702 as u8])], + }; + let third = + SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![filler_bytes] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_blocks[0].hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!( + logs[0].contains("EIP-7702 transactions are not supported pre-isthmus. tx_index: 0") + ); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_payload() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher: TestBatchValidator = + TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch block number 41: L2 Block not found")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_extract_l2_block_info() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher: TestBatchValidator = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "failed to extract L2BlockInfo from execution payload, hash: {:?}", + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_overlapped_blocks_origin_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher: TestBatchValidator = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("overlapped block's L1 origin number does not match")); + } + + #[tokio::test] + async fn test_overlapped_blocks_origin_outdated() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 40, hash: parent_hash }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let l1_block = + BlockInfo { number: 10, timestamp: 5, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![l1_block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: l1_block.id(), + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_parent = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 0, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher: TestBatchValidator = TestBatchValidator { + blocks: vec![l2_parent], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 9, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 9, timestamp: 20, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 10, timestamp: 30, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch L1 origin is before safe head L1 origin")); + } + + #[tokio::test] + async fn test_check_batch_valid_with_genesis_epoch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + hardforks: HardForkConfig { delta_time: Some(0), ..Default::default() }, + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + l1: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher: TestBatchValidator = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + assert!(trace_store.is_empty()); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/testdata/raw_batch.hex b/kona/crates/protocol/protocol/src/batch/testdata/raw_batch.hex new file mode 100644 index 0000000000000..311a7b99981d2 Binary files /dev/null and b/kona/crates/protocol/protocol/src/batch/testdata/raw_batch.hex differ diff --git a/kona/crates/protocol/protocol/src/batch/traits.rs b/kona/crates/protocol/protocol/src/batch/traits.rs new file mode 100644 index 0000000000000..19307084202f7 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/traits.rs @@ -0,0 +1,25 @@ +//! Traits for working with protocol types. + +use alloc::boxed::Box; +use async_trait::async_trait; +use core::fmt::Display; +use op_alloy_consensus::OpBlock; + +use crate::L2BlockInfo; + +/// Describes the functionality of a data source that fetches safe blocks. +#[async_trait] +pub trait BatchValidationProvider { + /// The error type for the [`BatchValidationProvider`]. + type Error: Display; + + /// Returns the [`L2BlockInfo`] given a block number. + /// + /// Errors if the block does not exist. + async fn l2_block_info_by_number(&mut self, number: u64) -> Result; + + /// Returns the [`OpBlock`] for a given number. + /// + /// Errors if no block is available for the given block number. + async fn block_by_number(&mut self, number: u64) -> Result; +} diff --git a/kona/crates/protocol/protocol/src/batch/transactions.rs b/kona/crates/protocol/protocol/src/batch/transactions.rs new file mode 100644 index 0000000000000..852bcf8f07097 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/transactions.rs @@ -0,0 +1,439 @@ +//! This module contains the [`SpanBatchTransactions`] type and logic for encoding and decoding +//! transactions in a span batch. + +use crate::{ + MAX_SPAN_BATCH_ELEMENTS, SpanBatchBits, SpanBatchError, SpanBatchTransactionData, + SpanDecodingError, read_tx_data, +}; +use alloc::vec::Vec; +use alloy_consensus::{Transaction, TxEnvelope, TxType}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, Bytes, Signature, U256, bytes}; +use alloy_rlp::{Buf, Decodable, Encodable}; + +/// This struct contains the decoded information for transactions in a span batch. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchTransactions { + /// The total number of transactions in a span batch. Must be manually set. + pub total_block_tx_count: u64, + /// The contract creation bits, standard span-batch bitlist. + pub contract_creation_bits: SpanBatchBits, + /// The transaction signatures. + pub tx_sigs: Vec, + /// The transaction nonces + pub tx_nonces: Vec, + /// The transaction gas limits. + pub tx_gases: Vec, + /// The `to` addresses of the transactions. + pub tx_tos: Vec
, + /// The transaction data. + pub tx_data: Vec>, + /// The protected bits, standard span-batch bitlist. + pub protected_bits: SpanBatchBits, + /// The types of the transactions. + pub tx_types: Vec, + /// Total legacy transaction count in the span batch. + pub legacy_tx_count: u64, +} + +impl SpanBatchTransactions { + /// Encodes the [`SpanBatchTransactions`] into a writer. + pub fn encode(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + self.encode_contract_creation_bits(w)?; + self.encode_tx_sigs(w)?; + self.encode_tx_tos(w)?; + self.encode_tx_data(w)?; + self.encode_tx_nonces(w)?; + self.encode_tx_gases(w)?; + self.encode_protected_bits(w)?; + Ok(()) + } + + /// Decodes the [`SpanBatchTransactions`] from a reader. + pub fn decode(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + self.decode_contract_creation_bits(r)?; + self.decode_tx_sigs(r)?; + self.decode_tx_tos(r)?; + self.decode_tx_data(r)?; + self.decode_tx_nonces(r)?; + self.decode_tx_gases(r)?; + self.decode_protected_bits(r)?; + Ok(()) + } + + /// Encode the contract creation bits into a writer. + pub fn encode_contract_creation_bits( + &self, + w: &mut dyn bytes::BufMut, + ) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.total_block_tx_count as usize, &self.contract_creation_bits)?; + Ok(()) + } + + /// Encode the protected bits into a writer. + pub fn encode_protected_bits(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.legacy_tx_count as usize, &self.protected_bits)?; + Ok(()) + } + + /// Encode the transaction signatures into a writer (excluding `v` field). + pub fn encode_tx_sigs(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + let mut y_parity_bits = SpanBatchBits::default(); + for (i, sig) in self.tx_sigs.iter().enumerate() { + y_parity_bits.set_bit(i, sig.v()); + } + + SpanBatchBits::encode(w, self.total_block_tx_count as usize, &y_parity_bits)?; + for sig in &self.tx_sigs { + w.put_slice(&sig.r().to_be_bytes::<32>()); + w.put_slice(&sig.s().to_be_bytes::<32>()); + } + Ok(()) + } + + /// Encode the transaction nonces into a writer. + pub fn encode_tx_nonces(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + let mut buf = [0u8; 10]; + for nonce in &self.tx_nonces { + let slice = unsigned_varint::encode::u64(*nonce, &mut buf); + w.put_slice(slice); + } + Ok(()) + } + + /// Encode the transaction gas limits into a writer. + pub fn encode_tx_gases(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + let mut buf = [0u8; 10]; + for gas in &self.tx_gases { + let slice = unsigned_varint::encode::u64(*gas, &mut buf); + w.put_slice(slice); + } + Ok(()) + } + + /// Encode the `to` addresses of the transactions into a writer. + pub fn encode_tx_tos(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + for to in &self.tx_tos { + w.put_slice(to.as_ref()); + } + Ok(()) + } + + /// Encode the transaction data into a writer. + pub fn encode_tx_data(&self, w: &mut dyn bytes::BufMut) -> Result<(), SpanBatchError> { + for data in &self.tx_data { + w.put_slice(data); + } + Ok(()) + } + + /// Decode the contract creation bits from a reader. + pub fn decode_contract_creation_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.total_block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.contract_creation_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; + Ok(()) + } + + /// Decode the protected bits from a reader. + pub fn decode_protected_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.legacy_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.protected_bits = SpanBatchBits::decode(r, self.legacy_tx_count as usize)?; + Ok(()) + } + + /// Decode the transaction signatures from a reader (excluding `v` field). + pub fn decode_tx_sigs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let y_parity_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; + let mut sigs = Vec::with_capacity(self.total_block_tx_count as usize); + for i in 0..self.total_block_tx_count { + let y_parity = y_parity_bits.get_bit(i as usize).expect("same length"); + let r_val = U256::from_be_slice(&r[..32]); + let s_val = U256::from_be_slice(&r[32..64]); + sigs.push(Signature::new(r_val, s_val, y_parity == 1)); + r.advance(64); + } + self.tx_sigs = sigs; + Ok(()) + } + + /// Decode the transaction nonces from a reader. + pub fn decode_tx_nonces(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut nonces = Vec::with_capacity(self.total_block_tx_count as usize); + for _ in 0..self.total_block_tx_count { + let (nonce, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::TxNonces))?; + nonces.push(nonce); + *r = remaining; + } + self.tx_nonces = nonces; + Ok(()) + } + + /// Decode the transaction gas limits from a reader. + pub fn decode_tx_gases(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut gases = Vec::with_capacity(self.total_block_tx_count as usize); + for _ in 0..self.total_block_tx_count { + let (gas, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::TxNonces))?; + gases.push(gas); + *r = remaining; + } + self.tx_gases = gases; + Ok(()) + } + + /// Decode the `to` addresses of the transactions from a reader. + pub fn decode_tx_tos(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut tos = Vec::with_capacity(self.total_block_tx_count as usize); + let contract_creation_count = self.contract_creation_count(); + for _ in 0..(self.total_block_tx_count - contract_creation_count) { + let to = Address::from_slice(&r[..20]); + tos.push(to); + r.advance(20); + } + self.tx_tos = tos; + Ok(()) + } + + /// Decode the transaction data from a reader. + pub fn decode_tx_data(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut tx_data = Vec::new(); + let mut tx_types = Vec::new(); + + // Do not need the transaction data header because the RLP stream already includes the + // length information. + for _ in 0..self.total_block_tx_count { + let (tx_data_item, tx_type) = read_tx_data(r)?; + tx_data.push(tx_data_item); + tx_types.push(tx_type); + if matches!(tx_type, TxType::Legacy) { + self.legacy_tx_count += 1; + } + } + + self.tx_data = tx_data; + self.tx_types = tx_types; + + Ok(()) + } + + /// Returns the number of contract creation transactions in the span batch. + pub fn contract_creation_count(&self) -> u64 { + self.contract_creation_bits.as_ref().iter().map(|b| b.count_ones() as u64).sum() + } + + /// Retrieve all of the raw transactions from the [`SpanBatchTransactions`]. + pub fn full_txs(&self, chain_id: u64) -> Result>, SpanBatchError> { + let mut txs = Vec::new(); + let mut to_idx = 0; + let mut protected_bit_idx = 0; + for idx in 0..self.total_block_tx_count { + let mut data = self.tx_data[idx as usize].as_slice(); + let tx = SpanBatchTransactionData::decode(&mut data) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let nonce = self + .tx_nonces + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let gas = self + .tx_gases + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let bit = self + .contract_creation_bits + .get_bit(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let to = if bit == 0 { + if self.tx_tos.len() <= to_idx { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + to_idx += 1; + Some(self.tx_tos[to_idx - 1]) + } else { + None + }; + let sig = *self + .tx_sigs + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let is_protected = if tx.tx_type() == TxType::Legacy { + protected_bit_idx += 1; + self.protected_bits.get_bit(protected_bit_idx - 1).unwrap_or_default() == 1 + } else { + true + }; + let tx_envelope = tx.to_signed_tx(*nonce, *gas, to, chain_id, sig, is_protected)?; + let mut buf = Vec::new(); + tx_envelope.encode_2718(&mut buf); + txs.push(buf); + } + Ok(txs) + } + + /// Add raw transactions into the [`SpanBatchTransactions`]. + pub fn add_txs(&mut self, txs: Vec, chain_id: u64) -> Result<(), SpanBatchError> { + let total_block_tx_count = txs.len() as u64; + let offset = self.total_block_tx_count; + + for i in 0..total_block_tx_count { + let tx_enveloped = TxEnvelope::decode(&mut txs[i as usize].as_ref()) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let span_batch_tx = SpanBatchTransactionData::try_from(&tx_enveloped)?; + + let tx_type = tx_enveloped.tx_type(); + if matches!(tx_type, TxType::Legacy) { + let protected_bit = tx_enveloped.is_replay_protected(); + self.protected_bits.set_bit(self.legacy_tx_count as usize, protected_bit); + self.legacy_tx_count += 1; + } + + let (signature, to, nonce, gas, tx_chain_id) = match &tx_enveloped { + TxEnvelope::Legacy(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + TxEnvelope::Eip2930(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + TxEnvelope::Eip1559(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + TxEnvelope::Eip7702(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + _ => { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + }; + + if tx_enveloped.is_replay_protected() && + tx_chain_id + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))? != + chain_id + { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + let contract_creation_bit = match to { + Some(address) => { + self.tx_tos.push(address); + 0 + } + None => 1, + }; + let mut tx_data_buf = Vec::new(); + span_batch_tx.encode(&mut tx_data_buf); + + self.tx_sigs.push(*signature); + self.contract_creation_bits.set_bit((i + offset) as usize, contract_creation_bit == 1); + self.tx_nonces.push(nonce); + self.tx_data.push(tx_data_buf); + self.tx_gases.push(gas); + self.tx_types.push(tx_type); + } + self.total_block_tx_count += total_block_tx_count; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxEip7702}; + use alloy_primitives::{Signature, TxKind, address}; + + #[test] + fn test_span_batch_transactions_add_empty_txs() { + let mut span_batch_txs = SpanBatchTransactions::default(); + let txs = vec![]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert!(result.is_ok()); + assert_eq!(span_batch_txs.total_block_tx_count, 0); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx_wrong_chain_id() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); + assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } + + #[test] + fn test_span_batch_transactions_add_eip1559_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } + + #[test] + fn test_span_batch_transactions_add_eip7702_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip7702(Signed::new_unchecked( + TxEip7702 { to, chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx.rs b/kona/crates/protocol/protocol/src/batch/tx.rs new file mode 100644 index 0000000000000..a761c1fd82cc2 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx.rs @@ -0,0 +1,55 @@ +//! Transaction Types + +use crate::Frame; +use alloc::vec::Vec; +use alloy_primitives::Bytes; + +/// BatchTransaction is a set of [`Frame`]s that can be [Into::into] [`Bytes`]. +/// if the size exceeds the desired threshold. +#[derive(Debug, Clone)] +pub struct BatchTransaction { + /// The frames in the batch. + pub frames: Vec, + /// The size of the potential transaction. + pub size: usize, +} + +impl BatchTransaction { + /// Returns the size of the transaction. + pub const fn size(&self) -> usize { + self.size + } + + /// Returns if the transaction has reached the max frame count. + pub const fn is_full(&self, max_frames: u16) -> bool { + self.frames.len() as u16 >= max_frames + } + + /// Returns the [`BatchTransaction`] as a [`Bytes`]. + pub fn to_bytes(&self) -> Bytes { + self.frames + .iter() + .fold(Vec::new(), |mut acc, frame| { + acc.append(&mut frame.encode()); + acc + }) + .into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec; + + #[test] + fn test_batch_transaction() { + let frame = Frame { id: [0xFF; 16], number: 0xEE, data: vec![0xDD; 50], is_last: true }; + let batch = BatchTransaction { frames: vec![frame.clone(); 5], size: 5 * frame.size() }; + let bytes: Bytes = batch.to_bytes(); + let bytes = + [crate::DERIVATION_VERSION_0].iter().chain(bytes.iter()).copied().collect::>(); + let frames = Frame::parse_frames(&bytes).unwrap(); + assert_eq!(frames, vec![frame; 5]); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/eip1559.rs b/kona/crates/protocol/protocol/src/batch/tx_data/eip1559.rs new file mode 100644 index 0000000000000..f35569c73bdf7 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/eip1559.rs @@ -0,0 +1,84 @@ +//! This module contains the eip1559 transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxEip1559}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for an EIP-1559 transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchEip1559TransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// Maximum priority fee per gas. + pub max_priority_fee_per_gas: U256, + /// Maximum fee per gas. + pub max_fee_per_gas: U256, + /// Transaction calldata. + pub data: Bytes, + /// Access list, used to pre-warm storage slots through static declaration. + pub access_list: AccessList, +} + +impl SpanBatchEip1559TransactionData { + /// Converts [`SpanBatchEip1559TransactionData`] into a signed [`TxEip1559`]. + pub fn to_signed_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result, SpanBatchError> { + let eip1559_tx = TxEip1559 { + chain_id, + nonce, + max_fee_per_gas: u128::from_be_bytes( + self.max_fee_per_gas.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + max_priority_fee_per_gas: u128::from_be_bytes( + self.max_priority_fee_per_gas.to_be_bytes::<32>()[16..].try_into().map_err( + |_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData), + )?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + access_list: self.access_list.clone(), + }; + let signature_hash = eip1559_tx.signature_hash(); + Ok(Signed::new_unchecked(eip1559_tx, signature, signature_hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn encode_eip1559_tx_data_roundtrip() { + let variable_fee_tx = SpanBatchEip1559TransactionData { + value: U256::from(0xFF), + max_fee_per_gas: U256::from(0xEE), + max_priority_fee_per_gas: U256::from(0xDD), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + }; + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Eip1559(variable_fee_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Eip1559(variable_fee_decoded) = decoded else { + panic!("Expected SpanBatchEip1559TransactionData, got {decoded:?}"); + }; + + assert_eq!(variable_fee_tx, variable_fee_decoded); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/eip2930.rs b/kona/crates/protocol/protocol/src/batch/tx_data/eip2930.rs new file mode 100644 index 0000000000000..0beb74b5f5c26 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/eip2930.rs @@ -0,0 +1,76 @@ +//! This module contains the eip2930 transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxEip2930}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for an EIP-2930 transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchEip2930TransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// The gas price of the transaction. + pub gas_price: U256, + /// Transaction calldata. + pub data: Bytes, + /// Access list, used to pre-warm storage slots through static declaration. + pub access_list: AccessList, +} + +impl SpanBatchEip2930TransactionData { + /// Converts [`SpanBatchEip2930TransactionData`] into a signed [`TxEip2930`]. + pub fn to_signed_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result, SpanBatchError> { + let access_list_tx = TxEip2930 { + chain_id, + nonce, + gas_price: u128::from_be_bytes( + self.gas_price.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + access_list: self.access_list.clone(), + }; + let signature_hash = access_list_tx.signature_hash(); + Ok(Signed::new_unchecked(access_list_tx, signature, signature_hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn encode_eip2930_tx_data_roundtrip() { + let access_list_tx = SpanBatchEip2930TransactionData { + value: U256::from(0xFF), + gas_price: U256::from(0xEE), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + }; + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Eip2930(access_list_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Eip2930(access_list_decoded) = decoded else { + panic!("Expected SpanBatchEip2930TransactionData, got {decoded:?}"); + }; + + assert_eq!(access_list_tx, access_list_decoded); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/eip7702.rs b/kona/crates/protocol/protocol/src/batch/tx_data/eip7702.rs new file mode 100644 index 0000000000000..9fb37b086eade --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/eip7702.rs @@ -0,0 +1,128 @@ +//! This module contains the eip7702 transaction data type for a span batch. + +use crate::SpanBatchError; +use alloc::vec::Vec; +use alloy_consensus::{SignableTransaction, Signed, TxEip7702}; +use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization}; +use alloy_primitives::{Address, Signature, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for an EIP-7702 transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchEip7702TransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// Maximum priority fee per gas. + pub max_priority_fee_per_gas: U256, + /// Maximum fee per gas. + pub max_fee_per_gas: U256, + /// Transaction calldata. + pub data: Bytes, + /// Access list, used to pre-warm storage slots through static declaration. + pub access_list: AccessList, + /// Authorization list, used to allow a signer to delegate code to a contract + pub authorization_list: Vec, +} + +impl SpanBatchEip7702TransactionData { + /// Converts [`SpanBatchEip7702TransactionData`] into a signed [`TxEip7702`]. + pub fn to_signed_tx( + &self, + nonce: u64, + gas: u64, + to: Address, + chain_id: u64, + signature: Signature, + ) -> Result, SpanBatchError> { + // SAFETY: A U256 as be bytes is always 32 bytes long. + let mut max_fee_per_gas = [0u8; 16]; + max_fee_per_gas.copy_from_slice(&self.max_fee_per_gas.to_be_bytes::<32>()[16..]); + let max_fee_per_gas = u128::from_be_bytes(max_fee_per_gas); + + // SAFETY: A U256 as be bytes is always 32 bytes long. + let mut max_priority_fee_per_gas = [0u8; 16]; + max_priority_fee_per_gas + .copy_from_slice(&self.max_priority_fee_per_gas.to_be_bytes::<32>()[16..]); + let max_priority_fee_per_gas = u128::from_be_bytes(max_priority_fee_per_gas); + + let eip7702_tx = TxEip7702 { + chain_id, + nonce, + max_fee_per_gas, + max_priority_fee_per_gas, + gas_limit: gas, + to, + value: self.value, + input: self.data.clone().into(), + access_list: self.access_list.clone(), + authorization_list: self.authorization_list.clone(), + }; + let signature_hash = eip7702_tx.signature_hash(); + Ok(Signed::new_unchecked(eip7702_tx, signature, signature_hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::{vec, vec::Vec}; + use alloy_rlp::{Decodable, Encodable}; + use alloy_rpc_types_eth::Authorization; + + #[test] + fn test_eip7702_to_signed_tx() { + let authorization = Authorization { + chain_id: U256::from(0x01), + address: Address::left_padding_from(&[0x01, 0x02, 0x03]), + nonce: 2, + }; + let signature = Signature::test_signature(); + let arb_authorization: SignedAuthorization = authorization.into_signed(signature); + + let variable_fee_tx = SpanBatchEip7702TransactionData { + value: U256::from(0xFF), + max_fee_per_gas: U256::from(0xEE), + max_priority_fee_per_gas: U256::from(0xDD), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + authorization_list: vec![arb_authorization], + }; + + let signed_tx = variable_fee_tx + .to_signed_tx(0, 0, Address::ZERO, 0, Signature::test_signature()) + .unwrap(); + + assert_eq!(*signed_tx.signature(), Signature::test_signature()); + } + + #[test] + fn encode_eip7702_tx_data_roundtrip() { + let authorization = Authorization { + chain_id: U256::from(0x01), + address: Address::left_padding_from(&[0x01, 0x02, 0x03]), + nonce: 2, + }; + let signature = Signature::test_signature(); + let arb_authorization: SignedAuthorization = authorization.into_signed(signature); + + let variable_fee_tx = SpanBatchEip7702TransactionData { + value: U256::from(0xFF), + max_fee_per_gas: U256::from(0xEE), + max_priority_fee_per_gas: U256::from(0xDD), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + authorization_list: vec![arb_authorization], + }; + + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Eip7702(variable_fee_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Eip7702(variable_fee_decoded) = decoded else { + panic!("Expected SpanBatchEip7702TransactionData, got {decoded:?}"); + }; + + assert_eq!(variable_fee_tx, variable_fee_decoded); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/legacy.rs b/kona/crates/protocol/protocol/src/batch/tx_data/legacy.rs new file mode 100644 index 0000000000000..b167341d13630 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/legacy.rs @@ -0,0 +1,73 @@ +//! This module contains the legacy transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for a legacy transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchLegacyTransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// The gas price of the transaction. + pub gas_price: U256, + /// Transaction calldata. + pub data: Bytes, +} + +impl SpanBatchLegacyTransactionData { + /// Converts [`SpanBatchLegacyTransactionData`] into a signed [`TxLegacy`]. + pub fn to_signed_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + is_protected: bool, + ) -> Result, SpanBatchError> { + let legacy_tx = TxLegacy { + chain_id: is_protected.then_some(chain_id), + nonce, + gas_price: u128::from_be_bytes( + self.gas_price.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + }; + let signature_hash = legacy_tx.signature_hash(); + Ok(Signed::new_unchecked(legacy_tx, signature, signature_hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable as _}; + + #[test] + fn encode_legacy_tx_data_roundtrip() { + let legacy_tx = SpanBatchLegacyTransactionData { + value: U256::from(0xFF), + gas_price: U256::from(0xEE), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + }; + + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Legacy(legacy_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Legacy(legacy_decoded) = decoded else { + panic!("Expected SpanBatchLegacyTransactionData, got {decoded:?}"); + }; + + assert_eq!(legacy_tx, legacy_decoded); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/mod.rs b/kona/crates/protocol/protocol/src/batch/tx_data/mod.rs new file mode 100644 index 0000000000000..ee39cc22dbde1 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/mod.rs @@ -0,0 +1,16 @@ +//! Contains all the Span Batch Transaction Data types. + +mod wrapper; +pub use wrapper::SpanBatchTransactionData; + +mod legacy; +pub use legacy::SpanBatchLegacyTransactionData; + +mod eip1559; +pub use eip1559::SpanBatchEip1559TransactionData; + +mod eip2930; +pub use eip2930::SpanBatchEip2930TransactionData; + +mod eip7702; +pub use eip7702::SpanBatchEip7702TransactionData; diff --git a/kona/crates/protocol/protocol/src/batch/tx_data/wrapper.rs b/kona/crates/protocol/protocol/src/batch/tx_data/wrapper.rs new file mode 100644 index 0000000000000..23a08b07e12f0 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/tx_data/wrapper.rs @@ -0,0 +1,171 @@ +//! This module contains the top level span batch transaction data type. + +use alloy_consensus::{Transaction, TxEnvelope, TxType}; +use alloy_primitives::{Address, Signature, U256}; +use alloy_rlp::{Bytes, Decodable, Encodable}; + +use crate::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchEip7702TransactionData, SpanBatchError, SpanBatchLegacyTransactionData, + SpanDecodingError, +}; + +/// The typed transaction data for a transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanBatchTransactionData { + /// Legacy transaction data. + Legacy(SpanBatchLegacyTransactionData), + /// EIP-2930 transaction data. + Eip2930(SpanBatchEip2930TransactionData), + /// EIP-1559 transaction data. + Eip1559(SpanBatchEip1559TransactionData), + /// EIP-7702 transaction data. + Eip7702(SpanBatchEip7702TransactionData), +} + +impl Encodable for SpanBatchTransactionData { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + Self::Legacy(data) => { + data.encode(out); + } + Self::Eip2930(data) => { + out.put_u8(TxType::Eip2930 as u8); + data.encode(out); + } + Self::Eip1559(data) => { + out.put_u8(TxType::Eip1559 as u8); + data.encode(out); + } + Self::Eip7702(data) => { + out.put_u8(TxType::Eip7702 as u8); + data.encode(out); + } + } + } +} + +impl Decodable for SpanBatchTransactionData { + fn decode(r: &mut &[u8]) -> Result { + if !r.is_empty() && r[0] > 0x7F { + // Legacy transaction + return Ok(Self::Legacy(SpanBatchLegacyTransactionData::decode(r)?)); + } + // Non-legacy transaction (EIP-2718 envelope encoding) + Self::decode_typed(r) + } +} + +impl TryFrom<&TxEnvelope> for SpanBatchTransactionData { + type Error = SpanBatchError; + + fn try_from(tx_envelope: &TxEnvelope) -> Result { + match tx_envelope { + TxEnvelope::Legacy(s) => { + let s = s.tx(); + Ok(Self::Legacy(SpanBatchLegacyTransactionData { + value: s.value, + gas_price: U256::from(s.gas_price), + data: Bytes::from(s.input().to_vec()), + })) + } + TxEnvelope::Eip2930(s) => { + let s = s.tx(); + Ok(Self::Eip2930(SpanBatchEip2930TransactionData { + value: s.value, + gas_price: U256::from(s.gas_price), + data: Bytes::from(s.input().to_vec()), + access_list: s.access_list.clone(), + })) + } + TxEnvelope::Eip1559(s) => { + let s = s.tx(); + Ok(Self::Eip1559(SpanBatchEip1559TransactionData { + value: s.value, + max_fee_per_gas: U256::from(s.max_fee_per_gas), + max_priority_fee_per_gas: U256::from(s.max_priority_fee_per_gas), + data: Bytes::from(s.input().to_vec()), + access_list: s.access_list.clone(), + })) + } + TxEnvelope::Eip7702(s) => { + let s = s.tx(); + Ok(Self::Eip7702(SpanBatchEip7702TransactionData { + value: s.value, + max_fee_per_gas: U256::from(s.max_fee_per_gas), + max_priority_fee_per_gas: U256::from(s.max_priority_fee_per_gas), + data: Bytes::from(s.input().to_vec()), + access_list: s.access_list.clone(), + authorization_list: s.authorization_list.clone(), + })) + } + _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), + } + } +} + +impl SpanBatchTransactionData { + /// Returns the transaction type of the [`SpanBatchTransactionData`]. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + Self::Eip7702(_) => TxType::Eip7702, + } + } + + /// Decodes a typed transaction into a [`SpanBatchTransactionData`] from a byte slice. + pub fn decode_typed(b: &[u8]) -> Result { + if b.len() <= 1 { + return Err(alloy_rlp::Error::Custom("Invalid transaction data")); + } + + match b[0].try_into().map_err(|_| alloy_rlp::Error::Custom("Invalid tx type"))? { + TxType::Eip2930 => { + Ok(Self::Eip2930(SpanBatchEip2930TransactionData::decode(&mut &b[1..])?)) + } + TxType::Eip1559 => { + Ok(Self::Eip1559(SpanBatchEip1559TransactionData::decode(&mut &b[1..])?)) + } + TxType::Eip7702 => { + Ok(Self::Eip7702(SpanBatchEip7702TransactionData::decode(&mut &b[1..])?)) + } + _ => Err(alloy_rlp::Error::Custom("Invalid transaction type")), + } + } + + /// Converts the [`SpanBatchTransactionData`] into a signed transaction as [`TxEnvelope`]. + pub fn to_signed_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + is_protected: bool, + ) -> Result { + Ok(match self { + Self::Legacy(data) => TxEnvelope::Legacy(data.to_signed_tx( + nonce, + gas, + to, + chain_id, + signature, + is_protected, + )?), + Self::Eip2930(data) => { + TxEnvelope::Eip2930(data.to_signed_tx(nonce, gas, to, chain_id, signature)?) + } + Self::Eip1559(data) => { + TxEnvelope::Eip1559(data.to_signed_tx(nonce, gas, to, chain_id, signature)?) + } + Self::Eip7702(data) => { + let Some(addr) = to else { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + }; + TxEnvelope::Eip7702(data.to_signed_tx(nonce, gas, addr, chain_id, signature)?) + } + }) + } +} diff --git a/kona/crates/protocol/protocol/src/batch/type.rs b/kona/crates/protocol/protocol/src/batch/type.rs new file mode 100644 index 0000000000000..9bb9a481d9c7c --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/type.rs @@ -0,0 +1,71 @@ +//! Batch Types +//! +//! This module contains the batch types for the OP Stack derivation pipeline. +//! +//! ## Batch +//! +//! A batch is either a `SpanBatch` or a `SingleBatch`. +//! +//! The batch type is encoded as a single byte: +//! - `0x00` for a `SingleBatch` +//! - `0x01` for a `SpanBatch` + +use alloy_rlp::{Decodable, Encodable}; + +/// The single batch type identifier. +pub const SINGLE_BATCH_TYPE: u8 = 0x00; + +/// The span batch type identifier. +pub const SPAN_BATCH_TYPE: u8 = 0x01; + +/// The Batch Type. +#[derive(Debug, Clone, PartialEq, Eq)] +#[repr(u8)] +pub enum BatchType { + /// Single Batch. + Single = SINGLE_BATCH_TYPE, + /// Span Batch. + Span = SPAN_BATCH_TYPE, +} + +impl From for BatchType { + fn from(val: u8) -> Self { + match val { + SINGLE_BATCH_TYPE => Self::Single, + SPAN_BATCH_TYPE => Self::Span, + _ => panic!("Invalid batch type: {val}"), + } + } +} + +impl Encodable for BatchType { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let val = match self { + Self::Single => SINGLE_BATCH_TYPE, + Self::Span => SPAN_BATCH_TYPE, + }; + val.encode(out); + } +} + +impl Decodable for BatchType { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let val = u8::decode(buf)?; + Ok(Self::from(val)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec::Vec; + + #[test] + fn test_batch_type_rlp_roundtrip() { + let batch_type = BatchType::Single; + let mut buf = Vec::new(); + batch_type.encode(&mut buf); + let decoded = BatchType::decode(&mut buf.as_slice()).unwrap(); + assert_eq!(batch_type, decoded); + } +} diff --git a/kona/crates/protocol/protocol/src/batch/validity.rs b/kona/crates/protocol/protocol/src/batch/validity.rs new file mode 100644 index 0000000000000..b29ef332e51d9 --- /dev/null +++ b/kona/crates/protocol/protocol/src/batch/validity.rs @@ -0,0 +1,65 @@ +//! Contains the [`BatchValidity`] and its encodings. + +/// Batch Validity +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BatchValidity { + /// The batch is invalid now and in the future, unless we reorg, so it can be discarded. + Drop, + /// The batch is valid and should be processed + Accept, + /// We are lacking L1 information until we can proceed batch filtering + Undecided, + /// The batch may be valid, but cannot be processed yet and should be checked again later + Future, + /// Introduced in Holocene, a special variant of the `Drop` variant that signals not to flush + /// the active batch and channel, in the case of processing an old batch + Past, +} + +impl core::fmt::Display for BatchValidity { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Drop => write!(f, "Drop"), + Self::Accept => write!(f, "Accept"), + Self::Undecided => write!(f, "Undecided"), + Self::Future => write!(f, "Future"), + Self::Past => write!(f, "Past"), + } + } +} + +impl BatchValidity { + /// Returns whether the batch is accepted. + pub const fn is_accept(&self) -> bool { + matches!(self, Self::Accept) + } + + /// Returns whether the batch is dropped. + pub const fn is_drop(&self) -> bool { + matches!(self, Self::Drop) + } + + /// Returns whether the batch is outdated. + pub const fn is_outdated(&self) -> bool { + matches!(self, Self::Past) + } + + /// Returns whether the batch is future. + pub const fn is_future(&self) -> bool { + matches!(self, Self::Future) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_batch_validity() { + assert!(BatchValidity::Accept.is_accept()); + assert!(BatchValidity::Drop.is_drop()); + assert!(BatchValidity::Past.is_outdated()); + assert!(BatchValidity::Future.is_future()); + } +} diff --git a/kona/crates/protocol/protocol/src/block.rs b/kona/crates/protocol/protocol/src/block.rs new file mode 100644 index 0000000000000..116fc2240fc14 --- /dev/null +++ b/kona/crates/protocol/protocol/src/block.rs @@ -0,0 +1,585 @@ +//! Block Types for Optimism. + +use crate::{DecodeError, L1BlockInfoTx}; +use alloc::vec::Vec; +use alloy_consensus::{Block, Transaction, Typed2718}; +use alloy_eips::{BlockNumHash, eip2718::Eip2718Error, eip7685::EMPTY_REQUESTS_HASH}; +use alloy_primitives::B256; +use alloy_rpc_types_engine::{CancunPayloadFields, PraguePayloadFields}; +use alloy_rpc_types_eth::Block as RpcBlock; +use derive_more::Display; +use kona_genesis::ChainGenesis; +use op_alloy_consensus::{OpBlock, OpTxEnvelope}; +use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadSidecar, OpPayloadError}; + +/// Block Header Info +#[derive(Debug, Clone, Display, Copy, Eq, Hash, PartialEq, Default)] +#[display( + "BlockInfo {{ hash: {hash}, number: {number}, parent_hash: {parent_hash}, timestamp: {timestamp} }}" +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct BlockInfo { + /// The block hash + pub hash: B256, + /// The block number + pub number: u64, + /// The parent block hash + pub parent_hash: B256, + /// The block timestamp + pub timestamp: u64, +} + +impl BlockInfo { + /// Instantiates a new [`BlockInfo`]. + pub const fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self { + Self { hash, number, parent_hash, timestamp } + } + + /// Returns the block ID. + pub const fn id(&self) -> BlockNumHash { + BlockNumHash { hash: self.hash, number: self.number } + } + + /// Returns `true` if this [`BlockInfo`] is the direct parent of the given block. + pub fn is_parent_of(&self, block: &Self) -> bool { + self.number + 1 == block.number && self.hash == block.parent_hash + } +} + +impl From> for BlockInfo { + fn from(block: Block) -> Self { + Self::from(&block) + } +} + +impl From<&Block> for BlockInfo { + fn from(block: &Block) -> Self { + Self { + hash: block.header.hash_slow(), + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + } + } +} + +impl From> for BlockInfo { + fn from(block: RpcBlock) -> Self { + Self { + hash: block.header.hash_slow(), + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + } + } +} + +impl From<&RpcBlock> for BlockInfo { + fn from(block: &RpcBlock) -> Self { + Self { + hash: block.header.hash_slow(), + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + } + } +} + +/// L2 Block Header Info +#[derive(Debug, Display, Clone, Copy, Hash, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[display( + "L2BlockInfo {{ block_info: {block_info}, l1_origin: {l1_origin:?}, seq_num: {seq_num} }}" +)] +pub struct L2BlockInfo { + /// The base [`BlockInfo`] + #[cfg_attr(feature = "serde", serde(flatten))] + pub block_info: BlockInfo, + /// The L1 origin [`BlockNumHash`] + #[cfg_attr(feature = "serde", serde(rename = "l1origin", alias = "l1Origin"))] + pub l1_origin: BlockNumHash, + /// The sequence number of the L2 block + #[cfg_attr(feature = "serde", serde(rename = "sequenceNumber", alias = "seqNum"))] + pub seq_num: u64, +} + +impl L2BlockInfo { + /// Returns the block hash. + pub const fn hash(&self) -> B256 { + self.block_info.hash + } +} + +#[cfg(feature = "arbitrary")] +impl arbitrary::Arbitrary<'_> for L2BlockInfo { + fn arbitrary(g: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(Self { + block_info: g.arbitrary()?, + l1_origin: BlockNumHash { number: g.arbitrary()?, hash: g.arbitrary()? }, + seq_num: g.arbitrary()?, + }) + } +} + +/// An error that can occur when converting an OP [`Block`] to [`L2BlockInfo`]. +#[derive(Debug, thiserror::Error)] +pub enum FromBlockError { + /// The genesis block hash does not match the expected value. + #[error("Invalid genesis hash")] + InvalidGenesisHash, + /// The L2 block is missing the L1 info deposit transaction. + #[error("L2 block is missing L1 info deposit transaction ({0})")] + MissingL1InfoDeposit(B256), + /// The first payload transaction has an unexpected type. + #[error("First payload transaction has unexpected type: {0}")] + UnexpectedTxType(u8), + /// Failed to decode the first transaction into an OP transaction. + #[error("Failed to decode the first transaction into an OP transaction: {0}")] + TxEnvelopeDecodeError(Eip2718Error), + /// The first payload transaction is not a deposit transaction. + #[error("First payload transaction is not a deposit transaction, type: {0}")] + FirstTxNonDeposit(u8), + /// Failed to decode the [`L1BlockInfoTx`] from the deposit transaction. + #[error("Failed to decode the L1BlockInfoTx from the deposit transaction: {0}")] + BlockInfoDecodeError(#[from] DecodeError), + /// Failed to convert [`OpExecutionPayload`] to [`OpBlock`]. + #[error(transparent)] + OpPayload(#[from] OpPayloadError), +} + +impl PartialEq for FromBlockError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::InvalidGenesisHash, Self::InvalidGenesisHash) => true, + (Self::MissingL1InfoDeposit(a), Self::MissingL1InfoDeposit(b)) => a == b, + (Self::UnexpectedTxType(a), Self::UnexpectedTxType(b)) => a == b, + (Self::TxEnvelopeDecodeError(_), Self::TxEnvelopeDecodeError(_)) => true, + (Self::FirstTxNonDeposit(a), Self::FirstTxNonDeposit(b)) => a == b, + (Self::BlockInfoDecodeError(a), Self::BlockInfoDecodeError(b)) => a == b, + _ => false, + } + } +} + +impl From for FromBlockError { + fn from(value: Eip2718Error) -> Self { + Self::TxEnvelopeDecodeError(value) + } +} + +impl L2BlockInfo { + /// Instantiates a new [`L2BlockInfo`]. + pub const fn new(block_info: BlockInfo, l1_origin: BlockNumHash, seq_num: u64) -> Self { + Self { block_info, l1_origin, seq_num } + } + + /// Constructs an [`L2BlockInfo`] from a given OP [`Block`] and [`ChainGenesis`]. + pub fn from_block_and_genesis>( + block: &Block, + genesis: &ChainGenesis, + ) -> Result { + let block_info = BlockInfo::from(block); + + let (l1_origin, sequence_number) = if block_info.number == genesis.l2.number { + if block_info.hash != genesis.l2.hash { + return Err(FromBlockError::InvalidGenesisHash); + } + (genesis.l1, 0) + } else { + if block.body.transactions.is_empty() { + return Err(FromBlockError::MissingL1InfoDeposit(block_info.hash)); + } + + let tx = block.body.transactions[0].as_ref(); + let Some(tx) = tx.as_deposit() else { + return Err(FromBlockError::FirstTxNonDeposit(tx.ty())); + }; + + let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref()) + .map_err(FromBlockError::BlockInfoDecodeError)?; + (l1_info.id(), l1_info.sequence_number()) + }; + + Ok(Self { block_info, l1_origin, seq_num: sequence_number }) + } + + /// Constructs an [`L2BlockInfo`] From a given [`OpExecutionPayload`] and [`ChainGenesis`]. + pub fn from_payload_and_genesis( + payload: OpExecutionPayload, + parent_beacon_block_root: Option, + genesis: &ChainGenesis, + ) -> Result { + let block: OpBlock = match payload { + OpExecutionPayload::V4(_) => { + let sidecar = OpExecutionPayloadSidecar::v4( + CancunPayloadFields::new( + parent_beacon_block_root.unwrap_or_default(), + Vec::new(), + ), + PraguePayloadFields::new(EMPTY_REQUESTS_HASH), + ); + payload.try_into_block_with_sidecar(&sidecar)? + } + OpExecutionPayload::V3(_) => { + let sidecar = OpExecutionPayloadSidecar::v3(CancunPayloadFields::new( + parent_beacon_block_root.unwrap_or_default(), + Vec::new(), + )); + payload.try_into_block_with_sidecar(&sidecar)? + } + _ => payload.try_into_block()?, + }; + Self::from_block_and_genesis(&block, genesis) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloy_consensus::{Header, TxEnvelope}; + use alloy_primitives::b256; + use op_alloy_consensus::OpBlock; + + #[test] + fn test_rpc_block_into_info() { + let block: alloy_rpc_types_eth::Block = alloy_rpc_types_eth::Block { + header: alloy_rpc_types_eth::Header { + hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"), + inner: alloy_consensus::Header { + number: 1, + parent_hash: b256!( + "0202020202020202020202020202020202020202020202020202020202020202" + ), + timestamp: 1, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let expected = BlockInfo { + hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"), + number: 1, + parent_hash: b256!("0202020202020202020202020202020202020202020202020202020202020202"), + timestamp: 1, + }; + let block = block.into_consensus(); + assert_eq!(BlockInfo::from(block), expected); + } + + #[test] + fn test_from_block_and_genesis() { + use crate::test_utils::RAW_BEDROCK_INFO_TX; + let genesis = ChainGenesis { + l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 }, + l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 }, + ..Default::default() + }; + let tx_env = alloy_rpc_types_eth::Transaction { + inner: alloy_consensus::transaction::Recovered::new_unchecked( + op_alloy_consensus::OpTxEnvelope::Deposit(alloy_primitives::Sealed::new( + op_alloy_consensus::TxDeposit { + input: alloy_primitives::Bytes::from(&RAW_BEDROCK_INFO_TX), + ..Default::default() + }, + )), + Default::default(), + ), + block_hash: None, + block_number: Some(1), + effective_gas_price: Some(1), + transaction_index: Some(0), + }; + let block: alloy_rpc_types_eth::Block = + alloy_rpc_types_eth::Block { + header: alloy_rpc_types_eth::Header { + hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"), + inner: alloy_consensus::Header { + number: 3, + parent_hash: b256!( + "0202020202020202020202020202020202020202020202020202020202020202" + ), + timestamp: 1, + ..Default::default() + }, + ..Default::default() + }, + transactions: alloy_rpc_types_eth::BlockTransactions::Full(vec![ + op_alloy_rpc_types::Transaction { + inner: tx_env, + deposit_nonce: None, + deposit_receipt_version: None, + }, + ]), + ..Default::default() + }; + let expected = L2BlockInfo { + block_info: BlockInfo { + hash: b256!("e65ecd961cee8e4d2d6e1d424116f6fe9a794df0244578b6d5860a3d2dfcd97e"), + number: 3, + parent_hash: b256!( + "0202020202020202020202020202020202020202020202020202020202020202" + ), + timestamp: 1, + }, + l1_origin: BlockNumHash { + hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"), + number: 18334955, + }, + seq_num: 4, + }; + let block = block.into_consensus(); + let derived = L2BlockInfo::from_block_and_genesis(&block, &genesis).unwrap(); + assert_eq!(derived, expected); + } + + #[test] + fn test_from_block_error_partial_eq() { + assert_eq!(FromBlockError::InvalidGenesisHash, FromBlockError::InvalidGenesisHash); + assert_eq!( + FromBlockError::MissingL1InfoDeposit(b256!( + "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b" + )), + FromBlockError::MissingL1InfoDeposit(b256!( + "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b" + )), + ); + assert_eq!(FromBlockError::UnexpectedTxType(1), FromBlockError::UnexpectedTxType(1)); + assert_eq!( + FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1)), + FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1)) + ); + assert_eq!(FromBlockError::FirstTxNonDeposit(1), FromBlockError::FirstTxNonDeposit(1)); + assert_eq!( + FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector), + FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector) + ); + } + + #[test] + fn test_l2_block_info_invalid_genesis_hash() { + let genesis = ChainGenesis { + l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 }, + l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 }, + ..Default::default() + }; + let op_block = OpBlock { + header: Header { + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + ..Default::default() + }, + body: Default::default(), + }; + let err = L2BlockInfo::from_block_and_genesis(&op_block, &genesis).unwrap_err(); + assert_eq!(err, FromBlockError::InvalidGenesisHash); + } + + #[test] + fn test_from_block() { + let block: Block = Block { + header: Header { + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + ..Default::default() + }, + body: Default::default(), + }; + let block_info = BlockInfo::from(&block); + assert_eq!( + block_info, + BlockInfo { + hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"), + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + } + ); + } + + #[test] + fn test_block_info_display() { + let hash = B256::from([1; 32]); + let parent_hash = B256::from([2; 32]); + let block_info = BlockInfo::new(hash, 1, parent_hash, 1); + assert_eq!( + block_info.to_string(), + "BlockInfo { hash: 0x0101010101010101010101010101010101010101010101010101010101010101, number: 1, parent_hash: 0x0202020202020202020202020202020202020202020202020202020202020202, timestamp: 1 }" + ); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_block_info() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn test_arbitrary_l2_block_info() { + use arbitrary::Arbitrary; + use rand::Rng; + let mut bytes = [0u8; 1024]; + rand::rng().fill(bytes.as_mut_slice()); + L2BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); + } + + #[test] + fn test_block_id_bounds() { + let block_info = BlockInfo { + hash: B256::from([1; 32]), + number: 0, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }; + let expected = BlockNumHash { hash: B256::from([1; 32]), number: 0 }; + assert_eq!(block_info.id(), expected); + + let block_info = BlockInfo { + hash: B256::from([1; 32]), + number: u64::MAX, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }; + let expected = BlockNumHash { hash: B256::from([1; 32]), number: u64::MAX }; + assert_eq!(block_info.id(), expected); + } + + #[test] + #[cfg(feature = "serde")] + fn test_deserialize_block_info() { + let block_info = BlockInfo { + hash: B256::from([1; 32]), + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }; + + let json = r#"{ + "hash": "0x0101010101010101010101010101010101010101010101010101010101010101", + "number": 1, + "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202", + "timestamp": 1 + }"#; + + let deserialized: BlockInfo = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, block_info); + } + + #[test] + #[cfg(feature = "serde")] + fn test_deserialize_block_info_with_hex() { + let block_info = BlockInfo { + hash: B256::from([1; 32]), + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }; + + let json = r#"{ + "hash": "0x0101010101010101010101010101010101010101010101010101010101010101", + "number": 1, + "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202", + "timestamp": 1 + }"#; + + let deserialized: BlockInfo = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, block_info); + } + + #[test] + #[cfg(feature = "serde")] + fn test_deserialize_l2_block_info() { + let l2_block_info = L2BlockInfo { + block_info: BlockInfo { + hash: B256::from([1; 32]), + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }, + l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 }, + seq_num: 3, + }; + + let json = r#"{ + "hash": "0x0101010101010101010101010101010101010101010101010101010101010101", + "number": 1, + "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202", + "timestamp": 1, + "l1origin": { + "hash": "0x0303030303030303030303030303030303030303030303030303030303030303", + "number": 2 + }, + "sequenceNumber": 3 + }"#; + + let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, l2_block_info); + } + + #[test] + #[cfg(feature = "serde")] + fn test_deserialize_l2_block_info_hex() { + let l2_block_info = L2BlockInfo { + block_info: BlockInfo { + hash: B256::from([1; 32]), + number: 1, + parent_hash: B256::from([2; 32]), + timestamp: 1, + }, + l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 }, + seq_num: 3, + }; + + let json = r#"{ + "hash": "0x0101010101010101010101010101010101010101010101010101010101010101", + "number": 1, + "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202", + "timestamp": 1, + "l1origin": { + "hash": "0x0303030303030303030303030303030303030303030303030303030303030303", + "number": 2 + }, + "sequenceNumber": 3 + }"#; + + let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap(); + assert_eq!(deserialized, l2_block_info); + } + + #[test] + fn test_is_parent_of() { + let parent = BlockInfo { + hash: B256::from([1u8; 32]), + number: 10, + parent_hash: B256::from([0u8; 32]), + timestamp: 1000, + }; + let child = BlockInfo { + hash: B256::from([2u8; 32]), + number: 11, + parent_hash: parent.hash, + timestamp: 1010, + }; + let unrelated = BlockInfo { + hash: B256::from([3u8; 32]), + number: 12, + parent_hash: B256::from([9u8; 32]), + timestamp: 1020, + }; + + assert!(parent.is_parent_of(&child)); + assert!(!child.is_parent_of(&parent)); + assert!(!parent.is_parent_of(&unrelated)); + } +} diff --git a/kona/crates/protocol/protocol/src/brotli.rs b/kona/crates/protocol/protocol/src/brotli.rs new file mode 100644 index 0000000000000..bd20ece7414b4 --- /dev/null +++ b/kona/crates/protocol/protocol/src/brotli.rs @@ -0,0 +1,107 @@ +//! Contains brotli decompression utilities. + +use alloc::{vec, vec::Vec}; +use alloc_no_stdlib::*; +use brotli::*; +use core::ops; + +use crate::MAX_SPAN_BATCH_ELEMENTS; + +/// A frame decompression error. +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum BrotliDecompressionError { + /// The buffer exceeds the [`MAX_SPAN_BATCH_ELEMENTS`] protocol parameter. + #[error("The batch exceeds the maximum number of elements: {max_size}", max_size = MAX_SPAN_BATCH_ELEMENTS)] + BatchTooLarge, +} + +/// Decompresses the given bytes data using the Brotli decompressor implemented +/// in the [`brotli`](https://crates.io/crates/brotli) crate. +pub fn decompress_brotli( + data: &[u8], + max_rlp_bytes_per_channel: usize, +) -> Result, BrotliDecompressionError> { + declare_stack_allocator_struct!(MemPool, 4096, stack); + + let mut u8_buffer = vec![0; 32 * 1024 * 1024].into_boxed_slice(); + let mut u32_buffer = vec![0; 1024 * 1024].into_boxed_slice(); + let mut hc_buffer = vec![HuffmanCode::default(); 4 * 1024 * 1024].into_boxed_slice(); + let u8_allocator = MemPool::::new_allocator(&mut u8_buffer, bzero); + let u32_allocator = MemPool::::new_allocator(&mut u32_buffer, bzero); + let hc_allocator = MemPool::::new_allocator(&mut hc_buffer, bzero); + let mut brotli_state = BrotliState::new(u8_allocator, u32_allocator, hc_allocator); + + // Setup the decompressor inputs and outputs + let mut output = vec![0; data.len()]; + let mut available_in = data.len(); + let mut input_offset = 0; + let mut available_out = output.len(); + let mut output_offset = 0; + let mut written = 0; + + // Decompress the data stream until success or failure + loop { + match brotli::BrotliDecompressStream( + &mut available_in, + &mut input_offset, + data, + &mut available_out, + &mut output_offset, + &mut output, + &mut written, + &mut brotli_state, + ) { + brotli::BrotliResult::ResultSuccess => break, + brotli::BrotliResult::NeedsMoreOutput => { + // Resize the output buffer to double the size, following standard + // practice for buffer resizing in streams. + let old_len = output.len(); + let new_len = old_len * 2; + + if new_len > max_rlp_bytes_per_channel { + return Err(BrotliDecompressionError::BatchTooLarge); + } + + output.resize(new_len, 0); + available_out += old_len; + } + _ => break, + } + } + + // Truncate the output buffer to the written bytes + output.truncate(written); + + Ok(output) +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_primitives::hex; + use kona_genesis::MAX_RLP_BYTES_PER_CHANNEL_FJORD; + + #[test] + fn test_decompress_brotli() { + let expected = hex!("75ed184249e9bc19675e"); + let compressed = hex!("8b048075ed184249e9bc19675e03"); + + let decompressed = + decompress_brotli(&compressed, MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize).unwrap(); + assert_eq!(decompressed, expected); + } + + #[test] + fn test_decompress_batch_brotli() { + let raw_batch_decompressed = hex!( + "b930d700f930d3a0a8d01076e1235e0c33674a449c13fc37ee57f9ea065bf41af3aa03d5981f1432833bd0b0a0652a19cd927ae4a22e8f8069385002252d78e1c3cc91a59ac188708b7074449184766cbcf3f93085b903ee02f903ea82014d884062b70d4e215ee885019d47a37c8543ae9f382a8310c97b9451294f5cd6e52c003ecfb412ca8b42705c618d29883782dace9d900000b903690d669b0cd98174ac3b57393839029ac04ad36454109851443b4f6580664fe06766a7dea5b1ed31e14e7c11aa738eecb86e979f874873cd3d7ca9481681b4b17d134316e7bbe828ef69339ef85c6f0e9dcdfe1dc85309effb487569383d5464b519bdc1c85fffc72bfe93d4081a3e1b75e5dd39f95a91df0997a22d8fbdeca57a8b35b4f0e277ec8502cc55581a94eec1d1000b2921b4d7c3985ace205713641d03c3975e4049e13b3d2c5926b224684e38beb3b8d2e5d4060b109aafc3f2d144783aadf6086aa1d5a931d21282711484a9c0537bd4981fc222444f2c057211708e70dc4223063cbf39e4af0b795d3ec0dfba32391611d151145c1b6bb33d53ce2bb7983bd7b6c1516f7a1a719fd876f4b20910aba76c16dbfc57199a60e2ab938bc285613c3802c17aa03cb9654f5142d607bac01293c9aaf4e58b422c543f7e5e458af0b7cf57f33109558bef71e8b5506da723d996eb8e2c265b1cae43dba571d07d3ea1bcfdcb73089597e3744344e049bf21b4244d5aff60d559010b69a6335f4bb21178de504f50808204da652c7767dbf11f2a34b4fb710e6df9ad8810aa75dcdb2c99dfe9bf898912817e490b4982d44fe09f8adb43e0da2a0c824a9069ce8cc36b5fb0074c2db895ee92d92fa6b7efdf5c97ae05ae27556bc07ddc9d9d6261a53e3a10c350c3b1da26b27b345768e17da7dabfe6e30e019c88ef4a0e8df840bbd3fbbb639edf775449d8be7510cc811564789b861372fe97f7b5b1389f20c9872517634e9225669ee80cf077f9c8606cdbad53819a875ecd9f7b6d778c1dc302ca19ae67ffb054eb99206fc90eacbac8177712d0b4c72700df3f5e2c88fb4e9c8284cefa66390a78605ad9320aee34f72f3cb263020204393d9359a65f48b0e6e942b016a1f2c5bd6579f0a65997635ab15fa38db76ae8a5d3be516441499819bfaf730ebaec389db082e41443660dcc6280315154888b9e726b971237fae5e06b01958aac081398c814e446a003039dd090c0efa5d39735ed0ab46c7b4e4c960ae414b045fd19117089e65aaf3779cc9045d6e62538b1b75c2689d23ba3c08ceed46d4fdf9b969b34a1903ebd96a3a6b091842480e638b095c1ec11bb5c599668ea1b0a5a714d13462edb39dfd992b569897ac8f45c587182770631c262fc459afa6f23d5670eee2aac2ddaa89314607d30c6bfd408980c082749ad6b48a5310ac75b880cc080a00b5d23a075615f50233ce278d11b7b0ba0ad6a01486dbf31c54aae096f0f066aa02d9feeb4771b5a37d1247a4cc58a64d392f3916b5602d9d41d97b52b391ffd47b9011801f9011482014d88a793ab3f17510b308821f5d9030532aae9831708c1940b6f262f685c8d0ff7dfc9ba9686d8f75b78923c80b89f7644852b70713a788b69f191c54ec8368a7f2675623b2369f9078516605d0d4550ff9f5b92b9da2147fa3a24cc17605f30cccedc5bacafb2bb86e2640db6654a514b8eb13d3c3ab6b5e344498de0c709dd9bef58a8af16d3efcd2c0b2cb69d6089d0af8d42baab434dea885253e42050aeec01f233e64289b2e894c680fbab4f25a653745dbd89edb19d97e35bdd4293794c69503b0e60ed9cffe7e9ab3cbbc080a0dd08ebab0802fc61ccf26c357b638a55cbcd6b366251c17e2fa52d328d9d59e5a027d334772553048d6b76fc39ddee5f85363810c235219356cb4c5c3dbf9661d5b90298f9029588e383f18817bb0d1c882c58aa6b12de88f3830a7831945c1c1314ed944220436fad3742023cba2a71c4a2886124fee993bc0000b90219fb039c014cd76a327bb9b3f59e8176f377249385e67cb1681f8eacff1dee5a5a949511438ce370f8ad6618f3af81cb1f775a0b365546dd7791b0ad71fb1f2f29154265a8175b7e518580732a5a46dae3752e1234ff779d4eb614af2c66beec964181ecd0cfd1640bb2ca2b860649c41930a60de0cc754884a780488f05d1d5833a381670b368c85bf08d6650e26122f6714056382a006fcd5f9c97f55a98d68dd9293bb1be24823eaa8cb007481dc78a7a670123976e7b6e81fc223f42637759a0c933b73ba89a1d902c0874fedeb0a97dfab298972a18378539c2894ca6df9c0a423c2e98df4c133e5e808809849785b069e323640bf93d4b82a0917aaea8fda9a3072ab9a00a4b8b9b7b3a3eb326e54231d0f6a064cdf4a1fc06c961e5087359c029b13e229fb477d6651bad52c75e503ac45002a803a7457488966cc16bbc9be5c1c9a797d0377710c028e4f05a6cb929cc1fd4018912929252e04e107ffbcbd4c81ba01ab4b11faa90be0f9f9a6a22c87257e4a2aa8283e6f71d7b9e03b5308b16525c4d79705bb0906be0e947e8075ac6ce2235356aa0a66bec39e918e47a6220b322e326bf8fd65e47778e14074c47cb62b7ef8ef956c996097d2919df7aac8ea2ed69c1fd9f1d96b6b82b411c524cacec0f4a4269821fd6766d24954b8870fb1d85f5cda0528ae18419915a8b30b25baf6a162978a4bec86009cece83017d50667a202b3fad18f8ed8b5140c97fa74e91be608fdb788202bea05f469660e363ec580825d1e2bf753c01db044279f862720a27831744b91494f5a050fa7445e0e6156dfdb712a647ef73a2dd35b73d5cc988430c831352d4ac7e8bb90458f9045588a106e4c16d06833a881973c4c642fba1bb83068f2294050c84206ba9d32d93d144884644e5bd36fc92d0883782dace9d900000b903d9b303f8efb68766822d7eea21ca4b7c5dd79dce832c4893247f6784fe47cd7a18caea7b5b4d8bdf02da0276aca185add01fa2d16c2f1188ff7cbf6fb8c6308999037b2b92d725094d8faed86f0b1a45b55de4f36dbb71dcbf4be12fe624077213e0c170afbbbb546a343ac3f2a1333a7a7a7db7be46640a73d61b3aabc805b022be416198d809b62f99d26cf4a3bf555d40686f4b8970ec15386462bec5f2b728de0da047d6b3f3ea51f571507f32f047322fa204f0c5697cbb56b4b5c7792acaa40f02926651fa715a40e1f212c78cd4ecca285ada2c8cbb6e5dcfa3823725b44e29aacbeb9b6224f90fbc895a5980d63da46688832e9776b0666e90deacbcf8a4c559b625cf004cd04c686aaf9d7d6e2d394f5d36311f7afdcec5033daccc63c0540935f59514c9aa8ac3c2aeff48f624f2dbd38062fcd046651e92fc7ffce4dd914bb0dae704e5b26a8b73b3baef8ea022881e15666fada8e43fd621793713cb8c867775b9cdcf3b066582fc9baa705a0e1dc61a4b33b1b33ad3ba3bd0cc41b5850cadc04654dec222178709910209c6ac3db9054ef91facae2d729d7ee54898a18411b6d20d599a3de14d5375e5a9c90f3bce78479cb0f20afca895e40b576940e063587f451a8828ec2dd4a8538b4bebc39f72a6c54e379a07b7d5e0c02ccd57dbff13729bbfe5e78498c01cea12e830944fd0a123b7383fdcda97d8d9cc831e542ab6d9b36774d540b180c2bd52d46ca7f0e17d400cf3cd559b1b4e51ba93cd954777ba27a9f0327eb6c68aafe74fabca4610210db7498aecffd3164c5eef8cede655e1b42d5f54f5a52b4f5fe9698a4463f30f20693263d41074d0403a737c4d4986f0ee7fee828fb7072a80603613fb4d6c219dfa47adad433af6b437dd199f3bbc651487718b2e6d42728034c242672a98a9f36fab6d4162f4e8eb7bf2a9868cead8ad657a67f0aa50286113db972936260323d7b11353328151e80691d551bbe1f7f11774e15db4f175aeac5b91668a712c3c2399a977abb9fd9c2b53c5ba68f2c0ea353028416b36a47028f78918e2b205bf9b3bce6f1a08bd4448abc3f12a240482b4be98dcb77c74fff47e92d833735e802465e50b79d51de5a7fe45a95b650b051c61a529d5f51cd0c603a2de67a3123be1c52263e1c9167765b13ad1e01cfb27531c9203f39e8913fe0cab9d8c14b17bad0100b76c41d41d68ae3b7aeef5f6af4f66d113fd29eb9c4bf994f04decad13880d9d1eb3865a30e2540e86923b36369c121ef2a6a43a618aa4b15560fa806601a85be361468bd09c6dca39ad7ec44809adc0907dd0458177343a7c23330605b802f3ffd3ae61b3be952ca2effae8222e9ed0b6ea4240728a7800e4882efa7dd1ef8202bea05db690cab7dc8c52c2c375428c0aa9ead02bf44e2b1f8ee06e1cf7af25eecc13a07d967fb12e1f0073adac46e0676a6006b30d780e6a1387afec76cbd1f07016e3b9012401f9012082014d88df6f092495b7f4148840c5b5541d013c63830408e194aef36f2041e560a641af89e0ba2799ea630a9592881bc16d674ec80000b8a3afb9380f9228224c1aa59eab115ed4172b471aa2ee11b3d4ac93f4b6a33518007a798170801f4f582e188b489005d8f108e2a4acd6f7ac28852580e73b6a1590ea1af1443666f1d14affb0a9d0655a5c57cd4190b2a00c07276054641ee4204ed8a806ded2b3aaa7453c24e442992434d060b51d2255c1cc2a002264b5dadb32057f4a5d52626e0ff453e2f05f1e0d8294614916c00110853462d51d9ab7e03b7019c6c001a06028ddc42f0d3e1cd6cb1ed7377d518480626d56c80e6d15eacd42ecf2f30957a03f6e1098b300b6329997bacc5e667eeed72a38f6c4e1db7199483bc9a18267d8b90222f9021f88c0988653bce0e07388fbc67f04e5c6772e8311bd5c94eeecd6da1ee441093ef70d8c86a26f4dc4da11588853444835ec580000b901a349e745c1cca19957c43f15309935f7bf49547884332dfe6d5b8b9d61542dd88ecc61187fda813a7f700ca96e8847a33bf8552690d91ec8e8fa70c21b380c9c681b54e859add36c3c19e7fda3075ec1a3cf47ed39c89241bb73f206d7497f93c47db9a85be7135948e19809c195ccd4c9a379ed464bf77ec562e360c52b9225f103d323364a72e8a725ad2b34a355928acc6aa563b67d120ddf54cf68f710624499ddeb30b0c94b8722ef2d641ae49f17f4a916d54350ec483ec5bcfd9748e0a228c3e73cee9ea248ad85060ac51b3e6834e1f771f725a466affa28453ad3726d794caab223fa76c8b994ac5d3a1e8ee830e4fadfe0786174364af3109c04d7d607aca17933c4366d44d9c5376ca34febaaa612707eec4e2fc5c6b1668b3450340938d17e5552df96ae84a905d069f9e3455bccab30640a0720f9b4598d8f82ebd19bd32b7e82165303123a0ed80c57375174c08d32ad3ae354251c97316b2977f3a2fdf2dba1c595093c88275badc54e3aad65f77c56f55d04b1e6d668406058ea01da2364fc207659b028d9c55371c776f732e63255dd177b95f857e3cbdb4c66fabd8202bda060830662664d96755362addcc0908287c99c60761cf9c7a613058894eab6e599a059cd2461d4a89458dc68adf287fee71a783dab0aaa05587a21b4aba1ca4f5efeb9017801f9017482014d88d15c09b7ee8f9562880ae58585f383aacc831e72f6808853444835ec580000b9010a2e818d2c4fa7a974f5c3acf3c0f9439f4c83721b2bb9df4fa290c7fa57bc1f9f77e4b80866845a8bbbf8030b707b1f07a54a0ab901188eb2e1262a45618a08517f943cb032eeec926e4343d5d3089c145da1d53128ae901ce91a813c205c615bc1ce9b8658a9da4c2d258fe36f6ffb6289df910566386dd1a9f73b44053bb64523d8faf7b9055c592695fc426c360479c1e2d1f68ca5c7965dd20b6879989606cea7c0db28f27ead4a591ee264f755b7358146586c6a1a8530ec463dd754f100fac603ec3360c0440874c12bb179c43a23e40957bd446f2573af413f3314e9f0668af2491de96156a9bf35bc469d51935305f4df051580b84e98ec8395fbd42fc0c3f3e7410ac4719af4c080a09a774db7e3a26966edb91c1f7956a091425044ead1589f435c8d04aac9533764a04325d5543464929773cc6ac555f5ce1830c997f4d26f2dad5a7e056db6f0a2e6b9032d02f9032982014d88828a67bc288355d78498c2cc318542aa1a60df8305fbb6808853444835ec580000b902bd082cb3f3fa41ebf06fbb17afeed9ccdcf3d2999e2fdd1e1171e0b1549c06de17dffc4ee7785232184a698311c7487fdf090e34b9954a41affc0d0ad44104f70750f6a896b1b2b5ff1024de66ba877c5494e67735cdfd45f9ec0df1c198b357b60e4d840abaa72c5667074c43bfa5e1f07b5970f018820db6fc2bf84341cd024cefe455c92426f876e51aec0fedded8d4aa4003aaf6970c48d898d8d82a8411990e73c8ec792a2cc4a129e526d0fa34a54c37ac13ecf4e3c597304cdbd327704fc97f2ba0b110afee78da5c3f46d3354bd20f56cb91b7ba8d302422428082748faf8b4828ba925ab1a02ba695e686da4d1e759b6456b0388ac8fd769f3b726332be36d3153ebee040b5d822fe62d73b629a6251c8e49a988cdfe599762759df03c9100db5f7a87ce7102ddd21831e0736924f230ffe6aaf6b012423e351627e118f2bc12736a3694b5468858ec6310017b10de24fe75ff0abc060b1e60271dc5274b4bbf0b755a0a617bc23f57ee2286c805086d5824ca4bb6297545c5c1ccaf03be03b7df33c953ddb183730313f09c88392e4bdf688f1d2b730318cc9b148e488c2f1e383505a383672755a221ee7dffec5a4f77e7efe66043d686a126480ea01a8ef0f72f9a5799e03e863a85b7aa56c88b7575d6ebb9df809a240969d3a2b2e086e742130e38cfe7870db79bbd281849912fa611e04b8dd0dea9b7da5d16a66969e54ab9def159b9c1d351d719a93821c40ad6c6014644c5f77374cbd486d6a7cfe75d7d849ce240ac86a1c0843aab27fba4d317c725eb101752803ea67d3e12b784bb424eee6f766e33d6664ca113af63c54ba27b8a8e904c572dc3fd09848cca3499c403a1c601db77a7f36d244024ceacfd9d6ae494b7e7e0f92fa5f83458d5da139eb127709e3dd75c88fd5f75244e15f1bb8cdbd3056bfa56139442c0bacbf3263f29ef34946e928b9a4f1c085e5df3b09f31c6e87397bd939c001a08b9ac3bc299eff8eedc51ed3ff077e49da6fb145a0c495f430964581fd4d230ba05fef2837a800e231a3178226f59a981d2c4bcebc4b4cfba9680371da1e2c1a61b9042bf904288821c649ab1ae8ea668896d6c78054ad7a6583121a8994e3294b628e98892fc56ae3fcbce852265aa657e7884563918244f40000b903ac0177c66fecad5135344e89f45ec7e083130a3e5eab1abb75bab0aa357cf044c0582542047a3f9985d3439a6f850466061142af44a9208656e278b7ad1bd0e03539cc019d6ebf8758bde3e0489ba540c523f178a0b055c1fedc3627fee427467ab67545c154106bb9e0c12a7120c175d66f9e3eb9183ae5c7640d4cb4bd3dc94c7b4e0c9fe70e692c3fd027e0ebb46bb32b73a269037a76731a9f114343ea0584c3f7e9cb4530d086609b59ab6b72e7dc6c2c0c95699091e06a33af5ba200a168ef483fe11056330e84da4f2a59db72d5d697d262b9565fe81a738a48d24a9f1c8c49a671101bb7db5eb64deb454a117eb00f4ccc31bc93c061e975ab6d375967544a2a06ff8b9d59bfe1ecb1dc47d5536c645d764028c5de77f3f34d6c7999785b70b187d9ec4631e83cc69499a4ff8ace98a6f17b77f648ab7a07d5ee0558a8efc19d4601573156a0264d2e6574e867c1eca423eac1fdbfe0967bb8f02524cc2d9933141acf619ffe99483305fbdd6913f1e1feb78a17fc6b81c705c81eb08d5602b097ddec64f6c334509caeed7525e3e34845b21e56e4424aa9609f4df8bb13f31c5448b6bdede84d9a9aeba9fcc38a3c8eb1f3f31b80918e045266c7d69b252c86f8b5711b2cf7136e2c3d86d1301608c7c16655c3ffe6d04014dfd55a9563c2a307525088fd017486ffeaeed45873013a7940a7a91442b975065c765c32546aee9b001ba78d8563e039c8edc24a92f9f457ae28172eb29e16cc588d52c8e75a565aad1a8f9d6d341189a24718c26c19a83c6cfe1bbec2f4b878759a7dbeb4ffc0568b902b1dfb18af00c7014f2822965ddfb56d7aec508822531834ad2c869affba1f95bf3dfdf1d1dd1c2994d904b9c5133900962c8137d7fce9f0b9a7d0474dff9173edbcefb4bf355539dfa791241031e90770c8f09af595eb1aa0d083bac4fb9b929ad7e23c0fc8d3ecc7458a0790929cf7588cc255916a6c16811f09d0c972b294dee6e1f739c5e9d3eab8016b565c8570e41bcddeef2dfbbf95910ae6a46a2834919742ec599b9ed204d1f86ce6baa534039ed308d8be0d289824303deb54af5f9f50d88807134b8f42485cec121432e58b83c8aecb32fc62623b06c39c3f1e0e921b1bb880d2eb017578e5f33a25a335a813f02259e1b12b8a76a90a65d015bb214032a095cd8918b78003d310a06a246ac95c126188911bda8a6623407c0dad308e25a438f78c7409267b729413b7d248a6a88cd64c73118999f00981aa4f6b639e4252d39b1706c686c7763ae9c41aea7b46fdd48bc490502ae876175e5aff8361ccc530ad8202bea0b0209fabc8a5c0e2a5bd08e9a6b532d51670f41513cf007781f27e49b070ccdba0795755f4fe231840196d847d100e7cf1e5650ae172890c469428269cb105c16cb9031ef9031b882565c357c3279f0c88e90114422a470a4682e988808829a2241af62c0000b902b424fb91666edaa16addea67f72c9e0bc7a8053bda59776ede2a0ec3f7c78ffac0eee97ff259f92b21378193aeeadd0253b08897a14f10ab537db63202a4c9f78eb4b399d55c5a256a8414f58f45b109e6228a75ed1eb09627f44b56eb539c334df412b30ee6f4ea39a04aa671aee9e7157b9cb69aad4ab1d9d75c6d90f3488342b29bb59c97ecfd2bec4f991b095038b9e20eeb591b641f64e32e5020130f8a8daf7c51caf93ca460a4e60132835119f99d0484529cf541ab9f922bf15a782521a0f6739c1edb8d4bc26a07e63790087b4c098e4df74534340bf7815039326d1bdcafa53932deeaff03a31e97c6733cc702cdd42be18e4716dd0d014f3e916b0cee3a16bd52cf717f5efb59fb7e41c8e4c0d7eee8ba92ee5b293b25612ee9a3b0043664e918a2aa2b602accd357c8f22f382b16f637b57f2fedb7d8f66172f22e67cc04f230e28ec96b928f449fba63b7862bc3102181d6c7bf063d9376363b8be8200169aa88c46732c5ab1e19dcbd8abeb34f1e1cbc632484d9864e630c4567c0f04a2bf5895d3cafae1b0e70e4c1ea28d4d9578a82611f09ddb22c3c4440e8236be2bf9cecd3fa64b19930af8664d78d6f10aa9c913be537bf2b539e3a9042d5744eb3d1bbc16d98564488a51ba45edb2713b466beac560789c4eda3c0961bab002b95eba9f512108dee2e39a8759c04b18a923f2f2aab2e1ca30ec7361b25ae71923027c950c089469820a4ec3ec60529f1509b92ef04fb7fac70f25d3e5ea5c6a28226fe19317bd4d0f42085884020a2b22dcb0ed8e5600ac969b4f910e54f617597a84b05774776d694ba38ccd3d1055a7245334cddb1ca20d7e001285a57001d03b2fc1ff893ab044612dba9b311247528d7490a9a7f3e7c3ed8531844d3b829de3604e8546ee8d4c3d7a308d32035159aecfa20ae4660e6dc94b6a155aa78150a01fb0e6c48b660a0f051ab59accaf4508202bda080d51bfef036fd4c4ebe7151b2755d6606122e565323878701113b84fc86548fa06fb34b02deb66359ae8095d3c339673ab2a8b138fcf9aed2d4276c8a16435a60b88801f88582014d88bbd39acc70c3229d884ec80fa5565439d283119a84942d89ae04c33fcbd75e3c6c43b826b266625b854f883782dace9d9000008911d1f14d3a721904f1c001a046bf61e70c69943c277ef7d09ce5e779a10e3671cfec81423e0f951254dfaad2a012fa75748afaa79673d94a17d35666009001775a2b868b9b839c77065649bbebb90143f9014088e1cba06e2ce482dc8804b98caf86fcf0898305c61980880de0b6b3a7640000b8d9854e530ac567b7d29eedd91690a0d2397591c6a1b1f5068bc292b740f6aa5d38003a933c0560971d4701b31d537fb7c1ff68c40ef07221089f37671b101309000e0eccbc42284732aa002f2cb3197def9947c2b2fe47d3fea2efc71b1f3cd681082d043dbc1471a56a5d0a5c757b8c115277a2af2e044e56e5e3c2cf8756dbe51a347096a4ead46fe53f4c03fc100fe0009f6b2fd6ade28fc89230602e9221962f4512740857b87f415f134a224c5149e374fe22f3048f0620f1bddbc9acdc268a5de1296d265bac65fc2650b3de55e6bcbc26bc4d01dbf7548202bda03e35d4429ee24e44134f7f51b32fb69691a16c60a0347d9283a8e593d5a095baa01c590af4c1fcd3aca728bb5aaf03f48aca22c756a87607b4153a5ac6be59ebb5b9029002f9028c82014d88aab881c6fe3d0b7484b0da2b368542c231bfe483115994808829a2241af62c0000b90220a8317aae8cca53d039d79f09934b9c5d0b07bf13ceeffacf1011fda22a85505eb7c717168c18d8fb230a7a3f166a4e93326fa82884ad3093b5e07b4edee095d98bb92f357fd4a98201be26960d4253da6fcd09874b364595a47b95d2b50f8cd45921931469a302be9699779775b59f27deea2aaae41a010a47b825a46103b7d355f1c154b3422b4fbe4e62c71c5b6b98b627beb82014ad990bda2b6c06ddd237543b3652c7a029928153a8cec540311406260fd3a55cc5788610321d66c29f168ffe5d93f92378359231ff89492db2bd2e90a4d9c28263d75b77842584d253fd7316e61c27f71771ac7e7a3c8ae6921ff2280c459c36348e0a098fe8da94c1546c15db7968d6b2821b24edced45a7ca8f2bfb2b9bb7a497b950bdaaf771bd777e918887c0d2d6ad3b72c168228f49fae155862e0baef308ace6952606a660beee10da3fd2d29b5ac31f2d55e34da94a4274e1bd679fa42bccc5db074a070b899e28948680d82c7229223d846a1a2c19143dd99c78bc42c33490b85be5067a25f6361d6b803b315519de254191557ec691967ccc3d087b8799dfa5888ad748b7a6e164da0c726bc1f916110b6fe6a013ce0e28b79bee045d250657a70211dc11a5dee69a2c05e9eedde536a9911883e5ef2ee76729ff8fbc3aae0fa13a36daf01199a7ac60b21c7fcac00d7c6a80f5ce10b79f4666d69a1a45b3ec864a57f1f6fd492223c539351326d7a25b18bcfd8697f55e972607b9675b1d40dea3ba4c0b3c080a0e69a3802e5dbe5284f817eaa05c76127a3898633d4524f3da9ba8d7e7b98af23a05a2672729a0136c572a68b494cdd49ce47c2c0e33582b601632b3a1d15f3cc38b9016001f9015c82014d889e607b89f9d2717488ee3a5d83a713a9fa831ab7e68080b8fb754cefe26136c37abae044d7be8e1a3b8aa3ff230de4579b08bf12020e9ea66a2f282ef549cd7f72d056ded10c2fa21fe339fe56715960a4bacb65525bde1671a0a691f44c0ed582e64d3799c4ee453a4fbb700cc130eef66cc66913d919b6a96bd31efc3d77e4accf3a7c695275188ed2e5a76526e4706bea7df44cf6a36fb9e43d0e37cf5d6e3c5b984062e57ceeb1c5e6a9d0c418a5a83b77c4c99e8799fba27bd884e51d5df3db1562fa0b13cb1051ef5d5269b4215078384fa84cbcdd93cd7e67d166ebfb88eadc77cfab6a09fd1ea8f82f530ecf62d60d176d3bdf4f2eebf57b45b532ba6471fb53312e32c3452ac69c7b0ce227a61e69cac080a0434df311dffabb4af9df6fd81f48814ad8f5363567d421c5466423bf3bdacc05a0032341e2314432f05701cb222c2868894039e6e156ee6872ebc8739a4c45a43db9027d01f9027982014d880843386325d71bf988456fca4e1ec42cda830601c994c5e72917d21e4aa0f724ed1cbe014171f1be66ff80b90203e082cfea48d8bbd73dc4f299c37a26fcfe1286a62d17e6bfd13084a47fbccd302a44770baa03092d7aa3bf8f15281bde3418b5a6f610199a7ca97fc11df8058de81fdc05527047d32e0e4527db10cddaa2e1a190d7dde1987c0501a200df8eea07d61ea0028930e7422451b44295ce91f79de155d6169bd64c0cadae791e59b67544023e5fcde77eb509d6418daa17dba99d0f09c23c7df78d609f4af7c1ad95b01c26edae2080556b8e63ac632d78b87eb57ef23791c2336775ccf12f62dba46b65a5b5c7017068194fd2b7bff11923ac2dba3ba0d7e28c1ed2ef1c5d2069e189c09bc51efb571c63f2891acacd6a327dc810180290f9699541f4b65bdd8935e074f80887d3f6f4c3ecd75a54c95476b26b42f02964c16ae02532433d48fb5b5f779562224d1bc099f51d332c67cecb1e619bcda1aee26011a463952719987f705b12fbbbf34e3989d6b5c5182bddc569fb545de391ef10031bf1b0f673f0ea1a9763f652624852bee8f09dd517250da77dd194f8310086ba52032212ed38e014a9bb3f47d8a16cd463a977a443ee02d5548ebb5c518e5a0125c6645f2ad2d52f99aec5c88cf4aba79167cb8f7012386916fe2b863da27d16a7c3c350442ebf9b54a569ccfcfe4f4e64853fd810e6a5b3b3cba9ac8525a260505d12492b99437309f94b91dd68c7658291052e2c4d414f87c1d7b7bde565791fdf99004316f02ef4d7c001a05044b928ccada6036e32565da0b9ac1b51d4a0eb5d702efb781a832c120665aca027befe34f4cf0deb37ef259882c20be1af0efa2ab726e06eb33736ab2f0b34e5b90186f90183881a09a2f1c8cde2c488c2eb098e1a51326d83159c2580884563918244f40000b9011b643c223acabd55c37efc426850758db45eb7a0ccb908d9e2ab6a122d812921618aaf4e30c377ed8c7c5b829846b473702496e87f2fac0a78fe92a7602239414117ba9d42c354b05e5561f234e4fc76ecf8285abc17060e980e1713a3f0ab031a53c6757c972e363485581436b20fcb4aa524281e6765ae59362fe284cb6c9c26e3980cec0a9b2f61d1446e9a1679fd055fca089b838872a26f866cb09ceaa5a57a061440ba3a342807d83a5a83589a7297afba2c456c628954a3daa451cb42207f9de22fd5dad066647b8e8ed43fccd3f335298291601fd8737a2ed69cb89e0573fc8eef594568c236f8f976870f2da93c65f77aeda9ae17d812e16dae936ca069e489d3d820580c636f12164c73795e287db92ddcc73dd6b341408202bda0b8ad8ad3d5218e0e27145286459b952ffce119c42b7b143d3ae68f08991c6198a07bd60b6dd3efcb39d42fbd3b15f2f65f9561ed6106484285f3a9d235d2962c2cb903a9f903a6883c0753f96351f096886eb111ddc0775d1c8308a6ae80881bc16d674ec80000b9033e6cc26ae2edabe8f726535a61e77b09496c76d81407ade4466993d4785c16ae669c39a5f9ee18875389a6004576a39465d66329e18646036b9ff5657ba1ec659bb2acedda2862458a642949d15f2108c9c9a712216e2d9d13077a134a69c64daa48018d835b542cfa7861a12febf7b79023af48f860377d4d8bf99639ba627ae9844ddd982438e2a508b6cb89c87d4b78f31e42f842f62af9cd59a69f4e899720156f7a2adf1d348e9b665481165af600a3f781aceea0589215f06dc022fd28fc6025ff85e3d4b7c25c358f35ed5f5f025eb2b0ec5511634494515a197f3e06f4e8a2fef699f33f58ab71376581b455cbf592e1e657115448db5237d010399045e023d0d69797131720de65ffba81c41037657951db3bd5fcc555b8bf6944a67f1fc0ae9ddecbdbb955743a86d2ca82b6239a47f0d37759cb3bcca9d95d7ad084bd8269d06f6cee9effb2173096ef22875db79714328f2d80beac6cff4b3f8fbde3ea1a1040b6885d86bc92390ed2efa52181d3fcf6b761c0a14b8417ea3878d311d3690f93258e57848e926364fc0a60dcaa161a1cd9ea4fda657c5e868f59bc6d2ded1e264a100ff752fbc32d30728f13d74f60a1931cf1cd302aec02f4ca94541335c0f0717cda44c966db4c2c1e522794e0cc5a9dd84ed6355f979c4931231225096d3f651aa1970fd8a6de80325a6b7b3362b11eeeb3401df138bf8742bb94fca940ed45f8b4937d1645c98adad12836b19e09b59dd1e4cf020a2d4efeae49aff02a0c92537dfbcd4a560e876d0a3da71a38302efd5986e70a0592c02c4a8e5638869db811e47ce514bbe71acb864580d9f3be29e73f8af1584130a448b85c0a4a790d750a3d67a4f1c3e52b0db1c7ec28b891c66570c894b9955f0914981f28efef48616b004ca747fcdb448d0a1b6d7196e2ca002e17cfe65e7bb08027b95bea17ba0dd5b9a479726b5cd32a0fe24052c2afb163e60733e6ab77f8d1d2f606de15a31a2db1c8b7827434b64f794b808287f612854c7df802822340442cb00b8c508eb8d74a6334da415319557d4a8cb58247a7e65c74ef2238843fd02d24d6a859f02c547fab6e35903f69394659a2b1bb02fb89a613733cce7c4af817f6b8cf2ce38f425fa8b59b3fea76273664b8215d0503198393443c926b578202bda0115d2f3409265aaa2d214d11e19f314193884ce34c3274f4258d5f09a97172fca0418e2cf579d94373b0a81e66636160ad2f1de4597445af60d0ec37e9a97770deb882f880880f511ab07ca9dce1889745de5325aa780e8311fec19424eb7935928d6e5fc275944276ee070e90b9619e8853444835ec58000086428a36f8feba8202bda0d3d221e5abc91d1bf4721d9f51100bdb7e25f4e1b2eb363d200aa1b0c09727bba07688424185824dde9b365f31e258987ffcdbf3c850f9992ed80d0e71e54712ffb902d702f902d382014d88e4400f9aa703b1f98501db23a8d88543ec7b3d868309954b94e59842fa49a842609ce51ec1a4e9f75a00da8e1280b9025a30fadb0cd19a05ca7d20dbd28ffd1ec743d59a1169a730091be383f6c571c51a8514f9ddf9961a588f38bd388786c9e7efc5d0e71ca89e7f24a73201839f40e9378e5305f4174752c6eef07273a2c51009f04350abed1b6dbfff400ac6f790013028b56aa08f5090e4483b7bfd1b08042b8651dfb27520b3167e9b912e37bbefe7f13153571ef8ae23f2034df09ae737e672bd09d896bb01cc035322407ab3ca2a026f1d8d5beab70178c580a650874a57787d92b6f31f7f86ee939bf8fac22b23c6b6666b5e0241fb55dd4d397f1c78fe6da9fc3e66c2e34058e223a4567d259e3e1a3560bae9f5e2e3e7df1b7384b6af9a4155f1eeb61a6bf4b5e149db22109c635cbe9a4266ef48c211fe1236becc472cb7869906e27166f3f017ce75d188fa708e037fe1a5729b43892460458478cdaa91af1f9367cd1164204b240212101e631cbd027c814efd1e46368b37041836964dc6a76701c38810f36cc02ae93eddd5ebe83c24527244a55eceec6d47ec8df4b158fd1166a7d0d7bbee043632852ecd8e5aab24d71717a232eae9facb45b534f75103fc57f5cd8f978a362249a16e6b3783443bc5100bd1d8bbbd45144b7c63393f5d8169c4381f645bbbabc899e022d58e7b4293125d6c4d7ef75436b4542618636fb247b48ff823f52f416348fb767f6146c1f443147baeea5c6ca7fdcfe3795e09112224301f87c5667027b74b54dcc0f3c4e149a1e67aa6f8a940e1f2891980a6e565821a1f06d522eee5803650f6c0b8c8f5452804f9c456550cb8f1d4827c7fd1c8fe77b71aca3aef9be16494a4bf7d40b274d28ed9cd92a2169b6de5fdfa3ed1b6ef8318c080a008c406d42212f12e384b8f8bb7bb40d0c4660b67026646436ca589d143edc5a9a055fb6596377274cd6af52d95a127c503c0af5b7df6df59ec493d2bf15cf02bcbb9046102f9045d82014d8822e3c64dba5192b7843cffd35685424e576804831aa2e894b002add3a6fe3cfc260c378a187213b6bac436f3887ce66c50e2840000b903dd35dffee48e5855b9f4e7d47630f215334f242c738b2aaccc6e4a815ad70d29a94bd5fea67cd0cc855835ab9bf81c789806e311f744dfc370960d5246099d70e509571437c3c61e11c2971782d7ebbe3dd231c3025966d5ae37fea256ab601339db76c325884b7939ac8e772ff54c8196d35cb823cd42287ccad89e0f1a8092caae92612bc897cee16c73c18a39a5b1ba5bc5df73beb108cf5c896a420837ff53f6e601052ec017e75d3554c0ada83b7874ded4edab8b1a25e39c56c4666ae2812fe82f65f5f7d423ab3a173261ff29495a5ed0851171d1c261129b2062fffa4fc682cb41394f5ebe335bc2220abe7e950d9afa85f305eac439eec8eba9227352f592804f5b47208c262b220c1eb39d6ef89a92ec3ef051e9cca642658a8d8e55b35e78583d7a6cfc01bc5b9d579a1514c201d34230684e4385a1774f8b5f38b5191682a8b91b536ccd3821ee409028180d0f5eabf6e1e2e3dcbeeae0d92cd83e52ae68842bf781824cb7dc8c1507361d7d03b03bb15f7f7a0a9bf12171e01408f60b35722a5a819d7d9107fcea1b94184160cd9890f1f510207d47752fc27f58729ca8490b81ea720d5fcae71db92a9b140099047f45526d26af5da8bfe3e41beffe14d5d1cbe31bd1e50b9c38b9b393ef4b1b5514050e4a934d9501fc70d9ee3720a22fe18533b420cda21aea8c483e5bd3cb4786d6ce2d0f97d1a653253efd1c0283772e8ae43013dba4990bb6c7d9c7087c0d9b2fd3b79decd9a775989c81b87ccbb1e2d6b3c4df6dbe1b7e3a147dd8ff6998a0dcbe3f517899f2dbbbc788d5004d2de3d23224268406d02fecb0ba553123528c6b41f6f55aeaf8f32aa767a9f3113ca91d92e2dcf656cdef77f966a6b2cba83340658aa5c26aa0cb8ce54ae3a55b1eaafef66763ff4de971cd6a0b65a680169837dac945b0a7f13864795670922c99dfc6b5a5465e5043ad1b3205e4579cfc0e037f0b4e0a8b22b5d6ddba7d24b31388620d4aba83f84c5a1334261955d52294bd8b56d7175afbae015933ab1e0ef91e8161468f8eaa76a6f7a9bb8c8fc1195b9d8ff5dc4a51ff73a74b0640999bebcecb6036ef676c65e9fa5b1be22872082989c55a789fc4c2252452f786a13c4e868b85fbcd09bab689bb66dfae14c2ea7024647ad97728deed03314b007dbe461c1836e97f928308d39e5afc43ee3ae22ff47fff183553f56711880cc5ef72c5d66b4e2c6f651c57311d48fcc0aec762fae6444a5be11793be04c85ba97450673687734e681a1f3c64699686880d32d4cf87202b49ce13fbc8771fcf30d5593b41ffa61462c64061449b2c0a24ad8a03d280500bc86049bd55a27a05d70b12c7fd700454dbf3869b329a1ffa9994ecc2a6ec9572e3adaa0056c080a013fed42f6ecae05ccdb9bd8dc88ed44579b6a8871118710058f72c29f6db3b8ea03d200c0fb3e4416a51538d2ba41be88cfe830fa74c280e8b4b66cc3fad24ec06" + ); + let raw_batch = hex!( + "1bd930e08e94a89daf73710d130fc039db221fa427e3e9d10b5ff602fca4577fc203ad9313f493c51668a017c2a4ed1260401ae0dd8967eb390d13f2fab12f43bdb0cf432a6630bc76a84c50bedb2a48e562bff35eeabe9cc219de13de55412f6692e1708609ce3440ac1909a693fdf68b581342ecf8d480342c3e3b435349a5d903609718170fa9a4702fc7df772fec119dd097c017e8531040192c66d18eaa4261721c01c8932d0e8890ac2be0630cc398f04f556750355a3a608612f9d782f52746c2c5c83c8e01cc0b5afb9b97080505da0ed526076535d4a34650979f8f1f98ddaf306fa58591a92e25a86a1d62a3ba6d6b53be59da78c1b1a3128059e51e7fdef133a3e0979cfbb47040a51c6e684b6320b624ee51f731fd95ddf7fc672367b4bce94f92714dc4ab37394f3b3e612dab56829e8171d3af31a6cf940504421122cf830dfe1783a42dc48c2296849ef352bf18ee96eb5deff308e094b61e61eae5c02c14320345cbf250a6c15f725d6c2b12e8a10c1331f91d4161667dda26ea1f2a7cbdcd1d73070b70c818d9f543b7b3523e02b58f08f6858b951c735820579cf0ca7e4dff854cb2414a29556658374c977897ff125470427dfcfbf5c8bec622fd5b5d9cfcb898b3ea3846440ecdc29a7f99da330597db06d49dfd085d0b56bcee9b1031aacb1d71d7df7509b2cd76ab53620623cc85f880037e10a14e6b55758925f8ae7eac9489aafb831809662dd12013e9e8ebf67fba771c88da3157aec7ad6a4ee554abe967f1ccb486c47592eba5ae33812285bf3f26dd11d232f63c24a5b6e5fb285aa8950dbecc16f501c87665df4d159b307d36d554d54240306bd6ccdeb6eb37648c5c2d6fae684e2fb5608c2acfffebcc595b277d515158a141f2c8f2a005d5ed82e875c9ed3546149042a2dddfd82107d3067825968eb4cbe455b6b2f6ab2da38c3ad83a3a6d87fec0ff797916e6a5220218436a438d6bb44dfe5cba3f7602cbd7fa0ef7d000b9e02b05b4b867b1eef9b76ecbfc2d6f2df9955e4f8ca9d06f563e3991d86e9f194fad8d7c05e413bf68f02c5592696cf28f51aebd5fc6cd1cd76b3543b37f994c17f83b79c7920c01ff10d4d97e35689d65913b4fa0d5748de37963cdb48cd1416d899a3083df547241e17f5f6df8917ccc0c5639912eb99ed8849a2c8140187ee114fd3253b986c3138906dcc2db911e6bdfeb32fd0c4b8346d3e2b876fbe3d2f95e752b71f94c82be7a77b4ae73bebc06d03e8ea40dea94450887ba163826dfcd21038bf7f560db0190165d83809d398eb32f038186ce9b49ecbf2a9dcfe0be406a71f457514a47dac76990fe20c074893a34a8e7f59d4a945e3aa4e16b6c37a28d9a132cee8fbd5c7052ddca49cfe12a4c14e9492f2e6b480aa70e39e46b481b38c7ec36d24fff714a8464e0aa8c2dc3bacebfb59adc6a17e5377e6fa4e70af286e318b47897ce7e75a65ab445bb64ac6159ab48c1310b641fed5b40c84441a093af75902be5401a3304a3f48740908da9209ee6a66a5442bb3eb344fec8905a7b809c531fc788421da2333a9c3d84a5e0b2c59bc8807796da4f6924da6a3ef92ec94107b8ba4092d1cac44ff621db09c007bc007040006570794ab5289e3a323b98e261151a96b3ea240c0f612015d99996ed87511cfad3d644577ae4ca93a14fb250484781975404938bab804f8cdd4dd288ca384f7430ada7852095dd0b7c04ae9931aab4da57816172e71a85ecab00f5149e9929fbd4dfff8635f54ddd91bb56a86dd60aea8af18dc242026dad7b52f271db63881b39577a15f5b8f357d3ccc8cc6d79665133f571125dd592caa7600dcd7d72b5ba73c0edf74389a8a6e3d4d190b76a559a324d0fe39ea88bc6bc8c3dc30d89145f253b354134b38bdcafa3936aa1eefe10c806c2593502f0dd7cead691dbdf325a7b72da81c7427d2088ad9485332e4fff004237cfe54da30913e7e0f5cebf71691ac1c38731c84d91a233a96424dc976ebed809cc7c01a681f7c26ec078dda8c46066bd2a07ac4df05d18920f47aa113136ce45aa04b9a4732daf0450a88bd175b8086c4efd7992f21b0a0a90e00d3a17a0b46ccfe9dfd9fc901fea75e74d9d127118d0f8832cbee68be4d2c020350d533276cfe5b9d606ffae3e7492ccdb0099475b66c33ba9a1d6f58d8c8de19b8475059e61907a44883ba381ccda9e272b16d797779e4a1b4e3db34def79ba78e8f9ccbf592be4a63f4c9170f2c304ec65a8db539e72e1e5217209b0b38b61027cb82ecd3fc60dafe36cd476cd291f5dc574f818a19ca74d73331e0c3297e25619041b7ba9412255b10df0722463d17eb600aa8c9ffe3f43df2945252cbdf52113dfdb052bb2491299113c3e371b2a035f9b323318f17923f807a394cab6729124845833b794b0454c42c088e119110d767b5456c82fc28a2048925f5dc54765313c632704493126c75f40a499f6408263e61162357d5ff80e37617e80e0aedfcfd0284259d0e2bd644d54ab3166a22630ac06ac802e97f600a73b0e38fcce39189828cf98e1f5c6e8a7dfbf3670ec6498225b00446125276b6cab6004bf4d2e8c1341085b1ac9aa127bd10bb2ed29c7dd74f78baa4061874f24fef9d0adec31b81a46cabe2e860d890edb27b2c7f006a37f29b9b9ed21650ee7fc27f8fb7e16e4cd947bb47d094b26b2def138f04ab29316ed57f12f3a13e988810c045b7e35f1451776031f0524e96d1d4ce2c41a4a35e7e80a127620b2252f27ea3445b0cb1b49c4c33444237a279c20c92086bdc9b0de1e97c1a7a477dc0cf1efdf3040a09a8d1f3993682dfef3458cbad84470b94a52af59c2ba0f08d80b31954937dbb33cd743a099ddedf31402acc348f83e5bb821d185e14975e2a43e40d45e3da4b70fbf397db46395c95eb9176d70b70b1b4d802551c2b035166a82623a61f45e60b4c18570fb034e7061026002f7e15189b7c2ee30b804ca545894707287ca7996945929b08cd4410fcf7bf28c385be9abcdd0cf576dbf6c402c41a7147f14038c97f3fe8631cba55007db867fca4efbe1ff39f537548ed902ae01bd6a0a236a67c88a661dd930c15f017dce1da3ec5159d0fe4cc9cb3488ca09752bcec884d2adc6fb774eddaefffb1477d80ea9e1ddb0b7075ceabbbbb5ecb904866e0bbf0bf8f905b6f7ca5821b92f1109548fc33650f68a9b67ae20b6b165cd39de17f7691b8bfd70568c7239ffc66765d13b72db4ebf890a915d6abe3b557f70550be6bc96e5642b82b91eb10be8d669691df365fc53820e4cb6517f753510dbb9c51a8b5d38ff436fb0c61cdbfdd3f85f318897a64585a16af22cc782fa05fd7794817ec89270890d388c35c3abc1e667e266cdefe79211fd369a7f504a334a3fecebf3027fb2f0ab1af37090f97dfc1d8116ae99b2ecd742e47e48c399a88a1e1aacfb927ba4be5d9f0fb1789f91b1264d7e0f7edfdf48526c583b823968b28f716feeba8a87508249bfd938d756ec8b2e51f8f2624fc6467a7b764eff1384b306bde754b918a0918c122a7e6f6c1698ef129c99126f8d40a9ed97d1da1ca4c4fb859804441cad11ee84557921aba96371cb0b3a90cb2c0cc76c9b43d5cf16de51d6f43ca89c4017fceb239bdb708bf45e91b68fac6b27b66da9172c4d08a63f6759a8d08c513c1b2a702b1b51e1cd866f5fdcee679ed65dffc276cbe93b380acfec273ec53a664f559d29a46ae713fdbf96b1b23a1546aac5d8b6da6cebb128d61832d8a3b1e0587ebd1328867237ad9d43a4a2de95329d26ebdd455779cd19d4361a5d7fa45afd47068302b55d3efafc6b1e57c9e42af6e2507ba785c554eba19449d5f4c42e5acaf20e9ddc8ed37201c363464cc03d40593ef2fa32f81294d00ecf1862c683fda6ec4891f72a5b5b2b29f0d8c2bb415020f8db1ae7976b0cab93845b08d7a0842d6366e59d73b593b8c5fdf199ff6d6564ece94aadb59fed75951abd39f67a06030f2d34d57223b62667a8fa315cd2a27af7ced30d9ec78e71cb8d675d8d61924db42bb3105556a57775e7472e93e648d78fdbfe536e767a71079e1217faa728fcdd26d8be1cc1bfce84083d5272d543378cd430a096deccffed011e5ff741c92bdfdd4d42a8ad0f907d17490eca3fa52b0dad916189cd4b19161f886746a18b366d8bb1047746282d772670bdad1b0566b789dfe8348993a1eff2a3b03f51aaf362711afd6b0150ed8ee20b243fea04fd2e1f1eeb556d66b13f18ce72155f52af95cf6bb1c1a879a4cd9106ecbb5a6891c9823c3cb958a4b7652502e6d1258dda66af2136800ac33d739998995ca73ffcb541c37288b5fd898133d2a1de5c020154dfe1603b80775ff375e6cdbd69cc4557afc794acf9336da712626ed13e50fb60d6d7c0d92b10b01762dc96f8a7fd7facc6e090a7442c52e5e90cd3bd0a1359fcf64fe2a77a9acb296c48607a70232b19947b6d8dccb6adbd195c33aa0f9a3df6affa73afc9d96b17dcbd4e0035e005400e022883b79c11a9d3daef71c06223ad5a240021cb3018849dd4ba3b6772f103b332f1faa8ed2ebaac534ba4b46430d18093adca381454c5f59d7ce8c9f4944a84a5f9d598260b784cb284459798cd0b3529f76dc5dcf8507ebea12e2164aa7aacf8317289b02b3708bb25354b4f35f41134214782f6df124f096fa4786c6e6615be1a2a67ac0d8c74a7c5139b2028f074665a56a4fbe42a2b15709b73cd55e5d242d4fb1259d45c3366ad2494da03538c509456ad6beb9cb0c10ac61a163fd1ef3577af4d495141a9e6f2b8fd008c082e8b4592ecf66d411782d17e00c48c7e63980d5584786992749937503d3cc4c249671ccde9dbe9b4c4f9ed1da22e44f427466633541b675646d794894dd0e53223dfe3f0ceba6b969ce04421c876a51348f9022403f767466afedede7607bf8d06c31c8c7ab38661f618a55e9e2fad91ee8b238a3ca1c64616392b0faf61ea8135a5e4b8cff5a0a0008ae58fa407a60ab3748745bfb167713ff5c96bf9847f67f974328cc933d76259899f32c70f5e0b15087641a9fc09962d167cd6a64d5c251d3f7e751924e243c9fd41a475ac5f3bef284470f4510c6f3250fc4ff6827f3c59bcdbfd166e593e386538b0b3c2f0085b5f6e271371206d6a61a2d8f74246f12968c462cf6c842999e6067a9e8a47c1edb89ca69689ab583b397acabed4b22d100b754bebdf8f270c0ba9ac8d33f68609c55f94572c5684fb0578f795b88b926ae7722223bf3f32e4b68be8878e842ef38be46a23e0904688447e70ed3cb93ed194d8d4bfd24b0bccbb39f92a553551bd7a8d77a6d6180b90c61fb3efbf6e6dfb987bf028dc61e4c22c2fc1d714fa7e1fe671925a1de1752c563dab2ac372093a57611b196db489e152e342e49b0dd2d6d84aaf0baf849db17bd993369caa66b74282277f69d18f4b009dcde6cc3305817035a1b104d056507479d53dfae3386b05f6b4688833381c18bcef8a3e6ed70b47d21085c07486b5232a02b5d64f013a0fc6308d874b3fc4ccf44e016b5456efe45efa0df4ab239aae635e4f9c879cda1b78fe69cfba7b93eb4a36af3d20600fc42c0ccec24639dd53d3a2f67f7f22e8d744ae9917f1cb5819362c38f5b4ed200ba23f4d6dbe5091aaf7ff47ededafcf23421fe16aa42a583d3f8a96eac23faa269f9d001fc00bc003045006cf1a21b65f26a45980910e2222eec2aaa6c248dd1e433ae25f22b186c631ab96577a3c0cd5dcf5bf48162885b91131756ea916258ebdeafe262bf0deef40b0093788e97e864676f127832f5540ea04e0c737edd0324a9b4723a807a70a35705e9e27ff94945c9c47c8c5312e5ce4a0af4b243e210c15223732371cf89b13a957b9a6c44293b0e7ecfc6611b595046bc3e7345bf92428052bd8264db5f2fad4096ba44f9bf62ee1c803e33bb03bfb185b3a966e3c87fcc337331dee6f79ff3afd6d50ad823ee9aed593763b77a88c9ea33d6104fbb98cf0b2d60dd4eb28f4f977b37e29048f01a646df6101aa7d44dd1e29671af77a71d1ef3827d736d1b7f22427e63a957ddcbf65f2d4533461efb760bf8574a8649e87a5bd2db0f50fdd1d89230dbb66dff78740b2bd95dbf78aec6c2e3a89c97c752049126a52a7b37a059246713055139abc5610499a452d2eabe40cf729fb11ed87bff8ec1319f773ce2cb50641b04e6dd745879dd02cc01768061040190c8ab6fd4d1fa6bd1c9e3938c51121514568b61506fbd696f91b12600f0273f3ddabf8d9b573375efde5ead4ffbbb9ac7cb60d524cd7ed46ad5cb84dbcad7795231f0d4e7c05bb30cc31b9e02d4434aece3405f1fa7754a40571982778b5c78af4c6a6d62f0cea4d9bae5f015aa987dedcd31fd22fe7a8370399cdba6d68cae1485de5cc3ab6f04a927da53bd7fefa2ed7f820d4b677a66749f169a0d2d5bef60435edb3d701e139fac5e6ca42951874d563068adc4ae6ca0a633866169afbf8b92f23f37021c301edcc2b57a9126f0df6f9fdde4806bbd2fa3c9d8bea443013a411a3fed267cd4854669e5b710e5d6732a9bd2b8e9d9a522204e491501f2347df956cd008612a4b3b8c5c5326f5ccb1d269e08b1efff02a1074b3e4ece599ff26d2bb2dd6ba42f969b12c68916da13ebe9f9d19bb7590e545a7bf053d8181dafa54117084c1b24111460acf93ac4a85fe695fef00a0a6da53b708c24c601aa0e329b653d4fa11113fca0185d788baab7a647a5ddd6fd6780874fdafe1d1d27dddae0d29c3fb4df510b44bef18a216b908522ae9b6c8d0323222fe732db82d1878279426bc8ecfcbce218a381e96bcdff308be996b67e7889d6894db070fdeec85a919f0f1b8791a50921e6d7d8e943c05057ddac008ffb0c7b20a3905545ca1bbbd94fae6431f5b5618fa953a82db758d7f76e73d231689a5e70930b122fcf4a060df8bfdf47159f7ed9e0b0dcfc27a352785e9d8403dcd092c9db5b749cfd7aacebbfa96934bc24de29a9d022216ab7534c3b15232f5e655ea9173b20ff8f45c5e91ff4b8d346e4f8c2059d514dca5cc11e066d208f0a4873eb59ddf61f2516ca1be3c7cb2d913b6b1fa8329f028a4d545d751710233e2f65f7426536eaa583e574c80d88ca4dd2f98674e0aa874fa6f75a94e5e3128083df9d5344c3aceb890ff0ccb1b716fc3733c61f149436ac794a863ba875da7afd49c5f8a19b9a68fd3f236ff4e5ee684beb3e4a63fe2604b10f18ef8e72f7eff55fe7e0024267be83743fe57fcc508e9fc177c90fc9a73a3346438ed9e3d5d3af443990a19627a45cf5b01b5cb518c07a27dc8ce246156fcfb5b51e9adf207b4eb1a2933a179270cd30b0c3d986254be9af0f8d4069cbe3416a255eb671d86451895bac7a068119f19c53662bff7fefb5883d6a04cf7082c6d990492ba8782025d03f01e753eaf55e7e65289ba3719db0ec3461231a926ecf6ec6aa8e20eb896ead7a39180f113cd8a9897cc768e80b181c394a897aa248fd4d9f569af259ad9e6e69f02e4fdecfba5d7b3b72d97532a364275e30369d01ef8fccf43f7b94f27e3d7e6293da085e1d0b93dc0e84a3ee0b9e49c2fd2892f70306685aad4d2233ca1e4af8252708466c72c3a43b77dd6e2d0cce45e6407ede7e54e58802929790a1b3ef4743229cd3e136996a35fede076f4df911925cd2e3169dbfe7bbd611154e18f2b39d11d0c9def68e16baa8cfaeb6e8b4b1973169d3aa6c784eed172730a05c4b1f265ae1844edeb266dca67d20a98410de84a531cbf53facd4f3cab9d78f56db51418e1be62f2f4fb76ce1bffcb2e6a3a5a197b89d18f6c7adfdd293bfa66f918ba34fe5a3d97e138161a4dcd2af98afe9b5976e3effd2857ed07bf7809ab577135902703d0e5d081d02ab35a7b1cdb0e9c97509d0e7cf46da7fb775cd3504fb1647dc721fc675ef09925f71df66dc30efb66e7b33d1aefdd21740c769cb4214e07d890b1716ef538c4a5965b77e149b3b72727dd44aab32fa1506956a0fcdc8d7d47ac25d7d67371ac9c9d7d56f93e142d14df7877471492140fa36133b69443c31cf9dcea4ac4fc84fd93593872961d17616cc0467be8eb70460c676bd120cd72b0185e430dfc01f088fc3abd5cd0730708f88a9557e248747ac2197919716ad95fe6401195c745586ef38f5f0c2a24bfdcebd6d1e3b136e5e34ee9c5698c1f19e818d41226e43971614615c9e20f3a125408397e12f50ede77f8786607f6b67cf5ebc4243291bce1d7438d0154e929d38db75a9dfcced5c0949af85cb5cc91d95f5d64697dc21f37b31bc40ca9ab309d23d8fc50e9cca1bccbef27d79de533b2ca5f49ee17bfaf5afab8b5f9b7ca93a831384ee05dc6afb31fd2ce082133615dc36f39c9d9cbbb42e8e3f3e763d2d1f089c9b94f7ab183da49f68eb8a1648833136e4da99b873b4ffc2327f3a71d00071da308977da2e9cd2b96b7beb424a4c3127b7aaea40c8973fd9cfc3998d967c7c3ae522ee8fc7984955e54fa4c6a76e133ad7ad302b515303cb66282849cd139160ee7414cd878dd24e7bb858520dc50ae28295a32115147c8dc19c0d3e7e04e80a698bb02fb9a527fa79129daab12c97ae65b37851827246d3a0abf3d047a1e03624f6d3f6184650e4e225a8bb6a1120b40ad658fa729e17b8af540a4f5774bc56e9f932bab885d5272c78ccaba460cad5275b0cc97d098cfc1831b8d1cf3123819263cf597f95888194e54633cf6c23331f80a339f1a61af05017b210de405d5e3a5fdbba53d082765ad9c8bb82ef7dfb0ff417987de06c937b84cad437c75b5ef3fa9f0c5089cc20331d0026e0eac9176dca2506452e969731b61071c3ba1495fa089c034d643ba43740528e013008e04a32c920ce8041c026628a2267c648682026ca17e4bb2f9b95668bf716afbc49c8f3c56012bb8a6effd7393116de8692ce1b5fff224f856ff8589823734a5ee7403a8d900ce2854c5a8d60c6ce304964c3cc5b734672d1a19d0d887e33c244837221e52467b5e9036a4d3dd2bea9c69e67e57bec76a463bbc3fe5872894b9d69d1c7df3cf6dcbae55685c5d36724abc930b9368ca69cdcd38ff603a57cd224254e3ebdc453bd327b222b3da635523c7468f8eab0f50fff3225462567208e00c532778c98309d7c87d10af2e4866ba31f0a1a1803cbae792aec7290edac31ff22622f82b21c62b3f497371213f85aaf1733a11fdaf2fe5e7dc3cfd822e26cd1875171a034e2f30edc4cbe26ea0025445921c502e05707b34feb9069bbce9bf05898feff72f5f1e77255f1a3208d298b39e1437c0f0589de017553199314ecdeb8edfb2f131e13ef2b606b35db4af3c9abbddb2da4ec1dcb2efc64f38242748157459b647320d6150842e8df5c109a778f108c61c9303ecaac0c3b69c23d5a404ff7ba27b9b5549897f5b5287af46aa58a248dbf65f2b303d44190bd5d711a1b9ec0f9cd22facea4683ccc910379a9885a4c48ea91c76fb72cfe75fad1ab3d8eb23ad96e39c31aa7293040f78fb9834f3051225163d549a67bb079c3275a4c0a5442526eb74d82d36bd353af051c317c5fde944d5504c6967950f58187197acfc159eb9308dd1c9a26c8cd5acc4c568633c443475aec9ec74136afd513299e425e722a3b00c376a39c957306fc1352eb7c62226a5a34520da4f020eff85997bf208b018795113cb24daca8119d2845dcb0bc681aab967468522acb7acd7526a17dfd4fc2cac819bf477a58dc63fe4cbdb007a035d2812e8a677b2e7946a1819acf5ca664c6a4ffa6579a4ec60910091154d7ca9f90e864d1e9863ad9fc70b43cbc508f3e4dcdfb2cf5fc9eb64cc0effa7b6156a57f97c4302bca139cab59941aed5abf56bfedcab81803d909045a2cf6b9e0f25955e57f5264f631b382c561d4daa5fbf009882e1ef915a0910e76645e0669ba57e5d48dafc10bfad40534523dffb4bdddc029d6334aea481590718f01c01022883bbe7b3a75c8628f3c02ae3e8a53a5afc736198d9e1a92c51753043a293cb26428e921db44d36168611aaffb96e38e6ec8db2801b01cc4d3f0022d3677e8462972a4417f434937b70e45b88c6e3faef3c5442043d0d4b6bab6a0e82f5eae911fd5a9eeebeaa8037af63039508f036608a8cc909cbf586d391ef3eeb0448be00c4c03b93909fccfba0ff6098ced8fac8f7eba830d851821030ea765b73b9151454ab112a9a4823b6ed73f917abb88990397ecfa4d1c2c607b898c1e476b1c72a633e2881142158b30c12594033896670fbc0d78f61b46b370a84025e5b220c6c442834b4a9df12f4b29c55506ccccd04815759b2834d9fb2f39f4557634464424ba1082c30c2bf715c4bed8d918c3cfd633135bc8bea596154740ef606fffdd2593f20e472492f395d703e1055827ec740df862a70605baadd4d184f6637634da6486793c6f240d0ed081637c556a0545297dff3f8a4bc83498023bfe9599fa8f94f1b6dbcc3e0446b5863fd4eabd6bca97df8fb37ed6f65c0fa9356316944b81724f27755a4b05583d59bd9dad2930a1dcd205c81c9611507298b90b42e08b13ed2fdc0fb7c4d397db7413df47df41fca319d0a2ff8966f0206a3bdfa67ad9dc044e00b301699aa8ed0d14f61648ff08635269e0889418ebe7d04fdd4a1e711915770f8d5c5fed19ce15f2e404c51cc354686efc3fe7bf5fa0f03f3a3883142cda47d0c0f37167fe58d0ac94f14d75e2585d3b9823ebc963da575db5f65733b6d35a6938d3b78a11204e8a54d4795d05c739e46fca5c8239d56e29f36d78a1ebba04f918263570cec4dcff2cef06c2da0db3c65acda270420c976d0949843b7cf6bdf0c68354b30a9f6aa588c111b3a64b6d1f57690e3d46621af3139c26dccf16a09f2688c41189243352bfe8e8871e0c0d1a2cf971a8df844627092d80c16e0267c1aa7bc50f97027737c45c9b334f5b02696ac0e822c970dbdc369c2e7343fdc710f89e99ef05c6b82fc84a20f93c96ee951a47379b8e29110138ed75207b41cbfadfbfe586a0211515ffc5d3008a8b8ed6beaecf693f74f435eabf7265af63ec10707a0b2d8cfb733e382e8ef0beabee9596c775db147ffb5d330b3b741bedc412dfe606168ade1b85d34e15a4b2da153215af27d95f83d65dce00171e9c8da8d92fb810a1aa34ca65a91292c1a4892dcc81a8b1966fe2e8f1cd1ab665b646a69bed401ddfc4d3d6f578be09beeb91d81edd4d0ccaf0edfbc573d70ad478cdf4f5c65c818ed6fb224738cb64f7b80d0f66e8c6fefb6f49c9ab59f0b05c900a1f1a55d51bf49fec5a6a67d162658c4e4f6d2cbade0f96da86afb15bbd8a91e4090ed378a4c31f65e03b53c5a816eb483ec7b6fe36457586228326e551a4ce6bc904c29a499a2cee9e447d318f36fc52e58fbc4cdcc3fddb37101f554b0a4bfb93f047298cd073c583fa0570d0daa821d33a72b8e8afcbea0a12a5cd91517e49f594f0531a07573cb06f08cb895c5c82b6dc8ff951decdfe306b5012c990448bedd4df17502cc002f00231040199c6fe61ff532e7ea01be14300e2bc7ae7c0d236c3f0c09e978f354bada35e719f2ebda965aee8d27148c2efea242ddd7cd41e8c302a2e597d9b3d1b33ec84f07bbcde86ccea2b01591edf17feab8ea9b95744d5a6b186ee2ba42ca92ee95d0164187cabc59c397d202aadd2e2803ec978f8ea376d8ab046d950ef3a4efc2defb35ff0402ec343cf1e3e70ecaad69f75d1c4e03ef951e8b9d3bf785d178ff19ff1432cc14b33808b86c1c39ac9c19c62fad10f41e9ec8ff95f556e4bf127e40627cb7fecde215197b1243fdca58c3ae8542cd874fb542e9f746ca7490edaccdd91bf8f4bff7bf6a7bc40fd28a67364db47f164ba8784e825baaac670ffed2ad9c5d56ae6f9a1cac9c43d28ca3b9fe28bb7465a4767ffa432092ca77985bafdd0a2f5bce2b6472a10a2b0f3cdcb60b14233256547d826b53b010682af15d0e29ce6b5dc0242533fd8f2831fec31d9dcb1f6e67e3eed94ad225c29dc040c00bd0170450062348f259d22c13bd80a59dffa8900e33af85c1012652478e18f0e64815204fd417c4bd0071d79b5e9baf904e20f436e8dfa9dc4af7b2f0f06dd6901fedfb275664190bde61df2c7b7849f0ef697646296fe42a684416bfe2be846ff4449b5cf8ad658f1804d90195c10324cfb071c764ff61355c64e759d1a4e9d631a6d78a760a139737763203600145505bf1a7f04ba4106014fb9104b57a8b44dadfa4a7bc1ded25dc9c252594da3a5f52fd364f29a088e9f451502be292785c15de7a651e3ae2a050e0539c5981c2d3406d5a0331ed451d7988b643bc658d258b4f47506bd02d6fd2e0775bcfa91b368bf51207ebd2d63180cb0f02d5b9f6be1b02aa1a962e41c8f26e2ce9dc15b131b9dd4e547fce08e99b2eb1e56d14e19f697bcec0710c7c60e28b5d9af87d9be14614f7b6c733c2aff9c7fba1f36503ad092daf2607896b06ab01fb6d1a4e4961b9374353ef340b4a65a2feac0792efccb67f2749cd73a60beb76cd304b2cd3e80832835d0cb1debaef54f8a3965a47f0993646ecdeb48cf792ae30a0896e1a1eddaf4f09332c1f352ce4347a8faff316f16850cb0367539f39a022bb34a029b12ef8b6712abb4565570a1d172c2bbd4b242f818b5af1dd46eaf106009a512b53c6b945b6acba91f1d8fbeaf224dbc904172c3e3bdc4fa648e1a240dcf2a1213529d6be1cad52bba9f74f5515ab08d1158cc3d2e6e6c9ca9a089a223335632c79a62c4977c417c5a48d1f63d6a0245856666571d55f03cbed3d07d6be645b595092b8d7acf7cbfd00889a5427fd546d19f44f4e1d6348670d91fa02e4ccd885f5cd87308c190bceba0642d7fbc975ff0ff58cf78a26133488bc538ff6cad84ebbfdb39997a79a0d99eba01310f9020803132216dd8c4fef0e8307cf10e309d5399dc2bee5d2845cdfd30320b212a214f8d3a33d14f42ef143cd33aec5a41d32d589b0ba5b6d8fc512ca40611e5dafc23ee47d111b6008ca94697177a14e3f0e66ab41f2f94c2e37a3e41717c7ebca9318d26a30d136bfe5da7ff73a7fa637f88d0787968986875d7c5d0d4da839ea1990c1cac315a187c3d3843ea9504a4d4f6a6b5da7cfc3b61b3ee9984bbb9789728e94c3663e2bf5331bd7f703d6f40f424e18d8adc839d2b121f7b4b4d40f0e47ac4b808b1e7e45c0204c2fdb2da3be8b59dad1224aab78ad447d52823c386f976d716dc6c6ca3f3e7e41746afe8e9b01946446b6e2eec7ba94db910febfe1e7fa52ffd6390e7f9c5eb173a4ba590f593df45651dde0ad68e535d8a23c46e3f6a7e855c0fc5d2190b57c9ddd7843eb093e5ff98f052b3b81808d803e9a88d9e5fa48847a3c3d18894ce49637bdf211866f2c71116384c40c82236cf84f82f213bcd5f4df22fb0f5087ebb7d344d33bf3087939388b8ab9ce39e4b6766ee84ae7c812e030bc16bbe58aa5fa7837f36626ef47b1b13872194d585381f3b17b488e3a0fdee45f5f113ada681f9913fa2bca3d70a0e7cedbe8c5dca828f116e9d4d2e7fe9cb25fe2fcda8322869afe254eb254b869d4819688782076bffc273eb9ca69a8b357a0be9682b06f959530a848989722c8a9f2c79d8f07ed80a76a3ca557280b830432de6571ff342b6b3a7f644aa7ab96733de40e5989774fe2e0bbf9370e58d4c6abedd5284b6c9a8f140459f3b5289678947c69769fdb20295a80cf26fce7e8c5b245cf139365aa1b8068f50c54fa1359710bde46fd74efd941ff4ef66345f117b0bed8346dc09dc17a6a64093a686736d4c4ba9547503826011b8fbe3a2cfc7ac3b51bd6a575eebd016edc987e49d435d4c2db9750dde190cfe44e96daa99a58c0c6c3cf24debafaed0610c5e6b5ff575c1c5f711ed8e3e3ba9b260b2febabccdc9f54e6d0f81c8b93fa036aa6cd9eb796ffd49c1a9ca5563bf99f01e90dd8cb3e365fe57131e7bfa15e52fe3f60c1cce049690de1ac72f45d1849ebd53420cb136b071e4ef647eb4b9ab528ad0f9f7c776f3fe42c570bc533e9f79cc793bf4149a78ddf0f8644bab7724b3ff55b884c4aba7aa6bd601269109fabf9d582e48691530d09f34de8658d8dd12b09755cd0a53886fa6d919cf81f77f52b5b0b9fbaf5d03d6a4266cd695984935e852b7a70cf2565496b84d3372e539a8b068109d44ad090b33d057ca3643380d1cfcbf5b34925e368b91dcf0f5fd92e84e7daf14bb907e6e4909c4959e885e9ba5e769cc476ccd9bddff07446251f9ac93afbf664449d60c7c9b56d041fbe584245c4ec8c7cefaa11f7984049bdccfac10afc31799d781b91f7080d37d443819291db27ad7cb70241a7da327ce5e22d76184a4e08bea89246c5b723374c084da38764edf91346aed329eda99668a889349467f752567ee00a5542efbbe2e158744e4e49abefb078a15efdfa1897f43085da7e1295e17ea626789af9b83d13c23faae98c6607da3e521fcf7c36aefe7d9b947b8cd6fc5842c8de3ed200fae36555fc510d0af47ac08a5c06720884a4c8ab90139562dbe6359c1926b4d5c93403b5021b615245b7e68e47145c9172e3ac342bd54a17fcdac155cfd933b51d48e5f46bfa8b11bf8165586ed2ef43740e119efb1e31ff35e828469456b8ee8a9171d8f550785312c3441588a9450b2832e08d803c13466e342a435a862a150c6dc29e0104f012bed29717adcd3c4992256bababd43c4e3991f7c5725dd1b2a486d2ccdcd6ad948f6f53da4ebcd66ce794f2abd5d363b40cf21607475c28680caff3be00ce94d4d9a2a1fb430cccf8154ea335feee4b89fa6839ea9125e97f068899de6f916004e229ae7f9b32b009af9398a83ea0912a27b379202750ce4f5209afb9da6331e6172a4c286fe0cba6e881758423c02a4bc99c363cab1ab9719fcbe7e37aef692c5ba828ae67a208bf5d5095c06be00e7b786da7d31f4ec72e8c69708c03a55c54a84e4b9bf706418629a62ea41a6c4ab7c858459ee01e940c9c99301d45a3c16b5c980fd751d65361bf64f20f9fa5cc207e998e46236a65d393b22d15ed8e388eb086104cecbc64b3aa15e025f0fdfafc889a3ab919923e28afc60ea724e405881d8096fbc26ec3d9eacc6e8e5b59b7bd10806e2b5a45af5059153df9718d2e85322809dbed51e92a096b82f27e8ff418400c314a9bed5e449de8c1b9493c4cadb7d7574c3a1a94bfa5c47750bad7c9d7dc47be8d683b892de0d9882d6a414f36bfec308742689333c6d07f88c8494a9fd52d0f5094c6ebb230b8ebc4cc9797e1d64a21a6db37130018abf696f28f30713fb7c3a55be8bd80cee89e9ed5295e804d2e48b10729b759d3cecee1d6d11b987b7d5678b6bdf5cb6113adcb7eec8af5362e45a1af5664bd85cc90d627e62f1f44ec932b74766c1edbadadc5de3fdf59c05bfbac44307ef94bae54846519b4987fb4c5725d593a5d84e635a912b5203b130482d897b8001a12a1fa4323c31bc30f83ce9caa3e5b6802130c69d633fe389c8c6e2d9b110b5869b54a9c9df7327d9f3b8fb46bd0f4c9bf299e5ee4b181ece08d6e978836aea653cbc22ced393d749e956ae2775e877dd87e9848c681e4af9c29f0ebc6152822318c8b32bdd3dd2388a0196fca2c6a176c37c645686ddd5e359db948bf1fe122958c68eca414f5a3c2d5ab4f896ce4db22d09cf540f6ce296726f5ec1e63203f79238fe75a7468ee51ffff67c4d103129a9d9c97e8dd8b8d0b52b6afdefbf1bde912f3cf42b7bae14dbb98d2208293bb0061192c12d525e1e84f0a83df6778c3f48d3c3bc0ceb68a374dc2c80028267c73fbddb09ff085ce5ec58a596f4058a3579ccc5af4e2717e1e6381d7cbc8accb65d85e1f787401086e11628b16e58f9141362dabbc566866d906d813632928ea551b39217239510ed37eb745e378f69fb0796b442ba11e8fe7ac3c0c72dfd737961a61ba36ba6c94e1873e00b8c3108a00ca6dc1b55ee524f6e0f17fa9ad7899050d1fd01134658749cda00ac9d2ffb147aa745e18dc677c36eeb1ae6b903071c3aaaed860ba4c06f706f7deec7eb6977de1f2d78b1df7efbae4acdec1ec35833f55321d4601995a15271f1b32c60662a428fbb3ae799d827136e0ff3496a6bc8251d55430631cfe500511787776894147330030fb47cd62b3cc73104d4b759ace3fd2cd2a936c3e65ff71aa2012bfaf2d7c47bb33d2885a6cf1b75504d4bd007fc59c947270c49fe53976cce349ef177c7d17d209abfb4b1cb7064cbbdb711e19f5194bd0402ef97e6e3210096b51fefc8985babbfb642d0c76373e1a23a8690662f5767d8c67e3794ed98cdfae16981aa5a008fd3fc8b41dec0642602d37576d01c2b87dce2eb5575143429ceaf6ad2fbdd709012a937280d35fb35e20ef67498ff72fcac92d25de3213944d550963c9696891285b439efe77376f2b9c8b5fac954998475745cbc76b3898f9eb09fe33be0b7619e6ef6379c41c0bc04fb0f15c988426fd51853be56025c50452791a6e3341fc5a558223d3e2aba49f5e3ceeedeffded3ed55e615118dba1fe14c4fa120a5f6ffb1dfe0794802a11b041b4d83fd90726e285cb771101e91b9dd180d42f30293e0df4f8952f1c5cda633136f1e30c803653dd90683f5dc722be491434fdd504dff1c917432e6e04065c1044b6b38d1d61b57f4eded135d7de22cce4eee11cd1e20e7f27536a75c291269e3c0a229a428a701de5d562f79c98bd87622beb7904f17119ec6ca8918ce4fca462efd6541cf982dd3a411f920068679b346efb363af976421b78dad8e2104a0e6b0cdb7e79daf967b66e68676044c36ee2e350f6f39f5120509e004ae7cd96542fef78aaafb64ddae778f8117a19459f6e638a969c3e166d8ce1bbab439a834621dc41f1f0c4e9fef18cb6d2bef30852a499277ff3fea4c5f79bfd894354d567c17b38e2e1db4874cc61e28ec951a92567d3eda5a7e299fb84edb235b9785e066f2ae4d483794dff059f9eab82433676d8db696ca98a849d61271c2eeebe6bea3410723ab20c550b62e6d7405523763832d5015bac29e950cb0b96809b41729537b627496f10cdeea00fadfab49d15e4843cd6512e2abbcca9e2abb631306080cf3121efe2ba87fb9972bc28965e59cfd9d34e3b9b275b43e793524daa5774360881a31f029181ea4a1d6788a2c1452898c89789b46ec6a8beab6d9aac3193c75a1b6f25bf5a6dfbc80e650840aec9521c6e739094e0e4398cf377897bc14d865bb0ffec2e7d67cb0c504a38c5cb98d2c39a8303b5b13eb7290c8bdf7d78d59bc1e4a2918eee0a5f4c28c1b567aa8d9f2fc7257f94148266465971e0946e55cf8f78b9c49fe2ff6dbb837d93ff6457d41f1af321c8b513173a91c6624eada68e8b91035e47133f91eed223fd86564acb10f1718adf5bbc81cce6cb2d7acd4f3c1b2f334b7bdda2a289dfe1008f6e702dbcf3fdb46d39d3d71e3f10bd2be6d15bf30f15da1f49e98191ed705e321c2e428e8cdfbe2f6ea9a714c2544c7b19f61e8e54af468318a3653a2b5d4e770" + ); + + let decompressed = + decompress_brotli(&raw_batch, MAX_RLP_BYTES_PER_CHANNEL_FJORD as usize).unwrap(); + assert_eq!(decompressed, raw_batch_decompressed); + } +} diff --git a/kona/crates/protocol/protocol/src/channel.rs b/kona/crates/protocol/protocol/src/channel.rs new file mode 100644 index 0000000000000..e62689cdd58c6 --- /dev/null +++ b/kona/crates/protocol/protocol/src/channel.rs @@ -0,0 +1,343 @@ +//! Channel Types + +use alloc::vec::Vec; +use alloy_primitives::{Bytes, map::HashMap}; + +use crate::{BlockInfo, Frame}; + +/// [`CHANNEL_ID_LENGTH`] is the length of the channel ID. +pub const CHANNEL_ID_LENGTH: usize = 16; + +/// [`ChannelId`] is an opaque identifier for a channel. +pub type ChannelId = [u8; CHANNEL_ID_LENGTH]; + +/// [`MAX_RLP_BYTES_PER_CHANNEL`] is the maximum amount of bytes that will be read from +/// a channel. This limit is set when decoding the RLP. +pub const MAX_RLP_BYTES_PER_CHANNEL: u64 = 10_000_000; + +/// [`FJORD_MAX_RLP_BYTES_PER_CHANNEL`] is the maximum amount of bytes that will be read from +/// a channel when the Fjord Hardfork is activated. This limit is set when decoding the RLP. +pub const FJORD_MAX_RLP_BYTES_PER_CHANNEL: u64 = 100_000_000; + +/// An error returned when adding a frame to a channel. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ChannelError { + /// The frame id does not match the channel id. + #[error("Frame id does not match channel id")] + FrameIdMismatch, + /// The channel is closed. + #[error("Channel is closed")] + ChannelClosed, + /// The frame number is already in the channel. + #[error("Frame number {0} already exists")] + FrameNumberExists(usize), + /// The frame number is beyond the end frame. + #[error("Frame number {0} is beyond end frame")] + FrameBeyondEndFrame(usize), +} + +/// A Channel is a set of batches that are split into at least one, but possibly multiple frames. +/// +/// Frames are allowed to be ingested out of order. +/// Each frame is ingested one by one. Once a frame with `closed` is added to the channel, the +/// channel may mark itself as ready for reading once all intervening frames have been added +#[derive(Debug, Clone, Default)] +pub struct Channel { + /// The unique identifier for this channel + pub id: ChannelId, + /// The block that the channel is currently open at + pub open_block: BlockInfo, + /// Estimated memory size, used to drop the channel if we have too much data + pub estimated_size: usize, + /// True if the last frame has been buffered + pub closed: bool, + /// The highest frame number that has been ingested + pub highest_frame_number: u16, + /// The frame number of the frame where `is_last` is true + /// No other frame number may be higher than this + pub last_frame_number: u16, + /// Store a map of frame number to frame for constant time ordering + pub inputs: HashMap, + /// The highest L1 inclusion block that a frame was included in + pub highest_l1_inclusion_block: BlockInfo, +} + +impl Channel { + /// Create a new [`Channel`] with the given [`ChannelId`] and [`BlockInfo`]. + pub fn new(id: ChannelId, open_block: BlockInfo) -> Self { + Self { id, open_block, inputs: HashMap::default(), ..Default::default() } + } + + /// Returns the current [`ChannelId`] for the channel. + pub const fn id(&self) -> ChannelId { + self.id + } + + /// Returns the number of frames ingested. + pub fn len(&self) -> usize { + self.inputs.len() + } + + /// Returns if the channel is empty. + pub fn is_empty(&self) -> bool { + self.inputs.is_empty() + } + + /// Add a frame to the channel. + /// + /// ## Takes + /// - `frame`: The frame to add to the channel + /// - `l1_inclusion_block`: The block that the frame was included in + /// + /// ## Returns + /// - `Ok(()):` If the frame was successfully buffered + /// - `Err(_):` If the frame was invalid + pub fn add_frame( + &mut self, + frame: Frame, + l1_inclusion_block: BlockInfo, + ) -> Result<(), ChannelError> { + // Ensure that the frame ID is equal to the channel ID. + if frame.id != self.id { + return Err(ChannelError::FrameIdMismatch); + } + if frame.is_last && self.closed { + return Err(ChannelError::ChannelClosed); + } + if self.inputs.contains_key(&frame.number) { + return Err(ChannelError::FrameNumberExists(frame.number as usize)); + } + if self.closed && frame.number >= self.last_frame_number { + return Err(ChannelError::FrameBeyondEndFrame(frame.number as usize)); + } + + // Guaranteed to succeed at this point. Update the channel state. + if frame.is_last { + self.last_frame_number = frame.number; + self.closed = true; + + // Prune frames with a higher number than the last frame number when we receive a + // closing frame. + if self.last_frame_number < self.highest_frame_number { + self.inputs.retain(|id, frame| { + self.estimated_size -= frame.size(); + *id < self.last_frame_number + }); + self.highest_frame_number = self.last_frame_number; + } + } + + // Update the highest frame number. + if frame.number > self.highest_frame_number { + self.highest_frame_number = frame.number; + } + + if self.highest_l1_inclusion_block.number < l1_inclusion_block.number { + self.highest_l1_inclusion_block = l1_inclusion_block; + } + + self.estimated_size += frame.size(); + self.inputs.insert(frame.number, frame); + Ok(()) + } + + /// Returns the block number of the L1 block that contained the first [`Frame`] in this channel. + pub const fn open_block_number(&self) -> u64 { + self.open_block.number + } + + /// Returns the estimated size of the channel including [`Frame`] overhead. + pub const fn size(&self) -> usize { + self.estimated_size + } + + /// Returns `true` if the channel is ready to be read. + pub fn is_ready(&self) -> bool { + // Must have buffered the last frame before the channel is ready. + if !self.closed { + return false; + } + + // Must have the possibility of contiguous frames. + if self.inputs.len() != (self.last_frame_number + 1) as usize { + return false; + } + + // Check for contiguous frames. + for i in 0..=self.last_frame_number { + if !self.inputs.contains_key(&i) { + return false; + } + } + + true + } + + /// Returns all of the channel's [`Frame`]s concatenated together. + /// + /// ## Returns + /// + /// - `Some(Bytes)`: The concatenated frame data + /// - `None`: If the channel is missing frames + pub fn frame_data(&self) -> Option { + if self.is_empty() { + return None; + } + let mut data = Vec::with_capacity(self.size()); + (0..=self.last_frame_number).try_for_each(|i| { + let frame = self.inputs.get(&i)?; + data.extend_from_slice(&frame.data); + Some(()) + })?; + Some(data.into()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::{ + string::{String, ToString}, + vec, + }; + + struct FrameValidityTestCase { + #[allow(dead_code)] + name: String, + frames: Vec, + should_error: Vec, + sizes: Vec, + frame_data: Option, + } + + fn run_frame_validity_test(test_case: FrameValidityTestCase) { + // #[cfg(feature = "std")] + // println!("Running test: {}", test_case.name); + + let id = [0xFF; 16]; + let block = BlockInfo::default(); + let mut channel = Channel::new(id, block); + + if test_case.frames.len() != test_case.should_error.len() || + test_case.frames.len() != test_case.sizes.len() + { + panic!("Test case length mismatch"); + } + + for (i, frame) in test_case.frames.iter().enumerate() { + let result = channel.add_frame(frame.clone(), block); + if test_case.should_error[i] { + assert!(result.is_err()); + } else { + assert!(result.is_ok()); + } + assert_eq!(channel.size(), test_case.sizes[i] as usize); + } + + if test_case.frame_data.is_some() { + assert_eq!(channel.frame_data().unwrap(), test_case.frame_data.unwrap()); + } + } + + #[test] + fn test_channel_accessors() { + let id = [0xFF; 16]; + let block = BlockInfo { number: 42, timestamp: 0, ..Default::default() }; + let channel = Channel::new(id, block); + + assert_eq!(channel.id(), id); + assert_eq!(channel.open_block_number(), block.number); + assert_eq!(channel.size(), 0); + assert_eq!(channel.len(), 0); + assert!(channel.is_empty()); + assert!(!channel.is_ready()); + } + + #[test] + fn test_frame_validity() { + let id = [0xFF; 16]; + let test_cases = [ + FrameValidityTestCase { + name: "wrong channel".to_string(), + frames: vec![Frame { id: [0xEE; 16], ..Default::default() }], + should_error: vec![true], + sizes: vec![0], + frame_data: None, + }, + FrameValidityTestCase { + name: "double close".to_string(), + frames: vec![ + Frame { id, is_last: true, number: 2, data: b"four".to_vec() }, + Frame { id, is_last: true, number: 1, ..Default::default() }, + ], + should_error: vec![false, true], + sizes: vec![204, 204], + frame_data: None, + }, + FrameValidityTestCase { + name: "duplicate frame".to_string(), + frames: vec![ + Frame { id, number: 2, data: b"four".to_vec(), ..Default::default() }, + Frame { id, number: 2, data: b"seven".to_vec(), ..Default::default() }, + ], + should_error: vec![false, true], + sizes: vec![204, 204], + frame_data: None, + }, + FrameValidityTestCase { + name: "duplicate closing frames".to_string(), + frames: vec![ + Frame { id, number: 2, is_last: true, data: b"four".to_vec() }, + Frame { id, number: 2, is_last: true, data: b"seven".to_vec() }, + ], + should_error: vec![false, true], + sizes: vec![204, 204], + frame_data: None, + }, + FrameValidityTestCase { + name: "frame past closing".to_string(), + frames: vec![ + Frame { id, number: 2, is_last: true, data: b"four".to_vec() }, + Frame { id, number: 10, data: b"seven".to_vec(), ..Default::default() }, + ], + should_error: vec![false, true], + sizes: vec![204, 204], + frame_data: None, + }, + FrameValidityTestCase { + name: "prune after close frame".to_string(), + frames: vec![ + Frame { id, number: 0, is_last: false, data: b"seven".to_vec() }, + Frame { id, number: 1, is_last: true, data: b"four".to_vec() }, + ], + should_error: vec![false, false], + sizes: vec![205, 409], + frame_data: Some(b"sevenfour".to_vec().into()), + }, + FrameValidityTestCase { + name: "multiple valid frames, no data".to_string(), + frames: vec![ + Frame { id, number: 1, data: b"seven__".to_vec(), ..Default::default() }, + Frame { id, number: 2, data: b"four".to_vec(), ..Default::default() }, + ], + should_error: vec![false, false], + sizes: vec![207, 411], + // Notice: this is none because there is no frame at index 0, + // which causes the frame_data to short-circuit to None. + frame_data: None, + }, + FrameValidityTestCase { + name: "multiple valid frames".to_string(), + frames: vec![ + Frame { id, number: 0, data: b"seven__".to_vec(), ..Default::default() }, + Frame { id, number: 1, data: b"four".to_vec(), ..Default::default() }, + ], + should_error: vec![false, false], + sizes: vec![207, 411], + frame_data: Some(b"seven__".to_vec().into()), + }, + ]; + + test_cases.into_iter().for_each(run_frame_validity_test); + } +} diff --git a/kona/crates/protocol/protocol/src/deposits.rs b/kona/crates/protocol/protocol/src/deposits.rs new file mode 100644 index 0000000000000..a1485cd326552 --- /dev/null +++ b/kona/crates/protocol/protocol/src/deposits.rs @@ -0,0 +1,679 @@ +//! Contains deposit transaction types and helper methods. + +use alloc::vec::Vec; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, Log, TxKind, U256, b256}; +use op_alloy_consensus::{TxDeposit, UserDepositSource}; + +/// Deposit log event abi signature. +pub const DEPOSIT_EVENT_ABI: &str = "TransactionDeposited(address,address,uint256,bytes)"; + +/// Deposit event abi hash. +/// +/// This is the keccak256 hash of the deposit event ABI signature. +/// `keccak256("TransactionDeposited(address,address,uint256,bytes)")` +pub const DEPOSIT_EVENT_ABI_HASH: B256 = + b256!("b3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32"); + +/// The initial version of the deposit event log. +pub const DEPOSIT_EVENT_VERSION_0: B256 = B256::ZERO; + +/// An [`TxDeposit`] validation error. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum DepositError { + /// Unexpected number of deposit event log topics. + #[error("Unexpected number of deposit event log topics: {0}")] + UnexpectedTopicsLen(usize), + /// Invalid deposit event selector. + /// Expected: [`B256`] (deposit event selector), Actual: [`B256`] (event log topic). + #[error("Invalid deposit event selector: {1}, expected {0}")] + InvalidSelector(B256, B256), + /// Incomplete opaqueData slice header (incomplete length). + #[error("Incomplete opaqueData slice header (incomplete length): {0}")] + IncompleteOpaqueData(usize), + /// The log data is not aligned to 32 bytes. + #[error("Unaligned log data, expected multiple of 32 bytes, got: {0}")] + UnalignedData(usize), + /// Failed to decode the `from` field of the deposit event (the second topic). + #[error("Failed to decode the `from` address of the deposit log topic: {0}")] + FromDecode(B256), + /// Failed to decode the `to` field of the deposit event (the third topic). + #[error("Failed to decode the `to` address of the deposit log topic: {0}")] + ToDecode(B256), + /// Invalid opaque data content offset. + #[error("Invalid u64 opaque data content offset: {0}")] + InvalidOpaqueDataOffset(Bytes), + /// Invalid opaque data content length. + #[error("Invalid u64 opaque data content length: expected {expected}, actual {actual}")] + InvalidOpaqueDataLength { + /// Expected length. + expected: usize, + /// Actual length. + actual: usize, + }, + /// Invalid opaque data. + #[error("Invalid opaque data padding. Not all zeros or incorrect length: {0}")] + InvalidOpaqueDataPadding(Bytes), + /// Opaque content length overflow. + #[error("Opaque content length overflow: {0}")] + OpaqueContentOverflow(Bytes), + /// Opaque data length exceeds the deposit log event data length. + /// Specified: [usize] (data length), Actual: [usize] (opaque data length). + #[error("Specified opaque data length {1} exceeds the deposit log event data length {0}")] + OpaqueDataOverflow(u64, usize), + /// Opaque data padding overflow. + #[error("Opaque data padding overflow")] + OpaqueDataPaddingOverflow, + /// Opaque data with padding exceeds the specified data length. + /// Specified: [usize] (data length), Actual: [usize] (opaque data length). + #[error("Opaque data with padding exceeds the specified data length: {1} > {0}")] + PaddedOpaqueDataOverflow(usize, u64), + /// An invalid deposit version. + #[error("Invalid deposit version: {0}")] + InvalidVersion(B256), + /// Unexpected opaque data length. + #[error("Unexpected opaque data length: {0}")] + UnexpectedOpaqueDataLen(usize), + /// Failed to decode the deposit mint value. + #[error("Failed to decode the u128 deposit mint value: {0}")] + MintDecode(Bytes), + /// Failed to decode the deposit gas value. + #[error("Failed to decode the u64 deposit gas value: {0}")] + GasDecode(Bytes), +} + +/// Derives a deposit transaction from an EVM log event emitted by the deposit contract. +/// +/// The emitted log must be in format: +/// ```solidity +/// event TransactionDeposited( +/// address indexed from, +/// address indexed to, +/// uint256 indexed version, +/// bytes opaqueData +/// ); +/// ``` +pub fn decode_deposit(block_hash: B256, index: usize, log: &Log) -> Result { + let topics = log.data.topics(); + if topics.len() != 4 { + return Err(DepositError::UnexpectedTopicsLen(topics.len())); + } + if topics[0] != DEPOSIT_EVENT_ABI_HASH { + return Err(DepositError::InvalidSelector(DEPOSIT_EVENT_ABI_HASH, topics[0])); + } + if log.data.data.len() < 64 { + return Err(DepositError::IncompleteOpaqueData(log.data.data.len())); + } + if log.data.data.len() % 32 != 0 { + return Err(DepositError::UnalignedData(log.data.data.len())); + } + + // Validate the `from` address. + let mut from_bytes = [0u8; 20]; + from_bytes.copy_from_slice(&topics[1].as_slice()[12..]); + if topics[1].iter().take(12).any(|&b| b != 0) { + return Err(DepositError::FromDecode(topics[1])); + } + + // Validate the `to` address. + let mut to_bytes = [0u8; 20]; + to_bytes.copy_from_slice(&topics[2].as_slice()[12..]); + if topics[2].iter().take(12).any(|&b| b != 0) { + return Err(DepositError::ToDecode(topics[2])); + } + + let from = Address::from(from_bytes); + let to = Address::from(to_bytes); + let version = log.data.topics()[3]; + + // Solidity serializes the event's Data field as follows: + // + // ```solidity + // abi.encode(abi.encodPacked(uint256 mint, uint256 value, uint64 gasLimit, uint8 isCreation, bytes data)) + // ``` + // + // The opaqueData will be packed as shown below: + // + // ------------------------------------------------------------ + // | offset | 256 byte content | + // ------------------------------------------------------------ + // | 0 | [0; 24] . {U64 big endian, hex encoded offset} | + // ------------------------------------------------------------ + // | 32 | [0; 24] . {U64 big endian, hex encoded length} | + // ------------------------------------------------------------ + + let opaque_content_offset: U256 = U256::from_be_slice(&log.data.data[0..32]); + if opaque_content_offset != U256::from(32) { + return Err(DepositError::InvalidOpaqueDataOffset(Bytes::copy_from_slice( + &log.data.data[0..32], + ))); + } + + // The next 32 bytes indicate the length of the opaqueData content. + let opaque_content_len: U256 = U256::from_be_slice(&log.data.data[32..64]); + let opaque_content_len: u64 = opaque_content_len.try_into().map_err(|_| { + DepositError::OpaqueContentOverflow(Bytes::copy_from_slice(&log.data.data[32..64])) + })?; + + let opaque_data_ceil_32: u64 = (opaque_content_len.saturating_add(31) / 32).saturating_mul(32); + + // Ensure that the remaining data is only zeros. + // The padding ends at the next multiple of 32 after the opaque data. + let Some(padding_end): Option = 64_u64.checked_add(opaque_data_ceil_32) else { + return Err(DepositError::OpaqueDataPaddingOverflow); + }; + + // The remaining data is the opaqueData which is tightly packed and then padded to 32 bytes by + // the EVM. + let Some(opaque_data) = &log.data.data.get(64..64 + opaque_content_len as usize) else { + return Err(DepositError::InvalidOpaqueDataLength { + expected: opaque_content_len as usize, + actual: log.data.data.len().saturating_sub(64), + }); + }; + + if !(opaque_content_len % 32 == 0 || + log.data + .data + .get((64 + opaque_content_len) as usize..padding_end as usize) + .is_some_and(|data| data.iter().all(|&b| b == 0))) + { + return Err(DepositError::InvalidOpaqueDataPadding(Bytes::copy_from_slice( + &log.data.data[(64 + opaque_content_len) as usize..], + ))); + } + + let source = UserDepositSource::new(block_hash, index as u64); + + let mut deposit_tx = TxDeposit { + from, + is_system_transaction: false, + source_hash: source.source_hash(), + ..Default::default() + }; + + // Can only handle version 0 for now + if !version.is_zero() { + return Err(DepositError::InvalidVersion(version)); + } + + unmarshal_deposit_version0(&mut deposit_tx, to, opaque_data)?; + + // Re-encode the deposit transaction + let mut buffer = Vec::with_capacity(deposit_tx.eip2718_encoded_length()); + deposit_tx.encode_2718(&mut buffer); + Ok(Bytes::from(buffer)) +} + +/// Unmarshals a deposit transaction from the opaque data. +pub(crate) fn unmarshal_deposit_version0( + tx: &mut TxDeposit, + to: Address, + data: &[u8], +) -> Result<(), DepositError> { + if data.len() < 32 + 32 + 8 + 1 { + return Err(DepositError::UnexpectedOpaqueDataLen(data.len())); + } + + let mut offset = 0; + + let raw_mint: [u8; 16] = data[offset + 16..offset + 32].try_into().map_err(|_| { + DepositError::MintDecode(Bytes::copy_from_slice(&data[offset + 16..offset + 32])) + })?; + tx.mint = u128::from_be_bytes(raw_mint); + offset += 32; + + // uint256 value + tx.value = U256::from_be_slice(&data[offset..offset + 32]); + offset += 32; + + // uint64 gas + let raw_gas: [u8; 8] = data[offset..offset + 8] + .try_into() + .map_err(|_| DepositError::GasDecode(Bytes::copy_from_slice(&data[offset..offset + 8])))?; + tx.gas_limit = u64::from_be_bytes(raw_gas); + offset += 8; + + // uint8 isCreation + // isCreation: If the boolean byte is 1 then dep.To will stay nil, + // and it will create a contract using L2 account nonce to determine the created address. + if data[offset] == 0 { + tx.to = TxKind::Call(to); + } else { + tx.to = TxKind::Create; + } + offset += 1; + + // The remainder of the opaqueData is the transaction data (without length prefix). + // The data may be padded to a multiple of 32 bytes + let tx_data_len = data.len() - offset; + + // Remaining bytes fill the data + tx.input = Bytes::copy_from_slice(&data[offset..offset + tx_data_len]); + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec; + use alloy_primitives::{LogData, U64, U128, address, b256, hex}; + + #[test] + fn test_decode_deposit_invalid_first_topic() { + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![B256::default(), B256::default(), B256::default(), B256::default()], + Bytes::default(), + ), + }; + let err: DepositError = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::InvalidSelector(DEPOSIT_EVENT_ABI_HASH, B256::default())); + } + + #[test] + fn test_decode_deposit_incomplete_data() { + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(vec![0u8; 63]), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::IncompleteOpaqueData(63)); + } + + #[test] + fn test_decode_deposit_unaligned_data() { + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(vec![0u8; 65]), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::UnalignedData(65)); + } + + #[test] + fn test_decode_deposit_invalid_from() { + let invalid_from = + b256!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, invalid_from, B256::default(), B256::default()], + Bytes::from(vec![0u8; 64]), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::FromDecode(invalid_from)); + } + + #[test] + fn test_decode_deposit_invalid_to() { + let invalid_to = b256!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), invalid_to, B256::default()], + Bytes::from(vec![0u8; 64]), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::ToDecode(invalid_to)); + } + + #[test] + fn test_decode_deposit_invalid_opaque_data_offset() { + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(vec![0u8; 64]), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::InvalidOpaqueDataOffset(Bytes::from(vec![0u8; 32]))); + } + + #[test] + fn test_decode_deposit_opaque_data_overflow() { + let mut data = vec![0u8; 128]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + // The first 64 bytes of the data are identifiers so + // if this test was to be valid, the data length would be 64 not 128. + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(data), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::InvalidOpaqueDataLength { expected: 128, actual: 64 }); + } + + #[test] + fn test_decode_deposit_padded_overflow() { + let mut data = vec![0u8; 256]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(64).to_be_bytes(); + data[56..64].copy_from_slice(&len); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(data), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::UnexpectedOpaqueDataLen(64)); + } + + #[test] + fn test_decode_deposit_invalid_version() { + let mut data = vec![0u8; 128]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(64).to_be_bytes(); + data[56..64].copy_from_slice(&len); + let version = b256!("0000000000000000000000000000000000000000000000000000000000000001"); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), version], + Bytes::from(data), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::InvalidVersion(version)); + } + + #[test] + fn test_decode_deposit_empty_succeeds() { + let valid_to = b256!("000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + let valid_from = b256!("000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, valid_from, valid_to, B256::default()], + Bytes::from(data), + ), + }; + let tx = decode_deposit(B256::default(), 0, &log).unwrap(); + let raw_hex = hex!( + "7ef887a0ed428e1c45e1d9561b62834e1a2d3015a0caae3bfdc16b4da059ac885b01a14594ffffffffffffffffffffffffffffffffffffffff94bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb80808080b700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + let expected = Bytes::from(raw_hex); + assert_eq!(tx, expected); + } + + #[test] + fn test_decode_deposit_invalid_offset() { + let mut data = vec![0u8; 128]; + let offset: [u8; 16] = U128::MAX.to_be_bytes(); + data[16..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(data.clone()), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + let bytes = Bytes::from(data.get(0..32).unwrap().to_vec()); + assert_eq!(err, DepositError::InvalidOpaqueDataOffset(bytes)); + } + + #[test] + fn test_decode_deposit_invalid_length() { + let mut data = vec![0u8; 128]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 16] = U128::MAX.to_be_bytes(); + data[48..64].copy_from_slice(&len); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![DEPOSIT_EVENT_ABI_HASH, B256::default(), B256::default(), B256::default()], + Bytes::from(data.clone()), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!( + err, + DepositError::OpaqueContentOverflow(Bytes::from(data.get(32..64).unwrap().to_vec())) + ); + } + + #[test] + fn test_invalid_opaque_data_length() { + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(129).to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + let from = address!("1111111111111111111111111111111111111111"); + let mut from_bytes = vec![0u8; 32]; + from_bytes[12..32].copy_from_slice(from.as_slice()); + let to = address!("2222222222222222222222222222222222222222"); + let mut to_bytes = vec![0u8; 32]; + to_bytes[12..32].copy_from_slice(to.as_slice()); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![ + DEPOSIT_EVENT_ABI_HASH, + B256::from_slice(&from_bytes), + B256::from_slice(&to_bytes), + B256::default(), + ], + Bytes::from(data), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::InvalidOpaqueDataLength { expected: 129, actual: 128 }); + } + + #[test] + fn test_opaque_data_padding_overflow() { + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::MAX.to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + let from = address!("1111111111111111111111111111111111111111"); + let mut from_bytes = vec![0u8; 32]; + from_bytes[12..32].copy_from_slice(from.as_slice()); + let to = address!("2222222222222222222222222222222222222222"); + let mut to_bytes = vec![0u8; 32]; + to_bytes[12..32].copy_from_slice(to.as_slice()); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![ + DEPOSIT_EVENT_ABI_HASH, + B256::from_slice(&from_bytes), + B256::from_slice(&to_bytes), + B256::default(), + ], + Bytes::from(data.clone()), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!(err, DepositError::OpaqueDataPaddingOverflow); + } + + #[test] + fn test_invalid_opaque_data_padding() { + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(127).to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + // Mess up the padding + data[191] = 1; + let from = address!("1111111111111111111111111111111111111111"); + let mut from_bytes = vec![0u8; 32]; + from_bytes[12..32].copy_from_slice(from.as_slice()); + let to = address!("2222222222222222222222222222222222222222"); + let mut to_bytes = vec![0u8; 32]; + to_bytes[12..32].copy_from_slice(to.as_slice()); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![ + DEPOSIT_EVENT_ABI_HASH, + B256::from_slice(&from_bytes), + B256::from_slice(&to_bytes), + B256::default(), + ], + Bytes::from(data.clone()), + ), + }; + let err = decode_deposit(B256::default(), 0, &log).unwrap_err(); + assert_eq!( + err, + DepositError::InvalidOpaqueDataPadding(Bytes::from(data.get(191..).unwrap().to_vec())) + ); + } + + #[test] + fn test_decode_deposit_full_succeeds() { + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + let from = address!("1111111111111111111111111111111111111111"); + let mut from_bytes = vec![0u8; 32]; + from_bytes[12..32].copy_from_slice(from.as_slice()); + let to = address!("2222222222222222222222222222222222222222"); + let mut to_bytes = vec![0u8; 32]; + to_bytes[12..32].copy_from_slice(to.as_slice()); + let log = Log { + address: Address::default(), + data: LogData::new_unchecked( + vec![ + DEPOSIT_EVENT_ABI_HASH, + B256::from_slice(&from_bytes), + B256::from_slice(&to_bytes), + B256::default(), + ], + Bytes::from(data), + ), + }; + let tx = decode_deposit(B256::default(), 0, &log).unwrap(); + let raw_hex = hex!( + "7ef875a0ed428e1c45e1d9561b62834e1a2d3015a0caae3bfdc16b4da059ac885b01a145941111111111111111111111111111111111111111800a648203e880b700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + let expected = Bytes::from(raw_hex); + assert_eq!(tx, expected); + } + + #[test] + fn test_unmarshal_deposit_version0_invalid_len() { + let data = vec![0u8; 72]; + let mut tx = TxDeposit::default(); + let to = address!("5555555555555555555555555555555555555555"); + let err = unmarshal_deposit_version0(&mut tx, to, &data).unwrap_err(); + assert_eq!(err, DepositError::UnexpectedOpaqueDataLen(72)); + + // Data must have at least length 73 + let data = vec![0u8; 73]; + let mut tx = TxDeposit::default(); + let to = address!("5555555555555555555555555555555555555555"); + unmarshal_deposit_version0(&mut tx, to, &data).unwrap(); + } + + #[test] + fn test_unmarshal_deposit_version0() { + let mut data = vec![0u8; 192]; + let offset: [u8; 8] = U64::from(32).to_be_bytes(); + data[24..32].copy_from_slice(&offset); + let len: [u8; 8] = U64::from(128).to_be_bytes(); + data[56..64].copy_from_slice(&len); + // Copy the u128 mint value + let mint: [u8; 16] = 10_u128.to_be_bytes(); + data[80..96].copy_from_slice(&mint); + // Copy the tx value + let value: [u8; 32] = U256::from(100).to_be_bytes(); + data[96..128].copy_from_slice(&value); + // Copy the gas limit + let gas: [u8; 8] = 1000_u64.to_be_bytes(); + data[128..136].copy_from_slice(&gas); + // Copy the isCreation flag + data[136] = 1; + let mut tx = TxDeposit { + from: address!("1111111111111111111111111111111111111111"), + to: TxKind::Call(address!("2222222222222222222222222222222222222222")), + value: U256::from(100), + gas_limit: 1000, + mint: 10, + ..Default::default() + }; + let to = address!("5555555555555555555555555555555555555555"); + unmarshal_deposit_version0(&mut tx, to, &data).unwrap(); + assert_eq!(tx.to, TxKind::Call(address!("5555555555555555555555555555555555555555"))); + } +} diff --git a/kona/crates/protocol/protocol/src/errors.rs b/kona/crates/protocol/protocol/src/errors.rs new file mode 100644 index 0000000000000..295db26d52df3 --- /dev/null +++ b/kona/crates/protocol/protocol/src/errors.rs @@ -0,0 +1,28 @@ +//! Error types for protocol conversions. + +use crate::DecodeError; +use alloy_primitives::B256; +use op_alloy_consensus::EIP1559ParamError; + +/// An error encountered during OP [`Block`](alloy_consensus::Block) conversion. +#[derive(Debug, Eq, PartialEq, thiserror::Error)] +pub enum OpBlockConversionError { + /// Invalid genesis hash. + #[error("Invalid genesis hash. Expected {0}, got {1}")] + InvalidGenesisHash(B256, B256), + /// Invalid transaction type. + #[error("First payload transaction has unexpected type: {0}")] + InvalidTxType(u8), + /// L1 Info error + #[error("Failed to decode L1 info: {0}")] + L1InfoError(#[from] DecodeError), + /// Missing system config in genesis block. + #[error("Missing system config in genesis block")] + MissingSystemConfigGenesis, + /// Empty transactions. + #[error("Empty transactions in payload. Block hash: {0}")] + EmptyTransactions(B256), + /// EIP-1559 parameter decoding error. + #[error("Failed to decode EIP-1559 parameters from header's `extraData` field.")] + Eip1559DecodeError(#[from] EIP1559ParamError), +} diff --git a/kona/crates/protocol/protocol/src/frame.rs b/kona/crates/protocol/protocol/src/frame.rs new file mode 100644 index 0000000000000..f74fb3fc48d20 --- /dev/null +++ b/kona/crates/protocol/protocol/src/frame.rs @@ -0,0 +1,329 @@ +//! Frame types for OP Stack L2 data transmission. +//! +//! Frames are the fundamental unit of data transmission in the OP Stack derivation +//! pipeline. They provide a way to split large channel data into smaller, manageable +//! chunks that can be transmitted via L1 transactions and later reassembled. +//! +//! # Frame Structure +//! +//! Each frame contains: +//! - **Channel ID**: Unique identifier linking frames to their parent channel +//! - **Frame Number**: Sequence number for ordering and reassembly +//! - **Data Payload**: The actual frame data content +//! - **Last Flag**: Indicates if this is the final frame in the sequence +//! +//! # Transmission Process +//! +//! ```text +//! Channel Data → Split into Frames → Transmit via L1 → Reassemble Channel +//! ``` +//! +//! # Error Handling +//! +//! Frame processing can fail due to: +//! - Size constraints (too large or too small) +//! - Invalid frame identifiers or numbers +//! - Data length mismatches +//! - Unsupported versions + +use crate::ChannelId; +use alloc::vec::Vec; + +/// Version identifier for the current derivation pipeline format. +/// +/// This constant defines the version of the derivation pipeline protocol +/// that this implementation supports. It's included in frame encoding to +/// ensure compatibility and enable future protocol upgrades. +pub const DERIVATION_VERSION_0: u8 = 0; + +/// Overhead estimation for frame metadata and tagging information. +/// +/// This constant provides an estimate of the additional bytes required +/// for frame metadata (channel ID, frame number, data length, flags) and +/// L1 transaction overhead. Used for buffer size calculations and gas +/// estimation when planning frame transmission. +pub const FRAME_OVERHEAD: usize = 200; + +/// Maximum allowed size for a single frame in bytes. +/// +/// While typical L1 transactions carrying frames are around 128 KB due to +/// network conditions and gas limits, this larger limit provides headroom +/// for future growth as L1 gas limits and network conditions improve. +/// +/// The 1MB limit balances: +/// - **Transmission efficiency**: Larger frames reduce overhead +/// - **Network compatibility**: Must fit within reasonable L1 transaction sizes +/// - **Memory constraints**: Avoid excessive memory usage during processing +pub const MAX_FRAME_LEN: usize = 1_000_000; + +/// A frame decoding error. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FrameDecodingError { + /// The frame data is too large. + #[error("Frame data too large: {0} bytes")] + DataTooLarge(usize), + /// The frame data is too short. + #[error("Frame data too short: {0} bytes")] + DataTooShort(usize), + /// Error decoding the frame id. + #[error("Invalid frame id")] + InvalidId, + /// Error decoding the frame number. + #[error("Invalid frame number")] + InvalidNumber, + /// Error decoding the frame data length. + #[error("Invalid frame data length")] + InvalidDataLength, +} + +/// Frame parsing error. +#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FrameParseError { + /// Error parsing the frame data. + #[error("Frame decoding error: {0}")] + FrameDecodingError(FrameDecodingError), + /// No frames to parse. + #[error("No frames to parse")] + NoFrames, + /// Unsupported derivation version. + #[error("Unsupported derivation version")] + UnsupportedVersion, + /// Frame data length mismatch. + #[error("Frame data length mismatch")] + DataLengthMismatch, + /// No frames decoded. + #[error("No frames decoded")] + NoFramesDecoded, +} + +/// A channel frame representing a segment of channel data for transmission. +/// +/// Frames are the atomic units of data transmission in the OP Stack derivation pipeline. +/// Large channel data is split into multiple frames to fit within L1 transaction size +/// constraints while maintaining the ability to reassemble the original data. +/// +/// # Binary Encoding Format +/// +/// The frame is encoded as a concatenated byte sequence: +/// ```text +/// frame = channel_id ++ frame_number ++ frame_data_length ++ frame_data ++ is_last +/// ``` +/// +/// ## Field Specifications +/// - **channel_id** (16 bytes): Unique identifier linking this frame to its parent channel +/// - **frame_number** (2 bytes, uint16): Sequence number for proper reassembly ordering +/// - **frame_data_length** (4 bytes, uint32): Length of the frame_data field in bytes +/// - **frame_data** (variable): The actual payload data for this frame segment +/// - **is_last** (1 byte, bool): Flag indicating if this is the final frame in the sequence +/// +/// ## Total Overhead +/// Each frame has a fixed overhead of 23 bytes (16 + 2 + 4 + 1) plus the variable data payload. +/// +/// # Frame Sequencing +/// +/// Frames within a channel must be: +/// 1. **Sequentially numbered**: Starting from 0 and incrementing by 1 +/// 2. **Properly terminated**: Exactly one frame marked with `is_last = true` +/// 3. **Complete**: All frame numbers from 0 to the last frame must be present +/// +/// # Reassembly Process +/// +/// Channel reassembly involves: +/// 1. **Collection**: Gather all frames with the same channel ID +/// 2. **Sorting**: Order frames by their frame number +/// 3. **Validation**: Verify sequential numbering and last frame flag +/// 4. **Concatenation**: Combine frame data in order to reconstruct channel +/// +/// # Error Conditions +/// +/// Frame processing can fail due to: +/// - Missing frames in the sequence +/// - Duplicate frame numbers +/// - Multiple frames marked as last +/// - Frame data exceeding size limits +/// - Invalid encoding or corruption +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Frame { + /// Unique identifier linking this frame to its parent channel. + /// + /// All frames belonging to the same channel share this identifier, + /// enabling proper grouping during the reassembly process. The channel + /// ID is typically derived from the first frame's metadata. + pub id: ChannelId, + /// Sequence number for frame ordering within the channel. + /// + /// Frame numbers start at 0 and increment sequentially. This field + /// is critical for proper reassembly ordering and detecting missing + /// or duplicate frames during channel reconstruction. + pub number: u16, + /// Payload data carried by this frame. + /// + /// Contains a segment of the original channel data. When all frames + /// in a channel are reassembled, concatenating this data in frame + /// number order reconstructs the complete channel payload. + pub data: Vec, + /// Flag indicating whether this is the final frame in the channel sequence. + /// + /// Exactly one frame per channel should have this flag set to `true`. + /// This enables detection of complete channel reception and validation + /// that no frames are missing from the end of the sequence. + pub is_last: bool, +} + +impl Frame { + /// Creates a new [`Frame`]. + pub const fn new(id: ChannelId, number: u16, data: Vec, is_last: bool) -> Self { + Self { id, number, data, is_last } + } + + /// Encode the frame into a byte vector. + pub fn encode(&self) -> Vec { + let mut encoded = Vec::with_capacity(16 + 2 + 4 + self.data.len() + 1); + encoded.extend_from_slice(&self.id); + encoded.extend_from_slice(&self.number.to_be_bytes()); + encoded.extend_from_slice(&(self.data.len() as u32).to_be_bytes()); + encoded.extend_from_slice(&self.data); + encoded.push(self.is_last as u8); + encoded + } + + /// Decode a frame from a byte vector. + pub fn decode(encoded: &[u8]) -> Result<(usize, Self), FrameDecodingError> { + const BASE_FRAME_LEN: usize = 16 + 2 + 4 + 1; + + if encoded.len() < BASE_FRAME_LEN { + return Err(FrameDecodingError::DataTooShort(encoded.len())); + } + + let id = encoded[..16].try_into().map_err(|_| FrameDecodingError::InvalidId)?; + let number = u16::from_be_bytes( + encoded[16..18].try_into().map_err(|_| FrameDecodingError::InvalidNumber)?, + ); + let data_len = u32::from_be_bytes( + encoded[18..22].try_into().map_err(|_| FrameDecodingError::InvalidDataLength)?, + ) as usize; + + if data_len > MAX_FRAME_LEN || data_len >= encoded.len() - (BASE_FRAME_LEN - 1) { + return Err(FrameDecodingError::DataTooLarge(data_len)); + } + + let data = encoded[22..22 + data_len].to_vec(); + let is_last = encoded[22 + data_len] == 1; + Ok((BASE_FRAME_LEN + data_len, Self { id, number, data, is_last })) + } + + /// Parses a single frame from the given data at the given starting position, + /// returning the frame and the number of bytes consumed. + pub fn parse_frame(data: &[u8], start: usize) -> Result<(usize, Self), FrameDecodingError> { + let (frame_len, frame) = Self::decode(&data[start..])?; + Ok((frame_len, frame)) + } + + /// Parse the on chain serialization of frame(s) in an L1 transaction. Currently + /// only version 0 of the serialization format is supported. All frames must be parsed + /// without error and there must not be any left over data and there must be at least one + /// frame. + /// + /// Frames are stored in L1 transactions with the following format: + /// * `data = DerivationVersion0 ++ Frame(s)` Where there is one or more frames concatenated + /// together. + pub fn parse_frames(encoded: &[u8]) -> Result, FrameParseError> { + if encoded.is_empty() { + return Err(FrameParseError::NoFrames); + } + if encoded[0] != DERIVATION_VERSION_0 { + return Err(FrameParseError::UnsupportedVersion); + } + + let data = &encoded[1..]; + let mut frames = Vec::new(); + let mut offset = 0; + while offset < data.len() { + let (frame_length, frame) = + Self::decode(&data[offset..]).map_err(FrameParseError::FrameDecodingError)?; + frames.push(frame); + offset += frame_length; + } + + if offset != data.len() { + return Err(FrameParseError::DataLengthMismatch); + } + if frames.is_empty() { + return Err(FrameParseError::NoFramesDecoded); + } + + Ok(frames) + } + + /// Calculates the size of the frame + overhead for storing the frame. The sum of the frame size + /// of each frame in a channel determines the channel's size. The sum of the channel sizes + /// is used for pruning & compared against the max channel bank size. + pub const fn size(&self) -> usize { + self.data.len() + FRAME_OVERHEAD + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec; + + #[test] + fn test_encode_frame_roundtrip() { + let frame = Frame { id: [0xFF; 16], number: 0xEE, data: vec![0xDD; 50], is_last: true }; + + let (_, frame_decoded) = Frame::decode(&frame.encode()).unwrap(); + assert_eq!(frame, frame_decoded); + } + + #[test] + fn test_data_too_short() { + let frame = Frame { id: [0xFF; 16], number: 0xEE, data: vec![0xDD; 22], is_last: true }; + let err = Frame::decode(&frame.encode()[..22]).unwrap_err(); + assert_eq!(err, FrameDecodingError::DataTooShort(22)); + } + + #[test] + fn test_decode_exceeds_max_data_len() { + let frame = Frame { + id: [0xFF; 16], + number: 0xEE, + data: vec![0xDD; MAX_FRAME_LEN + 1], + is_last: true, + }; + let err = Frame::decode(&frame.encode()).unwrap_err(); + assert_eq!(err, FrameDecodingError::DataTooLarge(MAX_FRAME_LEN + 1)); + } + + #[test] + fn test_decode_malicious_data_len() { + let frame = Frame { id: [0xFF; 16], number: 0xEE, data: vec![0xDD; 50], is_last: true }; + let mut encoded = frame.encode(); + let data_len = (encoded.len() - 22) as u32; + encoded[18..22].copy_from_slice(&data_len.to_be_bytes()); + + let err = Frame::decode(&encoded).unwrap_err(); + assert_eq!(err, FrameDecodingError::DataTooLarge(encoded.len() - 22_usize)); + + let valid_data_len = (encoded.len() - 23) as u32; + encoded[18..22].copy_from_slice(&valid_data_len.to_be_bytes()); + let (_, frame_decoded) = Frame::decode(&encoded).unwrap(); + assert_eq!(frame, frame_decoded); + } + + #[test] + fn test_decode_many() { + let frame = Frame { id: [0xFF; 16], number: 0xEE, data: vec![0xDD; 50], is_last: true }; + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[DERIVATION_VERSION_0]); + (0..5).for_each(|_| { + bytes.extend_from_slice(&frame.encode()); + }); + + let frames = Frame::parse_frames(bytes.as_slice()).unwrap(); + assert_eq!(frames.len(), 5); + (0..5).for_each(|i| { + assert_eq!(frames[i], frame); + }); + } +} diff --git a/kona/crates/protocol/protocol/src/info/bedrock.rs b/kona/crates/protocol/protocol/src/info/bedrock.rs new file mode 100644 index 0000000000000..45618df4e194f --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/bedrock.rs @@ -0,0 +1,147 @@ +//! Contains bedrock-specific L1 block info types. + +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, Bytes, U256}; + +use crate::DecodeError; + +/// Represents the fields within a Bedrock L1 block info transaction. +/// +/// Bedrock Binary Format +// +---------+--------------------------+ +// | Bytes | Field | +// +---------+--------------------------+ +// | 4 | Function signature | +// | 32 | Number | +// | 32 | Time | +// | 32 | BaseFee | +// | 32 | BlockHash | +// | 32 | SequenceNumber | +// | 32 | BatcherHash | +// | 32 | L1FeeOverhead | +// | 32 | L1FeeScalar | +// +---------+--------------------------+ +#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1BlockInfoBedrock { + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The current sequence number + pub sequence_number: u64, + /// The address of the batch submitter + pub batcher_address: Address, + /// The fee overhead for L1 data + pub l1_fee_overhead: U256, + /// The fee scalar for L1 data + pub l1_fee_scalar: U256, +} + +impl L1BlockInfoBedrock { + /// The length of an L1 info transaction in Bedrock. + pub const L1_INFO_TX_LEN: usize = 4 + 32 * 8; + + /// The 4 byte selector of the + /// "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64,bytes32,uint256,uint256)" function + pub const L1_INFO_TX_SELECTOR: [u8; 4] = [0x01, 0x5d, 0x8e, 0xb9]; + + /// Encodes the [`L1BlockInfoBedrock`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + let mut buf = Vec::with_capacity(Self::L1_INFO_TX_LEN); + buf.extend_from_slice(Self::L1_INFO_TX_SELECTOR.as_ref()); + buf.extend_from_slice(U256::from(self.number).to_be_bytes::<32>().as_slice()); + buf.extend_from_slice(U256::from(self.time).to_be_bytes::<32>().as_slice()); + buf.extend_from_slice(U256::from(self.base_fee).to_be_bytes::<32>().as_slice()); + buf.extend_from_slice(self.block_hash.as_slice()); + buf.extend_from_slice(U256::from(self.sequence_number).to_be_bytes::<32>().as_slice()); + buf.extend_from_slice(self.batcher_address.into_word().as_slice()); + buf.extend_from_slice(self.l1_fee_overhead.to_be_bytes::<32>().as_slice()); + buf.extend_from_slice(self.l1_fee_scalar.to_be_bytes::<32>().as_slice()); + buf.into() + } + + /// Decodes the [`L1BlockInfoBedrock`] object from ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() != Self::L1_INFO_TX_LEN { + return Err(DecodeError::InvalidBedrockLength(Self::L1_INFO_TX_LEN, r.len())); + } + + // SAFETY: For all below slice operations, the full + // length is validated above to be `260`. + + // SAFETY: 8 bytes are copied directly into the array + let mut number = [0u8; 8]; + number.copy_from_slice(&r[28..36]); + let number = u64::from_be_bytes(number); + + // SAFETY: 8 bytes are copied directly into the array + let mut time = [0u8; 8]; + time.copy_from_slice(&r[60..68]); + let time = u64::from_be_bytes(time); + + // SAFETY: 8 bytes are copied directly into the array + let mut base_fee = [0u8; 8]; + base_fee.copy_from_slice(&r[92..100]); + let base_fee = u64::from_be_bytes(base_fee); + + let block_hash = B256::from_slice(r[100..132].as_ref()); + + // SAFETY: 8 bytes are copied directly into the array + let mut sequence_number = [0u8; 8]; + sequence_number.copy_from_slice(&r[156..164]); + let sequence_number = u64::from_be_bytes(sequence_number); + + let batcher_address = Address::from_slice(r[176..196].as_ref()); + let l1_fee_overhead = U256::from_be_slice(r[196..228].as_ref()); + let l1_fee_scalar = U256::from_be_slice(r[228..260].as_ref()); + + Ok(Self { + number, + time, + base_fee, + block_hash, + sequence_number, + batcher_address, + l1_fee_overhead, + l1_fee_scalar, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_calldata_bedrock_invalid_length() { + let r = vec![0u8; 1]; + assert_eq!( + L1BlockInfoBedrock::decode_calldata(&r), + Err(DecodeError::InvalidBedrockLength(L1BlockInfoBedrock::L1_INFO_TX_LEN, r.len(),)) + ); + } + + #[test] + fn test_l1_block_info_bedrock_roundtrip_calldata_encoding() { + let info = L1BlockInfoBedrock { + number: 1, + time: 2, + base_fee: 3, + block_hash: B256::from([4u8; 32]), + sequence_number: 5, + batcher_address: Address::from([6u8; 20]), + l1_fee_overhead: U256::from(7), + l1_fee_scalar: U256::from(8), + }; + + let calldata = info.encode_calldata(); + let decoded_info = L1BlockInfoBedrock::decode_calldata(&calldata).unwrap(); + assert_eq!(info, decoded_info); + } +} diff --git a/kona/crates/protocol/protocol/src/info/common.rs b/kona/crates/protocol/protocol/src/info/common.rs new file mode 100644 index 0000000000000..545150ec30fc5 --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/common.rs @@ -0,0 +1,121 @@ +//! Common encoding and decoding utilities for L1 Block Info transactions. +//! +//! This module contains shared logic for encoding and decoding fields that are +//! common across multiple hardfork versions (Ecotone, Isthmus, Interop). + +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, U256}; + +/// Common fields present in post-Ecotone L1 block info transactions. +/// +/// These fields are shared across Ecotone, Isthmus, and Interop hardforks. +#[derive(Debug, Clone, Copy)] +pub(crate) struct CommonL1BlockFields { + /// The fee scalar for L1 data + pub base_fee_scalar: u32, + /// The fee scalar for L1 blobspace data + pub blob_base_fee_scalar: u32, + /// The current sequence number + pub sequence_number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current blob base fee on L1 + pub blob_base_fee: u128, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The address of the batch submitter + pub batcher_address: Address, +} + +impl CommonL1BlockFields { + /// Encodes the common fields into a buffer. + /// + /// The encoding follows this format (excluding the 4-byte selector): + /// - 4 bytes: BaseFeeScalar + /// - 4 bytes: BlobBaseFeeScalar + /// - 8 bytes: SequenceNumber + /// - 8 bytes: Timestamp + /// - 8 bytes: L1BlockNumber + /// - 32 bytes: BaseFee + /// - 32 bytes: BlobBaseFee + /// - 32 bytes: BlockHash + /// - 32 bytes: BatcherHash + pub(crate) fn encode_into(&self, buf: &mut Vec) { + buf.extend_from_slice(self.base_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.blob_base_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.sequence_number.to_be_bytes().as_ref()); + buf.extend_from_slice(self.time.to_be_bytes().as_ref()); + buf.extend_from_slice(self.number.to_be_bytes().as_ref()); + buf.extend_from_slice(U256::from(self.base_fee).to_be_bytes::<32>().as_ref()); + buf.extend_from_slice(U256::from(self.blob_base_fee).to_be_bytes::<32>().as_ref()); + buf.extend_from_slice(self.block_hash.as_ref()); + buf.extend_from_slice(self.batcher_address.into_word().as_ref()); + } + + /// Decodes the common fields from a byte slice. + /// + /// This assumes the slice starts at byte offset 4 (after the 4-byte selector) + /// and contains at least 164 bytes of data. + /// + /// # Safety + /// This method assumes the slice is at least 164 bytes long and starts at + /// the correct offset. Callers must validate the length before calling. + pub(crate) fn decode_from(r: &[u8]) -> Self { + // SAFETY: All slice operations below assume r is at least 164 bytes. + // The caller must validate this before calling this method. + + // SAFETY: 4 bytes are copied directly into the array + let mut base_fee_scalar = [0u8; 4]; + base_fee_scalar.copy_from_slice(&r[4..8]); + let base_fee_scalar = u32::from_be_bytes(base_fee_scalar); + + // SAFETY: 4 bytes are copied directly into the array + let mut blob_base_fee_scalar = [0u8; 4]; + blob_base_fee_scalar.copy_from_slice(&r[8..12]); + let blob_base_fee_scalar = u32::from_be_bytes(blob_base_fee_scalar); + + // SAFETY: 8 bytes are copied directly into the array + let mut sequence_number = [0u8; 8]; + sequence_number.copy_from_slice(&r[12..20]); + let sequence_number = u64::from_be_bytes(sequence_number); + + // SAFETY: 8 bytes are copied directly into the array + let mut time = [0u8; 8]; + time.copy_from_slice(&r[20..28]); + let time = u64::from_be_bytes(time); + + // SAFETY: 8 bytes are copied directly into the array + let mut number = [0u8; 8]; + number.copy_from_slice(&r[28..36]); + let number = u64::from_be_bytes(number); + + // SAFETY: 8 bytes are copied directly into the array + let mut base_fee = [0u8; 8]; + base_fee.copy_from_slice(&r[60..68]); + let base_fee = u64::from_be_bytes(base_fee); + + // SAFETY: 16 bytes are copied directly into the array + let mut blob_base_fee = [0u8; 16]; + blob_base_fee.copy_from_slice(&r[84..100]); + let blob_base_fee = u128::from_be_bytes(blob_base_fee); + + let block_hash = B256::from_slice(r[100..132].as_ref()); + let batcher_address = Address::from_slice(r[144..164].as_ref()); + + Self { + base_fee_scalar, + blob_base_fee_scalar, + sequence_number, + time, + number, + base_fee, + blob_base_fee, + block_hash, + batcher_address, + } + } +} diff --git a/kona/crates/protocol/protocol/src/info/ecotone.rs b/kona/crates/protocol/protocol/src/info/ecotone.rs new file mode 100644 index 0000000000000..dc4f3bc018ecd --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/ecotone.rs @@ -0,0 +1,154 @@ +//! Contains ecotone-specific L1 block info types. + +use crate::{DecodeError, info::CommonL1BlockFields}; +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, Bytes, U256}; + +/// Represents the fields within an Ecotone L1 block info transaction. +/// +/// Ecotone Binary Format +/// +---------+--------------------------+ +/// | Bytes | Field | +/// +---------+--------------------------+ +/// | 4 | Function signature | +/// | 4 | BaseFeeScalar | +/// | 4 | BlobBaseFeeScalar | +/// | 8 | SequenceNumber | +/// | 8 | Timestamp | +/// | 8 | L1BlockNumber | +/// | 32 | BaseFee | +/// | 32 | BlobBaseFee | +/// | 32 | BlockHash | +/// | 32 | BatcherHash | +/// +---------+--------------------------+ +#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1BlockInfoEcotone { + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The current sequence number + pub sequence_number: u64, + /// The address of the batch submitter + pub batcher_address: Address, + /// The current blob base fee on L1 + pub blob_base_fee: u128, + /// The fee scalar for L1 blobspace data + pub blob_base_fee_scalar: u32, + /// The fee scalar for L1 data + pub base_fee_scalar: u32, + /// Indicates that the scalars are empty. + /// This is an edge case where the first block in ecotone has no scalars, + /// so the bedrock tx l1 cost function needs to be used. + pub empty_scalars: bool, + /// The l1 fee overhead used along with the `empty_scalars` field for the + /// bedrock tx l1 cost function. + /// + /// This field is deprecated in the Ecotone Hardfork. + pub l1_fee_overhead: U256, +} + +impl L1BlockInfoEcotone { + /// The type byte identifier for the L1 scalar format in Ecotone. + pub const L1_SCALAR: u8 = 1; + + /// The length of an L1 info transaction in Ecotone. + pub const L1_INFO_TX_LEN: usize = 4 + 32 * 5; + + /// The 4 byte selector of "setL1BlockValuesEcotone()" + pub const L1_INFO_TX_SELECTOR: [u8; 4] = [0x44, 0x0a, 0x5e, 0x20]; + + /// Encodes the [`L1BlockInfoEcotone`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + let mut buf = Vec::with_capacity(Self::L1_INFO_TX_LEN); + buf.extend_from_slice(Self::L1_INFO_TX_SELECTOR.as_ref()); + + let common = CommonL1BlockFields { + base_fee_scalar: self.base_fee_scalar, + blob_base_fee_scalar: self.blob_base_fee_scalar, + sequence_number: self.sequence_number, + time: self.time, + number: self.number, + base_fee: self.base_fee, + blob_base_fee: self.blob_base_fee, + block_hash: self.block_hash, + batcher_address: self.batcher_address, + }; + common.encode_into(&mut buf); + + // Notice: do not include the `empty_scalars` field in the calldata. + // Notice: do not include the `l1_fee_overhead` field in the calldata. + buf.into() + } + + /// Decodes the [`L1BlockInfoEcotone`] object from ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() != Self::L1_INFO_TX_LEN { + return Err(DecodeError::InvalidEcotoneLength(Self::L1_INFO_TX_LEN, r.len())); + } + + // SAFETY: For all below slice operations, the full + // length is validated above to be `164`. + + let common = CommonL1BlockFields::decode_from(r); + + Ok(Self { + number: common.number, + time: common.time, + base_fee: common.base_fee, + block_hash: common.block_hash, + sequence_number: common.sequence_number, + batcher_address: common.batcher_address, + blob_base_fee: common.blob_base_fee, + blob_base_fee_scalar: common.blob_base_fee_scalar, + base_fee_scalar: common.base_fee_scalar, + // Notice: the `empty_scalars` field is not included in the calldata. + // This is used by the evm to indicate that the bedrock tx l1 cost function + // needs to be used. + empty_scalars: false, + // Notice: the `l1_fee_overhead` field is not included in the calldata. + l1_fee_overhead: U256::ZERO, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_calldata_ecotone_invalid_length() { + let r = vec![0u8; 1]; + assert_eq!( + L1BlockInfoEcotone::decode_calldata(&r), + Err(DecodeError::InvalidEcotoneLength(L1BlockInfoEcotone::L1_INFO_TX_LEN, r.len(),)) + ); + } + + #[test] + fn test_l1_block_info_ecotone_roundtrip_calldata_encoding() { + let info = L1BlockInfoEcotone { + number: 1, + time: 2, + base_fee: 3, + block_hash: B256::from([4u8; 32]), + sequence_number: 5, + batcher_address: Address::from([6u8; 20]), + blob_base_fee: 7, + blob_base_fee_scalar: 8, + base_fee_scalar: 9, + empty_scalars: false, + l1_fee_overhead: U256::ZERO, + }; + + let calldata = info.encode_calldata(); + let decoded_info = L1BlockInfoEcotone::decode_calldata(&calldata).unwrap(); + assert_eq!(info, decoded_info); + } +} diff --git a/kona/crates/protocol/protocol/src/info/errors.rs b/kona/crates/protocol/protocol/src/info/errors.rs new file mode 100644 index 0000000000000..f955d46297a3d --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/errors.rs @@ -0,0 +1,55 @@ +//! Contains error types specific to the L1 block info transaction. + +/// An error type for parsing L1 block info transactions. +#[derive(Debug, thiserror::Error, PartialEq, Eq, Copy, Clone)] +pub enum BlockInfoError { + /// Failed to parse the L1 blob base fee scalar. + #[error("Failed to parse the L1 blob base fee scalar")] + L1BlobBaseFeeScalar, + /// Failed to parse the base fee scalar. + #[error("Failed to parse the base fee scalar")] + BaseFeeScalar, + /// Failed to parse the EIP-1559 denominator. + #[error("Failed to parse the EIP-1559 denominator")] + Eip1559Denominator, + /// Failed to parse the EIP-1559 elasticity parameter. + #[error("Failed to parse the EIP-1559 elasticity parameter")] + Eip1559Elasticity, + /// Failed to parse the Operator Fee Scalar. + #[error("Failed to parse the Operator fee scalar parameter")] + OperatorFeeScalar, + /// Failed to parse the Operator Fee Constant. + #[error("Failed to parse the Operator fee constant parameter")] + OperatorFeeConstant, +} + +/// An error decoding an L1 block info transaction. +#[derive(Debug, Eq, PartialEq, Clone, thiserror::Error)] +pub enum DecodeError { + /// Missing selector bytes. + #[error("The provided calldata is too short, missing the 4 selector bytes")] + MissingSelector, + /// Invalid selector for the L1 info transaction. + #[error("Invalid L1 info transaction selector")] + InvalidSelector, + /// Invalid length for the L1 info bedrock transaction. + /// Arguments are the expected length and the actual length. + #[error("Invalid bedrock data length. Expected {0}, got {1}")] + InvalidBedrockLength(usize, usize), + /// Invalid length for the L1 info ecotone transaction. + /// Arguments are the expected length and the actual length. + #[error("Invalid ecotone data length. Expected {0}, got {1}")] + InvalidEcotoneLength(usize, usize), + /// Invalid length for the L1 info isthmus transaction. + /// Arguments are the expected length and the actual length. + #[error("Invalid isthmus data length. Expected {0}, got {1}")] + InvalidIsthmusLength(usize, usize), + /// Invalid length for the L1 info jovian transaction. + /// Arguments are the expected length and the actual length. + #[error("Invalid jovian data length. Expected {0}, got {1}")] + InvalidJovianLength(usize, usize), + /// Invalid length for the L1 info interop transaction. + /// Arguments are the expected length and the actual length. + #[error("Invalid interop data length. Expected {0}, got {1}")] + InvalidInteropLength(usize, usize), +} diff --git a/kona/crates/protocol/protocol/src/info/interop.rs b/kona/crates/protocol/protocol/src/info/interop.rs new file mode 100644 index 0000000000000..e1a793c7ebaa6 --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/interop.rs @@ -0,0 +1,135 @@ +//! Contains interop-specific L1 block info types. + +use crate::{info::CommonL1BlockFields, DecodeError}; +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, Bytes, U256}; + +/// Represents the fields within an Interop L1 block info transaction. +/// +/// Interop Binary Format +/// +---------+--------------------------+ +/// | Bytes | Field | +/// +---------+--------------------------+ +/// | 4 | Function signature | +/// | 4 | BaseFeeScalar | +/// | 4 | BlobBaseFeeScalar | +/// | 8 | SequenceNumber | +/// | 8 | Timestamp | +/// | 8 | L1BlockNumber | +/// | 32 | BaseFee | +/// | 32 | BlobBaseFee | +/// | 32 | BlockHash | +/// | 32 | BatcherHash | +/// +---------+--------------------------+ +#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1BlockInfoInterop { + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The current sequence number + pub sequence_number: u64, + /// The address of the batch submitter + pub batcher_address: Address, + /// The current blob base fee on L1 + pub blob_base_fee: u128, + /// The fee scalar for L1 blobspace data + pub blob_base_fee_scalar: u32, + /// The fee scalar for L1 data + pub base_fee_scalar: u32, +} + +impl L1BlockInfoInterop { + /// The type byte identifier for the L1 scalar format in Interop. + pub const L1_SCALAR: u8 = 1; + + /// The length of an L1 info transaction in Interop. + pub const L1_INFO_TX_LEN: usize = 4 + 32 * 5; + + /// The 4 byte selector of "setL1BlockValuesInterop()" + pub const L1_INFO_TX_SELECTOR: [u8; 4] = [0x76, 0x0e, 0xe0, 0x4d]; + + /// Encodes the [`L1BlockInfoInterop`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + let mut buf = Vec::with_capacity(Self::L1_INFO_TX_LEN); + buf.extend_from_slice(Self::L1_INFO_TX_SELECTOR.as_ref()); + + let common = CommonL1BlockFields { + base_fee_scalar: self.base_fee_scalar, + blob_base_fee_scalar: self.blob_base_fee_scalar, + sequence_number: self.sequence_number, + time: self.time, + number: self.number, + base_fee: self.base_fee, + blob_base_fee: self.blob_base_fee, + block_hash: self.block_hash, + batcher_address: self.batcher_address, + }; + common.encode_into(&mut buf); + + buf.into() + } + + /// Decodes the [`L1BlockInfoInterop`] object from ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() != Self::L1_INFO_TX_LEN { + return Err(DecodeError::InvalidInteropLength(Self::L1_INFO_TX_LEN, r.len())); + } + + // SAFETY: For all below slice operations, the full + // length is validated above to be `164`. + + let common = CommonL1BlockFields::decode_from(r); + + Ok(Self { + number: common.number, + time: common.time, + base_fee: common.base_fee, + block_hash: common.block_hash, + sequence_number: common.sequence_number, + batcher_address: common.batcher_address, + blob_base_fee: common.blob_base_fee, + blob_base_fee_scalar: common.blob_base_fee_scalar, + base_fee_scalar: common.base_fee_scalar, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_calldata_interop_invalid_length() { + let r = vec![0u8; 1]; + assert_eq!( + L1BlockInfoInterop::decode_calldata(&r), + Err(DecodeError::InvalidInteropLength(L1BlockInfoInterop::L1_INFO_TX_LEN, r.len(),)) + ); + } + + #[test] + fn test_l1_block_info_interop_roundtrip_calldata_encoding() { + let info = L1BlockInfoInterop { + number: 1, + time: 2, + base_fee: 3, + block_hash: B256::from([4u8; 32]), + sequence_number: 5, + batcher_address: Address::from([6u8; 20]), + blob_base_fee: 7, + blob_base_fee_scalar: 8, + base_fee_scalar: 9, + }; + + let calldata = info.encode_calldata(); + let decoded_info = L1BlockInfoInterop::decode_calldata(&calldata).unwrap(); + assert_eq!(info, decoded_info); + } +} diff --git a/kona/crates/protocol/protocol/src/info/isthmus.rs b/kona/crates/protocol/protocol/src/info/isthmus.rs new file mode 100644 index 0000000000000..c393e96cb70f3 --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/isthmus.rs @@ -0,0 +1,161 @@ +//! Isthmus L1 Block Info transaction types. + +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, Bytes}; + +use crate::{DecodeError, info::CommonL1BlockFields}; + +/// Represents the fields within an Isthnus L1 block info transaction. +/// +/// Isthmus Binary Format +/// +---------+--------------------------+ +/// | Bytes | Field | +/// +---------+--------------------------+ +/// | 4 | Function signature | +/// | 4 | BaseFeeScalar | +/// | 4 | BlobBaseFeeScalar | +/// | 8 | SequenceNumber | +/// | 8 | Timestamp | +/// | 8 | L1BlockNumber | +/// | 32 | BaseFee | +/// | 32 | BlobBaseFee | +/// | 32 | BlockHash | +/// | 32 | BatcherHash | +/// | 4 | OperatorFeeScalar | +/// | 8 | OperatorFeeConstant | +/// +---------+--------------------------+ +#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1BlockInfoIsthmus { + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The current sequence number + pub sequence_number: u64, + /// The address of the batch submitter + pub batcher_address: Address, + /// The current blob base fee on L1 + pub blob_base_fee: u128, + /// The fee scalar for L1 blobspace data + pub blob_base_fee_scalar: u32, + /// The fee scalar for L1 data + pub base_fee_scalar: u32, + /// The operator fee scalar + pub operator_fee_scalar: u32, + /// The operator fee constant + pub operator_fee_constant: u64, +} + +impl L1BlockInfoIsthmus { + /// The type byte identifier for the L1 scalar format in Isthmus. + pub const L1_SCALAR: u8 = 2; + + /// The length of an L1 info transaction in Isthmus. + pub const L1_INFO_TX_LEN: usize = 4 + 32 * 5 + 4 + 8; + + /// The 4 byte selector of "setL1BlockValuesIsthmus()" + pub const L1_INFO_TX_SELECTOR: [u8; 4] = [0x09, 0x89, 0x99, 0xbe]; + + /// Encodes the [`L1BlockInfoIsthmus`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + let mut buf = Vec::with_capacity(Self::L1_INFO_TX_LEN); + buf.extend_from_slice(Self::L1_INFO_TX_SELECTOR.as_ref()); + + let common = CommonL1BlockFields { + base_fee_scalar: self.base_fee_scalar, + blob_base_fee_scalar: self.blob_base_fee_scalar, + sequence_number: self.sequence_number, + time: self.time, + number: self.number, + base_fee: self.base_fee, + blob_base_fee: self.blob_base_fee, + block_hash: self.block_hash, + batcher_address: self.batcher_address, + }; + common.encode_into(&mut buf); + + // Encode Isthmus-specific fields + buf.extend_from_slice(self.operator_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.operator_fee_constant.to_be_bytes().as_ref()); + buf.into() + } + + /// Decodes the [`L1BlockInfoIsthmus`] object from ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() != Self::L1_INFO_TX_LEN { + return Err(DecodeError::InvalidIsthmusLength(Self::L1_INFO_TX_LEN, r.len())); + } + + // SAFETY: For all below slice operations, the full + // length is validated above to be `176`. + + let common = CommonL1BlockFields::decode_from(r); + + // Decode Isthmus-specific fields + // SAFETY: 4 bytes are copied directly into the array + let mut operator_fee_scalar = [0u8; 4]; + operator_fee_scalar.copy_from_slice(&r[164..168]); + let operator_fee_scalar = u32::from_be_bytes(operator_fee_scalar); + + // SAFETY: 8 bytes are copied directly into the array + let mut operator_fee_constant = [0u8; 8]; + operator_fee_constant.copy_from_slice(&r[168..176]); + let operator_fee_constant = u64::from_be_bytes(operator_fee_constant); + + Ok(Self { + number: common.number, + time: common.time, + base_fee: common.base_fee, + block_hash: common.block_hash, + sequence_number: common.sequence_number, + batcher_address: common.batcher_address, + blob_base_fee: common.blob_base_fee, + blob_base_fee_scalar: common.blob_base_fee_scalar, + base_fee_scalar: common.base_fee_scalar, + operator_fee_scalar, + operator_fee_constant, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_decode_calldata_isthmus_invalid_length() { + let r = vec![0u8; 1]; + assert_eq!( + L1BlockInfoIsthmus::decode_calldata(&r), + Err(DecodeError::InvalidIsthmusLength(L1BlockInfoIsthmus::L1_INFO_TX_LEN, r.len())) + ); + } + + #[test] + fn test_l1_block_info_isthmus_roundtrip_calldata_encoding() { + let info = L1BlockInfoIsthmus { + number: 1, + time: 2, + base_fee: 3, + block_hash: B256::from([4; 32]), + sequence_number: 5, + batcher_address: Address::from_slice(&[6; 20]), + blob_base_fee: 7, + blob_base_fee_scalar: 8, + base_fee_scalar: 9, + operator_fee_scalar: 10, + operator_fee_constant: 11, + }; + + let calldata = info.encode_calldata(); + let decoded_info = L1BlockInfoIsthmus::decode_calldata(&calldata).unwrap(); + + assert_eq!(info, decoded_info); + } +} diff --git a/kona/crates/protocol/protocol/src/info/jovian.rs b/kona/crates/protocol/protocol/src/info/jovian.rs new file mode 100644 index 0000000000000..5ea1d46e8ddea --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/jovian.rs @@ -0,0 +1,220 @@ +//! Jovian L1 Block Info transaction types. + +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, Bytes, U256}; + +use crate::DecodeError; + +/// Represents the fields within an Jovian L1 block info transaction. +/// +/// Jovian Binary Format +/// +---------+--------------------------+ +/// | Bytes | Field | +/// +---------+--------------------------+ +/// | 4 | Function signature | +/// | 4 | BaseFeeScalar | +/// | 4 | BlobBaseFeeScalar | +/// | 8 | SequenceNumber | +/// | 8 | Timestamp | +/// | 8 | L1BlockNumber | +/// | 32 | BaseFee | +/// | 32 | BlobBaseFee | +/// | 32 | BlockHash | +/// | 32 | BatcherHash | +/// | 4 | OperatorFeeScalar | +/// | 8 | OperatorFeeConstant | +/// | 2 | DAFootprintGasScalar | +/// +---------+--------------------------+ +#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1BlockInfoJovian { + /// The current L1 origin block number + pub number: u64, + /// The current L1 origin block's timestamp + pub time: u64, + /// The current L1 origin block's basefee + pub base_fee: u64, + /// The current L1 origin block's hash + pub block_hash: B256, + /// The current sequence number + pub sequence_number: u64, + /// The address of the batch submitter + pub batcher_address: Address, + /// The current blob base fee on L1 + pub blob_base_fee: u128, + /// The fee scalar for L1 blobspace data + pub blob_base_fee_scalar: u32, + /// The fee scalar for L1 data + pub base_fee_scalar: u32, + /// The operator fee scalar + pub operator_fee_scalar: u32, + /// The operator fee constant + pub operator_fee_constant: u64, + /// The DA footprint gas scalar + pub da_footprint_gas_scalar: u16, +} + +impl L1BlockInfoJovian { + /// The default DA footprint gas scalar + /// + pub const DEFAULT_DA_FOOTPRINT_GAS_SCALAR: u16 = 400; + + /// The type byte identifier for the L1 scalar format in Jovian. + pub const L1_SCALAR: u8 = 2; + + /// The length of an L1 info transaction in Jovian. + pub const L1_INFO_TX_LEN: usize = 4 + 32 * 5 + 4 + 8 + 2; + + /// The 4 byte selector of "setL1BlockValuesJovian()" + /// Those are the first 4 calldata bytes -> `` + pub const L1_INFO_TX_SELECTOR: [u8; 4] = [0x3d, 0xb6, 0xbe, 0x2b]; + + /// Encodes the [`L1BlockInfoJovian`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + let mut buf = Vec::with_capacity(Self::L1_INFO_TX_LEN); + buf.extend_from_slice(Self::L1_INFO_TX_SELECTOR.as_ref()); + buf.extend_from_slice(self.base_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.blob_base_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.sequence_number.to_be_bytes().as_ref()); + buf.extend_from_slice(self.time.to_be_bytes().as_ref()); + buf.extend_from_slice(self.number.to_be_bytes().as_ref()); + buf.extend_from_slice(U256::from(self.base_fee).to_be_bytes::<32>().as_ref()); + buf.extend_from_slice(U256::from(self.blob_base_fee).to_be_bytes::<32>().as_ref()); + buf.extend_from_slice(self.block_hash.as_ref()); + buf.extend_from_slice(self.batcher_address.into_word().as_ref()); + buf.extend_from_slice(self.operator_fee_scalar.to_be_bytes().as_ref()); + buf.extend_from_slice(self.operator_fee_constant.to_be_bytes().as_ref()); + buf.extend_from_slice(self.da_footprint_gas_scalar.to_be_bytes().as_ref()); + buf.into() + } + + /// Decodes the [`L1BlockInfoJovian`] object from ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() != Self::L1_INFO_TX_LEN { + return Err(DecodeError::InvalidJovianLength(Self::L1_INFO_TX_LEN, r.len())); + } + + // SAFETY: For all below slice operations, the full + // length is validated above to be `178`. + + // SAFETY: 4 bytes are copied directly into the array + let mut base_fee_scalar = [0u8; 4]; + base_fee_scalar.copy_from_slice(&r[4..8]); + let base_fee_scalar = u32::from_be_bytes(base_fee_scalar); + + // SAFETY: 4 bytes are copied directly into the array + let mut blob_base_fee_scalar = [0u8; 4]; + blob_base_fee_scalar.copy_from_slice(&r[8..12]); + let blob_base_fee_scalar = u32::from_be_bytes(blob_base_fee_scalar); + + // SAFETY: 8 bytes are copied directly into the array + let mut sequence_number = [0u8; 8]; + sequence_number.copy_from_slice(&r[12..20]); + let sequence_number = u64::from_be_bytes(sequence_number); + + // SAFETY: 8 bytes are copied directly into the array + let mut time = [0u8; 8]; + time.copy_from_slice(&r[20..28]); + let time = u64::from_be_bytes(time); + + // SAFETY: 8 bytes are copied directly into the array + let mut number = [0u8; 8]; + number.copy_from_slice(&r[28..36]); + let number = u64::from_be_bytes(number); + + // SAFETY: 8 bytes are copied directly into the array + let mut base_fee = [0u8; 8]; + base_fee.copy_from_slice(&r[60..68]); + let base_fee = u64::from_be_bytes(base_fee); + + // SAFETY: 16 bytes are copied directly into the array + let mut blob_base_fee = [0u8; 16]; + blob_base_fee.copy_from_slice(&r[84..100]); + let blob_base_fee = u128::from_be_bytes(blob_base_fee); + + let block_hash = B256::from_slice(r[100..132].as_ref()); + let batcher_address = Address::from_slice(r[144..164].as_ref()); + + // SAFETY: 4 bytes are copied directly into the array + let mut operator_fee_scalar = [0u8; 4]; + operator_fee_scalar.copy_from_slice(&r[164..168]); + let operator_fee_scalar = u32::from_be_bytes(operator_fee_scalar); + + // SAFETY: 8 bytes are copied directly into the array + let mut operator_fee_constant = [0u8; 8]; + operator_fee_constant.copy_from_slice(&r[168..176]); + let operator_fee_constant = u64::from_be_bytes(operator_fee_constant); + + // SAFETY: 2 bytes are copied directly into the array + let mut da_footprint_gas_scalar = [0u8; 2]; + da_footprint_gas_scalar.copy_from_slice(&r[176..178]); + let mut da_footprint_gas_scalar = u16::from_be_bytes(da_footprint_gas_scalar); + + // If the da footprint gas scalar is 0, use the default value (`https://github.com/ethereum-optimism/specs/blob/664cba65ab9686b0e70ad19fdf2ad054d6295986/specs/protocol/jovian/l1-attributes.md#overview`). + if da_footprint_gas_scalar == 0 { + da_footprint_gas_scalar = Self::DEFAULT_DA_FOOTPRINT_GAS_SCALAR; + } + + Ok(Self { + number, + time, + base_fee, + block_hash, + sequence_number, + batcher_address, + blob_base_fee, + blob_base_fee_scalar, + base_fee_scalar, + operator_fee_scalar, + operator_fee_constant, + da_footprint_gas_scalar, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloy_primitives::keccak256; + + #[test] + fn test_decode_calldata_jovian_invalid_length() { + let r = vec![0u8; 1]; + assert_eq!( + L1BlockInfoJovian::decode_calldata(&r), + Err(DecodeError::InvalidJovianLength(L1BlockInfoJovian::L1_INFO_TX_LEN, r.len())) + ); + } + + #[test] + fn test_function_selector() { + assert_eq!( + keccak256("setL1BlockValuesJovian()")[..4].to_vec(), + L1BlockInfoJovian::L1_INFO_TX_SELECTOR + ); + } + + #[test] + fn test_l1_block_info_jovian_roundtrip_calldata_encoding() { + let info = L1BlockInfoJovian { + number: 1, + time: 2, + base_fee: 3, + block_hash: B256::from([4; 32]), + sequence_number: 5, + batcher_address: Address::from_slice(&[6; 20]), + blob_base_fee: 7, + blob_base_fee_scalar: 8, + base_fee_scalar: 9, + operator_fee_scalar: 10, + operator_fee_constant: 11, + da_footprint_gas_scalar: 12, + }; + + let calldata = info.encode_calldata(); + let decoded_info = L1BlockInfoJovian::decode_calldata(&calldata).unwrap(); + + assert_eq!(info, decoded_info); + } +} diff --git a/kona/crates/protocol/protocol/src/info/mod.rs b/kona/crates/protocol/protocol/src/info/mod.rs new file mode 100644 index 0000000000000..17e66f367d4dd --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/mod.rs @@ -0,0 +1,22 @@ +//! Module containing L1 Attributes types (aka the L1 block info transaction). + +mod variant; +pub use variant::L1BlockInfoTx; + +mod isthmus; +pub use isthmus::L1BlockInfoIsthmus; + +mod bedrock; +pub use bedrock::L1BlockInfoBedrock; + +mod ecotone; +pub use ecotone::L1BlockInfoEcotone; + +mod jovian; +pub use jovian::L1BlockInfoJovian; + +mod errors; +pub use errors::{BlockInfoError, DecodeError}; + +mod common; +pub(crate) use common::CommonL1BlockFields; diff --git a/kona/crates/protocol/protocol/src/info/variant.rs b/kona/crates/protocol/protocol/src/info/variant.rs new file mode 100644 index 0000000000000..feb82ea5890b9 --- /dev/null +++ b/kona/crates/protocol/protocol/src/info/variant.rs @@ -0,0 +1,1105 @@ +//! Contains the `L1BlockInfoTx` enum, containing different variants of the L1 block info +//! transaction. + +use alloy_consensus::Header; +use alloy_eips::{BlockNumHash, eip7840::BlobParams}; +use alloy_primitives::{Address, B256, Bytes, Sealable, Sealed, TxKind, U256, address}; +use kona_genesis::{L1ChainConfig, RollupConfig, SystemConfig}; +use op_alloy_consensus::{DepositSourceDomain, L1InfoDepositSource, TxDeposit}; + +use crate::{ + BlockInfoError, DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoIsthmus, + Predeploys, info::L1BlockInfoJovian, +}; + +/// The system transaction gas limit post-Regolith +const REGOLITH_SYSTEM_TX_GAS: u64 = 1_000_000; + +/// The depositor address of the L1 info transaction +pub(crate) const L1_INFO_DEPOSITOR_ADDRESS: Address = + address!("deaddeaddeaddeaddeaddeaddeaddeaddead0001"); + +/// The [`L1BlockInfoTx`] enum contains variants for the different versions of the L1 block info +/// transaction on OP Stack chains. +/// +/// This transaction always sits at the top of the block, and alters the `L1 Block` contract's +/// knowledge of the L1 chain. +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum L1BlockInfoTx { + /// A Bedrock L1 info transaction + Bedrock(L1BlockInfoBedrock), + /// An Ecotone L1 info transaction + Ecotone(L1BlockInfoEcotone), + /// An Isthmus L1 info transaction + Isthmus(L1BlockInfoIsthmus), + /// A Jovian L1 info transaction + Jovian(L1BlockInfoJovian), +} + +impl L1BlockInfoTx { + /// Creates a new [`L1BlockInfoTx`] from the given information. + pub fn try_new( + rollup_config: &RollupConfig, + l1_config: &L1ChainConfig, + system_config: &SystemConfig, + sequence_number: u64, + l1_header: &Header, + l2_block_time: u64, + ) -> Result { + // In the first block of Ecotone, the L1Block contract has not been upgraded yet due to the + // upgrade transactions being placed after the L1 info transaction. Because of this, + // for the first block of Ecotone, we send a Bedrock style L1 block info transaction + if !rollup_config.is_ecotone_active(l2_block_time) || + rollup_config.is_first_ecotone_block(l2_block_time) + { + return Ok(Self::Bedrock(L1BlockInfoBedrock { + number: l1_header.number, + time: l1_header.timestamp, + base_fee: l1_header.base_fee_per_gas.unwrap_or(0), + block_hash: l1_header.hash_slow(), + sequence_number, + batcher_address: system_config.batcher_address, + l1_fee_overhead: system_config.overhead, + l1_fee_scalar: system_config.scalar, + })); + } + + // --- Post-Ecotone Operations --- + + let scalar = system_config.scalar.to_be_bytes::<32>(); + let blob_base_fee_scalar = (scalar[0] == L1BlockInfoEcotone::L1_SCALAR) + .then(|| { + Ok::(u32::from_be_bytes( + scalar[24..28].try_into().map_err(|_| BlockInfoError::L1BlobBaseFeeScalar)?, + )) + }) + .transpose()? + .unwrap_or_default(); + let base_fee_scalar = u32::from_be_bytes( + scalar[28..32].try_into().map_err(|_| BlockInfoError::BaseFeeScalar)?, + ); + + // Determine the blob fee configuration based on the timestamp. + // We start with the scheduled blob fee parameters, and then check for the osaka and prague + // parameters. + let blob_fee_params = l1_config.blob_schedule_blob_params(); + + let blob_fee_config = + match blob_fee_params.active_scheduled_params_at_timestamp(l1_header.timestamp) { + Some(blob_fee_param) => *blob_fee_param, + None if l1_config.osaka_time.is_some_and(|time| time <= l1_header.timestamp) => { + BlobParams::osaka() + } + None if l1_config + .prague_time.is_some_and(|time| time <= l1_header.timestamp) && + // There was an incident on OP Stack Sepolia chains (03-05-2025) when L1 activated pectra, + // where the sequencer followed the incorrect chain, using the legacy Cancun blob fee + // schedule instead of the new Prague blob fee schedule. This portion of the chain was + // chosen to be canonicalized in favor of the prospect of a deep reorg imposed by the + // sequencers of the testnet chains. An optional hardfork was introduced for Sepolia only, + // where if present, activates the use of the Prague blob fee schedule. If the hardfork is + // not present, and L1 has activated pectra, the Prague blob fee schedule is used + // immediately. + (rollup_config.hardforks.pectra_blob_schedule_time.is_none() || + rollup_config.is_pectra_blob_schedule_active(l1_header.timestamp)) => + { + BlobParams::prague() + } + _ => BlobParams::cancun(), + }; + + let blob_base_fee = l1_header.blob_fee(blob_fee_config).unwrap_or(1); + let block_hash = l1_header.hash_slow(); + let base_fee = l1_header.base_fee_per_gas.unwrap_or(0); + + if rollup_config.is_jovian_active(l2_block_time) && + !rollup_config.is_first_jovian_block(l2_block_time) + { + let operator_fee_scalar = system_config.operator_fee_scalar.unwrap_or_default(); + let operator_fee_constant = system_config.operator_fee_constant.unwrap_or_default(); + let mut da_footprint_gas_scalar = system_config + .da_footprint_gas_scalar + .unwrap_or(L1BlockInfoJovian::DEFAULT_DA_FOOTPRINT_GAS_SCALAR); + + if da_footprint_gas_scalar == 0 { + da_footprint_gas_scalar = L1BlockInfoJovian::DEFAULT_DA_FOOTPRINT_GAS_SCALAR; + } + + return Ok(Self::Jovian(L1BlockInfoJovian { + number: l1_header.number, + time: l1_header.timestamp, + base_fee, + block_hash, + sequence_number, + batcher_address: system_config.batcher_address, + blob_base_fee, + blob_base_fee_scalar, + base_fee_scalar, + operator_fee_scalar, + operator_fee_constant, + da_footprint_gas_scalar, + })); + } + + if rollup_config.is_isthmus_active(l2_block_time) && + !rollup_config.is_first_isthmus_block(l2_block_time) + { + let operator_fee_scalar = system_config.operator_fee_scalar.unwrap_or_default(); + let operator_fee_constant = system_config.operator_fee_constant.unwrap_or_default(); + return Ok(Self::Isthmus(L1BlockInfoIsthmus { + number: l1_header.number, + time: l1_header.timestamp, + base_fee, + block_hash, + sequence_number, + batcher_address: system_config.batcher_address, + blob_base_fee, + blob_base_fee_scalar, + base_fee_scalar, + operator_fee_scalar, + operator_fee_constant, + })); + } + + Ok(Self::Ecotone(L1BlockInfoEcotone { + number: l1_header.number, + time: l1_header.timestamp, + base_fee, + block_hash, + sequence_number, + batcher_address: system_config.batcher_address, + blob_base_fee, + blob_base_fee_scalar, + base_fee_scalar, + empty_scalars: false, + l1_fee_overhead: U256::ZERO, + })) + } + + /// Creates a new [`L1BlockInfoTx`] from the given information and returns a typed [`TxDeposit`] + /// to include at the top of a block. + pub fn try_new_with_deposit_tx( + rollup_config: &RollupConfig, + l1_config: &L1ChainConfig, + system_config: &SystemConfig, + sequence_number: u64, + l1_header: &Header, + l2_block_time: u64, + ) -> Result<(Self, Sealed), BlockInfoError> { + let l1_info = Self::try_new( + rollup_config, + l1_config, + system_config, + sequence_number, + l1_header, + l2_block_time, + )?; + + let source = DepositSourceDomain::L1Info(L1InfoDepositSource { + l1_block_hash: l1_info.block_hash(), + seq_number: sequence_number, + }); + + let mut deposit_tx = TxDeposit { + source_hash: source.source_hash(), + from: L1_INFO_DEPOSITOR_ADDRESS, + to: TxKind::Call(Predeploys::L1_BLOCK_INFO), + mint: 0, + value: U256::ZERO, + gas_limit: 150_000_000, + is_system_transaction: true, + input: l1_info.encode_calldata(), + }; + + // With the regolith hardfork, system transactions were deprecated, and we allocate + // a constant amount of gas for special transactions like L1 block info. + if rollup_config.is_regolith_active(l2_block_time) { + deposit_tx.is_system_transaction = false; + deposit_tx.gas_limit = REGOLITH_SYSTEM_TX_GAS; + } + + Ok((l1_info, deposit_tx.seal_slow())) + } + + /// Decodes the [`L1BlockInfoTx`] object from Ethereum transaction calldata. + pub fn decode_calldata(r: &[u8]) -> Result { + if r.len() < 4 { + return Err(DecodeError::MissingSelector); + } + // SAFETY: The length of `r` must be at least 4 bytes. + let mut selector = [0u8; 4]; + selector.copy_from_slice(&r[0..4]); + match selector { + L1BlockInfoBedrock::L1_INFO_TX_SELECTOR => { + L1BlockInfoBedrock::decode_calldata(r).map(Self::Bedrock) + } + L1BlockInfoEcotone::L1_INFO_TX_SELECTOR => { + L1BlockInfoEcotone::decode_calldata(r).map(Self::Ecotone) + } + L1BlockInfoIsthmus::L1_INFO_TX_SELECTOR => { + L1BlockInfoIsthmus::decode_calldata(r).map(Self::Isthmus) + } + L1BlockInfoJovian::L1_INFO_TX_SELECTOR => { + L1BlockInfoJovian::decode_calldata(r).map(Self::Jovian) + } + _ => Err(DecodeError::InvalidSelector), + } + } + + /// Returns whether the scalars are empty. + pub const fn empty_scalars(&self) -> bool { + match self { + Self::Bedrock(_) | Self::Isthmus(..) | Self::Jovian(_) => false, + Self::Ecotone(L1BlockInfoEcotone { empty_scalars, .. }) => *empty_scalars, + } + } + + /// Returns the block hash for the [`L1BlockInfoTx`]. + pub const fn block_hash(&self) -> B256 { + match self { + Self::Bedrock(tx) => tx.block_hash, + Self::Ecotone(tx) => tx.block_hash, + Self::Isthmus(tx) => tx.block_hash, + Self::Jovian(tx) => tx.block_hash, + } + } + + /// Encodes the [`L1BlockInfoTx`] object into Ethereum transaction calldata. + pub fn encode_calldata(&self) -> Bytes { + match self { + Self::Bedrock(bedrock_tx) => bedrock_tx.encode_calldata(), + Self::Ecotone(ecotone_tx) => ecotone_tx.encode_calldata(), + Self::Isthmus(isthmus_tx) => isthmus_tx.encode_calldata(), + Self::Jovian(jovian_tx) => jovian_tx.encode_calldata(), + } + } + + /// Returns the L1 [`BlockNumHash`] for the info transaction. + pub const fn id(&self) -> BlockNumHash { + match self { + Self::Ecotone(L1BlockInfoEcotone { number, block_hash, .. }) | + Self::Bedrock(L1BlockInfoBedrock { number, block_hash, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { number, block_hash, .. }) | + Self::Jovian(L1BlockInfoJovian { number, block_hash, .. }) => { + BlockNumHash { number: *number, hash: *block_hash } + } + } + } + + /// Returns the operator fee scalar. + pub const fn operator_fee_scalar(&self) -> u32 { + match self { + Self::Jovian(L1BlockInfoJovian { operator_fee_scalar, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { operator_fee_scalar, .. }) => *operator_fee_scalar, + _ => 0, + } + } + + /// Returns the operator fee constant. + pub const fn operator_fee_constant(&self) -> u64 { + match self { + Self::Jovian(L1BlockInfoJovian { operator_fee_constant, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { operator_fee_constant, .. }) => { + *operator_fee_constant + } + _ => 0, + } + } + + /// Returns the da footprint + pub const fn da_footprint(&self) -> Option { + match self { + Self::Jovian(L1BlockInfoJovian { da_footprint_gas_scalar, .. }) => { + Some(*da_footprint_gas_scalar) + } + _ => None, + } + } + + /// Returns the l1 base fee. + pub fn l1_base_fee(&self) -> U256 { + match self { + Self::Bedrock(L1BlockInfoBedrock { base_fee, .. }) | + Self::Ecotone(L1BlockInfoEcotone { base_fee, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { base_fee, .. }) | + Self::Jovian(L1BlockInfoJovian { base_fee, .. }) => U256::from(*base_fee), + } + } + + /// Returns the l1 fee scalar. + pub fn l1_fee_scalar(&self) -> U256 { + match self { + Self::Bedrock(L1BlockInfoBedrock { l1_fee_scalar, .. }) => *l1_fee_scalar, + Self::Ecotone(L1BlockInfoEcotone { base_fee_scalar, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { base_fee_scalar, .. }) | + Self::Jovian(L1BlockInfoJovian { base_fee_scalar, .. }) => U256::from(*base_fee_scalar), + } + } + + /// Returns the blob base fee. + pub fn blob_base_fee(&self) -> U256 { + match self { + Self::Bedrock(_) => U256::ZERO, + Self::Ecotone(L1BlockInfoEcotone { blob_base_fee, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { blob_base_fee, .. }) | + Self::Jovian(L1BlockInfoJovian { blob_base_fee, .. }) => U256::from(*blob_base_fee), + } + } + + /// Returns the blob base fee scalar. + pub fn blob_base_fee_scalar(&self) -> U256 { + match self { + Self::Bedrock(_) => U256::ZERO, + Self::Ecotone(L1BlockInfoEcotone { blob_base_fee_scalar, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { blob_base_fee_scalar, .. }) | + Self::Jovian(L1BlockInfoJovian { blob_base_fee_scalar, .. }) => { + U256::from(*blob_base_fee_scalar) + } + } + } + + /// Returns the L1 fee overhead for the info transaction. After ecotone, this value is ignored. + pub const fn l1_fee_overhead(&self) -> U256 { + match self { + Self::Bedrock(L1BlockInfoBedrock { l1_fee_overhead, .. }) => *l1_fee_overhead, + Self::Ecotone(L1BlockInfoEcotone { l1_fee_overhead, .. }) => *l1_fee_overhead, + Self::Isthmus(_) | Self::Jovian(_) => U256::ZERO, + } + } + + /// Returns the batcher address for the info transaction + pub const fn batcher_address(&self) -> Address { + match self { + Self::Bedrock(L1BlockInfoBedrock { batcher_address, .. }) | + Self::Ecotone(L1BlockInfoEcotone { batcher_address, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { batcher_address, .. }) | + Self::Jovian(L1BlockInfoJovian { batcher_address, .. }) => *batcher_address, + } + } + + /// Returns the sequence number for the info transaction + pub const fn sequence_number(&self) -> u64 { + match self { + Self::Bedrock(L1BlockInfoBedrock { sequence_number, .. }) | + Self::Ecotone(L1BlockInfoEcotone { sequence_number, .. }) | + Self::Isthmus(L1BlockInfoIsthmus { sequence_number, .. }) | + Self::Jovian(L1BlockInfoJovian { sequence_number, .. }) => *sequence_number, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::{RAW_BEDROCK_INFO_TX, RAW_ECOTONE_INFO_TX, RAW_ISTHMUS_INFO_TX}; + use alloc::{string::ToString, vec::Vec}; + use alloy_primitives::{address, b256}; + use kona_genesis::HardForkConfig; + use kona_registry::L1Config; + use rstest::rstest; + + #[test] + fn test_l1_block_info_missing_selector() { + let err = L1BlockInfoTx::decode_calldata(&[]); + assert_eq!(err, Err(DecodeError::MissingSelector)); + } + + #[test] + fn test_l1_block_info_tx_invalid_len() { + let calldata = L1BlockInfoBedrock::L1_INFO_TX_SELECTOR + .into_iter() + .chain([0xde, 0xad]) + .collect::>(); + let err = L1BlockInfoTx::decode_calldata(&calldata); + assert!(err.is_err()); + assert_eq!( + err.err().unwrap().to_string(), + "Invalid bedrock data length. Expected 260, got 6" + ); + + let calldata = L1BlockInfoEcotone::L1_INFO_TX_SELECTOR + .into_iter() + .chain([0xde, 0xad]) + .collect::>(); + let err = L1BlockInfoTx::decode_calldata(&calldata); + assert!(err.is_err()); + assert_eq!( + err.err().unwrap().to_string(), + "Invalid ecotone data length. Expected 164, got 6" + ); + + let calldata = L1BlockInfoIsthmus::L1_INFO_TX_SELECTOR + .into_iter() + .chain([0xde, 0xad]) + .collect::>(); + let err = L1BlockInfoTx::decode_calldata(&calldata); + assert!(err.is_err()); + assert_eq!( + err.err().unwrap().to_string(), + "Invalid isthmus data length. Expected 176, got 6" + ); + } + + #[test] + fn test_l1_block_info_tx_block_hash() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + block_hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"), + ..Default::default() + }); + assert_eq!( + bedrock.block_hash(), + b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc") + ); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + block_hash: b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3"), + ..Default::default() + }); + assert_eq!( + ecotone.block_hash(), + b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3") + ); + } + + #[test] + fn test_decode_calldata_invalid_selector() { + let err = L1BlockInfoTx::decode_calldata(&[0xde, 0xad, 0xbe, 0xef]); + assert_eq!(err, Err(DecodeError::InvalidSelector)); + } + + #[test] + fn test_l1_block_info_id() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + number: 123, + block_hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"), + ..Default::default() + }); + assert_eq!( + bedrock.id(), + BlockNumHash { + number: 123, + hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc") + } + ); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + number: 456, + block_hash: b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3"), + ..Default::default() + }); + assert_eq!( + ecotone.id(), + BlockNumHash { + number: 456, + hash: b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3") + } + ); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + number: 101112, + block_hash: b256!("4f98b83baf52c498b49bfff33e59965b27da7febbea9a2fcc4719d06dc06932a"), + ..Default::default() + }); + assert_eq!( + isthmus.id(), + BlockNumHash { + number: 101112, + hash: b256!("4f98b83baf52c498b49bfff33e59965b27da7febbea9a2fcc4719d06dc06932a") + } + ); + } + + #[test] + fn test_l1_block_info_sequence_number() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + sequence_number: 123, + ..Default::default() + }); + assert_eq!(bedrock.sequence_number(), 123); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + sequence_number: 456, + ..Default::default() + }); + assert_eq!(ecotone.sequence_number(), 456); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + sequence_number: 101112, + ..Default::default() + }); + assert_eq!(isthmus.sequence_number(), 101112); + } + + #[test] + fn test_operator_fee_constant() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock::default()); + assert_eq!(bedrock.operator_fee_constant(), 0); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone::default()); + assert_eq!(ecotone.operator_fee_constant(), 0); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + operator_fee_constant: 123, + ..Default::default() + }); + assert_eq!(isthmus.operator_fee_constant(), 123); + } + + #[test] + fn test_operator_fee_scalar() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock::default()); + assert_eq!(bedrock.operator_fee_scalar(), 0); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone::default()); + assert_eq!(ecotone.operator_fee_scalar(), 0); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + operator_fee_scalar: 123, + ..Default::default() + }); + assert_eq!(isthmus.operator_fee_scalar(), 123); + } + + #[test] + fn test_l1_base_fee() { + let bedrock = + L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { base_fee: 123, ..Default::default() }); + assert_eq!(bedrock.l1_base_fee(), U256::from(123)); + + let ecotone = + L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { base_fee: 456, ..Default::default() }); + assert_eq!(ecotone.l1_base_fee(), U256::from(456)); + + let isthmus = + L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { base_fee: 101112, ..Default::default() }); + assert_eq!(isthmus.l1_base_fee(), U256::from(101112)); + } + + #[test] + fn test_l1_fee_overhead() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + l1_fee_overhead: U256::from(123), + ..Default::default() + }); + assert_eq!(bedrock.l1_fee_overhead(), U256::from(123)); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + l1_fee_overhead: U256::from(456), + ..Default::default() + }); + assert_eq!(ecotone.l1_fee_overhead(), U256::from(456)); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus::default()); + assert_eq!(isthmus.l1_fee_overhead(), U256::ZERO); + } + + #[test] + fn test_batcher_address() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + ..Default::default() + }); + assert_eq!(bedrock.batcher_address(), address!("6887246668a3b87f54deb3b94ba47a6f63f32985")); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + ..Default::default() + }); + assert_eq!(ecotone.batcher_address(), address!("6887246668a3b87f54deb3b94ba47a6f63f32985")); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + ..Default::default() + }); + assert_eq!(isthmus.batcher_address(), address!("6887246668a3b87f54deb3b94ba47a6f63f32985")); + } + + #[test] + fn test_l1_fee_scalar() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { + l1_fee_scalar: U256::from(123), + ..Default::default() + }); + assert_eq!(bedrock.l1_fee_scalar(), U256::from(123)); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + base_fee_scalar: 456, + ..Default::default() + }); + assert_eq!(ecotone.l1_fee_scalar(), U256::from(456)); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + base_fee_scalar: 101112, + ..Default::default() + }); + assert_eq!(isthmus.l1_fee_scalar(), U256::from(101112)); + } + + #[test] + fn test_blob_base_fee() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { ..Default::default() }); + assert_eq!(bedrock.blob_base_fee(), U256::ZERO); + + let ecotone = + L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { blob_base_fee: 456, ..Default::default() }); + assert_eq!(ecotone.blob_base_fee(), U256::from(456)); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + blob_base_fee: 101112, + ..Default::default() + }); + assert_eq!(isthmus.blob_base_fee(), U256::from(101112)); + } + + #[test] + fn test_blob_base_fee_scalar() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { ..Default::default() }); + assert_eq!(bedrock.blob_base_fee_scalar(), U256::ZERO); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + blob_base_fee_scalar: 456, + ..Default::default() + }); + assert_eq!(ecotone.blob_base_fee_scalar(), U256::from(456)); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + blob_base_fee_scalar: 101112, + ..Default::default() + }); + assert_eq!(isthmus.blob_base_fee_scalar(), U256::from(101112)); + } + + #[test] + fn test_empty_scalars() { + let bedrock = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { ..Default::default() }); + assert!(!bedrock.empty_scalars()); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + empty_scalars: true, + ..Default::default() + }); + assert!(ecotone.empty_scalars()); + + let ecotone = L1BlockInfoTx::Ecotone(L1BlockInfoEcotone::default()); + assert!(!ecotone.empty_scalars()); + + let isthmus = L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus::default()); + assert!(!isthmus.empty_scalars()); + } + + #[test] + fn test_isthmus_l1_block_info_tx_roundtrip() { + let expected = L1BlockInfoIsthmus { + number: 19655712, + time: 1713121139, + base_fee: 10445852825, + block_hash: b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3"), + sequence_number: 5, + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + blob_base_fee: 1, + blob_base_fee_scalar: 810949, + base_fee_scalar: 1368, + operator_fee_scalar: 0xabcd, + operator_fee_constant: 0xdcba, + }; + + let L1BlockInfoTx::Isthmus(decoded) = + L1BlockInfoTx::decode_calldata(RAW_ISTHMUS_INFO_TX.as_ref()).unwrap() + else { + panic!("Wrong fork"); + }; + assert_eq!(expected, decoded); + assert_eq!(L1BlockInfoTx::Isthmus(decoded).encode_calldata().as_ref(), RAW_ISTHMUS_INFO_TX); + } + + #[test] + fn test_bedrock_l1_block_info_tx_roundtrip() { + let expected = L1BlockInfoBedrock { + number: 18334955, + time: 1697121143, + base_fee: 10419034451, + block_hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"), + sequence_number: 4, + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + l1_fee_overhead: U256::from(0xbc), + l1_fee_scalar: U256::from(0xa6fe0), + }; + + let L1BlockInfoTx::Bedrock(decoded) = + L1BlockInfoTx::decode_calldata(RAW_BEDROCK_INFO_TX.as_ref()).unwrap() + else { + panic!("Wrong fork"); + }; + assert_eq!(expected, decoded); + assert_eq!(L1BlockInfoTx::Bedrock(decoded).encode_calldata().as_ref(), RAW_BEDROCK_INFO_TX); + } + + #[test] + fn test_ecotone_l1_block_info_tx_roundtrip() { + let expected = L1BlockInfoEcotone { + number: 19655712, + time: 1713121139, + base_fee: 10445852825, + block_hash: b256!("1c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add3"), + sequence_number: 5, + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + blob_base_fee: 1, + blob_base_fee_scalar: 810949, + base_fee_scalar: 1368, + empty_scalars: false, + l1_fee_overhead: U256::ZERO, + }; + + let L1BlockInfoTx::Ecotone(decoded) = + L1BlockInfoTx::decode_calldata(RAW_ECOTONE_INFO_TX.as_ref()).unwrap() + else { + panic!("Wrong fork"); + }; + assert_eq!(expected, decoded); + assert_eq!(L1BlockInfoTx::Ecotone(decoded).encode_calldata().as_ref(), RAW_ECOTONE_INFO_TX); + } + + #[test] + fn test_try_new_bedrock() { + let rollup_config = RollupConfig::default(); + let l1_config = L1Config::sepolia(); + let system_config = SystemConfig::default(); + let sequence_number = 0; + let l1_header = Header::default(); + let l2_block_time = 0; + + let l1_info = L1BlockInfoTx::try_new( + &rollup_config, + &l1_config, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + let L1BlockInfoTx::Bedrock(l1_info) = l1_info else { + panic!("Wrong fork"); + }; + + assert_eq!(l1_info.number, l1_header.number); + assert_eq!(l1_info.time, l1_header.timestamp); + assert_eq!(l1_info.base_fee, { l1_header.base_fee_per_gas.unwrap_or(0) }); + assert_eq!(l1_info.block_hash, l1_header.hash_slow()); + assert_eq!(l1_info.sequence_number, sequence_number); + assert_eq!(l1_info.batcher_address, system_config.batcher_address); + assert_eq!(l1_info.l1_fee_overhead, system_config.overhead); + assert_eq!(l1_info.l1_fee_scalar, system_config.scalar); + } + + #[test] + fn test_try_new_ecotone() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { ecotone_time: Some(1), ..Default::default() }, + ..Default::default() + }; + let l1_config = L1Config::sepolia(); + let system_config = SystemConfig::default(); + let sequence_number = 0; + let l1_header = Header::default(); + let l2_block_time = 0xFF; + + let l1_info = L1BlockInfoTx::try_new( + &rollup_config, + &l1_config, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + let L1BlockInfoTx::Ecotone(l1_info) = l1_info else { + panic!("Wrong fork"); + }; + + assert_eq!(l1_info.number, l1_header.number); + assert_eq!(l1_info.time, l1_header.timestamp); + assert_eq!(l1_info.base_fee, { l1_header.base_fee_per_gas.unwrap_or(0) }); + assert_eq!(l1_info.block_hash, l1_header.hash_slow()); + assert_eq!(l1_info.sequence_number, sequence_number); + assert_eq!(l1_info.batcher_address, system_config.batcher_address); + assert_eq!(l1_info.blob_base_fee, l1_header.blob_fee(BlobParams::cancun()).unwrap_or(1)); + + let scalar = system_config.scalar.to_be_bytes::<32>(); + let blob_base_fee_scalar = if scalar[0] == L1BlockInfoEcotone::L1_SCALAR { + { + u32::from_be_bytes( + scalar[24..28].try_into().expect("Failed to parse L1 blob base fee scalar"), + ) + } + } else { + Default::default() + }; + let base_fee_scalar = + u32::from_be_bytes(scalar[28..32].try_into().expect("Failed to parse base fee scalar")); + assert_eq!(l1_info.blob_base_fee_scalar, blob_base_fee_scalar); + assert_eq!(l1_info.base_fee_scalar, base_fee_scalar); + } + + #[rstest] + #[case::fork_active(true, false)] + #[case::fork_inactive(false, false)] + #[should_panic] + #[case::fork_active_wrong_params(true, true)] + #[should_panic] + #[case::fork_inactive_wrong_params(false, true)] + fn test_try_new_ecotone_with_optional_prague_fee_fork( + #[case] fork_active: bool, + #[case] use_wrong_params: bool, + ) { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { + ecotone_time: Some(1), + pectra_blob_schedule_time: Some(2), + ..Default::default() + }, + ..Default::default() + }; + let mut l1_genesis: L1ChainConfig = L1Config::sepolia().into(); + l1_genesis.prague_time = Some(2); + + let system_config = SystemConfig::default(); + let sequence_number = 0; + let l1_header = Header { + timestamp: if fork_active { 2 } else { 1 }, + excess_blob_gas: Some(0x5080000), + blob_gas_used: Some(0x100000), + requests_hash: Some(B256::ZERO), + ..Default::default() + }; + let l2_block_time = 0xFF; + + let l1_info = L1BlockInfoTx::try_new( + &rollup_config, + &l1_genesis, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + let L1BlockInfoTx::Ecotone(l1_info) = l1_info else { + panic!("Wrong fork"); + }; + + assert_eq!(l1_info.number, l1_header.number); + assert_eq!(l1_info.time, l1_header.timestamp); + assert_eq!(l1_info.base_fee, { l1_header.base_fee_per_gas.unwrap_or(0) }); + assert_eq!(l1_info.block_hash, l1_header.hash_slow()); + assert_eq!(l1_info.sequence_number, sequence_number); + assert_eq!(l1_info.batcher_address, system_config.batcher_address); + assert_eq!( + l1_info.blob_base_fee, + l1_header + .blob_fee(if fork_active != use_wrong_params { + BlobParams::prague() + } else { + BlobParams::cancun() + }) + .unwrap_or(1) + ); + + let scalar = system_config.scalar.to_be_bytes::<32>(); + let blob_base_fee_scalar = if scalar[0] == L1BlockInfoEcotone::L1_SCALAR { + { + u32::from_be_bytes( + scalar[24..28].try_into().expect("Failed to parse L1 blob base fee scalar"), + ) + } + } else { + Default::default() + }; + let base_fee_scalar = + u32::from_be_bytes(scalar[28..32].try_into().expect("Failed to parse base fee scalar")); + assert_eq!(l1_info.blob_base_fee_scalar, blob_base_fee_scalar); + assert_eq!(l1_info.base_fee_scalar, base_fee_scalar); + } + + #[test] + fn test_try_new_isthmus_before_pectra_blob_schedule() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { + isthmus_time: Some(1), + pectra_blob_schedule_time: Some(1713121140), + ..Default::default() + }, + ..Default::default() + }; + let l1_config = L1Config::sepolia(); + let system_config = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + operator_fee_scalar: Some(0xabcd), + operator_fee_constant: Some(0xdcba), + ..Default::default() + }; + let sequence_number = 0; + let l1_header = Header { + number: 19655712, + timestamp: 1713121139, + base_fee_per_gas: Some(10445852825), + // Assume Pectra is active on L1 + requests_hash: Some(B256::ZERO), + ..Default::default() + }; + let l2_block_time = 0xFF; + + let l1_info = L1BlockInfoTx::try_new( + &rollup_config, + &l1_config, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + assert!(matches!(l1_info, L1BlockInfoTx::Isthmus(_))); + + let scalar = system_config.scalar.to_be_bytes::<32>(); + let blob_base_fee_scalar = if scalar[0] == L1BlockInfoIsthmus::L1_SCALAR { + { + u32::from_be_bytes( + scalar[24..28].try_into().expect("Failed to parse L1 blob base fee scalar"), + ) + } + } else { + Default::default() + }; + let base_fee_scalar = + u32::from_be_bytes(scalar[28..32].try_into().expect("Failed to parse base fee scalar")); + + assert_eq!( + l1_info, + L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + number: l1_header.number, + time: l1_header.timestamp, + base_fee: l1_header.base_fee_per_gas.unwrap_or(0), + block_hash: l1_header.hash_slow(), + sequence_number, + batcher_address: system_config.batcher_address, + // Expect cancun blob schedule to be used, since pectra blob schedule is scheduled + // but not active yet. + blob_base_fee: l1_header.blob_fee(BlobParams::cancun()).unwrap_or(1), + blob_base_fee_scalar, + base_fee_scalar, + operator_fee_scalar: system_config.operator_fee_scalar.unwrap_or_default(), + operator_fee_constant: system_config.operator_fee_constant.unwrap_or_default(), + }) + ); + } + + #[test] + fn test_try_new_isthmus() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { isthmus_time: Some(1), ..Default::default() }, + ..Default::default() + }; + let l1_config = L1Config::sepolia(); + let system_config = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + operator_fee_scalar: Some(0xabcd), + operator_fee_constant: Some(0xdcba), + ..Default::default() + }; + let sequence_number = 0; + let l1_header = Header { + number: 19655712, + timestamp: 1713121139, + base_fee_per_gas: Some(10445852825), + ..Default::default() + }; + let l2_block_time = 0xFF; + + let l1_info = L1BlockInfoTx::try_new( + &rollup_config, + &l1_config, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + assert!(matches!(l1_info, L1BlockInfoTx::Isthmus(_))); + + let scalar = system_config.scalar.to_be_bytes::<32>(); + let blob_base_fee_scalar = if scalar[0] == L1BlockInfoIsthmus::L1_SCALAR { + { + u32::from_be_bytes( + scalar[24..28].try_into().expect("Failed to parse L1 blob base fee scalar"), + ) + } + } else { + Default::default() + }; + let base_fee_scalar = + u32::from_be_bytes(scalar[28..32].try_into().expect("Failed to parse base fee scalar")); + + assert_eq!( + l1_info, + L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + number: l1_header.number, + time: l1_header.timestamp, + base_fee: l1_header.base_fee_per_gas.unwrap_or(0), + block_hash: l1_header.hash_slow(), + sequence_number, + batcher_address: system_config.batcher_address, + blob_base_fee: l1_header.blob_fee(BlobParams::prague()).unwrap_or(1), + blob_base_fee_scalar, + base_fee_scalar, + operator_fee_scalar: system_config.operator_fee_scalar.unwrap_or_default(), + operator_fee_constant: system_config.operator_fee_constant.unwrap_or_default(), + }) + ); + } + + #[test] + fn test_try_new_with_deposit_tx() { + let rollup_config = RollupConfig { + hardforks: HardForkConfig { isthmus_time: Some(1), ..Default::default() }, + ..Default::default() + }; + let l1_config = L1Config::sepolia(); + let system_config = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + operator_fee_scalar: Some(0xabcd), + operator_fee_constant: Some(0xdcba), + ..Default::default() + }; + let sequence_number = 0; + let l1_header = Header { + number: 19655712, + timestamp: 1713121139, + base_fee_per_gas: Some(10445852825), + ..Default::default() + }; + let l2_block_time = 0xFF; + + let (l1_info, deposit_tx) = L1BlockInfoTx::try_new_with_deposit_tx( + &rollup_config, + &l1_config, + &system_config, + sequence_number, + &l1_header, + l2_block_time, + ) + .unwrap(); + + assert!(matches!(l1_info, L1BlockInfoTx::Isthmus(_))); + assert_eq!(deposit_tx.from, L1_INFO_DEPOSITOR_ADDRESS); + assert_eq!(deposit_tx.to, TxKind::Call(Predeploys::L1_BLOCK_INFO)); + assert_eq!(deposit_tx.mint, 0); + assert_eq!(deposit_tx.value, U256::ZERO); + assert_eq!(deposit_tx.gas_limit, REGOLITH_SYSTEM_TX_GAS); + assert!(!deposit_tx.is_system_transaction); + assert_eq!(deposit_tx.input, l1_info.encode_calldata()); + } +} diff --git a/kona/crates/protocol/protocol/src/lib.rs b/kona/crates/protocol/protocol/src/lib.rs new file mode 100644 index 0000000000000..0694b3963519d --- /dev/null +++ b/kona/crates/protocol/protocol/src/lib.rs @@ -0,0 +1,71 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod batch; +pub use batch::{ + Batch, BatchDecodingError, BatchEncodingError, BatchReader, BatchTransaction, BatchType, + BatchValidationProvider, BatchValidity, BatchWithInclusionBlock, DecompressionError, + MAX_SPAN_BATCH_ELEMENTS, RawSpanBatch, SINGLE_BATCH_TYPE, SPAN_BATCH_TYPE, SingleBatch, + SpanBatch, SpanBatchBits, SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchEip7702TransactionData, SpanBatchElement, SpanBatchError, + SpanBatchLegacyTransactionData, SpanBatchPayload, SpanBatchPrefix, SpanBatchTransactionData, + SpanBatchTransactions, SpanDecodingError, +}; + +mod brotli; +pub use brotli::{BrotliDecompressionError, decompress_brotli}; + +mod sync; +pub use sync::SyncStatus; + +mod attributes; +pub use attributes::OpAttributesWithParent; + +mod errors; +pub use errors::OpBlockConversionError; + +mod block; +pub use block::{BlockInfo, FromBlockError, L2BlockInfo}; + +mod frame; +pub use frame::{ + DERIVATION_VERSION_0, FRAME_OVERHEAD, Frame, FrameDecodingError, FrameParseError, MAX_FRAME_LEN, +}; + +mod utils; +pub use utils::{read_tx_data, to_system_config}; + +mod channel; +pub use channel::{ + CHANNEL_ID_LENGTH, Channel, ChannelError, ChannelId, FJORD_MAX_RLP_BYTES_PER_CHANNEL, + MAX_RLP_BYTES_PER_CHANNEL, +}; + +mod deposits; +pub use deposits::{ + DEPOSIT_EVENT_ABI, DEPOSIT_EVENT_ABI_HASH, DEPOSIT_EVENT_VERSION_0, DepositError, + decode_deposit, +}; + +mod info; +pub use info::{ + BlockInfoError, DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoIsthmus, + L1BlockInfoJovian, L1BlockInfoTx, +}; + +mod predeploys; +pub use predeploys::Predeploys; + +mod output_root; +pub use output_root::OutputRoot; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/kona/crates/protocol/protocol/src/output_root.rs b/kona/crates/protocol/protocol/src/output_root.rs new file mode 100644 index 0000000000000..ce076d2951fd9 --- /dev/null +++ b/kona/crates/protocol/protocol/src/output_root.rs @@ -0,0 +1,96 @@ +//! The [`OutputRoot`] type. + +use alloy_primitives::{B256, keccak256}; +use derive_more::Display; + +/// The [`OutputRoot`] is a high-level commitment to an L2 block. It lifts the state root from the +/// block header as well as the storage root of the [Predeploys::L2_TO_L1_MESSAGE_PASSER] account +/// into the top-level commitment construction. +/// +/// +/// +/// [Predeploys::L2_TO_L1_MESSAGE_PASSER]: crate::Predeploys::L2_TO_L1_MESSAGE_PASSER +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash)] +#[display("OutputRootV0({}, {}, {})", state_root, bridge_storage_root, block_hash)] +pub struct OutputRoot { + /// The state root of the block corresponding to the output root. + pub state_root: B256, + /// The storage root of the `L2ToL1MessagePasser` predeploy at the block corresponding to the + /// output root. + pub bridge_storage_root: B256, + /// The block hash that the output root represents. + pub block_hash: B256, +} + +impl OutputRoot { + /// The encoded length of a V0 output root. + pub const ENCODED_LENGTH: usize = 128; + + /// The version of the [`OutputRoot`]. Currently, the protocol only supports one version of this + /// commitment. + pub const VERSION: u8 = 0; + + /// Returns the version of the [`OutputRoot`]. Currently, the protocol only supports the version + /// number 0. + pub const fn version(&self) -> B256 { + B256::ZERO + } + + /// Constructs a V0 [`OutputRoot`] from its parts. + pub const fn from_parts(state_root: B256, bridge_storage_root: B256, block_hash: B256) -> Self { + Self { state_root, bridge_storage_root, block_hash } + } + + /// Encodes the [`OutputRoot`]. + pub fn encode(&self) -> [u8; Self::ENCODED_LENGTH] { + let mut encoded = [0u8; Self::ENCODED_LENGTH]; + encoded[31] = Self::VERSION; + encoded[32..64].copy_from_slice(self.state_root.as_slice()); + encoded[64..96].copy_from_slice(self.bridge_storage_root.as_slice()); + encoded[96..128].copy_from_slice(self.block_hash.as_slice()); + encoded + } + + /// Encodes and hashes the [`OutputRoot`]. + pub fn hash(&self) -> B256 { + keccak256(self.encode()) + } +} + +#[cfg(test)] +mod test { + use super::OutputRoot; + use alloy_primitives::{B256, Bytes, b256, bytes}; + + fn test_or() -> OutputRoot { + OutputRoot::from_parts( + B256::left_padding_from(&[0xbe, 0xef]), + B256::left_padding_from(&[0xba, 0xbe]), + B256::left_padding_from(&[0xc0, 0xde]), + ) + } + + #[test] + fn test_hash_output_root() { + const EXPECTED_HASH: B256 = + b256!("0c39fb6b07cf6694b13e63e59f7b15255be1c93a4d6d3e0da6c99729647c0d11"); + + let root = test_or(); + assert_eq!(root.hash(), EXPECTED_HASH); + } + + #[test] + fn test_encode_output_root() { + const EXPECTED_ENCODING: Bytes = bytes!( + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000beef000000000000000000000000000000000000000000000000000000000000babe000000000000000000000000000000000000000000000000000000000000c0de" + ); + + let root = OutputRoot::from_parts( + B256::left_padding_from(&[0xbe, 0xef]), + B256::left_padding_from(&[0xba, 0xbe]), + B256::left_padding_from(&[0xc0, 0xde]), + ); + + assert_eq!(root.encode().as_ref(), EXPECTED_ENCODING.as_ref()); + } +} diff --git a/kona/crates/protocol/protocol/src/predeploys.rs b/kona/crates/protocol/protocol/src/predeploys.rs new file mode 100644 index 0000000000000..6d30ee0ad6066 --- /dev/null +++ b/kona/crates/protocol/protocol/src/predeploys.rs @@ -0,0 +1,141 @@ +//! Addresses of OP pre-deploys. +//! +//! This module contains the addresses of various predeploy contracts in the OP Stack. +//! See the complete set of predeploys at + +use alloy_primitives::{Address, address}; + +/// Container for all predeploy contract addresses +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct Predeploys; + +impl Predeploys { + /// List of all predeploys. + pub const ALL: [Address; 24] = [ + Self::LEGACY_MESSAGE_PASSER, + Self::DEPLOYER_WHITELIST, + Self::LEGACY_ERC20_ETH, + Self::WETH9, + Self::L2_CROSS_DOMAIN_MESSENGER, + Self::L2_STANDARD_BRIDGE, + Self::SEQUENCER_FEE_VAULT, + Self::OP_MINTABLE_ERC20_FACTORY, + Self::L1_BLOCK_NUMBER, + Self::GAS_PRICE_ORACLE, + Self::GOVERNANCE_TOKEN, + Self::L1_BLOCK_INFO, + Self::L2_TO_L1_MESSAGE_PASSER, + Self::L2_ERC721_BRIDGE, + Self::OP_MINTABLE_ERC721_FACTORY, + Self::PROXY_ADMIN, + Self::BASE_FEE_VAULT, + Self::L1_FEE_VAULT, + Self::SCHEMA_REGISTRY, + Self::EAS, + Self::BEACON_BLOCK_ROOT, + Self::OPERATOR_FEE_VAULT, + Self::CROSS_L2_INBOX, + Self::L2_TO_L2_XDM, + ]; + + /// The LegacyMessagePasser contract stores commitments to withdrawal transactions before the + /// Bedrock upgrade. + /// + pub const LEGACY_MESSAGE_PASSER: Address = + address!("0x4200000000000000000000000000000000000000"); + + /// The DeployerWhitelist was used to provide additional safety during initial phases of + /// Optimism. + /// + pub const DEPLOYER_WHITELIST: Address = address!("0x4200000000000000000000000000000000000002"); + + /// The LegacyERC20ETH predeploy represented all ether in the system before the Bedrock upgrade. + /// + pub const LEGACY_ERC20_ETH: Address = address!("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000"); + + /// The WETH9 predeploy address. + /// + pub const WETH9: Address = address!("0x4200000000000000000000000000000000000006"); + + /// Higher level API for sending cross domain messages. + /// + pub const L2_CROSS_DOMAIN_MESSENGER: Address = + address!("0x4200000000000000000000000000000000000007"); + + /// The L2 cross-domain messenger proxy address. + /// + pub const L2_STANDARD_BRIDGE: Address = address!("0x4200000000000000000000000000000000000010"); + + /// The sequencer fee vault proxy address. + /// + pub const SEQUENCER_FEE_VAULT: Address = address!("0x4200000000000000000000000000000000000011"); + + /// The Optimism mintable ERC20 factory proxy address. + /// + pub const OP_MINTABLE_ERC20_FACTORY: Address = + address!("0x4200000000000000000000000000000000000012"); + + /// Returns the last known L1 block number (legacy system). + /// + pub const L1_BLOCK_NUMBER: Address = address!("0x4200000000000000000000000000000000000013"); + + /// The gas price oracle proxy address. + /// + pub const GAS_PRICE_ORACLE: Address = address!("0x420000000000000000000000000000000000000F"); + + /// The governance token proxy address. + /// + pub const GOVERNANCE_TOKEN: Address = address!("0x4200000000000000000000000000000000000042"); + + /// The L1 block information proxy address. + /// + pub const L1_BLOCK_INFO: Address = address!("0x4200000000000000000000000000000000000015"); + + /// The L2 contract `L2ToL1MessagePasser`, stores commitments to withdrawal transactions. + /// + pub const L2_TO_L1_MESSAGE_PASSER: Address = + address!("0x4200000000000000000000000000000000000016"); + + /// The L2 ERC721 bridge proxy address. + /// + pub const L2_ERC721_BRIDGE: Address = address!("0x4200000000000000000000000000000000000014"); + + /// The Optimism mintable ERC721 proxy address. + /// + pub const OP_MINTABLE_ERC721_FACTORY: Address = + address!("0x4200000000000000000000000000000000000017"); + + /// The L2 proxy admin address. + /// + pub const PROXY_ADMIN: Address = address!("0x4200000000000000000000000000000000000018"); + + /// The base fee vault address. + /// + pub const BASE_FEE_VAULT: Address = address!("0x4200000000000000000000000000000000000019"); + + /// The L1 fee vault address. + /// + pub const L1_FEE_VAULT: Address = address!("0x420000000000000000000000000000000000001a"); + + /// The schema registry proxy address. + /// + pub const SCHEMA_REGISTRY: Address = address!("0x4200000000000000000000000000000000000020"); + + /// The EAS proxy address. + /// + pub const EAS: Address = address!("0x4200000000000000000000000000000000000021"); + + /// Provides access to L1 beacon block roots (EIP-4788). + /// + pub const BEACON_BLOCK_ROOT: Address = address!("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"); + + /// The Operator Fee Vault proxy address. + pub const OPERATOR_FEE_VAULT: Address = address!("0x420000000000000000000000000000000000001B"); + + /// The CrossL2Inbox proxy address. + pub const CROSS_L2_INBOX: Address = address!("0x4200000000000000000000000000000000000022"); + + /// The L2ToL2CrossDomainMessenger proxy address. + pub const L2_TO_L2_XDM: Address = address!("0x4200000000000000000000000000000000000023"); +} diff --git a/kona/crates/protocol/protocol/src/sync.rs b/kona/crates/protocol/protocol/src/sync.rs new file mode 100644 index 0000000000000..90d78bbd7c835 --- /dev/null +++ b/kona/crates/protocol/protocol/src/sync.rs @@ -0,0 +1,67 @@ +//! Common sync types + +use crate::{BlockInfo, L2BlockInfo}; + +/// The [`SyncStatus`][ss] of an Optimism Rollup Node. +/// +/// The sync status is a snapshot of the current state of the node's sync process. +/// Values may not be derived yet and are zeroed out if they are not yet derived. +/// +/// [ss]: https://github.com/ethereum-optimism/optimism/blob/develop/op-service/eth/sync_status.go#L5 +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +pub struct SyncStatus { + /// The current L1 block. + /// + /// This is the L1 block that the derivation process is last idled at. + /// This may not be fully derived into L2 data yet. + /// The safe L2 blocks were produced/included fully from the L1 chain up to _but excluding_ + /// this L1 block. If the node is synced, this matches the `head_l1`, minus the verifier + /// confirmation distance. + pub current_l1: BlockInfo, + /// The current L1 finalized block. + /// + /// This is a legacy sync-status attribute. This is deprecated. + /// A previous version of the L1 finalization-signal was updated only after the block was + /// retrieved by number. This attribute just matches `finalized_l1` now. + pub current_l1_finalized: BlockInfo, + /// The L1 head block ref. + /// + /// The head is not guaranteed to build on the other L1 sync status fields, + /// as the node may be in progress of resetting to adapt to a L1 reorg. + pub head_l1: BlockInfo, + /// The L1 safe head block ref. + pub safe_l1: BlockInfo, + /// The finalized L1 block ref. + pub finalized_l1: BlockInfo, + /// The unsafe L2 block ref. + /// + /// This is the absolute tip of the L2 chain, pointing to block data that has not been + /// submitted to L1 yet. The sequencer is building this, and verifiers may also be ahead of + /// the safe L2 block if they sync blocks via p2p or other offchain sources. + /// This is considered to only be local-unsafe post-interop, see `cross_unsafe_l2` for cross-L2 + /// guarantees. + pub unsafe_l2: L2BlockInfo, + /// The safe L2 block ref. + /// + /// This points to the L2 block that was derived from the L1 chain. + /// This point may still reorg if the L1 chain reorgs. + /// This is considered to be cross-safe post-interop, see `local_safe_l2` to ignore cross-L2 + /// guarantees. + pub safe_l2: L2BlockInfo, + /// The finalized L2 block ref. + /// + /// This points to the L2 block that was derived fully from finalized L1 information, thus + /// irreversible. + pub finalized_l2: L2BlockInfo, + /// Cross unsafe L2 block ref. + /// + /// This is an unsafe L2 block, that has been verified to match cross-L2 dependencies. + /// Pre-interop every unsafe L2 block is also cross-unsafe. + pub cross_unsafe_l2: L2BlockInfo, + /// Local safe L2 block ref. + /// + /// This is an L2 block derived from L1, not yet verified to have valid cross-L2 dependencies. + pub local_safe_l2: L2BlockInfo, +} diff --git a/kona/crates/protocol/protocol/src/test_utils.rs b/kona/crates/protocol/protocol/src/test_utils.rs new file mode 100644 index 0000000000000..bf1347d40917b --- /dev/null +++ b/kona/crates/protocol/protocol/src/test_utils.rs @@ -0,0 +1,131 @@ +//! Test utilities for the protocol crate. + +use alloc::{boxed::Box, format, string::String, sync::Arc, vec::Vec}; +use alloy_primitives::hex; +use async_trait::async_trait; +use op_alloy_consensus::OpBlock; +use spin::Mutex; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::{Layer, layer::Context}; + +use crate::{ + BatchValidationProvider, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoIsthmus, + L2BlockInfo, +}; + +/// Raw encoded bedrock L1 block info transaction. +pub const RAW_BEDROCK_INFO_TX: [u8; L1BlockInfoBedrock::L1_INFO_TX_LEN] = hex!( + "015d8eb9000000000000000000000000000000000000000000000000000000000117c4eb0000000000000000000000000000000000000000000000000000000065280377000000000000000000000000000000000000000000000000000000026d05d953392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc00000000000000000000000000000000000000000000000000000000000000040000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f3298500000000000000000000000000000000000000000000000000000000000000bc00000000000000000000000000000000000000000000000000000000000a6fe0" +); + +/// Raw encoded ecotone L1 block info transaction. +pub const RAW_ECOTONE_INFO_TX: [u8; L1BlockInfoEcotone::L1_INFO_TX_LEN] = hex!( + "440a5e2000000558000c5fc5000000000000000500000000661c277300000000012bec20000000000000000000000000000000000000000000000000000000026e9f109900000000000000000000000000000000000000000000000000000000000000011c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add30000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985" +); + +/// Raw encoded isthmus L1 block info transaction. +pub const RAW_ISTHMUS_INFO_TX: [u8; L1BlockInfoIsthmus::L1_INFO_TX_LEN] = hex!( + "098999be00000558000c5fc5000000000000000500000000661c277300000000012bec20000000000000000000000000000000000000000000000000000000026e9f109900000000000000000000000000000000000000000000000000000000000000011c4c84c50740386c7dc081efddd644405f04cde73e30a2e381737acce9f5add30000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f329850000abcd000000000000dcba" +); + +/// An error for implementations of the [`BatchValidationProvider`] trait. +#[derive(Debug, thiserror::Error)] +pub enum TestBatchValidatorError { + /// The block was not found. + #[error("Block not found")] + BlockNotFound, + /// The L2 block was not found. + #[error("L2 Block not found")] + L2BlockNotFound, +} + +/// An [`TestBatchValidator`] implementation for testing. +#[derive(Default, Debug, Clone)] +pub struct TestBatchValidator { + /// Blocks + pub blocks: Vec, + /// Short circuit the block return to be the first block. + pub short_circuit: bool, + /// Blocks + pub op_blocks: Vec, +} + +impl TestBatchValidator { + /// Creates a new [`TestBatchValidator`] with the given origin and batches. + pub const fn new(blocks: Vec, op_blocks: Vec) -> Self { + Self { blocks, short_circuit: false, op_blocks } + } +} + +#[async_trait] +impl BatchValidationProvider for TestBatchValidator { + type Error = TestBatchValidatorError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + if self.short_circuit { + return self + .blocks + .first() + .copied() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound); + } + self.blocks + .iter() + .find(|b| b.block_info.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + self.op_blocks + .iter() + .find(|p| p.header.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::L2BlockNotFound) + } +} + +/// The storage for the collected traces. +#[derive(Debug, Default, Clone)] +pub struct TraceStorage(pub Arc>>); + +impl TraceStorage { + /// Returns the items in the storage that match the specified level. + pub fn get_by_level(&self, level: Level) -> Vec { + self.0 + .lock() + .iter() + .filter_map(|(l, message)| if *l == level { Some(message.clone()) } else { None }) + .collect() + } + + /// Returns if the storage is empty. + pub fn is_empty(&self) -> bool { + self.0.lock().is_empty() + } +} + +/// A subscriber layer that collects traces and their log levels. +#[derive(Debug, Default)] +pub struct CollectingLayer { + /// The storage for the collected traces. + pub storage: TraceStorage, +} + +impl CollectingLayer { + /// Creates a new collecting layer with the specified storage. + pub const fn new(storage: TraceStorage) -> Self { + Self { storage } + } +} + +impl Layer for CollectingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = *metadata.level(); + let message = format!("{event:?}"); + + let mut storage = self.storage.0.lock(); + storage.push((level, message)); + } +} diff --git a/kona/crates/protocol/protocol/src/utils.rs b/kona/crates/protocol/protocol/src/utils.rs new file mode 100644 index 0000000000000..74424651ca758 --- /dev/null +++ b/kona/crates/protocol/protocol/src/utils.rs @@ -0,0 +1,385 @@ +//! Utility methods used by protocol types. + +use alloc::vec::Vec; +use alloy_consensus::{Transaction, TxType, Typed2718}; +use alloy_primitives::B256; +use alloy_rlp::{Buf, Header}; +use kona_genesis::{RollupConfig, SystemConfig}; +use op_alloy_consensus::{OpBlock, decode_holocene_extra_data, decode_jovian_extra_data}; + +use crate::{ + L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoIsthmus, L1BlockInfoTx, + OpBlockConversionError, SpanBatchError, SpanDecodingError, info::L1BlockInfoJovian, +}; + +/// Converts the [`OpBlock`] to a partial [`SystemConfig`]. +pub fn to_system_config( + block: &OpBlock, + rollup_config: &RollupConfig, +) -> Result { + if block.header.number == rollup_config.genesis.l2.number { + if block.header.hash_slow() != rollup_config.genesis.l2.hash { + return Err(OpBlockConversionError::InvalidGenesisHash( + rollup_config.genesis.l2.hash, + block.header.hash_slow(), + )); + } + return rollup_config + .genesis + .system_config + .ok_or(OpBlockConversionError::MissingSystemConfigGenesis); + } + + if block.body.transactions.is_empty() { + return Err(OpBlockConversionError::EmptyTransactions(block.header.hash_slow())); + } + let Some(tx) = block.body.transactions[0].as_deposit() else { + return Err(OpBlockConversionError::InvalidTxType(block.body.transactions[0].ty())); + }; + + let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref())?; + let l1_fee_scalar = match l1_info { + L1BlockInfoTx::Bedrock(L1BlockInfoBedrock { l1_fee_scalar, .. }) => l1_fee_scalar, + L1BlockInfoTx::Ecotone(L1BlockInfoEcotone { + base_fee_scalar, + blob_base_fee_scalar, + .. + }) | + L1BlockInfoTx::Isthmus(L1BlockInfoIsthmus { + base_fee_scalar, + blob_base_fee_scalar, + .. + }) | + L1BlockInfoTx::Jovian(L1BlockInfoJovian { + base_fee_scalar, blob_base_fee_scalar, .. + }) => { + // Translate Ecotone values back into encoded scalar if needed. + // We do not know if it was derived from a v0 or v1 scalar, + // but v1 is fine, a 0 blob base fee has the same effect. + let mut buf = B256::ZERO; + buf[0] = 0x01; + buf[24..28].copy_from_slice(blob_base_fee_scalar.to_be_bytes().as_ref()); + buf[28..32].copy_from_slice(base_fee_scalar.to_be_bytes().as_ref()); + buf.into() + } + }; + + let mut cfg = SystemConfig { + batcher_address: l1_info.batcher_address(), + overhead: l1_info.l1_fee_overhead(), + scalar: l1_fee_scalar, + gas_limit: block.header.gas_limit, + ..Default::default() + }; + + // After holocene's activation, the EIP-1559 parameters are stored in the block header's nonce. + if rollup_config.is_jovian_active(block.header.timestamp) { + let (elasticity, denominator, min_base_fee) = + decode_jovian_extra_data(&block.header.extra_data)?; + cfg.eip1559_denominator = Some(denominator); + cfg.eip1559_elasticity = Some(elasticity); + cfg.min_base_fee = Some(min_base_fee); + } else if rollup_config.is_holocene_active(block.header.timestamp) { + let (elasticity, denominator) = decode_holocene_extra_data(&block.header.extra_data)?; + cfg.eip1559_denominator = Some(denominator); + cfg.eip1559_elasticity = Some(elasticity); + } + + if rollup_config.is_isthmus_active(block.header.timestamp) { + cfg.operator_fee_scalar = Some(l1_info.operator_fee_scalar()); + cfg.operator_fee_constant = Some(l1_info.operator_fee_constant()); + } + + if let Some(da_footprint) = l1_info.da_footprint() { + cfg.da_footprint_gas_scalar = Some(da_footprint); + } + + Ok(cfg) +} + +/// Reads transaction data from a reader. +pub fn read_tx_data(r: &mut &[u8]) -> Result<(Vec, TxType), SpanBatchError> { + let mut tx_data = Vec::new(); + let first_byte = + *r.first().ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let mut tx_type = 0; + if first_byte <= 0x7F { + // EIP-2718: Non-legacy tx, so write tx type + tx_type = first_byte; + tx_data.push(tx_type); + r.advance(1); + } + + // Read the RLP header with a different reader pointer. This prevents the initial pointer from + // being advanced in the case that what we read is invalid. + let rlp_header = Header::decode(&mut (**r).as_ref()) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + + let tx_payload = if rlp_header.list { + // Grab the raw RLP for the transaction data from `r`. It was unaffected since we copied it. + let payload_length_with_header = rlp_header.payload_length + rlp_header.length(); + let payload = r[0..payload_length_with_header].to_vec(); + r.advance(payload_length_with_header); + Ok(payload) + } else { + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)) + }?; + tx_data.extend_from_slice(&tx_payload); + + Ok(( + tx_data, + tx_type + .try_into() + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType))?, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{RAW_BEDROCK_INFO_TX, RAW_ECOTONE_INFO_TX, RAW_ISTHMUS_INFO_TX}; + use alloc::vec; + use alloy_eips::eip1898::BlockNumHash; + use alloy_primitives::{U256, address, bytes, uint}; + use kona_genesis::{ChainGenesis, HardForkConfig}; + + #[test] + fn test_to_system_config_invalid_genesis_hash() { + let block = OpBlock::default(); + let rollup_config = RollupConfig::default(); + let err = to_system_config(&block, &rollup_config).unwrap_err(); + assert_eq!( + err, + OpBlockConversionError::InvalidGenesisHash( + rollup_config.genesis.l2.hash, + block.header.hash_slow(), + ) + ); + } + + #[test] + fn test_to_system_config_missing_system_config_genesis() { + let block = OpBlock::default(); + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let err = to_system_config(&block, &rollup_config).unwrap_err(); + assert_eq!(err, OpBlockConversionError::MissingSystemConfigGenesis); + } + + #[test] + fn test_to_system_config_from_genesis() { + let block = OpBlock::default(); + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + system_config: Some(SystemConfig::default()), + ..Default::default() + }, + ..Default::default() + }; + let config = to_system_config(&block, &rollup_config).unwrap(); + assert_eq!(config, SystemConfig::default()); + } + + #[test] + fn test_to_system_config_empty_txs() { + let block = OpBlock { + header: alloy_consensus::Header { number: 1, ..Default::default() }, + ..Default::default() + }; + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let err = to_system_config(&block, &rollup_config).unwrap_err(); + assert_eq!(err, OpBlockConversionError::EmptyTransactions(block_hash)); + } + + #[test] + fn test_to_system_config_non_deposit() { + let block = OpBlock { + header: alloy_consensus::Header { number: 1, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: vec![op_alloy_consensus::OpTxEnvelope::Legacy( + alloy_consensus::Signed::new_unchecked( + alloy_consensus::TxLegacy { + chain_id: Some(1), + nonce: 1, + gas_price: 1, + gas_limit: 1, + to: alloy_primitives::TxKind::Create, + value: U256::ZERO, + input: alloy_primitives::Bytes::new(), + }, + alloy_primitives::Signature::new(U256::ZERO, U256::ZERO, false), + Default::default(), + ), + )], + ..Default::default() + }, + }; + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let err = to_system_config(&block, &rollup_config).unwrap_err(); + assert_eq!(err, OpBlockConversionError::InvalidTxType(0)); + } + + #[test] + fn test_constructs_bedrock_system_config() { + let block = OpBlock { + header: alloy_consensus::Header { number: 1, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit( + alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit { + input: alloy_primitives::Bytes::from(&RAW_BEDROCK_INFO_TX), + ..Default::default() + }), + )], + ..Default::default() + }, + }; + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let config = to_system_config(&block, &rollup_config).unwrap(); + let expected = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + overhead: uint!(188_U256), + scalar: uint!(684000_U256), + gas_limit: 0, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }; + assert_eq!(config, expected); + } + + #[test] + fn test_constructs_ecotone_system_config() { + let block = OpBlock { + header: alloy_consensus::Header { + number: 1, + // Holocene EIP1559 parameters stored in the extra data. + extra_data: bytes!("000000beef0000babe"), + ..Default::default() + }, + body: alloy_consensus::BlockBody { + transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit( + alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit { + input: alloy_primitives::Bytes::from(&RAW_ECOTONE_INFO_TX), + ..Default::default() + }), + )], + ..Default::default() + }, + }; + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() }, + ..Default::default() + }; + assert!(rollup_config.is_holocene_active(block.header.timestamp)); + let config = to_system_config(&block, &rollup_config).unwrap(); + let expected = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + overhead: U256::ZERO, + scalar: uint!( + 452312848583266388373324160190187140051835877600158453279134670530344387928_U256 + ), + gas_limit: 0, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: Some(0xbeef), + eip1559_elasticity: Some(0xbabe), + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }; + assert_eq!(config, expected); + } + + #[test] + fn test_constructs_isthmus_system_config() { + let block = OpBlock { + header: alloy_consensus::Header { + number: 1, + // Holocene EIP1559 parameters stored in the extra data. + extra_data: bytes!("000000beef0000babe"), + ..Default::default() + }, + body: alloy_consensus::BlockBody { + transactions: vec![op_alloy_consensus::OpTxEnvelope::Deposit( + alloy_primitives::Sealed::new(op_alloy_consensus::TxDeposit { + input: alloy_primitives::Bytes::from(&RAW_ISTHMUS_INFO_TX), + ..Default::default() + }), + )], + ..Default::default() + }, + }; + let block_hash = block.header.hash_slow(); + let rollup_config = RollupConfig { + genesis: ChainGenesis { + l2: BlockNumHash { hash: block_hash, ..Default::default() }, + ..Default::default() + }, + hardforks: HardForkConfig { + holocene_time: Some(0), + isthmus_time: Some(0), + ..Default::default() + }, + ..Default::default() + }; + assert!(rollup_config.is_holocene_active(block.header.timestamp)); + let config = to_system_config(&block, &rollup_config).unwrap(); + let expected = SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + overhead: U256::ZERO, + scalar: uint!( + 452312848583266388373324160190187140051835877600158453279134670530344387928_U256 + ), + gas_limit: 0, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: Some(0xbeef), + eip1559_elasticity: Some(0xbabe), + operator_fee_scalar: Some(0xabcd), + operator_fee_constant: Some(0xdcba), + min_base_fee: None, + da_footprint_gas_scalar: None, + }; + assert_eq!(config, expected); + } +} diff --git a/kona/crates/protocol/protocol/testdata/batch.hex b/kona/crates/protocol/protocol/testdata/batch.hex new file mode 100644 index 0000000000000..f85db4153e011 --- /dev/null +++ b/kona/crates/protocol/protocol/testdata/batch.hex @@ -0,0 +1 @@ +78daccccc37628800100d1d8b66ddb4e5e6cdbb66ddbb66ddbb66ddb664fffa18bce6a56b71335a009b035b509be326d12b213fb6c6e578ce41c29be52f3a83fd009e7589c586a7d33a4afea79b3b4e83179690577837c746a2a0e50df1f82201482201080f0bf034010088102050505030503070906070507030b010b030501090b838c00010f030f0d8d008c000d060e0b0d8b880086050d03068a0c018e040f0f0d09040d8d0e0d0f0d0b8186040f010f090d0749090d0d0e094901010a8a86020e0981864a0305024b0f0f0d818905028f000b058e0405810203060e0605060f820406010604010101090c03050a098e028588080901010d0df0ff1ad0ffc0206b666f4f70aabce3d704a6aa2d483fa9f6dc97c1c875839635aad64da8d13799402f315c4638f4aa668f70e5be0dabe414555e0a26397aa6677cbdebb870c547d15b7057f208fd134c2792829f46cfca730ff61ae98c9ce1e6f3b55a78d3ff672f7d547a9c770702465b9459df0f43712ebd0927cbd859352c8d51a416ddabc504fbb2d20be72283d12930fda6f075a496f1269ab70967638f0be1cd861b9688c458f62bf777c5eba910183dac2ca8ba0ddceea022370b5348a029dbd5eab85c6f60cf78d048eb1d92a091ceeade1085f58be262208a7a0d69ffec511845040bd173c37eb1574ed46145a54da9d4b8d23e050e4017062badfc43f4d8cdfd550499c2086cbd1451710e1e7dd785b0f83b4cfeb1654fd004e1be30528f213099a653bd82f506fe3297f82a482549d8681cd1d540dca50af874563b64604d52d3e3ee65c30d94f22605ad62c0451bf478e8601d66b1ec7bf6e32928544a1ea8d6bcf94fdc544cd877aec6f8048d202f257af9743fb011e65230f981cb8b0295448023e3f720d58535e24a3c5c0a4b10adc0ddc1fd8e4ab4fa2b4036676df50424c3a8415a030f83325a5d0c2ebd705b5b08dadd9f3380d5ce28faa8fcccd9f684f5fba05694c316b7abd3d86a4ddbdf1d05281c131068cd8e600032f4a838dafa296c318f927db33d59c2d691b4bcf78b9f9cf63283377d2b2446268ac50bf8b1c983cf203a20329dc82921d7596f58ac2cf68d9e338d33efef3e81e5195a80070a4eb8066a1c91f18274b7550fce3ae60d2c16bac423c1d35baadbce6159f057daffbe139f25e8805e580e427acb96b351c2ca7319149c86c39d8a1594dc710400de6917b38e6cf8c74f026807d2d649ad6301aaeb5fdc31b602a1397d8b6c6866a67bee25857fa3eed5db61aa9db9e1fcfd541bda389a3e0a0f6871385158c4a05fbc8bb1d10f0663af5742205a33cbb748df336b86fcc3c6e46a79f58c847e1a7ffa18bad31a8431b3933dbddc621a33176f5ff9a6f79fd839cbdaaef7c0b374bba4e392e04ce1ee6a65b3d338cca70184db6ff2ce67cb9e7c4be1ec3faac58a1713ecdc95c872bb9e3d85fa1033878ad2a3a7a9b2d583fd808447e6520c2e93223fb332f5a0c5e3738de7dfe160a21dfe781bb8bb05e94f6b9c02a3119a12b22c5915eecf893ac9fa2e04e368dd4aa113c1137123af46f6d7a4fd50f7394c667f95284b4856d686411bfddee26654ef08e00afbb74921ba65b2daa952719a76cd3492ea485c4a689732dea23ae14c6a4dc69233653a07fe1ad802c0da96a1a14cb0c0c6ec6fabfc7e9aba1636a74d0a865257c6565efe2f684441ce7627406f403b3a72c1f40bbcf80dee28113d0b41a9b48ca14ffa14670e050db153061aa5eb5484059201c41131384ab8d2fd9bc1e75f713e3edf736c415c20a9783af8b2892f2bb3f22e0e5c131b71e3ac8114a3fdf7b3f9fe3d2cd2614c6bb8af67f7639bfed2d6d4800a4f2019bb5f06bda939a832aa08ff50c1633da7487e4e37f2523bce44689ea6fae38ac4e4ae0c55a0ebe8fcb6379b833fe326d23ff511e0d787314f30beca2cb67b75761977d02fcfdfb78c707f3cb485cf2ca534800832c0403e57634b93855c43a1368348df35a3b7e61651a688f49f9ddd44c56ca2902df236050c94298f522deee5662b1a5e64a16c6621d5bb7145c0dde4b7167e97289db803065df321918cce87a3140cd03ce591d37ef11de2d06a86038250444537b10a749fc1c467aa79702b16ce477b1f09a395571155c5da1666fc600958d1d506b47ff36d30622415ec4e9b3247de33ce1a8b5251a1454cdeb70c5f2aa9fad1d0fb39fc63e2eaba294359c52656b171213b73e04a604e4188ebdf6692cd54ba391befc2a0d9fadb96a47fb22af1e2229e7209dc8c3d0a5201e4096621940500c50b47963c1ad08e838345feb1b4972a9bdf7d7319f1671f8256db7a2e513a37ac3c71e4871b3fa384768aab21e84b43d8d732a14cc54a56ac640410aa7177006055982811ba68d5d12e466563916b6f387cd64fd842279550d793bd464144b9e8aadbd625ba011b3b238a022cce16cfb66a17e0ccab5eeb9377320e35672cab1accd0c179a16ea1f7c8de3f5b345516b3711d53a566955f4768d17fb779b82bb923d9360c10105ea21ce1c6cfe10a6ac5be194412b4fe6fe3cfd4276b5770f60516b0beb568ca42ceda51d722251717698b2e6d91c32256dbf77299240492a8546b3601bd88b7537230df8662773e58f83783693dae50aaeae9db47701a8f9aa53f5272d6b80687993c6223bd0d2f2bbed43889145e09f586ed565a5c97c63df19fa9f7e3991db55a019048a2fc1415b82f1833c3cf86f2cdcbca05251ffb23592f0d2afc2d173625571916f20495d7202ac0e46cabb3c3926bd09c7d10b0707c59d810a8d9139ae1adb4703f9c514856a195bcbd018f1e85bf384f5e5979b18eddb7c60c8afa56d7baf937be1fa2287f17c9cfcad4f18e8e2f4dcb8b5480bb3d1ef037a8d9aa7099b05b1def88393611ea16f7f66edadff20f6116ddc766046b694751ce7e54fe1befa489f36373705183cb51ae70e3f26485c0b07379aacde0776be541504bc7099426456481f9c382cfa8dc48d2a2bf5f58bbaedadb5fe63fea92ededdcf7d7cbcef141ec4f132e01db6e1becde49546c44defa1a8b92e41e4dc94e4cfe342bf831ba7e4d865dcd7be0f5796cef9bfee744509ebf1aa7d35cba24ed1961f73c6193ec17fb48bef5ee6cb99a662687e8415c95c6a18608d946fd0722572c6005dc95f9b2038e689e7eb5d4293b22718254acf6a5957ca699a93392329d735aa2f8046c22fc5f4e87a88fb85b17af16ac9e94f9b3d1eb18a0bc0f5aba0a5ec4dddd684db339963ab8981ef2722d9e8f2dfc946d12c809d766e9e08d7b480d2adeede777dfb7f82d81c973b9248ba484c260abb101e52ebcbac00a97560944ac47ef3084e19077a95df47ac688421feb3e57ecd10dd665f9e581f22742d5d5e6b3e963ac3fe1c8260ba815d1d7fab916b83193fabe9a7598ca1d932786e9612a76aa6ccf3e3407c4584d737dc7d8b8f3e182747979c6c60cf0447f523451447091ebe9dfb078b0da7ef3b5c63d685a7bde2cef89aa14b342e637ccfbb484ecc817d7a42353f36e320a0999a0a60270a969f3bf041e15f47e6a4593a99fd9fe030b17b25fbdcb9951ddc5b7050b5a760a1c7fc5c985f39720e8e8d54e050c145b890207e67385539d41203ecf4938dda7b5eac1248297b9b3946738d399c2f6513c253547fc33978147d0e1f07a827bf7c45a564450b81920a5d50a739df98e1ff30be68b6ccb0aa2e23d8faa400c1f4fd8ddefbfd85a7a0337cbdcef927af509e4f5441e266457a245d904cbe9e3c79da3979ca6191cffa00053db64387f953133c76be9bf12d3eb4884ff69dad1c78d9f4661894c2916171f499d17bee5dc281fb12e0b2c01ed7cd760831f84dc0ea378f6b992284a6d7027998a35434a93cfffdb8742961cd5568e89fb9d94cbb313f00808cbd62801bcdd462a389ba53dc0782ec26eda4de14ef7cdc87e2f972191398839933e9a96e651b650828d719a1410ac162e047717f742ba68df5a431cc9b6922e11b7e6c95fdaf69aea51a8ba9a2ee2c742683043aa8b3c20cb478d1c2046ee3ebb5276fa95b934fd892895d3236b531ee05e61abcc215cb406e41c473e9652b942e3971efb3b28add1283ebb9f18c4794ccca24709b374219de3c8a2f1b16bfe9706567da301ae6282e4b31767b5dbed7633bdaea0ca759b9160bc0985b56544b817f65e46ac11f13d23be60a0d8adf1a4c89279e17ac1890f85b03dfb656c284330925326a9418ccd0dfd30b7d07edbac16edc024bd9564994d6338cb91c1ef59fff372f48550109a3805e727895b51b73a26a45fadc22bd13723be328794aee2c0824b47b2c498799bd780cbaca08cedb2c0a36149af9a041037dfedc43a42d440adcd6646dda109d8da645c7e99158d52c72c918f0acadf3bd27c88cef00255f02b40282caeb9ece18845d8de4925d7511478f3415bd6bae3d00d5da130aa06a065de678b24a6ed251d1653e63d6d339e9e18f614b3c9af92da3a647e774b1a379f8e6577caf7cd7f2e1ea196bebe27087c0a2e1a3bf2b65902b8ed988e254740e5c4cfe9f400a790d0a1225adfe6bb4293e626677e47bd9ca96738ad08e5ff0cdf632b92fdfc5ecb443d0059734783d11256ec7a88bc0fd4b156608a0c1237c3b0515e2d9a789f3a62eb76e4f7651e6c4baef378a552dc4608626d62be599e4bc3762c2fe75bc1d0e264dc165404a3e0ffee07ca219df332e3c883f2e69b453c1e4ed845b3a299751a68a9491aea4daba5a7a7d74d136ee2060fdeeb371f28cb16dd26b200b5bb01d9466102b59773a8f68b19f08ff8c5ebe64467ed44d0776b91870c6ae42b9f315c0cab197a798601045a5877eaab71cd7c4b7b97c5d8b7670e3b9244ca0ac17827efd512a911a2893ab20db8da9b1bf2bbb27bba9f1458754788cab1467c4c6772ee5369db454571afcc0989e9654fbcf6d5258452a365e4a144d9a6908831c107b4d26ac46e58c0b9147266d9212b51dee450b517c2885041bdbe5de6410f3f034fe42fa79a57b02fccbc02c8ab8f22b43f67869cf7548af5a4445d432cef85e1718c5ca8a25d1fcb89425a96a78a8eccc2138a1c0debc10d5f3a38b4f4ac7653e2974254c192232e65936715c81c93cf162c06b3aef739d08189bf7a9d9c945f820edbdf51bbc476ba592c901e1afceeab061bab4fd3fe8be4c1f675ca3f8568dcfa8bae95d8fd4a1882c0d62cd84efbe467b75f6e716d8cbb104283c68a6445545e94d6f940a09d7bd109322126fdcbee0ab36bd7750ca3716f6e6f91944c44eeba11c168e333e0a2f4ae879cb0e3566016f62ca87ff51edb1496664ed24653a8cce41b937461d7dd6eeb607f9d4ebea6a047a85b25878914670580e7183d4a679af0957394b1a4113f7747471321512d45c6ab70b4cea77eca40ec1b8cb5e243b5215c74aaea281a0b65a7556fec3ec166ac256c2e8e037f9dd4123b6e1d53495d6f89be1428e9db8842c2dff77be3bb8bb1001283400ff8807d954ad7ff5478046c462e9da1770d45eb961ed89dcb79f7949b2105186063d6826018a53cbcc1a785f1369e5e58791cae3aabef809162af801be8d70d37f650b5c17dd80e333c1f028e47ef910905bb060d31bf40c2991faaaf405eda5ab9e1a294b6a6ab50aca41696bc1f68a6f0ebb289da445446796017a22fb36fff555231d4a7ee1fc36a77623f3c968dea5d8eb07b620155dbb86f5739b52adeedc4f8c58215da11f3314ec6b4a221b5037eb0d7ea4a48596de5220969647396c64483965db2dada1998ced063d7d8e19961ddfb931af3de0db4633cfdc47aaf2879b6d1c8083ce10f18c7e6e0b940ad6ae7b8511d74904d6f23de9e39df323f17deb28e812b4bb699cdfbf668f22850e61ca619cb92ab46d2c9b2ba175d0c1f8689c97263af8d106c9f04e63ff1b771b15e5c5ecd6e30ab2e8ce9828624ca7b3f8110051a6be30975e7cb9204141309b2e2717871425b0291e0ed831985c2275974f803bdfc0ad6d26a184d97c86f7f7f766fbb47c2227d1fe3a93baf0de6bd44ad10b955258d1abb4db42aac317dbb810af5059802dfafe36ef6de7722969414b31d2410ebc886bf1ac30c9101618a33b129a95f923002eed81ed1f89b01e262196085d39046b4eaf7788b994807a143d1303627258d7414f921e3186b2d20393b921b2b9b83fdf180cae05891b6218e4251fde64b95fb74ab02b876284d0cfc7a30e8dd2e5fbbe1f47f8f07e0143f28fa1923c1a1fa375fb117c1df3c3dd49e80c845ae0d1f43d18b04089611264cb4fd751e4353ce0d11936d3872ce66719dba52ce447f84242227ff92afbad13bf43de75416f28481c85e756a0efc25ed21f6ff953fdbd7e553c1561f9d7739756b6025f9c2873a002213647fb95a14cf6e20da527f30a2e2aebca1ac5e3c2e02f2b33fd30f44ad203adb6de3cde8a5361da0ebea29cf9552e6fd730f967be9a9c290653d60b5bb20ec089a71e619e28f2ff40fb60784aa57ada13d64e3b337d785905c3ead08052252d613c3a92e273050fae0dac87b8e67f732aad2c0ac98739095067d01ff5de75cdfca54c79c6fac00aa98a47eb77e0b5382448111ca6116157ee7c70f46c23f67af8be5a4a8e64bd3a6900d492cb5f7a4a7034c8f6240f960b59e4e07fa0c4081a234f96b6ff123b452b722da23b286cb208243f756d11b1ba461d132509b56c811b802a0f7a454eed6c8d2df007bd90cef29b419c4be4774440c4d30747fc6db08dbc0f6af8ad1cd43eaa42eaff72e5d8ed66db9a340e17e8700b1f5fe3cd24aa8e6dd71fa5620abf2f7901db60a1faa41ca062e38476e931316cc9b2f14f49c5dcdc8b172b74358332bf626ba15592d2d5ec9758b67ee555494484425259092e2839bf1a6fa9516c23a8cf27319725b12c8d6327306bb0cf58505f11cdedaa340e7e84f3e850ac8203132735cffb98469cd6f062ac043532b3358f4fcd9e6675b9e8bff0c75fe920444dab8a837224bc0bab60cfff6c3365bf94e94122e3ffb499e0fb98fe916acef0537b6e2d38aa8ddb4a239b20d613f3a5af211f06daaf52e03bce83a794a80eb24f4ec84ec297206d5733660d31661063e0a57fa85a219d0052af5e5f5052c1662f2a5afb9205fd124f61ff65af2bc51db9137fd27d51edd13b56fee03d7fd5246341739edd99a1a29fbb81161942a92bde1602de938b540be6ca7cd304d5e8faa0e301cb629691e9e53ce21df368078abe4d23eaee9d72cad3e293225b03a3a8d6801b8257ea4e84cde957121428c9542f2ee6e9bb2deb7b399413d2f1d7eaa0c2f5b39bfc2641d2e89445e071ff476f1522561b8757ab6c14247e1391d616c15a267707f3ee6af984e1d7d73b2e48d40ba5f98ecadf727b285c259e14975128be5144c4fada5cb3f708ab18a5deb4ba927b361b26bb262d68dcf6e31287bf5439a4d6941a6292bff06b5abe03a0dbd003199784a6f98cf51cff275dee5e4422324167318f97a257ee067da43bbab834ef581b4cb181840ea8087120cc1ca552f5b1d8c4e2ac74f3a1c8f8ba4e1a46553d4bea4fbf1ed0c4a6af397493b6b0cc28df9d9d3167cbdd353b1abe73bb1bf15e5a38c4bb0780eceefce19bc9354c9b5ba1470cc6944b9f57899099c362086c6b6657eede017398e8affeb621c0084b94089e54c7358ed25eab59254bafd0c94bcbded90ae8701b447d073a4fbb3fe14981eb491fc336df76548f823370be197c8189830098a9822bfc549811f9bd3df25bba72b3a47c1d3abb26dec8ec3d51bf367810ae5db4587bbd6b823b3264ca37c07ffea4887cd711a4a4708677c9bb5c599d25810eaed811001a048680b2477c7414e338f6cef07e4623cc4b1863002b030491d8bbef0652221b74bc710b1adad1420aa205f1e914c22c4d93782768f75b4b344c73f1630afb96930d10177c9aba48ed15d7773b778f1c5e66a89f461c4a7fd2b369647023889adca0e7dc3e258da771244f1c80e11949572fcfd4bbe5135dd53be5f33cc5678d85b04f4b430a83ed9a19df58e011f28ac13d88438a94498f6dcce8b826cfea7316f0ad72611c91b196c19f8466c54952562a94654e4fa1e42f6b012aad2392d021556e1e152eed3c32ca2caae3b14371e0c80ac57b492306924c0395abac18ff9e582df5c2c43e3cfecd47aff68381cc68445ea40282186bbaedf57898790414b3e1b2bbd60499a03b01d2e6e92a841b8fb714a4701558d6f74c9387f4ebbd3e702691e6ab3dec806e86ee1dbb9a85e144b2b7d4bbf983ec43cc23a7db62bef9235900244fdaa6b7e04b1bbc23a1730fd115b9fdf347ec98be9e5d43d267e770447e7ad41a03ddfd549060a0082be8460a2594a93c72f5375f5d37eff20a00114b5dd0692dd2cf50f22620c0b3e90724fd0188352695ea9402568526f41ce07c44776274a5030eb613fa06742a632924b101c92b81affea93c3699f64316229bffdd12548b5d547aaac711825ec2a5d89da7fad90df3ce5797767795cdccf1d77fd7321edd8679f64a66821e8eb4160f741e57bd4533d6a8f8dc29ef80cf1b3293ea28275b6980b1c3e9eed6728d618a6541288dc775f2f3a8cdc95f7b114ea4ac2359eb4613c476baa78930cec04817a5a71291ff4ae930c8cc0cccf361c4b205210e9b923850dea121eecea5e33c0f4eafdb1984e9e9dea0c33116a118bbce9848cb1137c86a1af59feddad95d488ecd3dd8a007a427e36dcade05f4d4817d575d553ac258a6777d5ced2a1bafb3b1664d30df034320d0dcb4339610fdd9f2ffcf9e84e30f37439a5b407cdc787ef4bb50f74924d47da1864c425474ce602aae2a636e2219637519a6709ccde1a7d1397c9800ec51332902b2dee287c7aef88c707994b0032b02b03b6b0355471cf4b4d9dbcda16c81afbfe8f669dd4e2d72eb245de01ef9ba00bceba5ab8f4f5457faa1fd96778c47bba423e30dafd29212c8b5fcbd7315c8c02ca9a328267503fd280ec73b5aa08b6ed4922c44c59ab13fb73a246cdfa3b5ee4231c1e3ba6ebb27fd351b2abf940ed37dc4b6d907910cde3ee6211e1653b901ac0d15178643cef087e8bb905915566d303f3b72d657399f182791721d787afdc19ba6ee5360d6b3decb12930eb7be059230e6a89da5209acca12c1872cfb92ff5347faa3247eb2f1f1c3b3731734cda7832d60798c0b2b627f7c50098725f591fb774fe5ca15654ba57e56d8eccabd9868ef2f97a9c050c5cd0a5c64cfcf19797a6c08bce34a23b3c3a5a2615e7e8528d91b580b2dc7720973563f0fa92877e9b01517f3a55f1e652dff34d9f7a2b86da91d8e826c3248f9c9d1b4ec4cdcf224d021c75c70599bfc9ffc4fb73afe8806342f51496a194d82abd835f1ad22b63af176a9a3637a6ff7e78eecbebd624b07005cf2aaa652b9d26530217b20a911b00d1112f7653e9fe07a76dcacc9a1ff3ba282af321692c34024327682c196e7bcaa78d6519ddaefccd11ae96a003a998d8f72eac1fdf1e0358174f354ecdd5264a789d2b806b35f492887cf50032f881a2de4c547dd1de685855eca7d63140412bc3a052196d3519db3142f192d6cf2092bf0d3132ccf5ad0302d937262a15fce7c9b3010dd26a904e1a102c56749f034c98d9ae033d31568f5443ba15a3727a48e11b7a029ac0075ee93e828506c4e79a718bc96583000b81e53c39d5bf6aa80f88a6a1db34ff17ca6f4d3a4269e690313ae72e51060b55aaac8e6f98aa66dd20be79138ab440f65501d0253bdb44338767105bb21c3433d0c63ee17eaa23e0cbf8dc608fbd3c009fc2fca8b947eb5447e923f4fec4a851331aeee719c07dd1cb44abe86e5bbd3876a42f72affeb14e03b95d57a66e6bf1d6c053bdf83444945c45cbe3e8566effadaa16ba6490b604bf1ad7d2d945302aa58b5bf508628b3ade35710b580c231d34dff08a2b866669f297f35db249f19edd2094e66966438f82b56db24acb6b41e3b2e688a1cbe99531bfe97568144bac1f542cba7fb1f82aeca705f94932895426b8f0c33e945263764d5d3c6d4487ce0d7e8fb285f74c93464f74806823f0e164254d4563d9fc5401dd93808116da1d634e4a63663630112ddbc635936f717bf15d7e17aac45482877fbcdf6aa3314402331b967a0c9ebc12bc3383980d4665e7344ae1055f713e1b9d4892564ef11e79e33e687e04977dce4e76c6f4a44a3c22be43ae7c065f43eb268be8f5cec3b83b90f4cbbd1f2cd6debba81654c7f2b4ebfb9fc44c5e2a07ef47fb63d3d93eaa198340991e211405a62c881dcd057d388935eaa30c22fbf709fc2d343057d06187d4795d46092d9bc0c54b29ae50ec95d3ad693dc09c24832c79355b4acce1db4d2a4336f733daeea4427d02d5b1a44f01d335bc821e012edddbcd67ac90a9854c347eb8fc663214abeb8e16a62cd7f25a1842dfcb2eeade1471b57fc99f24991c6b3a71ff07db2304e2988497960360fb9383cb175a099bb460ec3e726da918159bd782ff2b738d0ff683443007c013576ff72d3178bd20e3dcd803780d083191c436c31758d7c0fe26c075cdebcb9aa17d230b58af73f027cc265b91a6f7a41ec3ce3e74c410f72247d328c7eb45611e5858791f5036bf5b0ef3ec1bf0a87c58bcaf0363d7454bf6846c628d58c8a5a91f510d9612d7992e3950c5ed5fb0f86a25986fc47a44deff4dee240ffb58ec24b5539081a42c5456cebf2d5d26fa70c92bc4ad0a9d612326e7e688461159dce3a459131e0b75476a4bbdbd5ec5ed7c8f86d2fa646e0d5bb4eec164ac9c5fb50c5f072a08dfb8360a62a1e7bfb49309ab254647e294d585848265db8f0b861df84c2bc7751fee9da72035d89b6201f1c87a380fb7d9fadc0b21989c77cd4d330f9c3dc5017e0f26d48d8c4a8237a2f974c308d88545511d2855674a450cd646fe22ad5d12e73e4e2914e04eecd0c811c94dd09c3e7e6438d4b877f69b1dc96a39d5562787cd811613c93bdc4c8ae1018915eee29425c5b0ca2fb0e302bf1ae0c8f764f1b8f8a5f23de31971bb2feaebfd775640f843608ebcf1ac56466f15fcf244e8ad6de231e389206b955ee34008b18eacafd02a4d835debd4a94e8d45ba9b7eca33b35efd15df4aa695f60ac83906672deadcd631e850764c42dce97df15deddd7871562fedaef437a0262525b6ff3d26dfa4aa5a2a04c6a0b3337d47c09f5058854808949def21ceec726173f92bd58bbc8e219d76bde1da3d6a1a07fa29ca255d60952c1c53aa80c7b10b487f2c9bfe5dd7fe6185c8d24ae776519042bb81b649b61971ee2c76427c80ea4a0b016ea216f97ac8d9d3ec31849fb9d0e8750fdb9233a9ebe6ff8219b62d481e0a3ef52cf26f1fd68a4f4778b158bcabcb1fb1281987a008a09c76161610ab6bf2db70af71264d15ecc011ae30890de59cbfc2ddcca8e5ee0e3572ad8e029127189b39c2d1cbddc51100c061c83485565acaeb88c324faf573a21c203b4bccae90ccfa63ce137d9be3f2546df731e7d67c464469158a0f0642b8ae26780d0f9a877ff31425202e32aba84f23632946da9e3e5c6293fb99c0da4c13245750b70face11d57c7b2e330fec472239322d791c1cdd36d49ae8ccc4a9dfc92598fb21fc8e58bf6209380251a5b402e85b5394af229c61d6fa8db58e600b8defdf23679d3b88485f32a5a2eabc38b0a67550650322e14f74e9d62523fc1e296ba882e9013ed3d0e38af4d0f25ed10c19da631ed36982cc626bff01c391352f4efb866ee92fccd32670ec925901463b276cea81140a8882f23b33f245800a53b56970eddaf2babbf5b1c444036bd154af8512cec990d1edace0987d8e05e62b0dd1d9a9eb3c1cb2cda14648fbf3b50df22293b89d076482b09f25d4750667f5a189fad0dcc544e6d558e4bdc9eae09107b203662bd204461fde38ed31a4fce82c00ea882334fcf20d13c95aed9fda162490008a5502189f1bd0299839783010be6d47d20549d08af080ba89f0ccfefa8a2c3cb0accafa2e7f0fe5bb80091cb4ed2642c1607a6b6badf4b68aa4367ee567a58339ebc670c495d38234060b170550688c05edc6b1a79eb158ddab776493620fafb1e2473f02031b9219e995096aaf31408ecadaa514c81ee8eb7e8c816fc69ece1670983e2cf14b132a53fa352382d8a37eff1bb4db89e6e9e63397c330c1dfd1545d2a3c9df38fd661ca02bc9c458ee488a72797ae1905ab6158288f2b0e7cdacf825eafdbfc010c8709871f2ccc838ab24d731bb6fd2493f49240656e927706aa1d683aa7aef4a2a839a4e75ac0268e4b3c409b3782c161929abaccb3a081f3ab744844e5a414a74571984f79e421bea5c864af9f0b8165f9714fd8e9d47918f8800a0aecb63f4cebb202db6e80547190d5908f690eaebd45996df2114c4df376dfe273a6d0ace7aee32b89d26ace94b07373aff745bbe46c0090e62dc9f7406a0d2efd22077ca2d49b99bcffc7ef91767507e09653f534d0506af8fa84172834466609b7a0a9361745397ebe935247795e249102ec22c84047c22c86277882b9585d29ee1e8de31e5afcfbe1fd2fd6e7b749d90745786b5c7246cb35db358a5577061f1d5177ef631b2fdf7a058f8f806f3ac7b52243534f426eefc4cdae7f9034c07aa1005e4363a67ec81cfc7507e5c3f061a23e8176891c2f9703f3f767e66829e366666ffc2715bfe0ba49fb139a1f90b403fdf54faf4783b8756ca0800ae643ef36dd754ed652e1dfc8b1bb4a7e4e5b67db780419ebd5b1e46fa57cd72720d9c00d1ffb05fb9dcc46cdab1e19114a7e587caadb587aa980e9f33e425fd71c0caa54e19451caeae2b140121318d234a8e68f946a6141e94d74294d658f3e431f31f1150c8f7030c027485c489975ae9b801933011756b264075bd92d78bb6edef95e9a0bb8595d8407e632d73f1794c5b445693a1ee8eaf0e5d0a5b5918870a4dc33b122f0fafdfc2313d10e897c91565849260990a7302c56ba2d5e3db8975a27fe113e720c32a4a588678302122104b6b4aeceac022e1ef0881af2e5c7d5546ad40c9b811c6c8ef48e9b5f7e855b3053381d72b2c547fdaeac466bfd078388ce34fe75627ff2cd5bde44d8741806dac673a7dd69f62dbce3acea26d23f753d45d1435db49897c1a5b5ee5e7433b5ae2f07dd061d366c28d9616482ec8532579b50330ce233d0b9a48e7c42753f796661556328079d9121396afc16cf1b8acc21ac4440df783e76ad9f2d0d45c75750f71e761f9f29478c8846d0b07ff88ec7d7acf6488e845099808c695241ad8b359e3305fbdfdf115a1520ecc8e56a90268c11f804700f0a81fc176ccd7dad0c243f3cb8d9ff1c98596621d20e35115c9a00c512185c3fbb613fcc7f8bdb5d0a711bd057959edbb36eaad75ad624d78e16e01b98652f4aeae848a3955ba3f15783ca7305e8e9268cf968d4818ee98e51948d574669695282e8757731e9a40a8adcb5f5c773733ecf2e1e15123ac1a3566eba6e2f028f59eab789af673d78ed9d21c8ed8aed475fb1400a59eb7053f7341d89a575a8c83ea73fb2c4ba74296f0dff4c7948c589830164cc9738997c03aaa28114ee4eab8dcd3f4be4f8f637ed4278c52b59c169bd82997a9582f0a9f0c4eb378acd65e5c9044be0a2c3e9a6edd2e7453861e1c6fc49f5d72e5e2eac21d8fd9d9854f5e89c0c5cdc7ee34e48e36f86741966a3809520a9c8ef548a446e94b2bc4d632ac044f9ccc3ca736dfbd6f2688eb5e5f1266c7337e86c36c06ce1de6f12261055fa4f2d7881750a5f492aef3261274c5b509048443e3b7ad93f9b95403794af9f0d3d479b3dfac39a4119a6b7e1d6a200ae9bbdfb0607e628e082c16671a525dfe715658bf9156ffcd3a21d20db778a489d738edd78c758d42e0d9d7a9ef5533851be73fe2ea06ad2e01a19d19ac4c4aad03e6d0bc94467cd1388bf7554a5bd284428af183fc499dc06efe3984f714fbdbdcb9a7de1941eecedc8a0d412e779e6da263216f33e8c32af11732d7bae8ab6b16083b1bb2d65e07d8e28f0114fa78a6b2492e663c056f4a5504b3f43bb0212316feee4c365f3ef92fa82f586c3478aa20ce6f33b6da502e6e3cd165254f7b1ba800b21e3721769c1a976c39a737f60fb0cb0893b7a587c2dfd03768e514d83d18cb6847911fc6fe7a7b824367ce9b6d1571faa02524fa452a1bf1bf5edb1d43494bcd9b077803732d99943a6d133f9bdad562e7d4e08bd20b4187e8b6df9c2456579dc11e08e5c1dd0e30fb258bbf8cdfcef8118ee6c8a0c098cc401149bec0e6280e3eaade5340c40c84fb7d434778d74d1b3740b5114391071d81e463267a0394e91f5eb2877a8547531e78eb9412eabe283eb482917c5936c620f14e7a11dd6e81f7f96db8412a86da562d9d219b480bb6d9b6dd8449a3ee59d619e19b9b1ac08c5c2f1492d7e773490d632ff0d87f92e058bad992e4f7d1fe7ccdabd14e87d5c1ec61a0d6211a9cc2e7fb688194c2811d0c998a1f25f219f97d35cb189083a781a2a9a36131cf90bc34474a9fc02631634e41c1dc78e0612b8f2be29f602ff6bcabef1ed98992c8c5ff386e425f01ca81155e143b8ab810c389b02448fbe416bb2c094e7258b1e63f09566316579f767313a1adf6a3c23243c168d9026ce6634df263f6bbda7168c9eb226e28c10531ac609ce5791e1ead613228210cfe168fdb20c3f77c10c2ab35805c67f88890ae75c78b8f7eecc3b0e34c78dd197d96072b6d28aaec461bb4d2edb95507d587716c2855f3658a1070ad0b0b59f86765ba3a303761dde58bc4b3749c2b8e1654465557aaab6196da513f56fe81322b4b5aa7d9a6f817db4f4b67443a4628a90799d4fe5ba1a8630b0be6c3e4095cbb8f090fe3742df1c2bec067458edb7a2dd068510936d9faf79d6935eccb7a0372461b9b4625ec3b61e7f6315b9495aeadfeec3aefe73bc62745f15258bd0d9d2e3d79808342002b0c7adce6ebf232f1b904a183215d8589e9a1b1b83dc1a437cbd479426097475dad1af38a9b8555ffc6e3201e659e245138c7dc52a4c04c30c5ecde34e716216f7b6947700f480e60b7f26bc57cca61907d635e702cbc7e9aae5c98fad67348b9adeaf9b0e9fd2d33e3fc61151b17887077e083ce6abb0b3e9272013e28c80cbdf27822e424b209b4640b79114f37ce710322076734e8adf5698a20afd3c0c0f7182ca275478e62e06067d980267bf2966301dfed7df7ccf77d2378f41ed3079e67e463488dec7403b20ce3818387c0aca035e82bfbbb8ce59b43394b42696be64ba260c3ede13eee049a929ba52157f3ef7e46e79d647f0478e042daf54f07923dc0ea34d56ea52faea9f25d6265b65b634b0638b999cd8bf175459532a93ac90e287739bd5973bbb0fcd6682e35e05c6335ddb40488e0bef72ff4a25290013683647d9124e92680209eb809e5b9413cd7c0c88c2204dcfaba8fea2200269be73f60b4b40f9a83a8635ecdd8c59ada7a0b44afcc69a2b552b9eaf5d62348118d3c055aa43bd2e5a3bc74059be08ff3cbefc402f83056c1ac8b1a6ca86cd906f5f747c23f7252d5c6b97ceb81c40a3cb9982a99400f45e7e02ef414dcca0cb6778d896b9cee3ebaa9fc7baab14a8243a826ad6567107c839c48096f8a6eedec7435a22e5591bf09c5080e24ff7b062c7d3f00921311f3f60c10b98ff4c9ca54231e5d2b855bfaa705f994a34568c03dbf8671700842b90d480c5093a1afb122178926c1e6404cab7c742eda65dbbcd1e27eba9b838be0d6cddb284b0ca177196bf5f0c7c968dfef35471452e515b9d61fd38812907666e016f968346a1ac97b929c6d2066ae36dcf1e79682c8d41e06dcd6d39d014ce62db8b284de8c7f9a49864b3b07431a985afeb37ea819ffd9c5c61bf1982a04c589fe366570a799dd7e4eb7c061826c96558e0af64f261e324ea68ad96d255e452a18c7b3cd7cf62c85967ccc53e3d1d7152c0225176f9e72df31316f2fdc795da50b2830939d0827696c9c543b06da57010698c05a899b41291488b5df85092fbdc3766bebb03582631108c3b810eac082fb0526fa9f4fb95b4056169ea05c0115549d45a0dc9f3f6afa6a8db1cc71de92db2eb4caf46090e61f26e1806925675182cac767d91bd8417a71e9c62314a4e2760f6081692b9b8145cee2aaacc382c17c9d6e868fb4f2aa240900d35bc0b63756aa536b9bcd0cd17af625882bdddf03167d4f9410b9f93833c406089adf4eaad197ce39fe89429eaf94cf557d274cc5a1b557d3e0dddf52a8bf83f9b658fe2471e60341bdd4eac01a888306e2f6d0d5930a4089d33c27df4ce74d022a921bd805a3979590bb2e1077ea4458dc9665ad27a6bade2087911154cc68008de2454a3b0791e37e79f7a4fd606fd5d549c4015554174e317bb2eaeaf71317b79a362e7ed0474b9c7a8f6e8a5ced86c192539673231b45bca540fa2885dac47d2de81090fe0dfabd63991925394809f7a2a4fbac5f969ee05bc98b1173ffb9c24cdca6de02cd5ef81f0e178ae9ce0a62129f3ced470b64548267340d3c9558eabb6f73367cd40da658bd6f74a1cb0114f5de68f2076d2ca71a9af34c661e657eb09874a39b06a0ad52b4f59786a4b313d3a4a3e0b31c3bce4726845039df5b68250ba5e71852e675e9e681db7ef5582f7b2bef02672f7ac3ffecc5d8cdaeaf1c032b5d9c49b8b1fde74f902d5983d35361d6b11e3e0f8a71ad2bf0fe74400cee5c80bf4f6eed3bc016edffe81c9334247a30ea64712f84666c3556c576365e93d9cf9759428b19a0fd54656e137f6368e3d43cd73e641b74cc4dfd0e847cef3cbe3a5417e536927a469004593d17d0e3f603213e2e79cc52819cb68ace35fdff5ba4a78b0a3e2aff04b1f4b3003a64a3196723dfb338e1d0b7643c1869559116e667ec3d81722f4174de4e80be0b8b069c4a240cbdfecd334ccddc02242f150377db67b5f751404527528999a92d48a52735efb1173f3f64394d5d76e674ae0b1a8e59be7f8fe3a63b6bbef4e18f3aec3b6d458fd006032057d644536d2d354edde8abdd2414518b598bc69434eae3b3b6e0fbb129fe14551ab8b07d79177c33eb73ad4f41299bd5fd943883d4c5ad3c3bd033cf6aa49d571c4198d471646e426fe96033735ba6eb34752f4b15e2b788bcfb5605033f7567c9a71c67459c0e6210b0c4b9aa24614551d65885e2f2e8f0046af3a0561ca201bba2acc157ec42ca5d04d4890db110e026f03f80eda4ffe02702b27aabe3d86be415e9a3ce693a71cae3456b7a4f661edbda45528b613d81eee09c7c6be9ef05dc86d97b46346f5e60ad0f5e2bd0efc8258e23fd8ed4d2ec96f7a26adf95260057c7fdaf0d4f7a1520107a38de0faf21c249105a7cd960cf8ca01dbf3f775d212bf0cf199d5f9cd13bd406659cdf73a92befa37b5468786edbe99e33505eb5596e5671e99639d9d58a7c097080b844853f880d970c1f5d0bee1b6a69bfa10fb77e3961b76f2ea1fffbb2ac4011497896738f37ab544c43490991b6b01b5facb9c1dd98b23600700b507d9eb9163d864b6937ae2c684b31d63a53974ca6c4cb0258a7a78e9e37eee27275ab70f85b7c078b6abc7ae469e9e8be3fecb677d96e7c4f2d9bce80ea7c26bc60553792864b6a0e76adce25007ded8b8dd7a00d46ac05005803621d03a9a40c5fa1fa24b10179b499ca35c1d2d208cdb0a630a0b80637196a432095723a08610555fedf957f8f7b4b75bd524347b39ca0e4178bd56b855d245c18e515bf831634e70f0247950a93f00e23e1cfd0fe3e6053f8d66f8211db6391cd5a82858886702641c36552a4af0f818611cc370a6c366d8f857442775b392b569d34f607c34ae911bdccb5d9481ee066d892d9911ad4d8215d6ea7918f1016aa8443db09491a162cede43244e28a8628da06d6983233f225e01f9710ddc31862e6e5a3fa1bb09131eb0a0fdb278ead5d9cb440c18e2f80982c6d2fa468f89ae2069f54bfcb5f2d2cd01a2d94d169871516570deaea0b9df4b4c22fd636be654b8fd3dba214e2759e423b574aa5ed48e6cb050ca59fdc1354d0e3f79d4313d56a5328b6d4a2d9ddb4628471ebf2ef145e6840977c056c91665604b62186cddd527682e17eae406692c8ef4519eb126b82a22237a234ef14e0d5f8f5da99c2a2b53af64fc9fa02dbd1619695f5d5a1f5b601228191b7ee1cbb0975a47159113b822421a6679126720849287b329ce561295349b89cfc3368718ca32397646522b1525cf2a5e9f70032a0553d3757845943e3a34acb47f62cee33e7091fae4987795cfb6f1d116dd8dccc918d4bacc991440f62517f85c071b9d3510b92810858338ab86869be8ea775f660d67c6a0b4c2c6569a3858c7dcb1ba3156c7bf8e9908090bca993d42f9cef82553eb4f8e46eca98437dde8aabe991a441ba025046e120dfbc6659226c10d3978d75a1e5b55784bcc5ae90c2fe1c01b3ba69095a27132a46cfe3e67f133316638c206b093aca2cfbc360a3da261039e5ae781e1e709d2de483b786af7c613408add9b581bc013d8cb64283c373e9fd70891d532b6c6285a89a7c408c429b69d8fa6d35edf62caf44075b4954d64e8dc7690ec9cdff81e71f60cc0c10900d5fddde561b5ce0b2502199e0142877a0c061b4e7cc3fc7580e3ff3e5af66ea05233fcb27690f96c814e34c22a409db4768617b1dba32d174e265cd1ed8181221b6def58beff9e9c5a90c38de0a291eae52447e3a3af289a5d6b6dc6ed112f8c175ef48f6684ad912ce46ed45f531e6a05df0dd93e49cf6a2f34b087d60ac110ce54cedccf6c5464743ebe171c70b91ba01c16fec673a40856449cd27b51b9132505f1520ddd68c83c9931ea568dfc5c24cef168b25a831d59587f6d32838c79aec6686e86765f82aeb3e9797d18323423cfc73dd7bf61ba60d6556ebb43a6faa0c918951926838f759b0450854f3728615abb33ad6b918d3391ae2ca0a43a7583de27f4e3f1cc9cdff33e10d0adcb946c026970e2f93c2a860ee79e5edb15c3e2e328d3a99b9e149a06c52a9c32a62f2baabc8653e008ac8ce0dbd8a5b7ef48895aa15aa535a5117619e88e3f417b8aabe4720dc77be1e601aea888a79b09712f0e805435d04d7555158cfdd9e85f7fced75e9fee09d32a41c11a6305838568184ffad5cd99f613f78afa9a7ad5d795fb956dd4152ce819fd8dbd491b31de8e5a50a3c2e1a3c0153f21499e2b7dbc83856fe7243bf7587e8718b0ca4b6cc368de0c55c483e1cadf716c645b963ec12a9e0155072e0abcabef69334d93ad6c5dd8043b66ea962cfa027096fc9b0fc75c9d7432c3807f7a0319fb59d5a9dd13dc10fb05ef1b8b310806df4a9bc1b2f8cb1fc479a5bd9c7cf19d2dd11c37b5550cee5695799775d3f488e937f0cef975f6d5859e6f6a3a07358c25b2e86dc99f9ab9875ee4a37cfa89bc61eaffaa20f92a78491f9cc4b710708f6e88b9edef3ca98fe059832f9d6780d5153e1de2ad16badf8daf98fbf812a27af3f239dfcb994a1c1bd2e424d84f2025ddeb817440f02db0d6c9cd1fb68d7378ecc7fb6f3d2b61db9fbb03a103b852949407a9b6e4b334695ccb9f8c1598683454be2603ba9abe7f156c08b1b0758d932bfa6b132bfbced3e7dbfcd535d5759bedae9419019e9dae9a944301994ac1bbc288d8665c60f57675f0dd63884a789c7b726c4b5468501334fcb82f12a4cc2b9d5b6ca8f68d0fdd18ac613a804d3ce0c270d5a4c71e14ef7e954de63b9cd4bdce25d08fb512f52cddd2f31124978cc8bf9adda597091dbd4db6e2cb4139a801cc14a9d2e2872a2c9fe50cdc7450255dcb06f3d05075c05fad0d5d4c36905e1b14c31afb45e31df46dde71d883d4b7df093fd1fa4b475d16a8a90c624a96848b3f75ed354441302e37f63bbe1e694edc5780f88dd4bdd5293732f031f4898aa3c4c377737b69721f64dd5655c46434a58b6cc1112842b45112ef435fc8ea61629335b304d6b6c4b513defa577dec6319c475fa8e43fcacc8eca6a102f829715a9f3e898b34e733650872fd55934b9222085b668bef0d25c72a2b9675364633fc79c03a3cab84835dda18039928d5bcc2d32674d4e04e4641201823a010f16c03bc713c55e96aaee873be1363a236aec66fac57fbc34bd6a4aa17ed89ec393998c92494dbdaa06376a126a87363218ce5cdc124fb1bb7d8b43d495558965425c2a869a663f68bbf1abb4546fedcb72dc88b3ae23f24150fbb96320b3ee1c99abf3b24dd116eaec4de65a3f9887049b1b2d6417603082162d4e7a227e1283d853b4dfe912afb5431450561c7c44b35d50a60a484118cbabf1c9f71c0951999ec818ad9ab8038659f1befa8deb5691bb76f767c5419e1cd58be95fa1918a071f0a1625839b30c7cf90adf49eeebce130410bcd6b23bfabaa416cbe9b910d40bc545a117a61000479b001f42af87d40ac23c059824c8bce4fe358ab1ddc4f752db61ad0b726452d4b29b3f52f1dfb4f393a7c5432cf19f1ab8fa0e9128bacaa1d68cdea2ef71119ce0f9f81dc18abd8a22d543b6b514923dd7ea98f49e44de8daee537e5b278cd314a1248dc7d8367f9a455207a619a70256cf81dc81d65a2c72937c91547b0eec965fba5b8276f1f48b566c5057e7bf1b8bd0128ad84489c7c14c491fee96353a83e27208cdb3e9bd8c4ad16b127eb402b1ab3c9593c7eecbf4be89d957743602fc4bfb6aed9bfa283e86b8bdfc3e314d952ff02133c6027674c614e974b168bd0005ee2919812e56321c0df16bd35832e5a6cb452af34076f43b56bba2637c92436abed63e8029a487d079ae3cc9c97a26ddbc119a8b0c44f843651a6a7fb1e1e25074077baaa757265b61b3a5511676ccd62014eff14aa70745955166da0da68a7456b1f046c883384be1f680e5c8e6da99d56bdfb8fae23b6e26175eec20c7047d5c6e78ac6d2a10f3d20d9dd1e0fbd3089f7f45f7604538ce1d3fb7a2ff35dfc0420454aa06768deb361570bafd15456c9add98ccb0310dbb3c2abad3e3abb3c08014aa2df8a2867e9736f7817fb31f3f44dc0bf4d8e4a9ea0252189f82bac1ea213f4eaeb148a05328259c3bdb5e85fe31e551604445d578f155e037759c1dbeeab79481593419da8f811ce99c7ee02d9e5dbe5d6041d31d9f8aa1530d5e56576eb89bf4fb809f32803b41af6649ed5975aeb428b0469c4f039342e4d0e9a3f8b486bb5632d91af5ef7b2fe73d1bceee308de64487c6757e7ba3ad0df9d4caa27d49415ebe5c7615f7baa659140b5db80da5952b76e2d82214a2fd778fd672fc0ac9b2ef96e8047386e0543f1f1432b4e65fe868b691878b058f7b4dd1d6069e0c925da55e0eb05d6e6ca43815ffa553bb035d529122a043bebd4f9396d3cc75034f277a70b92d675a1fe62e9ba0e5957b20379ef7e38e56d8fd81a4883e072e8a2ff3b1d4a0a92b47b2ec7877957a4e33a2f5a7c8818df235dcfe079b784472bcb5e48251dd1b3977e14bc9c3153b3ac8dc6b6f4e07c90ff4d3bdfb58347764d3be3f0eebb0c6bd1c1e0c550f057d056573940d607b6a9d87ab6fa1420ebf6abed96e1b2efbc7b5a1957544c66b3d171bd732d8915b80245c60fbad758b3fb03f324ea7e3e8c0e3eb17884ba993ba3fe9edf1d297bc752eeb780d55e196cd86dd3610a3fab3c178ff23155267674e5db69365ec139f798c240bec6671be1859f0f1fac16cbc96d56ca04a593788339254a4a8eae04cf46a655c25a2127cd9aae4d46fea5929d6eb1ae686b5c328969cf900d79d4a2940944f2e160a8d992ffa58f0fffbbf06bee5c55be7c449e29c9765ae97d61a8c1291ccbac511b89c40f6a829046232d79a81e9748d94be92ec36f534a2b34026e601349c5da96ade36bc16d56883dcc4674126f31d2798a678ea9e7b9771ccb1e34e8a66901640a54bac734dd453d307333e575378807e50c572ef28934f22b456bb3f3c6f6363b376b756cbca7597f24789fa708566ed18f1a206278a8127645eca6ce3ef925a805024f3570fb6301b6ea7dfceb5a2cc30150968dfe02d77d25abce79cf4e3c2be9436afe2d655d2d203be7a4a2a3318af6905cfa64dfbb1b2bc1600fd6ef84d930bf11f856e754ce78e66f8d09b722eeb78c63636236aa24058133b977235a2c39039d8cd43e250aefdafea616942ac74dc7171eb6b0aa2bdc4ec3c6db68a32f627a8a746c784455edf66b72efe23ff921ede2aa28c96993e4d4b19a0faf32bc6a86bcb360156ceb4f86cee8f4518be25a30adeedd6ef7019e16c0d87af10a14b675c5b03afaae6da0b84e7433cf91510565f598b3cd8df1b59c07781a83200abe42ed0415d5118be5f6d72dbc74b02aa12449c196b566c0f0d45f70585425003be93741234b8bb4df3fc567ff5d0cf8d758a7e79dc3cc734e43d6f3259413064350af8ae8082cd9aba4b884227fd59dba5a46de6715fce27340d597593278d22660c30ef3b8ae73a614ed87d58718fc6c69a4dbfbd0de56ddba804a74139efea8afc0012dd83d10c4982d7c62aabbac8136aaef516056f8cf37736688b2cee822341e6201d2e44e8ef62d8a32f758ea1ad0c388b7f54a22e7168c7d13a340985eecc8448f7ccfe4d389c17fbf03a88c8a727967920dde01a799d40292967cc58f273177653ff8c5b0e01b896f0be1b10fc9912f35c9a0673f3d8f728b3886cbd3bfce32de60bf9344967e6c364ab2819b3efde9bead8e6a4c884e44816d8b60e7260468b5cd815d1e3c311d0f8667776a55d68a52be4b97feeef837670e6feb2be3304b0982befe45769e074114ebc1ee5332c2d96033e5309bf8f9c799a2ad10eec1be9fb379c736731b7714fb7bd3bfee84682f42e689d57f968d2e8866e057fa3230ea9f910496c5cf1a9a56882a14ec6a12ad85c635b5c81444bf5e3fbd8a141251a36db0d90d003a684a25904614c5ea7d4a88c6bd930d5ad3dea1474e56f6f8eb3c294b9fa7c67b7886a6663e4b8bf932ea19336d3778c2e118b1df4209540b015269d408d3c484e13e67d1dc002cde0de390c324307951eb3346d172d7bb9bb915e0340d15474348482780b1001e34038d1a41efa80abc585ad7e38610a76a8bfcdea4c5f1a59edbd18cb1c2102fd87f000040ffbf0b442e7d328ff2eebb6c00409bb1477aae928a572eef02f3d65c96a49b65583f7437a990a4ae32a9bf58735be15915c194ae8db95ee3cb066f5c1b4e0fb8e918ede8adf4265a5526017ce70f6f009decec569f93fced1b081ba809d8bbcf727512f1e4f12212f768cc31a2a8a0a27b23f68685423b8af6110a96842042b6d810930f81a0ed627021e3e7086675e1c49f8746ae7fbdbc16fffbd3e8a5ba85f09cde85d6790f0bbe7255364ef2ec5d10b0ff6d2f8ec26894eca46fc53b89d50fd587e0e1602ba07e838e8bfa1dbd3dba485b2597aec1f2449058c747b5890f9080c98cc6997eaf96c8d7fffbf355460e84e6fce49fffafb8c6422ed217c638fc4d2f6589bf2129bf767653d6d345fccbde2730765fe82d2e282889710b7aed0dc246ebab52371110355fd8899fc1bfb7d71500c599a326d1d745966ea823c46c89e98f70e76be51934d7ca5795b4fb7829ce0b1562b4125e96610f55c0fa8ff5fb6443db25df29563321b41acc3f524d36b7e2952489dcdbd49f25209819394a9c6a6223727e941a7cf1365f9733e6c20bcd427208749d2d437c96a3c07f4609ff5447d040bd39223da20aaa8a4db360d3d5f94b9fb42fb288a82799845ff12cf145ad0946d8a89ac40f3c533b0c90316ed751f937ba383cdc534d4e0c642c8b69cc394094679e4e9602a9f2a8d86a82ae836b84ecec0bc782c618e66886fb707336545b88facd0c10294fa42de765f440817699ca1fd4789de01d08a19fe8481b9f1179684aa9412911f8ac9bea9cd3affe7a0149cbd84cfd59e9751d6664384e8df090626189e3cc73c9d17252975b34309f149002640cc28597098a9cf7a35ca91917d45930b034843770e5dd72945fc725025686bee91c5849aff54c93c4487cc6b1b5278c5190ad740a01a539391d817edbac87a039a834a238000d787cfb6bd547d3ceceaf79919b1e50a02ffa2025805d3ce25322f7c080fd8ad40c27217ab833294c2b47726f10f46efc5ff8d7f1b9039c7cf0357cc4f9fb47dd5f6442000f3e2c186de73b56bc0f44926fabcf3f13c1a9b71b2af1ab93970b522124c2c269bfa91ebb927b0ab8377794a4d94853fa8518811b06d5ce1d3970fe58f06814bc0eb6fa876fd1b4114bd792115cd91c5e71f76b5937a74ab8c88c87cf7fca0a015c2be43e3c9a7d727ef8ba2c1b1d1a92f2c9536a3a9a085d56f54ab1e017ad98b2b87d2184069bd1dced0dc26051be39e30253409ef79197dd870ff37323d11b487a8beec972acb2e1a234324c0eadf7c34ca77f0caddf7a21ddcdae24cffdc6b08cabbba42571f2587325e4ba8906283c748ed88f6a5ee8fc5f2b7e345ab04c46afad9e6a058ab8fd8e4910febe6210d1a4b16e26720de9b436b7aae548dfda2b44563c99cd71f3064296be787c52df6d6574c6989fd656f16d467f3f4639d327f32032d5708a71b1272591df4687ba84aed9f36d756a4a0e33b2c3e1f9b2afcb1ea883a668b89bd6702d3c2b86d0161fb1e85c9e591fde5e9c3c263af5d6a9ea77fd8eb91a33f6916651b5d133b8f8e9b886c7106f4420f05a408af73506e01338266efacf6e1683c876a00ed92fbd5c41f78f5150b2babe0bfae9497a76fa7cc76c6c1df6d4a0891761cddb4ad0baeb73af782a9291c3d915ec7950f36effd5aeacbe47a0a9ca1ea4c09dcaafe2e71f7f9d5a39b35689f6392958fa08f1464dcd2893406da7d861a937d741fc892c54dc63f371812713ab4f2521e0886e36346a5b11c65279687aa0dd3ae89d947c2976985601ab6f917c26a9b0df82396b66ab7d5d5d3e732725c7eb17239dd7111a43c70a187035333a44aa3020bf6c01f3830fd18ced18d279c50953dbcd244e5c00399068143b0b48a399bbbcb5f4db435ca7b861d2e815369d8e76bace433905490ee2020c5efd790743435a9c17fa272ba23bf5a0adfcb7da081368f3e8248f2b4aae40ba1503d2bd3ae6af38d21f28483bb1c66b5cba728022402e5943f1b203bf4e33daec72133cb89baa16c659b7b5ea23461812bf7bdf5160a28097468fd2766f59a46142377168c5f484678aaca6276cc3ad15dda5fe7e82395912e9802d175766ef3dd51aab9cca27d341e111545dbf7d99f3e92cf8c855961932b96aeaab786d06a953ff850ec8e84feea94fbb5979f93eb36af635575a209eda6d5e5fcf5b19b1742aefd4a63b3749ad25cae3b3dc1e3c18d490b3a3199d8a3d7ec1f8836a52d41e5ab85436869fa70cca1734f3b338c2b4cda95825d0bce429f60ee9cba47e7c6fbbc1652a6f7bbc0c7d60fc886b485387b781810a8db7a589c1b6b2bb371a161298359204dfcb10856462df468b1af0684b98ca00db0a6e6220d179b2b567997d09a0fdb3465fc225328a72dbc3cacaf96fe37e8c20c6a153b02103783911f83bb6493084e47b9a840e44c3dea3eea2ec2051c5a6327bae0d28f74561a9778b80e380029e92995bbb891e7a5f9e36786ee0edb780f4003d6a4cb6ecb94fb53a2307c55b045e513499ab313246bf2508a3109da044ec3a4f81c5e075f0ec4c2905e661aec24586c5c0e84603a33440ed06cf6a5629e744a201a49299041ad033401c0c866830d5029de9ab2de22b35f458d7d1b1cbf2022014ec2b1c359631f5c8ccc95f1a7173ea2f8495591ad1cafff3f2184e37e2bebd2aea8628473cc276a3013fe9d463a391a7913696b479be6524fb1c0f8397c34f6e13c10d7c8c263eaf6f66276f6ef4cd2753f2ffdf3fcb9f2b6dfdb8912a609898f46adcde6c1ed47d19da567562569fa1342e98f93947bbbf5f6f4ad11dd1968de29f9eecdb72188f093c18658b0957c5c745556364f85e7aef2d19903e740fc5276c095f73f66e0fc04dda6e98f6538b108152fb63221ed99e804e46d14f376b7b39c33b823a358dcb7a91a8629a5bd93fc3085c457a12ba15a0f1d26dbda2910cbaaf88ae44e9169560f0522037956bfbe3f1521b0afa55106b04381d27a3fcba9be8d5be626aba6f254b5c01230e0a7dc19ceae028c379771e5b66a2f4b04c92cff49afd84c36cdc2e9d497569a2d36640a33c5c1fbc184683b56dcbcee990a06b399735770fa393bd8e8d5208c5b2039565721a87450c812de87d05e4bbed2582e7587f8605a80a56c335c591c25db3ec88cc276ae90da1391400dd01adcff8e26305aa04574a746e7ac2116dadc6d3e01ff0f004128d11d56d099cab53e8c8e450b0a30f48f6b257ba57d1eb9ff1ffcb2dafe2f59b6175cc54800866e033edf95d7a3c33325962eebb4b0077fb0f1d0ace4d321e27461a266a0ba471ba913b7d25857b60840b9944c8d9afd06b43ad6bdf4ba199825e78c157a483f3940fa564ee9f4628850f76ca53bb9be175d772498ee964b86d474e7a5dfdc3f3a85905871c242eeb137ed1ae82742aeebcd1c5e914ed695910ee6eea62ec3da82751f1d7f11b6b493cdb84048535c1b3c25b8c91f88dec4849f89d4a65e11b79499fe7484ea6e12dcef3fa3fc8d4be039c188bece02fb88161e9e88b99b9fe59edd14ef815206d179564de3ca0f75bec0fe92dafa6a77d90db806dfcd1be395678de4cc6a8798b40b4eb27f7a922e53a60853cd0e70c6524fd62d5ad95ab7a85d2a373b831ec5b7a887f689a0b698c8352f9ee1f600076eae8f5b5e72307473a7826906199343f8817e9bd793b43e8f4b05abf4d8fcc2e08a7728f2c1ddbd16abde1ed948ab223630393e77c3de4cce59dbcefcafe50d2bd2da1a0311b9dd8b012a14752cba3c2a46d7135e5378b874efaaad88c8f4c2b6bdf4c3793086766ecd202d2ba46cc657f2ff6ee3e6941a451df334db4d3781e604a2eda13874419c32c32543060da3db72a436ecdd91b3da8b4408ccf5dcdde87c90ca410aa1af6efedc9af5a35927715e7ac443979f75f774174be0066138acbc7776b6ba2d322a308d7ad7f3df8873b005cb18f609a3c72bf4d9303e4a26e43d43ad88ae371b8cbeb4a26945e57b4797510778ca4ac462a343ef2d62e3ae9adc8004a4a3eaae8b696d34d986d471dd420493d83763d42166a4e46dcc070ae8384e0d3a3f789b1c544210636a1097b23cd3955c02413719eb67ac91f32bbaa7a6a9d4e726a9a5bdeb46c82e3e950938baadca06f8ba09187ea7877db850a08bb43c6ba8360d6aec8aaba795ab855a514193e612747cb6191b97a27dc86400c5192940251105d63acb4b618ade129a0f94375cfeec9a334f04291cecaa3180269f1d8e0a24d9412c133ad27b3cd49e728bf35cf0e0fdf543eb42830c0f8c2caa2f425ba167a2810afceb2cc9d61effa702179e7ae59fcc47a3e47a3073a035d41b02566173d35a7a798aded8bb27e070c39d925e6468afa45e0739c0de4d111b3dfd1accf644fe25be3f9ce3218905277026314e6cf9baeafb9985cf9b67ef457b9266066c6ee30fe104900080f49b660ce854cdaba5fe805585774e1bdae702b05c8a752d5a60156047b18ae3202bfae61cc1c47c33a61ff42009ce36d77d7dfae642e72c2389a7c3bb99c615ebaac5e57a1dd8414c97d94c9dd2bf12407ac08fb76c56325ecafc25e6145ab0c9ef6ccc141c7beb6f2eae784e886c5725569d6ac6c510c61ecfb384389578d7a20ba5ea8bc3d4fdc6a8082ab4b789f79a869c700c650b672d8991c55bdf356527a2fd59900e6432ea7c609d48cff200e5c74690a96cd1b5fb0bac4a833fab3dc6589490ac26cadf7c2bdb5f92b8a787541944754e5322c85b78d9f644b678bec18bed0c171f2a9296592dce0ee57db028b6f1d73f4c1e27568ef72b114287fd9bda326e11be333d50b03ac4f6dcd5aeff44637f50aaefbf374bb15dfe3611660828a794c5a9708a455162188988635411f6c74ef85490a771d6184aa4eb1fedfa0cd72ccd1dd98500b5433230a3d0ba1b17dd080c64f8c615ff0025e70977855dcdd81ee1baa6fd27f079902ec472516f69e928b86a039e2ffa390db1e4fe4698e5654dc0161bfca89ceb199e54773973787be1a4ca7119146a72bf83d33d1f9050ba0b9424266fc73ef5a3f9d2833877ce5ab88ab13eda12e14e412ee243fcb09743d05417413f982559be6d85ed47b82394c44a7b479d1f8b5008f53887ff138b81d33a359d08da08766d508bb2eb164336765d65adc8cb1b703f57d81a513bad92460ac2406ddc5fb60947918e834a79c5912094e9dc406e0da39bcc9a99332d4dc59ecc563aeaa918c7c88a17e142e20dc4e7f8fc22c7a420339d8dade9af977070e7bf359aedc784c2af064d83f4365c9fea9f496182878cb796e57c5766847142e2cdeb59b8b478f9f3dd2016cdeee9f145ec5dd0589bddfdd56203739c0f1339a9c80143c55e8f0a14b18f46a8334cae866642533ac297fe2271d48dd4a65c0183a162bc75ea289f1edb274f1b204c62039a6d982a6a84d75168d13463f6161e591df3f0af0eda83a504e3185f4fb0b4dcd8a243c6ebb543a5b7ba33ad551c68897ec2c8e7a55eaf0a4efd457002c3d25f4c06342cca58a9b1461f4f02bc0446119cd665c33835704e980d56b5cc8944c1abe020e9e8e7bcf961fc3742b608ac96a82100ee1c08e447ca09ba080a2601e300beb3459e940d84435ef84c51c51b8601d2318af2ecfb65b31392303ae8220fc634018a24f20f8aeeeaed8a63d27c5b0fde38a550ebdb8a0a5d1b7c76ea9af6e4bf8fd020f4760de46338583d6529ec879852625309ebdc4227d35170dda1eaba2dd01badc8d5c08f2f6928835093320e7e6435945234a50de3c6a8a18ccbab14c7946d06786e5a3741014476866d4c93bd0dfe5f1da1396a3a223ba32705029df4c313c77af0aad60c7f889891e664412009a7c4273ef52782d1606854df19afd10810b9449c07bc34b5f4755375b71ed8472b794fd108991c568412ee66840173fd0029bf912ea9de30ead7863d703c7a27979d3877b2f1ba6627b9b8f18be8ce932d46fd4730aa46d3166917d0a30e5ad8bda74a9a525a6053937498cbe04679672462eef81b3292d2acf982f931576f18bb59d38292c14816205954f074930c8229dfe233774599b368a102d9778282d78d45279bee9d6dc97f3303e7195fcf32be9e98865af76190a66dc3e0eb0b97bce93307d17cbb02c7739cac90f4685c0a0656b33a693541f06ea4f345716bbe6af4fc918d98c5d152508338cb2eba2dcf25fa99af55aead3ef4ab3d2ae0b8964cb7cf8d78407c2367528542808964a120474e9932277cec1776dbb52cf13ce853fd65b0ba997fffba4d1df9313b167a22ed9ec5d56bf73dbd9ddceabe0e335c55241b7a27921e48066d463356f622877b55199766899a6d24dbba37513a20b2f4d90ff9e82353356960d11809be01bcd4e66e362fa9c832389c941a41295e710b09f1fce0503511cc7c3265d94ab913bf8a5210a45425af2935b8c2c3a0220cfa05e86e919bc97a47398316bbfbd757cdc4eaa865bbe570391d527ed51cdc314e329e290d570196594fcba7a0364269c3b7ec942c6a461f00994d3148d42282af415aa1827f3fdd30edb3c967e8163994d46de71d5df2a5b82005eab91c9125e99591fa71e7d62925b952aceef08c29feb5d7c23a675f311d5b3e235b5186d0cf403a730ca0631ea367840e2a48c62a498954a6ef850a02dd7f238445859b869d98a28e2978f13dbdff7cc150149f835bca03699f2309dc6ca490d7a0ba36ba2f8444bda95df0885a4300dc8a08a13409d644f3beaa91db1bbc0ce9107e5620b41934585d42cd915c4958f39643bda97b3d7b26707c9dd543ea7be0ae8871a65fbdb97f637e842db93d60e4e10ca81486b6f6ce9b989bef27aa35c801c9235214256ee10e79191e29c215bfaa401eee4460a2ee5b1c4b4fb8f9e8e16548da63d7a99633f309ffa2f5239e38641567e29a31cbab421831def9795f5ad47edc3b2afe1ff187b0d2d900358ea20cfdf89cb170af73f704febe77b0e6c1b1f3926b5a845168fa2215667cc0036933ed5df948d4cd0d7ad920d96c1b9b67ddd55dea7f79c2d4d5f3fd5d6893d3bce7dd3c8dcaab1d12bea4783336f99a56958969efc5c1338aefeba92a90b1dbb79ab815f3ec551e6151095eccc3d388adfd32718f9fb006f048c06c6e93c955092ac8238a291a320566001cb2ab3ae11429f735f64d6091cda9a7852aa415c18fa2a799493143c0a3ba74ac6b95c57fc8339edabf209b55710af6c3c9ceaff2e4b410dbd51e7d6e30191fef0975fa4da749ffab020ac505d7a53789c80996b5d96acf1239db0cabff4b9c083c14b489389e252590e2ba44fb8b727fc503cfff200f0105167d4c6f107d1fa5cf6e7bdf5587bde6e913b33decadc634279fb4299f921bb1b3fbb9aaa6285fdc28eae8d4c5746c79c6a7b52b27334b1f94c9ae82d5ea534fd074f0017ac3aebe08a5365d46bba8b624b052be07cca6c35f6b9eff74f80c2f9a43dd7741105fc94663b4b073989557d2172b59c42f8273562b057cbed519c5728a2d8ec67c99756cc4b55ac1a639c6d545c20b641de448f53d9b53f97d9d740283c64907e540ad1706934a02c7e35e8cbd9abe0aa246b07acd7e9ff2b22ebad3a2f8361fe736f26026e5d1404c903bd30ae78f9c71a72994cec0d062f5be477f77cf940ba7b10f0fe1167aec685af39b9b44db57c8dddb63d0cc9ba207a3de91ac309a7b6fb6956cefeb4e25da7b77a12f91e212fa69ded946c0e86581cd03a45d28a018632fa174917f7264812f490fc06af4580d0fa3ad3624e501af8fe217a8f9ade391c0453281a7702510d00844da7c505c2ce48a5c5f8d674bda1a6829299b472c16d8e5f120cd2392918ece8e820e29780e51a90c196c660036389e2cf5fdf867832267c6da16c7609ff6374246e2450c592df89b37296c96a8f4f495cca0b25331c96a2a2b7152fe9c915b8ccd16ae71d6177743c50577792c179a2922a473f95872b67bfae958419448026e2942a18628e1843a385ffcc38dc92d874b26d3d6530b3710863707623a4ea256e182a1f3d6e1d0ffd92ca12881dee9872b0e79fa794876d1a76efd473cc8d9c3cce267765149c33b35f2fb0495f508fb93b7f3a5daf1f375e4cbba10acb2069d73d302f4ffe774b7c9c17431858daedee70a37a8b10c6da82a4029a0179e853ab72bf928abaf16c652ca773fa4900578ae42e6896ec86d9b195f87985737dab5142c08e1f011454dc50816f65a1e97ebd61a3d2fed679e1f8df10a972d17c2f35f80b7285002d6ef3d0d543598cd79a4c3ffc88db64cdec4f9bc166d023a7273fee4691d4b1d94db687b1f5a20a450bd62be762f39b9ada6fac83e75a4ac3848638baebf1e3b97455b9d5a2268a5ff78fd030d0b62c5cbc91424e3555b5afa6be485b3f9fbf7d123c203bee9d8c29c639544883e8220cfe1852f3956ace1469e6a1383febdeaf1f37fd9d24ad8f98e1be9a467da969c8407a342bb2c6740113a530b60c5bac9273a92f73617be16d5b39137a9d5cdf93ee42b9dbd104b8dd61cee14e0f291de273726590e91783a1583ba5885961ac7c8f6a270097710cd45ae483d5a60ffe15261bc4e39be21be731ecdd5f1612ea78ecaff41af9542d68c75c302b7c6b3023e394304c95cd269c4e4db627d5ac6e175aa199a8dcaff1128a6873a18b8a8575fb04f568973007347c3b1be8692bfe7a0a0adfdd550ed8328e15c80dfb538047329d175fe6b66a74ffa772bd9abbd1ea75f1f6af9887fa2cf34b4ddaa40d67bd0227520f1b48fc33d7ca743c0bf4665a8559b0490f39d7e4b14c37913e3f6a90b910fb923bf07e6d1a87dad760e60428140eb1cb4fa03e18b0fdea47f4a6018ad3cb932948b22c044689e7c1c19852e4a9d209646645dac6fa30846d6d0e1faa28fb1e405d50df210f7e83b13a9936a3d8a480a54482e4994b3d915f0a9fae752c04b00ec207fa7717114c92d066271abe8cb793376c85c878b21ddb68c3a88afc2c996ec9a2fccd8fbe4bff20fd26c17db18e9834b3d0003a38f5f3c7e420b62059dedeefdc74b28814d9e72c604a093da98fc2703abcbb0f288c433b14342a6eef115bc7a74937e5ae716cce1d95e3003e7d54835c02f3b9a7928ff7a22493996aa422d0e11ee711d0c65d228142d69b8bc4313419e68f8b86c7fad0a6c257f7f5b2577c0ca53dac1e96ab33d1a31c3662fa05b9ce5167b6d4d2902359a7665b7e24f97a3e4f32769b48c697574c0196f064c0842737596c90383ea33120b806e98c622c63275d907afef41125016624edb9cf848ffa88d408efef675797a2bf15e33083551254f38e0f105dd3c99864633363204058a368fd8b48803962825045b72dc3ba42e8172445cb1508414b0bbd569168086c564f03ca942f1eba8506ba0641afa341920e139f0ca20defa70cf2dad38139796e3f210c74c1ccdd3f1257259cf7d15539c379bb3ebc86a337b5acbc21668465bd7300956dcc6dec29dae2b851b7a7a5b6bb387161cfcecac58a253ff802402695c11d12e5a0383c10cf89c02f37496ab1ed7d7bc786b4f506d5372436e8dbc87f054b9c99ce54d047c8f91df198bfade84d13398bc0ae4e13314b1dbe1c5d5023dd5205c142e41d36b00d99030f65730d163e088db3504ec0d3d33d942963afc662c1931331c19ab84fe293290b3b33e214e4f88b3374eee4b3db6ec478673efed736f5a9bdb5c3e7ad786c68a41b5b2cd846bf2e2efb1b9c98acc5bfac10ac78b4d8ec64c804b839304aaa833f184f6e8506c5fe3bd42101c4336033c50a23db0bc536eab0178aa5e19b2a7a9f14588e860f24e0e015672f981100de2cfa8dc446cfc6ad5c59c7dc71859c89e0fda1934e3ae349e573aa8fcd6b3389812e55e4a15e78a0bc28c20642988ee815c35b9fc4a5bdd7a537d571b17777ac5b47eef3167216267d45194053435a47e52f38d1b2c8ae384d975641c9a0649beb348fadfddc8ef6bbdaea06ef197c5e84e251d67827a81a25db5434c2d5d11133f60402f7f9934e7521f3888623fef468df25d7145edd9975b78a4410070ef58750d94bc0abba4dc749eea98acacbd87a4fe77210c7aa2f74b5f93f31f24fdece49ea860a52ee7b7dd22617b9ced11db4045ae79203e9184f609db56878de637d5528f6643e7e0c12dd11389764201dcd2e78f04f7afe03774a01b752ee53f385c8bb07ca57805ece7ffd27f6c79662c2aa8ca1cc4848d0632797e96fa0329b8841dc81e9bc0308bf126881b4688bfd2bec37978b1d7ec3e7a37ee5e65b7fbff6ac7d7b482c77bbf9ce75e83049b4510a31bfaab0079afdfd561ac36e7693a07324ad123f3063a47b448056188698739bb1abc2bfd4cf35a945763806818b92f671376cc821e6ec45642bf19dc6c4b7f708be2a11c3fda20a35094d5424d608be6fdd12d70151e98c11c820a8693849e9c63be8535be694ce0db3bcdf68f2108e18a144ddddc881bd416975c91c4bd700025a1999387843f273cafb27d423757465267aec9654de32906308d3804d5e121376fc7fc3ab6cc171de925021dfb3e4e9a09b463e2b70ccde871d9632850005869eca737dee4e6d3adbc688e909668274b0f7a518717831d633ad86798d645458a448b5abe778811132285fffdc8f28ffb5fbbe7f7608996c6d74cb85803f8e19cd46f69812a139966f897bef0979bc602648cda773dfc94bb55e03eb4be1e6b48c090b785f5978f9e0ad240a8d6a02b0283bbfb3d94cbb579ec999b3d19e5c52311db2a93d5a63fc055f340d53e0e706ca5bd84fcebe367f95a13343d957f11740c56be1bd3868dc29bdc1f179b20a9084360759c941200621ab21bfec59ae5f1caebb6da7a5fa97f0857a6942ba36102d1835db08023d3626b182806c33c09beeb9d359aaa695711d8fe2799e1a06e05d9ef202a82256022f906fda262960bd81e399adab173b7533bd70ec06f66259a66cb604ada7cc824b1def0636323ab5023886c8fbb07fd88cead1fcc60498b6ab27f522dd036817784f084535b6a81396bf562aaa25ad35a7a909829684d36d01da07e7d449cd07ff54c51f353d665a5a031c8c780044b2925e46e19ff178d28385e901353ce3a76cf2cb0e49cffdbe28518c3720067e835195b151bbbe6836011152b845f9f40d0648c825adde0037552b684c29452e7e630a0f8d3e9ef488896ba60805fcd631cb0f2627d873ddde41a5144c27897b355dbacd36a8e55148c1adfc8db7a93f3c0f96c764c0d1f859222ecec13f7ee8ed2c663fad1080e75a452e1ab9429d9ef529829a03372bb92608eebaa9ec840608dd598fe5da557871a899af5555eb9f6066655457ba668697e4c9c096e4e625d16d9a74d65c87b3e267c315088fa562f49bcce0031442f71c6163a9c7f728dacdb76f977593c8ab065abdf9bbf707764c41f3452cd2ccc651804f464de3c88e58b0d0c100eaaf747e7d08c01a37b221e23e8b3211912097b20a9c9fed743ae4cfddc9aa35fa73eb31cecc24cbc9efecd5db08b7e276ab4295c39e2ae9e05654588445b31dfb34ea6f623353b7d765a0b7a7e606091538ee77ed2cc4d71791e05c8c9c1fff5026007ef8c4543ba0a0500efe252a4b65b7e4fa9bb5cadab5ce9fc43bf2186221e933f126cdf88e04c06e5f56d3e164a53ef8290a75cca705fa2c941466b33d0f64cd4caf28118b81684511aa9860fbc42fbb5214104e5ba4685d5b9c3cfcbbd22202b594ec3a6980724c03e89efd6c34b008dae3c61e072fececaef4dad4b77763052576c1184e664d6a8407ac4249951e86d8492be0efc221102a6de427fe62a7b53607f3be2756324036d39be5638a42e4a0605465b7ab0ac4a6d8bd690a2fc5614859dded8ff02450e26328de6363d4edc61658554ef5f23f4f0056dfeb2ca1f4a17468d7ce21437d8d4155722bd0418cd0990c4a1e8f53b2c700078d562a45fd74e09ed72101839729c352d38b76aa38d386e5cbe39fb7a1f57a41102fb7fb887450ec155f29b18cc8430bfe8da1c14cb4c5f76f64be8df0b4d1a6d9233a4557097e17b093d8fd2b521f515da9737f24b81ab195dc9967607564bd4fa3d010a0c9333dcbfa6fa93af9407211190ddc95821ff2abf51f0dd46734b06372464dcd6129bdb9ce645cf12bc0e88fb1e0fb5e198ae12e101366ab5bf47906387021793d7eea5982a431a90b15870f4a58058cc29b421e7e6ffda75e8777bef92ab62e050d69ad0676d2ccb93ded79e99088f0e55e67f7a684d52129af4147773965d00d781ba26ca78253256ccad1b5096b1c3a89437512908850bd9c3ce02c8a7050746f88652f3e5b14f7de16a0e1fdda3cea6eed17dbc43b36474a8e8ef6c46354e9a4cb124bc4e64a7a98a1327b960b14a845a5c2d44ca5be88614574920dfff1017709405b5dfd72f2b2e314c99badf16756f45576533fe6efb77623d8df2d0f324399d4374127f168a08ff32594c0db240845a67f419ed89b817ed3a6bf89e37d34846753469e196b094fcecc238d54344a7052970f4db2123180375227a929d780238e08c0cdda553aac7a2ce2de4bab0c2fa0f6eeccbf31d16ab05ed6dafd47a572bfec74dd4a9dabb634cb79be51e9dc4657d913f86718bb7fa76ac10f30643bd72f77c927a4bde54f9ebfc7ac3e8794a25ec4386601b8d22ed7b2d3fb7377265ef09961c0a1a0bdf0588a52876545b8ed8a5e6713f2387e2b712865ba195ef82f9cd3b86ea35c4c2ad4133f60553925a0963de2bc2bd4ab6dcfe23a74851659d66a556a83394011b1578088572fbff7b95a979ab5e7e4aee9e0f69a40a5597161ef2a85ce42cecf01741b12ad2ce442f9607a784f03e5d22bb421a0f49c60d27aa3f756542b3dec52c20d172cce082035fa5716b9eabdf1a926ad2b41ee140d2a377e076c46ca45d34877db637bd82605fed68944f296242c62c97289d84051d87104fda40d1fa871045739f5b7b9d74cf467a156cbb7c0d35d988df44c8d6e4ce1405fe0bc1b6f0c596f2e7b64432fd6f5e5e3f43ac092bc03a6cc5bf15e413d8ea82e241d0a24e0fb6ac71fe5133bc8f6c1fd8560128c608fa482975ddc18cdea0f2b36a54b8c34ef37b0e2366a3fff28e06f3b12cf943675e97793851d52b60a79144cb33a5b0f9c15e45382d055625f35f62ba1e42feb8fb8c9303041fcf7e36241b3830b9576212c52c208304ba868994675dcfcec9995a1d1630161844693c86ab3e00c93ee00dbdd0e2495580a82aeb46992b63753c7dae083074ec944903710bad7b90e25167e8d1bc994a98cb7748e7f547a2b3402d4c9fdcc3e4f05a972966737a5e3a80c2effcc0c47e8a04c737fcdb0fbc26fa3c680baa684a6fbfecc36fe85cb58d062766e8537cf3cb6eea93d605c3e33acf69faa182647ecdf5f0814ebe3d83932d6e10d47ef3277a8fa4cc2edd146ecd22f3a77be27c30c4a7fa4f060470578aff579721ee7eec7d82a5dca0283a527f054ae1ed9391a1e525669cad9c245f962829f9fb37693383991c9df03f636fa00224cb8d8aa88b1388b60fa38f9bce55047e409d4c81ba2391ff3fdf4a422b152a2541aa5147fed0adf217c44a6e5b02d1d61816e6e483b9a8674b55059e7d8fa006d3df0f3585ddb9b801010543db48e657723bb08e3b7ccc2f88c8ecfe3f20d6f420fb3e1624729e95ebe46147198a2ed8cb1fad48cb7a3dad4a2eb1443108f39e1e07b62cd81391f28bc8f89a02b5ba90f88fff06074cb7463e909f38eaa8f71adf3855b4772ec9b8a9f5532c3cf0c52c393bb025e477f5e9680730c094715e06e1d1ddf3a906f39545f83c4c18b2cdd3c6d79979313da124f4aa1e54b8736e2e1bb8b7734cd6c2b29289d2faac78827c73b5f2964bbaedee0a15986f135f67c6cf04e8b07d68b4f3fd8227129d02f570426dba127f4235465a9e9b45f27866ece71ba2c74c641f38ace6518dff1bab356b2859377e422f3c88990552a3b703575b3b6b1b4723d2baf194776a9e80de7c3650ca577feb929ec647b881132d726304d2e263c5c0932d6bf9ec2e0e000c9481b4f29a0d6c3bb3df8222b39f8159aeeb3e6f93af932a1749750e8ea7596c05117caa82c2e9c5fecf0ca15ad078fdc540fd0486f6bcde8765821e2a8007c694c34bbc7a96a3bd92fe7a60852117742e6b55cb0bcb12ee931b0aa578d195115d48283139aa9ad3332b4726ffaff4b64ed8064e41a3db3b12a950d927519e883f26ca66d5bb92ae34e87b49ff8d9583138efee228ceff9d2eb43a3538012e1675d969178538b9daee75f304f7cba048112103a9027bb52d06572adc361873a885c147dea47fa000b01835e8c121aae9899649f297d07ff5a6af059fc895fbb6eb72244621599559000d4fd5afbc3291462d9c9e56d1a5f4da4dec6a188379bd244d790523f459e5ff9c530170d50c48f657c223ea84ac14a918404d7b67ddb0aa4c6fd92ec602b30ce5c9091a1095839c776d6971a4064cb19d298034159ad8e447bf663208ddbc2fbe932a236874726c84b3fc7f27e21413feb1a0670cfd9c90bd5fe8837ae3c310b376e5ac08c20a73d362ae1030e3bd3bc6dc977c17f7f5d7916457413a064a20647d4e1765b1aa2f2efbe48ea38b02a981dc05056249924c6a610646715674cd0afb2cd5ebb8aef752289e57be66b4a7644fdd1d5e9092b07d0ba0a83be855c9a7624b5817e7694cb45c6814f0b133b02401e7a694e77c14eb1e9c0ac09bd9d9b04ef22091a2241071b3b2c53320d65233f1938b78acc53ce9e1270db6b5440c7a8bc745b93fb406870de1d8d9e68f0363b8087ed5e91818acf87409fa3780305a1bb82b356b6e26e9bd2873afba83c7e95abeeeee8f23ef565f2519a8616e56d5264e0c56c7e0203c70857e801f5d847728e0450da78f5a3b6a9d5c54024dd0184b75d35671ab90412e1ef7a3f44698c6567520852ec9ee7cd95797076ff25d5b5ab423f792f041e1a4d5dc79bd67e8d4d1791e06c70162885e6572412b88d8a32cccc8b1875c6f8f4083f6de569a63a1382c108da572c1d2cda4367d598844a959ceec13601106ee816b2e4410e9736c27dd23c616f19f3e954ae12eba3c01daf2ed22b649b50f1512af32d3424c85371461c46a4ce066379277664346bc56a845f04ab3a1feec294b7b6ab64980c64c18d385e9dbc60af35a5c417582d0d8b2d7d7c87733f9b3591ab4759a9958875a6c0f50067943882c187b39df2d3ee5fd4a935b62f67ad5b2dac64c2275d0451ac2d6b18073bbe7df2e0a60dc64d028a223802c59212ec7b713d806978cc353baad4af6cd14c1fae9fbe030f8a92156f5fc04e6d3b888ad74147c40d9b27866d56a631d9cae8169f7f699b0475b113ccc9ffedd5e820a74bce9a601d68bcbb100de476ca70bfc6d3114b707455b9b47be6f3e48133cc3493c6bf1e68069195211c0e70a2a64be69482fa107362ccd1d1b7e5a3a0bf2b00a5c5d52823ed5c40d8a166606f578e6b5174f7ef1b3d4a3726caff4917c1b7f080cebb8cab4097781cf753c203bfdddf21163197af2ea284dfdef0b0dd4c3495e13003fe46cae184fea822073add3c4366f39a0d4b06a3ca9dce395494726369a3993dc9d1d847024abe47a56b9fea4e9a97a13c76056b03164ff18c9510af99a60b30115706926d4f0ea3d4d4b05b26a8778cbf359f09f7252ab76257adc5b7741096d46c6b28cfd2dd1d3e5bc550b98fb84b14798a43e6da2b2132cd340415caa16d7912deaed4686af88b6d627754a4196eda35d51488ce5e8c6eb9dc9b9a30f997f90b49c95d9e3e00952556d56180ba9b2d10b14d149fa27f4ffa59b6af91629ef811817487e884fca4c4020c60548f47d8cab817003553bfc83d8aae53eaa546ab3ac92fd10fb4dd919930de8003ccb5560e2cf21e907fd2410ab78168f8fa14b6740a619ed721b5ffc2655ef01c2cf1494459c46ac2aa665208fb7e90fb852183680c2168f55fa737fa6f0d289577c0083d0117e719faa9adad7b93ff8b73dda935ee8f2241d4a2911b81bec72d949999f8a7cfd7be09c58366cf94ab6c7a8d60a769498d3c1a968e04cdc0cf3288802ed2fd5c5d776b1d5a4a162686c8bdd485265769e8c998b4ca8a34efc720f89714170056c2717a02e8cd6da41aa5f66100518f1421f63a7ed79600122df7b955aba0b5b8c41d133bf69551f57addfe88662ca26992e5acd103def6023fccdedc9dd5353a2b45f26944a79f0ff3fdfe3c89688520d39dfb2382f53050cc5526a1ea6ccef185d7360febc8bfafc80fced014406216a57ca0f213bae3d5e81df23ccf446f396edf4990877c00dee5e9cd7a2231f828f29f114ffc8c031d38f3023205f25f8a3c049bb7c8d3ea93bc13cb5d2b670f52b792b493ca8c96adf4f73e32b3dd85467b240329e99dbf7e0f9dd0153caa195664f76e2b4bf4950e9a1cae054cf1fd14084c6548bc3ad5e2decd3c82477e0d709db071b089af91c0c04d7ce551d7713f30c70c4d6208f65c9c3be88038423d157884f4cc8da92a4840a85239f739647612d8ab884e3b8efbcd313730ca246cc198a414e1af06882266a7e61a36d55e9647eda96f3b3208fd859528226449f332fee39232417b98e0b6f4d09180fb6be842afc28f78aa1d65686e3b31c557f6b43be6e94f61db385a58dc782a1830e95be2966b93a6f0aa043d55625b32a294a7c14d7aae404e7ade11a710b458203510b8222b7989a96bd83e8afaf45b55574e2375178d358b1ecf237852f70b9ef2e107cfada1453842b39b7eaee0819cec1f16742a02247a5994f679f26e66c048155bf565d99066bedc825f20b4d63116e0871b3e7ead8b55854609978673df12318fd950cd4bbe9464cdd095eedb4d21fa66c7ac17776a44d95bdd6307aa12a2d137097defbbfa84f908a3a08ba9366bf2bf40e90e4f224aaf50b4c7af1b8e92c3c1fe0dd395baac1571cd490447f67bf986d7afe6315b716f0e85fb3d61209a4612a5a3dfebc617ca1828e892237e301fbe1a3d55feffb9108fc0c781b57343724c6b8f6742d5ea06f34d93de51176c3a975254c63a454ed75227a40445cded4dc5a79415d74f8f64be47fc0ca359c746f5c73848fcebd40f3c5c7d0986a30adb53e5705f406071e150649575b504af651b07d24c6bbdd609e5b32bf537b9ce51034b124590a316ade16f424f93736b0ca607b7e80c927f007a5b5942777863a41713e2b225a0202441de2ececcb341f12710d07b3649eb57dc2c7dde9765378970f2d1c2d84450537a472d5fcb1499da44e28d530339d6eb0ac7a87d2fdc62304b5ce55275ccc242e61e17ec6abe682ef00f03bf2e7c1e4e113cc83064489680735993c4a951a334c46482e50b0cde0f5ae08c6b3c996b86c169111d87d3ca40c6708e7281a3741e4bd6a049cbb38aebec17bf70f8597f921c486ec19f35730d2d67b1118e63cee8f4dd007c1f6f935b0c7c280c29bd232dbd3e88d6706b8fc13f9c22bf078b11e7351c2062976ef0550197521055a1c66817362dbd4bfb846da46e7d50055c0a0eb7071ea8f60b1dbe79defea5049baadff1022c44b014f5eea34218c114410e0d45bfa3e077f1dd2e17b3fa7bcec514722eb426560c166b6495c2667c367d7891146e78e1c642947302c9096cb1cbb091f4f5549284ff28be717a12c45c5d4dfb4041186df3172cd29ac6fea2e973eb4aa62f0a87cd6a6a75c44e7211acbbcd5186248561ef401572f0f8c480fe0aade858987f13ad6325f41bb52449fc6dcc9788df7f610a8afcfeff42a4f987d89479083178cba92ea25038089bca8070b2029bf63c8dcf019e76c78f9a27cc65bff597fec2489a99871d0cca5d55c2328146ab7709d0a5c915b01c17cc693827de4e910d9dadd4051cdbe90f4ba4e4b2d55887bc47953b45eacfd84dca4b3d30496f7affb5bd55a3bf966e4e66e8cb39efefc4aa98124719bff2c6353ceefd7159c855e45f937e612444b9d82070f45b3a59485eaaeb1b0455dc2bb503d15e95397a2092a96760a78fbff804591fdf969b0821aa96c97dc2ea119c614c91a3ee892c961bc9d4217bbbd6cec990f34b9a3ef71e1b096eac5fa26db8f8e87437f249bfe23356ee832bdbb5849b74c3d4c2cd4efba21c577c45d8502c154a351fde0b73f94e24f4882127df356e8c62824efbc7026422437470c61f41aa18b6574345050bfd7218cb987a9ad0cbcc85cd5f4ae558c231beecf4b431bd80db0d599b56c713ee311761724665a2a9ab243802b10bcf7b4d8f0973a7166dbec7d3dbfd2f5a759210776ed7c3d1ae7317c4f21f5f0145ed65ffe4db16d0bdbc16c59749c12e2b1bdd70640a6621dd1cf6d9bae9774045d6d32d212bee2603fababd87fa509a8acfdaada70f885bcbb948e64672910d68c26c491ccac44e483a53d564f300bd143510c7abd146280a6921fe995b9579e909ac6f2b2fc20db84d1c06c3ac8b1c0c50d957b929e202043b477e2cba0a7556c0c1a0c071950d722fac312a239a3de5f580b5136eb9aaeaaa259b29668e192a9c4038692ef388ef6e6c451e5cfb81675b960719e46cbdb85923af4057490a0b1cc43a78a69f799aff2fd0c65c8336b01975120af61c7d50ad02c60afa28227de75237ba3426a5744e1c8cdb390c8c7ab7ee623cf918332827a719cc4b014093ad22d43b36cb55c005dbc7f45a6df8bd1d5121164f28bd5b5d314f822e059b2640309627645904e66633842c478e5973fe6a100fd19315b5fc48d20ba22a0ab83932733ecaecedf056e6a79d10566193e435e51d64ed102b46d05135826dc8a305ecc50b1d9b2ae871b7e0629c361b7112e46cce1c5b9181d55f293eace9bcdac7184d269cf153ddc8b9e599156aad7fa21442ca5d3f378c405bdf4b98d198967abf1e250b9c19ef07196190a38254e18b1d13a4894a05f8a34c78653cf7b6fb1ce2f11a0f1037269b2b76c8ce80190ce838af73551d39492294f75ed6365dc3a8d03bc21ab88994e9503021a988caf85d3dd8d35c3cd48a1c6fde81389d5fc1eb8173852672406ac57d1fb1c735bf74daf28507fc4bc3caedcee95c9a900c247f5ce55853c5e80cd4e3c624003fe19613b0742590abebf2123217dbfc05714b24169be603a3cc6677e0cc4ee8e72e9eb5b261549b9f98dcd8b25c32e1c2786fb8667d5e6269b9d9bd541f215bfd1259d1f0065eb5d4bc612d41b820666d91d2fa9248c9b7999fce2f7c9865f103908e50675f425daa216ffa74f0793d74ccbd7cfa14ae09cb72348ddb18b4d5728b615394dbfa22c9322c2211cc67897c2786340c9db0ae8dc3b56f747fb7923954ac63f43cf199d33c84515b9feb483d9186aa723f322486b0d6e94da62efb900b943b7f8d457190e0d4faf6a636c10de7765543589321265bc43c6da501acc26496642b6676380d2650e26841a466c5eb3037b3242517404d159404109b61989643ceefb42e4ae410eef52c3755069582f997d449a917314489c0ae0966d7ce598df588b4d14b60e3fd11a3fd5621a585826c4eda6e4a22375ee02d76e6671e47bb3ba8000de1c5d6125ddc4b5fc4dc0031a821167d5000a45d07caff5749cf4f04bb0a86168b3c5302c38aaee9fc784201d367f91d460af392102f79040d51e3326217166256ceb7aa1469184ec0e2c36f2dbed6f9c17a4d4c030a99fb82b3b0c99011cd2c36a33462c9eabee6ee432239eb0993df72d80f061bfc35869365f2d3f7db680baf79487b90011d0133cf4267da3f786e5d90f0a29fda3cf4c8081fc648c2d4438d2e29ea1f8bfdc80ce7b359c0fff9570ce7913316d75798e6b995825ecc0f28c62acb06f3fa31cff4b8a8efd3eee0413a203e384ce552b0ffe5fd3b45a69c8a6317f0a8d01ac5a7f10a62e6135ee2f137976ce251f48201039a8859b235ec7376cfa0053d16653f86136a61554dfcba3188380e5a2c870c6ca6936d30c8f4092b06ddfb33822e39acfb25b348fda587425cca7302e347a8630379c16fa0c7b367f54476c5d788fa12b17a9b0ae6b3c7b70ae110f38acbcdcdbe4bb93f39214e0ea2b182b8ec2a42d3433a2b942f878532b621b32cde20aa626d126df9e743539d268e491c007c1d0e566e2a94ea5d782e5f9e6e8424653f0afd40194cc95f0c713dbb537eb0424357cec89afef798bd4a2a2a7fd148c24c30b3d8ed3a3033bd2aa33f47f4b3ca3116e383651a80f3e6c7ada8077747f987b301f51398b0997bf84cec30d4bb688f87e8a9a340085cc46e5fb0fb253bd61bf91376210e669a9ab95dcda774ec44a2f3dbfd095967fc33576b14a4026f5b52c08508ad83fe6ddc708c9e64c9bb47d9635f18502140e6914164e3aecdd3919321d001953d6ba4dceb1ffd2b9e5d1d89aeee2febe91d4e088f2bf5ffba9fba237837e4248f1c57eeb16442329110446140ef15b5333f11640bd2d4aa955bbd3339dd616df7ba68cebab2a8a35e168612b1ee3166fcdac05bfaaa3cad7234c5adc961cff44959a37257f60d356f9451d00cca63955b6f54d3172f3127a28a439cd4849035acad65f639b599257f415a9b029139462d21c2c68a8c1a2d795fdde43c7fab45906b9c7e8e4bd15a75c79b4f0421782f58c544ec487e043485505338817856abfdbc5a106c3bd627810623c74840da003a91b98502d9dcc934114c7ba73db5ce6604e423702a40a5aea19fafb9b34133707487da5b4c356d2f4592b9897149bd39b598bb17113eebd2a9c62abc419c9ce8b9611ed86b6ddbe67e84c6c04c42bf660445a06d7921f9fb7ebdad6e0452577e9e98de20c169616b5496a7047b8391b42f245cb63eb6ccd3ec59ff7318a7197cda657b452f11798af30dd9826f39abf1208eb9c4bf80d2eb854353468f83f8402716f059561badb9d6d660302867329653879088a772228a468071e7a53bf06d64fc4c645ac9175b5c7a801c9d1eb7a51959fbe1c7c0c32f855594cee7afa73e96554c70e193554376e2380edbe1a8f138421644ed1d39c58c3e276e1656a468f85582ec1a76779b3409f4f93daf1b9941a9171c08e2f82d33f610043d2d06fa77344e4c03d553ad9b84278fd747a69a9eb76c8982cce7e9630d1d994f278db1580bcdb7b2576541fa6450b09d5e59c5f7cfc072151bc22a0ffe32fca283ab14a8f142472cdb371b5e312c2f66835895b50a1ddc5faa1d06b8cac22432d2b686103835bb2cb769c09373a75a00ea3072381fd54ea9dd12e51b4b67d848241cc9398ce17d1292f1ef2e48ddb471d777ac6660217c51e2b5e10d02e1e02d80b984d7b684ed93f91f973ab7b3522b6c0bd29b552f6414f73aa751167aab2e9ed1d4157dd1168692286df427d8f6c3463720d117b166789a9810a5d08c6c866cce67f33e7961d561f7cec610a676a68e30df5215426fcd26fa2d9119e7c81d5b6aee625e21518fd691d4839797323eee345970e81c223bfbeb49a5c1ac4e1c404f89a7955e1dbcf622809a3669bd54adad62390df0351f027151680179a34fbd676d1481ae2c167880e96008b80047307938682f8874f69719d6057be7408985af68aa3f5bd2e3b5e6bd3df4a34f1474a528404d27e3383f55eb29d9c5506d653db66e5fb2254e0e3cb1a7f1fa6d77f94ddd1c58c30ba2defb7923cc789793f764814c1a3939503f5058ec75bf96fecf456a0a770c940adaae444e014a7552b7af744da5b62c1b86c5cd8ac108f3552a14a82ea2a688b89bea5ded07b7e38c7726f728f5fc1e3aaccd1a11664db9bc29818667892f0ececbe33c583c514a44dbb81bcfc060155c705f1cccf437cd99694a9213d30064fec2e33862fdf3cb68249830d92a2ea19a97684a6ee98332ea044221edfb5ea49732392f6f29eba3726efe9bc625f23816d557914000f2a1b6a7da1cc0ebb4b4445a2423550ae038424c4dd0e627fb585bd1f86d19da67a59b8d8530ee903a194d89ef7ccd6edb45fb0a76d7c2e6a05e46f9d2fe0a5030d3a0161273b1f877dbaee6bd6ed0a47aa1a8c5268efcb480f70ede4c3502ff93bc24b7f89c6182a07dea4969ec7cb74e0474625d149a56166304de2b9f0869e68b644188ad7582a6f34222daa4e34117dc1a59b499538bdbc0986fdcf87aa711e3f8357692e2fd01138002c5b84ef5c9e9e670c24ce01b632961042ddc8237b26c29f7e3fe8ebb503bc01258d101d899aa43e13f7b52f6444af46836a34d99ba0ff41c92c9b4740314d7e091b7f0cf8b243b101c0d9af171762f05f96539dfd399ad2c6290bc6878dbccdbe94b96a19edda6fff96d160eeb4c7f0b63280605721d5303c71bae98004ae6d846829683c74e2eedbe7834816563cacbdfed92a05b185ac2936e7ee585326cff616a8d3866c873c8bec5a044c6825880aa160988f6c8a323fbce50cefb357d78a6f0c55fbb3da346b5959085070ac2fb2f6d55f9a23cf417d078612b9326581c55e190f58d275f4b8330b8757a3fa512a87dcd553bcee4c57627d63aa950298dcb3a0b95c23a72dac636494a531609b0bd628f38cd72af3e41b5a084165883ca0642923f59b6a5b16c19d0c9537f609bbc4f2873184f396dc959545be5ac630a5d6a7bc8d2cdb075db2afe2d67275f55cccfbd1b6c6fbe64bd462a40b89b29c997635052671a0a05a0b94a4f4a370c6e951cf9d301b65d7eefcb4949f7de68ce144e5158f11d0c1617e03dca315bfeb1cd66eda218d9a08b14313403c94a2c479c78c31c8b7f59159a4f09397410b00f877ce7a798f5554f21b387be63ac1f07790833e600e3dbba85442e96623c7f69e5f28d1b67ee7d870120cf543a8c551e3a4de9bff79e2bd0ce088eee1970853c3ecc54e72c64af832b578fc9bffbfeb77d7885ba7c781062a3042a1b5f2106df677fd0d4f618445265be8ed1d20dc49d5f30109527f788ba1e2308e981574e392ff3289a39cda1d964200a74b03ef425f46aac67b304c462cf580a5e4e3244901022d3ff90458a3301a2739909d4bb41c668ee932ba5f7e26777d719353be8401b4b0c629abf339b0f1347ee84b02127661b954e5d5c10747f2461f7fc41aa2ab48ed39b949ecf70eeddaf547d2ef82538e15069a4b3bd172731611ec56f6e4001b3ed1db6d8ee000040ffbf624d2e90622921814012caecae678f6b0be8895ce4bc503b97156d523b5987e6c610f6f4fa3b7acef5aed8726c50c596ac18e058c1d28983550bc56a243d15dc27f75621c2b98caebb28befcd59c200db58284bc944d44563793d612d4697341f697411ab48df646e7e59b2717a5825d93e55d5effe0d523a773118d0cd8dd301881da4b2c89e9603cf3326733bc45de9dc61756cb0d16c812bf2a6e527fb23293d3ee56a71c8d1f231eed20e31a764115dea47da3d819072a3fcfcd27d2f4f878bee5daf185e5813417b3c51ad1e1ff68e11ed99ba81c166e53b814ac25a8feec38e689703a588438b6131afa8d48437458d9e9491f76146b1fd0c8f90246b728ce8fa8928539b02a2705868d9b2c716473e4f82c73582d83e6442b9ae7fdd40a89f08ad5cb8f041df9dddefc2c5a5b9876d9f3b0ba2e0be627bdc6c158de3e8bed341dd1a4c0a24977104d6d5c962e7a23d48127d534de0cec6c1c121538c2f67a1adfcb4f376265db5978b6cc42a75b5fce42f0990c8b886b37454dff1f11dc761620bb165441d2e1eb56021a5e8126432e4c25a259fac7f72ae3147c53052ff4d86206ef47a36931209efbad64602841d928056b8514122adaaecf9182479bdba61ee36b6f1f0d8b5af09de25c623cf9e9f1e83e021a96cc4fa6449c58f1daab3c16fc8e7073508020fa49dcdab8ffa29daa07ef0cac2f23c8bd089090a04a34acc2461019fdbd57585fe782397f1bcdcdc165d4641607ea2de413736df2bef5767061b434975304d748f37f709acaf2f0d062a5f6a619dba45f514dec91d080f11392408cdfb3d144d38d1c269027854bc9cf923cac0b5d20287aa5f64e6ab4b6a12844365bc81a900d443f1565c0285d1d39c39f6e7117dac3ed8f823ce09d730ef1629a4e617980df0384904fd5b59b451c9b1736c9f86d2171aa2d23a8bbd1a17760b4f04be4c78f592a28ae0f933f73748f4659dc3db6d07141ad05af53f82540bc1a49456fac1289854d16aecdb57d55e0400e8641a504043b775a230a5e5b02996a8199b2be852071fe4021d287055ec8bc0a0fd7649520b96f788e5d03e5605f3af882fce5471e6b155023a5cb5965ec12e53e8627d9da5408b70a8b2f29a657af1935b39bceff435fd1c65e82b475744ddd0cbb87049e0b2d15dcd52f5e5527bc154ecee845698120c19280c77bbaf756c621326ddd186d1f07a3b7dfec034dd1c93d1ba70836988ccb73a33f66fc6e9e36314d56b83f42f9fb29004d96cd078a8a46c8bc7ac4d198038a93fbc577c84d6dac76ce8e048ea4ef7465f912ad05b958d25af5e32f06d9feeeb9bfcf5ba2bec55606ad3c3170c5dd5ad622dd0fb1f14308a8d2f3c86e61e7fca0a21229a84fb2917c2a5efacb9681f48b74615735a9105482b2e73821574013738570ce69818a8e82da13ed39a0e5a7a266205b200fae19344dba6c22e376abf55ba039f169372a9358ceab3a6b570fe2fc4d9b08959bd57dadf88b38eef8cb9d86ea77a1ce33d0b32036c769c1e1690cefa4c2bb9b05d603b105eb86f5b226addce88ec63f8ec1a7d0590436946658d3a5a5bd978d86a06c673e00bf630b6f1e42227bb959dcfbea42dbf2cf1bd69373334b1946d691bad5ebf1c84cc4b486191d4a719f32f7dd5709986d05d8f4ebe5aa31a1883bda5f8168f927e2ffa95c89755713fbd4a2515e64e18ed0400f14f8c8ccdb10651648697108e932a32d064654ecf7bf6fd2dce5dc913016d0aea229788a8fe2e751b55e516285ab1dabd8edcb55e6c72fa1371f830b76342ec3e52e733a09c37ca53efbb6b3ffd2141f48a105a1e5d722e913c6de8afdbefb8a59967b1774dca1c7e436d25ce48066bb2787d435a334b298b6f21d2ed4edbe58af0947ef080f9295f7764715b1d9a2b0d3968269f224318c359b4f2d5663d01e235f27cb3920690ddd8b49c7e6309272fa73131e0e2b0302628854e4a20621e793a4b08120cf0ae623075db28768b2be308d874ef7ed18b5448f5f5b606e0284b1ba5e74c90e8e1539b943484fd2a022e7986b8615ecfa4343f8c48e699c372286967afd626359841f467dd3579a667fa71d8d811ba03dda26388a0486b7af1dcf29a81c6a38b07efdcba2d0b57d2805baafad5b1a27fefe5ad0afa0d70560714972026f4835fd4c93c4838b594860ed7b88bb64a17e402428fb1ef77515a3aaceaaa585232c08e00ac6bed06504577da9d5b639678c1e2ae40c0e427b8fd97a49315eb0e4ecdfb6545e4a04528828ac5d7a9ca5dc0d15af371093ce18a7522ab36384a752e335753636e8890c95afe5bdb9806e7d96582b1025c1605c3333129bf3673c3d7afb881f389ce23773f514c35f0f18ec220c86fe751ebeb1a76093135ddb446dd9afca7f6f525723a22bd19c13604d297431a94af2c4a17d62c6d738a74f072c87b8250962ec34578fff041e8e766359711deba7578b4a9bfb41589b6d93a5c925e87e0444c536c671ffa14f8297744699c77d53372ec0370b952937dc81528b42d2c65cdb8259398afbf9b508a61c8eba3caf148cb9795e0058a6d6a713b367cdae9039e08b1b1c777d762d0018945343ab7bce04ed4dc0b060b5d8edfb7bd77d07f03e0457daf02195ea54a4c81e720f6c457d0687c4d8b31d0db049096126b88c1bab581b92f85c5330d1b8e959cc493e9669789795183b6ec5668a1bc775c23f13db774da0848b109ae94b215662a277b7b5304b09d7edb989af8e324f8b893e68c57be0c184fc087e190c9376b5f0648cef05cede21d06ba4d79ca9c5e26ebe598249674cf4cfa2ac4d4a1800a4470298c8286a96e48ce0904a7cc9a47b6fa6ec81bd6b31587787386aeaa45a6c2fae80247687eb91703060cda0a0e2782feb0ee98b00b20bdbd1961a52a68101737b433f35c8a3378ec0b37122f9b9408a8c10c16147ea0dd0eb846c64ca790edc596ea1aaa5bc9e6754964cd6298234d84aa38bc73b214ed0564fbf092cbdeb4c39052751b275f72cb50cfa6c0e3f5a2af43b145752ab28f0a80ef13b7fc82f1cb7a0c38e8896bec98c6db01317bd983b0ae1dc736c586dc3e606e3cdfd49353648423efb1f105e4c85e75f22a61dd471714e8c0c6ae13a0ed16cbb0a4e545ad539e87b14b8bd07af77b3ec8f904ed7adc53c55fd4d5fa02a5dc0fb459c0691f7775994e5d83f95d0143aee569c6365771fdbabe1633584f44e06fc3babbdf1a3e09fb3ea0df7942a9489168b67a5f9e6171beeef3fadbf013602ff2baea850ee540db44cfd4636350d7f3dd014092ecda7ee6850fc20fa1dff1a7076479d3a441578ffcb4d4d8e915c58f743973b9ec0f32bbe02bc4775a57fb7cc4b1b4744281ac3094c124d5fed62f3b8aebd3efa281bb860833528438ea7204597c687f771b08198b9bd6c0fa276edb20216fc79c730fe0a8c350fa0863bec0848ef2915fb4ef945f9f17c8773cce7672c0197d7d3eb8053a94b303f526ede9b8cbaa33508155f4c0b684f594e4097c0041b50dda30b9bf54ae188fc87398ab60deaf166dc686c7c4157b67773cd4069464f364b76af81f9d8403b59133a95550db28643131b1f510d88efebc4c3053be51cfcbcc05f6057929fb6fb7d0346862478ab28ee532aeb089c506521f2ec2907d11a9e8e0f3fb82730cb82315e29adc987c069ebffc69a0b4fbe0964b8ae6a7a36b0419d38bf7cbe2e1818c7916fdfc0decb761025d846594fad0bd80db5e3868e3ca68726fe40582163a9cc34f76a95225ad7f1dcdf461b96080bd9e2a4ca35554b3becfe086054a42597cbb8de091fe2f27d7a69e711da5ff7b52d0bc32ba1f75c376d0e32304a4c41e43d0375abf3920f2a73a239cdcf5b3d0322123590908598a9a4b55040f79c1e42449e5572bc65a03c2362dfa390ca69ebc30f2765c9c873f4087ab3d168930bf8e1069fb6c5191dba966816cdd7fea9b6e72711203987959b336897f993d13ead5f7178754aeaa0500054cbee34a3ac9cfe922e3ea02428adebfb53f3450f41f0772e86359e6deb5c8166965660343e269ab968bbdb8863090839dd18743cb350ab0448fe18d07e3bb04ef51c98f8a5717eb16b0477bb618af2512c6617df6426029d94b0d5b3f14af62000176e2846a9810a7862e4ff2af9214a9084dd4d57cf2782e0103c8ecce5a63d6894d08564070c0d62f68e75dc3099160738f8e302b8f17c280fed11c220fa2372bc172f0cf3b8189c093056d77ff490b5d6e6dd13d76b2907d3e3770d13d96e8f62c31d49c213139cc369b1c35f7941e2b2f038dc4afd1d4c8ab8bdcf93adaca21630fe4301202b062c6b4dd25977bd95b95a1d0e3b1e22cd5bf05c18204935a44a605b8be37d3c30c786979a825cb5d10df642061eddd56cd632bb243647721c5a8d1100012c7a14f4aed7c464b3699c224393820d7c213021597a527ab533be6ade80458e9903f20817619ccd2714fac67d19e77484bbe79adf403ef7d25548805d000cbcc3b1d575c35151136d9fc41ad0c38e67091f04c977ab10051433350631b3a4f6a1dd87b8a77c95d922ae885eb7cb6e51a47af1e0e66447b33d776307df16e5416c5232b05bb8ffcbf5e22defa3c0332368554519773ae2cb7603f4b7f953a4f06613803f14a53c14ac08c15eda269c9b3b76acdad5b78144cb7beba0707d8d07996da56c750da5904dff939c6e253e378989bcb102512122e45f1d8c358bd3368ad3466698a3a5444f99d0d75f7ca6e9f0f8606c9da0598f315a27699f81d786a935f019992371b1b74b532d49fd147ef4514453d9a64ad3e84037cd1f0f227b09fcaddd257284166af937c9ac126bc7315dbbeaa7f8773ed1a1fbdb1f9688613b677854a595b9494453db01fcd43ee03befc7291661368b5e2c275869a41d806388c26f59cd69f8d8637f583682df0cd9a758966dad64019bb1f510266c8f492d239221ef1ba8a642b98d7de9b85758fb1ef996c51ae0efd36107ba127ce88fe9292b639031670625c318eefd69b2e7ec1325cbe81ef18a171ee284b353d5741c2d3b2bbd28113ec958f77e99de982c0a5999442b99d61e07120f171d0d40303620af6d3ffcffb675fcc53ea97fced012b17b92fbc8deac6a55750028f776416489b090ec9c59aaefdd85954deb2ebff8dc6226852ba20a011dc2edceb970472a8ed1e4f62de204cc003dfb862d4d37514158afb7bfbc13fd5d0544ac90eedad92d35242fff847dd96a4e2a8e46c586d19d79641b6acd6c0970f49dec314cff36a32e02204c00b6fc22581f44adaa9839246603c3b5da5ce14fe44738b8e5ffef88c36030ec82421a5da273634df99ec640050ee143bee26603029f05db3e67928623a132705c5b2cc1565929542238082547e1b9aa2abb33eba32f98617ec75eef55008c1e9834faccbef2ec98bad1aa3911e0f98d402b6c601590dcc0931caaa83b254eb2376860d6b15d61871a0ff0ba2296901b4ad67772f6a81a2eb3bc00d8126f79c689770ea7b04ff8898f116a6a0d39942826e2ec5298a3950739952512a2cca5819ebf389059aecd3a0fcfde13e1ac9a71c60ec05c782ee6ff7564c492971f8c1509639064d86568d4ab6c6f68ba5f72fd0e9a462342b42157a5c21588dcfdea2a8f6c9bf6b0e8d2e235b5318b5093ab7d8b232690695939bc7149ed4c0c3253847eeb04b245c517c8ac605840193eabc56165027a24964b3dfb2d9f2d5bed63a5778c59ddb66ffc5c8d09b29e93588d305d272041a34a418474268e643cea67a9b71ad0b60228e8977c63194ec6ce7d3b327d290d8dd3bf0d6a90ff09a2f7e61355298d57e21d4cade827013c0bf0169a3ad9c1419eba284cf7fe52870d30904095db06935d0d6d231d0cbb5d5a8b7b74ed4cad1f512442c2dd828929944f2f6122c7d9aeae5a965a93ae425dac87f1ee249592b9f37e29f9b843670a026106613ea8a49b86613dec9a860510a6a9ede191b4992f3a081b47fc87c8d78247752744fbf72f4fad70b848671525231825934b40a6af9f0fc21d79ada6161a04dfa5ea734ef6b9575d2e375fbf23eda9f1c510f41f13e70e627d63bdbe752312f0383b9efab711388c4906cfe54a49b461232115791e58911856d0f803c06f6c4288adbe24604f568742ec6dfa13842bea9dffa3e15a72aa1c78d9b3e74a80d2cd7f2843a628032752300baa3de52b0efa0a1320fbbb5731329fe0176c943c1d4db146af5c3a578c8efbec428d086e162ab2967886342c34b5fa963c6cc64b4bdfe142b16d83976e42dbcf5493b8b2e62bbaaa669320dc1738c988864bbe6f2dc2b1cd400237f880ccffc780ba0b0e3cd7df7f32ce0406bc0d98ab0b83d214d75e6981648194fddc58d9672e7d4694f011b6a52a75e1d43bf2cd1eb5a758a45a06100e548cdf1a461df3374f043a50fa95dc7b95a169e46526441dd437dcd7e6c0c56e341d661675e357c51733807ad5930e8252f4c5c354c11445d1ff77043d8c20cda66b118d3f1f9e641578858d6dfe02426d98644c7619e70a7a54526ce7d2766db28e439d064269a3bb31d74600140d0fe70d5d043350d4315a4c3b2b1aa94dfeea0517a4e446d4808ca8cb1c0ec4a550a1472ca860672900b819d1a71b3640e6eaab2d4b2c4f38c1fd86748a01c49d049de5edc104574086906913cd2293f318f13f1bef26d15a612635f3d778d55042616c5382d4337724f63c98b4ad273d254de8d009b65b034ea04d20fd555c43fc785c0f4ce29fb562da7df33366ef6ad8912324c7f98ab5aef9c47220ca693a78f4c95e90296f3ab354cd95134fdb5542e99600ea1c9ad80eea56f37ed45a28921efa2c7b618cb05b9fcae77e2c5954d328114dbbd254d000600ab6856aeebf5c8d4e11f2bc888999c8290e20682aacd274397933e348f48dd2019918fd5a53cc4dfd53509ec57ff52d25183a632a93180cd85206090cdf63125fcc9e185e623f4bcd2ce2ec12b8644176665c1a05e45df5b2e9753d1e644539272ce914d2bc2f0ef2c391e259a919057e609db9a499c46e0f7e89e1c08797eed47fe86f25d111374c201f7e89d55b123c77ebb912ff79b3f3a82e59e9b98edf251905849e19f778fc7312a8a0f0b6712085f69b7a2909ea2fe56ce6f88bdefbef6bd3d721e8392942181ec97a01c8c1c4e8c77093eb60b723e6a092ddc503e26921d06bf38ab364475bd72a13d22d3aa7e432dce5128904d1485fe1433958659672640f3f8a4c11ae6eaa13819518fc9632dd56f74f8aafff433f71ebba31a7a3ef0206d4169301d3da392b0d427a9309305672c8f90e1be5246c69e196faaa4d26abaf32e3f55b4f3cf96b8d40b982986457c43f166356fe0ca45ee1eb93c60bed4d97058e9dace97c2f2fa8a20f755dd505be99a09e3afb4d17ad640a8d65e52fecfd155952d2233043b281f289a22ef1481a85f573d85f5550e116108c8ae04156e9a2fd57e84fc4350618856615325d442e489455abe2d1f9078cb88f5bd4be4eab5fc51e20f772140b1d511e0c394c96968be8545ead05b4e8a216db124fdd7b68baad58bc9fc35c0156d32e2076af524048c7457fe6fa0a494b3182c9ad7e9617ca3edd9fb36fe2a7b2233afbe73386f5be543f49532070501a6174f3b9d10b1c67cbf693b89ff58ceb08115385cf687eaa03bc6b12c6ee54737d01a8faba1c9f52eae05f1b353ac866c52314a3886960f76787e8e99a28ab24dbc221fabe8de4a919c1a0fbb26b0181ac4635881d45f53448875db3c8358b57c7ad89cdf9bbb3c4ad86b581317a970a84eaed064389b1c618f8a2a79f654999d352dde5866d7a32f5e6a8966ee774b118084f5347ab76ed3127cca9a019f9a394fbafe2fcb5afc40214ebbc2ebc9fef58cde9cd8cd5ecad8e9719af4939c7bc2a2552038404013162c98aaae4dd47950e107ba310426f994906862e12b47fa6f7b79b4663ce6dc3f939a1aa0ce2efc88b7508198592edacb672303a582c73e7a1aeee4b7608bf0acaf60acda058d92408a01d3fb257b47907276a5a6c82b04a582d5a027de7c992398bf69e1c80be6e7f7b9039e0142b0edf6190486fdd794c7c7dbccd8da54165ac3fa709e01228de9be596997ecd5d326307ea6c0fa4d19163bab548cd4cd865aa3a94613559b0835efe609a60b4f3ea8183a266147070c2e983efaec1fec72ef4b1b661d7a24452c9b023e541ec6f98f675e11ff3ef94b8f20b7c720c9931a85be2a0642474404b249d756b3064f81fcb7779a524162e2e4e4f1be56a37063fffea25a3a4b77058913a437ec04bc9996ee7b18a4abe699f24c42c30ea66ca8e4f20f4135efe70c839a101205bfe7337ec6d5ca525b2536a8ab5b8d80ad4f5742ee9c1547c8ac4179661516922e090aafe21d0d5b2934fa57e410620968f002d4314ee24355250ce7b4f0a7842913a72212207322f1f3be5cc870e47742f5bb684ee5a6b4bf9e83e5bd40f879b87161b14a4291381b346387a90aff194541ef714aa51417d61791e00f2d35391caffe1bcee0387eef3e6a644578b8a8206f6aff7bff52672ce61710104b86ffab4325d960eb0f4ab156cb1ac800529e56141d5d59667c977bfb6d49eaa4293679463ba93e515b4bc06f8f991383cc4c7482ef60b4d5a9285fc8ffeedd1a30dbae296532c8b31dd5d033cacea55fb55f5ebd30be898c0f66f237d1edadd659dce4ebda3099beeee4c7903ad6f448ffcc9092ff42682b6bd778194f957ede46c40c703766f49b9f92600515d392c3360f334f5e28b7df8d251f50df0e19f3b116baf8e0e347db2aaf52f371f149cd1da55c725968b4efd42e58537f71e74d03a05feb5adedf709bcaf13e4c7c0760afda28ad362a85fb5345d47b50df58e79c57a19d818c0abed7c745c8a814df538895e5e0abf8a05988609ab7bccd4bda9900365a207855c607ebf8fb3e813bf7716e1eb7bb2fb9e004c4a0f657cc100a6e84f71e557d5fae44dadfa6e2b0275360356e7ff80d1efb8a6ef3f32e1b7a6d0ac42b7e9f7a47dc0363d8f5bdaae1b92eb7d58d0b7b32361ed5796233e1af2daad29be5c17fa78550af7dcf3d22f1b40e51d135fcdb19f0bc73d3f1e144de0ab84b0c90a838a4f19c29bf85d3a91a0790825b8f06f83d628fa02552e33352359284e386974b9807151b62ad7eff0d8e8c1c28d68fe7fd6ab620fb1f30d29a0e2da931696f1b29c1a1701de9073d0b86d55bfabefb6c3bdf1e715f820b9889dc7081c469ed4598b512ca64ed9cbb185ff9b20d6292e8b9d9754d10d0102b1f67dcf4224aea5f57cf314bf9c1e95a962cea25e589bbfe04628545b271bed0389fac8458f7347d6418e1ee36b3f174c19a0e4765b4a1e43f03704a8ee717759c66f24c41f502bbab0961b2129144480c1b42f63eaa651052547a37ddec1e970c85fe5b01c3ea363ec6d2cc89ae4f5391691ae357ba5a86693d134ca467d9d53d0b4f27e3ef140e1d22aceb99ec9855f1506973ec05ab37395d0606289d5b2a3c8e31b206e8458611f2bce01f56d808f93508e0ec754945d429ac4a63d7e00540b793f97a726a0e3b04270b704187e6b57b6b71de2930afc22f54811e0f7fe2c557cca5534cb040ef54b8c6bdcb607cd0a9d3661a7c7e84c72c5c892ac09fa72a216c60089b746a00db47db043e91132d7af9ddcf11c735348f3ae62ad8f8588ac6340d2e1f5c4558089aa67d8414d61aa018f01eb91c69710b0ea082d33b2ca99cc48059d767e5714f21c612dd870ca79d6da64775988334f52ea1b95abf61be3983ca6dea6deff153019088642970a539ec0b91e5f40b3df758f5831f6435d48ebe7238d5e45bc01837f1f7a5972c7ea8b4a0bb4cdafd6da1e025ddbf857ba6e9d3d11a4026c5da04109ebaf047331b278440434b00b7bd0afe12a1a3f208df238dd3a75d53a11e992045911aea37c54588b864ce6beba55ee80570edba9e8912c82cee0d3d1851784bcf21cbd0bc96f0bcc15d9ec2b8b22d87c5236a96fd4a42b921384567b1218530c4f0c04f81ff84879e622fff849b21e481473301c8b515b3ed6200163304f50c8dd455f14de11c49a03547bc1474f41922aa1c30fd61f7608184272cf731b0f41a0f0c9107d8ae0d5f1c75072007074c65ab2e8f22f75e58884b50c453f3dfc7745779e8b9c0e3f9bb1585c7d94beb1f68075a10624573dc36821015bd4aa93db0f381641bc71c489a319de912cb6bf6f7ad37a51d888dcbbcac8821d18899b47c61d25d7c26a96aa03f188d130aac89c639692b999a6c904763bd064bc6501fdf198f80709ef1f59c936eaa3dfda4ebe72e6e85d8f87d7b75de8788cb0860bd9390eb873c5bd67932bf10b16490506c280692f9e246bbece07f002c9aea339ee1047d407b8d8e0a4fc6c69dbfca87c290f7165072a57cfe17e38f75f3252d4d75392560eb2d5f3646c1eabecb4dbf5738711c1ce953eb5ccb693ea461c07cad0237a429cda37a7039410c01a57f1e998503cf6b4fc6b83a17ba8acb11e6f57e93e22c1c28fc5afa8016fc407a7a517022388ba60f39cdab5b4548d7ac841f3d212283c824c70dac3fb1992b1441089965d16f297eff28e04980d4f900e2594d08cb8c2f99afef0e448d4bed8bbc7085be9c0886ae61f6897341e4e324763eb85cfa14582b2620547dfee3a6b5448fb1b127d04fbd5c1a485bc5aebf467eb80254fe8d4fbb95229afe7654b8045bb9893dd03254a12153ad820b88c6ad8429c5ccea84e870010c5dd165dee8094e87e714b2016be066ef547c63712e2c3af91f44edaa0a9c15548675cefbf281cf242ca3fd25cf3a99f6b4eddf75e0d545ce6e23d7dc392fdcb2953ab6411f3827d225a4c534d7037ba03e388d08db20a50805c22739ca42d0f8cafbcc3c83976e3840d93c0c9a938a3d284225862d224a2698c63f525e6ea727f71d5d0fc5777d30144454da305940bb87be07265037c87a322b8755d904eb85017b7dbab16a62ee40c46ce0a925fdeec8a3133bfad03dd9564bf298f39e8f676445b6ae12968b47c0c1d7c77d9492c09a47b22de971684d58772186175c36bd3738b9c67d4aee7d02d154bdaaef65bd084a054b16c5b7fa20064e1a8eea5b9991e9e77fa4ad3aab6916ad9db8db17d96d27f78173fc7e5c73d26f6d893f55490b5ccd1399d507e6f6c5c5f021f5b02b3512e7dcc876dee71f09b044a89bd0cb446867cf5c24df57b7f3486aad5f47fcf86d2fa1559c689d989a68bea80d567abdcaeb9200d67e6e6521ff2751c8469db48280984ad16a72aa5d92514d5c8351265b3c0c0ee32892b47286e64139a719d830c85b48eb016ec258e2b201ba499873611847a250c99b5e19f0aa11e353ec7d19f1c256ae08dc2932e1f03293fae2859cfabe89bffcee07416802722bd11d0ee255894aa497fbb196a3ce75094d3badf7d8813c4e737247c174f43f974099ebf5650d300773823290317867ce241cf6b75b5fd6aeaf582c8148bc0c3249332b3b4acb347458a103e2cdc2952b016a4861853a66423bd4aac80bc8d3d782aa208dda13dd8a7a7e9eda1893a95191bde647161729026c9222a6d616ca7a5f065e50cd4cd12dd67ee4233fda758f58651bf73112e76ba94b8822a83e9a5e3c34d0a66ffd1789ead204ee2cf0b1d3c126db7bdda373da2bd49cc5f1da6623811d83d77bf87e671a48bb4bc02ee4b203201fcd45c2612573f5de88474c7b78ed2b31b3bd290639218d8a2c440150b6691b5254f1860df232d63cd0646936d12f1620183d6547947fe149e32243b0f1fe992fc115427cc576f9c575386c4ef4b65994028912fe0b66cdca976dd4480b80a56b49c04e666d623bb975abd66c8f05eea38b5dd2ef1b4dc1a0915eccbc797e16b66e8bf22da308fcac6ceb7a030789114437e200897a2af5b6ae21a2cba84e696a56413e512d1712bda3ade9ceb6759ec057ec78c3f3b20319e4f01256eeda09da593a88ead55af3711562bc03bb2ead7583377ed741cb42f8fcbc0236eafc75fe07e355317a5493c72303bf5e4b4d7c53de9677de6b083c279f516d3f919886b09c472a1a1be0cf5c12e85038ae7ffb345366249d32daad5c009d24c66c05f063f4e11e8861070215656fcd3a063e4e64bf31013917e47f070125d6cc5fee5b7265a8553b22af9c3329e56d390d4a3cad3ee240f8888eccc1cd381bce40fda60c7242fdb1714c81cdd410629da24a792fa0bfa9842158e198abee4a07741a7e79be8a97162ff6df5c76952d37140c8634efb117bdbb0c476c48a2f0b421acb3731f80a0169fce49c271673e7881d710022d0e7fca7f44e4271f0fdbaa78a0feed7527f144409dd59f5ea450ad4f59623762e7b28d0f71f59c96f8002a1c1698750dc2fbd34e128e33c15189ed142074d530d5b5a9bf8e540085a186ac1c0fa015a2d731f0e52e19c3745e9522c2e814661d34a034ef51b11af45cd0eeaf1c55a9aac9ac99b1fed9a8a2bde33ea4c9bddd3ecb7f02bca6c1121acb6b6b54eef19c0b088bd9047cfc783883da027c0b05aff510fa40dfe2a51fc409c70aa69d53331a95fe2bf95d12a9925bb10b8636d97f99fcfd32ef4e54ace2a403b6a61b322a99561faf4c6a09855630b8b67d863ae37aa5e3d0b0807be34fb7526513ccbecdca08c60c364c2cb1d3bcdd61f7a08a200b433a8159f9ee11e25655cd03141723795a42f9568e682f105d5dfd29568c7a69e015d02be3351c38d74fdac44e365de176a1925afc7413954898c5652b50a1faa5242dbfeb79d73feddf0b86b613a34ef90b17c3337ea40432abc176786eaeaefb10148fdeb3180c11da7ba157709a6bfa7a96cf91f3f336300f33567572999b4743159483dd54c0b7dba560b0af48a70c3713fa3561535c53a6e160370066b35fdddae9442d8105c027766bf5d0081ef93e4e06632802b79c02c09609d79b8ffd7a2e94e5c576c883e5a8dc8f42a4ced5fd08c87b78cb9949e6eb9737812ce55940116f9c6170fd08ededa8228734a804222539c599043008dc037d6d088e79d8c082bd6ffdc438dfb896fed76ebbd7c9a970f8798d0c650dd23bab002d716ff3271f33e53e249a56ede18d747689a418621b916692f6c10ede957d9e3d83a3abf814e5d69df2e1ed450d47102c54a10e0d0ebe4cec30385111aaed928f816c6731210e004424d2a8e71af106d03642d9437496999e86e768409f853854f7ff0b82cb73e799bbf754d527dd71ec64f5f4ff81dbfa2354f7e549c5dbf65fd4a9ce0072a5890b0a6a4400647111bdc2d55e03c3d58d4eda66a5bdf8603e44c430d94947da07ea5601d9d211bf9d3efa5ef19ea68fa7b6488d90e6e2277013fd987fa82eeb4713e9ffa3baae225314589c60a1389af1fcaa6588e9ed99f222cddffe26c8e2c5a6f4a1f909be0ad760e901c05bd6a5e4223a060f9ac8a3dd37439c1eeec8f60a8374408000cac8bfed9cb4491bef51090fa5003b4cabc7bf81cddca71b3e37fc9eaf5bac188bab5bb7f47f7f29fd698a89cc3d5095d378eef28da3ae66daf1838583449d1146a98e26f0e0ed8aec57e38e942502f803a033eaed4bcc9128c42bf5a4ef9f34ed0e4e72d9e28b620ffbbfe5bfd19285e85ecb34fa8c27e94daf66d8dae241a19f4927486cfebcb5e4ef21c0956437688cfc889dca52e4bbc542d1fe449a337165dd26cf39239d24a580df0583671cdc2030deaa99ce6ad97fc31d75decbf98374b3c06d426342944d6d783f4a11ab181f97ea3811ead021273dbe147fcfb533b19bf2569457b25eacf480c23836737d376f9f8d382ccb9ad31f691fc6b7f5e2debe2863fa120225b9990248a962301b6762f26662bf8afa725dbb3b455a9e5786887ad85f1805168cbc04422cbd48f0e88e51d160070e02b3135f078c6c9e486066297aaf8c258bd5d2ca7335a10b1611c180f492cf45428430c5b9d33550006ce1146c605522d3229022b214ee24e5511abb25c99acd95eae2e03b91de9eba34f5fdbc55aa2b62f535e9154394cd8f89c7c50b9b035322ca22e76f1de0f16c84eaf9b4a81d1a5920aef2b21e2fb26a917f6487196aebbe9ff36d028d26f2a5e2617a4793d577909176d92e1bb75fa2b80c1692994a37d41acff9434c937acb865a18fa0f4a375f9a0fea1b8cc7a11834c2bb5906f4deed7be9b45af870484ebe95d9734be8d17be68679e0526169201c99275781ce7f924e314428d2bb6ce8058711dac1353399f34b73c1c9630155d331f35a881808caa97a634350e9454c45f2fe0c32d3813a66f4fb86a1f3a9036580079b7d105aafbcbb25ece3baf5a0c8496ff95af5b818447f5a9a92bc45c137e475e23a0c0dadf967edec262577425a4853241f19390285f6d8ee6055a9469f07368cd5ce803711b10f32ac4180fe1af17d994e8e9e2e971660b1b24fbbc894fc8c7b7073b292c2e38ca5d2afe7197d9d9e41360894d9f5f285627fc12012759974fc5eacc2f9ac4b5875d6ec18d3005aa9fb4e9ce0f3e3ea9ed292cec4cf63bfcb758843dfc0870de3a4a14ff0f8cef85c2235be6fa7889ad2ceee10cfdff8e82e5f7edcb6c6997d81ad1bcb6947991b4b4696564f5644992390f0e761e4f81e8c016ade5284646c3d8f4510e8f1bb93fd371d13dabd58ce4f5f40b09eed8d407506460b95896fa2029bffe63c35a12f2820fedb4390f88a18c31a84e2122dbdd8eb45607256740a6c851b5edca9969c56b0fe651b0a6efea2d733914c60f4d1ac0286a013a159715e898a1d7a9cef975a1c9260d742c832e3a177e94c37c0fdb2619b0cc05a358df89048edf77033a3bb4b88a2ec47858c10c847f074e56fe48e24259a81e71b8a4e6f10c74ad4e56a60f6ed2157441c0b109cc6f406a8b4013b709c8b0d12c4d61a48e370b1c18c7dbb04556c9554ff49654966420e493d1ec2538189d6071b22af17a7212a9888d8c75cc347ba5322a3b333c476643c69eb0a4ef607a6cfba7fee2eff0da47620879f1540a85ba84bc34172f250da3d4fc754328d0197521024b4dda1a052b7e51ca84dd0ef4d5b6eed64c26a3f4841be561e484025cd14e59dbd2b4893313a527fcf37622303d11e8b213e18b1a77a81fd05ea56bc48bbb194ebb6581befe460a976eb09bf14f5fab212e39e176c4bd82d90bd71787ed82d044046456a7ee232600676f632480e5f4e058aadf4c9272ef7a84a10763bea8950a32cce6bdde9536ff3bc30620a227319f5eb8afafe3ff0f2942e6337335a6e72c43313dad15c0d4ad08da74ccfff8ebf97266d93f43972b4f0b42c5c3f64c69fb81181aae54810675aaff6b7899de9b1a0ab03fef70f04b3ddc159a212f4e6cfcf083cd472ccedcfbf08dfe96890d054664587926177cc804df924961ce400df126073d7c9357716dcbdc600e1b436d958b1cd1b0f25a57f877861da2e0b14eda815baf009fe1e49f8ee76986f4d262a2dcd18b6d0d0971424651beafcad484cadbd8850f0b0d1b3bbcde1f666209c7a113105c791d05cb8100b5e5458e4f46970b52528a65127230cf57c3bb288767313ca4de5ee583b8d905843c85c665c5c22ae821b68b60a4387acc4be0ef5ef1879de7d41767beadbbb32423ba09f4713782d442fc2ebcb0e74d4d9f3d308cda27f301c1353b653ee89a02d419a2887ccf7a6553befa513bd0f2d666ab7e6e54a66211df5d73365ac1950de20d91858b2d7d7390074ee08a88f930cc87ba0f9dee03263e0764ec428ee4ea3246e0053a1688d3fc24a0d12b5792506e7e1c6d8a5cd7e3692b54eb2fec64c5b08362cf98d17a1cbdc57d454ff4906050f7dd6fb1da1d047a5195cc16bcb3adcb95a94f3a0d148055120bd9e713c765c336064711b90c78871f532d6eaa9a61a5c1c4cd3a1407fa4021295c57ea2c3511c07347afc0a0984f624e6b7642893002e2e4c54db18a8b1670a9012a9857658377f09249e1879035de77642ed8bae95079ba11de543c09ae699d494db8fa5079621d8522a19c9d7c5dc4009835be34d3fedcfe9e3dc00934cc29f1334d13a8a5cf64539f12ce61b5e7c5057319ee4a70005b988534420393f39a49d229cf937b553da956c4a1670d5d07fa7d934b8869c52da0d408e3d655ffaa374238c306940401fefffdb9be388580018102ec25cebc7f139cdbf61c9046aabbd301c49fa0ae2d3cf05b3612985efb26e3c0ea8f9357bfa77d8fcba887f8ae44919efbd04e94725b65e766fdc7f6e96090702135c13430b7379a12274890130596de7fb88b947b490f19bc136a686f79a4299caf6e578331f0a3d461f31789ea2ccd48fcec7abfb31abef0fcb7e35157229db9afeb89b694ddfa1afa1fb2b9530dd1f6c7e5eec64c916ca8b9cb1b62db1d9244f26284388cd2ff0653cf6fa4d8c889b215185c53b30ecf97992f613702c113ff20115b8a5dca2e08dc36f95130422cfee0066510c90a6f1d45f736b77188c6b3a9750764dc4a397fa242b2a17431e91cb83c64b32c10efac9c1166bb23d2be130b85ff69954e4de236a45830cfe670507b1cc663b42fe5d1d7a8342508c30f6c4a85301cf14767ebc4855831f06cf32c58d426f31cb283f979745d76d43651043888e83d6f7a76cf6d7d6aaaa8ecdb73f019a5868d02f19076c13033b07b0273d8dc54ea1c7f58a8b704b48867a89a2e5b4d72b00bcad75e87930d7764dc796aac6c950fed774fff9c3251451f90737afb4dd1ad33d7a68081e7721acce4ba8fe14ff0d5b4b0cc6df26d04374d012c0e416244e451f81dd0eae878779c2f4af64697f42373f8290da11b861b8c8ad4b42a72b78efa7ea9fedf7a0d7b71012d6cc2242664f7094a5ce3f91f42c9ecd4c282d0a83241b105061f1159cfd92ea3a72ec53d38c374798a3048024000d944fdf9974f45f5fed78661a838d88a5545caeb571f4986d2dabda77b19ffc6bd439944d02e9a8dff30a1f5c98cf90aec7ee40865f66ed2df98f20c25e52785646577f285bb9fafe429df49972f39ddf82b2e65650ceb94f1c1f5d6f0a92a4385d188ba32b974cdd35f06c6d0238e6e2d0d8dd2db556c46983f5e2099a2632eeab45e2b31c9b7c2424013cd46031244a47b1ef459d4958601d18e8c0ebb5b847be68ff93609f9494bd5bb1972c8e6106b95a522dfe7e823315d0bf6fbc310f6907bc217b17dfe2b1409ab6ae01ea6cc4c83b2a40f1c490ad1dd755fa13bdbb91ef44c5b63bde9175bc28003176bf9f45797a19c0bcaa79914812f527c5928ec9ae0f33bc7c167445d49de074a7d6ddb61dc08fda200ba5d35eafe103cdbfd910e1826d9e149e66058adcc412a28e7fc1f7719c0a6753f0d0e2b258a230269c1ea680086836bca6dfe4dfd8aca62bb566c5a00f079d3141e4bf3d6bd296b5653abb5fd0401d8303be1da2d58a640b942b9bcb4819a535a157ea6b89d8c24d6bab01c8098b23344ee01301c184050a05cc31d193bf6d3de313b0cd0c416c96011e05f3c450b477e7b4b3fcf712d96951da05e222d3c9c90710b562aba5522278b268820caf25c6168b3414cbd4456fb830d4bde9a9292a345fc0c390a50a614534511dc99582b81156acf0e69693b459669ea0df0fa3af6e91633032ed9071e6c934eaaab1067015c3c31b12279149c9305dfa836bc363d03ce6035e6a3ca06d581bf011816bf32fea30fc12cf476c15905d07faca97b3ac0557d34041d876b510d970cd8fd6c9dd638b030f25b54d9adc8ac3145f8a86692ff9e81c4c020525eee576de5231edc363714a16e1524c94f19e02253b094cc8adb4d4076d04682a24424f523db77d990c2271d5fec7cda87e346643ade338d9ad538f63fe2a115affa7d12967b80cacfb378d7c64af1763b09c11775706c699616101271a14d27437fb662f7a6df96c0ebc18a72fa5294711364377cc8e5c51e39a39391cb7fcdc1377027fd09dfa4dd96836eff5eb54494871e0525a263f63e2102681731ac762d719b0ce796b2b386a6b8ef735a5290f5bf987ce95c3a908219c9b3f84ffbd386657df213de42db473c5765596faad56853c655e6d8e55f1d97d86af5e4cdff3c94ef3f4584b3ab6ca3b040dec45a1d182538ce30f9cca73b2f6134688364a80340df54c8fa705b3a4f36130cb28633c3ac3b0efd03424f2f8f448d70e5641fa3e3c869897c75ccd452b3819655768411792c8af7ae0be13bc0269cbdd07b4d4035eac1f5017e60728ff0c71f912897a1fffeafa6e709502f3ad94e69f0633d03a5268e2c6b07f942f78e5316b02636da753089cbd4faf58c7f7e524bb19819a0b9e4d9d50a5774a914146d139a268b472129ade4563570c7f363ddc5f960f965f5cdc84a7272b76c41a18961791365bdb22b7bb78230b80f37f8c70af086c617edd7be3c677f9faee4b7bf64a70aed0fa9fe0bcc6a999a48d838c7bcf6ed026273573d424f262318fbf486a5af427fc38519e06f0e2af277f070ca2c1710e47ca26fe8c02b05124109227b89b0ad97dcd2ba97ad529c105fc37a0e2e9521dc1a727275218020eb1a210cd5889006aba11fca65162964295c886636e7eb24a552ceefcbb28503b1ec5cd7faabd025e899ab289c5419e38f48024bc40d96d275b9c27324ef955f1d9aec23da3a3069815fcf2707c1960c2a1fcfe2b33432128530a4a37707cfe048275c6d493858892cff49bb557ca9426cb079ae8c890752a4ce309f7059d30c470076a6bdf7b1313ebcf8fe36024a65e35354cfba6cb1f85318632658155c3095378d4094013a18a12eef60fcd82416ca47c22fbf0a4c8d34ea3f46b84e43ec5c5b2689bf961a4210edf8e25bf6fee1f5e23c5d5a5ef1594621b46540251d27641cc3a6244b872db489186e396ce51d373c788c9d338583a49cd681b07d5d0a002e5cefce0eca575462ae69c0cc28cca7b4f85a58c81b1ea420ca8bb6927ca0c15d303e5bdaf96b8b7172d1b289f4307d001227bb72c93e2c25f1df7476c20540547fdd4443165c9bef30ebf9608df76a8afccb52a07a34cae8b695459b038d9da8d0fe9e4943c94bcfbef300f3598f6082df807a802d08e2dd34bbadfd10a133105896feba6b428741cd3aa69f20c11ddd8b73df19f0c8e43e86a8670e183ebf2afb8b7c46b9d1dd36188d0d1259768cb2da064773d1f41602daa011311c0e2443848300725d48df3aa06942187294ae3da1ac7da3fbf0842e0c4fd70ba3274ad3fbddce35b45e938898ec0b7a33dcba1b0c97620b99db0de1a13f86eba5729557c545d7721d21db14440268e52778aec869d72a931cc0d70ed456574238fd9dd60811f749fd5283e5e230761e55c025acd0e4f6d031bd360aaa8edefa4b3cdb58ca118e14f817d31287e7b7d4100777e9bd65d6b0982ade9b44d3c91d03f030769b70d87f19ef32bf8b0b20567decc3b0f34efb387f08ba5eed303c7e9ccff9c7f767794cdd1df0f7a24c9cfecfeaf59e91c68f705db01cf585540d9c90e10a1e6a6ceb352eea66f25289382b96901ffdfac5559bf1888998ee4bc1524c851d70b3a8842c77596539ee6c92dce69dc28bc632bf278822d615b33d52b9af0e8e481f9841f214102e5c0816d8ef6257dc1c27f50a7c34291301056b33a89959ae46bd601168162359993c836d6380ecc7bcb9329f677d4574a9033d731431344c8db8114952acdac0e18f5c0d4728a7f8094436bb31eb47584b9773915a1a3f27f260be8657fb6cb3755564058b760d651869110115c682beece0dbc5ece70171bf64562ac37a502825c6dd3629787fb8cc8fa89d1221cb484aff4265c0a5dcd5268c9cb8d1c214a873db5465e17a45b31a29f3c6349acf89b58235068ce84e929558482088f52b4a33a9b49eb090217baaacca0eb79686cbad1856bcf5bc8789411d1bd599e07f24c0c28773e57285fbc3019176081267fec0496d241a8ac592d352e1c55487f1e6bfcb8a5ba7bed8e5461cc6d62a3681317fd1072757e0f3f2453ef078847538d54bc94a1434fe551e3c54c77e1bff3ecc4fedce38cb8fbbf2a28b71203b48cb497c3201046d4ff35fbdc6c9b6a0ff6ac77382608be25af9187dc1180752000ea5ebc92cb7030eae15218d7dbbf1f0b8de8b832d876536dd924dabaabdab7a9cfd180a27bc411d01c2a54e2a6ff5cc8f6d1d196dde70fff737fc383071ab4169d3a4de7538b8f319e0b0bfd9cac138baf425d68177acfd891645502ea7832527e062900f6be5e6005ad9fbbdac6c0dc0fcffaea23810b15cb2561281ded7b66a2b60ee25ea703022270dbde46c8c61a3dfd9115e0253ec78ab409a655019d42a98d11eb79550f15761cd441e1450eab66ed39c0ad7c7cb302caa3dcae59be62fca02552909afa9d0c475265458cf2df4ff58393c9fcddeae4e97efc0563e52fc67baf3c6a2f04dfd5c105f349b58f5c90d1aa7df9e52a603f9714bfe43e31f7fe726e9ee5be21ed84ce5358191a74b032cf169e7f8283498b6d45b4edd0eec3c321c9952c7c65a79ccb659a039172041f7590d4522cad259312f9e6d898af51cf77017ea33cf73bed93ec3e4c0f9d9a7aecdda8ecd3e8d3eb6d2a41824f889f617db66b86fece1adfcdb17086d0aeee1fc89beaf7a4b1bc0a293f701f3e08f66e3c6d2c8db17dc37ccf754fbbd32a48ca55b3a0bf8a4a30194935252ec4db5a5751daddcb41f786a14a08dd1fd25ec52c17271b60768a23600c7d825b7778a73c4bc82257f5466dd98a07ed05703de6352d329798693fda827844223596886025d62514d1b90adfe9e198a88857f615f3422ae42b3d3f540e1ad2ec1a9c76f30d355bf8156101f389acee1fc9ebec419cca9dce2337856029272a3c740bdf6a44a7f7d86fb7d6da76ac20eb9ebb9a96ac8d8ab0ebbc0c566b74feff1d770cb345f4791470ace6a6825a0501837e5b243e0c2064663ff015cf7f63e571c5ec9f2c1f169b7f832e0585089dbc714811e05a7d1c49c11fd3379334d3bbf4a3fe91f47d3e5989092bc36477de1ec6e57b4a33712215a98ae7a211d8a3e723e7ac367e53aaf2536915114d3632f6c7a95af160a1ebaa6044f8b6eb78ac230c219fc39edfc0c1be63a1e2722e73a5c97b48fc3a7fe38bdf8b423cc8faa073d77c3c84534b9084fa3b603e8d32db41356ba55f34be964bd93e91f26cc0a663ea0cec02d321793a5a27bdf3c2b13551afd78953d6b25e5e8d66888f8364b063e4fe57fb025a596103c5fc64176a9e88ba7fefd0fa7538031b767e498632a7edf35417a6331b72ea756909e6af1a6f8f55a226742bbb8d2e5c01cf3e7ff81818bc7efba4df47f8b48e7125c500c02db2203d083c6a257e2b8c2db023d19da6c26c3a9a90a50b4624a7b9ad05df69191dd4c70449509db83278dbc96e3e83be3d2b5ad1b6428867eda9e4f814513781cd3953a9ef0981da7f06265540e0cd30b861e950167c1d01018e7a1670ead2c698d9c369b636a7b62d3a4069444d2de5666acd20670666e9c395bd65f5890a9aed163712b18dfe5271adbe0ab26951235c2d58eef3c64acfedf039a45af620eb5828e115eaf448edf90e6ae4dabffb8989906b9055e70fdd2aae82ddeca0bf9d41a155a9b1384d627ce7f699f0046a85aee8d0d6904b78660d18b3827cc899766d3b36d7aab42e359590a1d95959fc956dadb9c455d8ae59fe1e52c34f70f8100c50881dee2af380b97c4796ef6a5f006e52253047d4376ccd34082f5da8d7b4d8e34afa7f677b5415a9fedb14b029b35c66d9e3234f52921d3f42ee8753f559dd4548a67a2242942c8899d2cda67d5d9a8300955146604f9a5cc767db0e75563424f3349f4c96ff14c062ade3500a3383d793e82dac9f438a35481c8e5adfa998f3382a323d5b1ac1a3c7fdc562795686e00b6854e17aebe6054cfd068348fb8bd39a48830f2bb15ecd57a55f3eb58dd56cc8b73d9579808bf420372cdfd85dd4560c17b7e709e93d9235c2008c8248061a0b5064cb438830afd210fa763c63bce6bddb39f375dd60c977d64a34f6c455815ba02ec697f4424beaa6718d6a7fbcf66b7b7c4442e12fddc3fb4ab0e0b3468248ea1748061c0a05c3f456bcb49f493640d979540a1efa94ab338fe77dd933ab89251e619588c848950c8725f4c745185b417b25eca4dddbe71b7ff95bbf27d5fa0cd537b2fbafc6c52926b9d2f4a708b23d2bc37fc7334e981db0e7dea0533fe3e74b83084ab17e1f96f039b98ec61f0d1597840394e77361a86bf3e773c94b202bc5596804550e9b306d46c237d0af106e858347182cb722e39936f73ed51f035d8ad35eec6c5cd7e67458f77bd3fe5c95d40d88b768a58df27bfaead2fd7d5c19453d0af30122816e249bc6212c645449d7abb623f8964344bc46fdb87b8ac04aa18e65eb62d1c12fb411b39f8db16cbccfca8c305d440b0b41ac7ce84a6e2982f1dc39cb96afbd48df1c20f7a26f979ce35a1c3bb9860d10c80337501004c66a47b78c6a9fa0a569bf791fe3185f2529bf25b26c8db5e6e4163df0b7890e391c74f3af261b4769f9a9a899432bd48f2f2881d5d20e2e374c73accf130d7d7e0bcd0b7cc4ea67f1b4d3dd9acc9f3b9938d3068d7a15accc951dac941b32fec08d757404011826168d90b4c8205216a44fae80e81bfa9f39fa2a1b5abef88eff889ba317d910428b6df76a027b24c76a70239140611507650567b21e43282f9c66071b967e638646fce0770b6706c1632cfeb36efd909e349d828c5f19fde1705918ce0b7e73a1122ae60709239d7c2d94a1c02a14c09b8d5f823a6cd4e31ab81d63e81592432759107005d664057b8cc5e3dc72a99b47862ca9d9ea038418189a4519e95d828ce48bde72560dd27cdc081d382b5b29c8d2ac152524b365a84ece21d74c5e80345cc5c1d13031ae58d669d53f2c8bc830c66cf42ebee9915ea48edc82bd0b8e47b6d8687bc31bfcb60edf8833fb2c902dc1f2c5698ae692efe3608790e0dc65cd8d31655ad92168e705d187f286c1a94f08ec6f2a207ae7089521bf94971a78201c807c9ef1c145cdece5c3491cd41d66d9f05e3a23a7a95b3ba622931d321785e65a260f657c4926ac3a516afd2dd50acd2ec28e6b1140deba45373ec27b32bb2cf17b7c9000040ffbf136afe97d0c513e6389ea1a724894c59be47e321fa2d2deea6e9fb7d9aa8c0c4526ec34b61cf6df291bccbbf4474ab2842f3cdc877b82dc80d9ad7bbff951393ec263ec294749650bfab6437e805d5ff20653fdc8c715749732e8369239795f23dd20925cd326d5a64bc430a8cbdb02d0b9bad328422c28d0702ce6b2fda964bff91f3e04336d15c572488f3ba2d7b23498f0ee574873e791aa736e79639d36097b1de49cb314b1ed5fe5bba981dd700cad0261cad6f0c01b422bc7a1ac95a53a354318cc02d5cb066791d39828f4a93909e48ce60a059d683bdb5adf6315f1c3013346d27878cc1f6e49cc386624766e2541b48b1d26a3dc262cd182ca106f9d0884620402742809476b61c0986129d6ba0ce2a3d811334b2eedfb21c58e3d21505dae9308c5f0ba84093d673f21b6002c6a48c3a63635a18065d2dc22bf5eccccf56999a9cedc45e8402e292138c65685aa07ef7846947d3533a3e8c7b78e62e96b02a6797cf6489ccf037d17da8b2f2fb85e495db6538f4289611071a12ab5b05bcc54f2f16f287d793c04d3f7514939cbe1807fd71336e1a97877c426174ab4d7add558687687c5fb5bf9c1ed1595cfbc2e2c71cabcb2a155373e0b673624abb87140b170be5b6fcaa044948e7e9015d4d850ab31644738e25478bc68d3115317036f7fbdf226e534c9e4e70f163bd4be153f30cdeb85f9b05235430f80547256cdce74d80693840a0fb1ef14adc22da88040ec764d66b5ef0423ba0aa424794e77facc270008028d388aae10a5b44101ebbbaba2ea907f59bcc0820bd5b5847076564b01155df092cd06f79ca28013b3e3bda69706bff4348f3fdaad54d0414bee6e2741cd94cff77abbf45ce1e77ab923a6cb990e22acf2daba1c252b39d102819daa76d6c50ecb2d40613b67a41ab02c18fd4711cc5b20970768568eab166ecdbcdf2a8fbcf965d0a6adcc914e5d4dbed738fdc313208c215e559e072e88f4b471c6d12f2b2385e0665175e95e67852ac0b53410c6fd5f1a4f29b37811c9c0d14640389973f61b255063c2dea148baf3e4fa7a16bc1e94318298f8df556f2d8628259bdba9698fb02f2c0cb229e5546c278e30896c0474884e34ebf38a101c184097e4472259d9ffa83d71fe1fdc7c6b438b583b1e66e4b2f0cf52bf2c2b6b02941498e2431fdfff071c84dce10c7ee08a8942a798a18e7eb5c2d2932cca304721d160eba1c7bd54a0a6f05d16aef88a6d4590d8d2ae6c37b4552cb2b779b57905e76cca68ea027da30d4e27bc9c80ad03fcb11e8bb08f2ed22aa4f4bf378b40788fe42dff43d725cc74d528497e98ce35f0db774c1e8bb8c40a900f53f6bf182fb564fb4d2fdf365576aea0a4296ea72d1317ca44444883623ea42da8a5fddd550857ecb2be3345e70bf5e61d6b361fd6901eefd152289f01ef1df44bb6ec0d9439bedaab053f83938508d2cdcb3c8a8a6bc212a94bbc9af4e6d01aa3505ffbd170f4a5be0e452692a9ea1d73ca7aae8fbe62c48034902f0c0cf11d9ae4ddd2ae12c97cdba161e9bba4d45fb2cb4e634518a491eaa8788fa2adb112f90392d1ba6aa1ff9d08d8a0a89450ca59e3d7fe931dc19e4a1ed5f1e126ec438021cbbf497a8fbc520833fca3a52d4395b60325177c32df74901f6af945300d333bef98fb0875f7b95de39e35d300ed0856c212f8436dac0a3e00210b1c729c4efb9fa7f57741b0eea1c8390bdfb1ad2eefc3ceaf1f309a364fa06499d0c0d101772d467aac9044aa4d88d6f9dbf22341a46c06bf6c44b97281f7146c9d9097d6f8ee52f7e353c3e1abca8ed6c51afa39adb4f0a079c6cfd63acbd3bcef5461c063ef524c31939493b3562da0685ff321d9224e717502a100deeebdf782b457fb506015f57b6d9ee013f61201bb72a66d8e5327ef0c36a472cd114455f27b61d43f4f5e0481b0706114607da59f5b0b78b9d48ca29f95c4c4569cdbcb25e1746ec5128bf486526425b4f9cbc33ed8e9ebf93daeb78ff06891b19719b13fe5fe77f628b6a380d3dd7a0def7ac2cfd81433ac564e1663f09d5234448b4b3f64abed60db5f5d3c1168999c49cd5d3d88e11db8522094c16312563d57ed420c493aa3b8b7c4a426c7da098532bd17e13a21c1a86af3a9ae641437884030e7aebc22a59bd48c8e3811bb1a4f3a94aa0ee0403074620cf853205862af56e2b094025f806e7d65da900d02ecea618a5ac8f2b8fe8e18719f2740ca86d90db403f62054511b0d7085341dde13fc40882950f3a8055cc325ebfbf594b8bada73f96569e4c781d6263c9c2c7773a9a967470950fbe2ccc531ec23520666b50440db695b38cb053047cf39816fa8df74b4c6b07692d684f769c8b2a245d65c8627ccd3a157bc89a2f5767d48eea19876425479f89786dffba62eaea6b3dda21753a35264681b9d31a59d35f8225785830d626cb00b642a33e251c7a6bc1a69851b4cda56ddbc6f9970319680f8e0396767d8b3dde97249f29e5b7ddab6a6b46391a6459876a11fcafdeb770c126520ba185e7e21a90ad323e8e37a50fdcb77748fd37af3ea75cbeb0399a8abde7c798ae5b61cb84078dcdef76cc36c8cf7037c6a8034f57d2fe6d367c2fb25aeb45ae256f0451b086eda0d21e8f4140186ec0d3b4cb13e3444facc4cf1dc906b622950cca0c5320eaaff075e0b6a79bf6c07fda46230bf9711ed7d7a2a9bc62ac879579a5e8eb55ee17587e467e8f1ad1fd6871787a561960c99bef87f65fde1d0c41d01b5ee11394c018d63c6c72036b309b54f064675ae51a718509099e853fee9ed16859c80fd8da5866fbe1067f3c1a259741bac5c3b2ead2f7c523d051f4b015d67643c64ec85d3500b82c363a260620fb0bedd052b5734e89321b829b269965531570207998532cb5168633511801b7e479dd01b200602374cea53f853b9476f20836d04ff5fe4e545d6565278c4c5fa94d36efef44620e7d5ccbc4ad194e61ce5375d79c56d3d77db75d4e33704e6504c5ed7613bd104b88111c93d8280a1cb853bf868a4dd3318c31b549f66c2f6913d184e00a6eafbc6509a2686e74f3e61326593f439075e229964f1e04cc5b4f1692ed9bfec057b012859af015d76ce88d0d0b2cfc1ba22183341841ecb310b9b596080bff12d3a646faf5fa013a4417a37f5fbecaebd55096452fe154ca08b82aa8d42a3ddffa314b651bd1e25642ac8b0e380150ae7204c188bd4c0a9626e490a0ec165d698022c323dab05d9c5de0611b249eef65995a4080a6c418500ed9f4f4e185384846bdb1378487e448e19b3cfb7b21e4f1ca69df9c3ac0b07831c7525acfc8385a07f8d88bb43fa59fcb4679a7170dc6944f73e040a77e7993cfaafddcf9878412860f400652245fc57255768d4e169717b4fca965c5b7f6bd1643b02e9fe5f1114a932002b9738dac5097476e3501634a0de6d069a9856127e85870c90afc074f2231e6f51c4b2648f1e665f6b74a1391d2aacaa3b93f70b8b9e6b4999fe0e107b4f8bd9a0637e0f2bf4447a7f6389e7b2309297817a37646a7d2e43be87ba724515fdd6fa56095d4b5f036b960a71df05773ec7a33ccee18c4f1e18a0096b6cb43e468ba2484d7ace863c24dbcff55766c1780f0958f4abae8d8afedc59d043a6b5c076a1c345a5d2f57d70ef57b49b6c3ab914bc5166b3ecb5e3935cfddb126e007f361d13e906937efd1a22bf6d7ef9707da8938be4958db7bd6a93ffa47224af28aa9d3aad0e56c1178b6f5f7a7e1b1af63900002cce538fa8f622d13bf907b9407a9b0a88213163467bbd6e54462059d92d00d376863b266286a32a99664bcca909f69d4ee8a72e50da342c86adc63127b5b35cb401fbd5ea32358e09acdc023e4c42796e7144d589438725955300ad45e1f37fe5bbd73f147d48ac7a1ffbc480ec0f1ea8746bae195b90e05ce04de652a58e7d60b90fe4470f85f7212ae80643a6aec0b9f50232a31a3109b35a38f8fd866786b6434be3869e7ba178be778684b64972ba444a78e4e28535283870785b6da207c10cf44d063e48bc810d4f2a6d0763033b0c9b504ff00b074135bbcfb4ebc6b8872cbcf5d374345e3019a55db0fcbd89a23ecbcbda261529452b0cfebd22dda8c5bd4025e4be3356f9840d4071d98fe280f049fe444af749696e33e0fa8a63dec7c5170e288459d11d10e7eca7a97c777d3d406fca52c503796eae395ee6b252adc4d92b7481cd48182fff13f8827bd82d6c891387462f07bbf065d182418e1dd07c02e93e2d63f05b8442eae89de46c2b2d6adcfb486a9a2aacffa89f2dbfccbcb219bae7d0d4d9894b3b41f5336a73c594e5a20893eaa3803dbe1d95ad8766237c325143ebf90402458d622e5f6d08dec29bbb8704f85314a3042b773b5fcaeadf02d3dc116bf0e6eefad2762b115cc3e9e663b635a8f6bae4f8388a83417e2223f98350f5995840c123b4ab9bf6cfe893861eb81cc29e463ecdf3a29be656dbd7476bc10d77fd3f795a55d79df6534a30eb26c16306cb96324bd902a95307d3e2d4bbfcf6581d1fea6763002aed2a1194aa56c3a6b633bf424a544070650f918d1e046e4852c586bc9287af3aa51daab37fbd62d643b6d02678049bf5a492f14315ec5e445e29186dc258b2001e8ae37b99fb78ef80cf846503f7a20f90b199bf9d4aa11144162153cab369bdc224c3380e7db3bbba9bd12f60769d4e357f26d94360dd115b5dbceb1a0435cdd07ca34af080e314240b4f6c106e785506dc65b4d4cda56d19c9ce5e183ebbcca05f4a6554b8e632e3a37b89199bfce1dc0f627a1dcf0193ecb245723f7fbcd6b28d606ffb405e2d777eef4b4e4680ae47d4f4de6261c26465a086fc0e1d800e659b36edb0ae327a55b2ffea68c68600bdf2445492583d05c68b2d8fe8dbc1de99480598069b0ec019c7d931c4e707b3cada313fe111576445cb2695e8a68abeb6f2939efcdda856532092bd4702aab92dc860451b421b994c20e21c6e7eb91e981c410e6a06934ccb97e91d2785fda6eba33b1c37facd66a06f9bb88e071d5cd22d1acd4bd6e793f53fc1045ef865605b359135706b3b510e52f8ebe239a3a397688a963944381698ed7b6eee232045f72b2e924cb00241463490695b8ae4c82cd2c5fbfcc383fad839ebfbf8372f7defa7a2ba0241dad3a10a4ebaca495e28721f7264133c8a15285e1a06b77b27e935064b76bf4f5b26d28fa7ed746f3656f93a108c7c3b787ba0a34201c3d60831f24a00f975a6e90f987a255bf48921e7e5f4be23947e6ffbee0f967dcb3b014b2fe2858b8ba41d9ccc1240287fae595c16d823d7c973a2b7a9d40262990e9f126805b015159105f9cfec082666928a6e779fffe6c454a238f8dfcda48e51f7fadd92b4024948b3963b4338bd034abee661a8c58fc08a4f9fcee94ac127f41eef46eb57e54418b06483ee85a3f4879c4067c5d6e09b2555e40823a76960c03ae68922621be9a6e762634196e5558b0969e835739550fb931df7b040cdb06b786c380320036bfeaf484876281743cf1f15db1f5c3b57d5e4cabbefe1389ad35b2d62b68d98f5ef604409e37f1028cf0601653790286ce7bb18c933922d043c865f4277e1dfb085513d6c16f71860f540ab77bdfd04f9546516f22af0ef46b55bfd3c98faf6abdb84f548573d0854e2f039f0a5666129b6ed0eb11e925d90d3fa9f1185d64482ebf9fc662801d1e8da09bd33a7c1b5d182dbe6d074d3e75753043473fdbda168c7cb7cf72e26a28130f684b60bc673ff372b6a143e96ee8872cf4533af77efa63128a004637054b0e8df7bf8311248368b2f021c6902f50480104d42041ad5139558c94404c137a5a2a58a63487b139375175a2b17a25d6151f16ac7fed64d53ee509635f4b2fb22ddc587d2f213446549b6a3e83c0e7cbbce76c79a74ca9bfbef9aabbd20ffe5c5dc2464e21909b8bd8a1fce3eb3b7a31d9b9003339016728338c3bc581606cb684a4f85b1dd4d5405e7472e97c0567cac48839bbcd0ddfc43a246a2cc49b9b500802d81aeabdeb6cbe50fafb52ef350e6b22717611aa1c0cdf670fc45a71341f53b41c979d1a92cdd9890f5a918f3d3be59868077a954c58e2fcfda2ed9fa765b2ee6398341bc2992e8681b82aece9691c33d5407ed613a7a1ad12c62cf099ba8864ba0780a59394f02da482c4f043e8106831e7417023cffa86dae108856074d73977715bed62510293014c9d5d021cab99e1ef394c1b99d512358af054a252845bd1ff392f8ab2c17fa900823f38acab875f541fd0162bcb92ac426c5b1bc8ad72a84f298aff61d0cd2957063bdc713c84cd18781d30fa27f65d065e98e0ac715ed5818e88bc71139bd3c49cf0376281f3b2405183b7c1dff19ac06df48c43573fda919f3eb96e605d1fd636c4aa070804cc904f9d0833f3b5b397746917d35c4c0ecb05669b9648c8fc457aa87d39481a8274a837f80f78f6a4621a5ae6ff4a86dfd78672770233185a6c9ed472780e2af757a96a13da53744af29d0e72808c5d6725ce9656e9d49161b29889dcdc90a8aeda337060a1a41bc2c004f9f36b1bd5bd7cfa42c8830922c98b83bb56196fe3d931d7bc6443c3d095fdba6a7f31af9c051426bdaa91e0eba048055b31f7f3e49462fe10b158f46a560119f1ad49856e8df712836901c4ad0f5d3eb1779ee9786fd18e45716591db2bfb76e65d2e6705250f615e568f7701bd9529022d8ebd744eb2f6d41b5cc3abadf9c58c5cdf0ea65c0998c13272107c116f22bc0219d3859b5099da3f05ac4f04744d5ed36a352430a5bc5de79f6161d14eb0aa67cab06f1e3e3fa1971915e997ddc6e696ae3f72c9a5c8ed62cd1a0ee6fb67545f80c8db335a10763f6e401958623310017c0a8cc5b48a5b3275c484ecef63c9c253480949ba25656033f05696c04d89c30807ca1bbcc1714230cf9e5810fd226f3b39b20f948bdc6b3cd01a69852b380c375b2e89b0b7635c0184e4df5cde711499fa437ad65ec9006dab000a898d373a9e9b2ea31e658ddc1e803d4a1fdd0555c8f7810ff912af6d3a053b5f1e78a8dc9b334bbb865d2b7b96d7f4f9b5085b7438dc120f77d93b310d1cff1fe89702423c13d0b243119ef2519157971f9617edd0b5333f918caf73af46fac9cc8650cd91d9df6cd6954457e7dae84153c019f2279c92ff95f67e2a9f0f247b52bdeb113b3b141ce8f51752022dd3d05ea97f1ab075942577738524b0068b3ed5ffb65926069e9dc9019d1c356d4806cb5f5202b9a4795c025bf9da02e3545072b9ff0127ed7cd75c743dcd1b70001bea0ae26cda5a781d41aa8572947fce6b0673d02c82ac32f9988a7cf078156ecd1bcd01d503736ba89291c9cc80a177e7c941b0658122b4b6ee5a6766bf3dde2ddca8e0785ec7e13e6aa3755c1418fd8642519ee1f8bbe93b34e14d6304918532c99eba90d3ef257fa3e5400ba1e9714dab656128692fdb4b9086ad202992218f0568d90378f44d651de76653eaf6e6a93cf84e1467136714c92d4282aa8d6ee3644343d83dd79adfbc21cacc88ffa52e4e826c75a3b96cf84f7d0753a1d0a805c17c2716c307897485fdb33816e02fd849185128a97034c73a1f3e27fd06563bfb877ec8a7a190f5c3a8a8bb79540ccfd62ef16725b2e3c453a0c36bcfc54b5155255247997dd470075c2a5f9700394944b0b4706896814cb5cc1d648c9392321ad25b942984cdebfac42bf03f11592dca990f15dabb891e1d763e04ba281e16673715dd6baecab3c8e7b914945f1369ef3644e6972e6500b572a2f2c6f44fe9738fa22d5c9f5502758b133146254a636ce2a2f7dc706dec4c5bd8ff47775d7a2ff59dfffbc5bad75e47f4650e3f817336fec65226a7f0a23166304ee7acfd05b17c067624ffbfc6079ac2e5dbacba4e613e03f6cc91804c30aeebe6121b336d339f946e335d006dabb1154e8b1ab1cb5eda3db07b21e410e09406435594716f3a65a056d54ad6b956346e234ac99e8512beaa74672f65c237066d3f6d5c0415b5c53615dd6563e52f473e3be035575bdfa5d97b004b0ccec3c39754f6457311d3c6bbbda7c255b93129af023c6274316403692922393d8af5086432ef522c4e77fad600c3ef7b5dc9cdc75e341601fd6016aac6116a2e274333f57cad8f94e2b5e40b1000f34fc88b28f151276d0c322de67f153d2c21cee82f9140d786842359f528457489ec9ea7a8aa4169d1332d20dd2b6eff8c04a96ba9f10313fdd67f3cfabd6ab12297941cc0be377b55bc479921f140326c9080f25147fd299e82fb21e513eb19dca219791f44317c4e6ccf7e7c02853eebf8eabbd585d0d5649abca733c1e73b4774cc6bea6ee6e95d1a8df9fef358a7969a6557ec8ea7aa0215344f88812fd6c01a31bde9913255580457c163693e82eca2ff05d52049e108310685c72ca74d58929ec78f0b2f9f320574b830a49f5efbb620ea980a0fe64be053ac7539e9c70c5c904582ab654382bf8a36052025fd710a1a78efa7f3d810a9dcd8faa8e954809733c62ae4edaea3971a07c0c71dff9a21870c8c88442e5a62ccf1b8fd911cc9a6abcc90180926d4c85e8ef5ef366c1e790d3f4cf56ed8b4426a4748cd865b0a6ad49b2d1689c3984aabbaab99086a85b1b24ad3f5d9b71f1e823dd2e8a73112743e6a2dc23fd7b7caa3106d67a5af17c53c6dfcd425894a763313a1cb1a85e13578892717bd400715e50278ab640999d6b6966f1891f385352ab8cd6daee50cdf196c0aeb1c6f3db365c97b14ddce579a9c896453f1e9515eee9dcbf7331b61e07ab6267d8033076bdf2feb82534b954577137657378217d06e6c95ba58ae2ba18a8f9e6f0b39fdded614c98ba13a237bad7e427c7c0757fd4785292bb57161f385525807ccb2be5c974815fd563edd3dcb5c5d24c1f8dcef4c1df7e9022a91cb97577e0b1b573e5d123695c19beadf7a0e41fb4b1238379075c29f28eb0acee0a211d80be93ac7f4202fc300df60115e06528ad559c734ea157bfab3e8ef23330bb28f1f635e1bd891f8cfd899932cab48f8c127e1ed2a36ae7b24ad8bf914b17753d6f14537bf412f5287237706d8778de2dd1238f3d7f97579fbbee870f64fba0fc5e01735f4a2bc66416a26bab36723626996bdb07c3fd574ec989fbf83a879921cfb6abaa271209cbf21f27181fde8ecfcd692c874ee83af7a67e0f0aeae4aabf6ad7ab4fb02eae2f8952e6b2ea1d9b39d7f9d3e172ece773e9a4c21ef7e2456cea630075db7a3b3b761030a8debb64eae627e85b632af88f37bc6018987f4e3918d50325583483c218a449204cd4cadaf94622459f702883c1330d4a52775bbc5e5653c2fd675bae11c82ba522280b97e5e20902bf517b3e5e4dca9f5504e40031b1ea459ddd5351f8fdd1e684319aa5db728c829dbbb47688dff454ece5572f0c1c56808d4727230881e4f7cbf4950e6f00a5b93fcbb0403bbc424ecc1084eb2232e6ede01ef5c774acd70e0390c0db2ece920818cc3c68560504cc5fe50e7fdd02228c79108f5f853e4c24a4a9044c4bfd5bdc2c74f89d2bfc8bec72c3437c6c168b5030e6351b94ac743e9c92807d1556bc87fb77ff7787c200a5b0147ac48c688beffaaf6a802bf6d3daedfaef96f67892148f66d6f7ef1e360c959ea9596500cb3d40239c18028e73544a3c59d59c12c9e597cb3a1364ca8ecfb10fa71b8ceb7c64d94369e98b0a4475890c46212edc21275b5c0f0af849972e87023d7434b6b97ebed4d926d2745a4771a2a0fb76f084907f8438a028a67b2d8cc42fbe191e3b50f86e505dac6275aaf9be6ac0efda70345ef4474c24f4f2f224824bd08f91bce44ac3fca9912c0c811cc3d7ee9c5a2f461842990cd2e4814f855ba86365a394312ee00acd8651ba3bc463ef44343d831489ec28c4ea1be212071bdf345633546ebbd4ae6cd7254d91f336a3a9b7f8da872ff3c1aa774c804cc67e7acca4a3058156fb5d3e3cf8301c54b778768402fa88d6c4662699bf11972170ab4fd977529393215f1d1941813e82b61e033141db70bad1c2e14db7328afa4534d52f62fe12e2e7f69731e84b56d0b50b149a1285dead1b641ff79f38a8e4939620dd1609191d73856c2ec5edcc8ec53637983084ab07bb43340fd1076b7013ce2ec5d1849466f5e4cf98de9c0db4cce831a40d0fc6635b7c32f9774c69fef67b523636d8004a034bd8bbafa8ab8fc5eeb09ebf64a835a0b9a8d63ed660cc427b9fa2c38558f1668a8a6f6008f234363a82c44f93a6755c33e53df35965bd92f6ca03c7df0e7f1a54ba2ca5c57b8e8999feec96e8e58492eaabd5b5971a0e2fa9a05b633b857214b5a1141e56ad2387bb6431334bd58b0413799bb9444e902469bcfb96ddb189315dc1629ba6ed983a57e1084322a5b0bc341d30fc0ec90ffebe47376ac43aaeeef71516eda230569c0cdc30175e39b5d8db848c5638990934286c954436cf12e633f3ea7560c824dc0bcf3f86ca8418cfb27f9c6ad097a21dc761869f3f0ce2c2dd7cba5c50a0c216f8eda17697213f90c7234ceef3a3e5c8bd433822bb4e62639f9665b33af633fc3226b48a4e3d81444da677d7860c7eccedec0e2fdecaef19f5e11cc7094e3fa4be8ba4489909ec03681fcb187528e9e80caf20340e9dca162f05c43064d37e5b4b6a16269f229e92b70b9f3a03e1e30acb11b17185601bec860ed13ee6754e4a5576de7489570a7b708162ceaa9be1d8dd00b78119f062a8b3ae7e3d4f5220c3b1561bdb73f5dba588ced3d5292442e1f7b0234c74e01f2bd6953425c7fee4e554ea023903312c5a6e30cebddb055c0c7b5d92d75181b5fdc1538a991ff55ffbe4219459a8f35b6a40d35e6515035d653b88a531bc55e798351396d6c0d2317ab50bac37f5d7cfa98c0422f77d1ddb17176b9d8029442f0f62d7926434a59b5aef8b16dbfab9dc0e6deb7f550951fdd6aee68fe1b6180c46e9a7b73e65e226521a6ee8a046c5c50cc2bc2e4dd270d012e11b43e52af257bc9af4df424cb408649759842b14c01e8310632d0214ddb4c41839702005e803ae8ea227ebfb19f0aefa9efa4e954271f1de661402a83367b6e3b55c778de4393cacd69d0ee880b0c566d611c9a8127ae986aeaab8d30113814de3a84668f6976541efbcce01f8ac7f69d2a9b8765ff6c4e7a8c54d3628b606068a5c32891de2a8849291c521fa7751ce003faa58fc2e44abed794373cc17b4cb4c6ed1dcb7f4296d956945340d1f3f631be27f873ab6c339818a0dfc868c23ac0255a325f8a54de45697ec0c9a2f65e262046cdf388f962848db4b5b883dc431b1450758777a3573ff785898eb364e5aac3ab8b95524d061897ee740ac265519e3c8f0d19b4681866a7c649c1ba21ef1b9ef117e543bbeebffbad5400c94074d2965a06ecf3398ef50d8655b4e873646404b603ebe70128fbd3baf733d293a8179b0e55914a00f231c898ab176e78ca90224c454bebc5721db9b0e8267a4b2a35c3b1d8ee8f18f765d2bd6c9f7047969fdb6dd7525aeec3e6fc8aa29669ea02ccbb114d98c57aa40e64efed517a6fe12700a0420feb41a66f82742115c7be50ff48ee8d46d26cd1da5cd5d8b01c6de50d4c408f6c9358a72ba6ab65cee514b53cd8cc7e1475aaa3729473d07cf9037c33c3c21d6f2be8a19d101f59289262c5d108f547794ef5ff9681ec251f03c79fd74fc201c9178899682982838ec026d0a4b85a7f84e7932f65072d221d0838f8ad03715ef7463cea5f052ad00a3e2edcf12c87bbcc252290652338baf60f33667739736bc8ebe66006a9386f6fc47a63b2328a5a88bdf282d92e69f6d8e52a8e3d9369de3d6d13d5adaf7220286133e6adb8acd52c0616b5b5d77abe3b0064bcb0de43177e0ec5905f6610586a14912563159275c0ba12ccf1fb142de425f2dfa8eb6dbf5fa44caed5fca3390379d90b0e331613c179e4af277ff4c82fbdc74c90d402b3cffb55e2143eca02926d41c6ba3b4423a7ff444de9039012c8596036c087970891710573b267f7aa9cc48196b358a5bc5910ac761a933579fa9f9a5a71fb63d6592a75ad9c89b8cb945e17096f84bb27838fbe0af72a38e27384976ba86f66609c3bcec49394a39abfe58d502ec9962dc07f86bdcb0c28928c7c5bec6e4aa4cfbb6f10e9f1dd5df12d97464523816913e2ffaac13fb8b80cffac2726c89c8b8fdf38131b093e60025b5fc659ec80b7a45d7b1fd1028ec974122cc69b7b91529c864aedd97e32e410374a9497d1bf3e11280ac1d68ad7f2a4eae0ed5686a388fb2f78923544f9d5064aa92060a195142d4f1568d0543c117ffa691a8996ff5a1571911c60902c102c58cde3012d704647b03330aefaf555d10932ce20e9a4e22666be0fcd84cc00362f90e88833999b6a26a241f6fec47e5b3b3945d183579c1840f6398eb6966d5895185b95eee528500be072b6a5172cc8f2f97382c6dbadde3a48f709f1a1756dc631fd601524a95bd9bd921b8e7983444fd040f497a3fa1d3fbcb5ada20931fc7be61ad9d0458453f01ead065592a526b4fb08a9df721baee7c4afdd962fd59267e7dcec91e1af826a1d925c558c26e16f0774fb94dd087b66e82bbce88a0d0e3046bf1f9efc2d2de5379ea5db0ab749a711769f52430b2f729c9120ec252872095d523a77bc0241e1c14ca4bfa0865ce368c84934012bacc28896e09cad5776967958544b59cd6994d91b0b1ccb3b41b97ccc88551b72c485cea8e447d2551d7c6a8166889409986a907efafe74a949f0784ed0841bccc0627925ad081df499471bd102924cab171edadc256b6958e3101a8a8506e732346b7d360be425d745284cce2a3a6b4c1c810277473b8558130f1b88ea9ee9cf6feeb6b33686ca846548263fa82817cb70e6ff23c289a689667223b6009cde7facbb3c3e5d2cbde35d9684dbd0527bdff6484f88b82ca74ec78b8b90f6503a1839b47362637f805b9439a42de9d8f1bc333933dbc763c613333dfb3d5d608bc2ab7b03b80c9c15ca3b7cbc0417835461a964d3ca4a6f8cfa13c61d6f33a0fb182d05f1c962db6d71f0995edc05e142c82d7c8b85dee69efef3196546b3942babb13b05072c4c95a662c01bac60654b4a1523ee826469adf0bfcf654465ebd0ec7caf8f99d17313bbd770ae886a7fabc129659da5f7688cf9fe49e49ae6083a5c49a954b991db06d5896285d38a69eb698524357c637b552b3ee8db6c331b5f92085182b06a2dd195f594e674c1cef18413af1a44baf953464e54ddf7bd15f8347fb98dee7e773bff2bedf4997cfeb2e863cd1a04e002980065338129dae7ae7b4659da53b04d50b97b78d0fa013afa5872cd2e873d437f9d2db6170a11539d5f6d6a16f28901ca9db19cdf378f8b9538fd8eae13e467a707373da6f2aa9d8bc690f66840c11d0400950eb485ebfeaa931ae17314b64f495262cabc9e4f111504fde27517c966bc5afbe4a955178b675f2f2a014aea9866e9ca65271768a9508c493429732eb5990076c2173864cbdadc7b9e91def30f18ee5604a3754634e4150d2e6318dcdade37f01bdbd36314ffe99f8a69686ce021798e8f64ac06251024520a5b4c4c333d2bd3371f26156c79ff11699b559f2d1e21874168fa93b369487e5d65417bfdc738dc71a8749f3e2df1e2d4f191c0cd35fa2d4cc40424bd69702910b845b6d0e42a66a4f0b0d340e35f30e7d0a99cea57b5e53cf9c67ee264342fc3224a71e0cb8dc236ac08f9a610c66ca3c47c82d11a7e5cb57fca0772bd845ed0c892df004b54041e7bcced4c03a656bb7736e8675ffc4ed62f27313b640d2457db686c173e11a6dc88666c2cb3dd629d8d1bfc11d7524509a83f23bda42aec9e789816651f9fcb5d458b178a3f1f29b682c964e835d24781faf4b185dc219c9f1763e8b1a5e97483ffc5d3feeab516e4fd61595c86ce87eeae31a658e1399a90df1e2f9020808fa6eb3cec46bd6358b5c6d3077274b593d89c16130bfd7379d28fbc6c965d8c5098fe45321e2aed45e6aa537d1963924f689566e66a1acd34dbebe7cdad831b23ba710380484a2e1af578fe0590ecc8cbf8dae4752875cbbac51196339ca5e341f189ccc1d94458f318ab336b0e7357c84242df4b13ade6367ac77672ca5a9b7bc4871da2549a8a58fcb7de595b47e3bfcee86921e924967cea3a5cb28e229d99dc1ca0f3e8312ca45666800d887b3483f076b6aad86755cc081e2ab52f5ffbc04063d6a5e9d91c3ade7a7d273027448e58b4a5cfe38e0ad347027d975e099416230ad8941295bfe0a7c3704686e65fb49ee983bea85fd17e92fb80a6939e2a6c4658bcd1fe5db45266dda25bd2e6906da59a5524d647da69cc5dd886a6a95581d1d0757943e9946e5370c029dc48036e896b2384f3d9e21c67e647b8670aed544f22d8daac578f3d56a38b51dac7440192cabb0ffb8015ce11e44e357639d45dea7663729edf46ea8d8cd37e5778e4a1706d4f77c4b41c94f940420ee7fe432b66a76153150c3529d996598caa9af3b70d842ccb80be19b3b1789252c80f47ec5ec321efac31256e47a7a47cddab1572937d440f74d7a842553090fc19dadae8c35996f88a4c2e434ad8a1decca6ffbe3d98faccc0db1b328d9c46947e2fac265abb3c55cf70f3a13b1ee3ffe4cded068d43f9d50770c8f6488e3fb65e48101dd3bffaea44b9ae63838cd898ba98f3b037ca5596ea125414ebe167810d6a31f0243b5350464d7f4e1c25f610cc5717b94ea4b7c23787499ec0419b035586c137d3e54863d5e717180fb64d2d1c4c9d0f623fc7b93a7b0d6005b4826dc3fd829b1b665f7051404337659477169df13a9fcf9e2aabdd4ca46e91a95b15142a85d979e70b7dbd0f0371afb71f4a7f7b20fd3f77ee8f2d678dfddf49715028ec4d0b41cd1ba9ca89340cc9478dfa062aead699ccde7b273e0281c409e92c3fe76a8fd9e295df10395f11e03b435a63e454028e4a2689a0c0b1b8eff9b7d47ea63576fcd172ebe2d7282c1a3a8a5e92395842374e8017726a4c329480e042cdf10342049ef9439b27c31436e22f753f4cf37d62739ba15a8681e4e1f7cad502ac866c70369b8a97a12748cab2b8e3fa0035823160ab3388fe766502c679d8535abd1de8790e4b715a4c07970f45ae52b22c747faeaa56cb4996596fd0dd8245240a01150d08463a13042f191e6f25255f4c0e4637eb4e9d02fb6f45aa699b3789d0add26b899852c379028224ba05423b3c2a033c911ae786d9f9ff964777b6b95e395acc75897663fb23122bb231d37d9fc73630ccc65f9ad44ad0a8a925a2f68b118352705a67d5d95711f22958f58ed88765b0d7b6c77768e8d073d06278a19df462a364deaf13abb177f14bc686ef0b7c4b5fae6e069bbc8cac70250e8040f41f73f34f11b197971c818c80520d02d98e2de1bf2ca04e3400027b3b0ed64732ffb00ad80597927b8d0aaf47c9d8d1c55d1e44a03bf42a67dc995f0eb17275c4d34d201e2fa45cd376c112ebf7f5fe05a8910f8dfee1f552f59459babd219d60a124d2fcda511a94169aab2847662afd5332c773e482c16821b0165c74a195a0b116edcd9844acd55a5fe69eafcce39038a5ca6a27f0c4369c180e23bb099a8a28c55472a02501ab530b1367a527c0f8f7b2011de4d2e99e848e69244c0d6b2968f8418d8300723413f027f245349c5afea54ee041bbc95eb4b6e95fbe5b0f438e5115080e57b8420217ed2bdbbb77ab23fd574b54ea7e03cc30b9d649d2701b45496bd90f33fdc277c703bba3d0f7fc301feab6269bde20cdc5bbda829cd6bbcc99cd4d7a0d7e02fed2f52ac4f071bf0a8489b0f93873af21a23c175559448baca37517e1bedf696ecdf9e335bfd20fcb42b14fdc005876f2af191a91ca95ebd2503b5b688357ab7b806a0049f722f65c6650cba93b023fc475986138ad9373b16fe164ac6f87fa05aa2a13ca3dca2392a2717193628f7ff4195428558427e5c89b0bdae30379ca7aba526bad54eb7613252fb8eda0bfaa6ebe458be899ef86c6ba45ec88ffa02c69c2c4bafb0359c2e6d11b4bcca23f06253aa3875b2b5398f6f7c3dcaa6c3f28b6fe2a339b42f92c40e4d1cfd1300f8414c7a9fa6dbdce12146ecc21c6f20b3e0e846b02485e1c8e4ca49a855b38b87e00e1bfde947afdc5fee21d8b73ae5663bcb5ca140fd65e92cce054609441cae5f9f4367f999319c3a81ad07f86fc17071a965e29a1f1e26df7552090dabc5d7af94b3c23be55b6751d4a726c23592099bd61944f5ad21a7fff687780d349caee80f32b06e993c8a14bdc3ffb558e7e643c0919890c4062777974a0b4b20e9328b3f3bd4a43cee7fbd965b87e84f4fb6763b2f2715c46b5276b481caee15d71b427bb2dfa36d7f646bc99ccf1a7fbade803b2fde7a30c8dc992bf0fd7d022cf06acd83261b8b53318fb0a14fe41bbbe54d504bf39ef9a0b0bb39027e98006080febfa75a87b2c24b209fa8a932db096db196831566f52d9ddd5728c40c9e3497d333be7a5310f9386fe210de8e5455173b353d3939be331ecc9096cd93d4c9f229158ff4b8c840e50d71d56643c19fdc827bec0d37641b68db0d554d8886e41374b58ad58da43d3249b1fd2b2fca4c894953504a5e6051e8239b27432ab5d2b5efcbb460518f232408f114b37d081e3104012265e61ea042b74d539a0dec822712ceb4df2286e1dba81f223372ea1f944e82d3c5f189a1c6d4ce9ec8ccd80fef8e059ef03ffb3d0cd7407cdaafad44f0c3efab418fab385eb471525573831231078cbfc3a45108601e06f146362ed73753fa977d2893d11b196a658e6ec67bff72da35208b4610ddeda52094458af171679d420758c0bf47fe88a6f97e680ce213a7ea95a3bd5ce116eb35d1e24f30650ecee81060ce5bfdace3168c09fbb5443a64404b3d4226789ac00bee43c9d03e66a265f9df0445f620dbfabd567e65838380fe50cf45d4c8b3c44845798fb4f0f1321679eb190e42db4195cf335ef7c0866f334ca4754adbfddbe03c87176f0091a111c25345826e8dad823188368a270e2fd9e1e0646d7e42be18070811680e99ea5cb7b219693633c584d3fe168eb03b2f2bb18ce1a53240d17ccdaaea51ab43c8f8f465c1f9f4027f7c1bb3abba94624cb35f365acb917343b2c53c0053dada0e95e9b1faa8bf69b24279bfab14a004e51c6068eb217632a772014aade1a5f130638b3098913e7bd53d20eb7883354f6bdefac0ccc46002d8d48a1c7f6e59d036393ea498fd046bfb5d5e1dca50bd0b1007e62758613d24f96ff6f2304ba4159fa41d65e75eb4a62cde0fbb42d0a3d76eca899a5ab136ba0a10dd2668ccf34d70a76e8cd874669d5500038450975b018e01597c4ffaeca2583ccb889e7458228677361a77d5dae4d1549cb7248a6d61fbf8663c7ef2541da7e0160d78e416372c23b4915d5f0c768d28de4cd49f6262c6c0a70e8d6847f1d066705bded301f435ceede666c9d2a304b8c2f5fb93e26f13fba30d7c2743e696829d9d86dc7fcc4ae115ce6fdb73762cfc7850ae31278c5f49c8ce0fe20154f9a98d89db341011314a87fb5d4c34b5f55049ec36e8c6bb9e3db6b97e3053ad351c6dbce03838294dbb2b1c60e5f79ee3159387c770cb31b043b54e99d6f595d5c1414c471de116b4682ab4c8c2744feeffd8fc61139ed64b4ed03da234914fe161990716cb1b2079fd7fdf96f4727aff4e91671ca9add8befb1b8683402a7b83109bbd769dbed31fee8d8dd9dccf38b7d103589f07ee7134a2498bd5f81e466bdd5a036905401246dd14eda8b7d991ed187bde536b64cf772e327829deba78910a302ce6c6b6733dc140b4b4b9e600504c410fb2daed6228019296554d0de825735e351b47589b2520fbdc14760ea3326f8a149833a169f4076f3fdf72711fd064335e12bd64f62baf7c149c543b8244a035e422b9bf56fd4dc9155916f7545d081bbc705c36bfaa668d031e561c5ab210208db9c965d871e9d4e1ea82df4f02b76e3ab00de0b2118852257d3f246b59be3a09edcd39170d9a20c50fba080892d46ee9f7ed663837a3ca69348e209ed18c025b2c9aa29c05e7c400b18c024b1a110a21e719bd7423479e6d89548d947ce0f0b8659c06b70b8cff25cd9f6fdf6d13c7291a4eb78b998c8702c54854984364aaf05a30605f08423074339d4c2f9388b9acd815d2c51cbf9e3a31a8d035c8b1455a50dd8f8787ddf1a47b23de1e557c379951b6a72bfa0c8cbc50e680a045b1f6cda04b042a8ff19a80c09bcc7bd481059b1fd9ee41f6bb27de03ce8d29afbd9b491e7ca277f34e20a0bffe5c0f73a607d2eb44420142f29999bb67a7b155e29455bf68bb740a5fe909ce36b1aba2422c17ab5e65b41100842efb3fc42ed06f49870f19ddb3e85cff983c48196ca3fa6e0c4b1f64a12834195c24eeb9036a435532f6f771267476a5f280399dbe5d4ee1ffe68eeda65c6d9ed7c6ae4adc40a08a0621544e42bd359a157189f667b93074fe910986abf43385f527407144cf55bab686e888fad28ec443d9185bee9d31e7e954b00f271d5691c3062f0ba287d6375284cea0a7d587abc88f331c8e69805aa0843ae50abdcf466f8f87951250e53ef7bfdf2fea5711e0ddd5deedd68ea8d8ab8ad9dd122f6bb83d9e57cbc5cb11fab2ee77e833cacbc1b15c4167670d166dd500ef074e4f1b0428047c6e94d3de60f2a1ae276c4e7c878daa713afc6dafe0192a990943e1aa1a5236dd3be8f51d0e381c4c0f5aa5d5286b2b90af595be4d3eacc59d19afd90ef397f00a82a1cd3c83965416fc26239967a45469692c97424acf6777651af6968af9dd7f8ab1b2839e33e96255d904549b558c05ef3a1f458775503e9bd0cb3bcb25f53fe40df8f43f168efe7abac28e1a2e122c3ff9e93b4df7082fb00d230ef91fd058062cf1e253e753ddd9b5913633abaed29f0ce3ef83cb6c68c41e718ce800258f7f3fbdcfe8a20343358dc67fdad5ff6ca454bf7768d2bd4e9763048a6335bb39a75db8b1d577d86894d5b0884b1ab5d42067f44a13962c746ce69cf6b7f2ead0560b547c2e1c3093292f1d698d4b9aca78d103dd206665c30654acde6ba2e44ae6ca831481a967bc22a680ea1bf7d7bae4dbab9d8fe9b42621f258ba371b89f376b62f1e2a4baf2b3f548e4540871393fb9b3247bcb9bb087e36dc2909f2ab324ec8f423f072a2328792b5c635b13e49e59242a220eb63d993b4d176bdb053731532d939ea3d75cd910a12ecfd0f7c2563531c22c90fc955790925639e3353bc6d974e8fcf1ac9d1cc907e23efa91bd524722d90247191d6f79bdfbd9a6e4c54f3f288761c1b0f4b596dd38a8642b55b3d8b1a092d3f991be93deb7ef894d7c958e0f08f3b92086d3f492d8f89f6dee8b6700ae93a34f0adef38c472a43a5247104569374a5c37f812c34a826b1d272f0b7035fa43f2cbc42070b036df7d264c8b200d09e7ecdf8ad1343a1848a343f2ce930e700394ee4a765d4ba4d0a427ad134ff260e9dff3b33906cd60fc307a43ca27491522e0dbbcd02ec50a66ee16041d5d61e5b8008eb7426a5d07fc0f3ba7e66bfe81623b3d3760bb64826d7a61869ada76c9b97eb7de8aa148d22431b6803a52db17875c7f493c453a9f12b1c3af34043dbf27c346f9baf7a6578436437a3d1c4e44fd3af48591b9532c833e1893399013e101000f80a9b7d95cb7f25caa833e7bda39c8310e753b83d85b4d34c78a21fb4321c216ecb8292e15afe3bbcd2c2253ad21036c4c546d0147e26130f2cc1f4486bdef387549abc7324f1151814dd6a6b9f6582368491fd0b1926a2583de144d8ead188eec67db5a5841fdad721ed15ef61b1bb8f9be399f48722dfbe69af9e1005cc4f940e62b2a91f744c93d319b7c9159a2f999685b4c9b3a1d53ebbea3ac1c8693644c40c3d23744a269201033aadaec4cc012d16576740c23081dd4a55f46e3d10ec4e58ecab0c09e8a1ddfdb1e0f8aa43746c841684c25358ef792e00542b11e6c5cd02194c1c7f491c38299643f15003fc7934da6d326a294849eb8ac97490ee63046ac1d0df612e04e0a958a3f04fff03be929eb1102d822ee7968bb7768507fed68d1a8f72e18589c11c7bfe0f726b00b619fadfce183c3bcc091f7804da95ae28bbc64a1727bf573a297ab048d8ab48a97035d1541492755b5217f3e58ab43b0b6ca954362e01b345c1f2005e114029fea5976e7fcd7a249aa3e7ddeb4756d4780628a49333168517655a4c08bff7f811e6cf1ceef8acb82a62d2c0c409782729368ebe2bff16879ff831fb5bfc7819bfe5d0fc836a9e6a890e02d3663e81726fa686e6291d4ca9bbb690e4453c2d4085f4acf75068be63a878a7623472d4e08bc7c51fb05eafe7667cf3badf7270061b937c8c2a7742b70ba41ae55ec1682a2e97fbba1932e032b967f6b4769c85d754a18a5e5059217fa56b8c34cd0d1ccc26dd7e1d729e97c796b9dba6bc7d04d027defa517f917deae2c434ddbdecfe09b12f4d5e403c414478c1feef52a15e7472845f21410906cb996e4f6ae1209333832d9cc6cc8147485f00b79ffb6c9022a1c6091635ee07c2e8669251e76f598790eeb1f03587042b95f4a924f3c8e8d36c870df0498256d810168420190320e249cc958471b53ddc2c06df9523cfa92c184e93635ee924378f328e32a6ec628c63c03f09c9541ee2fd3d44a36b716847c0941643d84fcfd5c70330e929f2c542b36561326aabffcd080239c2fe26e2856946e8caf92a5efc03eb6fbb3da8e79f51e30f5ea5673c62e14a4cb01a69de87c17028519ddb9faffd7507b6748b5412202f8ee903e9c9e0dddc2f973f3200f1c5606785982e13c5e6883622613964d4539a799b160409c0ec0f0020b4af79e6f5e62e6eb4a2ff719b3277280635016a41c204e6d50d0a8a14f86191405002086e22b9b1360a877d48b92eb67af12c939c8cd8bf7665d7a40fdc7b6880d27ff4e26036e07ccbef3ee00c673df2a7c5105faa324151e972ef1490bbf0e454f29d91921a11b1cba41fa35a4d7eda4d00da8134daa7499e28d2417d17821a3008426504c52b9cce4d96d13086f173a1eeeb715e7d263d210b2167f272a7f2058fc7cb260b885d037bb68703632eac66b0852b515fabce28751b006659e3d09a56abf0c2e6ba16662b09997a525a311247559720ae35f5e43893c351a6b0b822be0a75b3e07b919fd85e16363dd0122f687ed2e40959c54e3efd543fa84f6551908970f5d525f8ae74295245cb61096c227a5c543f7baf7109588857b5a22debacec1f47471b229222118657f70e1fa39c722453d3f8a0201bbb4fa261f6974428f5fb72ba5d80d46e38a8b7665a89d277d83efee8d78fc28f2df54b60fa57747f92cf22ee4b9f55af5f934d78932621bff57513527947898999010f8e68854d65fa6e86f5146c27ed0878baf131c886a0aa0f587ba65a5eafc652c3158ba25bbf985fbb411734a1023e27a160556b4029df7832a695abbac28d24e298f0658055fe18b0b2f7c75fc20cc5c0f034611958a162a2c995f0875d4d484031ddfabd7bf473c1f6f99b8eb588ae738f2395c6628b1a146ac5c5b061414f192e70cf4705424cc019541c5584db4f941e86f09ef4ce9a603fb71120f1cb587021d54109bbfb880d3b2fbbfa69ad03425d0806d3213a36eb0e986c14577584e389fcf980ffd840022f12c8efc6e3f17fe505e02357db5509038f9e4684ef529b9d03907093860fd5e5e882b73642771df620ac995facc9862f0a8bf6988ac633acced7d641ae34a5a00e57768c33511340d5a7a847b347a100787ff9c085249438f3aca6e3521b8ca2371f87b3f47fb731fba7acca0a8bf313e17ea276ade08b41af97f17fb913e3b433086e083c85d0d5f9e54f0b8de5aae98dee6fa82c0c4de22464a577f21dde1e5686bc2fc9f93ed36ca6d063fc8547f6c4cad4c3d5cd2fd3de3e283d0c13468e7f267a5af2a40475580d414323bcf5f2605302693a026d0aac0bcdf3b905e95a8ddd77b636603ff45f0531455983e49b97f9afcb47e58751cfea4b972d9980d0733ffddaebaa4d10c9fee9e25e3a512beb962398fba3a0fc46ff93a0feadfb0b636fadbcdd4fdd4a0e3870f52cc3021c04612e8411912d12e3a10f6ce41d451b44347f220d79820f2f55827150ea7b92076c30c862cc6d20dca2ab14e50dbc46ba7d085895f06f54225d853c086d6e5dc28417ec50b8698e595d072f30b292710e3620980a3398dea29bed57c36b5398bd9c96b3d70ec5c1886a98026a2171afc94dee8b71472c7fed71956b35b4146cc5c22afea2452ec3334528eabd16b49a91a21b4f5ba342f7bebacc4de5847d1836460ae060586ddf66a8ee012bd5fab6469ce6ec7a15980f7fbe9a8bafc806607cb389cee1b2cbc0b678f3227b9ad63db6f0026e467783eb105f68e649030df736db5ba02bc25b9436ad6206af68277cd666561c93529f0f17758e9e53e8e77f12be6a7ec30c6b3d4d747b5b89a8415b64972677196b0c43388d9b89fadcfb853d3d46bf99f231caa8ea0ceba27bbc883b988a4fc542adc457a8bdfa3bd9116a11656a3eecd3c007433995e47ad38645d6431dbb94b57b83df2a2be2ef84e9d7200573d7a692547c9c373c582d1cbbc3e3b0d1d2dae7d902d56276391ab02cab07b86a4349153e733e166d0241c0e65f4753cec2f079cdb83c9bf44b6aa628e691f61bdca10f8213ccef1a28e81dca5c1dafc6585b0c71d65800edee22f1b4c678d0ee831ae39b2fcd8532d411e542f84aaba4118688376c630e274fbfa3ffa89fbc3711777bcf40d1e124b390ce2fb740dfc21db3a64f1ed424559c4732a54152d12b9c73309597244df15e9481466120cfa697dbb531031bbc77058d0996437112fd9c21d38ff42829be5ae0e4581a083a413d46eea32982d670d7fd184e84fbedfed56087ffc8cc4999ab24f7b7a7b03e6d695a14747599ba7e8124ef3b0eb01677cdc3699e92eb8e51bd2dc5ff956ff0cceb8f00a602d000040ffbf8e6b2f0b86f080b7001e50f6752b5e56fde5ad7fb1f8594b54bf766cd12afba846dde6035dcc343263395d2f9c8bf423bc29eed211c7a3320fa8807266d5329149d4ea773ae366fe8f581329b536440b19d928b804d2191e1caff7c7348464898346c2eb36fa271e3d6ba46c4bee3cd71aca18c518e5d7f0d3b1b068dac48947d79e9cdd0b7c16e9194980400f238fbd3ff669bd19e2629a8dc40492f88bcd7173f9cb9ade59de61f4f8e8aac6a1f10ab1625addd4724536e0903c32516e8c5c1a7271bf511280b7b3c69b6503ee5be3637ebd86be62447077319207235bdca3fc1793dfaaca61ba57b75275cd53c19b392e8d5b5977cbc2ea15fe0e3cf860c18fc3a9ba8789fed784fa8ba37c44d894e1d08b6289af306ac814ce54d17b84e55ee05cad244c00ef65f1f175b9d17dcc3d30df3ad7f2fa9525f828059e3cb92f109dfab8f2bb7b2bb019f12bb4697f1ed16baaaf44cf2fa95b086e132909bf714ba06f58344525be1a2b2d31dbe261f18bbe15200ef30b2199593e4128bbe3020ced4bb15c4e96587d5d5970985606e1546ab098bafa28d3d41b684acddcbf163bd8e2842ca7a84d82ddafd8f24c27f9172b0146a0499afe59d13cf6b868f08e2549398ef63ad1e2b284a79e26bc48600b5fe23abc86581ee44605d6a9235a59dd88ef2ba6b6a39670ab6be4b69d69aa863b68e63b292b814d82b3362faa48547f886ecc3911e17919c751dcf561ad58f10d955d1fb32cf377a224f01e02dae1882526b2c9a82ac26ce36d31b06ebba38b5e41cbf6163b70e7b4199a41173e207b442453e3407aff6f9a9a64116ce9324a14cd3f225a6729dda726f4bad42a979dc4d4dafa6a44501c527e40966986590f8d208b8063173cf0ba19bd16b60ffb4df0196387fa8132f7d1854d1d2c830b3c341ec51ab22326e1eb293240f8b9055d906fb7e890621ea87adcd921890ea5bdd341b9884de35ea22af7af7504f101016c29323ec6cd10e9f4f78ff277ad0b97981a328da663df282e8d0f9a71435d7a06ab074f4ff3a87459c503a96acc219f7e83450de5e1ee3a98452a86bd01e6efde8383c67c171a020e11ea05ed12105ab6880aa20460d7f34a6931a799e0b05866f9aac1b438b442a526fc37d6ae4844392fd011754d4c09ec40e484480e83432f26d027ea648c54a67571df8a4934d3b14707c2273b59f680768915bb8f397b8c1a7a80b565655cdfd10590e26d45f8b2de7779a8225d874e17985a96bc0fa406ad569e762de4d805a60ffdcf51b638ae807e665c27cee1bef651440b4e98c8d250074823259e3ed0d9a97c5fb1f77a6a490ef1e323e48a3c7c602e5f3a7e519ca824cb7ce2c28780fab59173304400cef55a30a07a1b9a19767f86e8770b6738855c61d9b45b57a611b247101a42babbec5d58bea60eaace20780073a4d26e1ef01fd0225c527cc14869946df28c7ce79cc97ad73c7fceeb8c70c7c3d8c0caef6ef274adf69acc55c50094d345548c357687117dcfaf604a278ffa956c3c4ade35b9942aaf93e7c36bbc59c767b55cc4a9901e423a457a95b5c0f362613944c9ac9389039a1d59648c1d9129ba75f43f406deb268e2dcdb57822fd343a44e4ff68ab18fc22882acc78e8f74f3b8367c291c5107b1f07e889a9e1964f9db9754f822d26d64ba2ead8527e8c62cd9ea8b096d33f3560f8a93fc671d7bc9f02e6800db8af3f582deeed6f3678ddcea7e69fb6adf314615ed67c2aa09d58635e356f3bc3ca297a114c603c6152dd3d0bc3e7a1920cb7204d742db7875481e476aacd13a7e3964d46f1120c84bd1778bdab6e0b9e6bcb3092ff3c74c4c35a19990f9f1317601c0b634be43477aaa613b3763489ed997194764ce11ce7500628f325e3cb1255453fc5b3e028caf4f5bc8b954e295631f36757671e7210558c7d83efabaabef91f5b72c21d4acab260dcaad4b0aa6d8f150e220fa36bc6e7e51d67090fc932acd885cd30a9abee9399aaaa6e5beda086aa43c894b12d848527d072e145fea7748f9d1a1b590052580f30f773291919faf1c3665b86fc91f5c1b3456ef3e8baad71c669a849f440f8395965e1d82b15cd960810ca470fa5a832116a4b0a9ef983a6e41e6529f995f202c6c8db2d795712095ce611d2a9b6f83684e219f466eb72faf736119aeacceb0f086597b13767c558014bee19523716060ceaa970c43ca12cf0f3ca95958cbee729750103018b296c927a070226c0f9913684330e87e8b3a2104987c22aad6eab9f9db5cdda408cff24d53125ad8a00d8a7f0e85cfb402de0fa73784c7957172216e499f00d3e9d966a68891f72ec7932cd46c0492e8c06f58081881da537f5c61eb82d4ad90b4d815aacfdd39260b697f44de71e9359d34271224d15368adbb8b756ce68171ea5e6195e405d0c7506580c7ed0bade4d28f72f5daf3e5dbe6fc96fef18b39cae61ea142b0304914e0081681862818d459b6292c85c83fa4a62fecf721e69a5713bb8a592382300906db08e473459b4b29e692ef04afb2aaae8dee9c9e56e9880f43d67efb6c3d833e5c936cffde16726a86412fc4a1b36d5d17afa0ff131b729c4edd66fe1b3c1d64f739924d417f000b6d4eb94121f915cd9770bf805b116b8f67b0d768699b8962fe5667398b6790c50f02592bd98cfb21ec313225ca0f47df6eebb54df4ce9835256c1814a45b2f147bc239c5bbe77e45b695784ff2b1695fd535531debfe2201a28720dd09a6c919afdb040dda634ab1571562135cbbf856705b75bb275b019ef84c218e31ee5f627dbf9d07573dbeb8bfc55644d5599b4d3804738438fadab7f57aad4da3ea351d3aab5b2d29e6825be35cf1ad8421b808caa1f212455fca8969f3d6437787b42574591ccb98636855f5ec7fb9373e1174a270af05a4d159f5dd4e4d43eb09412c5a096f9ebbe41211271bb36ffac3b2b55b39dd2c5999dd2e92b57b8035ee290fa6186d78a4f1cfa1048ad0cbcfafe4433a938494f1b4606d4c5574101414bca8910547dff07d6e28314b7128836e6ce7b2372dd52da3095c9c7bc298d4e59f67f16b8b590811a53a40191b2bff941b9dbb739f145c0b0493b8d1b9f287e9c35dadce7a553b0ac4ec641902107f9e4b610cab32dbdb5b23be67ade76e1d24e44bc4865eab6f697ab61e97c0a99b381f6bdcd1a42991e700ef5bef071e039f3a103bf7db53cd2e6c49f3d22de60c33b27f7086c61e419606ff1c26aef825843522c1e76f2a0c8cbcd725d24cb401d096fc9fc010d76af3abfdd65b8aafdc7ee82efae76b7eeeef85fd101d521db925b1da8825153b3bb1212067a00216e5f9d001fe5cd0eaef0d3c55b4f34bb9269b87297d8710fc9d602ed6c4ab27dcd410f162df7908afefdcc3605b0a289319dde4a8ea638908bd019a9e1b4699ea9deca45f719c72a1213624665cfedb800107659c291744e95c9cabed1ba8e804e1046138601c09dbe04bea1b4ae68db0d571337b3844e383641a3a00e1a0493bae7817bf7de7cc5d7e2df2ebae7462c6786e123a5c43aef6f5e78d063f39815a086984eebdf2e1c4d70e548b483e7ff432578108d46129ef3d964206530b77f280bf2af341c4fa59d9b28e8e6981fd130f56752e5cb33e24c7294c18c1658df09cd7b30a3eafed81977b67454b770f7a1271ed72e9d1cc7da5acb086afb4c3ea465711a688604521e78c793d113f4a0598b14440bcb6b4aed3e8e5814e79beda3a2ebbb30fc04d13cb3f5a2a2891ddcd4ce71e0221e51fc3a45126f66805ae897210f9cbb55966e52646c5db06ccd6a92075de73d577f7476c950a41f9dc1e7ac27aa6b40f883795208f98012e59ffe4dfd02ed8c04c390e0cf34ac520063455513e24fd5d47b034b47437e2a3ca56a5db10d445bdc2b7fbabcd8251a04b33f074d785285ef776ac581831fc43d82ca6c27a13925b50bc8aeda17bc91077bebd917aa14a9aab56e5e251640e6f703023298fea0934836853e49bbc53b7d8c417c3ebe865908ae66b446ae7a74cfcf56ce22e242ad1af7458ecd788ee5a5ff4aba5a7428dfcae89f9c5e5205c9545988be94b73326c60e38c329020d83860a47ededbb59b98a6518b8ae04bd2171cd7cfacaa159e83d5e1cb3293f9b5da888c9043e435519de5415e32b0ac4635ec3c606a0cffbcaea344cf87e074b03bd3b40c16f2d06a295a975ec7dde46ca41ea07dd6e2c1856207802ad7373c320f00a2f373279e783f8ff4fa05482cf5dc9068968d2d24fa6267d7f0796a19aed2c2c687583cab9fd39bbbc50583c843d01f7c1834be010019896d83d9e29518e1ee28468e46277a712ce50b8cdf90a5bbec26104324bfaf92414e29971fea97baed19564098d32062b0e36d52bb15a19f527043b1723275f38e2d7da1b906d6e61e6ae17d6824bbf7884327432d4a0027966a494eb6d2a268f0c38921867b590306a14e31b7e165b95a7569c2542e1d7658c10cd79556dde716f27431a5dfab6401abfc09fd11dbcb04c0a2a92f58f7435617f81e9f840bea94915df352eb5f34114e31bce1dc14fc68fc85020e616568d96e57c15adc45ed4ad9af4b848f7cc5aad42b843d32248f4414f6b4763a8f2aa00b60f513315179704d0dea64a372268e85c10f1fbf7a3a706e4adb6829fa129f72e9aed5c9f9f923ca5c53cb42ddba611ac18e4c8902bf0b0d51a312eeb46bc92eb9deb72cb6eb12cfd625a09e5ff54de514768f061e35a1be253f4d495a320e710394fbc5ee3e8f7ed1e20888fd44dc53ac0acfce1609f6580f8f413c429befc078620bf0375862695ad47ec4f19020b5795c9423b004c1916c33b10ba3540b66a06f350191088d14a01fdf8ac5ce3f9858c1cd2f5f9a2cb6978758cf5fa590d3a7b68de1c6cbb6f714f6b17ee1d47d594b3a0f8611cbad219f4fccaf0a89cba785e437872b3108139e0db1275003a3ec83d41a9f06bc815796c22a45820401e8a78dd06bd3f72682f51843b23642b141d71414e54c07e8e691525af2f99217e621429980696bffca2f74fce10c1fd507b368a7b8dbec7a8b7264b979748bcbe5303ef68f9a4e001e61b0aceb27968f799288157a1a6ef319deed382987cf104af4a49615d12be97fccb2b739650b444c3166314b4d6fe45a0902c4f850caecf2202a26409ef603edda0a0deeec012c331e42ac86f82d8a241a0c9ef405ee99c92dbb7092f918b4fd1e4c2e1130fff30ede19958eedf0f40c619dbd392fc33ab382fdf73b3a826138f0d982d4573b8f48dff0143ac47f5e015df5711790cbcd06178fbdb51e2cbb0a5257d38e6e88c53a9a10912b70d9fd5c9a1eaa2ff7f8a4d2676570ab38ab77464738989968e30d7d9f8fc7cb1ea23bffbfc3d386ef7a814c91c3c619946ca4ae38b58ab881891a9571bb0202ec535c62aa9ec43b479ec36e2642965dea589223e61f0dabf9e0424b410a2d5ed14ddb09a4177e7d45d42145a10687e5cc321a389d0a9ffeb2ab8ae97b35f3645c4953e7f8766c6abd934881f69f9b5d48df9423b5a0c328765cf1607c2ff2cf00f875714a3e85f4dbe9ac71b4b08dddf872e4ece4e3ef238a491e34e559c1d97c9bdef3166ee2bde542010df3f1146d7d53c1d88c56bfc1eb44787f8df6cd32196deddfeb2664734a7ba4b98a00eab392d08957576cde90633e4841a473cc797aeb36db4a6082b397f6d3cefcb33e9c70a5a7bcb0a65fab1aab72219e82da52e40734cebe7cfdffe96a117d90c9bed925a6356714f0052db4e5bc5d93050c12a207c5a5a5d1bad410dedb1d5c41ec4216e0830c3f485f12e49031271d3a6688bddc762e443d15391bb8c775895df4f463ab700fed8f3c1b8e129a5ebd8007368dc957df753ae48385b849d595d3ab2bc42721a0a0488dafba598a56ae05fe8748e6e219e572ac2b770e2b7dde2fc1b10e794adecc569a21277903599b10747939ee13a4d8bd4c281da365791a06a8c6faa4a53ab0211853f1a0d61d87575a2b1b872205378696c99d166fefd7a7022fa9adb749eeb1bcbc633171f11b7f64be66fe27d6d13bc2cfdcc3f20ab307ab62f46fe70770178de8cb0e55b62749cbe046e17f04198388cc2d38f14abe907b07c36551474ef0cf66dd0b5781f2ef49fea183db393f7f9ca2775bb1c7fcfef8a6befc039677e7e6cfe2cf70cda1a5b6c62e7a314b35478f980ff2600d13f4d10be0b5a64bb59439dd0d1f55570f061efc4a8b6ce843b45f5bcdb5c9708d869f21dab103ad5ab038ec68f50c25be4aa04a9b79823ed63bb810f8fc431c3c677c39e0d14a630f29f89eb5ae840983feab4cfa3b02ee3346af3252b6fdef48987dab9d88a0dfb93fb8283da3505c1277b2c3371ecacd09e8cc542c1fb35c0db3c07c53fdab371fcbdf8867358565ba16984435fbb85bb9d2ad600464f7d7bf6f39fcd72a264e85ebc3616c834c17150b4bd43cd9d2251a2bc83b1df8279a77df9edcf16c42e60a36d2e5566c54fe4d660ac03ab4f87852a4bfb684fa34b9907f625209abb36b605748055dcb57b47ff8da909b7048df9dd2bd34dbbb81fb076915dfc1f259281e0327961e1e70bb5d45161ac11a8e2544508ac54b6507d970ca9209d177861aaaca38eb9eb31d3c9d08f278750d076c6e30c38aed56d23c4105e4f945809516bec148ea635a5a65abd6eb1ef14b12446e0f80015e1c13afa6431d24db8637a6de0dc6f32df78e9822ee144250302ba6051293ab8d42ed24ff1053ed927db5b0d15ec34967bbb080f714f9dae7a48288f76bb2d252866a4f06077ead2a92741215038f0128c79abf739bb2cba98485355007a9359d2012fde3097867250781fe1a9fecc35910dffbae7d6a96d85b240993c4f001a8c061b84b47e6375c8b6505485e6cc5a6c25b2efc5b86f9375d379bf2ee35b6012f124c045dadf93b8603947be3516519b8ce8f4f5b46f3bcd738b79f792aa0fd3de2bcc358775e8e8962eaa53ccee165cd6426ad28b93b8c186c9669f774bfdb497249a3953099286a1a1b09d0b8dd8fcf25d4550442aad01efd351f09bde8ffdbb34d412da84c4c7240c1a811d2eaa868b2d9accc32c6fdc7ab82eea75ce0050788ea4cf41e1d41a9f696ae59adebfbc6100342197eb6651ad795f5d6818caa6b0e605d665b0a0cb32459e31cd083630e935b7415110721cc67a2e09ea6064fa245c0a628b20a5921e80805bb56c70a0d45cf8aeebe9ba0305cdecd45c41cca78fcaa717f41c27a0954d6581c8464e48775f1590b442b0dadcb288bc11e80322102a2e1c009ad496a1e3f6bea4085b46e3ce1d77689778e235e51a2e7e53f0b33f25f0669d33ae08da290c218af5782c44fc3ff1f97dc8b12170bbd43bd051b29ac9bba0cf838aa76b6e5c19ef5091c3808ced6ba8361c4c93cc47fbd00eba026362869ced2ffb099b7097a2ee279d7fbba92b43d120c83dcc0036f666e49becffab4449ed06474dcbf3edf53bae9da25ad34daba48382db06bcacf47ba1507dde04517c2ee24d14b85da9d77540270c04bba3feaaec45d259055aea1e07b4117572cd13e044277f843d54a090629f10507868d8b5128bdadbf0080c9eb9293f9fe63c05ba0182dacc55f538a26ccdc98b0e8cff787208447834fc3588ddd3f971886d731e4350e1a223a8aedd79d791af13971e17ea9f7f93a7d3aec463fd5e4a6544123260a06acb1d317886be47db3de5d57d77ee4bb30ef7d6558f7fa70e7d9dab1caa267186e7a673e59a213602de265da00ab2a4e4ec6d8a99b3b98cb132351e77ea4904f3145377058870c4d717250eaffe28afb151aaa25894e381d79960034034bd1cc5765f38eb0bcb2e823b53b3ea53378e98412d197261f7f41b45cf874a0a95f1418e76cec525eb002fcd45429ef298866d188b216aff90a48a407942324fa0bd7a079dbcd6583737931bac94565d578739ade1360afc83cbea1c177d527a9555ee0f3fe7e821ba01605ace9eb39712300ca52d4acdbb440c5b78ebc1990f2545de50f27bdd2f71d2d4fe501009ea905710b550f6c85524d3b48c82096a92db24ab8d4a974719e5d1ae2731f208aa2fb04413267a80c39c0fb47f29c67077a37d9208d1bc6c7563104b9ee93d1658c238d2fdb3de2b0b113a3626aaff90729724d7032e92b1d78665d3d8ee54ffce767f9bf3d813f1c38d5091a4b40b04ea42f407443846cbffb3c5d62821574a699f4d135fdda848e0c44d132aabf6726693aee4947c924dec872e87888d7014bd6f818902b519333b44c150fd503180800143326b94fca684e14b4249e432234ce8f2a6270b08d9a896029303849b35d1ee7889c7739d09cfc5149d356d91f55bd26d599724d22d1ec139ca43f2343ab0753901a0c0c45f423913d8b26ec8cdc52b6712d6fd9b8666d7111384c9ca3ff20490a3086fb9c14d40791e89f93d7be686774a808fc92325b22e83749afd4a32e96620c6d84279bf90d6cdb2135ece5e307b43a061608a7c516da80eeddfaac41c9e047d2709575fdf69bb79fc7616ecd1486a529a0926ad640d472fc55c3d85d0789e1fd09f2424ce0ffdd441faf4dcb73c82a9234c2690f5e5a5940ef01ef5bd3d836c167b514f98fd39ae0dc117f8567a421b62c5012bf2e5d1b92eb5d012aee941785d15241899a9cfced703d43450dde8c77d5537a0cbd8d39ecb82b54b390577f50d4c141cb9813acf02f5f4d4ee74e928a754abecabdb2a6dc01d86b674fa1042afdc088b57ca1a2f76b345071be101f0d941fb76303a4f5a7cbb1ee5841946ef6443fe9ee4dccd92fec88b7dbce49bb77843c3542b7814324bb4a81e665bba0f5c600b0efbce2ddf643844f207835c7830f8fe6a4396a1f2690d67439e5725744415672ad256f14b7aa8e971e784ffc7a7723e789b4b4245d9ca8da53b713d9fe3fa683949382b6ba5ba9cfb9897e78bbd9b1180ae1c29f5754e1a80ddda9bd956c635bcc2c5d7b8d1652e2da64edd24cb81c1806a7362a16ed5aa6d96502d89e7fa38295d948dce4c4b7dfe90a0d49f296df2318a22a8b85c00e82102b1ad884e870400e2d9ad07d4175335cc68ad2b358e073ec19fbc33358e3298af02de96be07ebc8b0110122f7ae45b86a332e6ec414aa1c7ed192b4b6270355ed7ea0cc28c7039c8a7c1dcb20f8b851ecf2b4829e5eaa6bc0f8a96cca47be03fb80215e2344b0330cd273cddd913c188f479181a4a1040f2bf148147624c7a1dced8a69788c0c8ece464c61e62cab60633cc76055e84034213e380359aa81872f95728a38d8694bf9feb329d7fb5c5e04820fd5fbbec9288f7c58da19f2408e7aa6bcea06fa882e23af4e9f2f54eb45082a12c2a89198e68b1bb60567faa933cc6c17d045754279136a1ac057fa5765f549f39029137204096c195c9b5c779fd6e6b2006675ff75664a10df77f2783be29d70adeeb27cfdef3188f3989420f13b9dc968d0ab33d75d9bafbb6bc615c7962360a926f42bd1e61df31033c1209501c062a91070f2921dfd4ba063825d4ce43242c851d6234342db049e703828bf32cf32f94bcd938a6351b9a60bb4d95557f9fd7a0534cc7a353b9464efb1e441308bf21c02242564911663d80fdb9e9480c3287f74941ca02884629d3cbc8b43c0c8c696c7251f2e5aed2f6f8142e053144ca586cbff365973b902b0f257d335c0a30cca2ab762094ef43fab224183cca7ccbd628fcb478ff242c4cdca42b29386299c0445283e4694e919bae15d3300b9ebc116766cf7ecba24cd0d4f18fc8647c14fdd79e52369e5d6c8cacaa31ff6c153ad2be3a67324a573272c7a9425589102883c6f54492c5e3cfedc19611d51ac9af446a04da0db8b2f6b70bfae8b519dcc35826e2ab421c41944ee3bea5d163851e6280ea5bc1f9efa6400806d79bbc75afb8241fd3a9d6e4923e60fa59feee4d0f74e44bfe6bf6126d98a53ecd15e0f0ee15a5b86368a578f58da0ea825ec9cadd5f27c165d476ce5910be3bceec4b13f1de8fe9b3fe3867d359f087f594bf913324c892bbdf273bd3b57ff9a6edabcaf8bee28c9bf2b21e11172e4b85c5a099140e41419ad28d4ca738369c04734927457c8ab1d20cf7f11f8ca883472a208e9bb0eb1576c25bff62c7ffeba98e08abd49475030e6763e9551760e1f917513e996400549bbf37ec2f3ba31f452d789147d523c6f3f686945cecc036cf5d32fd84cd63262eb6645a8222d8809c9f677bc467642cf95844958e4910ff848cf4278d880cb644511d1192e38c54f4bded966a50f2ae62372af821cdb6a78c1c84db3c384e4d0cd609163dda7e8fa259699b1d8e6776af296cd28b590129914b6d7256979c2b73f0789be7e54f67d3a27469f132784dce6e8ae40b6ef46bdb9fc1db4a6f33d216c0e24434a0b8c19853245257401592f1811da08409c648f778e6962d8f09c08984d55648b4944d3f9d28e696701ba1d52cc3759e77c1ca4e1478317e4b7d6c4d8ca46cb772d0cc4b3e8732348d427c39eed5d33b82afe30a3f7d991332bb8ed9517fa655f725333b308bb3efad83d3c292eb8b43b66674e0a61cc5a03c72f5b38f17bb6f981eb1e0d865b17f5c5094f14b73cb1a10c770b991e5f71a140b37c14e2512d364e08cbdda56156cd1a353c94dac5350813691762adb9cfa0dbdcccc9fc7bf4b7bcf641dbf9c3e09370507eb3d56a3c355c9500554401433011a64e9c4f49c56bf6e217afbab0e7767a846308d0d1a1d59b75e5d85994330f7c63a1a2e13ff201ed3dbda371debf800b6787bedfffab4c3ad041738bffaa24fb47b21562ad9ef0e315a2ff5a058b133c1c3a64250ef0d80273df3bf0501192278614d907166286e1aedf0ecdeae8d27a52ced671c6ea300af5b7dfeaf3e1e7f15e06b8ee0199e2808722a4d698c13c4e313fed7d041a23d74c4187fad9a6b15eeb89a47db378a50faeda259dfe2784691c14186436655579e5c5637a0a6578966ec3490e1025d9c066bdd79f8ac74232d07c4622f49366f648e3a1a7199d74392c8a3d9a8d8d8e65e2750c1baf67307d65162160f53be49b95dae937c185fa902068b2a32b7640bbe5001f067306e2ccd5c610c0a1dedfd708dbb4790abc5fea51d0ae0a420aa4811a2b811dc323e8d83ef9d83030ec8a43e0d87a556821cd1d049a465abdf13f3618bfc8e3165c4544583306cc394e5c852d3e754855ef0552c749692462238555a64e82d99d84411c4f7485a15f6605ab9705b28dce7c4807e305ff1bdd4eb4e631a080ba575e7693d1f8738c4a3d98ffe1a81540aeb24cd2fadcfc986ff0a35304e40cdf5ee4a7f7705509dd4c0717c583208fd24cc602914794d3643b2166457e88d1ecc4d06cf867e156b99efab726f79d271d8406e2d70f4c906e3895daeb22875e88c06f83c953b5311e583275b20fc1e04203a7e6866a321eb6fd0863274ac9aa617e58a30df7d031b66e995c79f3d5fee04b234de86d3c75336b2ea2be7127ec8dac1070eb3b4b1b1b4ce7c1cbc3c5cd10d4fec327d8dabf89b0223724a2349fa1fb728e6cbba5ecc0377ac4658cfe77182854874c8e5c91802acc47a85f229061c2ee96ff4f09d59235b2f1b2aefabb8d43b6763a0e15d2c4a1b856045103f040c59e8a29646a1c594c478931e44acfb7067128a4e8dc2f8a058e3abfad70a5e5082e8d4f5dd84ffdcd5608b62e37168ede6eb262b01e50e5a6ae7fc20caea8467b581835c2e490f433f9c6161e07e06a8c8dbb134f137782f5727d96059f716641ef30a42fcd13958e7b29efcac828026f05183df9deef3ce971863e2e8c32a3fcb1bf9a173ac38dd8fd2e8ebec8883007b6576485f4738e81b79bf74aa51dca62f5799c49b480830b1e6b6160ee836cb3ce2b671c46dc64c6466633f00e37abe6c3fb7ef5c4cc8522a36a0af758a0113ccb4b70ae701ea3ef761ab22f2e6887074fc36a70b38273004bef5a757902b4b8098eb634fe90d81483d62f3b84f62a373f410b33205375c0fe8ddfab4d3528a6652896ae94ff14614e13e5b5ad1d2c5671cac817212d8be4b468df37822f3b4ae1ceb9a7eb10dcba076e6f6158d24cfc50130067b0788c8da805976557629a59e373761a001438313f8bb486378d903e53784f1a0b6fc83960afd657e2aa00b3e0bf0333ffa1adedc1ba67552600e843a61abfcb88cba9107057d64a2834216e147d00a83828cec7c1121ac42704598c517b874bc07b3f53640737353a49b7ffbcdfa730554ca7d2772d686b876234c55fc8d555c9ecf5935b80c44cd4b514b5149f589ad707139286c704f5bc7df7c0cdd5b90bfa69dd0c38bc95157add498ae8df28632da58de0b217e1fc9afedc9d5b9c4ffb8029c516f7fae658777037250d0baf003feadd5c7b627e446e293f217961fd2fd344412bb393f0b67c0ec5634016612844eb40708684ecba00b3070f76104ce665deb73c03bd95ab16d4cd117f846be82b62dad97efd8745b8de8564081bd31e8a65104437709fd49e5e561185ff8a7d70d29eba521718bd7c16da35c748ced583d795b08db038a29a10fbb542be38c7bc9dd381fc0ee54d6dcd88c82baf884ca6252a443f1d461526b70a44be8739419e7d0ee8b25fd2bcfaa5e428ceb5e7f558cef05e98f0ca3f11da79d6d2472f49564b82ab3416bfbbba40fbab1c5729762a3c8907de13fb3b59f4fbd08f42624373faad19a331fdfa6845352f60dc390b2d8fd581083892c3f64ab35fa4282f9b98dd8a6df38e2bce1469fe90ce365140ce4f93d145c36c562ca0982a18fe9d8a5e4c91b5a4b1a9b1a97cb9edb88bb41255cb4958b37e9907eff8500306f14b9a8ff9abd8d9158a88f4eadef1dea296586520da7ea4aca9fb4a1716883af583cab34a3f1021adf5d2f8e6f00c19acf72eeaaed774cb5358445c1e9ca87eac6a46ff2e1de8beb92d903dc243ee0b45826dccf0046dcbc55cf599638571a34e0ebfd89caca6a690766d6fedcad93144d3eb34c200aea7ed6e263d1d7aaa0700272facc50feb5ddd4bea189b60a542a5167e52fe8c221cfe42389388c3090f964bd28a77a240c8ba0290caed51b802c6438468d2e0dd49e2fcd31f7d744273287b8219924c458fc01492ee55e944167d6a1bd2f02666fd41b137e480393fc2cae902ce60684a351a6deb8b81ab9174b6811e7c42e1cd5fbca52e6d5146a56248fee4aa048e34d754f0c75eb1390254684069a5adb5cac4ed1ea9fc91f4cddbf203341650758bef1f0ebba2e7b652c2bf8b57f5688cee0c1ad86b19503905055ca5d7db6404fe0a36345c75f613cf56983957a436e082e1e76e434e56760cef3ac7f59408e35dd0838be1814144cfe1b3c22bb8bca48beda987c39d66450aada707b46f46a2cd2dfb80f533dd4eb30c39a55b9ea3d073ca18d24a5508421b59e7cdf274f7ae9f0960b7b65635418500482ed191e970cf2395461a063455b564a7cb05214c04c000cc3a352727f28dc420892c771f6bddf1f9f7e2cbd1f00206d6e54c1bb44fa71c17c003a66e26258c4ac65624dbb4601e831afeac59c5cb0b74694588ea3893df6ffc385ac20051167bfc7078f7d50f5874a2717dc617e2ce6d7bb3fee78ac47133da881cc712fa24191d39ffadb72121e9e4641d6b2ec4563ffa3c97f97a8a451e7f681434f4d04054fb22b0c9aac77ace57dcc69b5ebc7c8c7da627816f80881e8ca96ada2dbf09d5f3f2dc70dd4a69e9ab900187ecb14ed91e0fe1699b3e55908439f040e415e2f04066c66aba74e9dc67cf9cba8194ad7239d6f5bd4e83caf2cf8e9415b172183e79a092bf726809de35b582afd345c4d19587330629b0c3b41f4e8ba8f85eab56b33290135030ff635bc070f8a190d2e816031a31e96fe4d0a959f7d830ca2b37e0a9b94330ea308483fa576dd533e2203034c57aeff98affe3865327e45d85849d1e30c2e613520fb1723e659245f93238a58bd62c0216daa2a351dabca5718872d7161ce89b7df85a255d8730b69dbbf7af856735598d24fdc39bcde2926f0ce8118b89db16a6de4d8bfb4b04110b839ae943773707692c8b70d9b293020c24db394d746bd1c3b96a56fb9e712b1ee45b3a38cb999194f556a5e0690e09bb3c931087224826f0ca4ed1eaa2da8823ee055b6b635c9f105cecfd3c6fff38d16a047c6703d9fe409ce3f778494db7ca797eeb944d0d6b82317467bb4441cb8b696d2b0def24a1ace3a1e2ee71381dcf76f07007c241a82694fbb011d8ef708a6ae81e2a9c084f380d9aa34f2a1d6f0f2d900fa0c863850bf43369ba7167f0326db7115f6d7e80d3075949deb84a9a8c48a69e4e1bc3fb5e1d71bafc31e6ec11f0b6de556e4ccea3f557b987216b15b50f8cf2de3750a8f60c5d573c08b11437099b0080ab51a4789e6278e79ff621289cb9efd8e7a084e0f77fa85ee6c229c32eabac1dca1c6d028ae41095eca43052f60232da69fa5d73b8b753aeb0d3b4a2a9020136c3f392eae507498668fd7d8e664eb8a644811912388d14bcea731f0a02d0dd41b5e4496221b6019592dc6c78828c82ccf271e4351d7051f776e54ba5faac32cc78ee1ef018a6628da01a5af581bc224f6f05d15dd264b80fd827aeba2b5fd256a8574b9e3b9de46f92f05ec1611bdb8fba360f045f064a9c9cfa0d1bf2b2601642fe6394c1870eab8e666d493492e2c5a57b16ed810c74ff2a0c5f4a8932c64b6897b89d0875626a46756438971e88b1010b22c7be13e2d16f98b4c65876099b4b85ed38784fd343c564c73034293b0b609e2174462014c4890637a18f6c54de46988864e2eb1cfe02c6eb946094a6d37bb5fa4943719f3ee3a4360b5afd3bab888a5be8a3db6219a00b41cb572ea3ec2ea8b4366bd2f1f2cd47f6327ebc421ca009c88402d835b33823914274511eebbd246458993f84e1cf7f9f082423251c5b03a454b61fa7bcb9e58f0e7407a17c397f81ed85a009c8e5d96683cb5abe69204af217222400b8e2ef2a605a9f721d8fd453a16ac49948d3f620b84b387c1e053f0a153486f251414d88bcc3efa40a51914d50eefd1ecc336e8f620f905d0bc211401d77a0de54c4a249339fd75d0c9c48ccd2e8d9311fa2f01dd0353c701033a681f0f9a12c9132b3cdb3cf699b0f7353d4041ed1e7b31073d0a5794979e4531e5521c348b28cd66ceebd5d8f7b473026484eaea125f5d485d2d97fba46184109b7400ee3908bdfeb0a85bccf67893dfd111a8aa853f1b6203191b2b3b67e2aad038c334c57df24199158a593b86876bd9d453af2c04d84541b1c532d29ae80ec8bd5a2de93f3cf08ab69270bb59f8d201ced3cd8bf3fc94bf86acac4930b4bb0c338282161a8ca88620f422969e5beaf8401d619bfdb07f7c824339b360f63adca5dfe33da9ed951ea0e18dd0cacddd9024a96ae164cbce1cf7ec4c95d469e128bd4aa07e635af8bb80e58eb5cd3feee54268d31a869aa0bf48ae46300b5c37908d1da2d14b006458577c7823c9ae88461a101aaba743728ae74d0dac90184b0cba298c3b05c251761900add0b64dc8c7e6e0852360d32abaf9d89bb0da2be58a7989a0a7c482956b3ca422e35ccd8cf4404eabce4ced764621a757be9b0ca10ae81612701c6277bf490869727047e1cd3a6f5258de02538b4d047e14fcb283a812ed73ef86dc7380e430c09c632b45bbfb1fb317ca4a7813593b85aab63a1c82333c0896ce0fb24560510dac9b3916525f82f44b48527db3f11cdb849cc8157e402c7fe0e3c1f2f671a2e7092b90a519a92cd93f591033abac542aaf88ae4744c3b9df942479dd88d5468f2f6a1c460686adc89e8efcefcceccc02537f3e5afe3c9d4ac37d91455b0dad2f0bf35d82b09a4b977f5342dfb194c56dc96d5934631c6c0339351bbf2ea0192b039508f08bad917dac2b298728bae06e9342af7e44aa7b9720edafc14d72b115df2f7d4aa64cf0fbf22fbb32300f229c5fe322474c7e1143aed1db824bd494fdc82a2815aa591232cb253de964d88db1f7bef6e4daa64e07637991bd882e4fb122f6299de064b3446467f46cf1b087a9bc2b8fbe279e432f0975fde86a4f727fbc5680915a01931b491acbee57f5b87cc398f790696d75599dda9b188be8d6a77d2fdf3ca5e946c2ef4928483950eb4a2742841fe4462a7b25eeaf86e369a47e22da5aa2411ea3ae65adcb9202e110a741b78cda73ef18d73123ea8e4ee2356e8b16abfeece094406f8f4692057d5baa41840a6bb82e43dabd8ffbf579975114f0721e210849a63a830c9650a893f9d7304f035e914e3ff767a33896690c311e4051e227e0d2ef0b8f350e2fa4aa4b18aedb347be351af85f8d2433af6efad6e63ce6c29dbd246d29b539f1e40a2f5fa776976e6c3787b5f91d52961beeee8622510a4a84a73160e8db8c7d16417908080447a2143c071c282eab20841d02d61cdbd6d9db7372303d9aec6a6f57aa6c9b1f23664dc4104d0ca764ad7939553c484e07ddbf61a5fbae76c1ce551df24811215eb86dfd9beb2d3888e565b2fc0bd70601b523f0eb6cf3a14aa50c0e8493855c38d61f70d51ca7d7150ab10bc38aa685cc00faeb4d92ec0f2c991449578f431763f53b3749a273d03fb2f6a262d738cd111e1b80fbbc27ea06545e8c796b98608efe850e6a311b7c22429351c45f5d664f95d83a84ef9446ce284a76fced3091d3ea7714905f7b3856aefac390463583ccb56341761df1374d1c7c5397791c1ae33dd69ada2985ed0e09f8118a8076d07e0dc49b50386f7d1c8e65fe4eb7a7e41d87915c421ab95382dc09b81c29e4fd91245b0c5f8f83eb258a755b35f9df5adb5eb4d96abd6db930c8523ce93901d4a3a6aa4963e91749e16004c51b024b6e7384f5645f8b2373a8daefce9ea9c8c6007095f936d7f648ffec4180e547115c4b82bde78a72c7bdc650b706e90066c074a0ec9aa21b6660fec094254946b5faafc82a4e9603f3dab88801f71ae4d5fc35cc53ed6fc3679ddff244016eaaf809ea9d50514169d917f70d353899cb0e098b977f10ff094b157166b9e880d87a59db3ab35cb96505da8f8e975515103e94ebef08d8609ce76a9bbfbb3fdf49c706697cd743e2dd915b5847d717a7990a91d16aa0e15c0739bed0dc9ffe91298b59bd23107126aa151ca71e914321168422a3fc571f40a49d2252c945c1708d280cc5518b2619a3027fafc4660b203da643f5c3842a1b7bcf6a4e197c103605f3f0bbe10dbc482577360830874861cfc3e1359f3abf417aacca391386183b1e5705027219b751cfb8d82edddcbc97fc7f82699132789a4d44e8805de7c277ccc12da4f5dfc1c94a14b51bb919a76c8fb13cdd9540a2ec1b98cbcf83c352bea8962b01e9f85d09cc671846d2e9385a70e264eb38f57fda9346bf4a1258fb33bc488e5a124526071ed1d7d043412c206a86fca53b42e3419b64ecb098fdd5ba50e062345d9301a135d07922632d71940f4bc90c9f59d87bc4d3bcd762c7bb01bb5b892b272a96ddb7dfe8ba7a710411914e6165f7987a8d64c57c12ac5f48b0795808de989c851fc00efde3998f8d2572028faa9f494eef63c40586288b1960617327088ce257706b178f21d70d544053c471b0e737324324123a8777d01d183ffc4087c71bf9e15665125dfbaab3dab6773070b4ce0a5b53bd0fcb0dbb0830af667c16ff8f725e8bb9e648cec5ab74e4b1b33a6d7c17a779462308838f21db9914407e5b3b0446dfe6709d685060266093a1cf355ca25a3fde596594a7d76ace2d0ec33152b460045c402a9f5a8eb84761c32638b73a6d039c871a8bd78c2092bd1a55e999fc4db50bb519f32c594cd72f09253ced04ad08f3b04742a3c94eff5ce3d7cb2c8df4451f264b854a4104069b49a295313473ef50b4eb00ebbb89fa9b5670d8707feaeb676f4ffc2ad63f3bc33b4b3394c58a1626eac75bcab17327d53dc919db90f82d969a6884772ee163c61f45525292de2184d2301bdc152df1311286358e158555843de67adfea0d8c910a20ea64a146a15728dfa832eeebd1b74cf7e70a1b0068cb72289fa19d06d810a403bbf7c9b2db371c0a208f701891f7321247d89118649385ab51782a86bc8d56609b514f91be45d0db53601db1a0a12bf89a85e948085f31b8c37df54f75e4e8de8dd8a4cfd0f7b28c90102e337be77fb9f5f3a0a92806a1ad6e3ccd15c46b5c631b95f76062e795c5a6b9fc38b28178473fa303ba1c93cad3abe80d616ae8c5a4ddf422e8693019d4e34c449e1da46d78fe8f458d282bce84229ca68d857d4504764ceca5647d7ef7fea55e2bd8e82500f3caa3eaeeec5cc5aca905f13ee60ed188e109ed757d8b0863c774796aa634f161a4e5e52c1203a53e1fbe34535e60f4c0ca70c269d2948d3d38affede15cbec72c2dde30c9b5009c25929844f7d20e7389e429d92f0332c62ad219da254107b27c6380d8f5d4a23e5c5d6001129646d6cd6be8aeec0435469cbe569be87dd944adccc3cf35e1cefca74b2d953976a863a56aab0129ec3a9196d12fcbe7626218e455cd1183034c5b87eeb37e2fba937ab37c36ae1d6c59124563611201477ca3c9478d53a9f50b3501d84f59666ab8b40230f42638037be5a52f486f3d000bde24173f877b1cd7f6421e1923f21254fa6d6de8ffdad421c71afa1ef12c2c6921ac190157638ff7dee0a1ceadee3af3aeb1bf9931599c2e80abf96a6a9f93223dcaabf2e1239a3d2250f8e69edc352f51297439f2e64cdb38dafcef1a5674bcff13d15338d7aeae3316e6080ea120add2e6aae3f81cf860678cacfff2bd15aea8a0a17c1e79080ece003aa5d7bdc435bb69202e63117ca486c46af613e71e85241ff4d3dc2e3259a8906e08c2dee61e671cfafb61e18d0edf9781d6e6b8210903adc4480ff05304549a5966781865a6a3dedc4d6476e14151f13dfc61169ad604b64526d2717e40e3569864895bd981b80f653f80f9611e453861947cd20550ac9d89a47d69fa7efa681aaf52da40a74c71f7b73e19ce15812cb2d98bbbbc3179993e4fe0f98de5495b49aa88e960d4d2c6e3c85e768aa8243d78a13b6cd151c690eb96f164314cc1fa769b8feb7d96ecf08cb03ccc8214b1d242c118c500a2df3ca30a333ca6cd298c6b164628bf357289ee75aa41bcbcdd02ad010ddd55a41ad74b7c97488b621a9995a6f3700a950874f2d92d80840a8140399fe3d9ea73cbd831987aca3529a4b0f64b4ce929147fd0602209dcd6e748a7e00f7277f48837cc7817268d7a94069327a1be8770371c56924ac4bc81a099d853acca5f99206a02c178623356c1a537907852b9c6cc22d34da1e5477fa3abb77469ce3674c0a7e6bfa68345b74de10838ed007aef04b54f95b5115cd89cf6426977b585565f61ccd16308be1c110f745c8143b957818cc9214d9e8088491305efb77e6ec42924330f74e22883f1e1bc512a4867a04c2900e4053c9ecea47b46a9396bbf15ce566481987260391bdda4bcf02b28a48723823035c2aff5f80311cba83d31021f520f21a3fb6fa8a5bdf211e1b82c0d93903455133a743dd5a9cf5a74d05034504896d5e237417ab0f7e6f2dd1751a87c202cdff85bf87fc822475bd68ccc204d748e84e123f5be181aa69af70da2e3b8245dddd93be0aec60f8532a1f870fc97312095c02adac1096b355bd9f52654b351592a20cfa68fe1bbf9666c4350e29184affe441a0e1cd72e02900ee4d0d12e32afc6e711c2708bceb4bd9270b7d83b30e1e17306c29a1dac89e71750a10033522632bffe77d0f58fbe2c73fecdf5bdacef1a333a1b36b92ce17b027f81e41b03a010579494c110a0dd8b074679b61f26aa514a01684e9dc6889f379f2d6d114902f3bc3c157d9a3c7d4ac86d5ef2e91e501e766bdea94261686ea098700f6a3ee9fe6299ebdd6927e29f8d711ca6c467deb4e924596cc5a36c42c64d51b27bb5929dbf67a433148f104d0767e3443f98c8737efe5a18bb8b13689c1d84163808aa5910deb7611a6ff3f22d14f666f3a98a6d9faa56c2d559271bf81f43f41e4e53fa1a94dcb28f645f6d6756bc41feb7d854c54dfe0344c053e69a08e9455c5f08dbf854c8705648364e125c9f70215ba94e503d4c8aaa161db2ad7df7440498652692c7560f0b6d05bf6f5b0f165c03258f011304f960f79334faad07af2e52cfa2179a62497cf7212bd987fd2339e47cb876d5316a62c936321435a1a46b2a006eeac374510a56777cec57e917d62418a505332c565d07636fa7826539c0bb934b2ad209faf366f917f2f24b03bfe7c418ebab3b697dc16f3a166dbf5a96222cf766156da9244fb76a00ebc131e32e8ca6179d34bf51a6706be92566c2c10c766640b0ee48f60db35ff96e1b696a98a3868315771c861d7f9899da5a07f3b2b3b2492a9aa32cac0db17b2ec4c59bdf568185119e6302dad42c99eb2c8f000ca45453e0a74f34d6069b06ff13020f6d11392cda20c4b8cb14a670685c3375e7598f75c3051de3ae6c284ec212d20ed4cf0d734f4e5b612054c7edac2f604589450f16c55bba2a302306e862d123a9de00ada7a805337cb32b1d1311368fbc929c396ace74f213dd0345f8fa8297a59b2ff3c8f768b4803be45f7355d03901ad3d1381a991665826e8c631e0e642a8aa0c2d8b46a088d0ba5407588529085e9914af212776f81038cae6e7e871e0f257e4ecfbb2715881d59b68f186834840565015715117726688ab50ac2097f6b01231fd49cf5b2d4c8c20a489178b3823305a4b2187ba44cefecdea2adf65822e7362b7f60dff2cbaaae3ced7b34947604828bca144e440276ca78837d6682a6d682644acc844cf701459a261fb7115e01fb44ed395e7e71e7e30de8d9fd437ff17e78c0529ea9c3d0004b682ec3a6bc34c478d4f516f7763e06eaa109f3fbc4da33d27c8c7174c52e596f7e6394e208b614973ddd7560c6a7b1877438aa410a262f297e64ec26bb64a04ac9c8d98fcdf58a1c18e6ff26192e22bdb50240be545cfa447df1294ce62c020d7344d3fcc24f683207b2d40cb3e4dabbc9dd36f052b87d8732072b8a5a40b59ccb6280e3744e3bc95c0379878d99e629497240d863666d9b43ea41204f85ec8b342e1d53753e60531155e2b080bebfbaabacd972189ba4b73254fef657a29ca307aacdefd4e8102b61eb9d7089a49a31d03499ab00a1a60f1b479ad4168e02295c32ca70bc05b93a51184b4b2f5038369b86704a0a57a266258b6b05e25ae537020b5b07f1c129b4c2cd03c94fed97bd145797778cf4d70f4486737d9a9883d7608d6941666c8fc41315dc6b8e549637ee3d8d171a6e7012371c57b6b4aaec60740bd41931c03c6622f96647b031f7893a0fd9e4c1ae4473bfd5e52c43ed84a28e0eed03ac084dae07cee96c4da50eca68b5b51779ac6380ddaa9fc3ea8527a0fabddff2ef0331763eb5be335c59562fe4b0931c81843e31386ef709b8276ad0f27a965cd389745f8d76ee4efea56f644afab6b2493caf4f60df97b3eb22ce569c75c9354e3af813fc482a980b367ff00698fa8e16c606c60b88ecacfcf39b547b8715237da33b356ae0762b0b3f28c9b1946af229010160f2bd608679490c666efd5c9f23a7e83d4f9070a487142d434dfad45bc98c915bcd58cd3e43ba44ef77767fd0286c2b943f85c62131622aa70b319c6b50764494c7d25292b5cf6f35189d8e6af8c6b68f1e5767b0729933a8787ae07d394623508b2207a002165388370f0071e371683a279895041340b1c242e807c763f93c0d10fdda9b661b5f8566f79e173ce605f747c208e670462bc13cf7b204932a107abc600f7dcc5971c3401c2b832679dbce0c17d4d2047d44fd830c813e72b607ca793f2349deda2eb2c62b596b499baa6cef7fe3789082cd5d69ef70db66b29ee138b9110e8aab5737735be9f09bf2c781ef47c37615d68f30eca8b4c52b6d70d8fb14dd068c0f3d0fdb3239bc225f5059681fccbb5d219e847e431af72fcac0d3811c33721296cb03660adedac36f9cc8a599c740d4c030ed8edb96372fff776d2a7f5cb0047076fc98d90a854d8174b8678991c4e6eaf01c1f4f046d73cf73d405a076b06765bfe3ae4e3262c93671d4ffa27a7c6056c096545f33f5222806574087ad274bcf3b64fd70ef370e4137284ae16d16cde802b238ae1fae0cab037c2d1e2fafff1375f092c7c8c35989dc4f883120fda976dabc6eabe8ff4cbc61e0bb6b68f655a8d5b862a600521a76fda481fed05c9a5e9dc907df953f89a580040e4fbd810100f75063fc46717295dc6eefbeb3a5d80082f0a7f8fe00655d4dcccf68137648c88e3d32fa722950c97110badcedbb67886dc0476cd535c5c9343e9f500a4ed2f2ac7e11520013b48dca4a53e5d61142290f9336771fdd0c1b83fce3971911a292cb84c2f4adb24f26a3fecb4aa5244f8e305dbd2931933760dbc967a19cfccb4cef5d60e5ca2895c2f510520686c1b23320d8b5223ec14ed37259b2e3e69f83c7a18dbbff71280c7761ad2c1215343a7f015a7606a7001abb8622e58ee6a7a2ff754ee95f41f96819e9a8450fa43056c656642e25551031c9c88330b2f6138632ba8c76895c11f5438e44adacfaa1b175216ed014abf9c85e5032fec9684f37fb57e3265a138ca5360a5973b18356f6ba0ed8784e4c0340d701004a68cc0032bdaff6aaaa70507332892de54ed864e588458768cc294ff1c3d27ec4a19c71468ca77479d9120d565a4f9be9bbe1c27c404208df6f886649d5d92ca305b64a639f24e57619d8f099a1c52468da9644ba60cf9955db52f0b8b3ae07da438be020f8077a294f58cbf68ca2d2adfa0548f883e974ae95bf6a284782f716c4589393a2e574c96a8e822372c8edc1dbab23fd01fcf4347932b0db84c5b6e2e056530058781d56ff6050842b78b453aef0ec0fb7e9da7f1fbe701b014a079dc11c0ca823ac1f68e624dbb1e9f74fb12b9e95653ddfc97210c9816477efedf1e7011fd880bf1acdc90e5f8794a8856c8fc0341f6ed604d23ba4b211abefdbebd01a2c247c71081a2c34c3f38696b9caa1b33b7379a4ae4189d81385ee057191eb3d97793f000040ffbf226cd74b71af3f73077a8fba4dee48a5a8b6f972f86370b42e0b0fc4606a5b6e53e7b20dac28d44ccaa2b1cc25a8ba81e703583eea0d0ac072ec621a3d3b887eb8ef4adb1c832827691b2831497084caa562e503007974d789f9e5aa63a0e5eb63a6db1d57af3be14267ac544d4a50d7a122e3537b4813cf2ea02462339ee547fc01b7c01f8e0cfd78dd62d73ed6dd3869711391378c167509763fc726de86b14f40c360ca5779e561756ceee0dca982c23d2fd6e7d275d5b9c73648ae4796231b5dd4e108ac9452e74b312b737be105198e33a2df4940d31250221672c1c78b90a1e527da3506af20267b78cfdd8b91e1ad97de7dd60465d8f7dd60ec7943e44fe170ee02a4a6b02099c98fe28559bcb14f38259e4b6f9588f81ddc042accd5f210bd6726459632a26aa6bfe0516672927e258bd0cb11e3706816715856fb0778aa96fca115ff131a73a51d291ac45ff745d243eee06f99a3aad6677416976f71c4dadc400db35ad7d5a271a70fbec278cbd41605ac22eba710e8ee3be4c9b0b6a4c398b973553b6879505ab8d76db22a5be0d9d36f70e5d65e9a4e8931cc758517ba32ff4655463a5b5314152006d173e6a81f04f835201d4d1f5bcc16ecc0f6edfd076b41978b33a05f5e004eb9efc9e9e1255c26061fdedd02118f40d5cddef9763e15b0f5b773af4cadca9fc5313ed7ca981ea3dc5976108c85adc81773262da406fd7cb7215a4e20b5b9a09e80c4a163740df63113ac22d99ea43667282f4421118bb5cbad9748568d3f9d1c21261aaa3cb59003cafc11cb786a74c648f8e4ae9c8cde811e82facaccfd4bdf36ce50a66228123adc28e00e6204445c3d928f794a97e6aa7f406f0a47582481853ef6ce0123111c6960960e4edb80f53b0a5f2104d23540191f083a7b7ebc86b05c060b460a9e17ad8fc27c57b92982912ce089aaccc4b467d73773fefe87930d232cb590bea7c9bd0737e238f263e7ad42ef48ecc794a2cbcca8e6ef5252d15708c01261efa305a8ca979b3e1e4defe64e9252d7080550916e8e72a51323f5e9b8c3e20bf71432f5615809ada92e7dc3ab142b8a06d5919deb1f4856272760f5b7396ef9930353c1dc3a9308232a1ddd03d251f7a016bb9fa57a50b98c1f2982608026666b937a17a329a4eb0a4e1f24763872c24f55d3c04bbd3b5f80146c2eda65de170d8b51a645d203962df0f79df2f388ec3eaf2019f7b95c0ed1103876881a94a4e9b323c4fe1f0caacddd56905f2ba3371d12e1f23e2812a1ef0db0dda2414962a63828d93b322cf04b29da40b38162fd065f10a17689f65d30ce1462395124eb5bc020c5f4725eb938aacd26ce771b83815a76f68aeb7e1b1d946b7b48e381c580a5720ff73a3f47b361e28c74e17d4eaf6f770ec7da8001c2b09ec99f8d21e87f5d631af5298cbd1575d57ad6bc13ac69e956d19aa41aca8502a07e4f869e305d7816dab9b3833cdce5c2b319120cf35e1e10948ef1048681e921e7ef959f99d84607da4e4daf49ae72944238e1d9118aa49937b83fa7bfe9c82d5be9699233c4434f2f28adb5440a749451c83c37f864c680002a722b07583bc20881ce339d6077926cc679743aaf048a7a837f5a9f8489771237be8dcf37c23947eb017aa13d42003450ed6fb25b713dc2e80859dec84c172ddc6fab980b016e3f775ad6864c0c14e15e42995cb47172224be14579ebca8ea1792f5fdbe73ec10273179620f9aa41146238ba101ff93f50aaad18990125b65b3db8cfdff76ab7d908d5257bc1541913c38e1d0606dbeca5566afbc8dab6a6050f8f51125aab1056d6a564da66595493a90c51dd2265574ad658a5a08706dbded6c435b47a9ca29c0edcce717a81d4d69eca9970a8eb9d7fccbabaf9d3b60ba4c26c612d0a5a3473aa05c55897da72b88e649bf545b0953d27b3f10cae1b29143a4b99b54777b96764a8c5d718f99830500824eb82809f2f41818df25332f7e0927d782f39f89efa02bca2cd80c978fde57458a17ee57ad8cd41028f1be11759bd3f0f2d431f90a2d136f09536296424862652bce090b21e9c3c6a197cf0860b857eaf42f2b773159626c0d9a10b19bc1f6550a0f4192465673ac1a8361a79abd336af43081dbf5dff2316564d5774466d978cd06dbf84674ce888dfd984f4e7c11b3d7d27a957e36bc98a09aabf1b45044516dce7a76a3802a7e3cacdac1ad5fbdd10fdd2d1563d361340b3ad9342ba274b58e6faaad8994be3522aedc803bc2cba4e11966b36b8995be45001c53c04bb73c2c337ccc00ece4e42537ee4bbccb60026fc6695414ad31ad3a11b0942f56f6fab9e89820ea952c1330c23142a8f80351972db58d198fe61a96707171fea45d6be36dc09a418b06c15b70b599739bc835a36b128d51a318c2aee961e17ec2824cd84e6362f4d0ce93b2080655721619529361437947af87297c97e9779758ace745dda01b82171f9be9897fc31e0097d85b872d1b7e63f1247001d5ee50e7221d211f9aea614ac70d0877e73d9c4b8156054fbe37d1bbd70e179a5fe569810bbcc22f2d9a60e09096dd2a8f529d0020418b5c03df009e424ebd8640361109147c8e2135841078f5fd9e08a8d1805355edb59f34b4980659afafb3e3976f108572b3819697096b3fa74256edab82286c23f2f170708b2fe2e7b6bff68cab3b742e8674e22ae34d4fd793d0b3af84eaee1dc06574f587684cf8d8a34ac364e7a726cc6a7bd549c88db227de8208c00eb31ce4f849c928ea044e665af7e11bd2320235cf1703a151250b91c89b4e2f3ebec5aa13fa58366101a66f80a522f39f64eafd61202cce6734019dacad776c6ccf86afb3669d0674c0cbcd782b189b655ece6ae44efa07a4ef89d2d8afc5cfe07bf3df4f0d93d7d1089fc6366aa300ad5accf851be927c95c5e41cf4155cb8bc2877433c729b954ba26567df2b3358529920b9c82b587c7acf0e3c9a14df72b268fde37e4a3ae5b5976e59cd03028aca06f7c466c9736444488f402d793a738dfc9869a25cd339676ffe140b143a0d36c2c9601cd4ef0b13d83f5284ae4a55f077dfd4d272c4a05a9c432997aae79c01cc735a2c14476e5ea57f83a43f45a38e175cbfa58006b184308e2fc1decd1ca5d63088cf74417f49d33794ec5f9a4ee6796e99bae3673f1d682a4be49760c5df6c337fdc417c3a79639310174bd51b1cab44899b4043a8c51d95f499b2697bd085766824e97bfbf2dae6c01ed15d1283f0dec36aafe01c6e981f7e639ed6f46744964b8ccc604a9a5a3dea6262959437df337c70cbaf8bed4e77ccef38b127ca73b536849e9a525aefa4c67a700c4e555a7cfdc0d2a446cbcc4d340672d3fee006f1c4c85315892d9301aceeee9d1792f42987258db35e4ed51dfcb99982002f093310738420bf1d644d1ce6bcd65771516e9c5e190684dba68733af376351d5f78b4dde423928f4259ccdd2046e69636ab06c8a6ffb00556ced7a3a680a81503780211c5c3916c32015bb73d3aad869261831676c90990ae0aa57657657b409bd5b299f4cc9af0b84dbe173879e41aec24e5faa8567149bd7f7902bebbf6fb7f0e6106eb340f60cee6730968d53b738316f917271879b24d8772c5b04c5eee2e7c02b1bc9ab34b8e4c74c030d9da5f41b093307b3db4f49f084eba204808dd9a241be1972873cab2877c7cc6f0a0eb4212a340b8b98ad541424373ec1b33a4a44ebb48689ec8025aefc319e01baa2e7abf6a1df00c8f30748d242764dd9a4077374ce8993caf617aef5cab6dc991a811023a7912c9557a69d45403026876187280b5340ced1062b16d81d0a8656d2fe307ece56921e0e45d404bb7ce3c3e3e351fbc13001e95453e272045b65318c7708aec2d0ae732e6cd6fba9937ae499239569d9268d67e2a5e08c00c4201ec5b8183aa90dc4f0a225c1e93fc5ed626dddc336f320877fa9ebd3bcea27658fac2491e5478ff07c4b266d51ddbed03357534fd3b2dab1ca137114970b430d24ed7f5e6784797bc1671a66a90c1ad8a7e9305629b98f7464c1c1e7e335c3403fca149a09e7dfa323d9fabab9d87b6ab695218684223df4eb1f759654619bae050c167a1894cf315332de1aca5e1f41659f7c72760444b90de37256228ac7c1aee22ffae005ed27ce06a4b413163cb54029ac7e6922965239d28633a02e433807dd44d29bd920d78481a70dab8846ef670b9d1f20e1ff9609ceb0b610115e6d33c1312486a79cb856b7187a824a29e543b10be989b6ba500f34a96976a36847c265de62204192384a4aa68ef98477126b8fa868015376afc486208b3beab6fccca1108a8010797757c96c358bd1c31cb4e2477f4a32133a04e95f4bd6c42c2fab9125912a4ecc3160c9eeead7c301041609a933521257b19c2ff81852d72bb2062a30b097d8d8f3feb99e27fce49d1811d0995eb52dd02f597200091d9dc2f61666c3b775a41b2a9028824707eaee15ee4d128e9a8e094341fbe73fc43281105cb106ac2388b1aebf99ebb5f55e5a79b2baaf8e41e8115d6dd9ddd39691eb6c87687d0550af0678c9049d919116d1639b2c956c7453e8072584be9b391a0bb3dca39953f5535a82781e8d6f26cc6a7a77f25d732ebc22dbe37161a16b999165e7ee1284e5786d1b7c9f96273c5d07c851ecb40d42e3ed79b9a63c948143fd39355ff49092f589ea1ac51ccb578929be55d49413b726d80d6e4a54cd369661084dac80e1597c88120ae6506b0f05f90d5c3fa6147e6bbaf58d283e143728560cb6416dd5373e75f8809a6edbf04d1a24121ad85f5b49e62c8fa351cbaea8c062e9b867dade38a7501b7222eb49e9f10a202f01d0bc3652da4bb9412ed15eeb1bd756f28c6dbb016184531cb513c48b5b23740659460adb6a70f46146004fdc414b7c7da71a48cb1434f3f4a4e9cdb01c7130a0bab20d02361ba2a4b251d142b69120a100eda44c941916634cd7f02f360561b84383c233d5d0690e5f472dcaf565fc594bab3f4fe6f7af839fcd1c83874d15166bcf9a9ee8a7393d35d6181597372a5049283eede2c7ea79ed3955f1d142dc98b5bb39a89b386ef40f93cfdd15140dd0243f546e5089b33011fada08820f8495cf505ab2e7fcac3c772b0b6e2b7a7581a6528e4d16f833d53e96c3c99395f639474ff5113e2f15884e31b017d2ebfe901d81487b9235448a3e252e84e3a8878347f93472f910fa99eb4dba231604e591f9a628f7f949da7c68d2b92bbe95a9d4b0b460a4ba2ca166df37dbcfec671b0c57fc1a5e43f0f05eb3d7fac36ec3d68f6e30e493d6540d262492a109dac1870fa61fc4595059660a83c67c3c4aa51d90f03c8772d3429ed1ee1450f297f879fa51e7e09af2a5b637f98d52f18b45796c7b27edc710c1e4c3dd6d57088232f656b1b319caef6f0745a21913fec7a28a20af5519e29b232fff286af2bf6159738bd69505dd1467e8f4f0aeec3f417619468862180325fdb6ca68d91848701749063285ba0751a35b4138ca9feb18b0d102317e4651ee77ec755ea4bb4cdb285394ed4ba641bde932a1797dc364caaec91aa453ba04b9aef3374c6b934156b0a57fb1ff65a83b27162fc4e88e1bfb3253739a3821f62a833e3a5f48dbbd347303a1e5e632992d5a0cf4fbc13049f0abc0d2ba16c5eb8b97e9c66bedc8f69e71f1226461c587a981a30c24358bc92637ce91ae3b456788236cb4937d570e63190d5812165686ee0fac9e94707a16c31571512e80b7c3d027d5c0151ed985b1bc81181e5d8075d680f79129e926568ed50b66b7a22306896ee3ca91895f11c2d3b65d15e5c130e01a2f166d4689c8e154493df572e8737d56659da062b6db526e243eb4bb18c51fdd89a813890424ee151ef85dcfdb2bc3c831c53bf797e2c8bffbacbedcf8f727dc25570da41a70f9b4058da3eb2708f0ced2fd85a17cbbe19e85a6f73008b8f310a271bba66ce156873d6abcff41d8be162c415322c0b24fcd6fe4ef39c5ea2cb9cd3ae63ad17507b07e7a888f9a43b456ccce988542258a785f7045eebabc156f49f6b6e3fed35120bff3f0e5432086262fe4e99f1513fde63191a8a45fca775ed7bc3cb331946d5304b92abfd8293b99ea5cfce6b2013781c3f9d2ba8ab612cb81c7bce27399b6026f30951e9988926cc314662f56bbb152da206b301167ed8e3b9452d5fbdcca0a2eb46f82b32dd9f334ee519180586daf37578912f2d4e004b490a0b9048d30f4bcf2197e9ae71fbb75e7905582a8f2144ffafed9ba129f0640feb208c8f927dace876b67c08d7cdbd346d8bc339dd68cfba092f0cf5e440bc24d554fd606c0645d0c5f98f733b68f4ccac8579c3855ed70186e5d969b1bc11e7817fd348f86b2fb8b1ba03e884996de51b161b6987a554ddee9953c6d459e375f8340c0fe81414205946c4988a70bdfb9ac30c674a47dd6a6a1b87184eccc25a11fdee13db2d92edd9820a0c7cc957efaa0889a4ee8effdd117829994562977f3d2d1c07e938255ee45f195c4b6973b756dd6d8e998f0b40d81f7dc634221b3f6c61b415292c73cd9668a4fa809f21ec43aa8178708c1b2e33fffef8bb003cafe4117c0d1e3033c9763a417ee0175334552747f44f823ec60e0471befaa2cbbdb364abe75526b60d2fd9e13a5b02c8378844cfdb806baf9806a3086d0c17fff7edecf60a28547084ba2fcbf3aeebba530e2737f6649c165a575765c226604862f6e65d222e6c14e713828658624552d489eb405043e5c11243510350ba539fb34d37955e66c045a0cc0c9bc5922251594798331905fc7094e29ac481b3d1ab0ea0f1633ed99df8d9907b014fb8b8d0caee59f82ae06887a6bcb8030c9d356428be526ce3e229479893240eb94450848b37f9b366d57427c3bc07d9b8bc689b981f9803fb5ba95aac89b15e872fe3f229cbccb3eb86fee3b49436932927e44f4b2565960273142e58b50d6f99847094d81d737ee05727dabeec954a80415e0f4275e0e5aa2ab883abddc07d540627bf882331b2819660c067c63045be355fb59f9efc4b2648627e34345ac0c254ceaa034c70179c9a99407027ff255d855a1d3dc87067464b50ae312c14e71b6cc6ab151864959493921e5f7a2d12757b917c317bc99853ad1e5965a7256700da1abf08eef4cf205ced5e9e76f3d640f28bd5190a64c849052f046192fd73f545115b74d7a178aa960012b06a5c6c5c7be91b8ed375efe477cd6fa5a57d02ff48d5ca5146c0130dca222b2bce9062db488edb664933c72a1d49b60222ef49460106039518d4d24e59134969f5a9fcabfc6c37fbb8e8ecf8aaf25d5808bc76e9f57aed0547422e544a1248b79033872cb1a11ee17f546f327666687fac62ac0bf740e72a81edd7fc543f35a7d54cde68a9dc23e008750df4c203271260e1a645cd6a7702322bbd9e448ac9977d3064de91712b9986dfcf399b7fa3d290787cbad92a9ed643b0822aa140e2c349387c1d6a9564d459d63839f4ec46c3bc675a38f8effdce1c57856ae992d71556efa9c21b2c319a87d923041038f7c14220cd92f68b1a6ae02726d7a3998d8befaf6b0496e8d5446940703c482d6f5f5bb27f536a00973106ec14ae2be11f03b8a77b869b21f4b876b5fe0c65a0ff26ccc8c6c2306a7a0e734920904c3b4faebf905f67a31137d4b8d2c5e5f8af07ff42511630af876dce4321aa5ebf0b2a998f2d24a1851ec68527eb620fa6638f46d4393e6973cd1627416c5c5b26bf34ab720d15b0799b6ecf30df9904d4e31282289f063177e99963bc8bf57c6bbee06722ea23eb6fdbbd3148355a9f01ba6712e8550d964aae822e39b0b0966cab4d691017828353906d6735b900162c2609cda9a2cf806b9ab9e70da6a00e3e84ae35b342ca3c8b489c0e4c8540a3a96ef7280cbda68f441a4a63baa7ee05c2c6239435702372a61f9ee90b3ca2ea13df42cb9f1fd4539f8e6cd84b34335ed0d8ad769ff3a469410116294b38cbecaed0ff0a32f6e1c280550ef02a2dc93daf95a39ea5f0b88938c0f8e2afff73827d543a19c5cb36edce0d17b8678f944d2fb679bef5660e92b3a7d0d78869041baf27855902e1199710b670856cb95c237e5f52ea8ad5591d45d1bf594c692e7fb5be01cccd1971398c5fd208b3511070a7ddb41b7081898bffba31d993edce673a837f67e33c5e1e10b2ff3bb7212e999fb27853644e29cd6da6e4f77768f0e582c25d166ba02c1e02a6e75798962b6801fa6abca418cee0016bb6a0cbeb5bba0b8df8356324f332dc358ed44e1ecd5bb6c0430f1f4595f97e4bae2b60c7f9806b9b0ac168058bbcb9699d3a8766b699ffcbdb26e0ae593fd267c1c5c7f4e5cc5023467fad258e701f19500f8fb772793591af92bdf6b91138d63d682a1a4f615a4a3c4bb0eab33722caa4b170dc86330d06f2bff23197f89310bb7ea47087bb159f03acfbff9b053f2cf00f0749aca8534a2b879aa4dea4e7749865cfc840380b435f6e1076cfc160f18b65b9d00e7f898e968f56ce70c620a1b7715edbf1ef58395e3f6a3cb449d8a17987052228e4b0835275141c071c3294b9a7dda2e165e92a1020ee8937e6401963a694115fb10fb26d5066faa02615099d92f398ff1bdbcc6211eb0da83623830672072de459a261e33064556734c13eb6946b09324a5d5e63a53d43e5417a3cdc3f11ff91a14de50ff55b017bff28adaf23cfc68c244ac9e55d6551c595e9cb907382dff2e341753e4200f997b0695158109234b5dfdc29173cdf2cdef90190720c99a14419991e2fbc7a3e9b205efd32bf6999a0a0367fc795f3b5c140b40b1af6ea8e9b179880eede62a190a3717a2f7665234d3f621d228d43b9b37e1fc9fd6f4eee6e4a2050ac41c851ec2d05d92b74fddd1480f664c330cf54e84b4c6468bbba59baf922f78cc146fa94e059c7c8c944df2956872d5c876f03a1be125012ec773069750b30a6fc8a86b22ae0710a77a283adac97cdab9ae7abf7471a12f720a272f526b3892447a9141845ccd3717cc24616ece47b1543fe83910686be4d30840f39bb30ba6ae4f5fb4f2e48f97057e47d411b494f607c33c6c9df2aa916543bab78b91db98cfbab4f8a6b65f48a45b0499ff270a00cc96e0335126a1f421370917f5ae30a0e88f7e111d8eca7c81a258735606f2e42c51d020869c68dbda0c44988f77d104f54f99fa3216c0f4a7cc34153814d140b936f79c55c690ea0e90220418b94b76aad0838ddf764bae8d624c3167761d653b68414772f23ddd9676e549e7f516864d9057c8ac2950c6001cfe2dd4b366a2363fdb0021c53173d451abaa361a64eb257e1d0d879d1307800792c6a96e686e7a3826586480b9327b86f068e3ed2866f08181f765383a90de5abd28b4fe979223b45af18942cd7b63ff8235f114fc65fa5c59aaa59dc69a0684cf5175f5a8cea269057dfd9efd1cc47057ccbfc172356e26cb3b3f84f201fdebb12459edaba6503041537f46efd0f34693acaae7d4eeda8a4b5333992d2fc46c5568a34f8d2da76abd40539f2f91fa977ac55dec81f4cf4b2056f3b779dd9756418963cbd8249eb37d739f6e50404f92330903a7e366912f981bbb119069a4a9d236c808670465ebaa22b6fce03c5c4a38d03b8e35d6421653512401ea488d76ce7a5dd01b134e4b3547e1fe86dbaec962962ec23320f962ecc2f8e2bb6ce69120007db7c4e70c01454efe874fd9c4a66b5ea826cfe71a48aa1bd264828b21180b44a1142386e8e7374d3cf8289bd564a4562973721cd3317b9437d8792de9d91c057852e65487a9a1cae9365533e53a8404ffbcaa1229a6e4569fbc577e9a2dfeaded7bf295a39d33ba5c75a6feb9d3b1693160d7484ef3917b73d2fb7312ec6379eef435c3b5feb2edb994662b03d19bca2de70822f07b41e07403538109a4484e52725e62aced104844087d925d8579d37dac67a302c094bf942ea5b69d91a0ed502938a40b75566129f3d254a59b328ab72bbd3be32ca56e1b43236d04c79aeb7e444720610e17f4e7625b1ace77c781130b7f934d5e620198310014d96fb0a6a1ec1a56f5a7c56637168a0d187969fb92673658d70b4e10bb2ed9f36c443cf8bbe1baf1d923494c0fd46496213eb03ce4739673a79b8daee3afdd67d026c2e7554d8cc33693158205459692f2880c978b14cebace880446fe9e30237648bc5a8a4524c283e291541e00f4cd703e6b62d260f8eb4e56a85e38f1fbb8d2518717df20e86de2e034baf4c2719c7e007e59b12bd0aaa416e95060016d3195d14e910e2e5225cfcc71c91940ccb491ca4c66b8ece6305f9c741de779040d26ae5f680bcab7124a8ada881c279898cf88e84a5b5c82dcb6cbef4eaa0261ef92bf6a8b05093616884bde92dc3389328c2cc505d38bf3f8aa9e5a7463bcbe06fde0bf5718f1fbcd452d3dc5ff24926b5e7b56445bb4fabd487b7aaa9d3b9717d21952992c4ab384be045b8cc58ade26c0ee331c51ed119276b426f8fdf848485a039c3f88e1a69d5e935b758eb673aa7b3ea6805a0667f2ed804f58367bb0adb78a954efa531a60236f6a920634971ae94540728db7a6621c492dd541b9cd3d09cfcc538fe38279319dd3b2efe2008c19084839b1ede6bc42b8b88e28acc49557823b6fbd0caba45b4f455eec01308cefe96ca0eef2e3f0d8d6b65ff42b8da8586567378505afd5ebc4583f07e71ead02bdf9b1b606e714c3321fdc71eca3b527badd5246aeada67902dd6ee2bad07204d29c8da6361364942c87ce8aff13fedadf07f0e3f6d7fc584206cc558ae18dcb00672ef22d7104315b69acf82eae1a7370e9a2de411ed7307e373ec74c699a7eb1d59ed36e111047c7388b330ea25591aed40982288f55fc2e8e101c6b4dd56130ffd84a40bf5c2ca8ca27e1718ab8b6b49d71d963d4b086006562a7b1ae9e5337721611f2cf5e77ed4302b8be2ac88cb6e9f4934b9a29ab7c01b5593534df844b4aa04b44f59e225667b369c9bb8db1b152cfcdf5bf21e122fa5060ffa684a65da4f6c5559cd1c0d28db364b83c4c1c0643355bac9723ad6f5ff72342a3277b11a84136943fb36ea77685020ccee1d0f5d8f2276d1952d6b6b0af2c6c9c3a16b4bfc41b0b979d87e9751a3e2720e993ab460c27eb06539901e872cee676a9306f119eade660052f5b65ff80b29e9e0bf96b761fa3d5c2f43aa4c3d98f002e913c7b7460ac75c66c01196d5e16571bd9beb5bb9f673d59bb93761d2ef41941ecff5b3ca1a5087ad03ad68941a3111e318d3e3e9560f7860c2e7b2cd435999a920f34f606ff96efc9732c071a54c33c56749bbf70c791944bdf6fe94a2abc330df28e9b1b7c231938677da26001a2d8ce1a53eda00f480448909e4aeef9d0563f79702dc6b040918e1ec66f6f350b551641c418c83e66bcc03a8a1a8a1674638f7d27508cb6c752d0e724f3fa0c15fa3e6c17f3212b52458bdadc0a716abe17ce853e76a01e34ad2c789b9628b645793e40604290d2ddcc1e43c1fc105dc5af8671f451bcd148630fcff9f1734ca74a69ec4a87b8cba96657e748662783687a62360096ac53d63c770f8231ab5204b756b8bb41d7639c530bded1104239668872c2d06474f63a166ca65442241caa3267ac9deb3823ec82d6da3664d4c77e5fd35267f21b3d8098173811a69d3c411a3f9cec25ece9eaf6605883a94a8d8fc5357dac01d4a05a32de66001b48461f3cbb366162df98ca96fe2d9c3922085d08ef0cce5fa2fab81ac2be42cf273d5840b7b11b8af2a41586e8ea874943cfee8390e107715faec01698bd66ba851f5286acd80d1c9b8b2f40cb58b7fb1efca5031e72435a8a6e70d706433fe1217b0f1430190e39422539eea0b32650a0448c9845ec858eb299ac839ced2e45160606ccffc19e979e3846f205e759ac0c01f0002b27fab7784008c7bc45781eb56cadbfb8757c54ef41c4b222a8c54758e3b964f7bea466390efa240272949040e16aab7954fa3478016662d48a2ff47f1ddff1cd05c0193259b44775bbcf66c947a17371b81567b366c37150f33c61450bbb010657c3d98e5b97a8b70ad205ad93383e2c8755d657c7f53753679316a79a13bb7782ded4b363c82857e08387080f6f40e5f20cd5784cf7fda05f3d53364982745bace135f61424a059134dc55d2a206bebdf355f17c829065451b2d0b3fb6c5d048fae49b51a7195a9441e00a00256d14f2422313f007cd0a81d0e1da46d375cac17f60f8ec54a7d5f176b456c561eeba0b365f2c0a0c6a2c7926a86acfad2d3bb8dc9d54201a44411d7af025b6dd3b479cdc59ab47dcd692b92e66321b5017701c720767221f7f8f46edad79a3a64245cd7063b97e9fa303d393abb8f9e5b167281617019bf0c172301431022f30cd3c580ecb3e9ea0c5deb0c2e9b71f68704d150ea878ac868fed69c74c19174297a77046f6fdcc2bf0438348da1bde4a20ca5285cf736be20c3548388f4fc52004788ea91fae8044f32339bffe6cb7591b65d03ad18e71868a47c136b383e2480408b2e2b5cd38490d0ebd6ff51fc02b859c6ea4dd57f4530db0e15761066d8618c6d70b8b61cb0ed55744937e1d5c00143ab57fba58c4c9622ec09d30506000921874164adbd5cf6b6bcb2acf500de67cfe3fc4939074713b11a87914d6783e22d97d696452371ce8c2296cc9117b432f0e797174f59f7fdbad8fc0ff8a62e3587d44fcae42623739624ce117c685393ddec638220d21cd36d2b844e6ca8b184d7dba953b01138e95960d90be536b45373ccef497a4dbe07d48ff38c1ca17b89e8a6f287c35119f540fcec762cc1d7d86090626f73f960152a540f8984759e6a0cf0b1c7de6aac51e34d3e64d576c5327c021e6215dd507428528eb0adec4c4819588af47fff9ba924fa510e6fc0c1d7fec51a68ef08b35ead7a76152b8ac1793f4c4211b01e097fc4515051b6945c1d5ef53821a6e1145b6fe59b396e4d1459a6163750f5cddd9ca6379452f339b91a60166ed2755bf8b8141c764ad7d5c217946126912d205ca7f8bfcacfa188a2793d1f5d838816170cc752724a1304863d255b414193397961a24429340c4e8c5960a3b359f5921fdc786095da9110a40de9e44ede471006e40b02d43f55f3779b6c00711b9ca0ceccd2e36413697d101f8dfc3ab4f427be22a46cace15588ef21906fd917266b4f2c20857113d1025f060852528091e674ebfd5938f8b83ac6b1534108453666fda0540d6d414ed3e27c6c49bc6493f2efcfc6d2b02e9230afb3e29b8d52831cfcb82f2663877feb8189ec407f70638b439ed2b31b8752b1992e62d1c4f9899730f64698c25e74fa866abe0dc57ff8adac4590531f001d7f4dde9321acfeb6f58124291ff4b366de8c5a1f898033860df2ba25c696e5e2cf602777ebc8229be3ca41f32d7cab37fd669389866852fc914b6f6704d67a002617e54f4236c78d36acec0eccdcb5431feddcbda7fb43f7f6151f313bb0ada5046f5607c85c46afbf79924f79d31dd09432812ccc22c456d2bcf46236aa67466b7d7a71923368554fdfe190d3b3860b1baea084cbc19142c6196382b3156f25d2c5cb438e7676fbe2880aba84f6f72810e513f76fa8cae2303aab009077ec21f6f99bbee4c4b7095c5f3b872c97f89fee338c5b6cf9dc6e2a7a7d90ebbc9104d9f4a4f7bc31b13de00f8f4ea5bebd9515df66b104b72b98f9d9a7e2c36b5052721dacba333f0f44b70994e5ebb0d08821f9fcc323a11a0af5f5e5535e078fbdd1325eb5e87fb6ad1c29a8d3c35b1dd10bd7e5d3857bd911cf7af1963afd34a77b634aa8a2d277df169fa582e3285787a1c138f9aceba27ba28c16813b8656b2729cdcabe76569b50e462e22ba6cdc039905f94c2e1beb737c56b684b5d845669847e7daf8cf1482ce4b6ad125cfbdf773a409e20b0f781ae242dc08da6af00b593c9ca22b47afda40d542ca76a2a5b8e49f17a150b9daa31b6f76946445a4e8d100529a44c4be2d1fd1bd96bb969aaa129eec5490ab14163d648cffaf6b4a65d4a481fd3a3bbb2ecbd377bf07c8ec9cabc1670d46640cc85f9354c1c928fa5c5f1332cc5a4324b3ebb1fced2d766af5449e6cac66b4525a5151d2fb0d2a97d98caa5ee0c469a802cfca5d270ee4eab2ab5f366a8c5b786ef486e9a12623ed98b93d50675a3ba3b1b25bd6c7d3ccaf52d971e39e5009c716c985d3512f635f260c8ab58cd4b5268a8e307c9a3db3f3cfc448481b970f71468f89395091f9f7583b7f45081800d2a1b1cafceb8480b9dc111c8a399379c0a3043e89cf08c74880a4ed3afc06694f8faeb8050d85e11c7b7b89c6c0f0e48f2a61d254710764ffe2b459996f2d95d47af28ea9a1f1a1fc2827d373f40cd7af65a4d4ef0ac2b12bebe7908d23f39dc49a95aa288cc7899b8cd807091771eb1d4c1611433597e1ec196a7d3b64ef697c900f210edfb90f3f08851e8fc4c82755c8258f710a07ef35d055f2655e9f40c88cf794c00cdb6d78562a74cb82af2c3e58962aea66c0eec2246d11a737c39f881b770d71ebba4c900286ef3632683fc89fabcdcb2cf773ac68114737ab6dffb038e99e0d6cb7dd6e2974f56a30d6369125676674233a30b42d62be816ef32eeba7b88468bb3f6436e6fff2691c6f8737ef28ba20f24e9404c063a8302a827ba345adf4a9dfd2a5416bbf861beb19d45fc185c0db17a09df8073585e90883451f1abfa602d18f9cb6ed219b11054eb3f6860bae7bac01b1180fc97d5dcf234ba8992edcfed131a9b08e3b8e7d900263730f99eb045586b65db0df4f392a9a2d848a13531922dc2548b5d643bfadf2d5063a245180ae69607b8476a29d0ab1ce5e8ccaf93653c263b4c1c630b20f5c25b3f4ee201682da8b151bc1210087b4b42213773f3a47210ada55a9d00523fa08a12250130f4100792b73f5d6f64adec2b9edf7d3b3139c04547fb3ab1741f88106261431db5358eb905b2fa30d5120642c71a5dfe06c864c9b819f4665422ea7c82939b82b4d4aa51f41c2092052e258374cb1a5c813574a84c8bc6df2e39d71982aea271fb1fb35f5794f818ff1fa5b552f25df36811f4ccca31f7ee713d9f8355fb65a579f0b86a3ef2d28cfa04e1e7c23ab7bb4f09afeaff090f27870cc89cbd8469d9db6d640f6c09c830e733d4932e0be1ac1c0333850a0cade32dd514f11354e76bc7b013d3356870c22f0d45ecf1e8056d0c6ca9174a96f0308100e499c0c89881f921d5323567bb3ccf773dd122481daac69fda804ce97c879a11231341087052c5b25c50322e066bc589058d8ede568d4dce0652abba310b8a8b6d1a7d981be8260bb02089d78ee88493c18284d562400782ab46b1901a3c84fff716fee1ef386186b25e723e811382f97a6366a0f6a6de2bde5b24a876554acf206290b19d55bf0686802e6d228cc16986d14d7d2a4f900aed1c08f47e79aaf7cb6b9befd6a4368d6700da5e14131be78045a3b3b02e13930bbe8a9eabbf8afd96f7760f13f8aa3d5db7fff6a87183485df884311ebb392ee68f133444e777e3ab6f870ef36dabb2a75447274a02ecf08a76bbfbf4f9e88303be759a3629ecdb33c5db04d5a6c23f4260c0430802a132a93aaf2f34a60bc14bdaa2509156254fa8071dbac495bdaba8eaa611ed0e6ec4d010c2ea4817227239983fa1dadd548107d6e3408af2d44a70999d7f7c40d84b4da95675bd4ca4de0036f68b123c8c499c1cdf69e6aa72c324b73ba4095300a0c9320748175d793a4eb5916c31dd5244a24941ebfb83e5027112f88e829ba5202b9dd3771dbb0aa5a6ef02e018ada38224babbf89cb1dab957e6c50464ea86a63c6dfdd3bb477024a936e3ccf711879c6732467c7f3518225ed29a533bc085b10ddf76134372aeab9c1a66a1387c7287003517d05780a2f4b5aed5ea6b0337422a0c43d4d3a760bc7e3a794043b3ac808026a392e5fdec68e68cfcb10361995c3dd503496f1036e3adba2a26ea392cec2aab81c6945280e49a575ad5664dbfb763d185a5cd56646eb53c16987d3d9f3a042dcba8b79c97218b71637937258f3a79e8929790ff489220250bbabe4da83debca15bb24621dfe18d746fde0c19c57182435cf17d69944028007260e698062685bacb4f71388620c37420a0ada6bb9565767c4f58e0e5aee858729647cc6aac08bb1a9efceff7af8f5cbc10f43d26c264720538d6b863c29f83f474e70f167ee8fbc2734e55361d2726521a4854469539c22b7ad1ef335d4d614115af087359bb1da4fe6338a45d696c1e44b781187409d1c141ea499bd273f9f105c020f1653ac6ea5683a855310eb37130d919b5a9c81d4e925e9b46608b80e340f0eb2d83df19a8990b1edc8a0a014a0d16e7ccde4946d56da757cb3545279d81cca95c33e77305d9785a84d587f2d118f9749ca7aa8d4363b802c3a51ba92bbd4e8bb7ea8baa45e7153381f68e9ec347e3207c25b590e485e27f3eb606b5a038921e0a233a444268e430e36d5ff3a349cd9ad1bb20fcdf7807dae9d23f60775b1206256eb8e8d4de1066c2d051709a0d17562b8fbf79e80364365dcfe3c15ded4e2573d258b39be6b2f585049644cd24204d380971ef864157dadd68aaae30b84cbacfe3097a605e3b911e4b2eaab4324412679a633504eb4d6209684e513470e37afde0ca8892a8605f530a26cb8141aa5e0e7c003b390efdc15d4aa1958e3635c74e3c4c578f7f8bcbe6e17b77f5b032af2e3e7f8c819815b16cd89ab5cbc35fdd7ae5e379d0693ab29696025018f568b2bf61316c4b35451432d77b55199956bbe38706bf9d4782e1bb76853e8152da187c0ff59cd9b2646acbe841062a43e252ffaaff19622ad5b1939434f218040e42da22f319d904465146b744838fdf85ab337be6fcdc2b7de2b8956c6ce419b8ce8f0b5a3ad55b5f25c1755ec2f1066c177e0039bc518ff1e525f628eac76f6b3efa3fd9ce8e4dbb6aa684c720603a443671df766f36bcab9892239df46562b3288c07b9e13aec8f65ffb2da74a49f0e103703a8d366c1c7230469a7f1c6b332a0ced96f4d9b4c54d3742fcc9801a68928682f9814e0b7a8cd5b812df11788f43fb215121b452836d5865b8dce404d1f488b975ac0e964d0c9f62060d8ce04d935c6fcd111c782d407c16c6b0d3274628dc74ba098a0c9e978c1ce0a020c7d6374ab231187acee79079ec1adc3279962c8ddf50bb510913266d6655e8adf8ee8ea4f92afa0cdb114d8f376327c9baaa6116b5f977b73ed5c5667f7816ae50e8bad9ccd6239129b276d39f5c94fba0a047d43d4ef1b691614f09714a47ad49c28f7d1b5705ac3e8f1d589797d85c0fa40c87854ff38e1b768c8761b84809a499458f7c75f14ceed17459967cde17f17cfd2eafa8e80c04d99c86488c9018cf8d46b602d7117281433433ae2c2654c05e1be6a5a08cd8e056b36cae7be903802e8c9008ff75428c9a42d0b29a77a4ae160bd4c21480cd9182c8634a069767b8fe245a2a2d7b473d675da328208d76c3337d39507edd65edce3a20db8fb1b33a3e55ed39a735b1b4801c9c3949672b420cab8ad3ff1642364da5160723d3232f97611ce4f6e65ad0a9ea74fb6896a6971e3caf2061f198320dd8479e26b944f145461962c0be395d9d3bf5c817970d19e13278ec268b3ef65ddf302b5971fdb8ea75cd520c1925ce28625c098950576742b6a23ab9d9f5e34f771c13d095c9e4722ddeb5428e31a48e0c4b8fc594d5141b30c9db45bac624fc9f0d1599b116cf32948adbb9c179a6d21a0157f669624982f954b4d6a4b566c5748d1fb7e03210a4719ee589a149ff751f4f166c6cd6de103fcb53ede3c6be0a8aa25c24c3d7cf6ef1a101945496e974cb2abb94d9b20cdf4b68d3f5f84e887fb20fab22b1c1983cbf38d86904c7abb6d495df20f523b08e2bfe55474ac9ea2860a613b3f14dc34d9e43a8af049f6a4adb31593d6c279f141eb95eb29afa1c7b84f77450dabed689e5dfea5f8acdf96b661c722703e493e717fda59bc529596de1705bff6e7ff4db5fda4004a94f698ad4083c9e5c242673fccfa0fd787cba02ceec0a52b5bf7a2ddb3da459e95352459115c1895494f64248894ed317ab0b49aac6651d45e1694ac1031d5c4694061cd1ce779935efaa4b3aa661a32d1eaa7d4b6647996a0e4debdbff072e0d55706293987dcc469f23110d677c5b70fb3520294171bcbbdd0d2f332539c555b6c38b9289dde51456a94529f0136c6ab3feee8ed46e39386e639dd4677ac0266956052508745d5df204d5f23c81bf3cd51e0a153105dc0eaffe6445be535b18cce377899e3087a2f1f051f69174e6d042f2fefa662b0e3269660c709b3be172a5a9bc6f6f7e109563cd414ecc6761f28826deea97373da8f149a53da9ecda5282f80ee17854e0e315162171a6426590c3c5792fb125949178c7f753f6c9815fbfb527d8455b211e2679b69be5cd672109d12c918d09c8bca9c5d29deabc6ac7d53561bd4ea2dd91401720c51cdad3e1e97ecfd442790b8282f9b45c0fa10055caa36a00c4067d35c0b791b4741db87ed43ed0d3281d40730971c94c2940f1552515605bde43cb005205eafcf68057c4435b01c1247f16aac75d1b2b6c0bc2440b43e7a92794f6631bcc2f088a2ab2c9e4674993ce442f202a9a830c9890ee9e5d428e00fea155722d71500834b75f6345f24e2214795b41b43e7ffde22ed9a9ca98de69642095d2c6a10bb9fe7b4abdf17f03de5477af2c1a81fc9384aa73f0ca167d893717c0076395098ad9a92d097122392a67607453a934160120f0a200cca9db099f3623dc51cfbc5d3441bd75bc71cd897909be70e0af490773862d528565e14975a170321d126fd91041544af1631d18d8b1d7f9d3fe2a2272c5d9d8240782d2c3a35d151839f490bb4e8936839098db8b21149df38c860504fa3892828cf8c8cff7f5b726d5a986b95bae4113f1eec457741dd8e8cb712712e192e63f0fbd8b6afac25f612f242bda36eced54bb1b91c53f2f97ff61fca951f468a05f151e921f8ba3b70d366a4013235c99e76b467af25b9a87783b213fa25a75a43f8f4dc22cf388742d98583bf2c205ed9aa846731b1fd682210da32de215fb7f60f2464baba71d46ab3990fe57fe9e8bff6ceec31941632a225cdfec24ba05f90e2468cc8a3fa291a14561c819dbee2f89fbc79ee8248ed85ffd6765257dafc5dad5f05e5d78e482b78a15e30d72f786927d5440213e5e6a3afc52d7766ed9eb500fe601330df25f5fb3ca583f0d13eb0a0c3d8f3534e187d32108ad8c54f59d2d45ffc0832fc8206e0072660f744bb565dd2da49873212428e150b5fef1035b43c04f2d00df2d4296f53bbdb2eaba817be7239d0703464bb01f229dd56eb540b58f7168269b98df9a81d7a10ac385d6711f15c6c453879c2f41f61e3cc009c96c2ecf67a2b9ab02e2adb749707f4bb21c6740771664bced16d810b5c6afde205957ace2315c6759744b50b3a817b812d730ba78530f0b4273c03e6cfaafdafd15c4f9d6ec6db695ffb7614c4c49ad4e91978390fc34d2cdcd151a3fc3abe9656f808fb81b1b05031ab8ae1079e51c582753ca12dd69197f5be7017f5b4beb5a61e68bcb06a7318e58c917c4eb2b000244381fccc422c137f81a9f2d5d1e3cc82c04616c4c43d1b4c033b2710a8d65f0d117f858ae1b7a843ff37542881831c0394a6f3739dd3ef7e433962b398f9f203069c214884dc18cfb48026c0943825e2e214ebdb8f3f6a080d4a500403cb91f81cf2c877976cb6b70299ddc260647869ae5ed1a01807897ed43a192211ccc4a80f2c9b2854e5e7ce864a63711637d314536c6bbfe6a11e50deb3739e001bc96aa34e3e63549acbb54e4ad1c6aad13986a38a2d293770e2427990e07226b906ae17fb4d9ff4ee154377fe5794273b645ef4153e9e748967d14ede1d72abe6d430e707cd413a648537911adef45e16516b2c6bdb7b51ed491a141a0e577ca636da7665fd4667e326af585b3db21eac16155a360be688af26112d229fb1c8b415b7afa3b1e383c6484f4d71c2a0f7573b4afd545261a637b92576118a40c43b84cdd181cff87038019880f0f87613a7791640b9d2d80fdd282adfb59759c1819d3ce9b43525b9e6a57c788da1c0cf588c5fa5aa608edc7db507539ebf3b1ea2eb1b7b8e8e50d2a9284cb10f9f1affda90eb33b1141361ef492659e6d80b4a067d7bc36e6c99596ec7ed1edbcb3bb25fec9fd8c1615eab7b54ea692aacee2ca3d717e408f3c5658eeee5aeac25c00716f0f7100e036b226eafce7fcd38900e44d313ff9e4aefc1f7a2e924533e75892ba12832e561770fda1a3f1437ec054e97eca08cdeac9fc68601cf747d50f287d1677e8d0c1d5d4d8e8df28e8c9d7b839148c60d3ab8aca04e796b1218f4b9b31ee267c269e9d2e91a747d289a2d1e7cd46369942d51c1f5c6d33b48ba7aa0a0d22d38d6b9f6fec3464b91681d65a40f39f3e5a3cd6b634de2df3108376d473496cc0e69f070e599c8adbe8f10718172c7cc84e24a9df3c3343ebc783b1d64036d21f500d42f6018e17fbbc45927cd3b6836d27b67feb711746bdf0ba6ca413c00ba15de9d556e593a5d6bf08138e73ab831ae72c42bf9873a3ef67d581fc3907a7ab34e6d3d99c55f9c1cc79a89b89e703efe17c44606e482de98015ed072cbdacfd99e49efb66dda0f330a4797ea3a0046534ff1fd50326e2e0fa0ab616259149b154a6c8f56f8e3299d7dda1c228c92dfc8b271de2ac60d21c9f3b5e53dd85d63a26229e64debcaf692e44fbab9c6b3bf7fa9ecbcd9124bb5fd1f21c84ea90734bb4098d57448339a8df249e74c6e4b28755df07e67e246fe392e802128f7664d1e589a32cf9ca11087cbb045b85f0d1c9524f899f715c6d002433d6e05d368a7b6e7b90dfff48975678cd4edce6fa17b3a14b6ae2b9b3a9a71fefe81e220adc21b9dc42822247eb9374ef980c77fc4869edda3593577d09dfccfa8b0da07b1e6cd9d6d2e4edaa73792ddedf047aeaacfdec1fd127ddef4ea1d48dbfe2c7c4eb19d56bfe408daa19e05af11fe4a68dd07ce8d24a8eb5094e62c519f3e0b5e6e215bc38509f691f14dbf4d735910eca9554075b8240b9334be27b4122f9178d70fce35985c1b1c4be35a85e30617911d190ab8332c999654edbcb73d68b918a107b68dae581976ecec30e977f3d080a0ca0c168d20468921a3440b9e43993947c9f83863a68ffd15494463e80ac1c06c65ab3440cae3d3a0d2f461866fb6e8c188df3d307cc806ee21f9ad8440fd161075992f3f15d73a0952e95d78a0c7769bd7259df4d557773bcd70c02d2a36c7726374f18eb3f244aef6ff6e6fdacd9b977e56d2147fb4ab8456d4a2cda2a0f9ff220651168c6aa3f9be16ea2cebe75204763b286725fd87ad79c067280895cfbceb225ece30a101ed86233be93bf827e9a266cecd3d6ae19a6de0ac45571dd4635207bc2557acae46cbdfe4b074b01a7f9727bc066dbb2459749b26f4a8857486ca8548a0edebd16ee1f022739633475c340f0be879d38ed21b297365a2c21732cee05c2e34204e4ca9cfa33aebc1811c9aa546bbbba286aa61fb5d3812bbc3b82beade3c09e4d60abefec9160e424b86e4cac87f91c4a91a0265c17fb72bccd01b591cd8558f13fe8fa1504029e50bf04a43ae3601a2f7c0a35ff4f1a502c7b06140ffec0e2d07f1d248b22f4bcc0d99f2239f1c7110c0438c3ba0fd7d4bccf50d7f2bad679c425d7f444095f573d25cb8c129893d2f7dd8f699af0a07c8a49a985c74bec4cb24633b5af13c71b5a8e69d4e087ef72d017d4d424f41e1c13ae97d65372f6cfcf846814428e4c677bde10ae0d66d8251fb56ffa93ac4d3d08345d3f03bd07774f4cdb78324a35ca222fd9ae8c50bbe05fa7c0274ce823bb916010d8e41a51bcd69d076908e7bf29be26221ec8263c8f8db11c56235f3cbf47b28661d78f352ffb56417f208081d8fd4dc50c872ade2d52430e9fd30bc64f68e509c2098ad77696da10a90db26be9e56cc7483e175b1e626d87f1cb9127229917880b208daf33423caf58e786c79788bdf7909530c288d163cb7fa7807386e9a7433d8be5c84167a4a66f5c71736fb498fe68c3f59b3082e130f9d31f13f8e0610979234a385e117b4832002c2776cc82f30374e793c4d1b132a953a970bd8adf99404ef7534ad59d3b3da040b6c09d0ce15101e728fcf8f2b9b541787a5183582634300929158e63611aea4b30b19b5e33ebb2a76020f3bcd6f4710f8995c463d20b16b6b2abbf4fd71e12947abe8fa4310ea2518155ea332ced6693727a3f1a2130f3e9bbaa360d25cf82901f557bb066f9b4a7c7947ae635836cc6c2ce4c14012b4110b8a95a8f32328a6cfb9925e8da354b190d097b9074180ccc33c48324ea4d28984ca3827434cca143f91ea69078c59702e6baf44091f0fcd3346d06df0c38e67009a098cfed03be87d78255e76a74ba36ca6aa9e5bd493c3a5abd6bdf3bcd554abbf50508d66465ca836a1cc3d24a6f0bea73b89fd86a16dfc66a4c0197b8ebc20e6f2ab13c280cba01272a069ab65e3ba27cd3adeae128bcdf0c05f04e2e98df39e43133294dbfb7273103e31b1372cce7e75dc71e01e9464cfaa173e480fb5d9048e148f094c827916647007c596ee74b2f8fcc6cfb8359cf22a1af1b5474a1ccc8d81618701fd457fe1d2a9fafc2773472dd3c990aec7bc8e9c6383df85f4af8534fcd18f81ef724dc33286729481e9dcb92ad0bd7e07ce8641a346c13d836bd9d170ed9c30b061ac2edce5ba685b4e5073fc47e59225ea556a0211fdc938cd0fa7d36f1c331f9d7ccf7333e1fc88ae42db124544a95cfa1b9930f2a100c6e91f55d5cb1b6e345d0fbc7541e8f219eae8803fafb210d6a52db2ac7452fce1bd7b73d229ca83b194d72fe2fc3fac93c020e2ba614eed2cbbd5c6a0844dd0bd73f91e41de8217612d5e2d67b0d30b9f1ad7e719bc2f5b537204d3feac7f38acb59ef94275fdba29ed79ac324038c944390384c90ecc7b66ddbb66ddbb66ddbb66ddbb66df33ff6cc8bb7d7fd0edbc75f747457566565cbec07044a448f7d241176f8c64bd1db821d63f3292ea24c7039e0aa0d079126b9af1efb2b8e0743613539829548ca00aebf46211f2c49aac2478b1ad0012f5cf60b50f89ec5a8890d6d96f945561b23af3adde21839c50b606f2cc8eccb7e6a89cec28454ff58899f358740befb1a517b9172770177f9092d653c2f472ed057454bff8c0c0e31518b471b3114248495fa01029a2b0b8bda7aee9083bd19c611d873d3344c6463e98bffa6d70110b2643db7c9a624e7fc63c65b731fa9d548169f11eb0aa9d27a3a9ce9b2eb0fe883f11851c0a5f08d618a51080453ae6f48c4f563d4e2ed90bee90d9d27a626eb215114b577b845466b5abf3f05ad0fa100df927f478dde00ac419ad55fd71338fa68282cedb18095262333e9c2b46926af44da49f5f530ed0ba16603de1a12e4604a3e57ce7c9ec1e90de318ac2d86976f8fc6a13f2d7b80d70845a9c3850de06455b220ea0bbd94bf6511beea02342c6d93bb42b7f5954f9b2838fb979b831ebbdee9cefea3c56476459a9dd44b6852140d08a588c5d19041723c58ddc740005a8120d6710c4225b546402135182b086cdbaa122733d0162db0c0817b7e0acd688014a7c0d2ac126e6e9402829fb3244a858582bbd4120671ff663d8ae2fad2c5a715169b8aa2bb5138af9cec5eb6cf60f76251ed6b7d5b8c0b51b28fa4660417629fd9a4355f6fc2bd4c7647171dea19106686bf9211b7dc3f0070c05efb73bbef1c8225bea162b182375b80ad13f2dfb4612ece9f35b34508277b441a4738de249ae526a17a2f019d9ae62585607916823f6bce6362e7bdbc75a7d5663215729afa81385f00097ff866f58a24824217227da9480694c8362abbcd8094a09ab327d74e46eefd87c7d65693f745332088947e3d1febd9e4219a4fe008eaa22c238f649e08175b30261377dbdcc35925056997119cf7e320237bf04d7f4653aac52988214566f869f9db9b1c8e9109d44309c06342e798cacd8132e1c70004286f46ed9b2253a603a504a3be1a3cc137b5f0eaec80ac383f9f3d52298a74087fff8c965a6bf8f60a1a16e04c224916718438a268dce79135593249e85f0b96473beaa5c0ac4528279daffdeb7bb7a67708697c6ba9dc40e15238de4bb85cdda974595d2c5e39f302a43719906f19ba8450d5a8db903a3c820f61a0d3d4db0f981d74ec41f19f8c20916031941746f8e2ec66b90aa943e9a2ed9391b87642972fab2bfbb7b7dc43d06be043d9728cccb6b0118e02d9cf6125684008c64b1340f288834ff43632cd17ecadb9e69f70374dec30d24818ff5012dfcdf30854a2c25056a2e5f890c97d7b895fb5cb1788627ce885653daa64d125e6f68ca73fd3d92b8003f0d3aae7e666e5fb6e041f25643d9acbc0698b3b977aaecfa612c6e12681e83639578f83792d85543d621613f3072d285781d5eea3e4e3343e55adf47752e408df35fe6304783a8c4bea1f6a20bbbfc00b7804523cc961c75677b8d3d198a7d5ba76c4edee066736a04643a9e679acc3f5553baf78ef263a808345f9a78366fa0badbf900e11d5a966a5008a2dc44492a0994d75d4d1e640eeb08ada1770b16cfd02f91fac1e9b7c70f8d4d7797b98ee35a2da020017900ca1821bc85394f5b2515b905b954cee448e24f98b415f83dd2db07ce3a753f3677c2808606ce38dfc6a653bf723cb8b85675ceec6efe3d6d90d89e1cdb553e106d0f5bd4854de2a72a2f87b3574182bb217218c32801fa623b3cf9d05f80047173ffe0aca87fa0e1391657706dd6d8c6dea2ce1645c831dc3c64762b799b452c802ff55be4c8acfbfb79acd6ae6101e08492159a8e24feef27b3afd7e89ecace9ed1597c95f3a168a4cbea6a00d45e08a59d1b8de00ee077744ed9ddb7283bebc7d64d6bf0dbcbf32c53496c9c65018440f1e64f6605e1dea51842d77db10ba8c00e3ed35ec00c5259850ef295e1238bae2ce2350c4d7fed438d1d00d1651a1edb0925b2969b0cee53c653b6fe34178d4bf51967eee4bb09b0139654710765e94582f7fcd59bba1a9c9341351327480c406af63ad238efc93596ae150c9ef62d961a3a8e67a55204ca9b2ddbafc42fc6cc6e5ba1c2b9c146a3dc5050c9c70015f408a523f74ac16bcb8e40371171134720ab27630de776e1cd9a7d8b146e57da37b4a161e8a3300a0e9d3fac5305228f3a5fb0478e5ad91208f221d9eaa50505837d290b28a9ef126d368875f01a1531327d82f5aca4f45d409f0e70dcc663d5e98bda6ef83d157e3c0af1c889f30631a108fd5f44def3b2beb1075b8b740af60dd0dc9dc922a30ba7c346bb47a7ff3b8ce9bd8e4e8f39434b4069b5a529c9c3681f035262c926076174c4c0b1176d69941e54117b13175e6d5a4df240be119f5a71b830a1b5865980d757bc31ccaf45f72e735f97f7bb55fbd0c38905de379e7094bbd52e37332d855a12f968e83d0cefbf16ab6259b93ede6cdda3e241ab179da751cc5e48015f088944fc08b552062038de0d4faafac80dac350d77fcb051c78ae8576fbd523116659dd7785df1219d20efc17e578c8f4c1d6cb651f68f3566dc458255d9002e5566bc8c5b6f5eb2b2a204e888bf93dda1ee055b71a045412827a5111f42b22042802b7eb17dc5c539f2248716721b94ab57632a34e0a2b699c01589f34fc1fce9983586c519cb8eed3beda3c24b0713ef5606e70b72df633babbe584d5f4eebde2fd63ab728380bb5143a9bcff147e6632b4f60c78c6d702201fa0f6b8be68c1d885bbf332f26acf5ae6d8aecca8a34a3e37871d518182a2233653df4dc1809b1c46296658dff0515dcc1ee4db3e978291857da561559adc17ef7010485838edd5c72edcff5a6bc9338f6c816d6e0a1104ba1ed2d366cad2951c4b836ee4bdc07056139707199dacb8bd14ab3c6fa5ae384bb1a35fc3b40cbd1d2b47b5eb9a3a396e73de58701f660ad4714e9c35baea069c30f523b15dbf4f107da87436dff112a647d9ce9bbd90e41b32902bd639607c4fdd17c36cd1c46c7abd8f9cfb0230f4ecba9f2c07da70937a4d9061f4f662bfb95173ca87b7b7d5dda9d3a50bafd62161b583e58634bbb52277b2a376c94f1c4e0ed0e6f0b402ee02242d0e88ccc664e16d79d7aa56cc4d69c891eb220d0f1bf4b28bc3c10fe0b272cce20b2c6735595dd9cf16937cc61ac68ff08b9dfa7f688546e0cc1f3604a4775544c81868ed4762035f286c580d53fae20d60f1476f73be8606cfb0bf86e8c3ca2eb9a6a3accb9f30aae438d87256163f4e0ce7a5883226b7be58442ea4b2ea3a9c64a89d949a0cd63f2218f8d363f7996525e21c6d55109c6860531e0fe51750d84f9f7df7d036327b84d988f093762d0dea617c62f02c44c430814e6243369443f8f8060f275886ef2e826f74ff1e7e793fa7863987fe513ad0de2752b91faf9f138edc24b4c7131da7f298bd895d66bed91894ad9a93775060afe19af5b53234562172c2f39d1f5877c837c54a32735857219a88cb5c5e0acbe5cf1894f65427b7c453398df7993c61ad914b02e8699b87e1d873296de1cbca89dd098b0489a345b273e207446e1fb072aedb6555df6cbe5785de18c172686ed306f91d73cf2b6fb27b76ad545fe9fcb3c01f663be75babc7bc78cffb73286a2d595a5fbc0f05831b9732ab477e10caaf7b18783e9a415ec247628e6e2f0ebf8b14932c8b6add584f0e51f8f2a9f0de5507a064e4d1c8873c2b4e37a21318d23f0f2f1ffea6c9fe5e7498774e69c2a665df35e23201df83cc0040f9ce3d89cd1228ea1151c4851c41828425a2590d873ca6457aa9ee9824e100ba828860eb6e6c32bee85cf5df000eba8dfd743b6da71a158fe162ece667cc84f0f7f8b653fd84d98a8e98d30764f92ec8509f6c5efa9ed06a99c6f235215ae7ac61c2380e907e29c6a18d2617a453049e1fb556969fcdc769075e1aef91977dd95bba12a5b0444c8b43eded14f5dfd5b01cde08065d7ee18858d4527339a12bb8f41e4efe585924e1b9b7dfad1aaabbd9e384ba89e9530c09d7be29a0536708a9836a43f80c11cc3262d001ec20f351aa81510978d16551ae68126899a8979d13d9f6cfd20976cda8d7ffde1a42843519a25f847ad6624844e251e70aac51b08b7b14b69602267e2ebe60b322cb963c1510c37b3b4fb627af937579e14156644a456a179be2217f966dc6be3cfa7217cc277f6de59006393446120554e898a42c69fe92aef5206b83303c6b91069440ab56c16b7cf0cd16c566e2fef0752e129a84bf971cd4acb4d6cda63cf63669cbdffb18038f3e0d8c100fda095d62af7fd58c3a7962f3f0897bb07e6f39641c04238a5dc4ed3feb671a04d3a7d258e8acb3f7cd4193d1b4233f0f0345618d7b589b927f864172035802b034e045aba98a2adf1e78de3f336a2e967edf4b6d56431383893ecfafc9a25ed8785a5328efe0f84de5eb5d190814b735291ebcac92261f3e1b3e657bd4a4e41b539b9b0abb93d60aeaea85e12f6773a2ed4c124f37a20d7733d9f698b300a4c2affa90baef2ac55c6a18ad67af54677e1d18246049f2774e0138b5a15a9c0f7e43881f406283d789fc604a0fb5b5a0dcf8f9d08e2acc0583d5f1c118c1a1b9737b4b2ae1f1594c9b0bf6079e469df6e35de49cb66d7c42b5d4cbeeb344926327d5f8c8d7b9f439018d9e228d597f169401ed092bb803d472fdb6071db835f6dd3369b85ed650197edf7de8aa12d6f79f8dd42ab3d7fdd0998126602ae055ad242214fa2ad86e93fd8d57f36c2cde4ca94313da059cd944ad72afcf754f579e2f8ca0c2751efb968e14d3ce3d18d892be991429ffda0e4f30b890ae1b272e18448b54e950f88e52547a6b2137f457b9fe3b43608afcbd610a39faa09e191617a6aca084eced1e5685401884811e625e5fa097181e7447ca965ce587c867999da988b3de4335ed777389612f4892e2cd03742212a447107279cde867a3f0a4a21b6753993460cd4e0df3d81cb17b9ac128a5e5a0bb5609c70eaaf64756e70b07f7c6e85f9d27e0e4d5a82997d1c2b271966a2c4266f3cd51cb8f5f6c4f1235917843a390ee1527cb89ed3e5fa6226619a6809da0154fb6a4f025aa5155c1019bfc99cf466679a646d2756d1534123bb60f8d0a9ed3083af2469bcc0e13c84673f62ee639f7d0c0756fe29ee8fd454124658f697613bd1fc89ae529694f9f837e1b20e4df0be9edb9775562df9eb66adc62e2002397b8f721d314aedd23b5e1da8b8a3df2f9d50788d3a407f398e288faa39422701323dfad647b5ba013de329dad1c71766246b493d9e7c7faebcb286b0574c193471d08e123d9e0ca901462c2d9ba80334c7f59b0f36ab58ab9630157ca472d90da14f94baf9a55e5b83d69899da941f118c2c6e003d71b95223b11ae3ebd02b7b1b4c48f3df432df3e465050e89a5ec583473b1eb3f27929b8b057167743a6259552ac3f848576d4e194395e9d68466200156b35c10f95ef6266e7b36dd3348c4fb7d672bc4927327bf6d522311ae14a118654b96c4221a976b12bfde57d4f82196170d973407255241fbcb782a3905f7f577a44986f43ffeb58d30574f7ea9d48f4bc0150f75e6f1d26b0af3eeb257d2a7fda8f9f9a355e12dd7a7c79a7ccc46a7a17a5dbfa9127234a2db2d978f598ea56890b37fbcbfff70de870986efaf7d96167d47f640db0034b09af5fef35a208e3b150e6acdb6f9eeed348f793f926756a408fe738181c7ebc03ef4b5d26774230c4013c1ed0764312eb0678a99f4e452d017482515648f092465596e993cfddbddd17ee575c98fd7296f650c267291775a39a321b4cf3e6223feea12280d91da065622d50f9173e7357967a07406adc84e5065b2bc5a1bb706b7457af477bd071a31e6ca150b29815c060ce5edba2e8e16f9a4eba032e47ad5e41b4f1a4db5c7441a0717a66de9806f5b5be58a6c960b08e3ff0b33906d76a0df40e4f55b8647f3ef8d588077d5200d54b90bafde19a549e9267c942273bf1d55e1f518f6a5ae27aeabc9ce28256ca4597dcacc508f3cb7811ca79e0d0037505007bdff7f8b8fcfa6857d2eaa920ee05bdb07c850e1ce2108748d71acdf47aebc687a8f39673095cd86e6fb61ab69bee6022564635a7456d60728c9fffd8b104c15218e2c6468fa1723f5edf8381e2237ac8664137a703ec4ee73cd03d2300d714f1dc3793c869be4be4d69e06684b49616e607c706ecc72f1d95846105b4ca077bdf2e66328d23ff6ed356910a6dca1ba8a587bce22c22a767bc23474f1a7a1eef0e377e99dfc4201242342b485dcc34ccfa3d6a7b12d11e0b1dacd1330c1058feeb085ffa19f8b626e5d39d928a33d3da5c510b67c1cb66046692903b2560b62311525549f8e6eadb5971222d3ec1ca4ad3f2c965d0389111473980b51b4624a543ab9bd79108411f76bfdd3680a7ba4e7f14ed3cb34dad1f88258d53814ae9bd3031ff86c190936da1cce17ecaaf0d7f761a58c2df39a4e7fb9a8897cd1a40274f502231e4018557bc78debb2ef021c1e9c8c2c6a8e0c1eb147f113ba3e920ed76024014eecd9c25d9dc0de2f33d32c9c5bbcc8fb394fb117b71e19c3e9867016a44861a0732c1686bbc8f90ef447dc7541951639d2ab07d102ed95ea2ed907b673a21267a4c660774f63f31b1f84f2b4be6b4114b48620a4a1d2667bb8af11e1888f331699cd51dfdb1161883702609573d9c8b44dd722507e3f06cea4595aecc7743487617e8bd94371ff1bb6a8b50abedf118432cc700179776ba98b4a495fcc62f9a32a5f54717ce5299881cb5c90a179a2c526e5a4a4c5e196de5463ab626184e90c255b1b6b0192bf1b427266072c4d1fb79459313e0a6cfab4ae64344a4bc4dbc65aadcc994a04f576da7621348e67c2e5cffdd4261e565f761a046b4304d81f694c11ff43530d215c045256b5e79268e6bc55613058d5970b4226dd0960f6570c164d4df98c5dd930a327f7827e486db1b3d247dbe8d5e396c41035e1249f7e4d14a7f1d85c47d69a0634ddf942d2aeb778fb7ee2482f5a5811cf6197a407cdb4a318d6632b05b5b351e2e183a6e03335ea5d0ec6a075b2c620527aeea1c8825e8a7e477250ba9ad057b352ea7307b30c9bf668ce694aa8cdd0a294953bcc395b2d090c4c3870e2ef52e5ddd03b3ae807b38d02fea3334b2ae751fc3c85136ef56d02a85ec5634ca181b4fcabdab79e1fe6cbfe6a6251aed29f4a6adef920e7e05b5f5bf857c05d7dd78d1fd90ed5f9f09d7dd182fa728c3d9f549600ae650c2cbbecbf062f6c048ae36189e972526f7f70da9048a44ea9cb020b24cc02a9163374040ee74d100771f8d85ae145d5d6f8a5e4db45fbca5adaf260a1e3497df5b4a823eaf6d6760d893e5e27f41d892030179e4bece2422ed67f0c0bcad351c0cb14c06f8a73d4a50891a4d107ae78618c7cc31203ef91fb3be7bd70c239030bc2c39fa5f30f5481781ef2069af86ad96271c6414b1e611a422b09793c9461e327f642d060aa1c1bf36b84a70969d03aef3abf0b2976045a2e0db8cdec0074511de0266ea973997c3ba7c6c3c65f0474ed5dc576351fe6c74b2e976a5fe90ccdcd9c934f6cab767421afab928876df58b381e3272302453a11960af53715cc108c6eeb57632adff3d0b736f4f962cb9025e9e3bef3dfcadfc2b5203cf71b60bbed0d762fe60d4815cc3824c9980c6d3c9b1229ba100c25702b3d1e85fa5fa13737d3953e5bdac2de233b94819524d795310dbb0768c5d8a085b92182bbe998a0a3636580690418b0f223884420c36d976980931e4733332c4829e53fb41dc827c66616e86dd17bd0d913dfeb43595683911ff055676a65d6e92d2d7c9a5c16345b361aafabd09d34937934e23dcfdc9f63bb484ee4f947cd1b7bbd043afd7f6de317d8b2d2c87244e2d0cfbb2fb9e9f4ec4062fe8972776cbd2479bee664fd06cf198dc3db083c0d3c15ab597f8cb4b53642a190619042c51ff122e51171a45d9c9dbe6bc17906568eb76eef55c04eb657df63f7baa7682d1e184d7acc29a20585c10d9baf9735cb75b706dd04a32edcd3069ba1fa1a7dee97fcce3311597aed0f41a750a9a6b976326d8e1fcac8d730bdd933ae4138075313b0246eaac4afb63efb6e952788c362f45dbc9e488971f3fd318a029073ae4abd52924da29ad8c13ed0d5fb24249af363a313ad9e0ceb410ac87dd8075ca9113e382f12facecd48ed3a8ec55e44ac4053950c824b0fa99d539718e751be3dc55395b7dbc57ae2ab2fde39281b4b68a0de47428cd6e6ba13bced2cbcf1db06ac7c8dafd4a74bce121515d586d25dffdcfd65cd72f2b9affe68351041412812c01bc58b4f3fdf516dd79a6542a3250147ef6afa15ef928673ad54a38d8b81eb4fd7ac8fdcc374a0df6349a1054fcf7368446c9071ced1d4577dd1ad9db4fe88da93d660e01a8bfb3cc33269904a9cce84b70744c6a1af0f58ac36e4b05f1adf92e4083fd111c9a9e839b57dca6a572c2f262066b3177b7b2665bff407a088b1f15bed4905a9969d87141d72a8473652a7e304f7fefa61bd4a6c954a0abd0aeb61b070ff89091b2b8b14bfdbd9e7d8a854061bc875aed18945f6087b01e0e97b4f1bcf51ed2e5b4044eabb5f13dda04188c5d800f108a9b808db467f2dc449db65c1550e18639d168971744de94c3338b4671bf8b6df820ac9c5e45f2124c6e96906f8db74ce940479932f143070227b5216d415d01c66850d2079535b1423529ee3011169885d1de23ef26e4667cdc00e5f223251d306b701f3973dd15eb2c24f61e9531934904392ef0395620bf215843b1cb73d55f9642abfd863fded7b77617667551097bdc89c760c6089c311deff164035f9ea4a8eb7947640bf00c5ea1d7480f232367c82a3c4d8a29fa0182b2ce3363bf66866b74d22eaf3af580c8e97fc4dd219650cd80c31de848db94f48941434c5d2988436f183ff98eccad65eef517881f95a613ad78dfbae80409ef9a5fb5bcd71275a84b560ebea349b75b0c4d882ec5b55e274d22921638c09e1db846d06c1406886cf1180b999125ab28cf9f0224949f0ac0d21d409b09ebdeb1afdad36d9bfaea26d8061068ab5e4482a1b0987c200acebc6c7dca609332cd6725b7945ca90f069cfe5bf49da7df79cc3f7e9997ebb9b4f9bd689d586f3ebeed65f383e6e5a8b30d7c898d474f8b1e44f813d76c4789c94f57f4d7c58aefdf11522f1ce15809edb807c9ce8c41616cb539409c770d5f0bf867e55710d62ba12bbdc5560bdc1be399f27c1cdf6f3256bf720c2f13b519bb7004488b8de081f27478bbd12aef526754913d2deb231bc2a399a3d3911b2c4c7efe2786bf6545f5efc95f7afea55fbdec269f7f09009afa5b7862483a71a2b399bae41081c66471df7f3c91486b445b8c82b84498d2d1468059645f9ea5eb7eb8e7faab10a398fe78734f84f46a241bc5313a55375543b2f919326f1d409967c605e2f3e0e0260425553b7572a3e910633614f0e4ecf27c59caa9c34cc4e32f84a195db1d321b0db173080bab5b5ad624b679e6e3b7d19d5d1a6aca5b161d2c72debd175224180eff4a5d983c84f47e94960cdd55efb94892312c314aa9c7df07d517cd88dd74fc78e120e1cfd63a43fbe79b013011e5a47f5d6c993e1779485a8d82464c6d843062822782d2d029bf7854e4ca796612b9fcb6203b0306c116562bad04dda32bb1041ce439047ae77eda4afabbfdd5621be4f6974a19c10bbcdc94ea332ba72f000b1f15eee66592edd67920ab6a733faed3f8eb9975e11c0335d508914e7b035191900dfa8446a72c81c71cc37ff9e8bfde547b49463f1a2708e3c8d1e334e3ebe87004c164aafb4e1f5190bc95dfed3c7eba84e2eea9fb84b2e96e78fdef9ca4d84d705e4700bb9c50338f57edc7d59056c38e96329103cb5f054b2e55cee0607f820f48a8a0fba697b8fa0543c03bbedbdfb7333f15431d79c03cdec5aa82510b69583bb15817e9d0e7cb1f6b7a8156b632ab220c9c50a8b6f58d4783e68f3344b322757bfe65a92e5db3393c48d0e1df8512a82a12956ab58542179bf78cb4db07dd5c1e17b52c1bd43f4a24b6cbea926d5b82d8fa17eb7a3f2e6f2ff37eeeb908d5a022e33d4f313069cbd9bf01401241a63b4dcc1aca2be1c303cd0dc38f143042a9b63b6c34527dabfb7dcffc6504d31fc47a9aaafe6b8e96bc9871a39f267a25815d0e81714cacd4b46e9aff138adc8869db27ef564740641a094b04b54db667d85c5ffcde564f49b5c301af11432cac022d7eceb9e60072842302790f05902bb6ead2d655448c2303793a65a72a8c737b909544896f2090da8eab61947ff883ee6c720d66c3790700f2d171bf2401c944d6c4afc20eb5f09015f57a9438b9c85ba168ee76e99b3c91455973625a81f6574b6e42e8192a07cc5fb46a595d1ce507e88d9b797fef786f0191f549493f04b4d3700c8a179d537136a9a937f62494e84b9af52f2aa2325f001128dcc1c185ebc8f785952ecf1d3b68edc36ce29bc0d77682bd412aef7fa227dbdd86326a1d7a594ef836e0fb25c9f993a0cd2bd1f7751a6a05e317d2badd1a1f439c28ae65759e5e955ab136e19e6b3aa8b97397d90c8c89f53c166ae296efcd7e07f9774282056c722e2bd8b5baeee34faad7e80611ac1b36a095219c288c9572b416ef482e0180d34bac7e72cea7a8a7d265551d598db1f1f899dc047465338a4fdb107cafe00c759c6e33de5cebf4de8b0f2aca49cd9585901a9c251bbb56a5f410a5d55f7887e1daa51d73ba948fac174940335f23205d669452390434fb2ab1be6b2f9ed9a49cccfbef285159d1df8b3c910f268095e71941eceac26393ac651cddb170ceaadb27a312e004ff62d300f3b52dcd2557c0ed71808169e0f56b2e4f8ef12862349a787e64d146cfcf2622508cc4c32c81355ea22963f63d3326e7638298d381bad8d73ba6b639b4bf97a59f11e2fa8461a94b29eebd3fa1df96cdb09671d36ac40d402f2785db8fcf36a08dd2e69d6f408ce63586b963cef7307916351c48402218fb43da096684ff4ea986e0d0aa46e19c2bbeac671d91756aa476350710bf5d1b3cd1cedcfa1e3a6877665a91869920dcdd08456eb467722082157c1959a2c7c3d355fd42b62e2e92cabfd7bf8a107421ef5e1b5d18639cfe9e462420e539bd9a9c17204d62a3ae0c2a1c4b023adeb118a093dc89a78650618dec8970ec43364df3a605b426425bd70a4742a498146255ed7c3ef8fa542bd7ef1fbb17a472f4dcf7659690cb534f6f3e96ff6e7e19e5fb9750039225fc3c099fdf4bbf6b5567f517aeaa7f9a88d66e2911d3402a6cc3f50e0ddef6036bb49202266ba34c8028b4a647033eea928a9c3a31ed7ceff3919bb85b800e490f92a9ea09496234fc6ebe8206ac4b0e784622fba26caa6f073f61accbf6b74929b70a3115e785d5ca98517f3381286a466a76ee61a63eb6f476bc712e70200137d2077cc391312cb0b6a7f1e384378b3fcc365ebdab31cc9df7e3c9c4d380f35e01e4ba837f6e4d71a85605bcb1ff9f7021c7cb0efa85ed40ed6cdd2f2b4a1d7879a84348cbfba6c1d0ba2385194fd0a1769f08c9492fae3fc8b2f1d36ee25d4c92d48b2404508d0ead9a79f91cf153214b6e9227277044965fcbea0c6e00b057d4a50f1a94eb3021de08d0892976102e6c4bd10459ecfd17cc1b5add4701b0653a8e3dac1841ba5f1de0e33c54bf077aa183445e957a99dd30670b5c81d744163c876c2fb0474ba7b102b32ca723544005b957742eec1041891d1bdb987143294f4d24f3ddddc2d55c3863fd2945ebbc41b50cf8efc9c7c40f56d6bc728ce0a50e621f70570f585f2c474a5202647fdcd014bef25cc7b694664ded28f095424b35c9b8c846bfef539cbd30477f43d68830c974666c4fd209b91ed36995eda133950361cb2f2dfa937dd8f785fcac4c833e1105eab42721a218c93f092fb942da8bad5c11a02045996ad2f3d7e51f7859821706d8cebbeeb387ebfd3c8947c1e1a4ad135e85c9971bb5e2cc4574db48b79621b622f33639020e8b165b3084b0a5ea4ef9140c6a119d081e5904831bb0eeed899272b4092c2323f39ca58f3d9ceabe93f0b941da24af640732a9c309aa8a6a7200107ad84e52799bbdaf14fe415ab7b71185d90a2f80f2575519577900f999bc2e86f61316863376f4cf7a38f92e8cc5b6ff17f4ed63a1d48cc5b3cebdbcf3785530f2b93232947a349f23d946dde1780583aee6b88f2a17c49dc812f4a397a6d91ee4af05fc9d8476e01991da5c548cae574d1e0256748011b484bd26677dc33d88c53ee16ad7ed81392a113b5ae76c87a7e6bf9c3a9fcc87b584c9f323bb2a985326e655110a350a6c9be2a233a4466b0459d43fabfaccaae0fcfa1768db6358d116d5b3643a26e5aaaa0dce95efcc826ae62460ccb11dc6e2c3f003ef11e98f257aa85536e31771c71905e2f870cf95a1f70e9b35472331362ff20280d7155e2aaaa7f06f94c1f6cf4c9ef687b8e2de42d884a479f6f796cda37a0bb518f2f13860539bdfc18b7996f368f14ea97b432fb5265cd96b21923caf5730d1ba0db64fcf9d926eba3e2339b33df3fc2753182f6be9de6695d549d60be7f2d157fb4dcf4d605e89baf0141475547111050f34a73bc90faecc1f76aed4d78d536b105f4567ae960229053a3c53b741bf27bc90e9b80387734af5424c68ddec2dec47baeb5c86ae3930c456c987cbe5032a9872dfbf7280fa71ef544430eec1f57fdea3eb64cb62365d2145d1cf9cb0f52087314173aea6f34dfccbf624d96fa870028296295a6ecfd2ea2e7606a726bd4d0f25fae093ac690c1f32b46f9c747efbfe712e7cd62804f0c576339dbe0f8045ec3ca9f58d08c6d6714967a8599742af10c6006d216dfcf38ce6c1d1a648a484f69135a39616cdfb9e56fffd5a6be687c1a38d83475a64ce0ca4a20fcbb379b065604853b57095e3252535d90b1a00ec1500f300aab98bee6bbf29c2fee6a768b10fdc1574e3137ae12297ad2885d1d8440c797b525b5ab443e5d973d6e0997b3b1fb12b1fab6c4b1149fa6eae73b009e5bd564b03807230b7e2d8e08c7cb4d0f940e3c6592e4a57de1b848fcaebc8e2b85f406baccbae006bc67235c530db84b420677b0b41c68b5b02eacbf5d401e40d6126054556bf4c46e149ff490e1de14202dc2e79f52adc38216c20ce35b42b56a6796988901fc7d991fd440a501e131254b1b81ac9078ad17f00d7e5bbf1c0860848fd9947cec646af03a0b96f14498bd76d491e53d9ad32e58057ffa086cbe35447c0a97ac110c4d6cfa88e8b4d3b21a36965b40a8703a80bd8ea103e95ec5910ddd14704eb83e940264c2bc08f3b971ad3f2996f4ac08f97e6f194844b6ad11a5220e1c969b0fd28e42983f3285bd1da3bc8b550e8b8d317d72c894ea843648094b77dbbbcaeda8fc253b79a84722d5eb2d68e10c488c0f7ec56778934570bed037d2cdbbfbe831c5288fcec468ddd7b16e7fdffe63d9463aaa9a7dcb90c196167074943b0207f6aa7a88d10696d94e836b7ff9767acde64acf6616b47adec586a7aa48db848aae8cc46dc4fa0659e27fdde14182c9c1b527663976ffaec3366609be71f89ba0abd10cb0d183335cb3ebc40b95147f51d0b567bcf4f69a81ebf75ef4e7cc59699db6b1f9dec2fd095a6e32a4350cc90aa02da3e774fb2ed235eaae81a7ffc055677452b6239ecc5f2e26e9a9e5dc6468f5d758619e05146ffafb520ce8948a369a940378c8ec240e95401194cb7b9bf6926d2b437974b3fccc8295919e1881c7774f3c113ad6524475b06039c188090246ef03e12233d368d2fa12ddb75f20c6c9a52fba4e0d3d91d52dcf67dfb9c5b4f0a6ad2c15ce025a69ce9374439c583d281830e1378653153e6f89798d28accb7f637ee27ab7dd5654961fe3f475d82091254bc632c9bc0d8bcce8b3e5ff615536b03f1d61931ed1372dbac2b1ce763144c275cac1b57310a4de0e391207947f0007f82670845545ee9f15de651f4a86132469c7292ee0ede05f2652b9e9d78863ac012018fe36fb9b054eaab11248693a87ea39e7dda15140ace99f51d447ba83cc10eb1610b7c0154181e398166807d11bc46b4ec68ac370846b34ceb6bc59e71cf0fcecb47f58ae3f47164d8eae4f9fcc9058a6255b16fda7ce9eed69fecbf4d6f4584cc701598429a74b3f58207f5c0cf65c8b0fd271d7cab1460f03ed4ee71880f7fb87763e19caca7f2d47701a0b26bb88a7409d41aae036e24f860b9afe70a054ad2ff95476db8dda3fa48927d58ec2c51bf24f5ebc69524614fb2f1f1f290da3fbb6869edc67c706cfd9a9846e584183d885dceb927f0dd48ff72b5cfe6abc0a87774eb3516bc34f990c4a9af6a799ec78de52d1543839c431df7eb644498e24bd20f4805f98d85e3a696182b52b88dd34e9674cec21fb80e8cb9ace3740b6ffab7fae93a3e7ca5c503ccc49f9aa5ed2a30b2ae29f0e9b2ad7f3214432e529c800598e366c585caa19ca08f851f4ce121fbef7d591f5491329eeee9811ba86a7000eb8a7b22b5b7c555bb59c64d64b57008afe8cdf25dfb4423898668864f735bea00704ea7d1efcd69dc7318c9087220c84715903209ff945a5e7eb30f38e9224a22bd809744c918d6f2b8861205e2b1b79de32b8219b0145054fcdbae63f66f6e96a505ebacffa8ab0bf04c2f6e2d01306f811538b09b1ce452ab6d5fe39f74bd70065cbd7038e76f701a448358cfcccd88e6433979423501e9a6c29d88b15573a6b126976f0a0acc4d5738b37791c59a3a4a66773d845b689dec7f2644f0c4f54cae3b793d27ee009057b8ca73ad6f43a77506e60fa3f69f953b26508ca14cdf7939df051cc574a88eb989e5274ac86056290a95bcd24ed49d9f63c8c1d68b1ab7ee13cbf603db4751017abba95fac53302999343363eff659ba1489641e8b654cefbfcb80c600c9f060398fb82bb47b18bb74f69c11a6f19b3ccc1dd17f9e7e9e194970433b64e6ab237a751cd4feada42108a47657fdf901bf37dc516e1c673b37b472e79efa79f0f6ccd55bb5f095f8a12c99a67c8485ba932ee9fe3d1f3c2dbd59c858821df30f10d795679da84aad922d1aef78123c7c6171c36e5e7a6ce8fb3a439c9003cbe52bcf56b674b2123260e5b2fd984216267737ae87a6273585f3ccba6e5f7defbf65d2d7c6dcba063a8eacbdf7759d6c4b4491449d8e1983b3b81bb7a130ed1e355ff04ba393589351927fcccc5336b5e4219dc29c0cf93b1e71d7e05cdbae96d253055ae29c890c6c1679c43c7c58daca8fb2c5f3f510dbdcb9b6798a5748ecfd7eaea698f28bfcc4ba64f5a82f9f3f408c9df22a0087ad50689387dfaa6dbb156e3bcfe53f73ded9ec9d3aff56f4d954a7e5e9dbe037b0270f651070eb4a82426791f49e77bf6117991b66ccf09ea484e0f5e76608ba4556f1f8bc14aa20a258a00c780433b2c04fab54c75630287369e312587988c571a3e821b06ed54400a385d0267f73060df0c508bac9671ab1e328b632f76b800a78a1a004a9cd52ad216a087ca6d1760b0692b163cb9aa3cfab4c539f66dda383043bebdccbd091548eea78105818328b2396d42a4efe6e88d26cb05b67fb7dd8764e7456fd59cc947c39602e7a09416d0aef8b1124a5b800d6a1033506057e3b10496f4d8dbc7195e2f1fadfdb53ccf09e21354bae6be78bce407b0aef1823825f275f735e988e09a8b46cdaec9ceb0258ed65855332cedb4f8926a3e9fc287176adaa6a655408389ba3eb67e54dbb5c2ae53b4534eb5810c5020d5d7f38093c3d97722e03e142e4c6541f7e79bd4fb115aad4cd3866fbae4ec292bac2ed680cab6ee522d4a568a2825400e6746f2bc56095d1bf099c1647dbd0fceaa02aaff1880394823695db859e096aa80889bbba75ea5206eba03e0d9d09229dcfae4e3a6d36db899d7e39681f9b5f146c7f3b25ab2833969e6a43501179db64205a1f4f94d22a204050c69d350c8dc543c3eaa86224eda5fd4718e35bdc1b2799d4555cb4231e53c15ae685602871b79cb940932ec303f6099bf57f18468237f98275954143902df3ab911e4fd02ce4300c16f5a9f3d02dd47e6872468ca034417fa6413da2f8fee4a0f38b5bf892eb7a2a4091586ef9e89c81cee9e8fa1c27dcf4e4802bf1834131ca3496fd62e6661219f8ba88fcba8a8e6c01ca0650f5565761fd880bcda12c4d2bbc4eff62e77a63af8f112c67ff5fe390bcb9a1a2794e32789c5c382ac4b62044fd8f54f4cefd59ba94597898ccf2ccc395033cdc563c036e7054c248cadaf7eac3d1a819d9dc6e7099ade0fd6dab62a431ccbec77484aabfa6d88802df8302797404e36f29c383b42fe2d55a7621facd74b47df62333b41c7c8cc31ba93d57795911e1f95aa32f7ef585eccff993ff1d52f581e38e090d4d1e1f0b0399281ebc2d8d38e8425b5285a72607c5ffcd11c30927751d94c0116cb827d951013faf4bc49cb8208fbab88bd869ad668d371b43cc06b0665356a30d26ffb1c29430e429714f5ef31124fdd05941cd2f3d39dc67ea217643fc4395f8de3982e9ce66d4a70a560b644371dd5b5bb7edf52652ac019e94ea9647a7085c629d467cfc9857b61f1053ff67e862983b8672ce61f53da1ff592c5671db84ca597e969add64dac64132372c56aa0abb6203b6cda261ad64014e9bd5a3a1f651f889353fbbc860c18b8494d911dee076b14ee69e340e272f5ac1d5dc6d0a0061ce7f1010f9f3de11cec9431a5aff2b5d832e733a469d8c8087fd163e34e69ecd10b4e0431eb7d3865395edf9bacfdfc832b4097852395a737601d5d7bc037793ed6025cf153db49e34aa5bc43d4246047b630cffb8bed49bfefb77ae3f6e088ac54e4efda31363462b00c5b520ab73c9dbf969be085bb0a251dadc6f08447dde0980536eaa2e59694e971cb0fb62f36162633e57cf42f200bc57a096930dc6f8cf3e024647e809521a74cfe11452f036f394ee083bbb3e597daf842ad8f43efe6a3903010077fffe07d3c499f5a0924a22efd9153e49cc5da2b0fcaff776051af2fad7f665c5cad94e44ba031fdcafc8eef78c92a31d1ecad7ff1ad7313864d08a35e2e9faf0174e21814b0450ba47391ca536074ad97104e242d1341f5cb1419872f8ca60f125582f38f38a5168b77435635581eac77bc62679e54cb88d972d03b5f5a5afed85686c29dacb2573e7e4fc203d82ef9a4d0acee9cf64f5ab01b7ce49388dc43be83b397e1ea732635c35bb1a385de369c5b3b5e9700347e093541a803b42f5966b34b068e20da0f9bcc9493d57b7d3c26d7598e50d32c4b6265ad64e8c495950d7ddb25ec3598fe7e7ac16b4533b047fd03a3d164e80626dafceb51fe8d4667e237b01ab8ba9faef584062ec9d0029f44c530e9d1c57c61a235b17d80b1826cb48f1e6d0c8769a67628c44c3d73bc8c29a6161130cf6a0fcc86300188c3be75e3f4eb2edd45c24c9fa15281c3785c767642a6608a41f9bebb4b82ed0ab8aaebc38299cea17be49cffdf642d4f05e755ca903eb80a50048d9d7f16fc481a6ce6e2a95682565b21f6ba12c4a5e2c2777319eb62b7c5fea8c507b0e11c4fd192751eacea4144b05eee09a9a8ef214e3502815210b98433d5b7b8ffd86dfaa3e50a31fd09b68d5fcff60a6039fc6ad3b7202297b0fc2403fe766d1c291e11f2d29a103d846b9bf2c264d928afacaba2db086aa98b3f78a4002285e5ad88a51c200e9e3e90edf208d69f63807807a712f7c6ca8ce0e8e9c902ebba3ccf79d758e0e6ebfd6fbefe040bbeb55798fe1c6c557f254643315d2682b0867ef0e89cdcbb7b2cc061238a5c57acdec60aae4c3cfd34f4037dd6d82a411d5b2f530162eea0290fff6af7ef238f9a3a6309857a12ef43d674d5497083c2af2fb364041a803dd84b5c68c3e043b2a0f94cc1bcdc81da9ad0144f30240314e24edadebf30684301c2bc9c47de6a47f0a33bfbadd9a9b2e138b58218b3ace37138f55c0446e301a08e50ce6a8698d5ef930948a34e80f10f958ecef21c7a4aa363559a92a455ce75127b2247c12a3bf1010bb5099142222394f582a1a3c2019b4fcefcbb00418fc86bf0009218a26da566fceb411c9152f5eb865e1e3f4a89f1d444e3ca4201e2cbf5699f8b2259c6d2e67dff5fb42b3a170c70ef2916559cbd5c7bcc1c4b4bf1344641dbcabe9a8f188061952bce99fd9b4a1a73f92142fe30b5d148e79ca3d77380e37277c10d8641e84c3e16103fef7bca85d5465bc9c48e5c1df48d575165e9d992025ea76f6f116ed64771c7dd79e2ed32e5d2b24d0341c98da389314af2d1251258e2bab9c37920a16da432d983c6f145182f989176493d2921c2b148032062c21a5689402439dba5cfdda2578834e2102c82f17f02cc6e5930172a3e4cbaef7851fca5c70c2e474890de3786c78854699dbb063110be84b2170a225dea878586fbc1250cd1b7c1cb83455ecde7b9ff5c90c76df9d8c13b6477cb5f9f40b644727c12797b85ea4376c2e07506c0dcc078997bbb7c79b050513755056dde56f6ff7e424a8472780f895447468bee73e99beca9c11eb1e391aca98fabf07adb4bd6c6273a514d9c8f98e398745507b4a2e3da32225304aaf0c9b698d53982103d98dd870fff4c4a2293f90b88f4e2d275caa97fa5465cf9b84c81c321bb25336182af057b4ea7fc53e74124a820b356a080d8ae52a40fec9da5a6e13c21274953dfbc17c226ee6062fec1d1d55e9d4cbb4aeff294d4b3577eb721792cc83f894ed10de6f4833dc4a56541deb0d616619a03eee5aa39a4a03c81a81eed7c9d4bdc01039d6ac50cfcb25b421269a280f17dc7b745a298ba0a39b85296f3b601c8195a65c2f5777aea9869a7123f9e2e269640c1944b94db4112790f12787f7d44a13c9f20b38459e22c5d4da5f82ac63b5e4903e41a537d007029823abbba95cf4693f500d7ccf9686701d3e8ace744aa0d43d2617829869dc42103efd9caf2f6b85d7427120ce19b6b2b4f5e639924855c0c880d2de75749012d7bba0487b0ab0be5858743e18b6905279519a51c63cfaa08850daaa83befb63a61ae87fe529368b7aec47126924acd97aa27fefc6e698e14009ea23cf867d365acde8b4b23272a116d1ecd41d9844116270cba7e2d8f88a06c8655655e4e01e32a97566ca7939bb668be25fe5d76e09e47486cf471d7bd84749188552b06dd482cc9677a7e5fa9606bb06d99756ea7dbab75073bcb7e84ee38fccd1b3e9d040a786af27cf6c97a46fe0d8e528076b8714cc8d865d691b8cd055fc0907900dde0f52c96322ddba51e4a6578e975a150cb9fa45f6e89982b3315816ab9015a60e3892fe2ab844633a969f06559cbc569b86ff68bae15e3cd10bd47b7234751bc16a11c282f8e29cfd287300daa4751eb00a8cf40222efaadef378a597f98b2ff09454ec1a0c03a339ef1fe49ae4c6fed64d13bd5ec6fdc160b4f6f9375a977b71cc5e51e73b9be2e6cd02f4e01bd2b70cca08d1e0282250ba1f4d234078cc1f9f24af044c5a92399cc036028e98596ee6d8d490aee30978b08ff4282a6cc5cd587ab7cce570054bc60996a8ba39b08ae7cb2af300894982bae98118c8112e3f7588207f8984f63340a80f9a00d99fb16d44bd4867cc260dc5c69c4e73c7b88da72951ede9fd78b328b39f1c2906ea81581c4f00a5136f2c907f6946d9d78715828a09e015b7d00ea5399eff9e6916117635cb25b87ac6a599f1ca52c3c58eb5c0c96453ea5388cb7038b6c0149308afabb01f6484c8012f629eb6704ae13b39d30d81f6b3343493471dadab29e1f9a7dcd37cbddeb55b7ce15fd892e86b8d90ca4c705622bb9fcf9c807a7b97884a34d09b431dc1d4a3b02579cc380f2bd60b10b05efc663a169ba088f023ebe3b95c80e6eae718744f9c1bfd7007e31fbbf8ce810fb74debeb59b178a4d1d6dbfeb7c47ee1d26e269c93b54bd273c287fde7b32e25f0ec9fea17d226e42178f60ec9d0801c1228e382325ad8c57cc6fc9d416d762c16ba39f33891c1c32028bb11d5cf227fee6a99c28582102eda348bdd0e0e0091ed650c66b07b91b1f758d85540818454d6a9131aa7118a496ea070bcbb1d042cf556477e175a6a0b03b6ec035e57b7c54b4e6263d4c495632a7e9a335a179e474d37cf54863dfa31a7bc314d846812516d4c20349889da8047cd69ea3d19ea1c8267727d1e0889bd008fd49b8b07d007f7e6216900f195146e8a2ea76c0012ff370eea7f1a2b52a1f7f364fa8884b629c273d31a8650896034c2bdd02f26b40ca4e55f8460351ed5631c426cfa59b059298fe254dafb74f5f8d4f50db7202dfe19791476ec269b82f06cc8a2629699510f856b5c4e127a7e9cb8293fd4d0913df50be66b74d5e0d5a01e6f38c741cc3f9d9853ff2f43562d188d11e152bca5680fe32d9fae82d8550bc08ae3c91b2508b1377c5e817013c33f39fe87ad0d5a70364cc6b044eff0860efa7bda9fc94a729612c63155ca56e79987ae0395ea3bfaaf26853ccb07b783ddc6ec298afa70a1066b2549d17d809a2f774050aca7916e323dfba3aad8605f9a04b3385ffe36685fd579cbd131d43996a743354b74d07d3a913c9f1c076a716426f7b1b20e3605aceb3aa9615a9bb06077adb7d9111173162b3c0cc96614b0e1b2ba48a4bd9810d4c9a0514943c82f8533f3a22eea335ce8c1dd7d3f35da50f31b7a9243d2682c015808405ab925365b248b810059f9a91db33491ef41dab0287b5b60cf98dbb8896fa435941ca61d8ebb7a3ba8de23adb8e6dba7b3bda933827177cbd5598b82dcf496a0480e79bdc80aea57f457482554c771e9fc554f2928ff216be7663482dd0ceee74a4babd64aa020feb720b74f2ceafbbefcee3b6eecd2c39975a095a19c96f29dbd8a74801f636abe65dd64aa052750d90f70f828d9d067e43689de4c6473f00e4f3316f740b2a46eb5757770734580779669bca7c7ac89c24cd105ea10681ff87d261346923468bdd8940f465060bce56df10b4aa4e04c0547381e79e9dff832fab2ce8dec05a1a49429e888f408eba7b4a2bd9552b9635a19870ed1f9418953d25aa8631aeff5a315ab4d29e2394fe75868d081534f5812a95bf032d6a2ea38d302bff1210a95b6d550f4d09faccf81af1a6bd7e947530dd7cfcb1250a732f2f79cfb6ccce40f25824ef9fe98932771f6162e6673275b094de6097e099d623c92236b662f4c43078e69a9054a7ab8833b7c59ff7722a9d88868138d9b265015f4f87faf9f011683c69a6e93bd24565b94ab386e258244022d5f584b7ba6e4a034415807a0e3278c33d8d64d66654cc699cc67fb195c104edc625eb49de9a443ee4a919f2989d09d81dfb6759dd649a5ef7563a35a42e50ca1d23b2077bb27ec8c6f1a463a23815f300af0428051ea0c0eb3bb7fcd70a3dee22e790d12e58fcd99747741b000000806f90498482d6df626ee00311af8586aaf2d2e51a5db5e3cb0684147a4b4885c962e85162c4d479d3776e2e758ba791b4e502ad35411a93f66936e199a094264647720a5171de1b49e75ce9622180ff75845474868fdfc4c48a966016482054bc2d64e01f580bb7fa9eb8b983c603a56d83caf3149e6b4410a89118f7da5e5cf25ee72c858b5c0b97cf25ece5eafe8bfdc71f60409616ad730b9e37b336e3f1aca5fbbb83fc59ef100e00c5007b4e001cd4aa2b00a2000000a76b70ffa5e3dff7e329706ccb625528e5c85a2b4d585755050bf49469bfb44c04f0d2bf9664e55bf5a97a5eb31d681ae31c650ee4e7aa5c0ff6a95732e585f4bf504444442415c229db11a5a54846a18082f7102a532a666ea4be9fc57e75c3ee77e87f5c53c0ce7861ffbf484727df233abddce87ab8adb127df02a607a6de4e750ef359a7470d1de6e824fe74be0ef4ffd2fb5f3afe8b69ef8712da0d1ec79a684c386452591ec701bfddff9737cc78510f2fe088bc50accad032adaca352bb2d88feab2f6dc8b574cc90872eed85bd3898fc33e5701238d882c992934c85602a0c10ead056c53f0916779cbb160f01df73166c16710921ff5819f228fba10852e9440e5dde56f9819082b8661007b0105763b4a721c879184cb86c0a79f68942229fd81b151e56967936df12a9ad41da8188e9bbf9913e04453b0a4ed85afdef3389e1f4ba730b291903e44b8b959ddd03a84d5b3701cd08cf0c80eb161acd5021f999a2ff4b5b0933e81ca0c986af37b1c50716ee8c5c6fb922f97ff9d49e9b73b21f5593a169f81fc98fa398c1b374f7f005b17021da127a3ea623e2267db6b221b6f0008ea7a0479a0069995f1557e46a0d9cad9debbd8cea7fbdf7eba97093f51ba41c7335a9cefab0f58441b960a6d580cfc40f1baf6213738b4d09ca2adfbe9b92f75f35ff17e322fe10c64081b81a2141b47a6bf78449994e2fdd4d3edd9db94f8549ef4d8705f894ea9f0db983b29b1e23a39e56d9519041b175b9f0a6a6c6dbd21fa260d1c0eb0925df74792f9cf4c3be92fb40e80100633bd832222f9cdff28d7e47735b4e2e4ae5feaf19a589d567f4871a91a5657dca7c2fce78e95109befed7bdb913b72b3fe89d8a17efa808a21d3eb99ac8a53efbeecabc5ddacd8f3d295b6f5744572a267160a2ffca30000000e66080ca0a0080098b1280d046dd058085ff62ffd597553bb96ae2e9c067468b8f6725c46b20efacf119c527b325b68fc3c8c30e3e45088ad45332bda8dc37d84a0fa4e9b49afd4070a42a35da8efde2ed7bf2682d114e9fdc382517fedb1b321e71831d2c2ce551d762b471bf59e0b7f8a5f7cdac51ffcd949110b0d57c368ef5adc99979b19c1ab4de4da31cf2fff2df7f65ecbd7c93d442a2113a29be2a0f68dafbed18c3b5194a8fe31a6259b4e083ec79b88449af30ffc4c5b7a794216bac85a8a67cca0e6e031529b5b6044aed7fedfe7fede5638f27acf65aa958fb2c5f6ce91be60cdd649e2a395bfd716a050195cf17e980d4991761736301ae34ac8aa9d52e84a67c83169fc03886ef3bfb2aecff314f85fe27d7ee9a54dcdbe23bf43629dfb8d24083fe6b1e7368ff8fbdb780caba5bf745e99212490129e9ee069194922ee9461a040444ba04444a69e94e0109e996924e49e9960611ee386bef7307efd9737dbe7bfbad75d73deb7bc7703898cf3f663cf97b9e39ff27312e2a686577dc063c438eebc265cec6cd417d06bd778d9b6cc221d3c9af96144f64f5cd476ef7f1596fd8b3ab5546f80b8744b80adec26c59ec139e9f6ca92d6d5f4f2c0ee00d2afb592a19f0cc7faa0d578364015a3d091a07884f03b32276c2b9275923e90314a9679573d1a83e0b3bb3db9db21f7651c4fa399afb359bcc34e0c31d80e4c3a88ca3acc6e84a997746858a7c77eb81de3426b916ccfdb714d32a68f35fd36d33305e19349fe14efa743ee330b3d5c695df3b375ebf388b364ba83f5bef3ff0e877cee21a37ea9609a63070895f1b1c01e91c68fd03908d0ffa64821b41302ab9a31abf475380d8e43afb44dbaf88695984d35248ab51feeb8377face054a47daa0b504b5816406a40f406d7fa7cf50e9884f7a57777c388576e559b626c8997ce628358a8f403a02b44672488174c4758c995f9bd104a736f276d9ddc63040638b0d377910644e3cfda61fefbe5dd6557c19375b46e4b3e8393f36eaca2f05e523f25a6f45198ee7a62ff8df6610b37025f9053eaa49712425b3f19630596810e63b54c17b49d96d507c3dff8242d379a6d2772d7a251f46a8a5bb49dd2a0d8beba39e4b19014897641f9b99a1bcf991eea7b87070ef5393391a4102976acfd7beb726676af47ab78bd160a53fe38a84c828881d30e960ea3e43454958c9fba1ed11f2b0623d65daee708ae8737d48e098066a42a62902fbfa6a3903f1da83da94009d739ff8b73aa7468dd613578ee22c9bb133b95f4928b7f55c0c7d3a9261600f40f23102e30bdfb665a48aee6ffd5aae91244f5972d2045a9fd57a60bf25116187e5e849cfb84968b6cc5b6d8dc4a770932211e29c089f7215bfb7d346f16b113a3e04cdcbd2a52faf06ca684f6e42d7a707fdafc20fac3d9140fcecadeb2a5f8f7b6e9e14e2ff22fd117a0f411b3e326ccd1c7a9cdab0c4b71f367e960b7dec8e5c1a4da0eb407df6896580313f65b93e7e837e864ea53742fda85a2f4315b378e6ed4cbda075b5c5d0386571db44431be85e104f9ef65dec5af5bfba2560dc9a766810d7f752cbd71fe4c7827cefe2e7e6b4b8fd300f2b3be1f216233065a9bfb84f807ceade3bead728857c9f060c6f93117fa80e68a49e5887d60706d900d0bdcc4ccd5ba8fae2e48ae8282f9167ceb551cd50f955d057a6e0f3863f4590e3dc09443abb326fc6f6f73d236daab44e8c57ad3f9d16bf7597b4e9fe57432ebc8ff949eff082a39de0097ad273a3345fd48d4f0c981cc2e2e6fa06afa2d46151aa8f1a34086b9e54121a7c797510d5fc2ddabbb1d041aaf224af671a1b26b9410f79167dbbd3c8e83266e2330c463ad06e01c606f2c741be3c486640d7119d306ef768d43f0f91f5593c54b122da38ade937d6471c9c9cd1535539bde39e8cea9f36d245b103b279a9945bdb015dcbed0db6b26bd7f60ae2b6b6fe6f40ba1d5abd5b4a68fac01f8fe4d83b4abeaf55e8617a2813c56bb9d85ba50bac052e9eb50ccc0d14440ff6ac4db9407c0ad22fcc7e73054496eb7d281edbea242cf53fb908c9c2417c0a9241506c00f29f41e308b7a73bf89ed9ed8d1513f676d8857ccae55ef71488fffef676b7bbee8397de82de4b0e5b12f107b7f876f6d34618755f53d129e47921fb18679e3ae582c696c27f69ec619bf23edc9f043fe109799c4c1f172c282eb314927183c3c68bb06616d4d03dd2594cc71479200bafed834b442feefe85e495a3ef61ae36ad8337681ca058085abf04da3650cc045a5f902f0ab2b5428f8caf8db5be5e960e0bf83ee4ad1edf72243701c5a6a0f19afab2e2e09cd01a5aa639d94bc5e59878652e9f8164a6cdf46165960a4bf09d847e478f8649e9c2db5721a0b59443a0b9da6f8e28118918c0f46cc16f1b3c4690f90f96639cf9dec4e11ecf03bf4d9bdc391fab2be123129df94897a3310e5b258745327bca1463824777bb50ad3fe3196228df04060efcb1011fb4be947974ac3fcb744379b5ad4eafa66108bc59f1ec34681cdfd81a5b860d0a4db27d2a5aee52770da57467736e69bcebbccb12e1cff8eafbdbd7c1f7c71a1fab551dd23d22953176c0629df254aee5684934de30ff79a8693354d614474b0c5b44e995182dabd9f0201be41f80ec87cec2f276e3d0f55954a5f85839f6737b21f1fc6ad09a836203906f019a53906d04e9d87bc20f8d46727ff0886cab6c3c8cbd155188a411c3a0abd6494d89fe01df26763686be912c38f09d17885f40eb01f2af40fe9af8fd6e615c770a7c9c37813fe145834df1df55cbff9df8f2bff0d5926bc36c5686ed7024fdaba66656ca4c8697f8fea07961c8a35c323deb63cfa86d65e55f51c711db50f8c1f8307e7b1daed2fc3197026f28f779b4c3f9713748fffdad4d23f6c54b47ffb31e3963fef082f95b4eca578d8565f73ecfa96eafe5db93c49185d80e83f8ea6f7d9649091dfec039c2ab8ba0f713f19901c87e80fc4ed0f36cf2b911560ce2dbbe2a3658075c7ac7b3d6489081649f9925cdca7366e7db3e9eea70895c04ba8cb5b91d685efede3c03e7e0ff6803f50f88a54169f352a479b60d1df51d4bd7e89d4f43f47a5befe52781aeab4c56b4847df68dbd17cdfbb050abf4b373844118489f36ef8cbea173cf5d7f22373cbf1433b3ade01fce0cad2e06dd0bedfc817c29d075eb537db01a09064b6e94fdd356fc3ddf91ecdfb195c185edb18e23a67dd161d77932e5127da1f1c31af3db4f7852c986c1f53dad3165452a9ce35c592c100f81fa577c8fb6b4966392fb3a9ebe015f55519f49d67e87f25d49031f6f70bae12cc67b47255f19d79887dfa0f5c76be1350f6a6c9bc692b9e128f41e189e929ef56082d61ce46783e60014939061a952774e644c4e087123380d671bc5348a8682623ff2f01a9747cf6e2956908b0f4adfc51319ea904001f9d9d0c633d0c6b520cc08dab8767236318ca12764355caaf40d0a750a66b39c9b32e65c454d140af7adf70b460a553fcf5a0b0e9c9840b6e2fb6565ef98a7c5b8ff3b06ed8fb9e54beb4db4ab20dd0ee283f0fb4a1bbe65c8826a5d2d2cfbde73570767bcbca05803c457209ded617855e8696c81fa7469fe9d08f35a7d5e4488df53be612f180a7906545f0b1c42e371c41feccf3841e3f0836d6696fd4021e1b8de566cead25dd6ff9e19e9ae3e1c8f05fe655ecc29a52321864f4fc68b1123105605f223a0d52fa0780184a9827c1a908e058d1764f7eb48e2a818dc9fab783deaa1a08d19c3e0389811d9eb5bec4afdd490df5d2e2dc0755c76648e1bb170babb957dc1fbe124a80863906c15db8e9f41bd756cab63e5309c393caa9d9d321d91e305199c61e5dc77c72b992fb078e593b742bebe2ed426b0dfd5cc9ba5fef02e58c5ebc09f82e47e9153b9a352cedb16ecb58b7c0d9fae04debea2cca19eea378c4bfa205ffe87a2ccedaebcdb0bf1d91a0dee1fe5c92bdd97d93756c8298e16493727cd0971d272ba8d3dea18d440720ef25941f81f48ce41eb9bd6c7396b2f4334ea4c69ace9ce704585d9d6e9fd4949576de25eaa76728df893c7e8bbcd065d240da0f863aea9ac9eb9cbd9c9d381218243ef3c35aede13e8ab80d617880f41a937d0e912e50a5e9d27bbe5f7f72db47e4a66299543326b1c12e865898215d28f5daacbe934c51f984003e120205f3e67587fab24398044a218f633819946562b618211d9d1863cf945c0bae0d39928c6363b73dfb31f9c203902d93290fc82fc1268c70b6a03f1c6efdcfb673f2f3206ee8017a3f89be75ec50a7729bbdb4313920d68632150dc087a8793cffc9245bc3f4eeb8b80219c23a550318c4d5c906c81627bdfa6579567541161d7d446ba4348b68862c32e6ba0b181f29220be028d0de4f383f22920f9056105d0fa52a0b97a8231a02c27bb93c0ab3cfcc5d9bd5aef3c081ea79eb8c243856586a40059a0b562aeae5f3f749214a4c3d286d91d459ff394b5f8bb3fe15d37633439b0f4eeb9ddb0a0ed566172db022ec4b8918180dfb5e7364f5777119d542c86639b77810457b41686d0a47ffa59e0f77bc66a308ea47c3671b852d5145e42caa09c034866407635202d51fc05bf629e8dd0ca2b5af932279b9e9c3ba075abef9f5aadf75e5d49aacf999456773a186c0fa601612320fe7ba131628695d89fc02890c3dd2ccb4ea2564c230fad9d065d078ae99639f0d007a25a0442bcbdfcada8c6b43d665fb3f795e15ea5787997ad600aacef37517fb0337bdbf9b795671d2ba9081cb95fe57399dbcd4795068e1714bb60e032efc39c95c0e8d0a0609b1963908bcf31bb35f9d02ee93510243dba17617ae81f5fc0706b192f887952434df2e593635c57b62b9def9ef60e1c6aa8143d839be839fe6bab4a07fb6dda8d20fb0bf259f9759cc9f028b4ec555787ae5751064ad00dd6fa41f8692562ef5a4852cb33a59ac7b95accc1dfaec4160788666bd78b8b88af079d90632a6a3e6c87145f7c01f9b1b37309db4dcc69699533dd49cdf509295178318691076cfcadb2859fd104c57ae819cbe173bbfc564076461c9d31aff489e9ae606832fa4294d5566274b20b176f8e51ed33ac436e373b91c6ba07394ded4f7440f17446526aeb5949afedcc36c7530d24c778ee284f555e8ab2454ad7f09df98ab96eec55799edb6e6734201f1894cf03b5c162cabdae718fea4ff9863f6bf4806a28d3f243c40f9222c20a14bc6d4d0f539ab6bcf633c127b6f7417c4af465407a472d2f08d734a2ed7ba2b60faa03f22cb47615e453b35a070827bd3699e6b02df5e4e7d1fc28c74978067a1e0843fe1ddf16a43b41ba29cdb8e455b2feed82349a4b1a87b271ddbe303a2d10065565ec86542df57ca53f2b61d78a2d0386c7c4fae477726deee7516edd0d811eee5c1cdeb4010f1f90ee88da82daa08d49407e18c8b700c69c807a0b90be07e5d0a0ada7e97da5bb40bd15a38de4f01d274876be5229e8d81e681b01738a921c578adf3f17d2ba57345b22c86a7f11d37905ac6770dd238d77e3650fed5474b0c11c15dbee589aaeeb342a0b50f8515cdcf939c0e271a9a2e9fd44fbde517db70aab8d2161fb6f1bc2d45a7ba5a7e59620de85364604c5833f3e3e20ebb1e3bbe2b576da3b577e53d83e1dc66959ad56bd733c7170c54e1f16a96b4e45172e15ddf116e67220366ded92e59e77cdfd73d1f7612f0c407532209f1f5ab904f1c6e6fd8edc684f75393364b9855002af17b2912cd5205d62cde7b4163443f8821dd77d4718ef4e86c437624bc16749ab3d9c072fe978efbc9eb33aebec108d4606d501c40b142b9ca14d765984bd2c6da53814217fbae9fd3bfe69fa4cf06b23dedeaac90a9718fb0065143c91fbe520df478912e934103955ff71babd7d7d007e11d95e601c28c707caf9ffa75f64f91d3f2f365e91b91c47a422eeb1edcf9f0a96276134d16bef5363ddedbb61351d4072048ae3a1cd65816221900eb32836d9132bf5a4f89857238a9eecbe225a61b10aad0fc7ad1050651958b0dd4fde378cb6089bd28b6be609d29d20cc17188b037c10d01c18c9be2e6f21bf1fd4fc41ba5240aa06fed3f7e84810be066a03d950d03b1433059f543c6179ad621363d5f551b087c214fdb6a75ba65c98eb47bb77d925b0c588948863bd4b9a205f1eb446d0e6a2cdb3e7c4d7d7e244ef163785c336deabf1c59ae700d9f334c7d7af3e1ebe27baf58369d322429815494a8510c4bb20ff1ee49b2dba3ecc3eea6c798de93f9e94f67806012ef2f414dadc13b4396650bc0f6dbc056a03d95fd03c0ff590297c32d5c43e1ecb8ff2c80fc872b97f94089a03909d06c92528e7009a7b103e096a0361782039527dbcd2e5ab2c515fc2e3faf66b1a1f23fb569b1c28f7096d6c0a9a2bd03c07c34c6f7397469e85e337ec8b053feb157649a82c9a925c3b5e0b25f3385ba6e871efd0256873a8b6ac90d2e2fce1948bd97f14b8b0dcd51064762909aab983563f836415b4e6d0d6d281785262284dff644556a84207f71d9b6086d37703c99d8a108b7b932d160eca9cadda06a40c68c5b987cfa0cd87160b5ad4e7c5e85661770f858d1adc8a3d967d1b09b2fb42139954291f74c7bd1bbd780287171de275b27c40ef60e7e8d360e2ff9158b7795b8c28189e4459a25b0f5afd0cc2144073206e359424724dc7c1d96563c7cc191912c28273098a6bfd4feecb4dba1adf8e783b9533fa1875fe1ab702585f028ac1a0ad3307b5adc505f419086d734ed77587368e522e865a77b98278f77c5c416d8465a961a0a029075d5efc35a504ed20b473d5f68dcc37238127806e489dae10e79bdb9a32da0e45aa161ead728043c4fa6b71e12c0993732c372f50ec07d207ccd12e652924bb16546ca74c58bb2b68048f8880fa6f664caca617ed9823a7004142af72d1e2b99b1f02504f02740468aebc8ac995661d16aad85d5e8bd379e8b5be18775a035dd730342a7e3b50edaa5cf981396566555002e3a311507c04c2784075c3a0f500e95390fc5e8a16d8b40a1818b17e6c395fb6637caa6f986f1027f3c9484ddda2982dcf5b36e0d67b49a27d56e0de87c8c22d9eb8c541caec2ced0c97eaddbb5fb624de1b88a0bcdbb1e291bf28a8dacc796b3e6eb97df8e6ca0c793f164fb7b0f2b9047cff7379e67d31cad9dc2ac2a49e807e368540d5772f5e1889d3bb602480703390cea69e257ad45f4366d01622965778f8b07d4f66e627689ee196295f0ec3bbcfd02728e3a3d62c7d8874de5f00c53820dc0794d304611e20db03e271d0d8407100b4f829c82e40ab77413203eddc837c5b903de70f51e68da9fff61ed60249f6bd779b9177297721884f41f5ed72aaed7daaad1d19c3dd362126740c7c8747f108d0d6cd81f6162c33f159e1c42d3dfa86dbf9f3e49c70adb0a08cf1961163aca7f8717c9ae89da9ef3b6b989eaf307341f307ca6982780384df83fc8821638e6243c2304be4a754ba66e3d1a8e885e9ea203c0c5a9febc1ab81c363b6ef5e0294c9eda2979f9e8b776c2c82f814da5c07a8760bb8cf01800bc06b8b3e3274e431d8095597ebb5172c4312e29b01f9b1d0da9e8ebbb82e16f9896cde9ee96cb973eb1cfe385db741f201baeecf6ecbab9c08e4a5209edaa8cf93786569c8497b6af119da3d26afce15c516063745c763611e85530c4ac5bd552207d58281e6006367135d48516a5f0e55b1b570de486145c0741ae47381e23c10bf80de91b882d43f4b3bb7486b3074654ebbf2d58cdac108c4bb66ae56b60822fa0bde308ef75ee67f87dd0baae074959f7a33a129ab00cb78d87f6bbe7c88f091f8f4efe4a3a09d536798dbfd5f2f077fae473d2238adce40657f2cfca3c926719d8b12dfdb5e868e933576b23bafa63a168483807c7410360cf23141fabe2a8ebe723e4c909cf7e950ac20a70c03c9bcd0c3e59d0f6ecba4dd8965aee9fb8f8e32f77261ead77f072f01cd4100d12942bf19f952db9ef4cbf3a64cbcd4f4a2dc65fe3b31e44f8ccdb45494d6d268b00530eee14780f64880b07f7f952e29ffb709e5972664b3cd6d6918b25d7709a0f5e1a0adad85767da16d03e19d7fb3473606b0976c7844097c9eb589ef65924036aa1393b31cf7cebd9431b77aebad836489eeb62098291b1172da377612483fcc876beeb44a527d2e75532cedf6509094ea7dfa707dab599a7701a52bf72b688d24b7161b86c685e2ae72c59c31169d85bf1c58bf01e96268f79682f6c580e61ee4f383e40d74afc2666a54b8716836d3a8554fa78a93a7a718d33028c7f73b7c7a5702d95009478bfa0da6b38d125960b2d29553b8aed48e1dff980829a3f96cfab33344cbb12b8dbd0bbf0774c7fb83bd9bfcfb6d2cfe2c1ccfd3c65e80780814cf2424449954151edc73b673bdfc84445a819fe0223d28852049c006cb92fdb92f2e42b945c70aafe93e2897fa3b72f94f69f3596d788fb72f01aba38c4fda5afac18af8720a749d4ce02c3df2f3f34f7a93593f60b6eb2b1dee530c94113e7da465a48d7c4732a3a810750205e116870eb4b205adec83629752b2b305d97c2f263c83ba78fa5e3c0aaba6c99f209b02d21ba0384f7f46765c7e01c148f55b4ed76382af8f3b11b622d85d7a2a8da4351bd442285f1b064e5318c19e51b9e83734b276179d3a289db13f1dc2e841af76defb1e7f864abce087c1155688486a60b59fa2da79e892f4f23d6bfee343cca15bcf9d474e1ad953c46098f20c69b4bec00bdb852e6b2eeda9ba20d9e992b8513a88b3b16532d60c5cd49d174c7290edeec583e6aa84308c446053beb5984cb7513d72b8f9eaa76112e83a90ef086d6d14e83a10060ab265a03668ed7458445f5bdda97e99eae33be20732acd518f118baef23a5dc8efc6a3314a8ef7a294b27a210188dee7c79f6d0812df18538364cc6f310dc97cee7bebc922b5fd8ef3d10ba680fc2924a0b5dc53520cb9e2a05bdb7292a77542eb9692d9571835d7dee45525ab6092368bca09809a4db998b05eeeca19b985362c54769c2a42add73e73b8316ab02f50f84e7b4b75bd0c19107c3ca287d132b21c5b49ccfd7865193b91f2aa77ef581d414d92c6c90e6eedd0c8e4f6fa416429f3e7d85a1f82a8e2be267e1e4c3d954944b0fbe67a23c9482eca6af7a59a2c9f4af6cc35c23aae065b37c556b9dc512fdf02e49e4248fdda941f116a8160ce4eb418bcb43dbf63b3aacea42e0871d7eb387de70bacd49e5e8b6e293c0cddfe98b3a5cb04f943fdb4f984c59a3a9dcbd9123a13c49101f401b47415b370cadee04d5f982741da86e1dda3a694a430b98f7d32bbcaac57c7c6faf65aa974b0a32879a1118635a1d095d455e38904cd1328c5ea575d044c5384fde6682eb277b825aed104b5def5e13ac92b99e192fa8f5aece2af29bf6e6e2fd5456f37b9ddbf71de3983bac9f52ba3d19b8cc544a432a02d6bc577c49b7eb5addebcfcc709b1bf6ccf6cd9b4ada00f50f94478436aff1962342ee9e8af1bca21aa375ad654b4396200f3f92dce9edd7b95d383a1673a74f4f765fa4e9f099a848d29d7e56ffc444f146d86e28b6d2ef18de5d64369727d96167f04113e7acad38e99bd1c8a2b266829ccaae81404133d8d2130cd18bfa1afebce1dd26bb244b6dd77b3f9fddf9107161ec656d716f3d73249ff0156599c18fb6ab917b5fe79b13680da78a41b9b185c6f6239d3dd381cf5f6b6131e4f67edcd9c03507e962a0ce01d48c81f854e93ae2ea4489f13b06c66a2f6d57ab7bd4e4d13b601cf5036152f3259a894038120d67d5698ffdaecc28b47249753bbf987caa9a7ea7a4e35444f033feeb3a49761466ca92e8657abf922cb2db553ecf92263d2b807180b50029e1dbf8272f7124a2d82b5c98b60f9fe8cd435b935f811f7bcbd2ab20989f427369fbaaa576d1efa130b0061cf0bc3f5baffdd5f657dbff6a03d50b80e40d24d3204c01749dba9ba2437b0686eff1d385a84f029c942ded39f8d0c65b6a2911cf92de88859b862d33549761880e37b0dc05e526c28369692da8aba76fffcc5932b566be2fb352d1016a43a2c30d137a5855c2e2b048d8700f262ea9f35900686f15c86f07e92b10be0b1a1b280683b63e0754ff023c87e0e101ae94b7a9f76af0cf44c6c3bad90be76766a038995e67aa4f60df9371f169c5bc6cf556b97e369d88d4bb902bbbd4b7be9d5b874dcfd77ed25cc2aa4981305a509c92d4daee1b7acd6716ac2f1b58ba656b9d27e39c046dfe0d84198172f4a0be80307d90ee0462af803905dd2bff51f9c9e847c2c7bb47b745e73dfc39125694ad413e17687d413122d0671d0dfa7ee1fa13872ebf9bb043a5339ff54e802a0807f15ccf329fc62c39c0269bcb99ef1edce0ced95c02c54cf93af68f2adda60a9df6babfc34e4a37b95e506a836221108f83fae73a028f4cb7c2bc1522336624503717224152af0df2ebfed2a77fb5fd77da40fe15c806407bcecedfe4923a18dd6efc63d73259d9f36a337bab4021ab09b7863e0ceb06de537b25537292d91faf7e76b1d3ed108ba82a3b2bc31d3345c93e946ab604f13328ee06e50340fb11e3044edf4fc43a5c23ebbd6b64ddf51bf14bbd25fb676350a0fa5810c60d8aa7df886aeddada08316598d2769f139c94286bd196836c37b41837a8a61274ce1db4f80bc88682defb971c81e508dafa0390be07e175203ec87977d6365bf1de64c881694b7ba0f411abf2f75568cf11f96bddfefc3668f50b688da0cdcf80da900290edbe69ab50049128f5d7f69a3f606921a905f95220fd02c24640397a505d10b0d61eca3a946ba615f468c727628e0f45240e6da3f1909ef1cd819e073a430d14f780ee05ed197d614c1182466aac647d1765baa2c94ae751d12c25686f3db4b81e28070eaaedf9b37340d062a5ff4af23177a25413f28d574400cd9ac977f5b8c77767d513745622a84e01144781ea7340fa94b5936083339bf6cefd174b9a6c2756dae4158d57a0da993cee133d6929b3e4f444549eefdb4eeb4793162bfff279ba3f995fa03d4b4ae113b35ba0d983671d423a51ca3423bc01520d4620dd04c29541b129b47e4e462bfcc4def31d1f5f8cbc070fde9cd38cbf988cfeb3c7fbcf90b77f253ef8b3fbfccfd04dff4a730fb2dda01a11d0be2c109606b2dd202c0364a380e7ea02ce8f009ecd02b06539fd8a1f1e5a1765b931ca04bcecac11e499713c81367f09da1f056d8e19e447ec10c79633d624b4128970e69717e3f3e65b5a61411bab958dc16df0ed18281cacfa5ed989bd3f78cb3ece089a6768e350d0de87df39af00d456b7ee1841162a5a23bbb922ebf9fcc80fe10c15a82741b52ed0d6cc82d61cd446da50c7bb1bda20b49f9dbcf4ec91f0ed1e6b84c8dfd9ebf167b781ce4707b5fd2be9d37f46dbff1ffbfcefd606c281a13dff1494ff00f9c0207bf4afee836c2eab6c1d6c35b848e6da6a0e71b555ec5c60bd84765f1bb4e79780e200686b35406da0f782ce8802e58f40f5eda0fdd3a03668ebdb41df1e00d94150fd2e083f00ed1183f65c6268db8e235a76c7c6184e24691448287654b394db3380fb8effd5e51cc41bff5ff50574560ec88f05f129681cd09e2f0abaee6bc6464900d544d9b982f7fbe654ab579287ddca91247a0611861c84b673492f7e3adad64aac1ebd9140203790d2f53811ac7b12716826a46158aeb75fc40e4371bae6ca5fe5a6be872bf795ac9219591059a8286ec6609118466390db9b887f972c4c357db0aaad826b63702c1b35257bcb9b55f232a538fe2fdb036e03e958908e006173d0ee9982f6ec63107608aab587763f2c28e70f6d4e04941707e953500e08b4a74bdf2731b0809021816267084dcce4d9938a6fc601a03650eefdda5990815b3377c7fc1d8c47d510b1fbcfa16a6bd01c80cec501ed7f9b6acae1fb26a23d33fbc5dc6bd157d02ef1185e0ab4966bad8a8fd6a504de3a995e7cd4165c4ae3adde0fadec963e55f5f777c74c478eaf30350b63363b1d079d97f167b78178c3e8f19c869868abeb9b75ccbae718a2841dcaf8c7862a1dcd2e14acb04281d6d7b05a4e518a238f184078e2dfddfb0f857f003c0703c057a07140fb6d2cd0fe05d4b8b7058c9331936291a9a6c9c1bab94293e621207c0dc47f204c61ac47f26879dc6853c4e03a4f00ed2e7261a4943c88ff407300c214285f65eb5a99ed27d40659e9b7488a9097d21ff0433b2f206c04841f80f60f82f66680f6b581f044506c0fdc87fb1bbc0b3ae31c641b413536d0e612a0c55441b52ea033c441fbed4175a5d09e277dae3175416466fb5a85e8a77d5e9d4d793caa18dfef7c6700c4a7a05c11488f4fdd97dd09a54d1e97488c9fa4c47cebe49b4aebf03b318eafbdd3fdfb95c9df6975bb6df303b01a748c2c36407300d24dd0ea0368f71680cebc88335ca72de5f9acc0d026c05fbf78f5dc043b6406743e3aa80e19b4870d544706b2a1a03384fef2b9a0cf2783d617886302640184c782cea484b67f77a4523651d4dbeda77591de5f616bc8d3b5ee8783f6aa82e403dab36840b53da07801daba7f90ec83da40e300e1f7a05a0d68cfe301e9713f6d99f761fb02071de82433db158fdeb631eff8be902da00bafa4d5158e0d28081ae59cde60205187367686f64c0ed01a399aaabe4b3978ae4dde9cee5fca8bd519a59c3b06bc1780179f7fa9c961555a449b5d29080993f802db11e7a00ef2a586cd1d9c323231c3292e68ada51f0f4a171aec9f82ea0fa0ad6700cd33683d40be32c8ee43abef3fb39b72971cbab2520f0f66defb6cb0bceaee1308f23141fb3540be23a80d54cf00e2a127b04a64aa6d17f6bdb0c285a2ceab4432bb68f8b748f6e40b957169ca608fd86a0661c6f2ba0506a1cd5780fc49e037aa00bc06b215203e00ad07c89f2c8968cf150f81dd56fc7a0b5ebfafefd0c75bbe05a423803522005f0524bf72e59ced43850fb59e1132af31b5493c0b9c283100c592d0f2c6bd80564621f98929fc7c7a93be81cec1ba2b86e644f72f75363d17af075a09ee6edc6a8bf40f108c46f4191d26aa26ad7d794210f06e50fb2bfbc10a1eb467bc81cea102c5a6d07ec303c45720df1bda7325a1f555a0fd7e23484740fb3d5790ed59bdea71957391c171756a091d81df8ab311cc7402cdc1ecb350b7baaae734d4a5b8e6471dfb2cb5ac03ecc0fd7e8847361c41bc8c41b3c9f0ab9e4eeb4f5cd20841cf03e953502d27c89681780374160d687da13dfb0e246f0e0332308f0a53466f7149b6e4c6b3978d638812a1c692db5e45ac6e5b915f2bb82561c42121fe50f5f03d380bc3fc86b910fde2bdc5fd60cb64db8981356c33cfad99098386a2342fcdd6ec1204147564683146d0fe5ad05aae60e55774d8ffc04b2ebbcc21d34d71c1f4a5eb06cda9e996be7904f196ecc8f1daf7476afb9d346c9e567454e3da2aeff24ea2717b5e6c19570a166f485e30be7be04ddd2eed5c2b58cf28d0ad94cb3c5af810e74893fd09cbddf7519c1c8ccf778f29251be73091c2ddde20d75546ebb78f92274d1538b832e200bf7b03c2ef4175325c7e634c0d530143e1d7b463c801156fc48e05faa0ddeb068c49007809b47e04287f0e6d1c0a7a1ef01b5000dc6c9690718c9d594a2dfcc5ada7031306affb2cbd36e0ac6d0c5afa99f5dc8b994aefb4b73adee3369602bd03c4bba0fd83a0fd151e76b0214831a802757d470f2f1ba716c40db0d940f206fad62a085b07f138a83e1b84d380fc080af507df048d48e7c312068a15ae0247a515f2e47ee7bb73201c04849582ce5a03c588f46149710ebe2ad75696053f4392cd8236f0df9382fc0890ce81f6bb41a0353762afba584ccd3a9f32dd6c7ef05a02e5a3096fd52bb99fa7793adab8e7435acf52fabf4bf0f453a3816ada403e0da866f101b758237ca827dbac600fd987aa8cec34cb0224902cfcce5e3268cf6700d95fd01942a0fd47d09e510b6dffa0b5bfd09e1907edf7997e671f1f88ffa03db315740636483f03bf45079019d03e4890bf9697a5e6b9b4bd3abb341b55995d7c258b2048d105ba0ec4e320bf09f41d0ed039f4a0f3e541dfe7fc29a09e78f458909061ca598c5c49e35055fc2bfc56588977df67383f2b02ef8faf6d75e3c2b3133be4bd1b781639abc5875eb37b857421a28e470f7080f805343610bf806c0a513997945e540d1b3c6b9d6a71dc8a4baab961d5dda7426e19e86751b9cc54d7c9b893f8d1bb9d3620de00d90a68717e90ad05b581706a505f7e47b76b840d3245ea47529209e2c70c2c3fff81ef7a0907fa5e3ee83b99a0b807a4eb40e7b98178c3d62c4b6efccb62de8775fccf84b3fe3965f211bbff8c36500de473f8f34601f1302203be49e199282d973a45d74868b16b68f76883ce6c00f9f22ac2619631c383038c326b6f25380688323ac50e407c00b2bfd0e6c6403a4c543d8abff5d2f35db1fd46e5d0a9f2a363e51942100f81e40db4e620ac19daba2568e71e741635c8e681e6efefe63afe0f3c0254db08d24de97186e1544819f64e5eca0963997e594afe28d3a0b902b53db8be8573f6e4fbc4f7967ab3b6b8071e3d4e63fd201d0bb26fa0ebaca4101d3e8ba6ac2cedd4b3a93461e318a0673083eabe40df2201cd3de8dbf3b0f3f61f335e858e37650b328a929bca3689b93ecfe8db5e3cef9c212cec1740e788a272641bebb7471e59ec4ade2218d198a05485f5df68b6bc7a42033ad3138449435b970bf28b41e300d57d817c4c10664eb5c45dc26b8f92dfba3af82aec2292e00e523a0edc58b0ec80f09db34b980037ee58984038b55bcb303ecd7067aa3e01b7b65a61026fcd8f8bc0d448e423267fbaf9d2d3f6d9e9337575f8e28ad507e668fc22f507e722307ff0f36fde7df6eba72e256bcb5331cc97507aea1dbbe88f26efbeaac4fea3a7d27cda7ed37c0e17e113808435c0540b17f0827f880de68f7f0ffe980c6bf88bfb57616060b0fff3fffffdef7ffffe57fbf8cd8b89bef2959418c3fb88a393a3d92560e81ccbc012a85a84d24c21df4662207ef3b3312cc183d726ecf6fc7323c3598cdda4337e2d0c18c445bdf853c7e5a6dbaa81f4287a831b1b99dfd771e2e211e1bc69d929773fd1903ab33c204922ef481ae5e374cafeb959182b39ecf404e7636a62edf995804cba41be16f1a4400fccf48a059e7861475d855acf97a65a110dac5bd1c3452f6dceee98e1dc21e3a14c1071c7930b99f1f726f57d2618be827b9b99d2db4047d0a5504e0a9e8618e67ffadb81ff5f038081a140bcd9aa4670659deb88545fb577dbf9e71bec774d28075c30303068505cf3473ff2fff88f02b128cd3f82db6bb54528013dc6f0235b289eb2b7222c0c2cccef31030cf96b53ebe811e6056299fee235566eea6d718c57bd6173e4f1dfb08d8bd052557901f7c09d39f8f8e37f33f4d74dcdae31a5aa719abd4975ad257e512f70ab76800c7fd735d2b1c56662271d4293c26606e16d7b9169ce1b89afe5eef8cd7abc5efe7bbda3cfa0f57a314cdfd37c0ebbede34fd599550bbbccc2eb8bf79b122071f32fc3953519e3af291cb4de020e39e76108a7384f69fe3bf45ffd7436355de2ded7536bf60587d6139fa351a52446fdd7d3b60185aea01f535b0609a3cfdf7f1953cbf37e3643a4ffcef8ffabce2191fb23f2f5f5f535cc6ffd607d7e7505dc397cb98f3f9eb8a13f0332712d7cd6671f17fe5fdcf28b67c2fd42ebc1ffad63bf31a6e61b7f38c307be61fb6ce3d611447f18c3c58405031b5e8e8008830a8f8482800c0bf73f7ac1afb436da1f5229f2fafe985e52f7c7f4caf53fa6d746fc823ef09bf4c9dfa3d707ff31bdc1e017f38b70af0595ac8ffdc8d7a9e63ae42146bf0d2f9ac74b1c95a96fe29f894fb83195f075623fb448f118f0bd8b9a36aa1ea8d1c99c3893254154e054525cdaaebb3c9c401b167dccb8a3e506bb66831d032f1f04d3ae891b230bcb7681b6f2d352902155432fe0c37a3d9e686dfa0792693f54fdcacd3ef41dce12780cc92431f92f1ebfb25708fafbb33bce05dc82b9de0f9ff53368263f5eb69ee65154c145ce8d53467e61d9ac98cee3ad2644707b7c9884b3a454d9c2ee144d0cf32a0195766976625ef1fb12d78f94cf07cb081bd2259de85daa5eb3780977b3df0693fbb7b23fbde38c896a49b0b84197c0ccc9abfee6fb29cdd19974fbd75ea32f74027558ca01d7d1f9d999cd70e708cf83e16f874768389afadf8974f6a7226dae4530cd924f5efb3ddefea5c702f19bb0e942798c382687ae436f49ccaa93766b4dadecffbce6b2cc276bbc2d31466d3ab5b61e5b7ee28f9e676ec3c4f67bba0746ac2138e24eabc23291b9dc81a7fd832b96c74da84fce3f9e315d2df7b9be1747a0795c73fa73bee4b183d9bbcb914a25970c49fdf6c903ffb15947d62692522c3936e2df54b8d03919ffe3fb9b7feffe3f26ff7afee17ee9e4f0cda621c9078448ed5799cf207367aa6d5577d0e8f7472763a77461f3378eedfea27fe4bf377f5410f303b7e78e319120c0fd33e005a2c86c492c06a2fadbdfe12f28f853cc405d421c6d6c417ced987c0a16abe013b5cab37b78061a811fd4a398850dfa8c6e352485663ea98bd195fe583b6c1484a2ac707782febb35a10eb667b1fc30162bc9eff19f58ca4fcb282b3dabfef1db4544fecaef828a4d44859fc60e958a449f3cccc585dd832f7c90f8366e6b6b6c4086d3dfb7703cc76042bd4966c06cbe41acdedaf26aec57ef6f3e47eff3f1270f42ad456f97203b8fff4d7e37bdf947dcfb585f856033c169aaf3e11341ceab5ae368087d34dbbc29c650c8a7db8cade57da8be9f6ac71f0ee11fc58f77ee3a0e9316f82ecdc6d0c9bac2ea63cc4138d53d78e4eb776508f92acd66d650fb827bb0532d2f6fd2ef9cb41a8d49acc145be90a5e84da64f3e13672ab849cf47617de3a911302af3ccef32085919edf9fbaa909b744411388fc37b917ba2ada1645b7ef0f561c5cdde37e9562f9be10987325fef1b187c156bfe88f3ad9b94e8269dd53ec652f77042902689a9e13df6d587c94376889015bde629f91efa2db3ccd70afe9b4a1f82a52556bd2098d77141fef398d7b785f87e162979cfe7ba4c7bc837e975e3ce399395f39a5f92df08e30d50e1eaa9206fdea427e23c5b59e8691abe0cd235cae2b88b335586e07c93fe8a9ce8224d217d393d98220bde1b0bf9acc029eb26dd6ecdc3147e789b2509893639f30c26baf77042e7267d4e3f7d71cb284644b0a7fe2d6fa03f9d019a812844c8eebed1a66368f184c99b5ab4bcbeaefef92751c69b741ebb89c50c7521328adc67d394227c31ade335104041a34fe8f64997f158f919291cdbbdc4be332558087d957c7bbff969fb8989775ad11381d8366c19a3f8999bf4cf3104a286348ff930329fc3863ab18c12310c94dfa40f3b2b347fd1b3831facfaa9576f19903528fca5e8265df9d4262d408e7f8fabb11537446c45c0eabeebc79bf4c7305f6011e7abab2af468393fe34e97e31b6f41f44fdedf6cdbdd466000f7e484ddd6f3dbd5e8ac6be24dfa3b9ea54e94efdf82a4684eabcef8452d92afc9ad6fd289e1ce70ac7d91ba751bde6966a1b28fd2a75441d85b77fc9e6fa14f28de62a0ce3af6e667cbf85cb769dea4731f0b977ebdf7dc6c9a59c7678327006b2f8c19627e7a82053a9cd87a732ab45b7ae02b90fa9d08ee35dea4bfcfe87ac245457f45afc4f0a6128f50f9ad14d2fd9bf46cac4337264ff1f2f5fbe696f40454159c9f4e21e46f1f71f4c3570ea5d2d445be4a64e5e99c30f40188e77bd08bbc58b9370acf850b3340315dd333ac825b7b93de143ff5f3dd58fb709b8994c0c16c15715bef88d54dba64fa9ac10987148798d0531f1759c188ccd8560688fec35f6849c7869c1a9baf739c3f836d2941e982e03f0f9eaf48a7f479df92cca9f962ac79cbc3d8062162010fd3251941de17b49e7cdd54d88e6cfeb4acd210f15f2c077c656272388d94fe806fbb565afea1e1c84dc40766a4deaaa435e73d734d4bd7c32e55f24f06b09e3f6fd22d57da2c87670c5cd9646a8c33558517c616e06e41b8d6b4366f6e453ffef2b576b94377d3342adcb4920c62fe376534edbe713e807da7ae1aed67819a637bae7b937e9bb4f7c44ac02ace8d9a1ddbf5d6f7795dee4108f99faa330cc5a827213c71e7f6eb51fa1cb1a4f3d1f1265da687ab1d435c8950fb438373087f1e99bae6e0d94d7a58800b9dce59de9d7eecd7d1b38f1766e4f927852002abec2ff51accd9aa1ae67af719c91892c32a3e4344704b21d26a5f318b3f2465f8fb7d60cb897fcd371f7493be413d7530c0b32460adcebd23abcf726ff5f5ca6d0850a851e60e427cf8a7a6d2366fec57ef44ec8948e16fd2693713ab9c50759607aebd449c58703562eb10f321ecd34c494f4dd8f2b33ed6ae30f2c70a79837a0b10b18e71d6804c790fc6f7c7c776454b83301f1e4b638edea47f0c8ae772c98a0e7eb965deaa5f0b1fb80c4b0711eb3d4db52c65e3a2cd94f5a978f10c7fdee223626515c4fcfd6814ff72c11471c7e8c32afd1575c054af3f84fc8a68f4d493594b5df820d69d1dec36b95316e012dea47b21aa3c4b1cf98ac1d14e4efe78d365342d19ef18c258a3a4f435226cadca236e49aa2c231b456ebca5bf493fde33d949e5e2ed413879941c7da96e2c53ec5973939e11f365e3fc076d4218c3bd506f2c2dfbe0ddf40488f14d3910af6317ba918c60325133048bed977adeb9497729153a6c1a274f3736dea4f5bcc7733fc8f09bc34d7a04e9e320eff3777c3b56f0f859ee3be24b6e74e637e9039ec7b7f54643f1420c74df0637d7614aede3bdb9498fc27038376facf5e5253059ecbd83f2fe43d187a69bf497082faeb77cd8d24eaebfac1bb051e16afa15aadfa47330dba43faad7b9638253eb8847451df63941fbf4265d6b47ba358ac234256917666991155e7915f9551a04d04d3e5ddc80554a3e2ca84343b9a37bd65570ce72932efcacc2d6449bbf5142c611db2ecde30d2a499e2a847dc94523bc3bb8585dccf90413ad2158fe9e49931184ffd1e63c845c8c9d727d68ce6e3abc76c872670d02f4bc63401c539dedc6edb147e0e895fb96e6780b65fe26bdcb00c691afb1cb29d8dc83ef950ba55f0aee3308ff0e1669d6c3ef29c98e0009cbd866545a74bc6f0f047faf9a15d468a298274e9fd61f383ba26ad6c22a2e423cdf68cf6f88f09a3b8eab0afe6b4b963a3bb931847f17707e35691a905a9effa48673aae10a4f4d4b630f02df2b21f718beb738ed985df2ad32ab3557fba230ee26fde8fe9ca45cc4a7722c0ee35849864f76ef6dbcdfdda4afbc374ab42c8ea6f45b881189db4ce24f110d80909ff6830c0b7db358d92f65831cd2fbc617c832a310f1c246c0199570f2244ec4308ed4f9c1def637c7ade29b742177b2e527c66ff1d7098b95d7f8be2d6d730542c41385fd733bef6015cc7da413eff3851c34b3a2c857dea40b48b3522e9379afb1cfb07f1c7ffba3b0d82e1142fe3e76b3d0c9130738380cec9e47c5ba9314abc440e0a33f63f284ac964fe0185e1e9d94948e45333e5f80c03fe7542be29a1f627ea3b2ced9625608a0e51c647a0c01e60ab4c1920933868f297486c6674ff79de2725643f80f0c79f887b04af1700608bddf60edbc2a3a9be92062fdabf58d987ab1edab8c250389631477f98d915808f969a3b4ba7f476f2168b1f4633ece3d51ae616908fd6b6abbba2062e3d0c8f4126e798e477ead43c115c2bf1e11947ba3859dd07b7a45d511b16fe39422420da1ffa7e62d7699b1d4026e59eb479015770fdae77942d8dfaf9694056c5caf199e9d2d2a475aa2f62299fbde3d47380892e0917f5206e34fba9e558bb03c445539f97ba037c27f01512c694c1b30a7147d9290ea49738e2d85e2f24f444224a346881f3f10b84f6e9095443286174dd04c9a811e89715c528cf9f79ffd1f5d85701a8d74a671605614f3eeca061e987cff2cb7265cfc8b201ee6f7827cb8b45fd07f015220c0fc8bffe0ffc1cf47573132367627d734b2b1317bf63fb89fdbcac1dc59809555d956eda1a985b8d9731d1d69096e6b6d6b770b4e5e23710f453e7e7989a73c9a9a8a1e8e462eaa562a728e5c4fc5ffbcfeff2ec8f5e03741ae876ccf4db8f9d98d388df9f94db879d9f8f9b9cd79798cb88d79f8d88dcdcd78d8b8cdcd8d38ccb9cdcdb84db8d88d78cc8d8df9b84c39d8397839cc3838b8f8d9798d7e6bfc4866aeb602ec6cff30fe1063a829e8a8dca5725c4c604349447c1d94b42a952b574ddbcec0b69d122f9a7baebb935bcee31ac1128e7262482454211d305a503b8f751a35cf1d898ccb602fff2b10f31c563518514df894bef93fb41eac84ba127dce6faedaafa059583171b5478f150d54d59594e4b50d14c5d41e69481aa83d969354fca7701d845606d5ab41848a6fedbf3bb59e4edaafbb3b3b54e63da8f260fdfb02f49f537926e7e34f92465e2381aa9f5bf1ab975dffe2f737988dc4c6ec1f00b329c7d6b6532849907b8a6d05a3d1d010d3f71e438449560cd9304752f04aae0f1d6b5c7ae91c1a5b26256fd2dd266551bb3def45365d0d29539852981e72b89442c040198e3edfb2b107b47395f993d2d5ef3e1f1ce9ba499fe1b3777dfec201cff19a09b985d6792ecbd812c24dc43fd577f2bdbf961fd87ef9cabad72d3b58ce0122ed9db56675fb09e546d13be48393024901ef798acc4108f52a664c4392241a69edf4feee5505bc6ad2e7a1540818f7ac8418a66c8eae3e4aa701cb76246d44c54806623923c4d930c83a90f8ec0da78f9bb455724a82a46fd29795103922673288ade79997794cdbfced03493c206032cd3d36a2a4dbd430554a1b1bf88ca35d2fbb4921c2ecd66e74fa49374ba43a943ec72ea7d78d61d4d137e9e5f51b1532ddb3bb43170d22fe0c9132f63aba0b10ea87d3f707dc3a81b65103d371765abce28c5c18849bdb62165c50f7088f076f309275407f9997252e1362fda82fa9ecb38edf575626b1b0bdafbedcf5c0e0808001479352d9d86d47f997d679ee33054fbd6173f4dcbe490f62994020fcc85e59157d0183360783d3dd4d0201234cbfb44130c828996b23537dc0a4e2c74482a9e577938ec3fb632d6dbbdcf30b56c6c86bcaa3afeba1141030a9e4f61be19832bb260a69cec5ed9813ce26ad0608fecce30dfad9ff086121aac8872f276459b9d668d216a2ffd39ead4e2930f5124e8d638804dd788f4c183a21dc50ff8ba596fc985b58d272444ff059a345f771b56ed2f5e42d35050c96ed8b8cce22ca0c9e8a8bdb9740c050195f9c843f87f2c3d1937c9ca676d120687fea0ce18686d6926c7d70e0946fcad5a03f5b79e262ae150311c69a8b8ce47f1d4188377e6a8e534ceac3c64bbe3a7d93eeed4a925db2e00bcbac359419fd78d7873733f225040c3276f2291ceb9178042241d1e1524c583fb22ec4f8325bf8b5c5d4705657b1aee23564c9e5671515576ed26df05ff854c05df355c9b7cbd525cddc9159ba8480696a1c08975cdef256c35521f3cac7e3ac7eb27381d08d7e9ea91fefca3ec3e5fca9a6bdf206150159107b0302666ece2e37138f11a8167868d44cc1f3fe432de7370845ccd7d6cd4a4a5f7fd132aa5fc429cd6a17a8ec060173f687a2fd2425d94b304d28493cf49c98d2e381809936cbb2f3b81233ee615b79658ba7dd33a0365f86d01f443aa85e34b81d485a36f7ca7b87e1909c295a2060f4d52269f90387228aed2bc300a94fe82e8d6d2910b6cb4def790d79045bc20a2a626f85b1814ecccb2c88306d10334d8dde43c6d0839ea2d0c0d30f8fc42a8ee926bd63b52ee4a18fded4886b6dfa8baacec7fc4f312052f6685ffb6c4594753d7ae4364f088ec2e55e8b1b41c00c0cad1bae1de6ef241fd7f190706f7f127b785c830ec13fec99bab6185bddc6022d360758f4241dd7d6e137e926122b5fdf7c3177eb2837de7b55bf1ddad7410e0133c429054416debe55caa54dde60a55f8d138b3a1676932ea572e971a7c38ef6c45ba083bac4dd402f49e6fb4d3a5605af140ecfd00ce54b41597c7bfcd77dfc0510f3e31379ea00e3c59e178de945f478b2a64c76ee89fc4d3a9dfc4be70d47d6a9f2049afb85dcf50d349aaa1030e093657ccdc97d1adfa4637dd62444777111ae792508fbc2fb7688e57955f5305367511dbf11d542f6c2fb9b74f1d2ca6a6a9fe3d3301146fd820b61da2f5f3e43c0c4f95d51cb3fbe2c68956a5c0a7f79639a73af0b8ee226fd1bc103292e59bf979b2d5ff1f6047fc0e55ed042d87e065446e9e4c0af926fa72c47bb98301e656a7ec482e06f8c3187f44f4a4ce117cdfebcbb54c50ae38e10308206bd12a971be3e958f88b6698e368cbfad18a46b712b20635f99140d3d76c957bafaa83233f21b650f843be1286244a03054d7cd120b5b37e03564465107313ebab47ac260854cc97e9edad6859ee0d0b91e1a08fda5a6bcbdcc64bdd5fb50d2c7753371d6675fea39040ce2d9804e5f6e7afffc56ccd467c14537ef325c6f88cacaf7b37ca1df15ec2dd89265761fe590e0a38c11419431fd68dda86e1fc0df3952a5bf4b10ee6698168e0d01931dc55fb9cb5a60796522ab335be19fe6e033c3b442c0843654130dc30d2a7409337e5ee3b65d6bc39710f3bb5a983c6e3238c1b1e38ed08ecb3faa3df37506a27fc469398c76d7978b9b5ee85564cf13b71bf15f41e847cf6a6491d79904f629c9d113e9c2fa0ce626ebb837e928fbd6ab5c9e8ca93a6e24c69358d1a8973a022937e9aada38b21425b70d346d28a55399f649a89c308e20e4fb730b1d71f32baf5b1def8e24491f092ed5265d40e8cf2eb8678d7ea466ef75c77af024b5466bc45a682160e4cd71b236e5e847b7cc188e0a1ae72e6c8b4a4f6ed2313417f78b460efafa04273c5a9ad71e59b7cf4144918ad5d52d39389458b45b19454ab58b391d19388737e9ebb82c23e942dc8fdb90bc245eca73bf74432c780a9126d3e71e8debe21a473df56c39f7811727c8782f080123d972db146ab8c88feb7b09f737795bb1643e6d8748d3c62f8bb4c7de2ffc98af5e3aa8c2e57f41740702065ab7e8cc37b24b564e7adba49b5be5a5b684190c91667d97ee9d545527dcf833a217461855d16cc03d0ba2ff72b5c4acc3db5ff2bfb438b6106be45f15207640b8db21cce2d2d6cea1b8af51fbd6d5b17f4417fc7482483305a7f7cf23d3b1ab9e0f953eed715bf1b2f92c507293ae2df24af18bbc05d3265efc290b81bfcc835b561030a9b879a68b0cd61666877038bb03859e820efa26441a076506a3f5db28017baa38e683de3befa8553e694394e57475127f5b8147fbbcd5589a13b731d652751f5d01c23fa9b88fd7ede9cd6252467734d14482cec59a09a15f1b4b90ac832a24fc3e3bce2d28900921f77bd040f8772fe1d38be807443f92065eb0981d34f512eda00b40c01b087cd55c2ecca9649f1a9fc938ec3f5cbf7b0de19f115b96a95320a1229a6d0e9b578973f4c7d7bd87b0af0e74c4e4fcd8820f4e299e250b5eb87e95952fb2b949bfebde4ebfbd94b380c1a05b9f23a4fe9d3cf0bc17ee9c88ddc71febe1037ffa5721b544f7237caed17e2fa621ec8679539e63a519a5f8692d6f9e5a1efd9ea4b6136ff2ef45de70ed4f5d7120f0b1fc739c9619e280c75346d145828fec5222259e7cfe4df4e401ccffd53fd85fc13712e3567a107919a42734774a5e1352e01c2827697d0aa00bd5b6870818b58285b3f569844318a7be0fb6e50e06917a4e3fff8d0eb68ddf2af54a2250cd44e9eff665af2f8d593140f276581565bcd29cca97cd127971ff444e1516db923452c8564461e1c3ed5e7aeec97c43bb9aab15b3ca8018e714625b1d1aaa9e396d981a8cd1f28bbb9f6e53a7be27c194754d51c0f7720ab745b7b74b87b5c22aa4d0e19e23b4770b092ff7cd96bd972419d3a2ffad2b3440b7d18a58e5b159684085a596464dfab011b9833f87cebbb6818f467eb6b16749da0b83ca8838c7f25cb7a4ec32d14882fbeb7c2bf05235e97dae760b8ee09f277984678c93fdf108ff128cff3b04c34078ea2bd5e26ead67e407ce377cdf4328d9a474fe91824191abff522357affa442dac44f33b1cd1ebc7d91fa222d0b73b095c4504ef5a3211e69d447062a8d9eccee6efe7916a78157ae523904833ef9fd3367b55113d3d2229ba7337b52ea8cc0e379c3dbc3b3dc329cb0af9f3d3c1e091ab8c99455fbc0609caae950446e696d73e9ae1ed1f365b1ff813657e7f22252b158803cfddc0d879489246c729c2d278b69fd9ee1abfd94d87233f971e7024c6edf6dd67f987c6bbb423aad0207114aea73e294fabdd163ecee8f8ef04a37e0974b1bf582467930bd85423ff4b30fe1d0463e39385d4704bdee8867863a0b842a8b2fd0164e5e79f2d1833e58efe6f54842e1132bc746cc48895cd8b9a4a4f11b634aa4ea6db6d2eb20df475fb2a83b045d2aad9aff13ffbfb330776700ae1454c2920156909207e916f21e69625e03e6e6ee1694af6180bb0fe885c709253ed5a44f479a0f31d851c02753e6a98421eefa5db5804bac8a80c99995c26af45eca8a92936ebbcba8d8822cf6392b9a3798b29ff4fe3c80ff58b3655e0cb1e29ac3d40e62d26b94fe1f35a3c7d6e520db6c0ecfebd4adf931d6a8266c3dd54f95779814ea6a6f28275d4634ddb58347f09c6bf8560041bc15ec257784f74522cc494244a9f64a83846fd230543acf34b544115bb27b6fa43cba93c4c23d183cb87b0d1da1694578eadcf67d228b034d1902e76098e904af2ef5468956bb639d66122d2a0440d04325d1eae31899014dd19b9bc66fa2c4bbb7f1bc15f5a26b6f976cb689c6baa5212bf144eff732431637ba10dd241914f9df90a076843df4f4be1a2910927de10602691ca888a78b7905072165e9f327cb269c50cee46691ec1fcb1b61f6046ad2741923832f669a1d2052ff2eb69c9db0845f51218397ad120448b8726a93f03062ffc768d8bedbc351dfe128c7f0bc1e853ed477bdd8d336a8fc9f4a2a3603628c91e2efa1f2918cc9ef0167a3f448965ef28e5277e7d4b83521cab30d29ed86d9056b4dcca26b26d7147e360bf28a3a988d144ae5180a75d26905ed7eb65e24e6e24d62306b4414e12ac055ad22267abc0c17b7a097d6f5f4a4f146a967e7a559368cf7254ba7b88f2e185f60611ccdb57ba1a4eb397a238f81592b7e148650839516e8be3eb11c55a8c90088cd70822bc3f51e3e52082a17fd8c6b14250aeb92ae315c4aff9ee43a07c831b5953ab690a83ad97953c7ed0744133e15efc69b50bcfbeee6dda2cb5bbebc4cc7f09c6bf8560f05811f36f47e6e391cba52f66656a5bc52a6bf0ff230583a60fe585960eaebe8f6cd4d3cd57dadf939f22bebf7b98f32a097f909d01639e55aa183562bad77f4a1a0f55b153c4ae8dc30d937b134101e125eb9ebb18cfa02971f057b25cd3f3603167e37bb7522df03d6d180eb50886239a55de188f317d6023223a7f1b30e6aed232ff529b37da97a56e563ef072d2c8d28e6983d262d3032e8cc4f7f59499eb728ce3f907698a0633f61f76723346e45b6c57960fdd07d9ccd5e1e0bccfd25f6422ba9dbf4dd04fc358af553e89b3ec7c51d46cb615c1029f8afef9afe0fbdf42300288f45693c2ee904e67353e763111edf716bff5e21f291868a65ad3d256b592675cb2ead7aa626bc95b331b98ccaed2c2446774e18bf98c52a76fd8c734e81eb4f33d2a5d7e3e7526af3f11a092fa3367e733f6571e1d6a4d92c4c367a6117de37c47a5eac8863d63669bccf58357f29db7a69faa644bd4787c855b100be9b3955d4817f1be9d4bc037d04e3597b875387de9e3a4c7e8499446415258a99ceb91cfd2617f1e77cdf4ee383787fdd1c1d178dec07b6b467e989870234acf859523918cc78b41848526ac5d97cc2191610f2c9782918ea794530cbbf7ffb218ff168281c5e66865b86f14ea5ff4b091deef7a9bf0ee81d93f5230aa373444b51fa520bd422c888fc6c2104e566a23cadeedcaa28ca6659c7bdce12b97f120098918df7677a30b95d649daf7ddb91dfa3b92979b0ce6936106dcc586c42f9eb8270cb5fa217788aabaa2ae64a36b4ae7e413d533b008c5952c24a5641fc9bf5c9647a30ee4222c9bf6dc27b49421cca327b22aadf4edd4716745f0c45b2616bc1e0c49cbf67afd35fc0ae7876dc9ba4ef4f8e4fb29e98be76dfd743f2592d7e9f3316a8f72d9d85ebfb19ced917de721d993db4b975b77372678b437b6b9ca83ec7f34f066b873b8d16094a0cf61ae30307fcb9d909ffbd7c235934dc2a2ffe2d65f55f0c2f40ea1f18b60d3c2f2e818ba0e1eedf7fe0828104ae7906be6c29411d9e639b9f83d99f8f5b113ff9d5f2ab14946d940ea55ccfafc081372d61b07e11cacbf7bf17f4ed72ffa27f10f162a242313277b67e7fff1fdb79fbad83a9839b1983d37b134b2b33063b185f64eec1dfe7ca457db7f7489b90d3dd62f9ee2f3abf2c45f756310e36a67aeb8712f94638baa6eaa6146f70c97e6d4cf3d5bc846d0b119ee1c5ed9e7ff6568780929b6b4c968fb536e53d7ef0d8dc45d0561d7e9c2f255922847c3ea39f1e31adf11eb4fb17f252bbf587088bffa3c5b85556e05a821b47e713a3cc8729cd6f87fd8fb0fa8a892ed7d186e9a9ca32888a0345924e79c73ce20201924e71c5a7292a880240501413218c89224484691202020088a92440141fcd6ccdc3b9733239c8badffdffde6b5d69a592e9eae734e55edaadab5f7b377b11d4e41e565ba77ff5e15084d1b346f02fe5f06f4b802c1c4e6cdf65f96efdb054070c0d6fead84f1c7c16dd6e17bfd363c367109a16bdc6cd98c8fb0ce5e476c0f13fd02f28b1904db1f1902c78a43a80d477de3bf648cefc09fc2daa83ef3a20df5be19a1bd854f11a0c048e8a1c4746d88e4f0cd62151e82d74710c2e3f22e8f853c40f9d0b57ff2cbdb12b6c60bcdd06db53f6bd448e532df038c112544de5b14120d092313814084217d5a102117504d02ba83a30e0fc1da4b08a1159fa9c5910a2ed5e9398e607da380058b80ec34c82013130d6427c3047c5f37f166f698c568c0008c1d799ab045bbed0d1f39828205f27d50a9bf6ab8dfba09e097868b9886fbd7dc81c1c50ccbaf7878635d58e44a54e3381a90deda871f5659da3c31586a13a10f143fe77edbd99e3128df5235f193a9b0674d63ddd2a0ee89c2860ef2ba6c2ce6a26c212a5339af118ed482bb2d57f8a19d4674eb4b6935902523c1953ae15a1695748a1fd453f79c28ca6f7a52142d65361507ef6d35ae787961ff92ef7f9e7c5b360d09f4b0242209195f9bab2be8b020ed1f3d948dc5984b43fe891931f9ce2b710d4a7eeff6e9997edef64bbae7feca45126dee691ef58c8c903ad9331ca1a40b50c1b732b1744a048c46e5c8e67c9ed62b248d3e1c57d0c9b7a33e99cd89ff28f9d6d445d6949f0bdf3ef19442e9c216a1d87bb257a8bfe4fb9f27dfd7831614c748387d09fac45bbcc5cdcaae6f5f3f54cf60bc1dbfc8770231f936eb894a7b196376ed5c2cf779d6347b141e9193e3374d1909e669db35e946621ff2123fa1af7f2dd92a2651e91c4055665de580864ba2a4463322d91e1b4ea6a57dfa07f514e985708cebb297d956fdd6d345c705ba4a4d9bf77fc9f73f4fbe1b1d982edc369a153523865904caf87f8a96bcd4795865ad9b2fb2e772889e2122dfafdb2d364f151b9f62147e737e29e94b7557f7256c89204a836db6086dd7d5cf5eba5c586f3ac6a373f5f9a7636e079f7a36a724a0596bff245ad503e5c9aea6432a22eb7733740765ebdf21fad00ff3214cd6b43f294c3fb59bf06ebe8e5c760891973cf7e43563123e22e31a23bb52efd74bbd05416b5ad2bd8e72e4f671099ccc25ac1b66aecf8f18b1bf85e927340adf97e051649d20c611520daed9e4d435660633d281e0bfc2f47f6ec19077b4b53477b7b4f8cefaff09d3b77014b7e5b051e617f7f43593b2f252d611bf6caae6ee60e9e421eba8acaf2563e0e96b67ade4eacba77e59ed07ae2b081ebd7f40983e0f8fa5252faf85a5190f3f07bb99398f1917273f0f2737a739a7391f3797158fa5293f0f1717270f1b872987398f95a515073b37af15a739278f053f171b871962e367e9e920c0c7c5cdf993e4435c52c32d34f1542f2bc505db78d4bee4e7d1ed78130b190572289ec9f510124b25f5010d63f3f93723986c9402106312bb15fd92692381445867f963b95beef639a0b9163b9042a8a430e0431199eaed1a94104808e5427be80b3b0d996d45f86ffffe31a1e6dbf1f0108a38869abfd9b8be7539c361df8afe4c6e50bfc128f2706964d267ad297879d0c6b547c591221d9fc1447fabf6cca80d85d8d9992c542627e98d3f62df2d967e42ec3bbb27fb3b6f664f8d01f35335843255d3fa489f01b1d358a2325e51d708861f6d54ddb94fcee02d12cf77e720de37995c0e23632023d2a3faaaf350e6a236871bc0e01a3379c2a836c919373d2b56c2552e2c4f29640690223cf6090556d6d4f3536549755ef763e97b62ec13480fe21bee43acbe679fc6ed98c35bf226d1096f8bd0036cf9bad39ef910a10d3cc2cdd03912884969e2c420c0295d4898f1567550a1e4a2a5282ed4e3bcddeb87fb00df5bfe3ddc106bcda61b0faf5c85ec2b38399a05100162f35673a618a59b0bf95c67d374da76329164d59800b16f441ac6c5824b863489ba2cf3d9436d2bbb1a568580efbff4492c8dd4ae27a6326b7bb6e5e5eb1b67630129129394f562589bb43502f16629700678861b8dda00b18badeb8f7b1bf8ce24d88df5968df05c7aabfb3abeeb205ee482e49aff05ade5d572cbb351ab2b8dadae7c80d868e684fb55e7d7c854ce699c976e6af146b12df90a305b727f1252c335a5f1e87734e7c9d91e505d6d6906c4a639cad15f21547ee523015d0cc7652db2edd2790f60dca7ea92cf63e57e7ed248e6be6ee0422ce6127be3e241bcce82acc79ce04e09bff9a2431a4373738cb41720f63a949399ffca99ab950930ae2e54bde9cad00986b583b89db7d858dc452fbecf4979ab24261fdf34af0f01f00b54309c378ca2012c585c615ed71e93e654dd02b88652e713e37677eaf79b4a9454a21254698bd013310fe2019ca46e26ea961f8a4a8544ec31ee5a929928111dc4bdf43646924743de8818bd18bb3d77675a02290c90828c2de1be5290ad9078f095791208166179aa24372076b4f6e2dafcd3f7b0bc3d2ff2a11dbb7681ba891ec0fccd54ea3ce39c2dcf5f2a9439ea237dabdfa402064871e9af48fe59f6462076c55defb588a5192ba19c7180591a77a54c0563dc79dd9c6f6f6c7b69e3a51a2dd07071ff0a1a1bdc53b8eb2be975e2523ea50e88da2940ecdb5279bbaffdb546ce30c5b676ef6974c69c0972003fb66ffcca66020b8e7bdc190bf105293ff86ea7222ba0fd06c3788d011fa52847c473e931dc5f2ee33c06f45f5d3857440ea68fd39925e51097530e17fd139700f527b9f07b28ddecf9cee954aba1e44a553e8b430168ee1c611f2fe7a09f3dd93d104165c3fc4e3d07f2109062eb21d6a37bfc4305b7ceacacea54710d5c67941502d0182d7bc514e221975887d325bdce5e6d2c248b1302a43094d1cae350ba227dd5b40153a37f4a5750883603105ba9ee5332e1302659ce7a653a7fd0b131e07db10c80a3816d2c1e15eb2cb56fd6dc273ef52ec76f218203303ece775f33d08507d075dc5b1387f8ef7eb4a0eb3e7910df3ed93acf9777b210eb5d4b41479af05335c350801a1f3e3f55171e77b9f3bdfff26d4ea71022f89807607eb7e4c58c3eb116556a63ea668dcd7d7c9aa5fe0920459a1b332dac165544f014ed7cb9045dc8e4f9200b40ece2380d7ab6a95b619b22ef1dce8f2a170af4d6e66d0ee2a2e9db42b4f0beb775992e3045e477a8af3d370029427bef5f9039e5cfde9221fe4aa1fd76ca6da4225fa0a7e585d419925468f7473f61a299261588c5d62ce0e446cc0fbfb7cef091d8cbb9bfba5014267cbf6016b0bf4cf72be9567fb9b1f8d646f811212e933892e613408ac390cf120ec9e22797775a91c59218dd1cf9cb8c01297e91d86d4feff95128368bdddc332c69894fe835133e886bb00c6bdfd128a74219d5eb54797af6e27e9b92cf419c33008f3629f8263db27eb1e360426946a6411920c5634e9335dfb589c7afd7ed3eaa70bd5b2c9f740907e47ee01ef3574fb9bf6299a3871b83cabd7a9d97a30790a22e21242baf7a3feeccfd57bbafc915479de3d7f500798712922e5341d79f982aa76cd8494e076aa98f9a025224eba0880e0ff388d5dc65eedd3e955112a2d72a0868dfd5ccc49c3474dc972bec1d45da5b7138ca1f3c006c80c9edd73184c535a9513157138a68b8b93412f50029626f445da28e7e418caa5ec41f7c4b5782e05a1a1dc0f55c66423fa35f87be1638251449a3c8909bfa5806c0962cf5c0c620933e33692d1494f594b211eba9337e1860fd3a615c567fdac7a65efb4d2d356547854e6b1b408d36114d5c527a78836e7cda751a4ef9b542f4963d2045e57becb99525637cd1c9a4f6cb8285f39a65d6e600359fc79e236feb0632ff265b8f76546c8f44c2876c806dcb104fa12f043ec409adec9e56cf7ed0c8cd7a05901b42422d9c2efb1da561165fb48ee284096fdc3a392036fd8dfafbe4e2a4706fa7f14b510928e336c16f3f008c573eaeb60c5d195979d1c341f43bb657f70d384300b925fa8588543cc20bf25facbcccfd52d86ad41b70660070a633f4fba2b22de351b7645d6eebfdd56832860aa0ff4486a7bdc5bfe3fd62df15decb2889c5369c8a2173105760dae95a5e50daa1ff54e5308d12f3bc4ea975f8205e80becdd16223d9f080954c2ef8ab79e7b9a47040fbb0847c0c2e7a97e6615e95c1c2574edb29234307787e2684a10aad43b274ef1c24539639dc18f6bfd2de3e88cbf9bec4b73a53d81736bfe576c656ce523b9354ff201eb15f64363fb835df0f6799c445316881e20401f6cf9467011b67883419d9a6d39e09d3129e16d52f04a4f0c498b78509d175e42c8f40efce6cb43ff7e6ede206ac8f64d872170c9ff5d546dc0a8887197fe95bcf048c6fad2cea6586bbe16c1e49d94f4e0592564f24fa03f6af89010909338cb8f660a3eb9ac9da735ffcaabc01b96904821c3c6a9af5bd1f90f26e4fc3ba4b1dedbf00f4c339572b1d8e1a4db7921146a1f90b8b9dc8439b80dc1f96d412e21ef58272d2b0beaff3fca9058124aa80e4174f958a4bd74d8977cf56db2455e2b2d477bec69f3b88f3db9cc37e9e1a8fdc480309fdaa37c3fa428a0070e4e07e9f91a97763b88f6fd682748300e3f5288d09ee411c7f4e36455b8855bcaed9f9718a8c37eb807127e03a16e4cdd6a88b6b0144935f89da6fd899b7979c7e0de89f328a8db834a59b4441b5020f46ebb8aa8a25a301d49062cefc74e90ecede0226957bb0789eeed6e7fbecd01d54c23fe82e94936eb5a8289ec6c141c7397a4bf22245ca0af0875c9baf53f0f7af6736b39c63fa91477f5ba65296b3ab66f7ef3baa9c37e662d15c09baa50a819c74c9fc1bd9e0cf3c1060d74c9c3d9645174a6700849f1d6d5a805e02522908ba80f48e731b812096a3b33fd7f48594f7a6cc501309b187882168153ffb2365c4caa6c1f1ad9b98a272be97a1976a79d78907763c087692c971addeed4b37580de284b9ae4684c89abd45c11812443dea3a0f24cb5b0d3fcb34783f0b72aef0f2fa7b012baafdd80cf17766527467fe62a750893e4e7f5bd93383dd4f070998d3ed9ce145be8fc92d09556db728a04d126c28f2dbbbdbe3b58f4dfdc22993523095e26c6aba4aeec787fbf7e8bedc53a177bab09a754d7c6ec78f8fa8533ee9185e2689afa2b810725a139e956d8b9ce7f9f868aad74af95622d008c91e703d55c61b43d9159f39752e35608936afa71fed7eea82ad348e2a342de97621f9ed6401b994a5cb14a0567d685a040ab6469ba9122404efa2450877c6702d349426ccae0d41b32418f7148a8dd8d84311fcbe5bc7a9bfced158abcb67dde7baa167c81993249a2a5273f2f065eb8fee3cceda6c42386a8a9b15a81fa66b632a123778c27163b90ea1e70b797022d87e50d9fcf6357e16130490cc4623f4299c771da6a67bc9a3dd10dc1c0804e95b371283ca267a25fc4fa144cf9361bb3576bd4414fa744f60c0c3f114664f21d312dbd70ba29f2b9447bc4dee49ad30c1a811b39503d7060b538d136f1c29d25e5b33baec17cfeb08bc1b39dc7d8704a53f77170bec1e42b0c516cdded4c7d2f5fb2de54c6cde9cfcfce656ec92e2fc667c16e6e69c3cfc96969c12121ce23cdcec3cec5c5c52d25c9c96dc6c3c52ff17930e09a02034dafbe1b2dab43e841b45f285900d3c78771fafe838f8b72e693e0efeb7820feaf64194bf1aba32a5338bd037424e820471a000ee7a71a5ad1622ddcd9dac8ec1eae9d561725dd5b5b5fb6f071b517ef1f1db0652f095ab111c1f30572603fa7b18c010f340a0cc9e9b87e7b9c3ba60549d14edc0ac3041dd71dab8f28d729ce77febd2f6efeea33fd6285404956ab043c5c4b7fed80041ca8520c54290d4d4e97238261e10bd30acdd4b6fa7bbd57bce800bb6d3fdcd7e44d93c69358680dff505266e78cb71b641b0d1f89bbae8daa2719c15014c5a8e7533eebfdd9f48dfac857214f9fccfedd0f49b73f8ac9de24cede1b3fb68fefb0e76e27fb8e7d8a13f807b8e8cd80906ec14890e1081697201f6163cda74eab14defd8be35cea70de4ee082efa6017f1a2fc95bb757f77bf2e4dce4a75282fab35576dfcfe10a32a138293560cc136fc8f1728c8690309455cf14314c0020dc28f3bfee6454f80881c59d9f3d61c8675571707e7dc385a3741a60e52fd891d2ceea1ec4d231d26387cd2983c0f7dfac647bc2f41e71e2e5275589468a89187952aba13ecdf44bdf88ef96aa5018b67c0a2cce9f62cd6acb8cafdd3d68f7d3927f0897e1477b88f8ee3fd54bc812c6912532ee9f5db0e16338f6efc9a9fbfe6e771e7e771e5c8ca9ed1f9308c01dff0a54015c810de3453fb99f353322a953c6c79ec52b3883c5a7734b36df9a3e6409676781f84f88327b483e4aa8a6edce88ac679812bdd3acb3c94fc9de141c6d5c20f4cd2cb3473d5eeadcdd6fea8f999e33fbabb53510259a356d35131d6873660e0f3fd9a9fbfe6e771e7e771e5c8ca9ef7503333031149ba6ef8c0eed1f3d3e1dccf9c9f811932cdb129594f0d1f7d6d56e0b60a3df7b89e09a5eadaaaf3488314e9ac738d11dbd6ed6882a6747f7e1f947811f9959c87fd56d6cf867d742f54b4f1dcdd5b4390bb8d7f202014ff7f20201405e4f9e820963a6cc0a230e2aab8fa167abbdea0451fedc38df7efe6244b247f2d3affbc45072cdafbf88b0ed22622726465cfdb74f4f3c940eca555177fe6a2f31ea5b26bc773e3ebc782b405ffca0e1432c347f8a7cfc576c6a2c2e6a8f8eaeb75948bd36bbaeeb4e56cb9053c092597372b7fbb5d3afaf02acb0a8db74266dfd51fa514584d694b9ce5a57f6ef489bd02c3614550d8d6f9d4aff9f9cf9b9f60f3eff8416940a5e0b8726465cf7ba41b13e9cde343ada99ea29f3b60520b153f737e660ff18e918d15278a24d071bb94b2dc6d20fe540af120df146d33de6c097c55ed566a357ec3753f5f178ef114c6f3e8bd678f24fbdced3d0b41b398550155a1d91f15b03810b47055be5c3ea6523ea8f1cef9ac8f12229f917fcdcf5f4afb7195f6e3ca91953d6fd76118a335213aca18881a9857dbfb33e767ffcb9d8e92d6b2e829a3fd50328a2fc5d998fa59680c6c4361db77771d9ef14599c87e788c2751d9d56e35c6d4c0c62f327ca74853e962912632f62e4e8bcda2d78fda3f2750d76f97f18d8b719fa6287c935175ee6a8de9f55ff3f3d7fc3ceefc3cae1c59d9f30e1e86694a9f64978b7a7fe9c8162a2a4dfeccf969bcdcd7f351a351585ec47ee145a7cadd9a15171dcc9635ff91eb3b838fcce842d97189ed6d3fc0e3de46872c633071f6b843f26ba8089bb405d3c97c4d6d1884103a540f44e22419d60a90f04342cff45942e0d06d677808e92b9310c3ecfc1a0b588d2be0b6d4aa977e5be626d5ca1f0a506ef6cead1b50b09e07041e4b20dd0e3ff12e804af7a5a9e4e29df7c153be71f3872e8db97423eea7d97e3bd72357fef1cef3efab6b91f3bae11efc889dcba160eb0282d31210647fa930f70cb11fcfc0044b0215d146fc0b089437078a8e0441c5c0444186a27dd70bc01836475e77c57ef552ca9178ea29f8d17819f4ff14bfc16c7e249ea9127a34ded179249e250a765d184ac21dae572a1fce04f662d2d911cd8611975f92bc5b9fd73ece6e664b8eb572a5492c65c63ada32639f408d9ad01435489596e4cae32a5a3277d38b7e0c8bfdb550de9859ccf73c9416e7af2ebd48c1c4f6fd629bc7ade179222c5075c78c8aa2b85c27eca11483dfc8ee99abeca7d11fb5dc533f297b8b2cfd265a3481b459fbf50830bd1585135e9f541f1bf170dfec04565c3ca6542e0a23ca69ce04a50029bc515be796798216c6b24fe69b649e24036e9dd674c19d9b8357de49e7121832da19cb94ed199a319271d5ccddcb9362ba997459eb8c15938f5e78ec721634c05947af3ffb6b27a17073b6758f6c78d9cd3bdb14664ecbd75508c270e0a79bde68e50f543eeda16bde41d9f8771e85334b793f298782f596ba4acdb0dcb57c85052c6d83d58587098dd3aa5d512f67bae50a2e8860bd8136aed119a7ccae8b5be75c1594f63d42eaff9e438187644cc88347373a4029392ff132e916baf3ba3fc8aa02b653fecaa1f0730b8e86a99999cf595d537b7b4bf7efa8ff9f1c0a6abee64e32da5c3a2a964e3a66ee767a76d22e5e9e16ec1c0eca8ee61e72e22e8e120a5a3ada924ed2be2e3f30e0ff7f20878295a539172fbf25bfb91937173b2fbf152717371f2727a72987192f079ba9251ba7b9390f0f1b073f1f271b0fbf29bba50597252f3b0f3f2f17273b3b87150f629d8166e9e920c0cef6f334a9ce9773bcca1c8c976a6573bb25468ae5b6bf766c6bd2c72eca442bc756b6677159e857c76fefd299f875e61732f5f4cf043deb23511590f4167fc0fd6abd4e8a10cc12b143d40b0fa1b83a5f4bd44ea4ae5887e0700238fe60cce26b15187721ba99c4e815abccf2e9e3d1b10b717ac7c1117dfecffe3e4471b0f41260f50b1414d5d3de163c592abeee2de95959a6c22efbf63878c962d2400ce10ad3133bd567a3f69f16f71e335e394efda054a4a2458755f2c76749c2558bafa7b95ec3473b0e0ef67eb0faffd7edfbd9fd83e8f822fa7d38a72f7c42738cc4edaf7a9f91bbb63a9f688677faff65ff81e16045cd415c7b3fce9cadff46d8394682aa5b24a8ed803cfe025f32fcb050754baa5dde13b3e6a6b09b586c028c6272faca57fb573c64e67cbcf5fa2a281af8f29f00623d579e398dadde75707f5899b275aa5e983cfcf102c069b51f8f1328f3e553e67d0bab5207adb1ee8c6b96807bc2b90b77896f9787941240cb36ee7c96e5dc6a8800e4e2f03d6762fe52bf2e1dade99414dbe5b55a9293c939077165f4d5a44da8a24ea4fd3e6ed3f446b068da36a07f39d7942d9c2ef47967a76a4560bfd6eeaeadef02c442630ac67fc6ddc8c97c2a6d2c3f91adfd3849240f704ff857caea165ce32d35c524640bffc49b8377dfbb3d3c88cf64ec0a0e47466c33a3bc2f1ede5565a7d2587f751037271112bf10c55dce6f16d3c5ed88447d29741190cba3cee151fe2d36459aecf56b495fabeada269185468f337e9412c97a2ff8e55bf19fa543893ef677de61cf87fc2f8d6f2e06218a5b7ef9de16cddd655c4d2ace1056bbb2e38c7fd36dcafdb2fb42397303c45afc437bf1d3295dd5c7918f06c1f0da55e4b8243f3a58e8fb48871cffbc71961f293fbb450d98a5502e2f1757d62d9c31666c34f7d380fe7d19722766523fd4a2cd77ea8e93df4ed607217a4088c4cb9dd9f2f5264f4bdea74fb75d1b20e19137f1387ea47c7a94c804ced94ecde6ee61e5f8a5c75ef0ee91e9388efcee25d286e92f0ab7d467620d66866e71f4132c0d1dc499e54e7ed24481ddef9cf0dfcb452e234fd12e3c968a4fe53f6e482e51691be9c712fafa460e89e20c0920ccc9d6e22ca993dbd31272aa04a576d8d6d7c0e05a1fe0f879cb2ca1a977588870c09d667166342e02c78fe88cd503b7c20ea255e58b4c724bc161a68da2d8c0f18de0266c2587bf5250582d428d145b3f0f1c5ff20b03d769ec7c844482d1ba521a9e2ac555f200728d78510f306d94052aa5040553602a74f59e365805e8875d8bd8c203f4488fcf2d3a465d1ad4db24d49e07c439f3c018c8e75ba99a0233e6a34eaa9d637861ac00b8873da36cd72b5af6d9e20e143a815ddaeea1e8f46afb20fe68aa69e27445d583186f196bf7c1bb351abdda80fd41db6089e19e5e2af6b3e6d79503ecceb8d57ebc805c1d13145bdec953d806e42b31359257d219875eb401821616af37516c909f21d8aaf313d0912f5af9c2dde9799cf1eb4d72538d7370f854d6352c4ebeee57c42836aafdbf34bec3a5283b3df5aa5d1dc318cd6db1c8562fc333248f33fe0f666f4a9f4b65c41ac4d2c337d432e3bbd08ab7761cf9609023aae8490db9516bc22bf425d0274c5d2f39e647cacf936e1cac58ccf8d39ba2f7632ad45e52790b869502d6c7c5a21ec6c7f1eb03122ab28c0517f764e31a00f1d6d3952b29ac83c4738333be4fd41fcb616028e09afe48f9bc242c8ae7a36828f9822844fd5631cc5c1449d8e938f27b6dbf14afecd1353393647c97908fcc5f7d16d300b1b1f4183e05f32236b4b4d5652da6f3f261f457393e417e95ffff2cecc2693529475853b855c93802c79c8fb2d5b9a8bd7d7d28b84749f95ce1681f8560fa93a36d355fee1a1e694b59c83c9234a2ef59ba76184692f9a16d4df068f3041adec0a14e3962f2e462f9b2b5a3eb93581ede01c26f4b3c688fb6662105788b7d6ffb20ccc4f71565130e0fa37e663adfa0379074e8f885ec2bc1ea778f3c1fc11ed11fc684472227154b37e34c38bcb26f88daee00ec486b13b29402c3a1df17b114eb5771c4f8d95e1b5595049f067fa4c634f1f809a93195bbebbb052cf85c772c3a7b6894442ea58d7300cc458ec2695fc2ee94ce7e9450c1e19fa669a4129a303a88974d8a243cbb4c6f6c6312f5f8d9ad9c70e70bfc008e21771d55d95d55e9de5cfa38f39e771bba4c2e15370fe2a1b35aa40d507f8f3a1392ec5be3e64a9bca5e80e9b45586dc5ba6bf1ce992b345e4beb167d3250c03a476bc2877858564c8c6ef14124f8954e23dc29b2ab47d07f18a59cd5c82730adc464acec29994c5c558e23a2f01dbc94c5b73761f0bff4c592c46b0234b3d99f2474044297ecd98010da3390b1edd42207b7123f713af930071347bf1e52c8be604df1669b745c3195edda8a44540ffb64e8bd83ac9d96fec4ed6ec697d14d29d0d9dd93988cf2e5de4c1f15aacc0844f4ff0a845f8606d3f046463bda377133b20d87ba6c711fdf572e31dc8fc8540f583f82001958afc9ccedee0f0bcf76dd56d3287a7ed80d471d96726e8bebec21f24d008607119be398d594f0548ad74527b6ff1ce8b87ace4e6ba37b7eb985f3d646605388a3f3de90cbba895b53c95a8c1d2b46575bffe4b19c0c7be52d9efbb03130d89a5da0ae016459abe33ace10b30a744155bec97b7a8164ecb34c17a2a32fbcb1f00d4c5c9de8f466e7e9944ed97a9e4afb838b0e351e9005243068c99a956c1ccb1502d549ecb2f95dd36d9f20304aea545428c2d50570a7633d85de0f2efaf95d0c1001c0413c17936e5b1f47668526b06831a8efb58a22e60b9aa39c13b50d1f3b9ae1b9dcbba7d6e2039a5ea3620b566d195c49d32e7ab53c35d9615adf6016a22164c80e32abad8655e9c740ec752469f18ba874d990dd40280f9815bcfc2d0441c3d7aabb8fac383988e332afe0380942feea8b4e5194218576a78e3bcf1d8677809c26f028e8b2a6e75d1f4b83928410150b9287db8d809c322406ab385b077c29a2e26981c6bcf4230a969976bfdd6850ee296013914307baffb211f5eead9ce12612c48f1013202b8f39cd5d0bb6793a220e97031c452064f50f82ac01ca0195b41db9ce32d66136eb021d8b12421debd0688e837e6276b83084b999285e3c6aa6b9c30fd1cc40b481174d7aed681d268a9a173e319af1faae1fb7c8d4f80f9b9c13ed0b116728b948028cab7d0b55ee7d6e407406ade7b89215bcd850e6efda4f1814f618cd006b104406ab8af9e5c719f5bb517917d5e6f9cdce0c0bbf1bc1930fe511b17c9989b7920265f689eaf57acaf289cc601d030f808a15962142cb4d83554781ced25396897e401ebebbb28c22fc881fc1f02065ff463f26ae89e821702cc3583867ab9925755b2f0c5d2140731f338eabfba3001d5f9479e298bb1599158556f49648cfaea31e00075b466ad0dcd8cd5a93fb7028b7a88cab6753bf4ea5dc0f8246c131b5cbbf491240bf5267ca610cd79c20fc0a1794b48a9947ce3f51e36abf1cd7cf7a986e1f45c2f803928ac77343dca23b2b3873e263e9cbdd9d163dc18705c9b217d21b297dd9b5e4b81d681ff2c75a6ad0ca0eeaf98281a45e429f31be1f7ed06ac27178ff43301bca3edf118666f1291626067e6cf4a576709de9c1f01a83b4f74a8a1fe0924e9be762c73e799cf5990de790de08ce0183dc58121eff376483cc33c6da3d3e9486fb1771067fb22c695da6f941a3da8c9578cb2716a602106701cf4e2785b41f7e0e93399e99c895c64eae8856a1a71c0fa2c15888ef2b4f8bc9dcf8a5913d7852dfc4926c076ee5ae1e96979f7d2641b232b9fa57fb6eec7fb3a80d4cce18a74c1aa3424aa549fabe44d0507f92b563300eb97077eb2ec6db8adf5bb7b987a6486f87771910801f2b1a9e371dd924da062e6ecd7c7a713439a153a9b040fe27eb5cf2252c30ded761370d905da57b9c80d3e01d4154d5e56d6ce65b78639cee0963e22ca938aacc480f56ba160c06f2948afa9fc757de0abb3ef75eb2e5ee43d884b3ec2b4b88d973d8d7fd5bbb7dc4f29751be60b5097da0766a7458d85c7c3a6de7c288cd91bdb5ace04ac3fbb1e69459ff0569ba294214b8384c239c101ad2207f1015bda97bc86fe3cda1ee64daeb40be2112572007350460c86f547d3f27d65f36229ed312864cd9dc8fe207e623d910ffb3151a9ccc7aebe74e68b68c9d3bb007decf6bdbcde1afb8c910b3c237eac14a8410ebefed607f12c6e3dbb2dc8d5f78d7d556fb5de8c4e2b3754d91ec4dfb0a83dd3254655eb1e79f126b70f2350de781fb07e786784d5c1703b3e727fb94f34563f50f91e17bbea209e34b5c9b7f1b29c496094e95473ee5bba4e643e40eac7f9730ff65eb07590f8a492de945951421b745700e037535ade0df86391f95c1efa001fbd9587d38c06384f38173b86cb3fdcc9650a6a6b3bc963172031c20920725596e66f277d490d2279fece25b021af5530c11970dcde76554f70a845bf723ed0cb51289e70a175e81de0b86cb4c51221a37b2b35a187e87abb4459f69be54f7807f1e0e7b22f2c7a93676fa23e3897b039c47b5e5d0e90a6f0ec3bf4851cda9a4bea74318e63d5f794e90c6200d7885e98489a8205d9ce57c5f131b964c4db19af9a02f63754bb0cfd2de20ed97b15bae502c5cfb8d3b3e800f37ba65e05ed8c6a3efd72a62ee3288a6440c3683c40feea546d3b3f610bcfed32a4bd92bebfb17b37ec0540ffa1ea66e57eb60d19907e74db82ed1c6d55ae724f00e084b1defb6572bd8fa8e3d375f9bb69d3320529fe007a86d68765adcf8a688d2ae512b16fee9b8e8c873c00a45e955163c8b280aebedc7c4e936ad1a51a9a91f51e9015e84dd053a4223daf137b31a97e6cce16276f08b5028e63aae20eaf53933a434f870acf32e5a61bdacb7b2d024e00ce393c6b389c9e6f539cee88e98ccc87384100edeb45cacb3a15ac7a16cf9a66346ab13fb1919c16a0ffa6f73ccbd51018ce596252bd578fdce014512008601710114d3e4ee1be847e4a37abe5e4ade5b4e738520f0027acde3db972c6d06b0da7efbf9c6775c194b8160a383f9c7a1a7a7224b769f8ebc206d1f218f2fa90c949c0fe601dc10521bf9b1f93e9f3beef46346439b6e101c01ca54b36ea2d788fafcfa938d9a58dbbc6e073617c23f00ecabf5e6670ace467bf178ca07f5d6bf947eeae1aa90ffb1f6e1fc727feb772f12abc193a1cc16b4f891504f933531dbcf9093c84e0546ae897fbb744a0231148d30ca6eb359050ece5564818f6cbe722107833747b181e8afa7116128a86b670aea6fd166aea616c22cc376d6343751b1bdf3a7e333abfd25849a09f5812e1d5193c8efb1cac7cebf685a37e6f65cf74adf968aee93bbceb900c9b6d51eaabc1fb2e56c20553e46f0022fa5d5cd3297808ccd726b5150a319f0f2151d6ba038114df29e05362a3b7b2679cdd41d208c73ca5e27aebf7cb316a91a4b4d5180b1024e5806510441297d4925755b9a4a9ada6a6a47f49455c4b5e47fa9296aaa2b4caff1352118074f7ad1b3f006a5f1a418cc12c7177bf93845054b5bfd209eb92231279fdd1953b50e6dfef17a985d24451a94808a8d03840ce4babe6443350a67a0c171dd37477283316698f1f53c8068c0205c1ddeb9ef4608f93a567d2f3be1ffebc7b8b4277c8aea6452b4bff69da93f9eea744f912f23d1a44adc33c176c7b7342752175025ba2f6a126175add6dbc8b53f0de9ee9707a58394959d009b71f919d5d8573299f51a3eef8bc42652ae527075724be493bc279624f3f2f3857b1ef6e6297a86b895359b74792914d9ae0a3e18ef933255c0ba26e8b58ce94d9030cfbb5fd95e5c95f1e39336a4710a458b43e937b724fba684ccf09dd1f6374a38ea3fa5ab4b9bcbe4612ee9d6e85aca1be946215f5acd34fd1cae320d170de859412c96411859ab82b6e2d4f3822cd3b3e6a429f126b37d9e1a56a4a412958327bb77c071c515a3365b6a42f359913e66ba611a4bd83b8cd6b138e5b6f9066d2540dfb7aa5530964f1f365e1d0a6ed3ea1e4e9bae11f53695f67cf20f2e84ef5e3e718bee7ccbba33d56e33d90259db82877746d99de945dce24eb712deffc2c5ca4c3805a9578728ef3914e53410296772c1c6bdb2f245ac8e7abf7352bbad3269fbc7b774dc65b0b1429c61e76d45caad687599d7b98d090c6c032596fed97bae1fe4c77320b43a9208196446099e8916e1134d89e3e040fba83c30e0f39b3a4f5db7fb538349ba72f3c406c0a82f12ec1924882cd30603e40e9c964ac13f2973734327ca52e2eccae2debbd0d380e0ef60111908977dce5f1dbb1a48d6be211ee3dc21e37ee1d5af16bad6a76c26825c8f301bccdaed0229d9c68794d6ebb88966e1bf14c8ecf0a875e3d0f3d112ad247cce3fdbddff74dc337823bd14f282608d64730df2430b59729fbfdcfb3d9793be3166f9bc5e2a4301e98f3de47f0fb40b628b0748768527354836f8eb30d114e8d6472760a49b9098ac30a1df3fb753f2d6921d806b0390c96c901ecf9cd48bfcd42c6df8672a4ea03e16b96f2bbc3955714d08250ea945f73e9fd67c6790db865489d43c2fad63cf8966c2fc1247348874fde2473211c67cdd0343d2dd94c01d494587cff0b35e360a1310912fad31a033d4fe8e0393ff4ee3873f4f873102c272cd2ade3aca1df984200f901ebb3e3ac8150da50779ca64a72c4e60858c411700dff0ef9473abc7d7f48dc71ea873127885cec16d5b0add27ab48273e62b6c20b818413513d17c9c5b7ed49a4e1eaee696d402d4669696563ed4ccd4e20e4e1e8eeef28eda9a52d402d46c2c9cecfcbc3c9cecbcec3c3c1c7c5c9c7c7ffe44d5c3fd3fbfe1e3e5e066e3e6e3e5ffed9fec9cd4ccd41a965696aeaea6f6d402d4d4ccd432f6a6d66ed4029cccd4f28eee96d6ae97dd7de41dad9ca805fca8152d7de47f7b0c3b3533b5e6656b4753770fd7df3e48ca97fdb283a9ba962a3fbf879383858c299793b9ad8eadab8ab30adb79354d2b2976690507270d2e655d59272e251e2b0f5d736d0b292775eff306ee52126c9e1e0ee7e51c78789cec35d95435edadbcad9d7438dd3c742cb554ad1cb538b9790cce5b49ab58eb594bc859724b6859b0399b5a3899aab8c83ab2caa8d99b5b6a72f0b968397b2b2adad92aa8fad89abbfbfa702ab1b39ae92b58eaf071bb68bbd95aeb3bd99d57543463e5f3e573715431d5d0e1d672e4d0b2b4b76667d59390779267f770b47376f176b1b1f4d0e4d2f134f765b7b4d69257723160b37713e7f4e49676f552d0b7d671d4f0b6e2d6e567d757e41597d0b7d7b3f476b66117e7e4beecc8ad22672a65cd6fe3a66ce960a165a577dececb4555cfc5d94e49555749d35cc9859f8bdddadbda59c39a5b414b91efb2ae8ca7abafbaa2a2a68a87b5139b8dad9ebaa782b6953597a584afba8f8d97a3a7a5b8b0307540c08fd89f41f523b04b0bbf57fff931fb2b12b867b56c35fabb950f90f50d4abe380691a56afe51facd77647f454150ffa369fec393ad4ff5133cd9d12ae8183a61174e567c4ceb3951f01435ee5e38c0d3a9ce73cf8fdee39c04c77b35ea71dc53063294803bb8204d6f98b3d1970bde9c6858aefb8a756a6c968f0b70899e859dbd6ad1d07b692bda017e252393ea98db1701668c4f5ec2f2c322fcafa88db60dc29a49bfe045ac00d2f1446abf19db8e4a53cf7243e5e017b32b9e512f00e8e36c4545c43b9fa9ec337ad1f8a3d576bb14a9ab01e7a9b7066e1ea7d7b8d567c4980dd04f3d48887c0f913f887be2c42af82bdd3f11425d00b7bf56c14d2f9b01d0150c50f978da15b2af6727326ca773fa93edce3602f6a2afe21d814ed7f694769ed463d4525130f23df001b4bfb26ff24d8551c2cad68d14efb69b7ad5af4eea033cf9eb71de49897eaa451923457a814df8befdd2a27207f11ef2243c7a3b1ece3bea686fc37389bc193c0dcf1fc435982a129d510619c6ccdea8547e6a3d11a0c60f20f6d2ea3d424b6de90dc71419d5b8730f7bdd13ff2e205a1d57f2e219cbebe4e7d1ab5f662de6d64e3313eb013c6d230f53a978e46791ddbdaf6db944ae7eedc00e0678aaf476ea9fd9c899b1cd918b48bcb13ab9f2f82e11208a0aa7b8b4fffc684818d3e7b9d9940baf34d4564e032eb1fc0cf5197e796576d686d9575f281943e0138ac8f4413cbedc8f3e7ada478f3c4d61be646ded09b5b03bc012cf1387b2233762b86abee7fe08d38d38a03e5c3819709e88b8699642429da5c4856c70ae19a7c14b7d1ee0a92a92f5e8e1e1aabac63d69c73ebabb6cb7b91d04f0b4f2157cc1f256204c5dd4a84e96f34e7662a81503ac970fc48d8bdfd4142371a9274248dc032fed3422013c758fbb3f8a350c10a3b15e6e1efbea6d945a70da0f7049d2bde2cf2121e554aeeef2bcf03201910d727d5d005322e40c2d74fd036bc66bf67e2ec6f7c6d30521db004f4f7ca8d15af989708ea1a5264ba7ed8cabe6d4d4809b8063e565fa68da2a4fed6ff62fe7d85e47abedde04506744ebb991d9b5382c9d6ea9708891d18436b3a5b81ec487306b8745382f2f312a5ec29e181fcd2830ee01e08ff3499b34531a3ee63b8a3c0cbce13b66bf2bda0d98bf7dcc689d148ee74bfae4d04e16cba9bf9062032c204ffa08d5c6871dbadd2ad491f2d16476433de4019efa95c40676fdb37341b272da50833cc309c3c57d804d4dab84416dadcc073736946f2c3887e782d25b4fc0fa15d6d5290a75ce83d3ae5fc9ec0abc644d19e30cc88b8d7d2ddcf8f4ecf8e944c6567f9eabace27a358d002682adad3fda86f1e76af1dd074b0f5e0caf5fbcd106585f8b92a15bf9f0cc77c13b7831cd7b53d4e34d9900f9a2c5507e6c7e9defe4958b71e31f2c2f909357105202995e3cefcf24fbb859afe4e8a74d77dd0ac89103eaaae19478da5c9ee13dbd4dbe453b57e685f11901c45c63a2fa18d66629c8c525d275fcd025a4a1374c00a6821a974b60b17fec85a2b5a78bcba5ccf496183e00e298e5a92fcf0ca950ab6589876e2563765d8f1af0009884d3981904e4848b4e500e67a55b0ee8d492482a0088c32842e7ce556287441ae0e97bf5d4254adfd91496051c5c575c884ed6400bcb4ba6f0cbed3bce9896d10288fb5fc7a434d35e272541a65ecf85043ed163f631008c0f1709ae43a56983d823f740be4221a2802102519d83f8fd49f2b48cf8ec213c34f850ec07f277cb9be500264dec46de0b213cda62e18cde88b96c7e829cf460c0fd0701262ebeba8fa5518c448b2acd3bde846ac4ddee07c86fac0f5c38d6d9e89dea5eaae512f669920237c0fa824243bbdf76cde284f599af7d44125bce39530a00264e0b466cf515ae4761b7379767172e3136c1488700c4cafb6aa1e104a6f53d6f298889b83e19dd73d3260530759e735bc6c5d485a6a0373c4a24ba8854694d370cb824f559357a66d58ecd6268676db2eab0bdd9e69205c641bc4ae70992c2f2fc359fcde03db5a68de71fb3db1f03f4c1a9e6ab11edb7dee989e1572af75a9f14ca53013031ce5093df5ed555eb5dfbfcceea9463a13f76933780c9233d65a9505375be83ed425b54159aae81305b3880983c67dea39b8fa7dac82ae91a96631760f184a41460cf39e789799b59823b9fc08cde72eabccd3d198c45809bc4f876e1298b172f6fb4242be91be45b77d6a19a00f4a5715625be22d59633b679d91fc96f73c69e62b100c847fe39379abef80bc4b269c6fafad1fe6635e55a80feb9a769c639d581a385ac9af976fc1cd3edb4549c580093ede36a228de727f34148bc073e65e42aa9490e60fd266db2cd9a202ba5a014bd5774e9c18b71bc4a5780a7aff94187b89fb0549fcc7a2e71797ae285463b2600715c3f9559e55a4a936c6561745d20357a3619dd6b001329988f09767e4b5c5b9fc576259ec39ea87ed01f307fce33d69aa832aa5ede9d5871d5230f1e74529b07d80a18130999461a8c33f4a438db149de23bec1a6e0188f1e352d1cd04d919c81eea0d3cef35f15e3a4ca57a1cc49dd0e9e0c6225939e466f0f343dc70776d03a5a700fdafe846d6c4b99636df32a3e5d1e79221cb9b1480f62b341923e3b96cbdbcd56ff4f4b14f548c68b1ed6780f23c7367b1e4f23dc863760c854749927383745d5407f10ef230ca4fb3a2a70daeee8fde17e86eecda8e02cc4fc5c5a16c453edd5acf48c9e8fdc1a0db84d1db004ffbbce61a3d927fc5830c71f7127765b7d2a6b53240608f08fa231f99650eaa85642d73fcb353de5b6a3c803c0b951a7ba85c55a5989d2a780f263ab5ea3d4edf03c8f7c0cca30dee90a9501fb8828760fd17a7b0112a807ed774b3e55cc145640bb1132dd8163c9158fa972be20fe20f3fef0f8ba6e89c74ca30ec68ab5a2af6315805300122026a241f53344dbbcce27a51115388d8d8a500024fc6e7df949b2bbf8c5c9ed05ccdeb5b9b2109f70530d12e949c9afc50f0a4139b2854f3cb6dce74633a313f0053e5c1845f11214d10b554779f32d79548a6791b40e0eb8ca0e55ac53d1dffb3cbed1f9b0bd0a2df447b2d03982a1ffcedab39f134d6f03a1fcba53641916a7af200efbf6292a91edf16986f1ab058aa3fcadadc6ede05dd41d20d37f840e02bf69febcc7e3923ffea8c444d557338d902657e9faecb375aa336cb726338e9f047ffd19d7ff1e8ff952410ef9e341dcc467b6fe06ee55325bd1451a68fd31387b454d45d54069701df1fd20c1d89e057a1931338fb5797fdbfbddbcffdffeedd1610be5e13be4c3276beb8abf94baf20a3ff48e96d84bddbdb8af0106aa28a9abfdd76885e7fb1ff948bf30d27d5e07adf8ae8a91b7955fa87f61456a1df8a261cb203e58cf81791fc5fee5b0804291386bc407f68cdff8e777eac3bed8e6f4e0075d781986a31568e63ee107732118a6b7e98de6690bffdc9953b65ca927ff938f8b1dc45b6579fe0ed24822c02e75011714759d933e21f8d079c18c4dd7f3f5ddab4eaa9abd914e46b31a276c3bde2b9f7ec2b920ce8e60e126704b4b5d4e64bc21f89819068b86f1203dec8e31dce8ac288a763f88465ddbc6e39a9ded30707c165050931e7d0f12e44cba155128cd992c97e9091638f2f8cdeb9d2f4e2ec7196bd6fd5876e5f0f3b4d422483f2fbcdbe7ccd6f6b425fe5cb3022b89a023e9b81aef8e51a5fd68ca941dd8bcf33628962920e47d9c97eff9a66e80ef45fb70d334624d442514823d700c689ddd35fbb47f96b46ce4e7c08f86435a164e392f81641672a88b314c9e281081c1731714034252330e52392b359ce34ebebda8c57e34fbdc5f7876eeaf2fd5f676a02b48f1cf629abe5eb752a9d5327f031ba661a30f3a71f20f2f066e850f8ab4b45ecb720ff26dbc17f344f4e7968a6193a124ec496b9f01f0d28b419dafefc8f376115b2867040fb6ba40851521600c6401ec9dee06b55ec2ef40cd2b2c2efe4ddb2146f83f5d5efec3b1aaa3fd877ebf050c2db26cdbfff2f8fbb9b90f35fb9f6d8abe2705957ff6d7844b7e65bf73a75a2f9b7ded856828722274740febee9aac7156408d454d9a5e5a7697893e4bef3af0e1501f1a44b42b72fc1437f93af50743608a446ddca9e8918cab24bacfa2db33816cdf51b4ddfda20392190dfa06f91f29abff906d681aa9bdf7ec3962fc9616fd8f225a134a1672e1db8ef41e36b654ff8e22d89d98ddc7610c5e7b3327d49b16a9ad205df4b2571276ee58bcacb6d22acf8ec4063e121e7364c6aa1a19592288a3f72ddfc8efa80f38fb25be9c3f36d533c5156ef07d7aa48429172bce710d9aeacec032511537020a1ce92a40b20bfa9518fe07d795e8803c2e45e34a13fb49bbd63d4b66a14e44188dccafed9eb525439487d2c04eb1f55826b6ba5ce30695b5f7731d08711f46cccaedd8937a6b65bc860f72453dbdd7bb82aa880bc38e975edf95ef2503321b43fae066bc73b35b33981361a3a9c199f6847b1261b46a669dc3fceb943872f4744eb65b75ec4c816edc1ee609979ddae4d81df36af95655d2847a7b3d0b53ee6628c04f39d1761e1e79d6c1f7335aeacca587180a45c853e8bc0408bb58f65c30a2152858761e709ba43e0cd4311c8cbae38d70a212114710ca148ea0bddd0edb53f76578aacbe9a79fbc705004fe49201b207775d40c5f2c8e9fa25984c5645d4f5a7dfdb65fa3c324f762d0fbf4692d5537656f1f9110f90c59d7f190cfc13584aadb4e717869cc76b5233a66aa35fd73b798a15323d05529de1a144b51df5bfff2f12821d4c9ca4eb4a0aa03a77581d4975668ceee5eebaa2fef49b546757952d0e8aaa93f2b9b976c6083a1801e53ba8cee9cd3b48efe021d4bd2f6a91e65978af9c40ec84b40830d04d53eac75c31437793968eac31b2335279c7a10bd8663e6b8de0854a5aa5be703b29f37e74da4e39ebedabe3b47761b4354ff5cb4b3b4683132d72a40177cb0d3fa87ef7098d449e67e7ee11ea3689ecab89780f85e3b4ff6f857825e628f8af0effef50afc0cc28901d14e20889e54ba9b5e25821545c25b52828b56cbc60dbca112e78a404d29064a7a3ab2383480dd106487db0a4c9f0efee3051a3998d8d16acefdeb644664e2c7282dd61910742730552d04e5d9c70b85747ac54c9f6ec5aeacb95b4b949145a90fac7a2a87d0307a329836ddb24089e0a488edfe61f5aff7fbd88232121412073ebdcde437f4dfd51c1b978624604d11774208550d179c1a1abf01022acb721d4b941793ac2cf89bf79fc45ba1afbcad2bb4ab01d82d1bc83acf4470405b2940cdbad318be717eeb097419ebfec40db15737e8bd43e2be159457bc5f4eb65699ee16b4501dfbfccfc5df9b51575509edd4e5cf42cc1a2d47d66577fbbe2c9c4e10b2ff5a34bced73f21a89d12da7a38385bbab2587a9bdb983a5a5bb2381caf9799d8bcd9fecb82f8e11aec7480e8edf33f920d0fbf9a904e5c00dae81904db1f1902c78afbde36807ee3bf648c0f44213bea766fe80ecad6bf7397ffcb82f193f2979f4db03e158723239edd366326f1096f283568595668eca2e586ef450849b302e1c8cb283f143e1345936ad3100278c1e6e1cffe7bfef28f0f1253c9ee9e3935ad67f4e8b10669d9698a337711b2acfdca5ffeb3f39763285bba9b2a9bbad97d67fdffe42f37f5d27416b7e0e231f0e09176d4726277d4e0f4e4e270d274d07550f47465375592f2f4e1b490d275f27413ff4966c5efb08bfc80fce5ecdcfcfc66e69cfc56fc569cdc7ca6661c7c66fc6c3cdc56bcfc9cec9c6cec9c669c7c9c965c665c5c7ce6e65ce65c969cfcfcbc6cec3c5cfc7cfc3c5c5c567c08b5ffa7e72fbf81b4917c115753f9e35b33a6500a3e0dc5576706d9f1df52cbde901918ec9a4dd6155f83ae54b75745a6a3cfb4253297389be315229f2dac4ecd334b761f4e1da7f86f4cb77226753507628fa14f2371953032235f34da1ff8e37004f2d5c0bc0ef8c11fee409f45c47141d1ee3540fe6d81ad8536538d21e12036ee50480bb5833b0b3dc5d6c9d633e37c149ecf7b532f630ac6ba4d32bab4049bd042ec8e638eff9ee3da710abf72b8b8147625a7482fb674e823eeacc4eef1d5437ffcafee42ece08270413335777572fb7e163bfe5f34b26354dd34217e5de677b479e46887e36f4ac171f8fbdf2aff765846712cc3eac61b5f186e93d06d05fbe40bd90bbafc6ec0174c7ff20d03be3e83610951547b1bdbe72925cd87ede30d64b70ef326b0ee8c661096dc834076a0bdf010d6fe8d5a683bd15d218095f38140993d370fcf738775c1a83a29da81596182bae3e849608616b0e78b727110aec905de13d8985974ab433a7be2b5e56151015caf54ce9e67cb3ebc47d167921c66ea84c13a1ee57f69e67ee3f944470b2fe456f7a2e19f6c736a82238e761088f9e1fa113470d1a92c30df7fc5bc6501fe2d9fc9b786fef7a3c14a9596d783f81ae371c62d8114d4e0c35f81aabc7247844d6ee5f7577c4b23fe6f5ff1972f3fd297e391e8cdffdc93202a95d8f992d82c5da4691a1a804e733c5f0e2dca8d77ae8dcdd01ddc447808d1fa64c8d904865adcd0f922f75e1b82db33455feeb28ef4509192f5ab1045dabece8e7d500cbfe9eff588d5803aed471e798f7f64c53d5a8ccdb9d6405e80eee4e6e0e476f9bb5768d1dfeab373db7af17a9b9b7b9b7adb71b2d95b71795b9b5af359f19a5a5a73733a5bdbd9f0fea60eb97ab2b1795b39fa38d85bd89a9aba7158d9f170f0823c1ebbf9381b7ce7ece3d5c5bdcbade8aac651769c91f58aed675bc0da8fd89114cc320766d9444530001a1344f1c582ff1f1f3c6e21a878438ea3f82fcb984bb43f936fb8a9596839d293a0e724a671e238078befa88f60fb4103f0fec613b4a996d1e3dc75bd83d7b7193633dfd9186eb927fd13c74fec47f62f18fe13da06d6ff60f2378f326f507e9c3e38fe15a3c0a0bfb7ee379fd7c95f682069555dba3d1871993a23ca1ea135deecb3f8ffb07ca01c473ebea36fff3f353fbfa37ffe71f313ac0fbe83d5bb89d8fcbc70b8cf18cd6b725ddf78fc7f583e8e353fbf23001b51f900ab4fc3421ea0fcffaa7f7f42fba108eab7ffd7fa19584137f5b6d7d6943a9c57d6bc83caf16f9709e5427b2d2acd0f7097fcadd53ae5ad91057272438d6697d75f8405cf0c7cfe72ba1f626464b3acaf8457c4c0daba877545c6948b1ba57642186d6bfd884bb9ffee2ee11e2d1774bc6b7c368ab12e89aab55a53d6b30aed1883fe2dfc97bbe4e7961f77ddabb9a79e9a17afafba9ca48399929aad9487acb5059f829da9b6acb7a6be35bfb5b226bbb517bf83b3bab39bda0fb42e1de7d474fcf9f3dfb84bccadd8d94df92d39cc78d8b92d39b8d978794dadb8b9b82c7879d92c38784c39ccb8f979b9f879d9f8acb8b84c794dd92d4cd9b83939acccadcc78382cd8f8cc106a3fa6a5a7830017073b0ffb0fe8bf1b62fea1b30b897c84f5417b3528fba28a7a56aa20e2330ff27cf1d753d52e433b8db9c41f535baf73d5542f6b98d0b03e2d756b142ee4f84aca657fde8f422592afae5837357258754353138d65f3ca86fb89296ab1f2920d85253fb0eb62a13b50fe48e4984d77d7f87cffdf799e542aaf6ba13465d88960f45b108b05fdfe5128e99550d4e70c2c32e74a8b62f0f9759e3d6560982563686aa40bee5b746c148cc865f771fc5f32cc5ed57c4834acec3726f84876da0a0e3511d035e93f74d1f957771efd441086e00fd831cdcd5c2f5b1ccb150228a02e9566e8f6c33f7d70bf07e0700cd6e45d34b0703c8ee4580ac8e1cc47fa265ce1f76df27af289fab4b0aa12a2abdaedfe176f3e2ccfa396ab5671c45d9a3c59e3f1c9e3cb33acf7af262b186fc2e6f19aa13b48ebff0a1a0abc5b8b346feb4904d84bc94b63b41552876bd94d2e24edcfb92b3cbf519373f826e3ca5deb08a61f218941fed10509d10c94e2dfddffffe55ed60cdd39a5fe67f6edda5352ad7a182888ad7120d9b921a7c0be1924ed280c64f2c2ac40706b10dc0604bf0c82db82e0206e71983d080ec2ab8381ac3430103e32cc19047701c15d41701077090c4467847980e09e20b81708ee0d82fb80e0be20b81f080eb247c24088a2b040103c080407d111605740f060103c04040f05c1c340f070103c02048f04c1a3407090947fb018103c1604bf0a82c781e0f1207802089e08822781e0c920f83510fc3a089e0282a782e0205e69d80d103c1d04cf00c13341f02c103c1b04bf098283686d3010a503960b828344f6c2f240f07c101c24cf02ec0e085e0882835c7d0003a1f3c24052f6c24a40f05210bc0c040709e1848104ecc040686fb02a10bc1a0407d3ff40d2a6c340a2e4610f41f01a10bc1604af03c1eb417010de21ac11046f02c11f81e020f6291888d51cd60a82b781e0ed20f86310bc0304ef04c1bb40f06e10fc0908de0382f782e07d20783f083e00820f82e04320f830080e120c0d7b06828f80e0cf41f051101cc43f0203f138c22640f01720f824083e05824f83e02f41701056166c1604074993000389628681d87f61203658d86b107c11045f02c141aecc808164d28181e49182bd03c1df83e02079b060ab203808eb11b60e8283c42bc33e80e09b20f847101c249011b605826f83e03b20f867107c1704df03c1bf80e0fb203808ed9e06028283d81469401cb03420fe471a10fb230d489e341a10ff300d086b8306c4074183098283c4cbd36083e0206139342059b468f0407090b00d1a02109c10042702c18941709078791a10561c0d29080e62a0a6013350b7b7a32403f6e83eb1f64f27af27f3cd07490b5899273d2228bf782ce6f5378a94cd094900ada33c00f353c43de37434883dfff9005961bbf57eeaa31f8104b2ce7c7741fa2f9c0a48427f6613fc33112ac33f2da3206d2fbde1be50ff49c812e90caed2d0270c5764b0eb3e00dc0101d3354db7155ae3d2cf8d8921195c8fd72934be40ceed8a364377f0d5e12198735e21b0a7e8b5f852c1a53a3dc779f0370a2a621d039644040dc429840922f03880ef1f7f1eb72d1b90abc342be9afc91485e1d5f7fee3982030ff27d50a9bf7a3b279d36958d92991bbdeeb3e810d5a1324e9ad1ad23386ffee9de4eb045ef6f817aac76a1c21971e6131c0ee57efc3cba0f14394f1daa7c31e6d2907f6246e803c50d9730e81395bf6e95f268186c6e96a2c4ccb0a2449bf7c33fcd568aad1193177bb4c47cbedb2461a8399eef8bf73ae5d2171d07d319febec573cb19e7a5539ae1a77f504f510bf6f4456348ad52dcb4a7ab083db5a922607cf3977cfff3e41b6f347aee161cd7fcfa40363ba93e87e04630b3cecf94efedde3aa774878128577b73cfb43b91642229f3ef9156dab2d973161bd6f50b9b255b79f39d1fbcbf66fa1452bbecb8567ab1b68062003bc26354c4e4319b61960dc50feaa98d751486a2320afab3a3d9f867d40bbb32f35d61bfe4fb9f27df43f71499926cfcb475871f4a49ee78b7a730aa1f4a3d64462dd1f14742280983785551fc07f2accb694f4c5172ee0b370fc63d24694969d28c1186ee84ba91afa9a14699f244bf9963d91b346c1ca44a7d31376b48a617789f833f916454a7d5b3f347addf19ce53ee7b4d220661226a1b746e2f7c4dba2e73fd92ef7f9e7cef3e10a37ae2c8b7cf6be7babaa39e50dc3e11c37958e582b832cb54ebbd3144e4fbfea8c1043f5fc030db5dcf5a6ec23a9a1de377d6946f17097be1020d24cb7948f85cb3fa312766f3cd0a161854de47beccbe899455db01e7a7438b75a9a3205cfa51eb37ef3a13dfe476852cefbb1aa39bf47d27b0cf87e2fcbf96ef1f1faef64bbeff2adff98c547cfa1b97c518c995b50be158fc55e7cf1d4d3ebba38ec8022e7eaf5a400a39619c6e83e86cf91b1face44f948ad93a9e575c78870c5d22829289ceab8e5e8e54c91f5010c8f6afff385a37173998264c97dfdb215458b344101d8ec8fadd0cddc13c70a8c6fc11876a64c4841e2cf3271ae0fd5d4b84366b0b94274eb8cd2e6029f2befbbac025f66bd3f9e74d4a53a255dfedbb187dc90b272d02382e598cb02d1cea11647c4c4992bf8750ec8bb88a731ea561fa629d4f34fd0deed7b21f7b38bd6d2e79cec52d3cf960f9362a3658aaad598f1877da6e23ca95b224dbfb3c13e7bad2bb87325535e96de57ab3516e3f4aa9e2887146379ced341565f830e389e4f6d8dd3b14fbd7a6f3cf936f8207055ac1de5a45955ed1da12f1bb8ed0269c23195b68f8984f11916fd90cb2d1e0d7273096188561fd74a263a333158c4d1d5b7cf022bc9b24a85a3d241b57bb485495210e199fb106db12a24f85e1a2f65c4566cdc89cf40e994be6fd514a95b049c5dc72648c7102ecf33eeecbd29e1c519d9d5febf73f4fbec3f9e5f74308b695479160d31f42e95819569a0ef5686a4ac971bfd343cce8338ea7b678aed20abfadc636336625608aba589ff09c5577fcce9dbe4a549f5a244948a671481a37272d6317e526f45e6ac3d4fd21ac93f8776b6f918fe3664d5c434ca9da568787409f1a8462635ec3f97b263646dcc60fa38cadfa7ca7f39f9f5eba38463b37a975d8b3f01a762fc57bfdf9cce1c2506cb4a9b37f7f26cb2441d1de17862c4afc2bd705f324b2ce07f01fc93886a214dd6a86ee2057c24348be0685303e5eaf45ceeb867bf083b40dc4e30196f61bf9b7e98bf483040f3d565feaff47dc5b05f51574df82b806d7e0102cb805f7e016dcdd83bb3b2438c15d834bd0e0eeee92e0ee1adc99aa79fafa3e7cffaab977669ebb7e55bf734e77efbdd75e6bed7ccd1d137e5d0b759346ed230838a46e284878445804286898ff872af9ff499bf45fdbd8c2b68bff952627ece81efbdfd72b85fffbfad57fa5d90b3b3dfcd7ef23ecdedff6dfd7ff3bcb47d897f5bfd274846364a4feebfa8f6ab1ffe1fdc230e313a1169b34bc339e76f4ab366f674ece54476ded3138a3816f2d964e796fa279f2c2d0805222cfc78d8cf91385165ed17bfbed71a9b027f5cda28d2df5467d7c42aad02170c97352dc88d3cbb716ff2ecaed40e239da6aa8bb13bf6eb6b11b33c9e82b7b4ea24018aa3f6a1a1799f2afcf87d4f15972d050636f23acff9493ffa7ff872eab7b334c8e902ad570154e2aff19fe81dcf90c030952f43e42bfb487617e04c3877d067911228de70ddb747944066d9a08962fe35702d5494cf1ef982f0bfd44292b5cc982c1f42755c548278b65823d1dcfb18c8fb2ecc2dfe0a214af785375f869ced66ad18cbfc5e719e85dbe558d29b70ce9e55834e0a073090564ea6cfcff7368ffcf458b60950eeeea6a666bf1adbd0e34ba506f0828ae3268080458784428186838c8ffef0f6d71de74c0ffd67a8b77cbffabebad19ff957b52dc21f95fb98dc53d0bffb57f5f7ca1f75fb569c5373bff93e31f4c7cfb8a29e43ca294f5e4cac5e19e3f2955353a23395b58083712e155cf409f57b16c1e2315ce21d38f15b773b41f7a29829906152fce8ab71738f8cc23d27abb17e9b7fb4e87b30b29568b3330dcac362c96b9a56373df2f2f5ccb68bce9db4e6d3ff3b44522a60836c019cbe7c1c27a95185acd79f8854738f18b9cc0b4d0f5fc4fff8fadfcc43624a17cade709fe5fb6b8b56cdfd7a39694707a55eeb7405e9819e32ef6181e56b898618f5717b339e80fd88bab5eb9818fc7ab10c8f3ee9d0de1b32cb8e46846894e52f9efe7a07fabb46afea0624859d2f1bf4c1ae7425740d9407f6315314feceaca8685931ef7eb42719e825f294a181e1a5157ade559b1b7690cfdbf6799927254fc9f99f1f6905711104ca4d1d29c572046fa90fabf07d7c002e5e4a3e17c12e6b418728bdde1bf12185bfc331e3d607ef4a99b278769e2e00b020b45db68ad790646592aa0c67da6f7eac31c229fedba487d4966ad85bac5ee07840998cdf8f6534f5ed09ed890103418890755344900f1e98846a5662deaee7d3902c2dc8962d3e4fb6c3ee07520ee7c9f8c159c115dbd35f264377a21a563e7e7fbcf75fd3b74ec3124cdc2fdd05ae8f3b9bf1a33aa8a8030d2b501e3f34943e2faed132e69df9705ef21575ec0451b8f79caded313829b4a83ff3e85a695e420481120debb8fa06b69b1c4cae8c6b8424dc6fa593b85e203c60d23b0a8bb3b9838be7fea4911f710b3b1c43fd603a91d3646e410cb60f36b8ead5107e6e98d22da7b6180b8e688ea5e400bebc9f350b9f58d6ad76006c31b17107e39339fdca8fb5f2f64f7ebe28b301ad1d823be01d8a3436dc40192ef5ffe0cec1064aabaa212ed3807e0fdb041ec2dace1ba667ab622442068f30c5dfbb000f3e5fe91d5453c9379940a0e717e0ed78427ffa26e05086bfc07142b9b03cc30790632fbb0933b3faf3d8403c43f83638228ef8dcc9bc791dc6b71788f65c61c08daff5c4f8908cac0fd2331e7430f67e42cf0799066d51518dd6b2f4753f1f7c067ceff23f1f994839f563c17387f5b043eb7a010a9a76f2c4a9b47b50df20d4b1c0e10d634f57d963b194820ff89ac6230267ebbaa6cfe008c5f1a4c14c3ccf8f95ba08a6b51757378bfb580f31e10d628c30946e36a7e32b0c171d956b17991dc11a305844957108a727cd8f5f0c9ad0a94cb5b3f7f44ee3f01f3cb578813acd24ec9f8cfccbfc0a3706217c7e4e102fb8f3a79f2a565126977a0fb7dd45416e2aa75d68dd27faeb3ab619bb3be9c7b8b330b19a5f41c5d6345cf02c225db7fbf2d6f82adbe9af533de30351d3faeb19501a52cf176c22433a5a0200e4ef6e3d42f2e593aed24e0ff17ef96707133c8d249c9ce54fd1aaa2fbcfda300083fe975b5092f2caea5be7db1b298931969ab6e3704610831629bb8bc2315e619431fc45673288c7a7c405889d011de6d9572957c10f87e22767ea26688a210b0735b3224eed3832025a27a1d984b371fa0811aeed4fdcf754b9a40abc9a7181d58590de6113ee6d84f10edc0fefaca6e1b8cfcf52c3db5544898357b0b8d9cad1f101e9af9c22ff3dbacea89cdbc31d4ad50e686451a02aed0854cf48b56386a744792474315e453bf177ee600e7cfc641b8094568b6e852483be46ee97c29a35e18e8d1b3c16b674125ddc4042110bb7a73377dd3dddf02822fb5edceaed15774b811012e5e1b8935b53baee1ff0529a94b647547b92a8725300c5965a76bca54008427eefaa2fe7561a483b06dcac86e23778e5b2d05c0b8b7bbfc595e68b39daef370582d9b450ba25f487380703ba043c5f2f200cafeecbcc9713bbe25a5c35f1110d6f753547dbd0ab74a4028b9c8d038e5377addd70238700b440a7ef4474d56055ff46c50912c635ffed602c286d56c2ded0aa7577898d478bad13cf4cd598d05809bf4f7bdb268c30f995fdf0a04df7654e757c2291a8cfe73dde2b6f4a83b67b2ff137ebd65dbb14a83a947b60a10a9a37579942def02ac235abbdb3388d5a826a981f39ba594eb4ac5dd1dca238f10adf1c18c7ba1b816102ed68410c787ce28dab25dce2a2c382de97dfe310e10cfb1179692ac2cbdb3936a27c59135a726c2159d01627891bb3f2cb644ede8693ff28fb76e83328af81940b8dafce8929684a631e4cacafdb0c2d374ebf9771e80318408ea0f51c41ec55ee45f369b965568191c6900e19d4af131951974cb5632fb0b3626a2f8dabcc27720f9d343fbfe84859aff2cff47fef7197ff8a76ecb43c6ff5c673ccb32925c9f17487a87c56170bf8a9ec6170eec9f3571cb7fb8d0523073b82ef2718810ae38bd5b80f0a0d876c1e24af663af1f2a6e75e898aab5a83c0e10bf63a47c9259be1564631064e86654457a87ce210370dc6bbffa30f65ac09155c6863fe93bc275bacd2e40b8f34b0f7e7f7007d28b2c768071a783a48a3595121016e4d0a445d03364d077b35ef63f0752e47fad3b00f217a50795d55c8daf5f1419bef62ddf0a73144bde01c2f82d91cd548b837a7385b3076524bdd47b6fe66a207ef76bef3afee85d6a252c10d9c0897d79f0e570072a6a33ef932ca4592d236266f7f91a4dbd4399a1b7f7ffb9de3293fb3b2c558a95cc2cacf5408736d6af2515b87fd162641eb7fc75219d82161b1e175814843b3e02fb3fe5644f374e96eecbd0901d396aa326fb9cc108f07e8d425a9897120229146842bf3a27de61b0e56200c61b5dd5d13b1b7f399ccd0a38a9fa6c4bfdaf1feb01e12ff521abe23b57a80ccb72dc8d8122d1f2f3bc12207f3b5186ec11d7ebfc8c9997d92df3f5e97d191c2c208c8616c551490d7747f7f259156383b68fbab65e048c01f6c8ff9c3037bb119ee9d896f666c71830b53002c4f8edf397ee67c95b8668c8a3081ebddc1341cd5980bb75d872593169efd25cefaa29df8b8be7644b440e7040b275567e2463d1267baed8562a29043fa89fb500c47c47862f2c904f16c3d63e8ddc6a04c18a7c13e380b00c77aba99997d118fdedda3559b8bdbe7345ce9fee3fd7cb73f3da8d7a17e56274e1c25f2cecbd16aac900e2bae04bc05ffb9c1a84607f212846d54a4a99862000ea0dc862bf92ce4de4686d4ac85c7cce907675fc0354bf2818782586b3f91110219a663f2be94ddd9d6c01e164ddb87e8ea8bc8bc6771534ee6dcd9d6a162f57207ff92d28c4b6dde92861cd2f385a80e26a626a130208473f54da289d4d9e851d96af93b5889e9ba1b2d900c249cf6b3a5ffab7029859c7e45fee363688774bf30a00b4e8d6501cc2b72c2bc5ac23a139e223d348580708876a2554a4d04a4e9c56cb3f6546fc18ee1074370584893d542eccd665f6df17bb69e727111e875fb541e1def34c3c938ff2cc34de6f563bb1a7633242ef0c62a09c3922eaf3b3c5ccb932900b087b42c4ad4ec5078a577f284e348cbba6b95783c57ba9be010de9765100a84bf10ec2c4c392c6fe03abd944625a60c72d7b0d0ce5b308e96dc7c9e3e46363e72910617a0920def391fecf7596c0180657b7efc56cfd52f724b082da07e79b801f3ce79a481a0c958e5369aab9354d895ebebbd400202ccf7fdbff77f2b74b5fd163687b15797f14ce04021076f45227a3b94d5a372e425699376fb1a7efa09103df376577c2386b4c20f345fd0ce6bb7834dbc2ea853290dff5bc348ae67e0c870b24cdb44a19569efb93085835cf90d139acef76d3091812edcace41ddf247c500c6051e908e15d464ccc5c347833a779b8e21f2fd9e40716ea6e3a9f7bc3ce158375d95b2eefffca5b4840c10c68e64d76d27ad8d4d7234322288e0c177566fa503dfe7b6c438b53026f6b306990bf6e65ed110a13331c03117fe77d99a2e39b26eacbdb69bed08698db47006080b37a9fa68c5f26ad6e7509675a176c65ca6790680fc29d72dd2e21452afd30af6ae4f063519e2c6121510264325216f4a24b6fc1a82dd4d920f281e201c6301f237ad79addf478fa6c8c228a42f5482352def8da180753a2e77c5fb4f487572704a52e37c3f285367cf01e16f6b5cf8280f25af06f6ee3f57b40d8a79524a7c20ff26b3e58e28613f7049815bd3442e5ff6c27b7905deaf4171ce603e953b1b971cb550d231a3716ce61e20dc536d2d1f3db128b2e9fca7b502e5dd13ad502807184320856592265e896d451c4f555af4b73b9a631c00740bab645c25c6cfdf4db1ea3f678b132b0b4805db02f7736a371c36c260ca4e2c26775ff4d328bc952309509f7e527ba12454c3a6afab98872711eb9378d279028c49b671381fd875dbf56b1889216f51c6a33aeabd80fdb15fa03cb09449f12f34e955344a0a7e66908b1f005718fc4d9856dc609c1009285ce5a23255e739ab00e389c323ba3befedbfd25e6a222f84873faf4f7934006ed8bcc33a4ca9b4629599aa1fc17d7c4c9e28920ac97fae6bb640f43a6c95ef40ded897981fb99019f73802f1f35cab300b45fbdb36be5a4d9c824f9a9a46270d3d707ebcc6624e508dbf0dbd77aa3f1b8ffe2952d006e00f9d1c1d422582b1080c07ea90e28c79c1b39c9280718c985d302b6771b297cd84bcd3aa15310af6e003209cfca0b8181e153c6bb1625a543acffca52ae73331607c919eebcc33a6425c2112b03027f9ae12a2f4341a10967e46a27efd41fad23216de457474468f3bf5fd1c787e9e066b07baef6711c75f18dc3e268a6eef1e6403f98bd1670e24c83603b486592117d162f73443c143c07829101a0635c2dcb0ccb8679e0c17f71cfd7bd32490dfc67fcf88dd42e62cfb59d58452885e72c6378d04182740a1cb85bba16b4193b207ab44056ce663b42203c63c3724c3822a7f9bdbbadc653deabd58be42cc1e01c6195ff3df7ae7ef8d96c972798429091f4273558d81f8f86ff62a5c39a47fb0aad62be35bad457113773490bfa8487cb23d2baf41d147e280c8829af9d4a53302b0bf6b35c9bf530ba61bb1acdae445e77cf358cfe5078c55ac2acd7d3beeaa2f88ffde8fce1d54e979518503c2f9d665e643ad9d7baa4e998c2a9f04d4b5991563e0fca88ab91a26b5d1d9314e4af2e5187be32818c500e7b7a8794db4c86fe0ef33e6acf13bbffa372f8d74005f59f3b51b370bdeea60d93aaf9cd09a9d868018048c4f8a7fdb77ff5c46085e4b7feb8bde46d1de0e9202feff3bb67aaf0b22667ec254c59a112a99e596535ec0386afec72ab5bfc5fc003363a454cc26528105043e20fcc5baea6052d3ab2cf6afd6ed2b62737d9afcc406d4dfcec3ab8bfb82b629c6c24789bd7ea803523f81f4012298a3f8dcb8139679bad3610b9dbf60c8afed2f205cbf64a2549f1635ec2abc6ab1fa499c084dcf210808f27c42d37ff426d37c2c1421e2ab518689f0cdb0043a4e541836998fcb64c1276f3de2831d7bda622b5c807117b515cf84c11fd40e8c36630eccb44ff035b9fe40fe066f39c0e6a3139ef28e98654f8ac7a18e16aa0a302e19bc78c9a50c85bd443684b2f372c984e5189f048c3d3e23d2206e34accf62342317edbb250591ea5400f59b4cd97e02437de4069e9f173a990f0d1136560d503fdb28fcccd49fa05b26c41d5d979aecbbed877806f23302b28cf8a4846bbf7726b3e7e8bb1e2632d6b1c0fea2d7e6f44f86abe94464d791a7630d96aac6b83701f67fb2300322bbc477c1dbb727a57f2ebdd92fc3c0fddf4f125bb22f26569a715c2425de2c79fe7eea3d60cc351e935fe2148ed220738ee79d1dc0d7693ddf0ec4df8538fd375a19767b2e1f46c77ab27fc225ef4f00fc869af7e32c049b0d8faafc3b9e3a01dfc8af011e00779728f8616eec5446b00b52f42f46dfbbb78d45798003fb296e1bbac76c2e6b723f3724662828815e1834c6f9a5cca2abd9188c3944e2c483198e1935dc8ba506dc0fd88252b30b9f6a3de88e4739bffce0a7a78707344056f053d16548bc4942f89e5584db9124c8ea0b40f30bf5e9a3bbb4d024c62a251e8a654648f13783cf407c657dc45b4f0f78f41b5959b99afbf2a63f34e40918233ab9961628b59eb99f7d99dbe22ededba648ed04cedfdd6d22ffb9594583809ccd908999752baf1b06b0bf752b88e7bbb5798aedcbf79a5cc6b8bf9c86b3011d519c0e935946b9d7ea56a98c4ccad79aaf9b2b3f00e344129249ec89a1d58907642608629ab642865662c0f844601d66c5ff119a41855edaaeab4f838c9fcd1678bebaf9e8e4bb6c8f63e3a32d224ffa6d7ad8d70620bfccda0f28602fe6a06064eded5f70178fe34a3706eed7c46f630bdd950ed40c0f64418d2bf363a459b14047bb616d5603bb155bf18abdf41d57d965ae4c3715505f16e9bfc5bc50ffb9a75b243028fdf5ac1167a101d4c7a235aa57bd1f24784f2d3131797c58e8cb337d80f367c03efdd1412fb9fe2fd7efea6c0335c4dfca1580b1c3deb9c53127055957646dd3911d850f83bb110e18df67717704791e1ea679f46d429ebd0bc87ec203c68fe66a731eb93bf1f603a256c1ea318e8346a3288071dfe4cc2b194db57cd91d5c3e3b5f4f3b152e3f3b70ffc829ad57efe79947265b17bddf99089d2fd90347917121114b1bf2e090e3264b7758e29d9d1d352102f8e56eb0f4c4b687171633821db4c090efe907c97fc0fd42f41187d550b191e2875a3b7f197719bdd48b23604c723ff29cff8355b66f0dc905c559ad5cb7ab191d303ef0791aa43ceb784bfc1dc6408fe8afb9391e0a0dd4cf3aa2d239c7f8782dc8a83b6e6d0836934869d18071e687111f2fe37a651f82159292cf192bb3a9c8c8007e1c0fa9a59862858418ef17b6dd7cf19baaf08e0ea85f3bb9fef172ae9a60a9f49bf7beefffa82a7b5607e0c7737f60208a552c918a30dd631963b5f0d453f8013fed16c149d1ccbf66c985e13b3f02520a729c297380fad5a75686052bbd38a7c5c4a4f3e53303bb46a90df0ff23bea13e498e145b53dc4c73fe4a3cad600a1f04f08b9eab490de1c043f1a57ea63393de833323832f8031e6f4f30ba2b23dbc594fc4ef269ac3cfde6c5ba9407cd5a4a650c24cf223cae4d3aad295e73deb7e1b05f04781e3d23faeea7ccda6f399fafcf3c19d54b95dc0a075d72d7ee8e3b6b951cb0f5c767c67e73202bf4581f35d647d0455b9ec5422de98f7f7a8282d69267003c057d6f6e5e6c376052f73dbf046cf045291ec50240171b5d90917f683414a07336f41a111f563b4f5e92d600c70e3f04038501383fb5cf5da1084a52bc449bcf011885f19a3b5582928c48df92c32a50e84cd4f5c4240fdb74c88da23fda65a93fa655d567ab06aef1f2509703eccb9322d3f455a55d11bfad392e4ad78dae86e01f505ba26c5ce7eac5f20c2b2f8303685b6033e692110df3e1773729267b165d954905dfdb6cb167dedef05e277cee1f1bccd5f4304833e17623805b11c570923009fcc2ecb0fcfa698fee8d0fce562fd953bebf7b23fa0bd1ab44825aa282b8ac60e23354f9b89d7fce0e601e4a7e1c73f8b8de9a820f8681d22681f3c08e43c71007c7cf30186954d955b354738ed7d434df70aeb001c6028603be7aeca5fafadc3879179c7dc8195f538c10ef40758f33cb0555dec6fca6636f2688d16b89266030c00fc955116f9faef3c255101c9276c0ff3f6e0e740e0fdd3a7bc698e5086a90c35289e71b41ec1b313f900df778b9a89f7a113897307d3315fd3d307c5338408c8af68ac123a8fa4950398c5929f613f099d12e2fd03f2c3d8e4bbfc13fc69698a7bcd7764d44f98ad7a1300fe8243f53eca19660d2ed5da3c8fb5c3ab4499861e30ae74c8c6ff407f4b5d44cfc67fca94558b50f2900c2498ca69bb51634dbf3f4572fd91e3606337fb655b0be03752a74273e7506a61752f91f058261ba5262dbf81fb1bdfe186fd38d43a3d4c9691ec482420c1a4ee04308e44f9ac898db5b45e5c9bd9b5334aeb80d0684108e45fd3a686dd2fcb7dac77d7a2f66a9b337565106a008dd66492df393a5d13bbc94710717afa0b0e6f290b80ff468b5f86fb04e1a7ff54fff1ccde3ed22c9b7508186fc166d2f8d6938e2ee88449b345d5865829d1a701f8fb5290af53f51253e14e79a3eb1c41f3afa23b3aa0fef425fd6b90c12d48fbaaf9857aac6acb3b11ad1ab8ffeafe04114f6cdfaefc4d646b113e64c01a702607eacbbee17da29c1121ce138285b93494376e8c7f2bc0ef7be653d6980b11bf4ab3125ece6cfb9bc8f32e01f717d766a13fe52d84ac38b79d990f8ddf4da77b08600c42c0dacd1199071fe41d0775d49963d34295420ec437e7ef965941a89ff36ceac636d41bd06b8e4a4b3400fcd60199548bc56bcb7c05dd002502f2bdb89a03f07d6ca711c9b1cc2e50b98ab325f7a82a115de87c0046d679f76729e15338a9c5538d2fc20c0bd808d89b407f28ee67949bcab96ff661702759da9dcdeadee20b608cf714493c1f50813a92a3f1b9c635fdd1a9ccbd1ae8afa865edfa2645fff8508324af81ee9d737334b200f4d75f923ac38aed6ffb32ef17c9d5582ffe66c8f102f85cafc2a72cc99ab5040aedbe105e9577ed03043f80fd094d555478719fff79643b577af364da0977d61288ef6287a950f19479cd4addb074ce1641fe8fbf0681fd9138385c64d243ff145e87e3ef52d9747a9e8602187f53ed880f8ff3ab04fcf8a471ad1863d7581191039cbf95b6a021042cb73246742fc8c387eb278d0a088091e8cf2bab5744a8a1a484c03ff3d8b713d3834af409b85fbfe1b123baf5974f2b0f3f949f70cab774fd05f08761742d3af9118cd5e3ab279f42bcf2d37402f07e40442e29b623ee8b7cc1e9eee544331d6c332607f2678ea9a80a337609c4bdb78d9fd349237129b7c30063308869486d2c47a43ede981c61ef4a5d3fc59405c00fc3582fb70869bf19f97998141f097c52fb2bf60cf4c7604a706b3efbd31223bbb0522e53f9a731406c01f80bd66e972906eaf8219924c90baf656d6ef3b805a02399c69f80fba9f67eb8b8723919c7592a1541cc17c04786ce3d70cad2d531dae9846486371c9449b18c016369c19b8986a783dd24fbf79b9fa2afabf2844ad581fbddc090a074ab6d9979f6306f8c279c7da5428b1ed81f76c7daf6f04e5fb60476763e30e67a69159f8febfce77ac5cfd95134f6bc7604fd50171e683e5fb29273009faeda80d9244c1d0892a265d24b748cbb3ff6bb05f1e57ee38257b4ef3b052cc17de51246b89866f6c0fbbbcfddbdadb597db52dea465552092a4dd642902fabbfce1817953bb262a5fe1ce68964ad2f5cd58e980fe8f5acc8f683efafb4297daaa58c81fed9b7d50f700e5b4ccc6434fc0fd6f957c992e0dfcf244419c5d0c703e2f6d955d3f617ad09ab61e542f7c085419c96305ea8780d76f9e30aa1001a64542d4c9da9754269a5180f1527f8cc1a7b591b7f2860f78e792ecef9ccb7d0780fd6786e6446dd693c24659ca90a17039f3fae1380a3046bc8835d6e4da284a435e88df66c8376ce8d74307eaf7cd967d4f650a7f4b61bf8fec031da148b44d3f81f3a16bd162e271bcd448537b94274ac5d96794ae0af4871f0e75232e74a11f5cc9193ef78cbde77d11c505f809a429b8f9f282a55f674cfa3c1a93f4e53a501881fe4ca0babac12fa46fddcf08f8d0c38165161108e3407d8055e532f5e4eac5b6d9f874a9fb988cfe7b7e1430f63aa973f9a8e01e3ce0f85742ce578b882274d81130561c6c74e63cbe88f0e4ae39665a43e3a1a3b2dc01f8619caf7f9249fc115fe7ca379a6be1f60c86edf380fdf398a91422911fa743767e1d6b833156f5fde321f57faec30d8b04d4abf31f1be768d5f09e9bd64b493402fca92784bb869ea85d8a6198dceaacdadfe3470a6b007f40566295d4bfd14b26cfafaafcedb35be3310c1550df1adc9bffe2523914ecb8bd9968928b9524d3ec07ea5ba35ad692127ab2b4ac6b1c026c793a06668925e0fe52488ee968e5f41cdb80ca38aaac0b0b140ab205eabf762beac195001e77ebee63e20c970296641d1fa07e85ce4d0f74ddc5ea52616f88dfd619a29c97fe03280dd8fcaac2b42ede4f95cfb40f55f7cc9bb68fa802f8a594a702d52b06adf293ae96614b49a83ac9351e805ff40944e1f541334c395e59e4596e2f38cd791d01320086cd0898e53ce2202eaa8e3d42dba33d871812c0989f963fa63b464a505921cece7449cbd2ee8f0513608cfef9bdc70f5c24fcc5c4eb005decd8befdf9245aa0ff33838722c57cb5afaeb4f482672e15668330f90d30160fb97dffad595aa4de8c58c4a7a8ff82a50b2d08e8bf3b068ae2917fce26fb38608b526529f970e7a40cf4870d43c5b4f5663fc85e926f6c8d8cbb48af380602fdcb4573d452666eacb6f46a9ab028381a31b58e3ea0fe55b93e2f67835dd486d550803dca61d7eb8b7607eacfd51a7bee84cdd6d28e62de6ef2e5a83314df7f40febfb61cc3de5222e5b61b96307dab4d558a3d6405dc4fe995fd4a3bc8b7f7fa981a873ee53b38a20eb6c0fb29bff8eb96aefbfdaac7bbe02f6f5edbccf42916c01f5c8fac7b4c1fd8d085aa9b82b849116f27bc60009e6fae76be8b5dff43088da22cb1d27c489d24da67a03f69e1b93eaba5c57bd776cb252e50ef30dada4508f033e65a496c10f8481762c9087d57a824f47f14ca01f9b7eb06672217be63c18232a471d7a04e208cf539d01f244fdb883a7eb9474e63367cd6b69d1cb3e83d00f07b3d7dfee89285995d5a16194b9a6d232e3596efcc407f7a238911365c94e4e223a4307a4d2d1a9c5e37801f9956d6903f2b8b59a9de4c9ce8567fac1ee04e000643dc3474f355a12bce53607f897fa1160a5cff9d040ca68897094a1aa4f8fb4bbd026fbe7c2101222ad01fc04727b21a46919514664ad9a046f0e3fb4ad8e679017c5474c380116617736f5eeff76fd3847c0c0ba116e0fe7cc9dda061f345b27c2f5ebfb979ecbb9ec7a106f0e7fc45fae61f67c2a71bc34a4ae5b93703c474dc01e7c8959636537b58fb49fdbef3a4cabae29b1ef933c0b83f3b5fe528409c156a7f959fbb5be025e7a74ba804b03fa8d98f9a76be92f4ab551f6b6f0942b7f18a03f98965d6f4f5dc8f7b1271686cb90b6bba5b0d3723a03e6476e8cb6c0d3286237b105f83ae921fdf458f02f2abb79075aedb4601af3f442d41a234766aca863140ff0f32390a8561f944c65d0b8f064502f2bbd8551510df96257962cebb9b3525a9d6e560dbd7681713d1017c4d85762126b0212142d1af359806f9701ce68721c06f79d5f2a3bb3cf5e6de1465b49add72386849f204ee4f3fb7f2ce44d184b7d2d2d9eecd1fec8359b6a2407f91f7d1d4cb80760a66dae37953babf332c7f290ec0d7a8f9140ba74977f8df717c6868a1563a52cad007f8a7135612961fb8a3a1c882aa457233dafd67621881fcea589d84863faf3823cb45c958bc23a0f283292a10df10cb1eaaf7fd6fb6fa3854c8ee143f3c917d6903f00f13b45a5fa5008bc6c38d9093063909fa18174a203ffa37e9762cf99133bf385dc7ebc8099a4c7e5b10e8df5e3b761b9286791caad0c193e9d4d2d95cff5202f0f3a304b60657c3528eb969d8d0a8387942087f75805f69fff173c818dde6345fcf77e9d7c6079ac4252c20ff4216f119536e9c7c795b82a35dd50f2c695f3004e2b3f5aad12793dcd2fc782a22a369d6fa044c7704a0fef8b62d8b653dea7c9922fd6e2b173d5f7b2e241c883fdd553bb444ef51da67537d778c0dff297e0b5824077e1fc083e548f72fdb8f02cae4bb43dfca978324607010ac7a28a3ed9b0fdd979c6de7929a3b2a283207600458f1379ae6f5d4686ee7930b25685528f7fdc15da03f876042424bcb84c6f2777133639e4749b124440fc8bfcc02bf3997dfaa2fe575358cfe151d1e8351cf07ceaf1cccc603be74f7ee7ab19fb34c547a4d4f490d20598912793f803f90905c2cd3f36732684c8331530dc8dfe6b209fcae8e4370695d28095a55ac1fb878dc01feee94f1be6f72a4e10cda281e3d23fe4d7e63b92fd03f649f825e6b1cc8a6a13b4f4d10f639bdb86db806f0adef37454610818f91497c58dd1e19e46152d52300bf66d056804350bfd8a928e071f09c556624cc9411c00fbc130d0dd69fe8d189037dc217d81872b566a601e373b64fdf2d8f52b0aa24f72f1a9d1613cc981a0a01fc637a1605c1cc8d7a7c77c5d9038ebefa46efb51bd85fc79ff496040ed93e4fca58347c4365e6d1f06807c439b05375d1bb98263edacd171bc5f24c9a6cf8e05c1353328fe71e4896a3f0b43faf6a9ce1142fdc2900ffe39a00425b487bb09354923c4c9530486619aa04b8ffa8f5bfdec8b188f07c2949bef1fc74c133e1530e787b9055a518991844074ecd42b9780a84e129476502efe78375f603cdab9834c7dec6f0771a435c1f2a36603049956bbda92e0cc5a7296aa24933ed985c83660fe0fc3740251f1d69cee90a34620c297591f9b8c90d03fcf1a4b2be7785a5363d82c8a80c7b588933e70bd880d4d6603fa89a8c4dc6463b723cdc9dffe403dac21d909fd6056d7166b27dc514804971f52184d1365e9d00f24b64f9dbb285fcfabe963ce5ece022a2b10ec42b009f728d64e31cf9fb57e81f5592c9bde68651d9080570bffdfc4460ec422d32930dc57a59331e168e523a050cae42716718822c2af0b736bf8051e3cefa1a901006f0e7cc37a02f3dafa689d0f2b3b60b8f94ae24c76f80ef8766d81554a07395ba006f8aaf4d24838cb72a08d467699b419fabbbd3484f37509f59d3718f177a28017cabb5a8fb036965a3dd0d37e512b10ed674c5870720be6da8768c7b76f7447d7d9512a67f7f6ae0ae6f06e8077c0ddfe3ff38f8f0c0a2a525b679540631b7b80af00bdb613389e859b8dbb90b088e654ca5a49da00c81fac8bf9185cfc03d9e22fdea41684e287ea6481711e89fb8a246255e76f22ad2f4bef324e17cb393a28106ea4b01cff9e94dc5e098e507e1f206e11dde873c7920be072cce0c50fe93dc17a771b1e5f85a4e40b8cb04e06b9679ac9c5a2e1352f533efc243a8debf95b7b802e7f3cf6dce2f28484967677f970f0f118478b7e73f017cdac29baccab75c698dc52b5cac040b6911b90819c8ffa270b346838e8bad3b262c21c747a7eed0782981fa4b2a924e1e7a6631439571f281ecdedbf0fdc741403c87199c7518bd4f65dc9a919da46a68a0afc0880ff44724c48254f13e669e0f6486458fbec32bf663bed003f447f8bf5f702093d642f52699ed545bd36671a3017c90964a8008cfdf93f8ea3623eeeff2729f34ca24c0df211945bcbeaf8d3fc7bcb45f55972344988fb105f0b92a6b53631ada2d752675cfd02557efd6c97811a03fa287c156962873f1d12119268ac5bb9e5cb65601e87fb418253c20d1e97af1360e11beaf5961bbca4d07f2530486ddf16388145d54fe9126f60741731c3455c0d87eb47c2be6676ed477dad99c863beb375391992a805fa0f7370893131a5d5c7c6aff091f630473d28e10f0a8454927bc510ba93fff36973592f155f226fd1736d01f8649ed5290cb52c9e35420f09dd395293df95806dc1fc2bfec857de44a875c6243b6177cade9b3bcfc01fd8a72f41f22833ca827cc7ddf212e2a362aad4e2920be29347ec6c2749b0be9857aaf9288f8878ec04418e0b7340edb655987712ffcbd5b784b6c6b46a7f86206ec1fb6dd28214c3ac1283d127fd65ec3a83a2ca361a0fe2dce090d70c511a2c98b24fa90167da3a5bd4703d40f3e7e843b3684cbd17a6b48690dc733ca5bf2cbc060be8fd1ccf4ceaabd67ade24e7fe027137aa80298012b13b75de42f72f85cf54e550a58e25ac86f3e9f5e01fc3f13d9190d6584adce07a6dc77b1cdfb98a32811e83f37ec841e7e8033382e8eacf94a366983e6722bcd04e037b7c18e34c13e4f220c3e3b15e45b6c9e1edbc0609da94737ff0024c313fd37b1c797c805a7d6b52f803e8b37153d527b136b78c25e943fbcce470ec7a202d85fa87fed795c6dc41e0b690b02676fa130c728be01eba1d2415fb1e1e69569ff95763a183d2443198801fcfcdd1c8f16331fafd2046bf3aba6971005945b2ae0fef29c821094b70ad0a73eb6375fed955978543d04e2ebea0cc3a57aa49c077c443649d77593fc5b7901c00f322d320fd4ec8858780ec37d37ab852e1a046b04f02b3d99f5633dbc958dd1246b39e3e12eb022ed3981c13835474adfd9f2c3aaa69c56a6e433962cc7ce3980fcf302692136616d21e51e36e4298b3bab66ed8510e07746a6f646c5e17084422b352a5cc01cb56025ca03bfbf0adabdac2530686a22f00c13a94b52d2cdd203f0919e2a23c93ecc08abc0f75bfda5b40cb0789cacc0fe15a637d13236222156ccd5bb2f0eb76814e3e705061790369ede08c10721890593eb8f661b4a6ad36c02830d51c51111191423944d4a2cf4e5fb9ae6b3be1581dea9d669c2f0677f02b36d5df5b0992f0e66ddfe01fc47f4635fb7161735cf1794aa4bd48ddef8b8de1800df1013cdaf7d6442548be26d908629930ad44ff9067c9f7b1534878732a6cf15428bb8e22b5b17163d0900bfb146c4a06334f90e9646b947f3b2dd028e7200803f20e00284577592ae0fcd5f1ff05fa336e676d40380fe41c6877066f4aba1efa124f73c889ca8abe7530c003f041a6e1cd212912d0aafb968bcca6e81f7336e196004822901bde7961efc1db2db87e6a2bae31d74ff2d507f73c29441d542b19d39afc69beb56df2cd504210083894afdf924e7952af6ab95ff6d081838eb8fd02200fcc8bd96c169272f991b09e5a9467e85c122b6c97980df7bb0f34ba41f9f25c7564040544775aa7a393613c0a7d2a99e2a866626de25fcb9a6f9f855bc765bb10da80f14a4bca052e577e0938f5deffda90a056c16f8007e94e058c19018ebb3ae16058d6f17f6ae7e260f0670bf1e1859b48659ad2a49d0d0e28cafe97fe4bc2806f45f4924ca11d2a9de23841dda02ab3130983c3e7a407f3a602b9a26e1c3b8e8c0e5d0a21deab995290538f8c4fea85da0ba88325f2873ff7e1abe7eeea45913182cb966f1ad5a4341d382d3361b8fedc1f268a88e1dc8cfbcd4c91b50f0941c7ca9f6211fbb1b2443a5c581ef33bd6a4408e5fb8ae5d8f435ef5d44a5218fa124505f71738bfa211d4ac8850fa6e7bab3a988deff6201f6672aecd099a4429ed9174d84f9485fbaa39e9b8fa0feea33ae71a995dedbe0d111ce523f0574411e35703e895cf4f30d5ec5be35a5d9b0fefe3cb279486306e05fd8c56d07db1709096f730c81d6e8cbd4e90948f2007f0a5f2e7d2aa9b48be487769b3f160a8eed1b2fd09f19d07324a095fb219edea6a6d7a9f46d20766f1cb85f7a1dc21772e6a27b59063b54e4054c655ee139017e47b886e91ba571a207d77c76cfc33f84b2fe244e00df1fb16023d70de8e8d1b2c16842aff52c6ea86100fa9f27f897216131d5ddc2eabeb19eee53cc75694300bf42ec64d7bf4a852811a7b598f66366e7a1886e17585f8878b4cf582b1fd88414e15e26f54c68e29202f773bb6ef535db67795be8679b61bebe2bc70cc15ec09124c480b3d8926fb86378c2e4a7245dafc3b6eb17805fc026295b29fcc72da9bf39dbb0ba7d7ee9fe6e0c180c74c78172e311bc036f4968a382334546669ae60cf0bf1bbe7df85768953c1b99ddd72a560a89d82ea600e8c3a9b9e38a6abd529d7147e2cbf0a31a02b1cfd880fbf5a8f53dc5d69e0b14d94c482eb7cbbdc3e3bb3b203f18f3ee1150460e5185e99974baba2c705c526701faa79613ad0733c305aac4c1365f0deef5e8edce3e01fd53546791360bd4a8d9378dfa5f7ab0b6dd051a1b407f302f3b35de1b2557f18a5d49e0cbaf1d9aa17787003f039d0cc7e793b2842f8500fdcf441a5c79334f76c02e2070ce8bf118973573fe9626bc23fb9b5773b714804fc18e4a3e39601aa3683035928c7ea43c247f8a9707bf7f071b9cff5a0eb2ebd3aec39c21e7d5ec1c90ff37c1db952221883aab902cd2ec45f422fc7a5602f451f65e3bd38d75aa0c10b2463bac9718a38104db00be12e15be379d5a7d475e8d8cde59077514033450f0cb68afef81549d919b16a1da27c815bb53236546106a8cf64cb7477c26e45548ef5acae066b8f15205dee81f850acc992658d3b04fdfdb9bf4ca060b69dd7b607b8bfdba5eb2eb0ce2b050278d755f73116c427783680fe7ff2e17da8b76dd994300aee013ede06eb4cc602c06f892f4658b717ad3e602f2e562b1c773582f7d50006ef5297e8d148c82d04702232ca263f6ecd5ec84a03f8b4b9f9525a50a79e8d1ef3dfc248c4d0c7d5aa4c805f740629dcf82a37f4512e56a6bd152ff7f55a8505c00fb0367a2a776c9d5d58735e59370e1a84c954b000fe133449bf4cdeee015980129222d3499b3691b401707ee5f8e2ae832462aac324ec48f8509ec363f7b1007da5a42dc5a2fc0ffce4fa4e213452b7d65449930a60604a4addf169f093277a0ce23b495caf586f425642009f2cd66b6cca1e818295bccc5d8e89b4388cdd6300fa73872b572610bbc76d2fab46852aaeb0dea4fd8b007eb579e0416645d997e60e334df1be133d45102e14e03f3d7c15ed9cab4129fe52ab5ceacd8869d0649f04dcafc567d14504fa04da517035344e11910d0cead6c0fb0d852e5d15ee71edcb27ebfce1fd1b29e42e1d0fa85f64a708bdb814dc141efe40606faee3127f7a5605ea6bb9c200b82a46a8b66719628dbe37f8aee43e5880ff5e8bc218082dc4b19b2587af6476e343a7b8862004e0f75a10790f95eeeb58fd684ed198460e010f7f007c3b7d652b9ae091a022668bbac0efdf390c16770c10bf1bb889180413029a9f2f8e972148cc03d1f49181fe8dbcf0a5ce98ede95f9ac054661b0b8113fb5a5c4e403fd2b3a65c6ace3ec71832d004dbb74c692130c9f69feb78b28732918d3331185ff9f3fea8a8750c7f4302069b87f054726016c51ae5969cdc2738f8dbc4045203dfbf8d5744ad92949bbbdffcdd69b2029662c6a022d0ffb4711b6d55dc566abfb88c2fcd1154f22084f900e207ed7505a78b48fd2fb54c2ca8fd142b3abd8800bfe58e8e7e56928f4bfb6a464d630c1e9f10f76507d06f2d2fe5da4238dcbc4de45e0a3962451fb74e7603faa346d225449ee0dcde0c25f82f6cf7a7f3ac5f61017d9f4a4783cb2d7a11d15a4897235bb9f78dd9c314802fe7335249cbef0d8be1e2234cd4478b2f4369da0000e4ce12f4c7d362d15ecec2a1239db339d33d385db03fc7ab21ced91857c78f971ce747fb44a27ec10ae8c75de3de9f184a9b7c5c3f39d73555d9a9fabd960be81728726d877d19dc826ebae53ae3a11c717f5d2302f5c7bb1649f42019b808ecf3a22c2958e6eb9e977ce0f7b6d042bd4436f614b04cd48fded481aab96943407d77e71aded588cb75aaad14143726b99cb5ff1913e8cfa9b6439c65c1b4dad10ec671089466b7b78aff01fa0788b841bc8f2766ce92739ed738d79377c9e47880791073b6de47925f26cdf56e05957c747472adc9c6003f07f592ecbb02a925d57cf8748de06755192a9238409f792458c7e0ac56fae172be8e0fc5b8ac53a49802a8eff1a5d97e04f45d6a219c3ea5e85e2d0eea12e001f5fbe85ed19faf8edd78d8da213f606b389ccca2b581fa5aa7724d437cdf56658fe00f43ce9cb14f2eba06c08f1f8347723816eec943d59e279850d6a9d0133100f2338a589aa530175fffd7cc739c931dc5faf3296620bff0a3343513f691cdc249fb08b594475775db1408f4a7b0057564f4614ed302aba47ee770a9cccf7d79e5ffcf750d9c08daa3dfbaa49f4659aa105d5653ec8be5017e2f2b760d51e68cb2b017ac608953a19c5a0de23980dfdc492517c49a3454fc7eb4ec4d5225226fdd5e04f879049dae236224d2e49ed171638aac9285018e3a007fdae9fd01eadbe3c6a255ef08d4f38a59e109d60fa0bf2c975674e5ac51da1a76ca976ce88d894738f708e427f60f2f7e0daa1e842fc1b3f7eceea8a7082c9d408198bc4b6b511996399d97795e5b80624ee4859208dc7fd0d2f1bc5b7506d71788a95f2b8fd2a6af5e3081fab381f78959193e1c39eea20361080a8bfb22dc12887fddfa0170ae73eb037a1c160e44c49c4ae7ec0140fd90bd8c95c5e07b11f3b68c3e9f7a641b8ce7ee04f477cd999004bb91bd8f0dba26264c8259ef937ad380febdf73a11d16a24ec40beebb78d7e3b68c6083b1de0fe238b97363c1469a580293b0932a1977093654604facb4eaa0a08bdf94878a2291b12cac75faaf342b880c199860e9cc4c385125b7af7dd03bfe32fc51ebae580c1d0bec7b86d113fe1b94574a3c3644e8f638f4a2e007e1393cdc25c238635d64f932f1098266a5fd7d531017cf2a7eb4fc20a07321a6eb5773ffab47d963eac7f02f23fd814455bbc6e2886d3740dee3f4d8a9b4c693340ffeea0aa7c491e0fedea9feee584137ee0f1b0041580ef1bbb79153df74d87e5b70b2c198c338a13089202fd95c6a26031c49b93a89e4b79597e4cfe5961f775207e7146f0e3147446d3264a3d90a71a3fe547a97500fb83c043cbebeb127d224a39b4c199e59950dcea1d802fe7e7d0a33b685e4550a3d6c6adc74ff83fa59803faa6d71663cdc9d83c5d76f6115945b49dcdded31cc05f222c1623009f96a668495f423bb4c39155bb701de8bff34d7dcefd21c64252ecb64314a66bd6d7b1fd13180c5e12b5115e853da8fa3ca92f20a828bf6d78040de437704fe76607b41b57a66dd36d1bf843cc3bd8d6c0fd2c56b94fbe58f2fc50617f2ffb61aee3de8de5a506ecbfd39466aa41652ab2df7cf790d9a2c822a700de4fc76f550984ae435b94e7890cef9998daf8f3bf80bf48971a8ccdba7e72b20aea9b6b5704227a5aa00db0ff475cbfc7c861fa345cafc945beba26cc0c307e079e3f5bf7be52958a2f215c0e6a938e0d77e128241de03712f6f66a6b339cc01748760a043a9467c4438f03f89455bf34dc9fa3bd8efec1a799af616bb2c70be400ff384f5ee7b26ad4a13d70313c9ce3b2ad4558d202c01f693618be7858e0e83f5a185af5dd42cb77cad900f5e3b266986a21756415ea1cd2239511ca4d10f930d03f205d3ae1cdd5ab0a280fec9bbefdc0fd7b3b3d0ae83fb7d6c71fe45b714356d135430b10e8683a788f03f92115b6464ffd461a6ff18397cf69c8653dcdde23103ff464d1e6610a12318e034c11cf6cb0d80e83d681fcb34dfc22e05ffe0c0399d15f0cbb4f6396a55f31017cdc7c7ecf6220ae519bcd88e46aead7eed4bdaf2b70ff6f50852827ce708d1cd8bbb2254040c507690d01e67e36c20d45fe4ae971bfabd864dd92f76eeb233800fe8476d559776d84eb4d7238ebec1291dd4e8e2b34c07f0d90ea52ff8692a6c07728dbac7f3c69d37f150ec4ff378a075a2e72358cb40da1c43bab32eaef7f6c81f325440e69c43fba4b23cfae8854ed2c270eef6606e8e708f99337f78e6dd92b27dc513ac2a789e01b6781f713e64b644952e562c9b15d8c26d227c40d916e0bd4970c1304d92166cd155f4775745f7872e0d470e05dfe73bd99928c92e3785c6edb504c3907d5b308a3fc08189c7931b5e73015345a6e8056b00bdbff8254988703f0cfb96c08b3c6a6d8501d5bef9962b343a64f61bcca407d9e15b5ec51efa1b5828d31d35bb122f1a920509f6e09c422addb91615c5e6aad4d5fe51ef436f901fe49a182e417592af49aa57f2ab1f7f2bb82255a3a01fcbccc744922f45ddd1815e223dbd212bf8caa4b26105f0ce59ab249affdec6c6617b810f70b6cfb323b01fe9c21224db5364e865d9f07f3a3f1c6f7f31e84df96c0fdd031fc066d48b028ebd22d2c14ec9a82c74d08c49faf3a14b0f34f9adbc17c712ded755b049fd4a700fd33325f6f348735fe3a49c0c6247fedb9c7f6ea3da09ff08c4d749d15d955b83e2ee1f0505bf467ab2a04ee67a367cc189a31a26751ad1fbf31f2df4a982a0280fd1da25c99d91c2886cf8a847c8bb465a03f0a7906e08bbed6fc5fefb6286246e5dd68f67fc8c3f2f2ec03fcc4949464740664f1d10979ec1a26b9e66a035367c07fee2ea107b3dbb991e5c3130734b9a720ac223378fe62b4ec864c1eb9cbe5b3356dc462e7936fdb3f02f533749237fd276c0b735add5f472767df7b6555c681fca2c3d7d1c722f4dead5a8185df725fc69ea2de1218bc6a2520684af9b66d8bf4b7332c6f79b44d0a8a09f007eb63417f3f48b83a95a6a10dd91bef42b32f2505f053376b435a7173be276a8897c6f069bcee3d0f4e02fe415a3c0c4ada258357d01501cb5f59e331a5047580fa20333d2fec95a19242a6f62db4a55cdbeffa3c14c08f58a18e521cbd9b7da2cedb535c6e7e911d1fb502f8c0f30893fe9ca00c99f89d4b30d7e676d53fa46300bfc0ed879c78accc54a58d90ad429ca00e92b32203f0311f4883bf9dada378537ed962673aff9494fe2900f16ba33659ee26616744e6c52fcd51bdf0e5a4dd0e787e91eeb5b0b63e55280ffcd79a6b0f8ccd63ae48207e27e94a0a7cacdf4164903ed178dd680ba8c3bd06f21bbf9f5c69a66b8731750b3e0f30b41f961e4e7000ff8347cfa0505d575cac9c016572e2e4948e7fb1eb00ff0a156621c4ef8a87651cfd8c90e36bd500e7071ea0bfecdb058f5b349718dbdd5cc7edc6c1e5e2cadc0ae48f0915ccfce1f3bf45fd7598deedf0775e7124c902fe976664c906fff01c88a37793d091a698cd43a6e601fd646cbb40bd28a72cf312d63bfe2f414dd7ec1afa00bf3917eecbaf59e8cbd1489fb8e4541a694716886520fe7dbf0e5d2b5ac1aebd189fe53be7db1ace506b03deaf4a523ed1bbd7cfa1aefa8deb034b5a95ff7acf01fecf6cbe517be09f6226a4f39a1b16050affb61a0dc01ff24e66aee6e78ec5b8e341aff868a953b694ce30103fc6d06f243a2592762717e7e90f75b0298f3f3203f891fef29ecd331211eb08fed245f921f94f3d066f20bf9c53a7fcd410114c8571b721ad1dbdde794eda07e0c37f17f3058be32b58ff1d3429455bcc668e607103f589aac2e48172682cac50a983f52fe8683ecc386e00ff6f3d8af6b6e848ebb54e2465520ff950f0a12717c0df59e01cc7e443a6ce2f91169eef39b06f74288f81c1f9f1d1cb6ac5dcb38510e571a3a34d4156e5b86e40fe3049398771d6578fb0387be44ff35b2121d3391f787ea3ca2941a97753459397e6f21f7fa334f284a602fa4636c476cc5f01541b892369e97fd423a731503701fdfb29abf5fa7dab3a6e3896c0fcfe7b6b2f5f4d70f0b8d2ab9cc60b73879eca598f9f03efba397a0f1bb0ff3b64a1e86891aa6c9825bd72e984d22a6d5688c0fe8593b20bbc198254d30cc137150e9cdb702b45e0fbce7e343185e5198a93ea52f0bd3edede6d68d506e2f72be1f8974a5f529862b132a599498517657737007fb063c28162f226d0e595c9f8e9c7eb22d2396406ee7f6c0bc81fd69dfc885d7482ea861c8999cb5980ffdbeb7a4cb25629b57ba64f11acb2c129d5e3a91dd0ffa2c3663a95cec6faaef38a8d1c7a45f52e5a238be47fd1173c2868757d9c28f0cb47a30c62f861ec0cc467e86583322bf111238d5af74ff3eff1112b056080f88ef8768d42b19946d832a5dbaafef39ea1b4070788cfe1ee79b722c726ce768a8f214ba6b277a5a91180fec7ba0e57c095ec7c60b66c5329109f75c4fcbb33e07fd0ba5a0823176101d1218e5db3198824fba03001c4a719c79f0a010221b142f0b89175ccf2bf5ed484017ed63302546a79974567a82abb3152b2c870c7bf34207ffc7e1a8c93ae317d39f060a60b2d7e406606930af8cf6296beaf50bfa9371a70f330ba703963ecba3505f08f4e169b877b3f66c55f66229b79e81a9c3502d280be39df59582ae6f7373808e66f30da3bdb99891470d260fca5da51d08f8526299ec1b9afd355fa49bdd401dc1f71496f0e96e246ab7be5d9a254f9829cb77280bfe3c827e9b11fa1cb05075b055c3f294220064c09017fb2a82951426537a112e9047e68ad0f13d1c2f28140fcb1ce6da50c679991d89671df8e185ed9a00eb106fa3fbd61e77a399305e1268ceedb2d9ff214ad49a080fd8181473b5f1cde873fbdf833a813b245f677e830802faa5c1852183caf56624b9336ffc0d3558447c200f0c9a1f5830ba3c4ddf19fb658c9b92a11014e1df0007f7d3f34c4a18bb87ffb398c4e4d6ed2a3b6e8197507a82fb13c96e3a093358b1d908a5a1e2cabd945be01fad9b1c6e937fd44aeec32883bfea891c7d0db280d401f2f41f23b8beb8f7bc5adbcdd9f9406e1aff13f3f02feaada275326d16509c11ab0e2ef4544ea4ad0bd9f017ecc1d9a611bc6324b36b6b982910ac999f06b6312c06fd249e648c7d9cb33c2fa5433fca9e7831b42e026c03ffcdd13e64a6b23694931fff00055f391b8731813d09faeda872ba0bc56b190d772331970f2204eaea800f9ddcc455e048b9e58a3c2a02823e6dea93b464f1e802fe3ac07b9a243e91c42720ba622fdf9b4a7134502f427a35b5be9f6aecf8b3363beb046ffbc59b5861305ee87fd11436737b882483f5e4ea193ca3cfaf14a12c01f25d7ad40a416691b86f13304b3322f032b568c1c70fed08b9aa8161ebf6cd1a85dbd0dac24ac224cf602fc0ef2a9422c3e65c24eee3f4621e684f7e9e7160a40fd0de5325e96ced2a2d8f7cf0135c022724be8e70c809fee4b59cdc44e429dbe05b6751c2464a48b4c3801fcfda969ce433b910aa6ec4cfeee2576cc4411ac0500ff234f6d615d9f5d4462c023ff0913a057ee5cca0ae867a237ec19b78d3ac6dd97170ac9a90fe9ea137c80f31fd065c2bc363893301f19947d16bf755ba8b10a7c3fb403089ef6ed7ae6c5744a6804db0b83699d5ba0fe1e67e216fef07bacfc712999ee88c3f40beff726801f9898865816b80e5943f3e9be2bccf7aa6f9d971de8bffc4bb51034ab7a900ed7fbd623c43f37f7a72615d027de59d3c017f91d2b769f300866251bb4a938720163c4c29519beaf1e20496f96cd268df2a1bdabdaa402f88556bc7612ad1a780d1d157ea2f44f7d7038bf7681fe9c7f7cb1cf34d1e5e1c626ed6e9ddcc6ccbeb30ca0ef59a6589e1af39f18e49b3ff9354c51fb8da7e218e097c8c3abb811a98473ae733867ced152f40c375f9700fc8a810da6fdd393695b9ee8159b2b9410893d4c20bfce2fb38c90887a8b8220f3b9e0fb98fb546efe08c45fa12d6bbcd098f56e5f1c2f376d04bc15fc6e52a07e4fc1e2fc1ec5405c6eec91b129dfbe72361a1804f4bf66b5af5450c210f54c7be8bb52c5bed48e502300f5773cf473a6703ea33f3fd7df412b4cac0f8b35fb407ed698bc3f71fe45703f12dbfdebd31329f7076d5a80df75445aaa21505fc0072bd57b93b9fa1dbdfa0b0a509fadbc9fe0fb1319aa9cb79ec0bcc84bc31897d007f01b667c9af768977336f86927f7d4a20755c4691281efdf8a451f3198ed2a417569e0e748faf7e373f618c0ff15cc7aabbb176b30aa08ae5de28d6ccbdf608604f8c5dbec7978ac6db2e893190a7112e76ca6f2aa2c6f407d96ac50e2b90be1344bf75c999dcfee6521ec06c4a7e6ca19ad7cb22d3c4f3ae6623437ad54d15131e0f9ffacb00591e4958fadceaa48946b95f4ad22c501fbe7179d747154a0e2689cad537a89f1ae96e58e0b90ff433be61785102c65db415bf041985f7dcca45203e2834286be69214f62f600513865d745df7d5d991a80ff7194dbd307aac307aadc93a70ba2f7558cba6102f1416080075ef09b0107ad2c7d06ecb325416eda3570ff45117c5fcf1adc4af610bf563b9d8463e0396d06decf8d4bc1e95caf0e33122143d82d4aa0b4f5d131105fa9bc3ba1fc35e673021933d32c9b3a0f294b3d80fc8100295f524e776e50e5330ce1c0bc50990de53c10dfe2b25a4fd9d1c6625f2d91a84ef22ee9d9133c81fc9595e0d248320d597c4354793ecc4e528a149b03b8dfd77ef4115f4d6bb185e72f94df2140d6dd20bb00f1ef9e9ba4d68c428bb0a67d6589a1216e2140b704d8ff6f97a18a6acacff224a52db444c5a20319bd8a407e86f46d5588f5f31f5683bde6b613abb17df1b41c80bfe05b486d9d18652b01d9f4896368f2a9a111ca06e07fde66d42a95ab0c22d2b9c76cec9d5044e37f4506fa53d0476f0a2be8329177d35271bd156342e5e7dac0f9edd688d1a65052e17676b15713ef9f0a22222006f8fde684f8abb8e63f840a65312ab131fc21ecedcc007c2e098e9f58d5bf29e3da5d91517474d43c943601e82f5c6084dcdf3b4f78dabeffbfd8fb0ba0be966c7b1cc73d38040beec1dddd1234b83b0408eeeeee160224b813dcdd2db83b0408920482438020ff9a796fbe6ffacddc9bb99399fa4ffdea9daa24756f7f8ef5e9debdf6da6bef4632d9c84ca0cf4b7103e21fca6299eeb67d5a3ad50f6b1c842a3705ae3ea8407c6096ce2af8eacd2bf51cd8fae886acb7870e829e407c2f5ea4fd47b92fdd32a7ffb690984e0b96cf5302203e3de4477c4edc79378904f516fed4aa0987c6fe01e0e71ea2ec3c937c2e6ff719a5426fdae422661e6d4703fee12ba9ade3478bebd7aa83c6019cdf7c66f25701fd37a472ced4fa77913ca3f98f346ddb0234e19d2ac0fcd188fb8411bbc32dc174152eef16088d6d41c80fd48f4898081e8f8e38343fb92b1dd02be3e7bc883a04f4ddbdeebcf7349b23eb7c715470e5366106c11a3080ff57fb8e3e6762382414633b6417d9567e7a8e9c05d037c27e19943396af7a2af7e9cd4b46925043bf9713807fefc07f57d22c1f5e15efd840953fc67a97fc4d13581f4c053894c3f7a82abee15b0d995d1c84bb9e0700e3732148171abe282b8535e6f5d4c4871b4ca46406c0bf31feb6d56348fb54928396ce2bdfa3f05925791a609f4cd44c1b6bd54d3e06def91bdedfb2d572a02702f5070abe7c56cb86406855fab623263fd652921df312b0ef3c1b4e751b9695cbe18dcd76d7af160ffb59a700fb19ae7f3b1cf4984d193bb3b23b3b5cbe27158a1a882f197179b88b52fc987580f7e5c27cfa51e8e8a510d07f5c780403f266a8f86db77e108ea9c925ef3e6402f1554f07fd2c5d233f6f19ea1df6c4215ba4976a5b003e837e329eed77211b19af5c8f1a5dce21c2b4a70ce4f7e0fa5a2428a584f6124fa3d11c7571f55e331a02fa954f26f019f4b852770564087c8793e189fe29f580be747849c91087f3d3651c95e7bdaf9b0423cede11a0cf52f2deac98b02327c3b50b274b390a899f5e6c00be3fe7f3e980108ea8c068f2e98bb5e716bd13282ac0fc30a97b5623fed143b0bc5844dc1e8b5c18dd3715d07f7ad73d866af06750119746279fbae4107b8c590cf8d77d387ea8f56d59c42ac13bb14ce8b47142f3e740fcb0e2fe936ef6828d54afcd858d40a9e37741dd22207e33f95cab4c5c06176ed68f794a73a969324c6a09d0af951b5e3c6ec6bef3f7fe30da761b9bdb455e6500e80feb1bc9e8379037875371166d2dbd298a9df8a781fe693b71efbe5274fa12870ca116883fa14cddb802f0b31514a5cd497007a626992c159af9f1aef26e6580ff1b183efd4a4a2b8d22cba7e830df586cb06b6e01c0bf07ee262b8f8ad2cf028b7ba71b356bab33f189017d588b791a07f532e93475cec6ac84d0a1866ae6d234a8af1adf3992436e72810cb9e65da973fe98f10d58df5cdcaa555a2bfd0aa46e4611a38cad7558233f01fac1b408f7b247cc1b8705c339f147bebdcb933042407ee4e8aa5052f305699734271fcff5e118d231033910df6f51161b6436a0e4c4e6f65884ad69afd9a31304f85db488f07948f20883cfa89dd76903a1d7957c1600be7fb45f7712d8f763df46c6880cd5aa3f717b5e17c83ff928b7ec21283e17b8e271b526bf232e7883be0fc487209eb6714b5da58596b45d37ba698d668c07c702ebf74ea1a8b9413b3196bfc5f0adcfecce406c6907c03fb92daedfe8c1d6d6b0431115b4d72ced4b3af702fe7ff6cc8b1105e9e3cecacf1ff2dfdd6f3788042b03fc991541b3de0061ff784cd1144efefdc2ce3bf32e9abf6ea77c5dcec6e51d5f3440d35b8a35c7e9f40c6a15d07714665258a38ae5a5efe71a32979b267c5e938504fc83bb099641ada577ed1baccf8d531f75a77bf75301fae8606fa81fef291912c68ac6df2b93567c3e60fc00d87f732d583129d640c11fee43d2a44a35b1e628c13420ff488d8522d0437091f486e016db4b34b50e17e06745ad0202064fb7bf27bf88303878c3f781d8e708d04fcbe03308e86790ea7fd0e8f908954f1dc50a3b0df0eb5da76d5f3567b773a4f02a4edd58e5da3bd943007d3ac7e612cf71c767bf69ed6ec3c16d2f960a487860fe5486b912f7a50d31eb16f0b3681eac412246dd02fae5d72c1196a890456e7bc345f81d50166e17637b80fe631b815e53a26bb84bd6abf38546552a41b6ac14a0dfe4185b3ce1d7531f9ac794d526ab24890bd53d01ea93924073e23368dbbb4a76a0a38aa9f006d3622302feb3e8992fcb1c0e15c5676c957a430ba454adc256007fbaeb0d922a30165bcac1e81ba9197f912a5db906e28bda30a5d2c3325362b2462b17c6c7edc8a5f6db407e23c3645ab0818689f4aed8fa70010e72b56ad72ef0fed45659c7f963f86acc4a7ceb29e20979ac511a407eec8fd9ef855b82eb98bdabac73d64b45c44a15f0007f522584a860caa7b429f919a65623ac273071b712c0ffdb0262f5eba7a5576a6f6ca9d9461146b7ab70007e2237bbceb7e0c93996ce6b3e9e45d9d557da45d0807e6873beec154cd63117c44a42c9b32a8e83f5897b805f285bcf50cfd85954ef5a5a4f9c4978bdf6bd590f78ff7ce538d379fc2c06c23348afb9575c181ebd0180fe9757f6f876d753248cf3808dd5747bce7b520208af41e4f6c12e41352ad88dfbae867410da4aefd0f7033f90cd3ce3b0e416375f7aeb47238b79be9f41df09e46f5ea484c06f0dc591cf3cf13f350b77cb6e887305f48fbbf04b58b43af4e39fd2bfd0a0cb3e5879abd800f9039cb25f76a52d4b9f5aefaa3954736e6b09b7b901fc4d2c8e34eae4a66cec7b0248defc71d3982ddd4f53c0f847beb09e769630917d08a493785f56d1e9ee087c9f51967cdce59252b5cfee30b2e6e15e76a2d7e4d2c0fcbe76acdc2d7b453cc6fda1e97b26a634b97701601f844cd41a1d5eaed524a3fb0f5d9e525cf1f1ae00e31f0e718214f7e64d90e4aa43d81992ffb3f8b27e60fd89ade8271bc3b9af2c83c6f8feadfaea4cce6e06d0ffc712d7bfab22c42453905788de1d73f8b2568d0de813a146b9fa2d7d44b0d8ae223beab2e93f58cf7f01f4d3f686ebed19c10cb2debe4a4662d2b69ce6b35240fec9b38b08c62f4feabce11ce9eb8d989507ed7b9981f777aaec210ebea2b058c292e32750ed094c154406f4e52a29779ab64975be178c6c28cae1885230033b003f2814765a84fac1e013a26ffe0d99d3c918b5991480afd6a7598a0ebb23b122662994ad20df7f96a7cb05eaf35a4f8bc4ca9c6f2c6098f00f962f0f3df778d109f063df6366b13c44c37806bfe5c4b9b4dd22a4a12703f62f8b6fc898beee7d777a73268ea52a1e3b478d3f10ffd038b412417fcfe322e298d2413a228f49c9ed07f0abfda14bfed7873346d57bf8deb353792c0a6f0201fee6cb8218d24219bf2d670ba15ef80a6b43f56b1fc03e9f63b52108b36569887b340c5f4de8e3e82bc603fae40a4efcb888a84c9b12bf0b6de3af44636b0b8900ff71c6c252dea024a899772d55ad2640170253310be8631159840faec52a759eb13ceb6b9bef39da6f1e00e6c7f910169ef1d3e13975bf8f5506c925e74f5f5a01f1c9fb03866428d627b7bbe2234f7c7737bfcef983f52b4bb6b0c3f8bc12605cf26d4bea86c923ee857000fde45baf3bd69da20a758cc4d3681f014141d31d3980bf636d7ce5ed6269ee512d928437fdbd020ff15009d02f6def874b8bfd38ca85f7fb6c10f1958a84a8d212a88ff572c1f27065491ef6dd5118a25ab71051ea613d80ef1472ca792265e8d4f50b053eb55d0bb222ff3000e26bcd821e75694a042877a1997bb81d7a2148d05f80cd4b6f0da2101fa22c3ee8bab79aaa0f6e24f3856801f567651852aae0bf4a0ad46b0b7613ed87b36cc6c101f865937ea63626fad98f27ba7021842c651cd29f6d01ff2e95f5407776c9bcf909b46f67b657bb91e40521107f41e44563657923ff79c54b4d1a6970e5f0cb672240df8bc9583e8975bc46315ccf994cd9cb73e4df9d01f8573ecc2d8d6ff67b0b7cbbd34714fa934292483580f929df17425714653e94ffc9819a4be8a321b1670de03fd267b75ac553397efa66d026b14c9d7671251405e84bbe719bd75ca822506d3b20b3f86a615f9aa50703f80f529ad980e5fc62e707db84c523df8ea9ecfe0e407fec71874c23d704bbad92b5cfbbbb141ba2604f09f4ffee4e8157c0f265920d3d4f9b431aeca6ea5a29d0ff7934089a9e07aad7cf44dcf38ffa3c8a1e132803f8e335fe777b35e69ce64876f41f9f30e524be70f603d71790b716a0b83ed640188c9ac5d53f22339cf500f479e121228581a5a2623f86f3f22eaf48b4318aee80f81c24a2d9a4bbc81beb7853e11e127222c343e160405fcba5910ea144fedc9e795db52fdcb76492253102e03f15a0f1709206211bbed09dd940aaf55810e43e03e2ab8ff80775d3be9eb0bd7ae231ff55cf8796331f05883ffaef1f3a05e6ae7f357fb5c9a677d7ff968a7618587fe35615dfcabec16e12a8b624200df5ff36a2170fd437d8281c901bb38c8922fcbc85f188ea3b04d5e915907f858497707d5c4af154a072a1bff61c01ead18e1fa0bf7899a4b0c743ad2506e18661d9f9e5ee33fe8803c0cf4c43ed58966fab5ad4afa566fa2f34e22b06f302fcddce210f14abbca6675b26eaa79373faccc00f9b807f18ba88541732d7dfc6ec57a1b755734d85f0c519f05fbd1437c56272ba7493565fbdaa2f8c24444c8804d6377d5d6bcf451a842b16b5bad50bf6829843453cc07f1a098f6f71b054f3a3ca205ddae15d8fd6723700eab310ee444f07672870dbfa0c3c22cdd2ad1e918404e257fc92a9934c6e29d94d4194e6ae9e879ad33233c0fa3b95b0763bc196b5f1f9d13d671e861389376c30101f4028df742d559a7acbad72f7f4cb98db1276f110801f3c2937216c5791146d7dae91e4820fa999b8d080fc3a28d44241a16f8382fd90d9fa3b6de109b03790c0fe4610e3a6e5f29c2f24f165d4dd55f0d268855d3201ff3825e0f4d883db9613d6f1adb0d080213dbba422e0bf9cb48c8eb37c8a965bec688d48d2821c0ad0cc05f8592b69bb71d5aff1015cbe8344d329e95f113d5481e76f17770e7e1ff396b886946e1f47f6b56ebcc23ea00f23bece8d66666fa2f3d4b6f07d76765f893d940fcc0f79772c32ab98d390cb8b3ae8a4a120cbf3d076403f85adb47732287b1714066b157995e4f6d1e6811218bfd032ebf8a23e31b3149701ad71f22759e1fa58407eca275db87c7282343baf5a5e81b1cb5899d2e44d607cd0bbdd6853e14abc3885828b2b6845a1fe7c5c0dac7f787c0dcdd73c92b62d9530761851e70fd22c39007f3589a1e8e27cc098240f13dfaa48a87e3c9da107e09b2388ac3b02445e0eecc4fa0c7b5a766847fe6040df3705811bf29ae032e322fb5313372b455e4cd921c01f5da746a2cfa17b9172f52a463e625648449707ebf7231d9eeee6a52d6a9fc12d16f53f157b9c736a05e4df0d573d1e6aa318baaea928fbacf5a1a0613c5706880f0db32b3b31cbbe7ae36b658cf0ed8346e642523dd0dec510b913437466a08127cd7f9d8e6b7e74bb01e0a7391215cce4a261af99a433f7638f256651ca78e0fdb812cc28cdded76af4234176f285b6eac3907f02f0255e8de0fdcd4e434516e9b2d2586b8973e24223503fec5a6b4d5ffe4921ddf106fc2bc83308fc42bd4c203f4961497c288599b5e33bf9e915ae7b63d39df219507f60631c5bc971017b5ce48b8b246bd9e33c21d353409fbba02bfce95c4348c5039d252ea5e778a9e84da004a8dfd287a7866aec81665035e0b9ae7df9a39b04e0ffd3b278e636375f55a4dbc11adbfad2dfb3150f00f65d41ba5503ed73f3cd8a763d53cac2fd4dc55333a03e0c6e08b2b077d4d3d900f480673de95312846817203f30c031128cda8012778fab5688e9fe8d77290ce0af517605a31efb27488a7c383baaeb3c1cc67f3d08e8ef18f17e0cd0e84c9d6419d9d149722c3b140ebb01fc162fcb65975658b4332b3c09cc3d4c47fe09ad07107fb0db0e1c5f467b86766c568d59aa92a1ad2f1403e8eb82d1cc6a6cde74fb971c47634ae96f87f0b94f03fafa979f91dd764a4ba9f68ca907570d67987cf4a701fb8565e8b5dd25c913b2de86348b53fa636e495b1fd83f9bceedb9e5a2c1ec8fb0f7afbb0f73e2ac199daa007d4ab9170cd57873a49c19d4f675078f0014dd512910ffe28d687f25dfb5cbb5a6c8ee96d4caa45e2e750fe82bdf724d4498c4de20d94567cecf5ea4401d86ce00e3bb2d7dd02366d2bf97435029e0d98b9bdb76113ba0ffd1963fc7ba3b49219fe757a6e6cb11bd0cfef408e007bed34165ae89b8fa06d2ae69866fb1664d697b00f76f257f4750d733fa35b6f5d18cb21b554a1c2226806ffc9ff4cce05c1b45efac1d59a7237b208f702301fa3606f71dabfb3831447fa6d7175007136f0fbbb9017cb37fd8bd57d6f6188a374d907f534a639e497a0dc0cf6337061faee970635a5b767bdaeae4c6e838dd00ff99b87b04853d81d6539d69ea4d78b4d5298b262ae0bfb7ec9ebe0a4e4daf53b775540db37960e785df01ea4fd415fa7dd6585836f74db856c9fb64687bb0940df0fbed27669eb91421c65429f23be4db2d978487cac0f812c12b334fe87513cde0a3b4aed4e73bad999807e29fb304df3060e4a0085a5075b7b5157dd02df77f00f12d0c2d6feee4edfea8bdaa04ecd0bbfb0cdaba0ac03e2357e0ee515df5c2d98d07ce74e0a4b5b763f800f16325e8960fb6d41992674fe7c5557297d4337d417e3d8d532096ca6874d9c18e7e2dfdccee2b3d7537501feefd171c0eb965a27187e6553c260b5be58dc96d001fc847ae455cbf57d86aeb1c2d2e4ef29f93de9103f61f6e7a8bd74c67521ce1f586e6b97db60662c26005804fa3e58defdd4928785b33d9319f2fe7459df60801ebabc115e9467ecaba9497618d2f0b2b9f4c7b9331d0ff97029fa107d19ce7c2d66b2508c3ba662fc8dd00fcd30dffa0f6a2e50627000b3932c3ddf1b6257d41f0afdb69b38b6ff09d3a05cca91f87bc853b657f523f0df0fb89fd7daa49843d34a6546e4fd7a5833fce1f2900f81ebf31b727946f26ea4162c290ad98823f73c301b0dfcb323a11a8d6129fea977dae5546d07414778a00fc4bf69a7218f744a5fa52e78bdc77e219a570f944807f78b5503f2ba6cf68d216b3b035199da8df39be05e0631e64c96916e11a9eec13c7fe4fc99a09d11cf400bedb9ad7e0e531b7a563a21651604b9d4fc14bca05ea0fbd1f58a9b08d14c8558a8365635b1e6cf4f45002f88ba935092a091848bd7e1d127627ff3892b7572380ffbb75a7413582b02fc77a42ca41b91384ffc58108e0af9f7f7de332b1f1edfa80ac41a73337798eb39917882f87e73f4887f9209cbaa32ba174d6a5b3dc8c4800f56720c6a06bc4610f5227a82ea74f437516be34a503f6d182058afb1b023af3a0431c72abe941d95d5e33b03e053639b4ce0470c8076a779fc7bf60cf76f93101e8779a3990efb330e11c57b96179bad2e85e99a77f07c61f54c571bb0ca66548e705878d34b3de17ad230b407ff390ec13baf6b6fa6cf03bc5164ec66d2cedfe0a101f81b80ba23bca9bb062a7dd8cc073f1d4406db503faf75464fa917de8bb901321c615e754c526acfa36407fbcd589c2a36c694535264d28889dd8fbf913bd2030fe3af84db586a3abb13b0d215455266461dd688d00fcd387f16c6658d46ec9478b49c5e000f5b0da971ac88f2ddbfaf6de9688c985df573b30c8a8a421e24d30c02fea37e14e9653882788dcbae01c3bef289deafb01fa6e31d60e550f985ac56a98ea47b97186f95888b240feb92b61c8dcdb5df9b9acf490265703451ab5ef91807e6062b5ad47ca85989a9b709267f5d1fcb7976ee580be11eb619a7217fdf92ea3891a8cf9b3c7e81e4378a0feb6ffd5a60045779485e9a4f136d6b2802f9a33105f3456df9936b668741a2edf3779111256e5f42907f0ef4ee0738594f027ea25615806de08c53099a9ec01fa047adfbd3cf97001a445da322d9dec5d98aafe4500dfb41e4a3f8cbc5075671b3b2d8e1d55e5256e5e04f2630642c69926ae86645a1e8dd05276e936d63b5403faceeef1941a894d9c98802786f20dd38bf9f6c59d40bb38b96e9bf8750a8572271126e21159ac37592750ff864152fbe55cdd812ea52a6c55e79ced93ecd066c0be68e826b47e2f81f57ad7ad4c72575a0399c1520af47f356293dc8f7285c9d5447c1df77c6b977a4c0520beac97f944ce3872030792e8f01d9c50887d099c1aa0af9aabd8245beb7e58e6256c16e54739f1b0905a04eaf7d22c322e4b9b28d5460a23672131c8945e9100f21488592f88cae42214e1b1ef0295e850b68949543980ff5765fb437ce3d3c936eb722d7fce2abf00e3dd1d303fb6c93635391126d4fd89c59b0f07752e5e435002fb33c119bce92558389171d7a5e13e96670edc747a0be813e088d9fdbc19de89578c6b7b5684b5077e8d2600f20b75b86917ca6e88a12dcba2cdda7ce45f789d9b02f951ec0292770411fb130abd0613d3777b49272733807d4de93297a2a6b2e8e3cd3c66642b16791ab2d80deccfa9fac155e88d15b631897fff56b5e9d0103a443b50ff4d31bc798919d7a9eb74417023959e84f866b802058c0f2ae1531078209d0ec9aaa86c1c3775feaff8e05093c251e971c69b667551b237162d7a932c65cd807e0353a9b12d2b7efff57bb6821a42b7cfcfd7b480f793203f7ff65cb5a43e5cd459a6ac7256a4577506f05f9feeff907e7ad9571e350f6f0c036b35a57bbc04e803f8e3a821cccc387fbc3ece16a3957b822b237f00f07f02728df5f6722b763dc5085e52d1f85835f56480be3a29d3709501e71d8fae102951dfc8d16607e43e60ff86eedcc765e430b14d60ef743d056ec7e1d4cb81f9a7cc5c43483d922bd9d012caf5b1ee31b6ac241c808f28a7d1144405556f0ddc6b030e117f1ce66ca603f5c91b7864ecf01d213d0fbba156f3ca5ec58ed04303ebeb31ba15393ab6e674b2f06378fac711ed25fe1380ff645cb0d448de52d242ded6d4f580cfbaedd9b402ac5f285c2c01d93c62eec413299c4801cb03dd6b0c80bea59d0b29ccf4ecc7914f32d1da22fbf719ddd374407facd1e0af78acecf3e34af54cc8876f9a4203d20ae02fb5e368fc48b80aa6dc18505e492960eb3c8e6d07f8e5b55a3c64cfb89b8949090d9da6b4f5139f785b00ff2cc9de193cc0af699323230e9aea4bcaa990b503fccba2e859113692de65f0ab4b044a290546c52873805fc478727b9a0239fec3759cd79c9ab00486311b06d8ff62c6d4c5ccf161f015ea680cf40b2bbef2fd4b5ac0bfc0a487b0348af06c6b1de2326bfea210e032d600e0df47d56b67bee8efac32c43a388442779cb448b981fc0a02ab3a6ca62ca6a32093cb4d0dd5d6fd032c0f60fcd72829419e9e4ad0391e5194daad97c8eb59ad03df2f7dfdcdc1e991e3a8297fa4762886355bc1eb5be0fd7d2a839c4a13d44531147065fbac3f137c249007d607073d1fcffb91778410db5b668834d59a09a1c7407c6c16a6b696a597b7c49c4782ca35a3e41b5a9537d03f706381f32507284d524f2af96d66b49b64950780fc683a63bf02d4f2ec93a2683621aeeb9a2c7ef169403f46e900b5e20e691f340d3b387c2aacb4ae1b5704f0ffc3b942ef106f0961b856c6befa08122c4d88d003f931bdb8cf915fbb701bed6e9666fae4601584214702f71ff62ab57ce0b52b3cef2d9985aa6b0c64931600f477f48f03cd09c73d9e056ba6f718d42a2b5ef8f380f911478778674c9b41c8ab4c549e838892e27c6640fc8591e204a2ecaab54383c791fac9683e42064b2410ffa3aaf85c6f37977793491b424ef903c76519b38c0f888f4ae19c48b5ed323f73ba23c0cc979067fa2a01ac7fe57e153e776cd08cce3731cdc6ad3241874e16407c5369e0a59730d25bd3e1a37a78dc81052bda7106203e85e9e61f3aa7f775a5e8457ef231e1bbeb1e4d16a0fea85ab96a123b1356f2c9ccbdf8ec7b11a5effef940fee6d53ee93d4119bc68287b1574a68a4ae186e4281d98bfe55c45fc41ec533e99e2b7d961aac56d613ba07f85a39b37d5ed5c2ddaed4b950eefb05afb36bd80f85e3f8daec6e6bb30e467588acca20ed902c856ca80feedd2259f8ed6491e65b44c47b199c367d9942b0de08fd4290922e371f6b2a4f8bae04824d20707268481fc7ab7cbddab350bbddd1efeac269c9b9d74bd5d39407f3032d6a0d61fc9efe281c07a8e366dbb077be8043c1fa53087a5db1c43ce1df316fa28b5601ecda90580df0acc9033e6069d035718cd7bd95ac41e2547bc02f493825e443b9c1c27ae59cab53cf346855f8d100300ff0b7583b72786885620567ef0f6c9d67ef48a5e2f109f8015e7b45b6e6899a519a7e89121311698aef702e29f21583b911de7f82802f1f80886af1b3a9a4ccb80f909b9e5c51a55c66ff52cc580092935aa9520400cb0eff579faaa1918cf288a065056edc7b3663761a201fd13facd54401904fe7735d60b7f488e555907da49203ec97abe2a7f56d5dcc87b7c8514b5716e60bc8506e82fe812a53a6363ab5430de57f32e1b4df550e48903f8c312edbc5fdc3fe7db156347c7d77d6439278662203f3ac695cf4693a78526aa24ff4b03b730ce408f28c02f0770ac17558d35179a8cd17cdf89fb826262ff03a84f3aaea43db40617d8f8d846742052b22fcf67880fe85fcfd11926e81f6182a204ae07342408083a4ee3407d8b0339dc7367c15868ae59fb37f20893532dd85a407e63c8f4ebed3a1ae691830b78738a38075ad18a2c809f88ea5384fe3ee340c043ddb7c3418e37ff348d04a87f2221e3669d6b2b79b4077b1079f68de2bc149206c8bf7d8d58958d733c69c8838fed6fc3889277b93006ec8f50936564225fe4b2b75806d9743111aa39738c0ee4f79706969da5ab11291e1c241fbe4ed82a64d66c07eaf7f9e60c97af3fa87a8a12a88d143b223143478b00f9a11121116b3b64439a862c06d3d3d0fcdbcf0b9780f881e181ab0d7b4c5873c537dec3f73ba78d6f5ac681fc284bebae5963649898b86f1f38680bbb3ddfb94800fce2915df4d842c5ea394118a5555f287b904e4737e0ffe678f4cb8bc7550edc0c1e7ca07e26797bee1302e8074a82e05b6cead58aa3f073dc3ce8670b525f6b03f88c8b798a2bf5c44c4e3237ffc7fdbbf76693fd77003f20b8e2e792320291f531eabda1a3cd737bdec66ae0fde99da0a5424e5bcad535730cb624df4b3cc70903f81d53ebe2ab41be32cc2bd97b7a91f4510968075920becc38ea4a062962bbddcb235703a94a3a1a3b8205f0bfaefe3c9863aeca75dc7d302f101ca45f938ea602f6a9e225cdcb3ae319f4fbca8caaf16f101376c56a00bffbddb4b2f1582588bbd4228266db7b4768e65012981f4c9bccfe6fd4124bec158be689c4dd7964197380f8b9fc01652d418fa5d883c09bedb3b8c6da7ae31f807d1dae44c49ed9134594ce7fa2a5dd975d40943100c4bf3d5df8d05f5d33d50c44992cb7f43b7e180cf703f82119e40cbf0f5e5fdd8d1e448afa2363f90bcd6500fde3ed3909b5c50c4903dc0646cfeceee0dcd7fc28a03ec47bf8e1b705419065504e7ef036dbc3b50859acc0fe1dd7e11bef978ac430f8564cbd6e889e4d04bbab01fcae208665628318ae7af908e4a34ae6e89aa20b7dc0fec52f5363705050f37e4b62835c54636fe1c48c06f85989034c948019a764aabd97ef4264fcb0d19d1e00ffabaef1361d958b6bd3eb5db1aac454dee5392b13c0bf149b0ca88691452ffaf2c593f3d061fbd2a54203f83123439fa4cef2f9f41cd71643ffd2685fc2960fc05f3873d0396c84cb075c8f3c6939213bb0ab2a9307f00397b6dd6b558a8d04c6a761fe4266fa4dca5f5e02e313eb3629d4a3d47e84adf68bca5290d8419fb936a03f088ccd7c3eed02671114f47d6e44ca19cef7e40b509f1887c406eef85bc0f817456cc27d38bae8053d47401f3ee71a66ebd78f767f475559b5079df6d448f803c0dfacc02c7e09a3d4711f2df4b323da151926d16e01f8135badad6b2aedd4a7021b3aabf891ec058c6b18c0fc10ad3c2e29780f3b33361bfed45d4aef89025f05e07fdc6fb97fac7a9f78feb238ac19e10e625fe61e03d0e76e309786cf69f4e347191fafb4c95d464df7f700f6339974e4d3dc4af7f5213d43f3c48d5e24de5e31b0ff339589cc9be84248286a114ed8211b02c7cd0d2660ffc1bc8c19111412d4cf5645e4b7d0cdef1d7883de01eff7858ff91ef676f1f4f42632947df4c081c17e07b0cfd3f10e07223a0afbb6771aa6320217ad1474e2c0fe62f516428c3af20df13b756f5eda04209770c89303f81ea26a6975add96ec7f3b8a4247e342de8b44e0cd06765c350a19ade5b277832a834f9137d74c19faa01f8d9086fa9a6339a6769d34fb1975f88c2676fa98801e3b7f301b381713096db65fceac75e973cc4a3164d40df8e1981ff3dc59bc22201b6bd7d444b0649ebf51260fffa0d42dec7e3544e2ca59746cc48b446335a5f03f1476634ec86262dd441461ada18988ca8157347e15d10bf5e86a7c95a8a20b376bc4f5fdf905e3b8e00fc475aeb4db36e6cfad18bf7bd639c1b4f0d644474007e0ad7a31b036bac6df65470db81e84ee9c827fa3310df2126c018aff4781293d2202525a8162fb7b68202ecffa42190cbb45e8e86df66d9413efb55a33e9f3a00889fd97c1f4a13c6b3db9437c2b457ed71ec7bff2209d08f9232e3853fb4b222b606281c72b6ac3bd3843b00f9a17c299cf2766687fafa84affc433b9507e1e6b981fd0767b1489796f70a28776659a9c2accd1975c94a81fb1fb9b49e1ddc08b13d3e23c0c673d3cb8ab14304f025553b662aeaee138531d44204a708854cac2853a07efe1e6c65c6652a0b9b47ff97aa4e610ea86e7c268000b321b8d7c026ae144a9bcccf343311795b792a02d8a71b62738960d2e40c8fafd174f5dbe50702a9c240fdb0d72a8dac48142883541c99ae5c38ab75d830f6407caf3e1c27d172ea4afd7e86452a82493261b9d31da87f87a4847bd13ebfd174202b95c42893c811f24e18c88f65c14a4be79d42d8084882f557fdfa61858c2f04c017ce620cf69ef08f1a25610266ddcad7f60850a101fc13d41bcba7822ac6a9daf8a655742572efb9dd26a0bf608dcacbddc2827d78e4a3c38f62a451dca899058cbfade77bf3b082c5179230a4c283c14eaa5e691b00fe581b7f392310ce2f226b2830548778fd95523016e85fc7cf46bbeda45a8d0e3a742f47876fc713aa8e80f3f52fcb95f5b05f4333bf0d932d3869f8fc05a70bd87f481a5df8998475cb32760d962adc02a302ef0710bfb8a14046dd72db33770beaa536d0714f183e2501eaaf358ca8ab79ff2899562824b5d50e2e63b1f4ff0ac4974ba5a5df2f751ba612a91bc5dfb56b41bc0bd104f483cc22888f162b07482e42f82f3443762f6c477280f5df69663b6479d7bfc5dac0e0391577516ef1a63f30bee349fb9f330e6d07f575b724ec27f51be3a991c800fc0d89c0f726424cccd5a2a8befaba8801c4afce00ff07df3810ac99f94d84051ff76b5473a3aaafa829105f1bb8f3a04c99c1be4d463e8d51c8c166c523031e1f421b3344e02870536646aeb02eba58cd9246e51eb0af0fb00ce6b8b9676ad73c492dd42a501c174ba4003e7a0ce7e6d48168151e60f46324c0acd0c83ee529a0efaad099b31dca6a13d648f73149a92d51dc4e2b01f4f517c7b31b94bd984f2a7b8abcd9b25ca5ce676981fc3d3de67acd5349d5dd7502e22d3ef363ed596854801f5ab6198c917ea79593d8cdd34dbdf891673f1702e87f6bcec30ae8503d2d392db22449158f1adacf6f41fe9e39324016e7a3df95b4c24cd8d788ca6f258d407d5d52d9d7cc5bfe0d5bf14c55d464650129b60fdc40fcb9e6fae154f8cd8c8f4ff450e76e73ee84c02e39101f7f65199e22630eeb21a26beac23c89cfddfe5507882f535ee7ba2e65880b90fb0cd9d2aa5a72347ac800fc07ce7d9206c275d6eb61c48b3e8e51c1430b340b809f3025dc4b5faceec2cd23585f7ce8d5b4bef6c406ea6f257e33fbf818afe182ca9155d745f856c7f3bb6b1dc0bf3064b98f27a9e56ed847acd2e0bcce76407302f853a1da2e3e8408e9761e04873cba53e16119bc0e80dfafe71166ff1670b86032b455e7cbc2aa3e0f2301e8a3bce73b4782c8983363c34d550a22ac6b1b2172007d0da7fd775417ba51c48877c2f57e7c6590aae708003eb094c14fcdbb857bc77050e7823f32f5b48cfc0e583ffd8f6d504cc85cd1b45f35bb109fe9fde8304305e20b2a89cf6157f3bd7425205fce987b7e98ac121403fc77199ba011f57dd7872c6e87fe08d5ed4b19c96c607d14ee1da4e8c3c3e6b4c7bf4624efa3bbbfc2ac02ec73e387399c07ac671de74c42477e7d223b6184ea80ffeed7d2c5d7998d1079de70e2f62c1626adb89613d89f9016e72454fa25943e0a33f4f46cf757f9e8a95220bfb0adc1db414621d09732b3f79332f687cf4c5f0b007e28475fb311ca933db18b6e80b39be59da771f929c07f7520eaf4467f326459717dcb3f3924ea994f7803d87fb840ef3c34a69cb194afdeda89c7153696e4bc80be4187c17320e436396245828af2ee872e04f19c2ef07d71b43af8c60a4c8aae5ce5b34c23cd2fed9aad80fce84c42e8bea6faf282c7c9d1ad8f383f44fb744e00e3273c936ea4e4d31dec3bba11762e7b7f1753b405607ca3d57be9a97e4cddb7c0115c23a455622c2878028c6f088847ba114710ec9fbd8f64c8a15ac6de758402fc547eaa016c716a9595077ff9129cf41e635fe03190ff1830d684a1856536bd00ab8a63c385cfd84ade0cc47f55086529eb3ceb3433a05496552362be8bf24801fc5fc1e42795715c5dc7898b16d394a30ff91fbef800fe49bb12d973fa1f577b6604df041f2fe6ed17469a00fcd26262b263894f4deacb7b5d3bbc4406ca8b744f207ef33e8e59ca77b9ccb63b48517126e69cdb9f9712c8aff3d36caec25e873aca79b37bac059f1e2f51c30fbcdf55619df1ccce86c0882ee43cd9ac32dd980b3e809f48cc26075e3226120b3872069e64f81eaf933200fcf76b8de3ba63c2b689e751e77a7de69ea7aadba0beecb18f794ff013fe126d38d2c6092a51e248ca3000ff275ce9bc4f3278a9959e3279ff21659806b3df1d58bff3fcc7a509e3459f62beda26d52c2b0bd0a96e01dedf4241c7996e34eb7153eb47f562381c415c5f5320fe3bef67ad382711998d161b3e57601dcb419a1f0bf8e7f71502a4cfdce1c75b3ec80ebc7b207e5f33b006f00b04b98d4635c85f5ab02d5bbea17d3af673112f05aedfba344fb9fc623a672ab15dd42dc43966e41eebe3d58b503e4e3a09088820a2673f9a24764c8dca217ef7c8072087548b79f7bc7c06fa920c396eb91dcca1f40b4f37a8e930b3b7d1164979fc41a83aa6419c6fa7fdbba0ae21afc298bded555b0553fff27f9b21b707072f6a7fff7610febfd902c56294b5a8970af1078e0eeb25e18ffd9c222e2ebb92f9c7f0cb2c6c4ca2bf7f86cbeb7ffa7c4639d4baad8ddfbe34232656d710e5cf1e79e3f79b217f723a2429c4bff780fcd913fce9db6b8422e2293866430421153307b1418d37434aa829d115fee44c917f7a68fcf7a3898aab3e5354305051535292d3325010557da62e69a0aa282ba9f08fbdda4fda7fd6b540720593c4a6f3f7396128cc40be518c2e028cb9681d80dc3883903fcd210c2a8f6c320ad2c6528394d6da8ffeed4bff577776415dbdf00f829a2e0e46865b236d9228817d072c987341b20c54106485519a616db177a4142a44ba8bbff7c0b0671da45dd75063fec1706853f1cd507ddebc532cbff69120b37fd2fe27848efe5ffffee5cf9ffffbf1dffbb5b3bf4865d3dbb895e6448f54043255fc192b44969d24d1400efb46e38f6b52bc3f9ebdc193a08c92ddea4a3c2d7a7d91cbc26c1102812489197695697fb08772feaa2d96f6ce695eadddb54c75465f133b3a622558e5ebdb2146d8fbee49141a6f465ad549c78c00b2fe4c2a8ac06626bd6ae4d354816ee94f84da0467f6121e5c1d268f36be9c41b41ac4618ef20abcbb91c0188c1a4fb3c3e0154496ca98a5dc3a87519996145870de52a4dfe21d193bdd2b64d0ac60a94d2c266adbb8f3aeece3f42f7ebbd6a0a7610c4df481d2c064bb9b63788703ae8893fdd0e253e6e12e3be9ea7151b4c8a57b9e7ce887492c34f66f9f220e47d49294c96688fed9f9f90d1a024912fb7fdb6dd5c7f7af8a1ce0daea8f309ceee2d1df74229c72fcc943ff077ef30fcc88fcea2d1e015a03df17cc01fa8cd9ba8152cb866a49db903f9d503f9df1a495508dfd3ebbe5933438890cdcc3cd2c9c33ae50c97bce6131fd6e5b01ebf8ee7f6f99b886710f83c6888e5177300d42131309229e4e698631cd977bb7f747e6efdf19ba867fe43b489745097c68eb8d57c3dc72e48a0c9af6b259d0f9dfbfb9adf2cf9fef4d4f525dce6c6e43975bf8bdeb995b3388fea27d1485773f38768da54285c5c25046187abf8703633fed54e02b61a6e74cfca23787d4323481350b8a175a631a669301172a9b555f568168b10e46ad367ec17c6a8a9bf017978e5fb4af3f5d797eedfcdf6ffe79ff43fd743473f0f8f0680f5d2425ff09dc3984293c4afb6cd35ac87274ca0e53d381d0b6f68b4bfb4fda2980fe813af278b490c6c77917ec0d2bb45691fc08562de557c6d73f303e45db3b28cb23ba4c3f06f9430faa19b3b1a5ed2c31cdeadeb7186db446a2bd5437a5ff644f44801dcbb94c554aa2e8dad32fd684a0c310a23c5389f9d24e467120eb17c79f287b0171cd66f653548b262d7ada6d48115873fbf6259901da37b0a1d7c90d92b88c12c2a507d73a21c4cb34a46cf04539d503a3637cc21b46a5ae4dfcb0637eb784ff00f4d10cd3b03d8ce5114afcb3ed218acd68869418a66895bb486e2efe36c41828f87e2fbeea7eb5402d2c046ed62860fb2ce11c3f454dfaf2372faa8194d9f7000ead24f6d3a550622e9f4e43c7b63915aa512c019bd05f84753f59b6218c21a07eff0734e83fbbc53fdd03ffaf6f31fcffc7978069a86e00d884d0930d3114ea62cd5b523e2ec93502aed038bc9d7f1b0aaece6d63eaffcdd6c6d2e0c803b9cdb10f0f3f397ef531baa0ae61b94325b8e474aafe076bc3524e51d42dfedaca0bf337e341bdb227bc504666aaddd8f26425247043a2f308734c4cb358a63d8a98b444e44bae2785e18c4e9afafd96c95b780ea2afbf7dedff7a5c20d08161fdee668534e8e0a14772b063575b6295dbe727b616e2d76c31d44fc63bd44fd6121888fff003fadf7c7d04793367237923a757ffe4f99c96f6e64e7ccccc2f6ccc396c2c9c9ecb9aa8b37948a94a59c88b6bc9297888ab739a72bcb4b470e05533b17a25cba1aeed6224256dfc2f74637f118b88fc2216116371e760e332e5e6e53135e135e1e0e4656337e36637e536663167653131e6e0e1e6e06431e36535e6e6e43062e160316767e36131e73637e2366231e264e3e03132fab5ef67e66ac3c7c3c1c9feafe83f86c165fdab3bc8f3a231e8bd906c7cd1d3b8cfcbbf7f3acaf6cfd66a7ef4c54b34ad5778d8ab37f9fe380a0beec6e46b5c032c5bb23933521671180d48ba07afc41a2ba6a618e49cd54de79bfa05c9d8c8e55befb83394e831def0c3fe036b35f4c2ff369dd07dbf6e3aa1ffa69dd5cfc4e8e6a364737d8de8f3029d96b3a8218d6f81e5a19cbc73ef5056470ed3a16ce9a5dd4267536ae3867ea4e47143fe11d3b9bb4ceb73c9d6804dc9d0d8bbfd84aa597af4a6fa3fdb74426dfc9fe9fc17994e631e1b17570537393b53690b7b5b63294d71730d537915773b2ba9174e56e21c469c262edc3c761cac0e2f4cff73fa57f4be9721a5c0b8cfc950aa630a8e3d43d4442dd1e8fc43be83f956b3dd318be71a7a22bdee9c3a9583ee418f8f5f45f4adaeff5b23f81c45262d464416e82eb8b73f9fda73fec182ee92017ffe2b1c0239102b29c80ab70beac43f18477866facf7fe5737ec060ff6f068447cb8eb40df62fb94108a6dc04540102fe7fbed29f4fa91bfef35f7f7d0a6b13e1d70bb9bf8493e15ffa10d23d36fdd3ccf90b9f7698fd7f7cdafff169ff5fe0d3ae0cfc832145202082e15920209a5e985bd31341316d516a8bfd1d548344195a77f5771ae0d82120fed4744bc2962219f7969e26bbf9c982059108e946066cd7dfbd03f3a27bd4dfbd0395ffb1c06fdce14f4dc486340ce513f52e949ee6d6182b5fb18dd372fbfee43b26f8075153950671f675364306d743f800916a640693772a3c58e7a519eedcc397d42a936f1e80486418c4f2016765dc55346efbb16898f388a04b5add4fba7ae8f7fd87c7c5bfdf8e67ff7b2b080ab6d7c06f2c5e26bae9ab7a5fffc526dbdbd83ef74c52b2462a7e09231abe66e9e4d62ef7f7c9207a959ffaaed750d1fe41642752cd50c1d5e230b2ff6672f067e703ca5079a7f2c6a7bd6b5c11e6df268f6bb0832173dcb77e854031b7f695fd457239d85e1cf7677c866da0948b0af2c0ae4406f476701abbfdaa7d31fce7a927736b495b9bdc5601129f7f723ef62f9eff7b47e0eabc6a66bd6ab37c59d693e31db8c8d70639a866b139117095d7a9e7564f424e9d514f5fe15abda3cb25d9c4ddac4fded1722447d6b1e933ad29e54cab9c48d523e44e580fb890ef4498e622d43c7ede8c4b8c60b6c4cca5dae7c440b69c29b3dc6b83d0c8591456809317cf8dabb04a2f28f21ece78f0d1a4556988e553ecc359cd9fc0862b59ff20128eb22689d3e63b609be6bf67857ef3339a48baeb4542408c8642ea097d4f85082292c9f487ba867bfc174fe32ffc151cccaf7b1ab07f43c87a98e2f2f1b9cbcc662e7ccff50e9850ba655a79d03e95daa148417c4b71743ca93285c8b31d8e7f239d7eb5a352cf2efe473c8d97f171ba8ed6b5e79aea9d9a45df95f7091e2c0efeb33d0d58d2fff334fe559e86b3a5a79b03b78d92e44b564d75f5973ce6e2a62fb438cc6cec94ac9c35c56ce4ac5eb24bf23839d958a9bcf8d73d3fd42f6267917f3f89c36a626e6ccac9696a62cac169c6cd6bccc36a6ec4c1c6c9c363c66ac6c965ccc366666ac6c2c569c4636ec26166ca62c2666ac469cc65646464ccce69cc62c2f14bfd0367e66ac3c7caf24f9fff2f787f362e1313132323337616734e364e767653535353336e0e362e632e567623230e16167316367363234e2e230e0e5e0e3663232e0e2e130e6e1e560e734e561ef6ffbfbeff4f3dd55a9208a1a0fca74ef0aacaf9dac8a77be5b24b5e70e7b2d53a3ac1ea5dbb3b71e26caf6b184a724511603ef1e454bcd0723f5bfdac3a164662d7878cb63bfc0281e8e72414a47b18a4a9b89ca796d7ffc4364cc38693c67f86678c0f76a5d6759f11e1a78e996d9acf7a87aa5475f47c0c2d2b39ddaefbac700904133d506ab9e5d672e83c114f07dccb47b54e8fdefeacd7fc7f716cc0b199a979f0b2fdf37d9f5ec1d8db179e55f118cd615655ed2a0ee159dd9e314efa19e20bc5ade47ca168ba06b48026f38079f624717c84cbfb958de0623708aa1a4ef263e362d6f4c9d17fa4ef79c21e15afd1a3c025ff4fdf534696e1b6fcee8966a08a5db253c0c957a34aab2044d97f9860253605ae0c68bff7503a48bf0f5489fefa4183f364f90c8f6ed2fcafdbc7e7fadb6566331bb5b1242f7a827c681c04c90d7ec39e093b0b4b3da245f3fe191466f8c90f36fe0856fe7bcfd7057555e51f8454cf1aa45c10d794cf5fe99cf08bf05b0502027265a5f3e1fcfffd8be2d7e47ee00c81626e4d0f0d4178686e4d3708013116062f40fb172007694be94a376701799ddbe67ef2370c33e7fffedc477ff200b38338cbf5f21bea0da87eff7956afbaaea10efc83c8f6f39ba1b6a998bd7ee65f00903152c8d4266bb9601c9a46fc767b91d0a0d891fff7fd07989eae1761dc1f9f0ab041d03b972c6b4dfdc8bcd6eb3dd2f373c180ee61bd713388a8fc59fbafcd577067319e75074c454f094fc8790fc902e2d52f0fcf30ff7b59a7e58268fabb6f67f787dff9ff0d0a32d8b2eca0184e9fdd6e81349424c30696089c177e0abf4e7740fe6a1027fb17f109e42f9eff139f50baf32ffdf7a77bed5eed26cdb17fe0de7710a92068151f527c6facf89b0ff6448b924ebd16d88de41af2c03f8884a8a219729b893b00e7d7ba0e64cb3059d71b2e9c332fc72dc5735d7266f664ec14804470df2e9d1faf2cc8a72e5ab5c288acefd52a535e7ff943be65e533099f48f84fedafdb1fa99f298b2a0bb754ffc45d83fbcdae698ad098a4fe9d6fdae4feb8882614e38fbcffdf1c4fbeefff5ef3af4b03207fb67c434c843c953d2aa70f7af2393f18f2c5aef468a82f362abed4781091a18b3fd43514ab7f109184fe9ffe3443516a1cdcd1fee25cfc19bf71f5571fe6bfe605d439e65fff0037b0c2c4a56fbbc4575693bf3779c828df260288073f15b17ce1a1e1a4b3817288565e119e6b62a61cf747bb2d8833e276fe5bc3469c9c37dbec7742d5f66c3a6047f48c9d7136e41f6f0b675408d7300553101df69cd47eef7a840adf019e8c8fe25a1c0f1be16b1f2586d5f7460f94e4913705ffe81847cd5eb06bc028f3e7daa4a82e20a960cc229a94cfc36011a7bf6356cd968f2d7172f7c8c9be2e52b12e4385e57dda4cd8db099bfeed75bd87410c9b11bbe0f80d5176cfeb4831a2e17bc58fe28f579133af35de2f0cc4f4c210df979a3cb731ea39758c2fb143d7a66a8379b576bc731a98162802292cbf4490f13dca4efbd673288af0a7780ab125cc2a7941511c02e2cfe227d2eba066c442030d35ba5fc49abf7dfcf7ed7e11ab62d9d99bd92a9a9819d94a99994978d81ad9589afc1137bbeb8f3cf2cfb866e0b02cc49780c2fb7da08d9cf0f89f5f070f3b9045405a4f7252ba73045dad794d85f8da503f0ef9514fc8d9af1923f8e0785102c013f0c5af9a9e9e29195a831da41f472de08d469c09f8b5b51012f806877fe7f823dfe067cf8744abfaa3fdadd0a5dde359c7e0bdabb466a524967fe73784e3b7b9fb233d7ea492df8e8a3bfa01e3a57d3e91ec4c398674bbe22f7225ff6eae06e29f5db07f7afc03fdff0fd82098c94f8faafe83b9c49f3c3ff4f68b2271dc3fd8c7907f10cffeec19fcffcd63c8f03ffb1b40984e99a3a5fd916ff02d5338b5f59b3aaa79537a592213ddac96558ad82ffa1cbf72ed7f77fff4f9f44007fe91fef9436be91fec9f7fc4ce730a90200fc6b5f648d67e10faeef56454c76d3afbdf6843257eb1fd7798fe2ea82b25ffbfe8e09a2410f58b8044cd38e7c4f54016aaba89d2ea1939cd1461fa8bf5df54f819d813199e31f6977641cd84a17030c6911e400441cda20723a3e835f9ff770adb4c5e3032525dd3dfa6b0b531542939daa1fc200ff3570ed533c17a21fc95f837bb4b686c43950a1da26b321c12faac858d9b142204921ef614c21fea4ac53f1879bf072204f9e3bc10c4dfde26c8dfc51bcd565051e89c189a50bbf0b5d6d2ede4ef2ec0f46f73ba7e7ad5668262024515da6b244978a58645e44d4997a927bf77551466df98ae6b9f68ffa0a791ebcd3ec15c508f0c7f0d068107b5d766f77cf0f7e8bacb1cd7be04426762c5bb9f405568000b22ba5f303a0ee1c0da5bf0d438e39f54379bdc00050c7db77805d93f08c50fc19b2a3ee5ca1714c50237f0a253eea1328917bd2675645e7180912d0d9423070aec78870a7aee1640235176a5870e78aa943689ed0009ec6168ebf02a0a9539a86aa1097478b6a5c57c73400163254e13ab18fdc79e1390e4d70f8896cd6130be006f50654a1a981f74d2388315267f63cdd671c59e0b14f07ae0c136c3cd3ec69b72d17741c70b7adc4c220f28785089fcda9e216bb036eb5a4a1f5cd059ba47d801393278de68ad18bb347b7a05c74eef8ec4dae482018f1542dd1013f7b08c36ac584ee8446bead25dbf4032fcafdb6db82858a6527de8ba644ffad4f8116f79114401137099c6d9f6b8e4a292ed07258f16541fb30464385000df9a128fe949043cd3954a7f75e29dfef14cb50e50404c2f87bde9a9be64baa3aad6cb1757c88669c528400e2ad6e8949d9587c98b9587300c846537f26767064001894854a456ef320a4323ce4025fff0cc10fd294ea080f7050b7cd2034607d7cdbdc754bdd6e72cd7fb2520d448eb0d0f6705c342cee709356f6900e77381d7026ce0b00473749c1297a03be337e328c3a7bd5f22f618904928bc8c36c5c5aa19bfcdc0c0eded0f66f9e62df6f6afdb9d77990c3c359b08d1835e1cca6c44d39cd3b103b2990ffdba4c3bc244cc07419f11f7dad1f1d4e48f802c15d57bc8ec842833ed5041138fdcaef48f74e807802ef98a51f163c8f5b20bee71d226dbc4fd6e86105860e7473a4b610332c7511acc914e4b392c65eea83d40add7563d720e9c46edc69a11928f6c46b55d3e7c093c1f7ef5009721cc89535759b6c1eee1f3a2fd27aa408188cb80acba74df459457cd283ba66ec1cfc5925181ef2f6f98204ad7efb6fb24a669779dd7cf35b7370fe0c1bddccce3e0458f2fb7b9054ca0bb26f5a6441d800d2ada9b87fdcb17c44d179a049e6fe61938066fbb03bc5ff9f6862ead0b11f9683af95bed27d45f5c2e8c800271fb9629b7018c1f67bb093b78b25c92b712d733800d1656283d5b050bfa1630ccd1bc16a77dd8cb9c87811cc8c8acc6f4c612c41d62afd159b5153a0d4eb828a080ab7f77caebf0f6744887f8078579cbeded46543720b432323809fffa2cfb234661d23d6f298740889e1de0ea2e0fdd31e1bf9c1ffaeeda3ab0461897227ed30fc8b2369e9f0f5f067d321662dd910adb99d261103f0320a658b028e1160f5b0347c12539c654bc98f4481550e0cce8e15d07a3f59738b9d8091e78f664e7f3ae14001e10ce985ad7a77fa92f6a0eea7f8b81e1b191920a6c302859623f54124b93dc0d33676bcea4db26bc6604847e5e8d1773d9af2ecac87dfbdefe72c8ca518ffb1228d0fe59d84337c0610c0df2b1fb949d46d0f2bbf60da040a6c7abc2077fa1849810e8af7a6c020fdeb247c240812686164bce294667b87ea86a41970f0754fcd8754081555519d704e9fd253d3d688c4ad7a4a915a1926ac07ed45391f85fe5c3f53fa4c76c55b9799fb3f15b020594d9d95e29f6270874ccc59797ab737a33f5ec1f02eb97a890b782c6c3d8fb07bc49149931d82da75519a0c05a80b5ef816899022d3222a37c4c40936e3dfc1db0018496a6f092ff5ec743d1a367764114af726198137dffbabd1507eb62b7f105b3dfdafd13ea84d8fad8865aa01d6570304ddd2b9461cac73b50c5401c6f3b9e022870bbfba14e9997f0055f1d1dba34a39370b5bffd1e4082c18a78141fdbf5e5d220d64674f0f27147ea4500344355459ec3b0f9a44b092ca2a2b044e8d70ad766e0fb27e6447c26d941f19a11170e88ed0c2b628e6605646808794491f6d84a424b55c9a195b2a86b48c80340812be448f8e0a92f466b3ebd65fb39907925853261c006631c22fc9568b8e526730cacc9164f1277b7b241fb1fad5fda1cc76e69ffb04d19c93b3f6f3d478a0bc0e766993d1d5d61f7ba70e9dc2296d9d1b0d50c1c007ecfc1486422d40e4f5d95c1c50473f0916d5ac003f357a5fb0422a000423995d11eef34dab564912110c8f2f34b692383cbc66d83d5c58ef3a9c56ceaa74e0442a30f969bfb7012f244a1459b529f702487e185fd002a719ddb6632800d3278ef85a41f8a94e3bc944d0050a06bdbd637d8743784ac83cb1116863a37a7205c1e28504e9c3d6cab10ce2dde1229a6d31bd22688f78d13c02fadd1f89b4adab5e6bd777c078d04b8f098591640016ebf991c9b3742485ea90273361fa7561648489c01fbb1211488536d9cd347fb28b92e52ba829b5b530728a0148117b3f975bb6de2cd1b21d8f5a5e424e34fae4001cb0dad2d0cdea55b99c151c9daa8749eca1cf2e70015a7b64ef314fa9d26232b24dbca2e6a0263e86a20b0413acf89f5b397c3385b230fcc1e7dc577974210e6405c02a7c629e41d433565086a10097b288c94d37e005040f5cd10ca347c5a6781d8f382daa144b1f860af156003eee6681391fc77438eeae27a7eed1e7aaf874ef580b80814fe6bbfca41e6246f13f3103d6417e38917acc0faa5f77565b48c3b1df1fd0ecdf286ae1d11f4f900707ffe819b2b578404dd6a9586853c4779a21de356607e8e55ca2de35f3cdfb5cc3b4462531bb18e571a94f823d797c143d0ae38585a751564bac1c09dbb85097c0aac6f75a6773be275f08473249e91234c9f6131c94380f129848ccc1abfec40e2834bbb80f649da00653b07c03733f6537e9907edc34c9405c48d1f89229bce99800d86a53771493fb6e13c89cfb0dc8e6a4ba9f5b0cf043c5e98e710eaab739d3c02ace2bdebc9af9263d4218002b6569a14d94dc4fe91fa755d30192d8295331e6e40012fb4cc6a6174eec2eb9a68ffbc5ad9220c13da25206b0c667fbdf7fee36c0ad463728edec247ccb668cf0059722bd23eb43ec995cec4c396b86c6ed474076420b0fe1e29a7b373ce77962a0e18b70e286a4546165403f643d066ee6d3eaeafb245d4737936971e56cdaf6dc0067035a6937ea51885d87a7bea7a429773241ea735f37fdd4e80431d871055b41a529b491b853918f318f72b50208d9cc1a44529bc84b6b660e145b9746f2634522570fdd03e3f415bd41bba11d2db75fc6477974f08d6408156cb33e2c216b286b6ba4fb5b16f73348bed5853005980d754523b6b47cc94b7e7b3008cf4be5c29265820dc83dd513b98f9754da0445857ed9c94a5569cce0028601956d35cf47e22a1ceea10c6a626142d7dd45e1e90e584eda3c31409c2da426a04e0452ba816f0d6bf00542bfd765749c847391f0d939e992fbb303f4e7925077c5fadc526a421d2c218dd9a6dee036b3c7f236d1900df7ef1e3f0795a8f27a2840ab570a057fafdd12b556083466316a4dc3c8640c7a9fe5694114c0b84b69631c07e1be2d9c17144c99e3d6a8c9449388040ffeec2086e10fc717b9bd607e97b5f604f93dec7b1d80d7356a0007197f0e55554a9adaddb1c75158950b0eaf3533fe7bf6e1f50d350a4ad9ae53bbb88cf6b149ba462cf7e06484a37058a36f3a510236af8b1be4ca191c9b7bf79066c30d2bd7968dcab4f9af0454dfba9fadc699fccfb7da000b114bb6cc7465490127a8942bf9d8b88d7972b148046274b75b852bcbaebc5468c753337c855bb462f01fca7b9f9a30b92047c815cac8cfe4e4d25023fdd66c07f332ed71e407527d547674d1d8daf99ab2c7e39051498ffffb1f65751757d4fd3308a13dc5d834b709720c1dd5d83bb057777f760091e5c82bb070deeee16344070ceed7fde9c67fcc6f75eaf016cf69ab3bbbaaaba9b93bd215d29c9e14896b0953a9fa6e3ef95ef0bf0f936544d19b1d7b2de072e213789b5b1bf8b844d0358159ab214fcb63a143cb97c0f071e3a27ca415d7140ec1acd9fb3b6ede058d38e2b837716579ec67a6f06f87056f33859f4b61357271a33c7fe8a3cc2c8dd8a02f5b58860ecceda787c07e79e272cd54a125d52b726606361cfc9a30f3adc96fc4419a88fadb47938c8b60650c8432ec60a5e71b19dc8d7e3e6225fef19a768a480050efaa6ef5ea497429b832f11ca4409c45789c87a017c94f7c06d1a21efb5af76d3c513568e4f227ea50ae0b3e6ef1bc810b568146f7ff0f6066ae2c5203fc1010be26f71150cfdf3d23a2963b3ea157fbd7640251301d6274a7348767f27dce482a0944fac3548b6479b1c6affe5e79de469b792e571c47f8a36f3a9eb7d37eb13ee06e62cfcdd74b7b51268116a8b53c2ca5c502b3ee8f6073a68e2386bd7027fc53a88245c8dec63d24eda7cfa01bc7fef8295a0581143d3b99769ebf88d14334dc43d60c18c4f205be9df00256cfd9542744f1afa2fbca6307dff253ef5e6527be8ace13215e232b38888bfb27736d06aff177ca45d468d641eb647fb202cf32b655cab30493e03b0ac7f2a97e223ecf13058f03b87bd824530ed8bb003f8957bdd09a333c378c4a8707df96e854bfd379b13a0fe4a3dbd6d5954aa7553d6aaac963a8a25d238dd0006f0a96cdd4f78551505f2584aea864a7f6ab4c2b501f2df84549408826f4414e347a3ae6b932e09e1a521a07e28b10bfaac6b7275adc391b0bdcde7b9f521a604201379e8e392bacf39b86c30e0696b5364ab04b41881fae4ab517ef193286cd61fed9d8be1ac1bf242a2e9d1ff52fffd5ff59da5c60ee5278495f21ad46535e6520f6beaf748407c3eff2226acd5969c8e37fa703991e1cdba9fca1e013e9f9ce3943a323325f1a2fbbdc63afbca2e03c47faeb50aa614163d25fd0cf708f4147993c62c1120be4d2a1960ec32848e333914e231255ea1f474c903f875172b831d59f628f8b2c4d26f0a2b69b2f75f3db020f5b8b075873ada20fbd04900935cbcd8c11af61170ce98f21abdac1cc439a3ad3f9fd19ca1d9d7c9750e00f9f312a93e24520ef1ca20703caf8b8db8c76003c0276cf53f7e6143497aee3515cb4b98b3cd8cb73901de470f73346963d9032c9accb1d77ca6d3ac7fa65cc08ca77ce70e5a52d3c75252a2eab5359af86e45a91e20ff53f6789e5d5f2c078521a0cccbf9a3372bad5d01f95bb429ec99561c89c308235922466e099111fe37805fb5a39b9fafa5334d89bb7a7c763dd78b1b6abfd803f1b57427264be96771a4b38679c76682a56eca29b0604a9a6af082c02c5473181fffc22b8d0b520b8b55fe7f9f63f8299f93b21c75dbba6cfd2635a9c8138a51b704f2efe7a2891867459b9b4aa29249fd46f6e9317920bff56442f679fb2fb012b32e9d36cb87d3434ce803f149c3a7903750f9ea3221753cca3a2df943917c076057c966f2f62bd26ef1fbc8e058978b90ecb423bc0458fa7617d0759421847026872f2219df8504c1503500df4ff4816f9c0e3bc102f308d3fb6fd6d81c5e9392407ebaf6d864a4b5d5af89a24d254360d4a16f96499ef8dfe735a9fcab9476c50843bf24381a60aa1dd02e21007e415af799fe7875e00052378329321d620117210458c0e7d783c2c8561bd90123de6c63291c59b21baf0cd457129869b8af596d58b8cd7572e16222b670843695ff257e427a7ed230d1f193adf7dfd4d5b9fa5e4b9acb0fd417b0490b5906ee45751ff41085191a461c3f5a6f012d1fb95b43f091743437b123b0814e5a2b44250970c0006ca69eb081fa73037cb1fb91da32d11a4e21de4de0f3b5af077eb12972c421548ce0bb622ccb679c4a00f86f496a1c86b52ff67dbc1fc6283fbe28e1e552110afdeff33456a2dd2b3afe9f3d5cae38a6973d4a1768ddc0005d2bad00bb518e38f31fee9e7ca56e4f43922e3c40fed4943eca1679ec483c450c989c2132f9feee341918609a8b5d312999066fbf337347b5632dc3c8d17b08d84c39fd9b8cd50faf4bd009e6074df3aa851832118101ae764a8cc1db7c3617181f13eb9fee7a3736c86280057d70a4c44994368e438393750879073dd0e7c84ac0f9a93dfa92868266c222d0c487acb9dd7f13f7e40df003f45f0ddc1b44e98cf3c57056d69bfefad72d920303dce54e3c6f9bdb7d46952d07a88d08acd9fdf7b0003bd820ce31f1042edc711945cb68bc6c1ae67b277520fe8cfc79c0d2a3fa545e7d4fcd72e1337e4719b007d4ef47c97e16df0838855ccb215c85750946613feb0213cef3c408627da08fd8eb3fb13c63974b0acb6a8803fc2ca75a97780e8f1892a986e0bcf3cda70c827223e07e4c9172f6b726449fc4f5f37c51dc6d222dd18101164c3c17526ea947667aa8c83c0f200d524f619f2802f2e8ae06027fa1c17d9e2911dc4112b56dab98bfca26808f28befb6dddc1a040bd5d5393888eb6befcd505162036141e59b3d6a613d6d07bfdaeefa4852113990216c061ef557c31105ccc311974adf50e4e6ad1fe480a0bf0a7226e63f0e123753ef11ae89f3715955bf0c800fe3f00a5601407d6543c3052f4cf49cb8c796e6619c03fcce484f389ea9ce10a72b1ed073698a8be372701f06fc62409615365c7fbb6cce1a2dda4fe679f6d603f01c4afbcf446516bd23e3961ee28bac64fb051a459fafffb9c4cb430f09fcdcc786aef72d4e43ea32c438f12b0a0f0b80555e283f29886aae68dce3f4ddaf8c61fb9c08060f1319add2083b7277bef74b244025153241a0880ff3748369d269c20c2aa0ca2c070d23d140c802905cec7c270b55ebec6c528c672ce2fd6623bd4e17848c0063e6a73a5abd9af2ce048bba40693966894d8d806d46f661c02d331d25267df421d4703239711cdd2a681ce612c54538c418641ffdbbf472366683a99fbb381c082b8e63bd9759ebfdf6998252506a622f02d9cf991cbfef7794a67015dbe6aec7112599b0f6e5cf53df32f5e405fa92ba458ef7babf4ebb48188f8576b08639446099cdf5d182c85c59dcb5db4d950f6670663d1264739203e7653db886ee41f8f62f8e3df7470ea5543f63502f9eb1f049ec1235f9f7deb7d4b80dc8d593f2eb500505f21d05c3eed2ea4727d13d36579a5592f5e74bb02eab7862bac0eaac1cf7ffd617d04d1ee1dfee6257f070e889dab6e50f53f9ea90a336de4c4befb8c8efb6c20febbf61a1f36bf41ee5278bcb5e0324d1c70959203f8a670aa1fb7a2b0f8581e9fd6961c9ee1cc694213f87ea71275d9747619c92ff608022bb0db11f247378049b6c95e5f8da3d0b0287f2eadc323db271c3a39c901733e7802a4526d8dfcda1ce5c8f706cb76c90a82e701fe4cd829b6bbeb2573dfb68312137be5938c7a223580df34edbce620aa1220ff880c7ca9a9d3593b725f04f0eb14f32069ca8fe97906211ddc0ee7669e1dc86260810c74265fed601c81e81fe15f8eb2841856255e07c0f9f23b69e3923039868ffbc80f37275cba05e9e309f0e36dd0b31676c5b0d5acb595d053e5530eee26f8405787ab8b6c656a971bd589f3f645697a5176bd3d1e501f4f42df15a435d5fb701ffd2586202a5e32b70c02f8fb064245287adb1312c306be37245a7fefe1b22a40df73aa1afff6bb0a4a3cdecf1a2799e22aa509f71a88bfeea1192e5bc9f309ff5ede2143c629351a456101f52b995cd3a0ca239b56b54455adf5d36b4f712f81c2ff3e0f46a4b5df63f175f1f14ed6e43a64848cd7dc02ea4f49443b35334d9db87e78b7880d6672ebae161fe07ca7aae1bc9473b5af744decee7d12eaa31eaf1407bebf83fa926eaeb3c0584f8cb8edcc84083f277470c15905d572dddf205925084f68772cb3a91a0ac63c207e5afb88ebfc20fd6b82f91832e932aec51c3e8800d4aff0e48cb78722c72c47f2f3d04fbcde34d4fbd380458efcc03a7edb416994bfc73c63b4902caee3e31fa07ee77528be647d88d51d9ce70b3f41bf8d4930d3031664283b5fc91a65dc63bbdde805555970df64cb0600f83c816c4ff12d39092a19f54dd24d0cf290e1ee1a5890968f6387e5553fd574ad1d30c8fa8dc405ea331c804fb4038a49ab4df0913b46bf2b197ca643826d7403be7fccfe0bf9bf9d1988ad1c779f7ece18d7f2926402f753f6e03b810f521febd9ea8d4e1da5b6a73dba1380efaf657edc6c0f613c4229e695153a56f5ef48300016a82243f98e1e989bf0ee18d539669a542da5665ae07c7f45fa80da44716fc39ed39f25cd82f54b79190ba8ef4b7e55054cddf943d6c749278c862d666fb26e03f5c7e0c8dd7a492845a62c5c89cd47c2d3c565af7c00bf4a0c85bf1d1b4fa27132d0e7a07fe80c4df55b04eaeb295adf6d792adb795bcf637a7bf216bb4a327580bfd3cf084c898d1735458fa078ccf7f6567add32a604e25bfc3863af3883407b0f64f4dde104479be920c02f9c38bf94f4fa3d71f3ec1162faa6afcda6d4d1000bf0be992eb36a77fc88881b746ecfe310d855e9b202f01d7573eb034dabd0cf94fab92ea5f6d78fadaf7b003f456827f0ebcbdc58e9db9a7ad23cb710dab98c14c0ef7e69c8658176fba05111b4a54fe257f18ec4170178bfc5550146c27f887e9e7e689dc5c8e2d25017c406f41fe836b9af5fa0731d028e0539d5f46b24704fcb81fca02620288648ea9a2aa808b129b0e2bc5dd8be02b451886c581158734a376b9c6e43cddee2a887b2b000633de63f73e4cb6a95285e888a447114dad9e12e9603fc9ccc3a1bfbd081717f88911b394cf013e55233130b903f857052f9ea1c94cde8841cd9bfe0b38906fa00fe039ac9521201be7d7f1b891fb8826398489b28274d00bf72fd9154715b70cae8da054504ea7368ad601ab0a062c2dcb22bc005697d58727196d1536ba38c25e913c85f0fdf5eb6f6861d9a947e73d0ac36dbb3bf04df2fe42d9adaed68fc41cf9736cef822b2931faac0021553d10cbbdb74bcf02b91afd4be33678e819bb080fece71ae4f6ffa33df352ce95ab1c961f8496d5d03389fdf12e74b6f1e23a2177d34a558998721958a0d017ce426ac78de50bf32bbbf2465402b2d1304af1b0af85fe44ebbe303e57c118d98a46abee6357d8e1e6704d60314ac87365d7c57af61e09b4ca71c6dfc023dcd02e86f3a901f2cb92625c8b848480bfe0974318756746401fab9f2d364b5c2572c539ef3c2cad112e8f8903880df4679cee06e8a40c88898777874781a3e1ff7e6021648be9ae62670ef9fe05d4c2d51981d55e0d7622c030b824857490acf0ac5149b6e1d06080689d687d60b817613344acecc7d87bbe8ea1a5c45ec5a5dcdc441166041f94840e5fe99576ef8ebcf6f2eecd2eeaeaec69f017eb723af32e8c3c90445ec8025912b8f323fefb932c0ef4afd659b1abffff4b106ee9d7b45c47628950d3f707f7a6cd22bd4fc630d4774e0a91f17d8905550cc80cf47d022eff568dd3fbe84f3ad8e46fff2358ef216a8bfee315e3b9f5190bbd6d8937f39e657c41fa1526a01fad26ac05b13c98e8bd893359d721463f7285b12707f5aecdbdfc1278e7e13b54467fbc0f6aff3481b1d5820a44c979f5e160aa11ad91cd76a8893ff821e4800e85b7cd8b476bf63bf57fd583f5fb8f0adabda687401f08358c0b7a848a211428a421ffc6e4647b1ce741f203e5f09fbe57131fa3a5b7c5bbf99738a5df2534101f4af8d7e68ca9f885cb383966daa9362b5e7b1b64b1b003fb36d43627f80add613d9a0a4a99190c591120fb48d5b9e7a2c1a195e37580fdf2bcbc0e4c82bd89501fe18dbd6c4fe4efb05a7c9a71118feca40175d611d203ea4063e33a8fad45eee1f144ed253744c53366301fe34ddceabce6bd3746ec8d971ad69294a49836f8340fdf6c552ad17119d3ad3cef5e0f9d21cffbaf0b21258f02db8c92be27ba06301abbb7edf3d58e77f099d01f0df334ec524b6072f107418a6b04d47abcf93a95740fc576e7587bd8fc3a222fde9bd5de835a7f5125604f823321408ee52c3d4bcc348030a7e0a3e3eccf614006d28abe3ef32ca9bffa63850740bee376cd0e7fa14026d1e7d119dfa49a48e55d662362742ed1d11622d96beffe5f7fb624ee8f9b588fe6bcb169e6e2e99fa8005c30bbcffd608ab6fe27070837861f4e678d05f3997ece0017fa1341a9ab1696fcf77bc8380e265686e7f6bea53c05e8cb84c5010b1409393fe4f2ef8dd764bff34cb20e05f8877f948433c3edff3b48c3afbe9c418f917cb0250bf520ddc14b318afa7294cbd6457f47f0ae4a26306fa32b7d7448b12edf1eb7ecc66b2708fbf137b84c003eceb5d6bb81c4c5288615e2e36fbfaf309345a96df81f8dc9fc43ae359694ad3167b20fdefa6e4f4d2da0c58c093caf738a2659444ac4ecccb909cb5f363a12109f087e06c680eb5508e4947c474b3b397e090e8135d0183778759513fdcba550cd804fe443dd18bbecb66a407f86b4a7485af3359489efa235a2a0c89585eba4bb3807ee172956ff8c76049565ae17b988a493a41eeb339a0ef293992f8600c8b9c6ee6cd2973b5c7be6d63f000f88e9f71bba69e21386a141ef33e014b45044ee312a8ef7a06b103f8d02417d8998da0a38211d834df76017f2d5d1cbba067af617552f7be1b52c4f69f6fdb60fdb019f86de9f237349c2f8eb15361b6f223135410c00fc4943f669abf68d35bf016abdd06a3df9c598500f8428fefe70bc5ce4c9f101ce6a457a753adda8407503f2fffc0aef952e472a1d7b709e1311ee2fba79810f02fd21609b2aaff581088ef45655f51aad0edee3b03f405b9ad6f138aa20d7d9c3b12bbeaa4f9b9171d5b40fe28265d7a570b75f0e5c12aa1e6ea2a1bb64fbf0ef87e5d906df931db95523f45c108d7d11c875405fb03f95bedb940a6a6ca2513aeaf7eeb2d8982d1d64919a86f4ba33f7ed4e3a464808ea48fde36513ff875400ee8e77175f31a38e4052a107884e19bd11322f54e0540fe690f6bc958cf1060725debfcac89bfdbb8fae31c886f7e98ed7303a13a5d96ca740c8f7f923812068e80fb83f41276cfd2405466c0cb95ca2e55ec111d770bdc0f56be4531fa05c8dc5e776dff02e607c28e7f42805f1c52e3edfcae89a7bcaffa3dc291f33fe65b4922207eb8c0126221ceb1405e1e09b9f35167be90ceec9f01fe014e2b85d3170fd92fd0ef3eda1f19a0eb3d5902fabd7299ea603e4b11bc0ed1ee8c55a0e09fab8c6500dfb5daaf3ccffed631a3f15de0566de4340c98e501468cbd339a27bad678b0e79c452c2cee3f3c117ea7078cbdd3131235cd68c6b00f11cc3943ca824dc6a64501fc839df3b263886988ad64e806cfb397deb83f5361801d4086fa102e0e980ecfa8850e7dfcddfc7050e3cac0fdf941c97dfffebb5e43d81112a2ca430ad2e9d74da02f3369db8967c32bd32f477b4ce83898da7f873b1b883f33e3d237d8123ebf1056c4cb0f72cc29556fa981f311cbdd75bd7faa684faf59bf8b79e07ba43f6808e813143839a366c917f3ac54040c4dc1dcc7033e56405f36c61f29949989a2ccaf1fb2e8b8fa9fa8e27fef020bd4a64f919c2b6c57669a3affec88be3b19fb519708e86fe2b6559da25590d6ed677c7f7f28ad125b5f9c03f5679119cb11f77a6eefe61aa34ca9b0664b135729e07f17aab198ae9611e19419939a08c9be90cd9b9b02fc9fa681878f10a1972b4acf3f42fc3f0eed1d61b201fedcab22f1c607890ccde0ad9219c7bb56019dfb17c07f683eb05782c0b245babefb587139fdc7c75eea3da06f8a323a3993d7d1d8e9932b7a2bc10592e9778903e7c3f852b7fc8ab7e1fda74304aadd183ea92d8e30e0ff63bd3197b52f8fcb2284eafe94d87dad660805cea32e7a71ebe63659204d1e10f72a128018731b7102e25bd9e3bf6aa3ec96686d379b1668c1095bf9f477e0580fd38f9027ef9b022e6cb19c8b3eca43672ae200fea6b43694c79cee484c83bc827b856ada2b054538a0eb653abda5c4aab0b6f00dfbe94b553c248de3da57e07e6b1da092898b10d9c046421e76119a6d27ac8502f9fb57533277e345283f7fcfea76b5df0bcf3cd412505f1b7411d73eefef40745291ec3eb0de7fddad2205f8357e0721d3f65f5bba51ac8fb3ecc898731ab2760e40fe6fb7a9cb9c6fbd67b45346921990d2d1bcce03f2e70ca4f0fcc98bfc89e8f045cd3844c1bfa4cb5200ffb164eebb796ba4d807ac3d0418bb3d99eb545802fa5e9606cce2d57b5ea97d787f6a9b7416bd7bd246c05fbaf8d23b44a466191c136ea182db83c7693c0905e4974697a53731e3774b1ece6724359f799043147f02fa929aa531ebe8e5111e84afcc2736267e04a86aef9fff05ffff5ffe9e39783fe947d6271765f3d71f01e107a4ea8dccc002c96895c92442e2397b2442739fc3405a362a23696081f0b05448d91e3e1b8c567ff383bd7dc43f0fba65807fad10362f56caa9ac83bd14302664ab9b19145801fc9d4d5f9db8c2cca5426bd8ade1929af90596eec6017d347b4d5fdb13f36bc2d36bae536c930e1a7ec01970fe3f4da88a9ed39b231c7faa4795c009e5ca391d02fc97899ed59ef9a13557c7053841b50ba881441462803fef36b024324e66a50973fc93c9d2c83a593b572da0ffe1607c5079af9f1dea0c0961e9a208599bb46202f0f3f2650b0321e51daad503c6149ab0d17f94b5d781fb137bbb4fd48299929d7a06c99a3095c03571381905f8f3af634770537c3d7e931abf7f5ecc3772632f01f013749ccfd4dcd48767b50b14f2135f6fed211e64005f3f487370d3142a45c4d6fc6d6dc23e8f30500217d4be5759ee6c1add12f9f1278c297b275758aa240de0172c4e4d89127fbba75b35869ba20cbf1251d67403f5013e3289f3cd65b1bce4736398225dd55b86001d804f278fedafe5caa25eab12bcc97156f06983646a00fd0cd5e4e3c017eb059ebf96e2d4e8fcc27862d31600ff914aa495881ecd952c3afa86e9d38d84bbd3052ef0e4d4a3b42b3c42e546cdf4d811875a64b560fa06e8433f930635c69138420d974676f02f705a5ebcc0fda159ac591e97a6e8ef8cf77490f1eb30c71ff8be03fd01b5599c87f0d5d2f670cbb4ca0d3ca47b0c5ae4c0f713fee91a828434787eb9550c06a6cc9467c34605c80f2dcf3dc25fb3ba1a242e9870dac43e94acbb2202e7f3cf85f2ea3866250d23c4827d328a19ac42c01760acee5b5a35a94f1c66411e832566cd67320e4f186b80ff7694baf488515427ba347d50a22bcaf1c5a7dd06f86b3c384882cadcac2c8e522f8622248ba958ac6340fffd9006af911a37b08213fec4db4e3cf3e2179c04e8bb7d8e7f15e827ff61157454387c2588d9e9823600da4baf56b593cacee578d960f70f305b7e69714f2800f99d0f8e25acc1240fd1fe345a04a9e49a0735a109389f9dcf9f15c72673791dab98ee70a61452b0cbe601fe9c7906835e80a4f170da63c799e824c40955a20ee0972e83f1ce975fbb62749bc230dd3b46abb1f75780fb87b5f5c59cb1d444b8db16e56f8f188656e80401a0ef9f2f8d33140855b472cf7a964c645668f1ab6502fed7d96f5fe215d68e8548619f771316ec0ae279d981fac849358ecde8893f0de97bea432b46a7f2771d2d207f0ad477790c5c66efab7b0bd2c0b4de3df40d1000fa2e1fb2a74c2aed8e82efcbe9df119db4b711f560c0bfc943e0f7846885a25d7df1646faf9d8be292ea0cf8bff2622a936d1e06649aa9ee67c778f079e0945e80faf66d2576e16be48f004659f855ed3a5cda0814b003fc39cb68109ba31c85d07a3349286fb1014e5a61eabfe4870c36244a89769fe0bb0cd70286e562ffd58d14608795e1b783bf2682f508b1f162ffe487fa8f177d32017eb702f75c5820cfa0256c5a462655d254ba901be40f7265be7ecff1e19ef6eb63a947ada274e0be7702fc3b54acd294fc764810c14d84594e5f5b7f5e6052cafeef73125b52772aaf008430aa12048a183eea1c9545602f8e0d0256f30874ce7304634db1f8da1a1a425934507fdd15b739188ab593cb42e44f1e1d12780a274703fc0aee6f229c334ccf6412df6ace6ee31f98b4937d00bf69a8f221020a2fc397877bbc86b0e5a62591211dd067c7935a9fcccee2f5c7d95ffa1ff106c895838581fae1c0a924d0120d3d3a9b7c8ca9a5f7f9afc9bd0950bf2d8732eaceeb4e4d14f90f48dc69ec6ae98519f1fe177ef6ff8a1fffd7f9fcff8adffeaffc84bda6cebc4a163b53068f7cbf6f3ddcea43df08dcafb4aa224501aeccdae29a7d293b5e2a8d5b9426604cc7be09bcc21708d78790e28277186368b779890552803ee458a3c4c0225c3c0a7db926d6e4f42a9b4600f817f95b8877f9f738a5bc26c2af1687fa6b5ea44a01fcb22c34170f95a0251318ab3807cfd481eeff2a0cfc7c2b13ee436140439db39157cc242a018cd0bc0e30520993ed83ddba2931f1d44b205cb037a73ecc2404105f079ded7cc52a60c8b534d22773b56bb60fb34e80fa3ab18c652682dabad5c65a76ed47e718945fb235105f8fb3866b6b0375bbe3bf695e12141763cf3e8902f561f8c965bed5a663bfc109d9f5e1e2f18167c71ea01fb8d1a89309bf86c4d5e870a3aeb47754bfa77802ce97670d9463174e3c2c7e71604f42ad92946edcc3dd7fe1ff5a7737065a8da35bea287f99f4c9e287cea08e03fedc5e9c279fafe51e1c4226ceb97168780a381bcc803ec9279fa9be6e8fed9e3d2fe80e7fec1b116462150ae0271cfda55802b3dc9971213a29b47391ee7326a0be22820d3f84566c2c1a1df1f4dcaa6e0f2557ec03f899b497abf34d5e733ab19330cacd7b065a1d665b60413f7b99b8b5cf3b4abac4ed19c6743be53fe64bfec0fb15d4a28b19eae6e232a7ed69458ae437cc7b7101f8b379bd418c393c94172e4dd6bbe6beaa9877a4ca003e38ccfbebf8747ba842c1327e2049fa9c45df960de80777dfafa61d94fe14b4a976b5c4fb6b6fa1941103fed220fa11051646fc4739d34e4401b3b3bd600e57809f1c2d2bd2e4f68db7618fb3b37334fecd17bf1e0de88bf9fa632abe9f25d0207634bd3a88e63c614a99f300fc5de5e2d79b6b8681bd423f64408052c2b9a807e82792866717ea0d76ad14b65a975c4244cafd3b0c40fdf43917aa8c997807f15f38eac60f8b577c35b75020fefd7e8cf6a8867b4fb748d7027fe2bc4e769ac30cf437bfa3ed8f38202a6d15dfda2381816ebc72e19107f0f375efea5e871b8aec0e229368be10674dfbc911a09fc7a148c77dbdf0ea70a7c7ff80eadf62936bde0af8871d1b370a9353c268078a78a48752891134b22f00fd808f33894433ba37829bc1e454e6acfff63d7303507fe2366f44064623f856efc15b7bb3f43cec71a302f9f14393d617594684c9903d4c06749524f1e1db1b20be6013f12342d8615b3b86aa19923ac8cd3e78bd07f80bfca5ecdf1a2b8a1a4135d561ca2897dfbc1e5919007f6fa407fbe8de721391e14b6026114632274e2d70fe2b3c0486d4e725f759ad2f83699347d0237309eafff7b9fa5e7cca59f79de73f3753413aaea94bec5703207fdcfaeaea63ca1e9179e2309a1f43d8513abd2300fcc9dcde6c9ad513913a0702e4be8f18dc228dfe70f7803f25446e8405ab7407a5208790313a4a4de11516f02f287856e7995af094e646b5c76b6d074eeca77100facc6031c51dd67ec804365cf49aa0de649d527a30c05f990ee89a971a0e11a852880c1f887df490fa310de0071cd2f150ca8bbe95e8fe73a78a37aab8cffe007c87c0f5d5b3bada372bd9a37cc4bf70679ab6861204ee9789d65b03daceefdd799518124684be5f3618ce407f651c9c792593d69bf8a76c710d2ec6041b63de16c2ff7d4ebceab6a94e4798d39d3a92e487fcdb426b05871bd03f07b1a7966b532c715c225208cd6ed47d1e7d017f0ef6d84a675eea878ef1208426f6e87ba13ce74920bf5176d60444e67bfaf58fc8f620e9202ac040ff06f413013606ec747616646e0e2a92b0e2acadc8ec20e0fdd6eb2f8b67bdce3cc5cc6b6a35c3206fab9b1f00fee0e0513ff116d772ca4f813fb1694566721be1be01f983315143a263204188b1fde132c6f8feb72b76d29ffff2bc353b4141a5b507c1f27e6daa1e49c9849b2d16f8fc6c6cd90681a994748c9a8d543b12fcb349fe7e003f61c514fd4cdb16bb4c3c23c5cfdc5e4bdbc1f319f87eee24722c66d8ff605c06bff19a75f15b4a12b601fcb15c601fddcaaae5dbd49f233b6b2da213b9f448807f55eb6576f24575f67c475ea941537fe353f41200f007218c97bb768daba4ed9ccc558363370c87384e803f57b0696b195f31311f12c137912633cb8ef2ce1318c99849519e214cbf02e5b714ac633b325bf50dc56d1cf04799b4bd337611369b3d1029789cb71166b18c01f8453e7aff77d91433245e019028c7736ad7e74abc80bf084be43a88f35a437ea3058a77f6bd331e9e7f1f707f59bea91dac3d440e3118d1bce1ba5d8c12fde802f01b3c06d78c657bb685a0bceacbfb90720d31e26460a9e7518a5568b8225cb58dfc490b25ba15b6184b01e09fbb1e6a534dfc0d810967a72fb2d76d94908819a5fbbfcfd762ec2da95dc7de37717e33c62446df51d24206fad7892c76e8b418fbba05f4a19d7b102b833ac7e981fedc70c3822cb3e0cdd55742cde7c8bedce19b941f68007f5a951dcbe545a480e36d2daa9abef2cbbb4b1bf0876ab17f4be7577a5f1e5c77bd3ca89dc6d36d6a01f06be5872d6310d9e1467ed52999b7f41d77c55223c0fb619198de3970b44af6dd965ad45eb1d0d9b30d01fce7d2fa11bd543c580eee5401bbdc432dcbcc017a80bf48127dd546f13404d6b81393c0db9771ea512115a81f25968b6ba7c6c32ce1efa5e31e4ce627f766d1007efc2887d63bcd33adccc656e2584a2b44d4656319e0b77b479c71c6fa8ba7ef3bcc3e6e60d54e9056af02f50be4fa99674f490e79a5a8d2e85fd10f7e6e9d20ff7194e838fe0bde5550930197e33c43f91aae0408af1055a36a052f0d0f125cb8861cccf126f0fac559003f9aa8fff8380d9156f75df58b83d7dbfd1cac3537a0af5834249733e362fcc45f399312172f8aec5aeb02fc456b1ddf6f3765dc7dc3b03be1e5523d71a7b9cd80fc0c2520b992e0b3efa599611e1787d8efe763e10bf0a70a90b324a6345a0e460495641bde4dcd49c888c0bca1b3cbb18671eae0a694beeebfaed0f16bbbb952007ee12596daf211c3d8624d3a3331c651db2bb57500f0cf4100a5141c2dbbc0e82aedf7eb5cccb70e511aa0bffa9dd6c9d29c7b598b8f5c252b35a2f15e779e04707fc6594b3ff920602891fa621c8d65c1175dfc6a05fa9fbb63fd3ef3ea53ebe41cfab3269e8609a9388401fe80fc7d975e11eb63e468627c9ae3daf677d1585f81f915b24563153ec9cb321f9f10852a1a203efac1ce01fedee040454319be686233959119dd89d319eb5105e0fef8bdca7b1fa71cbe7dca36fc46cdb16473c4d901e0c7212bdcc7931bd769eb92b1bea9546c0b8eca6c207fa1c0485a5796376f97ad3be97673058b13208f00efaf48c2cafd5de5ec81347bb381e890067a403214d0bfb54a7dfea7eab1b7da1bfde47afa225375e2d33810ff91d652db2325bf39e259ac12e0f3dfc6dd5a2602fde9a6d98b5744b17cd6250a1b74f518736dff3ebb7a01ef87ece1746ae3b474501d1f37709a3dd4ff2dd81af47f40546b903de53e106f693e1c2a154186585701fad262cf934609bee1759c62f668996382001322d01f8011a4276dd595524ac2d0f766219857c80c9503be1f1f88d9bbda308acd78e9d3bdad89760fd32dc03fca2e924f76d2f0fa1c6473102a03877c1c85dd0ecc0fba7a3ed383712be36ac84ef3b4595d6112f15200fcf5640d6d3741efe553472cbbccee90257373ba17007dc8b04362b7c4f99e2b9d665947ca3ffbc47bdb18e0f75db933dc7c5d8ded84c8dcd77cae9b43e61abe00fdfb2345f6cb08dd97239b14d88de4f45c0f4d450ec0fc8bbf4c93b42ec63d4214cdf8338efd36181ed1b1807e6ff0ec87fe0a977254c02774faf17abbdc42e212f0dfd2f0b60e4c17ffe0515053c88fd6c0edf287292dfcdfe70382158da12851e38d3596bc8304847e4f4fb440ff835c425946a6f28f84c1de8cd57e04fca18c797940dfd4c1be70973dc8f04a7b9641d81f08205fd6ad019e9f87931cbfd9e8d2115fdda274e64ee534b42d03fed3e7eaf4a8ee9cfe5efc2d27c1eecb374685de3a401f456c0b3663c7359ee975eaf0088c23c3a60b6a03faab263fa40a610517e521f791d266194acbc6fc3903fc8d9607de0c3876e93a68145736900f66374d0486007f493a57deba802c6ca2724403f956e4cc626c0e01f88b98fecc383156d0a9fdf9115bad2d9d54bc899005e0977e9d8f3319440b8c1f28eac31728f278e6712f00fcda5cc704535e3f966379a8c09c98f15573755b1ce0af8f8d46db55f89cec3c7f66a6a8c3befdbb578603fcfd1590acb3fad579b39c31a192fda503300ef3d7c04a7227f2a6f7f3db0bd1db5dba8cbf83d243633f0a00f1a5d90c7f47ab08ad06b567a6b8aa83224b58ef01f097cf2c38716f20b9d234c3c3eafa7afcf355ed8601f417a1042d7abdb5352fe9eb23b80cb8906f48dc2a80fe3041c656cea151b7416d758482a0fdb9886a901de85ffe6ed7eadc88899b905d1d2d27a0d792ae025903f037938591df5c503f9cfce8c2a18d6d699ecc334105fc79293905966fa34d2857b61c6881732a2159a73a2340fea0ff2979b4928351a0f04f73e7578c05d49c32501fb9dc666142126acc68f0da59870a29fe693e3c01f8af69e9d2be572e2ceb83e0222943ac57344fcd3dc0ff30b6f4ef65568de83381396ff95d685e606f6d3a30bfede62462ddc6cb30f683160522ca8f01b792e76b20ff15d97d0fe6edcf887fa9ae3df53170b3eb12f902fa03e6c4890d9de07eb84775052e0df191d3c57003fc180f462f5f505c4a668832b4375ef1556fa916b813fe3061e82e88e0822f83f5e3a4358f83ffa05d29a08f7e808c105954a094af62a22e4f32b7ba5a2f9205f24b122273779bea5f32ab02dcc128557c3efc9bef007ff891456eadbeb6226cda2a4bcba5155a2811861df07f241635a86c418abbdfae6bdb3a16758410cc1702f903a514891aa2c7f40c9999f99cc7e0fc265e5d1de0773746ec72187429d44a07fd16cd8364f4f5c2d496ffcbcf079d1e7afd0a35d4e9fc58169deb00658dfb5005e0978303f89bd4298668594abb4c333d1ad81aa26c80ffdede3377480909d2f985a716f537c656fc9f0c21c01fe76e15561a248aa67bb218cae8761f7de229a304fc554be4a1d325eabb23100b4c7979154d1c53069f007fd10ebc8980404ae6d5edaa66f53e8c286c5eef23d0df3d05f17643b31631d1297f53b85784a07c600a943f1093ad300dd733a538da9eb86879c7278705fe2a40fcd70c1839c80c9e97608b27bddff17e334bfdf80dc83fd839a39ffde955338675247c821028328eda5962417ee59d75e9f2bfef26bbcaccfe234c916bf6f120fe7e3d58fd24571726c8e315871d9c48d6679a07c4077cea4a45d2c792c317ed5d3363869c20f7ab6e20ff3c3a236424f72a7419713ee68962c9ee7c758cfeffbf7700491fd06ff59c4c9aa2a28eaec59c27b68f7696893d3f01f232442d9a76f5b761c628b399365a9f8b52cd84f012e0fc13e9dd93e37f95aa41259b6878652f1dcc842707fc21ee0b6ac4887ed3057ce589f26f1d3e8471d09c80ff2a85e16e02eef001f3574197b91bd24cb2501be2f47ff9f9d2bfcdb6e1e7424acf37e80912362422ae1334007fc11825e73248956b4718a7b44e363d77752e3c0ddcbfd10968c887dd09be72aa3458973912ae5f2f21c0dfcf427b5f8b2211667be7b3cf14cc5238771f19079ccf81bced9d56c69533fe449a1b5ddc0896b04b29403f4a1c0ce9681c6f883c38458035b5f067795af604f839e90bff430a294c3e5b660f6a9282e42a0fdb35003f3d4fffcde8dfc272996056e825238866ffeda50ce85f9915b6449f5c4c7d2a93047f15dd24c83f4a3800f87bc3096e74c71b79984f5c512de4db278e4bf411a0fee66fe598270b20c7978a3a7c4467d764b6628104f4f1bfb079302cea8937074f2c75b99e63dace5ca3b340fe9684c468d87d13ba0914299bd20f3286176001fa67ad246c3121926fc51487b6f25e36e43db90e2a80fc0fc34437802bec71dd1fd5ee40bd85cecebd9c0be8ebe610ae2bfd5d595b57f05e1672137931a1c8c8803e5b8485f9fd71b71223301fcf31235cd43bf51f09a09fcffaa1b5e2676beb66ccf830ade28b298bb2ff02e213f592200edd66a03a2fb7d54752774e6691c319a0be78b28c1ef88669fe3ab7f84e7c6e7ec22581d50de07fbbe3e3330afde3424f8ff1c68bd1a1fd85cfae017e5582c1368d24d9d6dc9b73b237268493e0b3892de0cfa6cc24a6cb8caccf88dea25798a09e69a5bda403f2dbf2ba5c05046fd21b3494cfa4f6929d568a9fa501a86f338c0ecf38c82fe4f928da0de29277bfbf03f80ba55694bbd6518c5259f83c5183e768f20fe6f200bf9bcaff93a600e7a8ee21efc37714837184838040807f5a56fc82a5f49e765c63776728258ca1e22b292f1020ec2abfbc93f7bc7cd5a14ab21aacaeee08d31c06dedf5745d40e590415f143d45813dea1553ea1ac4ba4ffd21ff6b796a5dd5a4d7ce291387196ee3e4cf06bf977a0bf7348ae2f838070cb272af771831b05edf6aa0b11e8cf6f592919e25c33bbfb12a177ffea1f4b736c3f00e01f84b0f33b9a144b38a65fb32d5628bb745e3ce2003e912741c32ff1de13ae594f5bcea73da24c0f2e01f0e15d69a5969b81c70392456987afd66bdb4ac435e08fe8466b6fc9ea1cb0772424f02996623580962607f0d553989fe9548222079f8dcc25c6fa87e7f50026207e0d19f8d811d07912fd7097d4f9b4174fe3ef04fa8f56636c6409ff5de972190bd8c13eb066b36c0c03f83f68cf6bfa81b221eab132cd82d18e799598b301985f65a82072ff0b3e7a36403daf688baaa8bc1ca919c0df5507e2842ccc197951e73a6608c8b6f4e9679200ff89e39e18cec2e6419ffd6a5f19556ec7ffc01803f81fec6bbd4244bc577e6e3a89cafc8249d873702d0406ecb6ae9d91f6fc6446b4bc15e19814d9244cbda706fa8f77ea44a9288d366da73d19ba153c7e55733772020b17bf70a4853cedbc56a70e17311aba19bd14e77b02f999b03285620389431381ea3e497723b6ca406b1fa85f46e6b698c9f1da3776ba953388abba8759fa0580f367b51c28dff8d8764b6f62ca58b87911f5f42006cc3f85b45bbf38de1b4af4d4e3be70c7f2cd0d9c7103eaf3ef7fb74c59a12a664e8f84439203fb7f92dcb403f1338cf5be21ec825c8ed4d979c728ccae9b66ab03b87ff30c4e550618f346c608de67b0b915b817abfb807e29f153c89957e3b599459518e7c28061aa35b709c01fb43bafdd0c54fbb614630570ae1db415fb8bf5003f112fd0d8e27d7e22f62a45d942104913bdb17102e0cfd6dbafbf3f2b9d321375695f05fe759ad2e3e304e207b41332e2c69f6e6252b18a0989ceaf33e4e4ce00be24691ee8e7fbfd5704b78c5c741ba52a463e361cc87f85e13b43acf927385cf4dec69688c446b3a77f80fa34bc257ec14bac79b1e6b541e0493ed253e4c89f0ac85fc9e31b309ef8694f776461c3aee16303d31a003f5e1f1d10653052529ab7634d87f18817fd46790ff42fffb6422d1a260d8c8a12e996f4cb48f1fc131909f8db19a85b66b498761b7f0bef23da228a4a9c19be03e207a922097a1b31e7668cf47c47d8bb053371b64a403f3eb9876b4ec22da14dccb0582c43f76d8086ef00f09db02fdd3983ec772f55bf0a41b13f7ec4e52f63807ea47fa52bb4fbf9a431108ec800de88b97b8c0606e80f31c41841fd79f3f6fb26f3abd84fa490c45dfb06e0f71385b219594d27f39ba57b73a41dc15172f0f901e73ff8aab54c1d2b08b78bd49416c581c8554d6b02f0277166482134c37fcedb23fc3220ad1a74f3fb6b20c02f43f7fa1093fa1030b56960bf88547b895ae03203fa54a7c524974e4117c1fecb4aed15cc1f5dee3529c000b7b02ce104b3a6fbeabd2f75d5de3caf0945c92608e89f7267481f0528daccfbfd103d731864303cd780fe48c3ab1f346c3ad14f7cea1f745e181b7756ad7a81fa0257e4bd5a1fb7dc406f59198d95c73b8518491ab2ff823f5bd704d670b3c7dff6df95149fabf997ced11f01f14b9da80a267880ad97ee9b6d87b5846ef8aad03ac04f7fa70ffb299ce92dda74b3138740c28664a5b909f85bac1c7efd0d11a0eed2afe1ad08e3fdb446a35801f40743d9f2da58ed11637ac03f0eebfbae2294682901fc826a8f0949e5fbcabe0ea2c58061a1a35a829f6840ffd9d8d7207e0b528645caf76937d671720fb3e4c1c0deb2b63ba673aed6ccaccaa7dd0399fd6e3ab6591720fe056d7764342be1c4f2bfffc5c5525f88b2424400e04ff97f5230a648b4f851abcbac36eceaabe7944b40fdeffe7dbfdfb86357526def7ec9da42689c067d1f985f3f4a7394b7cca6bbdd477526d556e350fb1d1107a81f026110d5f1e333c445998465b14633bd207197017f4ffe7dd815a9893a843309af5d1ab664f37b3f41607bb164ce0c83279dc8ac9276819a069df71241923ff0fefd9e39fafb39ca7982dbde1e85ffca48b3379201f5d5d2f19e52f11b8caf83483323377eb380c5e30ca06f38c26498d5419ba7ec9194fc49ff8beac234720ad49f3ce4710a24ea1fbc4298051b7ed6e9b4c8ab8901f94bfef339d48950a79d0ea182e67ec2697c9e8b1a806f24d8996d273408ee783dac39b4e7d920a4d43778ff8bbef07ff18bff17bff27fe1dfce8e16bd00ddc77fab3885fcac57765846b89f01ff0b2d85ae1efbc62d91b37b61182a8393e6ce3e3e509f1484857c76964ac4d3380eebb8f341fc98699409f8afe9a7777684e184217cfc71bc1a3ec77156dae000f8c4fdec21b473e756fa00f26b0734e9e2b03f8d753250ff6949284b4e36a0da7b409ab361d47d23f03105f83b262ff632f7a12e97c52bae3efe2bafa9cbaa10c05f7260e2a561934f2af0813d2e387f90548079f51ce8ff7d645894fd212178a7e17ebdcdbfd8ed2c5d3006f8fb6b63935f1d66c21ccffd6c32dae3a80c7666fa80fb63721c79bfd1a1722bd11926ffd89eef3b973f76fd5ff25b2fcad3afe0ac4708bb275eb2179cfc64d7080ee0fb21c2c150641b98f8a9ee57ee79523ed42ff7c513f06ffdd3485fb923b8caeec24a9d9f75be7259a06a01f8c77e980ab5da8d05449abd7ffc2fdfffc47e666004f0a9db19ee8dbe5dc9991ad4064f3355444d4923aefcff4b7e01a9687332486e3f9250ab449c42d02f2016e51ac81f3690ebf1a6a1af9e5c63fa5754e4449dd19610807f86d9d8cebaddd3ef74ccf869fef34d695359003a800fe3dcb34f1c3dbe2fb1ccde63b13b58e577201703fa2ba3ccf67ef8ac6ed051ff04879943d3eaaa212ba09fdc6d758cb9540ae1c7c10a2bc59adeba906ec203f33d8e663a5d503cf529be6051bddfe1c67ed876c807fceba773b8c439d447f222353578944c9cb644c39200fe7552b0ea7b5d19aaef976ef8f881f6c0de17a118d0ef48aa974aeaba987f618f26c2b66224798cf57800fe9ce59d34895355e873ff08a7a809423312dcdfc9807e2de1ac91235fd4512a934e26fee1c4374da3bb0ea85f5dacfa338702728f5b1b4c7fe5b5bd53425ae206f0e148585f12f40dce9dc74778a98be3e69b1dec56207faf4cad9fc02abe5321eedac2e74cc56ba80d6906f2ab36232e34dae414d76f23dd92a54d612ef737c84440bffefeb8486de941954fa5d94c4a0795839a2e0adc0f353cf26445051448138a27020ee787a163d131c05f7b307e27eb12fdf846e57c6bf52a98a38c3d38073cf7dcb6e887ae79e7ce1fa74c7fd8ecc48192f300f0172e9717e4761c0ee3c5c1c8368deb88ebd93707407deb1dcdcf67eb41cef69ef20b9564fe3c774c661be0bf930ef77eb2be3f88867f6718d6b0df73c6f13806e0e79c8568566b5571e7719fb07b98f031b6639360801f64524fa118a97c69dc9bff109bfe6ffec3647f19707f0bb55ba66683cff1f44d8f8b1df5f0e15a36ff00f5a9ab88dd3faa3c8b77f772c8e27b3742eae9118d80beef0891a7ccabbc715b44f192a2fe52d666713402d4e7aa663b81d321367b33a9aafbbaec76762b98bb803e44f9741c97eb13de30a0a2836d8c5e83181e8c00f03b0deac2369abb44843aed07ebbd02bdf0aab0fb807fa3fee6263dc03e47382225b52d77752e28bd340c883f9b5cbd036a47140d5ea82b25d775a129aa6a3ec0f9dd944d281daa75abe1df62eb29cb64e05bd1fb0c10f89e739f32f24d0eb1fc594d9674a091080377ed2400fcd7d3cef7a72552d5a0817e25696a4307018d13b8df2a71ed67668b032a37c6f6c1a1d4b8aee4d7d3c0fdd44d15f2f69bbd20b9cc8adfcacb30668933c005f2477e8dfc7c1083f5127b46922e45adc77ac0d83c80df0b7c790ab9721799f2946761bd47fd6eb1eb407f1ab20d44d5aa130143641e32397c3066228fc02330ff86e7be87c1f8e03756b65b9e4ffbb1d567422c6aa07f42c92dd293cac4254ffff43d16d791de6fe4420a20ffe1d350d3e6d6d7b64bb7e88c5db95b58986a7d07e65738e9d5bd76ef04330ea061be906d1f29cc55b402fc9b63908d362fb49a08cef079c07255038c5add25703eabda788dcb8cbc9bd98d61fabad84e78066204007cd2ef83a17903f97ed7219f70e3aa95ca8e03a51ee83f8517667120278f64f59786806c9f67386b3f2706fa0bde0be311fd963254ec83f763d0938520e55e3104fca55bb0d5a46df127b01d105a27769306d64779592eff2feb5767093ea4899e2bd3ee1b8979676a743fce527d4060e0609e5a7593a0b9f35726829046141d1c09a504f8a1a779e4e013d8a9a5a69f506c9e817048fdefac2cfecb7375899e1202ec9efaf2521d73db959db1e1f315603ee1d6da0e93516e3625014114e27aa0009b608910707e02222908be6ccfd8733e52e741079e387cbdf80cdc2f78e4ed4d130222dcee4746dd5fde546865baf580fe523e4a77ff0b922aa90842d8b59b94ccac466314c0a7fbffa65434eb0db20facc5563a527e44b5593103fe813ff12267fd8e3532263655b66eb32e38aaa77640fcd80f679428d927187c3650a59b9dd88c6b0c8804f4cd0d83206a5d0628b387b245036a48e818e10c01a07f845c4612a18acbb946a1f81971131a99e0bbb22910ffbefa3ed6b31d579c9b892fa662897ba81f322a02faaa700baffdc2e762675ef5dac1f4c1b2320d973bc0bf2456969816674088e650f18c2cad149b5393b004cc5f4bbe858cd7266657e1d9be88c4be8f8269e46b077e7ff2e67dca96f495371357ed47498f6414d44e22a03f2ad09641a5be4a704eb7ba94cf61b5baeceda60af0c7f8982425dc14ba28c98fb5797285ffd54b0e9a03f2175c78369b95a065f5e9b4fccf58c376bdf9b40820ff183739f2975c2eeda57453e7a96377ea544e4902ef1f757a218aa5eee70bbda2a317f48f8d16abaf7f00ff2e86fd41a089ccfb89267a434a16fc650423b21a809f82e29bc7e1870a316f96f9386c4a9411ea1a1f01f0637237bd97cf87e6bb8429cd26e39ab1b6dc4342c0fd92bc0c7c70e92b13656d55bb8af55779aa62899c01f495af08fd52a4559c6a87393562c7a986f4a5a2c07e94efcbef46157918bec28ef979ba3511ee72c09c02feaee05aa83f5f8c5f47a6e9be37f527dec82822d302f331fb86cde3d1a9fecd47882e91ba51fd49cf632d06f815e5672d957f0e9a61a4304a7d78fce743dced0a003e6a89600ef7fa206fbbcedd15c8818783870ba104c4c79fe42490414212d73e29a7874f6a1c2476662540fc58c22c5c6b31af6b481998459917a8393bd38705f8a57fae84d77d4593f64cdcb581ffc21d45fd595580fedeebf59c4afb8587d16d6e05a62ec9d3a86f8478807ed6e6c9fcadadef5777ef6f916c3e04846c87053260be191c4bfceb3fb583e26b57f76f9d777e7da9f83680bf317b59fd2eb7534ff56c2a9bbca50989da467f0de08792565cd575d7edd6fc264cde37caee68b673d603efd79180cee5d29c3b7989c00cc63628ddce528404a84fab6a500a6e186f153f4c4a1879e9c2fc5914c702f84ba7f728057b1f271d71b762a0babb082dcf501100fc99f920dff3614b377964226b67e66a8727d18b1e982f348fddbc4d15e452623e393e9f1132a9a97cf61bc04f4411c7c86d7abafa79ebab316228c3d87f53ee80fa7d6f5b0857397b2e5fc2c922e96433e1b4af8f10c01f518c482981a4cbe54f671a6777146690b5d31f81fe39143cb730553a81915b923c2612b29f689501f1c0ffd783ce19c488723096a56912142aa02de76c32017c7f3fd1978dfc495afbf85d0838d47773f60a6eda80fe76dc8ff4132141dcfc1b0a331f6effae5cce88a102f559621bdeecc7a6a1458fefb3128eec7da529dff9007df4b339539f237fbed5f16e1eca7440e447a20e28607e4bdb15dfdb93d96d8413bbb1b593da97e397ce3440df82d8cfa5f8f65aa57bc06bd834f469325666005cd2f8aa3e11e6c94a58ed70b4fd1d1e13d3b8024717f047158b24a9be0668bcb8a28543af7041ad0fdf8f03fc157b55f5a1f08a44eff0472e7fe5bd24c323e167801f4bc642620ed3aa09a143eb8e91a72dcf4f16fc02c4a7e483fc87b493298cd6e51f9fce8f4862488f5c9f01ff238adefd9f6b4479bae36b48086e0d9f8eda6400ffbd462ec6b44c1d2f5474560590223263e06e3202f541306b5e0977802a1bb90e1469fda6989333b22d60f0977836acd6ad79b16e186d37f963fbb1c14efa08c87f1ac1129f5e345ce3d32fa435cf282cd6fdd9db81f9f77b83591b74c872d78ca80822e8ce33471671e4003ff3f73c98ef9fdd9f0801b542b836a62c1aab2a5ae0ef53926c074ff322ae526752f71ed8120466903101fa692aaa9c46d56fee42afc4a0cc335e39f273943ec03f4351b8265ca2ba802950239e01396250800bf502fcfd6f731fd3155e9df38e86f5585762a2882dd972007ddc594a3fb65f5caac9cccdccf90e328f7c7a0bdc5a1a61d1f2a9367a1b76ce69ebc615e10bad2abb0cf0f731b499b87b3409f626feee46172566c9584c4700f7f3f3795a554d47f5a41c114486f772fa22c57a31e06f5940a70bf80af96f7eb39134ab11b600375ec01de0df46ad4c24179ce2f916bf06676c9d391ca98df902f115e129487e799b87c1275ac368b8aa23a33c9014a85ffcc3c7cfe75bdfff40a87030471a3357fa59dc0ad4bfa30e5e7dfe33354934f8a1fed9643dc1e46694003faf1948da11c27ebde53f54566662d8cdf0cfb704983f43a914f1bd2624ded87d80826bdfa9c07f31480a988f573d899d3be515c1862677a9a679f5ebb70d5c0ce0bf7cb320976262a55d90b4452d50b08a82f4c9d906f2c3d32ca9b58fa57c39fa31bc0b820a412b8bc404901fd7fa20b7e09913095fd16fbf52c76d561b09b303efafd412c39bbc42c05a224da9db199579905d2900e0a78d1c5554b4069a8c97082a590432dd8644b1e4007d05fd01414d00c6e4b8debcf51667fcc6197b7101e82f9a96ea5e9a8dcfc03062744db80826f4354f1c01fcd9becdd7efa72b5187ede08e201ffa18de7f701207f8b36d75fcf59c00a234f28ec2b3e066853fdf1a6e007edaab33df6e285c8ef43a2d182155fd94dd8aa817a84f987b085d33c8933ee9a0be09bbae649a3ef93a00f57b0adeccb7d487bf5fcf6861a2b1dbab83165fbe00f317cf7bae886d3bde20457d8ecb28a6cdbfbd21ca00f8663b2c1fe3f5237399376de9440dc6a9766e322c30ff8ebed3777fdc96b5c9c231edee188642c1445115e09f147eb77492b638d25e21b6b062feb82358969807f405af771643063f857306c3ba5a8c30157f18f87700f9f7fa0d067640c2cd5941032d997c2730a1366911e0d7e6ac7fbb8fd2d8f7b5c8d106e747e439c08bc900e74b722d7f4c6cc5f76d42ecaf0a3e36ace6d9d8a324109f38fb6ebf0ef71cc2d62f509e6c04c19e520501fd7792be7244124beb9feee50bfd8d907fbf15a33d00f513f506e91765832fb6d67038b4f9bd651f30fa4b007f2fa6f39df18e64367ab41acc39fd49b3bdb1af3e80af634ed2f6a16ef0d490a07b5427091cc213827b80ff3fd1376cf27d78523bfc4c85b9213236afb2fe03e0cf1fde798ffa80e1d01d19ae13fe5032d91cf0131de88fec373da75339cda5ae567c69a8d8fef410fbfd2bb0bfc1cfa48d322158fe96acf45d726bd4fadc587424e04f99a59bc7c5612958f1a5ac3b990d99aea2254605e6a3f28f042dd2af6f8a5945072ce673948cefc40d00fc6f6932dde34f4931dd9faecc4d8baa5bc641975cc07c236dac230c5a4cbae034e6354146031113d1e551703fd56f69df4e4d92ddc777e7ef12faca259b3bdd017cf5cc7c37dbb58965728bf38337170ff7670d921690bf27b65a4a030af93776cb963a282908358f50bd01fcd8db4686e414741d2bebbcc6efc5f64cb4ed5f00f09b2d7e370423c65d9a59e91f69ba04d1dfadc72102feca69f4f45d8e475b52b832d96693b12782efdaab407f8f4f09cee8e6ae8bb533b6005f856097b79a8512d0ff4f99f0232033ead6f233cf9bb8dbbcbcc3b60116901ffba38330dbc3f0314229fa99c48c839548a4f281fc7c6984cb4a81d9765ead5dbafa0d36db2581e007b03f6741a3935baf84a84d0ac96859d7f184e004031b98ff82479f3c277afaff63efadc3badab6bd71babba5a4a4bbbe7483200d8294744b77b7740908484ba9202125298d92d225a07477a7bfe79e73effbdbf39eb337c7cbd9efbdcf7dcf7a1eff61b8e677aeb5e61c738ccff88c31bc2707973f51fdf84999566bdb02ac0f6f49bf8b08ff459737738d612bdd1d81af50a0fd7f45bfdf667fdf66dfa9616475a556da575d1d329e9cb92f774b2957b7ff0a3ee6aadbe0bdf75d0f566437a8d3c2ad61309aa51888cf14c540d123af8a1265e59998a28bc730557d4b00fc9b639f9efb51fc6f58c9bf3e7ea978d1701edfd604e4279f62c047a4926a5dbdf5a0ca796d52a8cb2451018cefcf753e416291576d9bd549443b148d7e24f3ec23e01f760df57df92c603a3a8cde7e1a150bdbc7f629ee57debf2fba7b396fa81b6d6c04d94718e3d2e8673ecbebbfa23f373ca33772be0926add3517dc594bc6af15f8603e21b1530a7abf9a39b4f325467e37854105f9e551301f9edb87c733adb76f782dbcb9211bc200a7e1e7e8e003e62ed3617aee0717ffa0d69734364799001f65b67c0ffe69b189c61a535be9a167ef35d3e5d075a6f6002c07fa565cebb62f26b96b45e169ee5c41e5364db0b02fc8cf1dcef616f09cf20deb3c41db2bdef20525287c0fa5544547723558fe0fdceed9c314a4fd5d65d7704f8cf2bf59f879cbc1e1dcba80d7e1452fa5cc8f9750cc0af0ea9d503092cd0e408bfc8c35e99b8ca890771f5dc36fe39cc564030e5667e1dccd207493879a83fbe286e9143dff17ea01c9b909cf14f63ad99abb261814009c8c7f14d470a935beecff923a1b98d9fea2dd3bfa55d3cd4120cb5badd2dffc7b03e8b3e1bfdeca197b80e827fcb74d757b11d9e0171d54b6f0de76feb5456ad1ab7dc8f728b5cf5df5f32ec3014942b805fd14e6bd1471c6715ad4fdf4710fc8187362562ad0c05e59a7dfbfff9675d41544f8473d88c4c45905c906fa49fc5ebc538151515d5af3a7c5652e86c3bc6969444cc71cd2f3085167aa056ab4f615e859dd97bd0c30e9b713a688269bd9ed640da74bd28b59f185751bde0acfcd43eea2017c5a03e9657711b7e4da6e2759fd58ad474ec4777d1b9c582c55246526b61251ea618dbfd4d427331e8f10846a6a03a923f9ee06f96fb0336efdb962bf0a923454c6db3a70b0660e924af9626490cde3a092efce1dd706d2d4132aeeaa85d2b5299b04b21afb81c661cde22ae0d918dcd262ecc43ac03a5d66e93df32bf5b962b34d07d48d1b9e42353fb2c6f84f9f6e05e055e08f46b8f7f9fbfec2694dddf7d3afb5f7ee6ffb3c5f33f2cf009d11bf8a9b205eab3e4e805c94c1b6a242e41dfaa21a0c46e79268abbddffc72a020a0a0ae68e2aeeb6fb0dff58fcb0e53fdedfbffd96985cb60606cb55c628f1dcd1b5469d5a6836d2efab0f34e4588fbd0aa062c9395a5f4030a9947e1d5a87d4fdf3d43b6a67802cd648cf5f0a6d82d58d77cd988248ea4527f0930420cb9b64f4c9e22119d7eff92a5f4007405d8c4faf00c97edac63c11fc34b5fb38849a8bef7afda99fff6805c026a5cc93b8494a02fa0f4f7a8690e1294cd7a44c006371487aa942ca1353cefb64a9243e634dc21d720d181386aaa6e61bd96ff7272f18f42e34eea76cae940364d4c9cbfa03cdcd753853bf9dec7bac24c183e66c0098df12ccd64977d4ba421c70bc0f33d9be954b6905180b53e7a5cc062d297b62df33f0c84fd60452e9f0013241561a0d32ebaaed45e5019e01228b8a06ffd5150096597d6dd216ee261459b4ae22b84f792eecfec619202ba3ec22477ccd9d74f13eecf3f942ac778c2e1805042319b149e128141e53e2cacaa3618c9ff53d9efd09385b4d9ebe4e122959d42c27c1d426835627d28b064032cb8ace6ba940bd11ddc32e9689d31f6c16309f48816099c6627cd6a71c117dd5c2b19facbe395930688d80b13b6ff8b68ee4681987b1d29784c2b95b0c63f23b500cdfc859c087e1694dc78cf2c3399a914a33f77c39c05948aa6dded54528fc9a8ad6a86a044f6885832a0324e3da6a6270876fb2e496ba6a5322f984a67cf29c0182c510a3fe2082c103a71a2324698f53a2370edf8881933333121e279d236111ae6f67f954234a7883840c60d35c612c5a08423d6d23347d498a69aca585faf827b075d4bf9b997da841133e9c6437b71b385cc310f402c09009e5469937cd3b5aac1a121eaa57d48f8f2d29003065cdd11623c23e2decdc58674c8ab173a12db40b501d9b5557e422bb63320a35f26f34f41bc3659251816424388c07c39a0537300f4306668ed14ffdbe59694300b0469d607d3bfb3da761e5021f65a09d6439f33540c6bff2e1c1a08b8fceb7ee5dd238fa46e55eaf5b099079b34c8df0e3e24622e8e4e9a2dc8921faa11edccec0fd1fe29dca77a211a8beb39ff1c4ee40e59b5401c18a42943697175ae21a2e88349735cb8228fd22294031edb917d3cb7e8f3012db1378e57f9af0d84a628d01c1346205f193fda7557ced5a52849acdc9c1a5e873c0fecc3bc07e4ea518ed73880e89f07eab6e1c33c109384b1cb874a474f50cf1b543648e3e18d41b08e8c700d83628f8209adce471337bf8836ab9ce4a2dba07f4005804e75f7998450821b5550f575cb3c5cd330af7048ac5850fd3db67332fcaccc9d3a3feecd3f550aecb04f45bb003addb9bae72f9ccda41ed7a0dca43257d37800c1a4918c163469fa51a972a7c05f195966e6c4e00c8522e9ec4305f708ce46a5e5e11b4bb31ff2ca9dd00f67fc798be1f99df4d510fdf69c996c8817121cd7700ac6174e3dec6bae75298a2d6c7a7e7ccacb555d2051463db2c65298b1275f7a611989749a8dd5e44fcd408348bf5345ead7938fbe90371de8d1ba3653faa7bee4bc0dc78cd8119f49258a39ac4648f18d6227ae764d20e70e620945e498a5f595ef04e8a61a266fc8451922b04c8a6695086d07eba55cad473b4cde36abb18aa5dc8403463446df4e5d32f4b2f74bc1bf649f7b7cc6c8a4201b099d51922928da124f094986add8dd02497b1f8000816b869cfcd85fb4644d13e221a3a3ad62c3d4e3802c8b0087dd8d0757aefe94796ea77964fd0da0753df00c18ef05a71b899ed77ad57acc7175fde7da29cff100724030cd84ecec49787c48b33d4a604203a5b38798702c9326fcc951b5c385ee06461bf1208178477268c1205c002e9134a6fb517d5f8639b7a17abf8f3722a615c80775587d5c31a5bce7f4283e9176aa395144b77180e1463dddc5f55cbd91d61ce8fe449231f37587f18b301147397483af991444262b442bbd779c853274b33a40c907585e0db3dc7bef58cc49066d5fbe07d9bc8b8c004cea71a0949b377d42b90f71c7dc3fbe622316ae51ec0fa904fca08234bdaafd738ff0cf3bd926208baed0a30374d5fbd24cabf6a3e86fb8832b1477225567b6c0738123fa1f909aa2e529fff6c1c737e32f98eb6b0350c086628bf4c342827ff212055222b49de153b32b6bf057c1f5f5fe10d128b1c338750fb825a15eed234ec63804cde99b3e96a276da74cbef0fab07e23cd26d8840970d60b464aae21d87145a70fb63ca85cf8e709932144bf95bbfb5623d0a751d79f5a477a49623abd5371a901ed34ea090cd997bc474109be9322925c2c3bdf5601323a07966ee9ecd150e4eee235cda5e441d565153ce0cc872cce66aabc0fac876b84527daede6fbcfc4d09b06f74ed73609fbf666a105c9a197380df2daf407a0fec5f95a7757616fd591a1c464a861382b2b83059ea005819e545238386cb5f758a937652ff5ca5d1118a07d45f8c5df31532d09d21596a1f3d254b9e065fdf00c112d835e77a5f12be1d9a43f63c8cf2d97359a45740321427a36cf7b356d2b5b02668d3f00a8428931d31607edf69b43748856d62423607928d1f083099cbf701fa3dfd09719ddd7c2853336f33354d6a04e552cf1b2058f492e0e0fd600e3b6aaaa381af87456aea0a8b2f40d650c5b7127c2420fe5025914f3eb1e064e85ae710f87e58a926aaa42567f866d5334fe5c632c3f53b5e01f65501feb78577b2eec65aebc9731eb08d24e72a4b00d8a53aeb3b5689ce2b86b64833e9937c79142a5e00041bb01f85701722330c6c5b0c6d624742cf137d540182892eaf3211276c4f0eefb9e9a6d1d107210c502402643991f2366f05be5669ba4e42426607eaf9c2631db0582c527ca06f442a1a4d11bd77b1f51ae1248c08703e635bcf5932701b575e7f3825938cfe4469648b0038f242394bcb2fd83cc6d830bec9be522312b5ec0591033fc53aa6a2a30fc6b3349f23b5a61731c80844816050c5bba1cff0bdf2aa5425160fb51fd1764f7c3f01c0e2d7b544380d868493e999559619c472696607f78060f9ccf965f50bc7982ca50fc81c5c2d5b6c0446b240312af6e46fdb9f75fb2e266cac2b4660b77e18bb4b02f6fb616cd623eb87eb289f74ebfcb2128478c82e4881644c22a3a3a4f2a3e89286b6059f7117a96efec5f9089873388f3058ece8184d47d3604c09b160f2a9e23a38d37c858cd5bb795dd086bf82720406895e6ad0e7f07be979f8235168b335a39989fce7ff73551e903fde9e96f8783aabae114b61e28f812966a55b7e32e016b97838ae1d349e6a9847d3c27a1a7da55f06a11d9fd6c2d96b78db496de8d78b8d14e4f2d72f9fdc37da2f59080aafa84d27192e28a449247674a43773e2645fbbb90d3781fa93813fe8bb211db7ddffc7e2dbdf3fcc6da8020585817073bb4d9e0afbcf43c4f19c40d4c4109a8b8622cda716817b2a471feadbee886adc22a706de0fccae27fac42b019eeb101f7891d9d22474788de4bbacaf7f607d8a6f8b87fb4c3f172437acad59801a8e0f92af5451bfe12ddc901fa9e6b409cbda531f52f53c729ddc350d0fb21d3ebdb4c776b475b4fa44643a153bfc88908043ef8eeb4f5cf61dfc84de77cd23d6480b5f1bb6387f3a15ecfa6e562a389b8bd32d83e198018527d4c78a8fd93cdbcd206e2b1f9dccd4dd70c688ba4bc91e2fbddf0b36573320bd6d01c29ca904fc45e7509c07d74a21eb83d40b1c8eb99a6397ac93012bc95cd7d723abb2f64aa57f381e5b8f480bcc4838345a73b3d2a845763086ae69304ffa70c05f7f07e52d5b3027ccc0dffe4ebba94455be1a6b18eeab7e47afa6c987c5d837e17ff83b884b05b70c79efebc0c3edc76f43f14c63daf7d3b403901d1067ff6844f884a6b6969e80602c34ef90ebea1c1198d1701855419b1fa96250c130237921a82855b5fff618ea0121a89b6d50cf51bf8f8b40d54abd83cf00c8186212b6192fe43135d2586143897d31a22b5ae0bcfee867f178172d5a60cee138028259ba7a82856997ebe068de27ab4cde51f3c0df0d2f840eb89be680f9f1c772d8db347f3e9a5cdfa73ffe2f3367bf27a18d8d6389b1e66280fa132f731b46ad3bdc1e7265ef61f65f7dbedfbb7efea7eb57e5bf70997e67ac06163e2ab349863a1fee5151a607a4e784567d30e5e7f35f19d0c7d821f7505aba42267e0a3b1ab1626affca3ef79f39bf30a8e92d9eb2b8b36882a63df130975e61d75755ff8df3a39148b6b1bccbf76f39eb0d0866c228aeedf84e7eb0ff5b89e089a5140dbdc23ddf7dc4d52d4d5b88d8cf010002bd924c92ac64649396cbc0d43d1029ee78ca6e0ac4f3a7abe35dcd8d82c6e3eddb9b271977567cc8512d2b35f3f78d2bd48ce1b3f22e7977d7e1e3d4939d48c727d3a6aecfddc562ae826e332d657c76924787f868a4fa5b98619ac50c2df1496ea82f7c642fc7578ea379642fffa2cb09160d83e94646fe5697636363e33ca6ecb1ed501b8ee09008c8390b61fcfdbde3dad93caef803aae5af410231fb3f2148201fddf4cc52254b6c4311615567a8b76e842603a8d85e828887183d4e5d48661b832edfcb976e9d020580ac6d3921163ca34191a3b9c37bbb53a474b379cc0048d3dc9cf21581d86af3aafe07fef7fcb41798fb2500a326fdf124f57078361a218141e34d399cb1963e2ef07c17ea3faa68e13e25d571a11b9e381e2554d322be0619219998b993b5f0b5057189161f1e8d4c5a8501204f7a8ac1c638075a022413b622685914dfb31c0a18ff007b4fb191cc55e7fea3b37b1f62d3cf985cca81f793c05a5d8b430d27bb37f8f3e28103e5888fc33b00e437f47090b22fd25eff162a4bb48a767815129a0764e4d83f27945f092abef0513bc56e8b62e570ae5a039cd4a761c179a1d8aa3f4bc9ee0d6b4347c9f9fb98031571b2c7fb45329bbfc432b9462f424be3d20f79e8038c0b4b0b76e17b04cbbbbc1c946ab044aeec3cd05a40c525de280f1de19b5743728e4d8cc29fded860bd83001d813d12286926fcaee06eb0dbce5cc5057196a84a004fcb83c38f2d181ef5dd8ea026b3be41f86c74601c00e2219394c545ceefe3cacc95b8b63eb45bb55c41029cd87018b9b68985768da9d6fcccb21b9543f581874045b246273d7f31a9530b9eeb07188d2efb29de613a00636a6f56ffadd45aa7576ddcfbbce918f7a6e3b11e8091d8178082c0f6560ae724db34307fa3c133a3cb0aa8a85dce590d63813421bc918563786ce8294cbbff12c8785e13aabaced473e5e8c63f50c3e6628f38e64d032a029e96140b15957b67627c583864d8dfc9edb97f0280843afc395322d30e7e0fb43f6accf6f1606c7871008c97f568186a8c872bd6267992d18d3e997afe949f818ce9e524da84450ecc351e024f962024844586023d607defb828bdafbf2144b938911c7f37279c54466a0930aef06d1fd5d66b5ec9f56a957e7a548053def69116c8786b7ec58e344a9f88f41003a372af6f900553cf0d0089cdf63ed271519839efbe9e8f7eb3309c2d173100643c5df559d8d455d31216592e8b1390297befd57400fa0516bb216882b37b3f8b98ed98fc24a741865503a8e84705d3bd72441aae5c58f8aa809f5ffa710f11020002b513abc895be9486a5298f0fdd93d63dc3611802188328d7a4ea3c9b5e8a9c1d5913cc35e7c751a2eb40102aa0d43cfc071c49e2cdd6bd8aed88da7c74f10f80bd395937e5e44e10842f60beb289b33c707071fe0d086268a40c4854e21f644920925193f5436b4df37c042a7eb866bd96a3d020fd8a443686d45b80aceff61215a8c82e5161be9d2d198418719d28547e0af31c455c10e808c25a322486d26e9b44357940a4da87ef5a6fb50c646c7fe73efada4cc4304f11827ac08787ee0a9bc707fcbe35eeac6c144b71a26739059473e6178a7e454a80b1647ff9ecd384dc7b9a998f99cd15b1513d540e39c0fa95ecc5862aafeedd1d66b869fc86fbfadc035d1c08527cf3d69a910848b74fa3a9e2b749cef289129804ec6d83ea778ff73b0625685579c97d5dbdb76bb022818cb7b866ace469a6683751c8f5ccc1bc7327fb640950713810378e5110c7bb162fa8a8413ed696b9952f1fd07f07092898619cf78989c9de673dcf97be5142890182d04e294dc5d8822d1841fa5e45575435b930fb6f01902bd7e3fb773e8d727aa82dd489837752922b2c5dc0fe54dd4e568f6f2911c8e5b84ee50ff0e148efaa0718939eb583f79a105f120557713039ea4a172e04e20219230b58828ecaf5e4290b0130d710686a5a579766c09d6bce88d8cd5cbf886ff8f88c39e9a70d59d6650910e4e3b65a7e7588971043fd95707149b6ffc590530890b15dd0113efcb6d1dc248fafa8317b2f41243e581f4002b60a2352d9e9e35c84a5b86b053ff57cbab4140740ba025d1caaa832f46255bf323ff2056c292f476520485e562b21f912835d19eb92123dd0f266daac9b0ba8684128beb54174c2ee6dd6afd81f27262a3da8100580e4767e4996a79a823338c2b1636e7d34cf1ff13401ebd73c013d8e277c7fe8b13b69c266cf475ce3543023603784daf6041963322ca3fb78eeebcbce11de36e0fe20e588600cbcc04347b1723dbc47acc8ab17ea40467cdbf93796efb554b5dfe9cfa729449318a8b679005b1526ccdf0eeabda1b7ba8b1dfffba90d421f8133a0a203e6968b6443e5189db1107b666bf8eb990dbd30a0e2f5b2440f8917c6c3bcf298b5056f3deb984f4a42c0fa0c4c9a55229d626afb2c527fff93ff7dfddcaf9f81f52dd7bdebf54c8a5258228a6957e51b072445a117c8381852717adc98909a1af8938ffa8060e21eebea11a0df2629e1fd4ba22edf3dabca5570cf992439ce37058218eb3b2f5718bc046d1f16a8df8fed86bb864b960518e7b16d266444b215958b8b6fdbd4f8f35d589f0d0041a285eb05731cc457a3bd568c040289dd8ea2c5be4045c49079ded3e6bd2c4a5184304f8297ce12d35c4b00a31cae0763b21642918607596d22a1daa43d1381072a528be26fac6719a4a0da3590d1b72a7ea6fa101100740cdc3642b3f82247b9e3d9ad7edc2d96f21e01d502a898c2f90c1e010eab2989e591a1fda725c3a30e950020e3b47cbdb0e7c012b1cef0754d7bffd64b6d75e2d780bfed1266f6ca6a45ad3112bfbc0755c2c010cf590708c2c3bed2696c2d1941c7ee67a5791238ba67e2b6056444ef7d3caa499194794f1813a719e2b544e933250b20c1f226704144099373177d8c489d87417d1f6c9900c678785344eb531b1d525fe50016d550cf593d447b008f782596ab7f266839b2d4df86906b8d2394e2e80c0409dfc9b2310e8c593f4eecab720b52bea41b27b001f49354fb7b3bd67c0625b99ba70894d8d7a8f7aed901c6bb76fd9187c0f1cd49eaf3c1cc981546690e546e607da5702d9d8da53fb00d5fddcae9a6ff8412428b05f88a0aae5e89f771c8aac99060cfe7232b93c8edaf81206a9175d4c263463ff914ac2449d6edf0fd9406522048d63f278df56aadafa3e8b06200eac92bdf88d7794090714b963da516499b7768b07273d3e175d9fe1781a8aebf3833017ff56a20903fc1ab615169bfe0668566e5e22c9a59bf9f973aa75c05f479d0639e7ba6b3b367163ffea0106a7f723d86a05f1878eb430a906f288de4ec756f8e5873221d65dc6a00abe283b904f6bb62c7edf5a76e887b6e58d5c1b59280d5af11321d6e5723bd3b96ee6cb8c57c24f2a3d81fc07f79da9158d723aeaba90bb6b05b87d5a4efd37000bfcfafa2cf1dd719222fb223386e60f8750b4bf40b802011d8e8915b4363e08868d1f1357fd52f7fd25f0820383f5091b249adc9346790e5436962dea4162ec203f3d7288b1b2a9bb9ca732b5d2cb9148dea938609044253ef61d8d4d6ab040dde1bb11dd6cec9cfb7a77601a17fb9c170b5346ca962ed540705aafb4cc48ae6ac409f8bd688ee5da631b75a4be84797e79721a3566e7e401dfe65f4d982a9dd265ad5c9e6eb3aa367b4af67e7c13a44b630ce374e7a513b924fda36d1d3184d492f81f851137e734b65f30feeb765d32957b668dfbfc85c023c6be1cdd525c2d3cb2ec28244bd9c0eccac4e2a7700e5dbf47bad2ec4cc5d87598c6011d731b37fc4350de461c233dd57a1cb3de988ea2225924aa4b9f254d002a837654f13cd0d82b028cd87dd250b51382d5dc98e80d0b6d0a8933d45a529ca0af5c327d396172cdf18be0208c94ab289b0dc9c058d7c72f4b08e4827b903871790c7db2c4873f966e77290b0f10412159c36459c3c06f4d921a142d4fd7cba12e9bb9311889fef24a040620ee451edccc260fcf05fe63af7adba7e85e88826653a0a9c2aba236116cbd9d6d18e93f033abe2ab9d643dad80571769f286ba76e43ce8426a0dba1fa7a675971705f03ade40e908343e0e4ec856745cfdae2b08e5670c05e4c1cc6fcf13d40f2ee5e49f43da3ee292adbf74eb00709b4611d3e016286a2d7c062a2ca2adb4b91f2f2201ad9679a65c3d63ca91668a1d08295551fb7836830850db3a5760f69e78ad57e9b6e491bbca13b13f2df703f208de3a89648896711d5229b028696bcdf29b61720256c11ae55ed5bb2b3f95c22f14d3ea4fd5435f655202a7f6e0230e6dbdb7b819e484a96c1b0fa97ada036c01ea8d47af98822f1ac2d6c5375c5c0ae6a5509c590d801aa19254596dff63661a2593b7e3cba327c1a9ccc8401e9272360b397e122b5432675ee3d52303a176811be0d40cab547ecd2ebc5bd761da6f18167f22f12ce60d702a3e74960e516e2116a1e9f8862aa198ecbbcdb30f206cfd907783e9aafd698f903f671377bebdb6e37f0f583549a3cf6297f729e74e1eb47fe0b5fea2339ec205ecdf51923ceb57440f69592e73886de44413bfa344035a7d9b10869db42c063387f56069bf4e88523d3f14a8d312205a025f49ea83188a687a72512f96f7d06d1aa0967409f8e4c908ed768a3afc409bbe27f740c99e19f09a021625943fb7a513a17f984fa16cc73a54c6e006bcda082be2386fd78e81332d5142f7af3e4f5d79a8d47f2b27f5d86e4b6b781a9286a9f7815cc4843552a31fa036065b0d8f7160156226f2dc27f91498a0ea4dc00078dd6a1bddb6d9f9b925116fa8097b577908e6dc5f01753a9db021fe922e4682c77a443f9b420e60ecf9d500aba1e55bec4753da71bff55c9d16b6adae82ebfba1408093e43e5242c1f8d3f01ba77b4bb4fe8c5fc2f228803c1e3e55fa001a5617065a58799b1f26e902a36c6540c44726fec967ca9d0f5e26fb4f65d84c54b25999e0813ca26044ac40831d06cc0b652d3f6b15b1cc7cf526e0fdbc4c218379f88e345846f185fe3c73c363affa3da04e03f1390ed78d415939a1e887c7dd96fc4c9ab0df00ead3ca3dd896c5e31aaabcab5a12e81bad17c873c380d59fc4c6f24cca062d759f23d8eec68fead17a7d18804a75783ea9cb5cbe78f89a7686f2a9415f7af1b630707eb784dd43ee99be897eb3ba736275f5bc409ed40e38bf4fd0092825151ee8882b8e5591bdf2e38beacd01bcd6a9d64bacb1a865aa167359aeb9c8ae32976016005542930db73154ba37dacbf4e35dd785c36298562840ddb087ad66465a969c67fb185dec70611b446182035047315efb7416b00d0a7d0916ea88cdbc90a92b8002ea7876974ceb050f04c9dd246aaf4630d02ee046f200fa8fd8bb3cb9bf4be1f9cfd5dae8558d081dcf8c09607f0fcb9e567aaec64b0f430d41c75675d3c0de2705f2e06865595fedcf900a0e1b0b98896f0b9f05e8a1817d80a4ba88f715f6043638de0a0de67d993e497401a895b302d45707727d3848cbc25106825a9f316c62c13ceb2115c2984b9ff2ef6972cf3edd3f2fade66b04f43bb7a16c74f01357cd339d38640cf3d2dae88a75a00f64faab8f825c73c9d69ed1ce315398727a36c8b6409d2ca6826a837b9d3d2486d56f092d03960ba7ce30006a5467152982929e0f3dad833d0db582948582421d50a7cba79ed9db887fc5871ad5a33446b79580a50c19c893dca554996fd4a1eef72c165154afda9a52d0790458bd3424be24334413a7915bc62eb47438a7886102006a15197b9ed7dc89dcb1914c60c0e9914ea4dfed0a9c2f8c468b5983e5e8956a2f116733eda7ee3bb19800fc92a7a5b13c13254e44dd2a39a4bcbb7a708c95acfdbf953394c6d64fed2586b5b062cac76008e6f3475b01a8af165afd41aec34336b4de23090d7e812dc49b4ca08f921cc298ba171a9918b2e2441127cfb558fd6b5d60fdf290f23857a66a29e1ba6c8fbf66ff32b95a740da0ee92811f10a5a4e1316162a68686ceca399e7e2e00bceeb5dc4e9365441fa1e29b65e90c28cf72ea8f8e80d729dc7a3ad9fbce50debc9ee1d14cc0764965cd37805a35684c35929e18accae97b7fb4f9753eae1d9437605fb1ad7ba1ad3fd78d13fec13676a86142562bb007a00eee645a138ed3ec6ee581aa1a3ffcb2ef37735901df17df984ecd512b40a5705223fbc1c39dbc924d39a04e777cf148587b337ce31722e7a76fd451588795d9813e7eb4b60cc9e953ed2311942b072a5e71bba52f9500797f8fa411f42ae90f75221c25f48d55ce3c674fa08efe3e31bd5993feb61aa5c5498fffb9d8c413a8feb47398c4302ef35174cb077ac1a4ec057530213c899a0a77633ddd962ec7987fcbfd30a8770b0ddf962b73d75c1eb1ff326be8df5ff5dd484b771b1fe66723c51d9fffb6093249fceddf10a0506bfdf5dc141af1cecdd3c3837d5ecba1f510b61320fefd399e230e0504a3b17f0da6256eaa43ec082ad1ecbda3a77dc735010bf0295412b10527dec25c6d6753a9a5792f3abdd431bf77c77776cb9e8285b376c3019ea1faf2a63e55d65c79283fb32d5765aa7a884199f16edb065aec8ecff03ffc82b985f3020d272e7f08748e9eb13f527c9ac8dce45ecdaa89530fcf30634c0b44faffe105fd7f2e3aacbbac23731bc8d7df9369e77b4809a2ff31a111daf44de49ff882c5f7149d9acbac55cbfaddc97730054617ccbf50ea2339d6f188eaca9ab83db408e44f28eae6240a48268f7f15896c7cb0004fe3c2d060feb0acde4a6da36e2e309af49f34914032d92736f03efdd34727ad6982cf39a696968ffeb53ffff7edcfdbf6df6dfbf7b6fdf9abebc8dc0632f7874f600431f83d99c6c30a58ce2555ef3f737fb2371bceb1dbcebef270cee2c5b460ef79929140e0fea6c6117ac0b64ed951944ec865217910b95b73729ddd2d2b000539960669d767e4d14b5baa9d276ac21d0feec2a66d8139873b0d95e255d02d87fa0fd6681ddcd21075d5e4dd8e66b8bfd99a2972da67039d8fc4d3204e6cd2b98a038d5e78a393d905a35ce2650447eae910c51b58ca7c146e167df855fcf23cf767bf3ff65fa70b20f6466af8eb76a4a92b160c8e37c54b9a025b63a377b33c6ea572de628ec1dc426287fb9fbeef61ffe4f191e4ecaccd4c5ccc4cff8bf7f35839983b0bb0b1a9da6acb486859d8a9ab39d9caab4b2a72a9b89b7bba7aba3fb4b5b676f7e055739230b53772763697349170e47ef6cf9bff5d93206e3b366e4b829060f73033378770989ab1b31b73f29af071428c4d398d4cb83938b9b88db9cd20fc3cc6267c3c4646dcdca6c6fcbcc6fcc6fcbc3cdcc6a67cfcc6bca6a6dcc6e610ae3b3d3f82999bad0007fb9fa716dbe37f86589d9c43711e19341a143079f27c655b20e8f1280bb6fbe2b538bed144c9f61d737911c34c279cf12d8bfc00c9fd8446d50109c67a1efea88fc17191dd24b7aabe3f64cab34acdbb9c8e89c2e00409f461b710638f45ebfeaeb783a05e8a4afd8307b705e61c5335201879c13d987a04b10e53ea9fe022dda6286e399ee16e191ff19675860ae871d83a07348c84349331a22e3986f0a9fee53c4cc47f9970ff72b17ed5c5fad575646e03f9dda43a34b8747ffb965b3e21dbe0873fd3847b8ac8dde517331573733d2383b6dfaf21f945e573be28952671a3c8344ecd19a2d2031861788ba7586d1c0fad02fbdf7e94806fc6e9a77c4cfae8b852ee59cd90db3fcbc51aa147821665d17be426a67c153ba99b43295b92f3affdf9affdf9abfbf357d791b90de4dbefc9d07fd454beb9cdca83f01efc99fb135ffec48f7dcac2f4c3071a549772c4b209f21f816cc263cfe52349cdc9875fea522447dbc6694de3c83e892c8f65854fca7a4cdb5cc43ecee7e3ff9c75dcf09926c93f692211095831714668fbcdafac7872c9c33a528d63d6ffb53fffb53f7f757ffeea3a32b781ccffdee429ecfc110d38e2ffd88af48ee7ff33f7e7e915db19c1c0900d16d384d0936416c176fad39156f35264dab51cec72a9c0763846a3d9b31e02fad55a0a02a973f9959810bae73608f13b9ee87350bb1093f27fd6f9f984a447adaaafb797f41031e2fa44a837f5e466e85ffbf35f10e5af4294bfba8ecc6d207f9c1acc226bfd7b22949d6fd2621135f7feccfd29749349b7f27cd3e7a0f91ebc735bb9ace62bf4735e2b95d2483c7969e6675de7622f9c1ac35e200737ac2550bdac6180e32260fe5c0fc32f51a45da53ab6e546477a2788722c1c5d6e650dfe7c10ea2f49e621d8f37950012d306361f0367da6090e50bf493dffcb9fb1a568b6d35bfee6cf7cc28a04bd867ff367688ece770983ffe9cf672a01c1e4217ff977f7fc7ff86cfbca7fde90d03fdf1db52918f6d6b69cc9070493ade5ffed68fb9c4d754ff82cfa9d0eb4f4b8a2124453446a097f6f7244519a273dbceb68bf058231e5928319b4d1fe2420d892d6b409634a29201da191acf0d852c821b96e7a33dcee5401f1d41b09ad6ac5795ea76d7ad3a30ec7e6d96074de84f3af00c1d2c9f6fb4e6da793f66b9ece0e556fc5aabdd824ff0504ff8f06821174cc9cacecedfecbf7ffff40f0134d05090e1708970397bc393f9faa1697b3a284a2b8ad24971984c35a53c74ac2d35942c6dacdcec382d7f27f1510cc6706e13032353333e687f098b2f3191b9bf21a9b40ccf8f939cc79b8f939b8b88cf8f821bc3c1c9c269c9c46662626fcbce646a69cdc5ca6fc5c7c1c7cbcffc3816009d65e11f167ac0ad56afc58481a3f0eee35f23cc4ada1ac092ce40df240fa3ea3b8e3e9a7ad9ff571cb0cbdce29ddeee63e54e20cc95a4f94bf2b8eca1642fff35b81e0bf668f08e0fd09d9236bfb4db2ce227b3223a391793e2adc1da33631404de54dd834da3e7d7879e6d5d757d4cf2c08779b7c00439e71dcc32c6c7f7eff5b1d4f9afbac4397dba501905d31a91e5943e18735e935ad8f44d9e43dffbcb803c8ee784eae68ace0386a6529c1a791f58883fbac3406e822ec57f9be0ab728c5494e68b56fb57b5817169e1fe09931640d04bea314ce8fe9d2fa3a6416c86f17430d14066d77bab994179262b61891fa78f8485c73ebf53d20d67adca4dbec9b8726f6f8117429fcb1d5cae4ec3ec09e2eca8ba5798eeb2f023d69a9f380fa9030a9351728cca6698228fc886dd62c5efc2df61ab93cf2b86d0ed0a5f17948a6b014354eab0aaf95eec328afe89df92980fdadf7c26b9aee8a0f4d6bb3704f834ebf3ac68e07080c465e9d895ace7a2c1c6cd2e1940507f73ce0c001baa8e0755e7eb2747b27949698d9a8a10ba979a75806e4b42962349c4d10dcdc24eb1f984f4e976dd09e760207520395c87c913422af6b89f941d68367d65619f100bb72686ce34152b519e6cfd88165dc6002c50ff97240f6cf6732cdcb18280735fe0a898367515debe248ce40613b75d9c71a6d55ab09e9ca4d81586fa2e37c5bad01762aa12afabc4184f5d562759bbffe204ddca05f229033bd9ef9cebb834928b3fd98f85a0d935f977efd05b03e67ae0a36bb86047ee215be2ed999f36c8af48905ba5caeaf5636750e069a7f4839f775a79dad4b5310006a56fbd74be2c6a08867699ca3b7ddd0ba24956f0b005ddea4cd9e08684127374ab31253a1e937ad2b65f1a801e1891a734df8ec471336b07279bcdb1232eb2773c0f88821c76a789923efdbe561254c3249d85d49ca812eb216cf4aa366c78be9dec3a7489fa8aeb1d3745f00858bdb58c9188d3339ebcd1259686a6864cab745db80e7470f0b5dd46ff08eb5979685ceffc6afb3cda80178cafe698c0b593588d734cabeb47e9875b58e44be4076c140c8e27a1ad59ba43c891759419ff52a3ea4eb038e3816bbf87d5b591fbbdd551b846876a970fe19816ec0c2493df9b076bacf1459cf4558a2779450b57b093812eb7e706778c6b1638f1929728e7a6b9e25d7bf04d8ade62f389f17d5a946adf2bc3a9868eda83b88ba0f74996051357061703945813d7e28f37dbae545819232901d6021e8133d8aaed7c190bbb67c48f98cf63b6e16b0fe3a56b6a4bcf6b2a60b1b1f440ffae957d660c802d5cfb8d2f866b46d50f1e55f37d591a92c8505721502d9336ea1fa074363b31c0d1216cae8edec3a5897a1407da27c019823612cb133f887b939475331acf09f2580f1a55f6d7b6039d93512e4cc3d209f459f3863a004d8bfaf559d2213d98d91d26af10af31e508eb2324280c294c545cd9f0b1b39cdcd8e3f30f26cab956e2da901f5795e62113662f3b8d2c409b0983616754ac74b33bb03fbb340279e4bafc4bf97ee2b6b4623fc5b2acf10a0ad054eba7e221a7b2d3c344da2792fc721d1ab530980466bc3c9553771e5c48ed33336ca88da3d6926670764ffa83df2fb94c8eee071c204434018f821626c130b70f46a5167ec87b19fa4d292c9ecbf8e31e4b7897a03b0ef0942247946c5b80caca0f247cc14d694be7ff904c85f5410be3f2126a3ba2879b994925a42f379cb17282c2b251ff39e5755fda70cf997a22bd203d6a783ab40174f4ebf12b7c52436abc304c5c8e7ef76df7cd9b90714366e2b74f7e3c608bd5859dac15462688729a3b004b83623e93110f716b317d6bacc619f0e2f8c14fa5b81ec451fad0831cbeb33a699e0eeaaab78ae3794c95282bf954f99fb71bdc5d5e1a8b6115ef63117f94cf9b318e8c2cc5636d2449c58f86e23be8d416fa29c4503d20954f589ecf1328a57d1bedfe5a291dedc5aa8a321a407b0d3bf2f05aaa4b02b7e43e8e1b5b7efc2afcfb8cf0b144efd095d2f095dd8c81561f0de0ce27f3d67996e049cff9e6b19cf2d371f0ffbe3ae7f2d1a65e5dcc55e00da924c7a2ccd0abf92cf19ec6563ff2ad2b888e75b0c6407c01a136b20f0e87cecf53dc60dad38aacf4930d4fead5c7f31d41ca747f75ba5adf250c7d95bb9e52515a04be1d7b0b9d8d59f9eb621681c853132d35adc7ed240f623437fa87a46cc21f3f7c4b06b9f20e6fd98561da0264f04df60c39e7cacb5272d2583d9d18755be9e71a0f0bca326d6543f034a083496764057255181de7914f56fe51f24ca4df885da957de25faea921ef42e4717a81ec3291b228b661a4d4fba52742b527703602ab1831b6bf95abe6cc2e5be29d2d867d7c617b6eb35db6f60c01c8d997f7cbfa2839481da87889633e37a67bbdbf560f9c6fd72653ba06e7d08e08a422b9b9f7eb7a9dbbfc007ff0394bdf86ad82281259315db3de841a3ee71b4a80ebc51f4bffd2af3b269b417c7b22ae3a287c88d26ae6b7720ffaaf3eeda329c76a71349a29dcb6261f1d8e0700f90f0b6698969a6967e58509d764cd17f8e1b340f691517b2659e0f306f96f61509ab5cf3a90ace92e802ea1a61981b97c6e9966a754b01e62339e4acdbef6404ebad4b0a706fd202f7a6483ce2a357b7461900a29b03e082094c96d0829ba9d36f2c39e2c7a4ce5175ee2bf954f303e333377885230d6ef511dbcdc29312b0c0068e2467826aaed7988325cbcd07d055f14da0f87a980f58bd2afbf4b313a579d5fcdac7bc1be8ec938e2037c9f8437e86f8def23aea5544e90bf937beacd85a80bcc5f69fa312bc5b3b435e9ec0964fbeaedd4a9c0321540ced8c53b8c14aa2eb5c56884eb6a4659f47317f056a41abf9ca2717889b56ef2add6c76e669aef6f1aff563e36170dad918af67251b48781e4fdc210ea7c2d50789752713b75644ec23a10ba714f9979c33d5daa13e00899e34d95a4096bfd587a248d6ec78bb8d4f18c10a879b0effc9dbfbfba43afdf793b1bafe507d6debb15e07c77183d7d6fd6ecf2928e9ba2dd355154a5381a8ffeb7f2583c0be2ddfba2d1a6565215db6cd52664342740cd1579b523fdb67b9f84eec7dc7fd390715f957b55cd0ed41fd988cfadacc40406b9b0f3156daf1c63e785feca6be9500e8e3930fa5b6c2873daee70ea5e7763f8f3314de45779a6701c6b2b3f6fb95a60cee10fffcf9875f02b064f346e2b230860b2bf5a060fbabb3de08e6e31aebd83999db28999919d8c999994a79d91ad95c92f789d60a5c8dbe6ffab78f11f3d9f9d36bf740cfcb75be241b716910561b1c187cdbd581a75b3eae4e786fa71a8e86dcf0fef880a75c48b137302aee4bdf2e1e191775f66e13f330e6014f047238f04de2d360276e8baeb37b86d7e28f48f2f9bd2454eec09479d4256cf5ed5a924b2ff59dfd0e155137a80009f0ad42f6093bbeaf94d18047dddd8160ef9a4f22325d80f9b94ef18f7f9b3a1ad3ffbba35eeb35cee03f01262357e5a8b4adbbc9f503f2d778ae55077c94bc5b8cb3bf8cf6be057d7d03f90d6c5fa77d73de2e0df5feeb4ffc4b75bf5ef0cc950dd6072aea2bf2dea1ba5619e75422c5bb9a859151d45b7a1aae8783df247e361cba105b40c84c1505618cf06408590f59b4105f43e678ddf1e840a669cf30c8039873b0b4340a1212217830abe6f3ef66fffeae09664d87326159dd1871195af89bdab04b4a31c7f16ffc4f721881edac651a9ac6e884acf21bedb59017e62e650aa9aee1e84e4643873258fb8145162d3a4983fbc5de723e496f16f4bcdc4b476b57530736235f330b134b2b330fbd52fc5c8eec1fe0f5e7fbe1eb8ad2ce92f952dfd3bd72fd912ffbe9c6eb97edcedf961e811b7a981a4ea1a81521b1e5ede71db7dc1887aa9075fe785b1807e1a3b7fe7fa9567bc6dfc5f8a50fe23ef48e723ef1d8f92dbce8aeedff97b3114942f1414b198f8df4b5f1d5c442fbf2500a72311bc63d2ba1cc029ee1c2857a1f0c2741aef82420c3ffbca7618f2f786fcbbcb9362bca9d9e3ef7d945f58cc8c494378bfaba0ced4c260e07fd6222289fe35a26ba7217adba6007022af3e652f9db68067656d5e9b2b894ed64cc3fd5ae7d06aa13a87585e6250ff36661db494860a43e11d8351b759e5d0e2928fe594950cd435545414b40d94c41fcb694a1b3c56969756fabf6271009af6360a3fd1d0e7108b07780ba72f6a74f8178657ad0794fec062fdebab84398787fc4798fc2f65fced47ebe069fe0961f29cdbc2e455434d6d0a99485b84535f63b5e8656074df245d52b42514785e434ed1a4ecaa7f254cfe480cda416b17a19641c16a61e85d57bf0a74f9e6bfc2e4ffa3c3e4488a662e468a46cecfee1c26d7d0b455e7703795e77868ffcc560ba2a3c465efcaa5e5e26065e9ce612d2963afc3e32ac5ff585bed993cafdbffaa30b9b939afb1093f3b8f392704c2cbc7cbc363cecb6e64c6c565c26e6a6e6aca6fc26fccc76fc269c2c50ee1e433e6e6e0e6e03782f0189bf171f1f27340203c1cffad6172e0fdf556a26264f5c8ea32122f99e31c8c9a2fcaa2dfe2c7a02ddd1666270c68f3f75967b52f908c3a142a32b015416c121de1203a3125cc366bdde29b17fb562cfb0075efd22de17530464646368be8226efabd25d9908db6ec15e3ca9edbf3ad7ec330fa0fd5f97f8761447bfa5c641bfdc5bb8a2106528ffc03a545e2c4203d82b27d5e0303668c8a79a95f519d02a958513af3b8dd03f6124211953e0af816ef3dfea53aff1f519d4f8c3d6d5d8d55c5dd9fd8a98a736a4aba99dbbb3b73f2f23b79886b703aaad96b38e888abd83aa93de49772f95fa53a4db9b978b820dce67cec5cfc9c3c665c9c5c9c2626bc4666a6fcdc7cbca65ca69ca6105e230e764e1333763308bf89b10984dd989f17c2c9c1cdc56f626c6efedfaa3a6fbbc4c5f01b3cf95f5e0b3b530fd1580ed77358b24d28caabf75b1e5b20733ac7271ef29a7dff62a7ac47e43c6b125b51cee2283fe0ec08f7c58a0b792754183f8e2de91fe827a51880188c81635b2bb5b59b007d4758ebb2a53f943726041b27e0af20ca99424008ea1541e6df42349871ddc773bd95b30e947091cbd00911f723d9ff581b3c5d9a849908451658bd789b16029dcbd512025dfee24908b4eaf2594b5f28cce30fdcc893c14c18c50183a106fe90caf8bfb816217f71c3fea65d5f77d67f43bbbe235359189bca1e4cdefdd2424c5bb9a1bed3a6bff17c7fb15d9ff21d7d22f1eba0bcd7c268ba233791541b33a9136b1341236a23e3d0158e974c5732e5506ddacb1b78bbde19a3ae8d0be1f85570e9190958f803dddf0412a8bf381b139d3cfe7fbc5d9ff2dd8e4f280a754a5ba337e230d3cab0eafe383a428e9c7697b5859431b63fd413d97c32a812ee885cfc77b7ebbb6d7d8affdca4c94e24797b9faa4f146696e46b7c28d68b2fe5c5c4ef55cbbf6e3f86ce27857e216226e5883258a73834a78b8071554c73d258584b222bdcd3b890e4def0e68ed935e2e381b3a882b41dd54b8d0d4191d211231b618a02f9d8dd978427c75376d6b06a0fc318e97a42821b725f623d85ffbce267dff749eda3fea0c445da0fbbd9ee96905bd5eb5f099c5c987f0281f38df176763b1507e15b86cc831ac6864f9a1a5f01b4f9e762245be716be3ce92e51ccc9077d5413197b8020d9ad6d89ca99ea541ac748ce437e46b3dd301805143d370c718879ff68946c2c7166a4f33a61ebbcd90a684aa1a65e24e8e19713c561be9d8ce6186383a4b509dc9f4277cda0961af32cd36b38f51e8cb28cf5f547603ff44c2a96b585bc96f45eac5392aab8d7f0e107684e7238ba2548618e30c670e7f276f67e1631155c02b28d8e513c7c5ee01e9306cc8651956033b44e6a42039d5b556f1ed1c716f39e1b1b98bc7b0057819f8be60964dac0249912d47bddbc6af98e9caeff52a18161e61e50fe75c06d245455ea30d284f6301825eb3b8b5eb608d0548419cb758ce2d15624f6ee9cd1779da78787751500462364e974e1d393f75d5052d563d050eab37c4b1c407038e8dac89d0f9cafec368cccf77e3c756daac0061088ae84cd261b779eea196d942304a0b51f58f18d01e59515d142fb24ded442d3c5ee5ee677aaccb307e003fa027e922abc00ae72d83d8beb2217ad005f991e1e20a80667ed1812bed6142af1afec634259975e0965039a4690ecf9c4413c73e12e09ec0899b842d307d50480f368e6d933cfa245cc33e1d58876398e47159d571b005a4d7d2fbd38ff8849900dbd7c4184e9ade8072d16a0a9915decc5c2440857f3c70878bf52a3f6bd6c030580c0c3ff7e837822d6f5b5ba78c7099df9bc5bc8040f60b996af86777eaac011f4247f0fe11e3b5d929bc4068a2d0da8b5d4fdec3754603b140b4071b262dc8b1e0398016e629bfe546dfc5f1a86b1e144ef09b9409a36008226bf244a8b27e1a754d355aa3a93b38924c72334607f9e61ba1da4d29c44882f91b86a29b94fa1049601049f000faad1979a2d48dc41d9fd41db2d3d3e2f6181a643643e8ae4ed76d1b357b0a439dc3f0e9f1e0f8f01e5ed735f3f08c0a85ae8fb798e302fe133a3e9a77f0510802ba37408d53bb2a78c21e7872daa1034763b12a0a90b7f2aec3a2103852795e4fab5017a0ed296bf1048900c8ec21ef8d8d98263ab984f2ed4b91fae130534fdf93037a01330a38745889fbf6e2137bfd5d55d0720f96916974479d0e759ef4f65b53366cbe01d84ed01fd90a839f036624eecc90473053ab7ed836e833063405d96493a3df99261d54e88facc745c835c97b65c122856f7912906a53a9836c8b9d290b99fe2a209030901284f8a9f9196883bfde1fc0761b6fc1cb534bbaf8902b07f5a2c2eee496b3c6f4c7e594c1c1eff963b54a400688ae1972262e6b20ee9eb8c484ebf5a4f4da3083c03f44b87ed3ba61d0f2692980d973294f705cc0612ef80a65eeefb9a188732df3293df9ba6609bab65bdcf878dfaaddcb949f284f0404234e2c8361a8f4f99456ac52ff4b7728cf3f18fe504589bdc99314b529f3327cfbee001041d15e69fad35029a59b855a839d05dad1b36e55280fe5a7f069f5db4ed5a1cee8498fa6ee919b7ddf516d0148c23a9ae0133365f9d79babcbafe19665931ba1d50deded1a21c53697a847dc602ad14f5791e5afd67328060d8955fa3443a355284b9423df6ae94cf6738ad13c0a87d8798eb74a8d032ccce4cb5a3a6b9d570e1c580f2be047b123dedb1a86ecd02f58ead71b834b26f0e4f7f2b97d353287c6b9cb7e8947093412dd93af5e02800802be2f7f4386af1481b4b25719fa38f29c8d70d1e0204618b088ce59876ea0dcaacf1719ff702d551d5ca006be1495dacd213f14dace0e09874a49a6e3507faaf40d33e7f4ad740e1b3a80bf313bdae808e7c6cc7ec67c0fba96023e0e6cfb27cb4bef8424492214b576dca0f282f3f901b4c0c45c0c1c2ac4ff9c5604dafb7bc9613f070f65dfa764f3f555d1ccd44de93542ebf4c86b4029138ecc7ca7ac38e704a32d74efd022129f9fc22171580bc5f288267b141b1aebe160517078bbbcc500820589bc5b893fb3b9e3888df87b06836fb1d79212901fbf3d367d688b4f99e32c1a9f24d767b63a752c543407f344970ebb369b1d4f2b5260b1dec375c2be2ba0309166f8a3e7d4e871be8f7fef0d8bc2299f7d31e7311505e399a1e77f49b6afa42947b43ae64f431bbb5cf4fc0fe78445dfd6005e67573bec4b06d0f14ed9c139f1240908b5c2b4528fc5605fda940ecda32d73dba849e1cb037bb78ef9d2a8dcad394fbad2c98867d9e246a1801ce2f64dcc28d5ac4d43eeb920f1d196e6e0ad6bc69008171e78976ab4ae895e5e6def59e8f41094e72cf7d803a05a3247c148ba9be4ec39899db9f550041601c00f0b451e9466b7d533c1eb73d6f38ab88a4deae685e80809c6dbca4a5517fffe5bca2889af5fb951c44cc368000fd3a6ece5e3b80d13ce085ee7562f902918a781c600f3f318a76932690a1d9474f9c353ea9595d3e5e05ec8b59d769ff17adaa23ebae128f56e6e29c3c089a80f2e2568ca173c9bced4dd29f56e1e9f452d7a2f69280f38750abb6dc4285557be2a4d2d3efcb821c010604185fc8aa34814a4e1bad5aac5cc632ddff2d745e0cd814119f9e9f5af1c3a864f9ab12078e7e417f6142a0291b9f8749b10622d6677fdba230f607a9c65897ad40fb93525dcbbd5c912fc42470f473f91c58c7a6ed0ec0fcb99e3ee1b4155cff61ae4d2bd8ea39af114dc10b346d445ce7909951cc35ad45f0fc4098d1879753d60d34d57284bbcfa1e12c7ebdd0cd4b3cc5490b993a3704ced7024746872b9e19e7abcf9ef5ca7b981a6c2aed00015e02df80d8d4121ba980b3978ad59f9524e928a307b09fbf96aedc848de65fd1178e1395c97cee9850039a8aad9219b7497c969fe02978fb7c8afed4b5332b11682f7260c3307526875f5caa2d1beb18ec09418d4305f6cf31beff4124c40c69edfe316d7b40f327b32f0636bf958f47c6e4776b3b4eef8d1d7cd49d10ebea74d803de6fe0698106efb9b0108fe00e4ed12af5c31a8d4b80a0cc4f86f456f80ddad60b68b3d8c575f6c19fec6e807d1c9b1dc13124f5dea91b9a48e52bbc1ef17b6b0a20c129681a8678124bd991316d16e55ef3186d14c1ab8e3fd19dc1909e1ce936136db5860da6f160db24592fcb02cc191942de8dadc006223ec44fbb84f8851a5bbd94c071136157b80ba5b563942d25f3d972d09c2723170630e72654e3eb5ca065d0f4f64b48e1b5238eaa39b4806e3c5fc73ede78f1e83062ec7ca13b32dbe8eaab4106fa2d772453e361566439910de4ed23b5ae1508784c00ee0edcb229cde1444fd70a14dcebb360fbf2834026e0f9c3ce9478a21d8b6416517788f68d7d3011266780c232546517c468509ad314f0ec28afa8e562578672811e9a7cfc2c07032a9def76d5767cbdc3a838683752806e2ad8abde92698f0503866929f7e7aae4a6f7ba2881edd69cf9c3fc541169e31a064def92398351a13316c02d498b42722d8d92bdb3d09f7f2f33fb2a0b9ded01f0b5d3d104e16934b4f3f7c37c96eb33b57a2620bd0089d05b98e635e15047bcf2789e1c49307aee0ca77cc16fe522d0f23a44aaca70afea36125418691f7ac607007ce594c703aa2936b2a1ac1f8eba4325d1717e18cf01a8788f76a0ff73c49a26a82c21524914e8d00d982f83bf95d70d07c508d189178b37b0045ac268085c1f220180e25eac7092b995a4087c3a341fd76cc361eb3d19c0dd646c5df68e2e23cf9ab57da0022931db0a6df502882fdfeb5e8457356064364c6f147f8f76e0737d1d031cc712297e356cde4a9c3d87eb3c84dd8859ebd22f80e3500af553430c65a3324a67a5d9c5594a64940929a02e6ef2d12ee44ecc1f4c7056b63c6ddf8f4f9f1e007a2ca276f2dff011419f66c1f2e53186c2448affac07ccd14d5d93e7210f6ab713a5ab284d0ee3213933e98039f382218b6ede0029d953dedf8e235d929501b610c8f7391db53b526156cc58f6ad961cee814ac0f05005be5facfab2381fe71389401ff8524adf05ea97cb1e40e107c4d6f1f2a026fec7b51679db63fd78f0ea71a848bf9517b9b28c1e92dea4f716e90764d66230470d9203e4358461cf2b86e66d7157df6993aed074d2e4124da0dbd4b40d191fcb08baa1d0de8bc430430938a89b696c50db30b596c4f3dfd43096f261cd845405ba5702f92672b6339cd24c8931cb97e85f73b3d96d9ff4db03dd3644b5e10c194bef993e44ed819951c8404c750e02d6076b9892c903ba9863b8d9539b2986f394e96431a0367674b65d5b0cdb8b31e39a09f101b82c62c7a20719001cb1279c9f3e2dd1f03ae9d5792cbe12d7165e2ed06394089d094a6da9e168ad643f8e8c85d5b39773a618d83f7d89c6645589a47d09c53bd6217aee15bb84e8bf95670d90db38b8de571348c70bc4fbeeaf12b5dd00c00d6f7e2a577ffcb6c734420efdc5c4d5d46066de18304737095a874353ddf0c3b1ccbe359db7bdf934a105e8af6b679a08353b629d831cc84828d5c50f84efe4403ec32b35f175e6b20b91ad16317349a3872763137e803931e131e5bbad39f51c75e7b3c1b860dc0fc90d59408e3ee0754a63d98a98eb6d60aaf0cdc28efee01e107d95747216b774b3fb713e7744d2bfb69385825e0ce463d94d29e8438bf2d69a665e13f30f3ae8a23b2703fa7d64b4ef5e0b06a19e6f4f3264438d8be7ac9210e82676e6bf9e572b73d4451d8afb688f2e0e39dda500e8b13ad960d755fee96bbcd8ab95f9d224d22712dd18807efee8b640eb3ac5a4ed829d40c2c3b1129793900df0a0cc3c67f0b10b91a10cb199bbc5e9bedc906a6501fa29a039c717ee29d6c8d0ca565e3acc162f81f022dd6fe50a5167181a979ee93f8adf9b3e56b8f726c5e9334094b79d57cd8bf8a95da8f871c22d8b935302ee1b0b90af1b2f50843630aa15d6f103b95e330ff9a095ce0470872ed02fbe4f24a5aa4af1e407a4706a4f23f7968d03fbbb01233da1d8ea99a83cd721a9e83825bdc50e900f8bb741193010802d81a4492fa6688b571d8ad4400098ebb67882ab6f614e584709d431081d1bbcbc3e01ee6e7ec41bd7b4b4b546dd17f328c6d89c2f8a1cd701fd88b25394bf57ccc73f4cfec4bf21803968e4a90de08eb651e1ab0ee3c036a31195708456982f31cd5300e64e529f8a6950626d4123aac83aa988f8f9e72579a047729818c972c5b87868fa7398455d3cc7599be35d403f3d4586cbf0a13cb63eb0e4b316917de268b85401e85f9b4943c6bad288a10015af9c356a789c07304e803b8e8a4754a1841be437ce556f12f905f188fe412f90cf2b900d0f739fbc51f2c8bd8faee141e9411e9e3c607f883f3687775f78bcc37bf3b89a516c1b6e2a3101c827871542b113fd5a18fe54571deebbc5d8fa34ae10d06353868ab9e41b931b1facef557e16fb99ba50da14e0ae3c2be60b84a25d8c33ea461f27c1147516778407e04c862e0c83e8b9fa7b84ed8b2f339f363e958aa704f4532f33c1a3802b470c61cde8b7a3ac3d3951fb0b009c12d31e86e06265dee09b8f1fcb69d3f05585fe35c0c94eafc55a371bcacb18ce7d6bb2b9d162734d2f08e86f9e77e36531b3b4aa711bd9495dd298efbeb32203ee54d9b7ff8fbde7006b2a5b3a84de8b34692202d2252181a0d27b07910e4a20a1f7264524127a11900e52549a62a5481114010129824813a5a8205d440569c2ffedee7bbb645dc8b2c1ffeddbe7f9d6f58b73cfb9f79e3b3367fa8c9e956a242b3b0eccd7a6939aac770a61c730c73c0c196cf840167a04efab82b2cde0a70f538c361881a320c64382f463b6af51a26babbc25c6d63a469318e6463d87039c37e7fb7a29cfc7379faaf66d3f1ed48261ceb290884e7dc2e6ae609c007997e4f564b0d4f302c6f3c5b1d746313ea1cb6531f0117beb9238a82c4f84c1ff5d06bfaede0830de7870e3e37ecdc5c7cbf3637730f85f3f9909db6cc879a1cce375e3eb1502f0c7e7aa30e44fe168e9bcb5509f4cbc5b026c9a53d70e4f93716374c36ab579a117136b76ce21f9eafeafc2ae3441ac6f30e8b78bf605e77ea5020130b0d17feaa6e58b53e6ce18e666f6068e7911125d68faf0a9a9f637e5ee945cd618f994f736a3517ce4308f2022b13a73d9b54364373330e2445b9146eb0414ac1bf7bccc852f35461e70f0afc3e00ff5fb4c086d965925e5786fcc919e17cc1eafa5b0c550e797ae91f76d2450f21b406a5fdca65bd3b8bc89a11f5c18a0b1affccca9f9f0daeb55edf3836373880318f99c0fea1cab46898db240ae808553453debe00f1715fe73713fbe514f0c0b923ff80b1516b1bd657ed67a950e9e1c014915f56beb904b9bdaa966d3b7713f84a9bace4c8f8042731946b0fe4addd722e9dd177fc4fdfc8fc4fde8c355c012b68636270c4d8cec3ded208a1eba8e9a67dcf41c8c35348d0c35dcb424dc14957c54bc14157d34fe51713f300988040826098742c56c10e2109095350c690db311878821a1564804481222662d0a03db4045456110099804142e29262e66230e8640ac454170d1bf77dccf550a8615c1fbb2f7e9cd028fcee5bfaf7a47da9aedd351c73c64232fdf4221cfe2e6b87432479eaba4e2e0f5aca463a39feb65e44e28674c313ad8522ea0c42b02ff5c89f97ff13cdceac8097c12913f9e0dadac03ae50e8a1d0945562e8c3e19255147f8312f3f858027488b0e02129c6f3f58fc9f208b6119e6bb3b81a0f86dda2b6ac64b1c1914eb03c1f50f1f72538775ffe112b29cb02fed1030f1b2ff92634adf57eb596bd2b79a40739b3a4c4014bba1105fb6dd3f8f8af70b32c09e146eb705d59d290b39af3bef4b057731dcee57c9eefcc6fb1f602d70e11bbc665e48e1d970de23fee1fbb69175cc5fc3094d22df19c2268605f8a63bc54c227afcec8d8bd2a315bb289ff50ae0c3ec0d5da2481c83673bd2ef350ec077efff3f01beffd354ff1d097f87c41c767579e3757b95f771adf6ef15c037bcaac4adcf0bb14465ec7aaf82097bd2ffed47c6b0374913de6e5f8e34b51e39d4ba492de71ea073b1e9991a4fb1c4c61021b3a29e6d03d4e62356f7a102270c5b4378235dba66daf4a9c9f2964191ebfb0ea5dcfa51409523a22222f43b8f203bfff79f82dea7c6bd4ac2a7d2364e9a313255233c0e3823dc57693797c5f422fe7eae284df45cc7eecb1c7d495e8ac2f2b7e8eedc0bbcdfeba32c19341ac2c91b651c618ff39494e8c5319aa972b9893686c1db5da37d555c0ac9f4731dcc046057270aab2da2bfe3d7c8737cdf989b421aa5fd9f53d397d388f5a94dc0ffcfee7e137557fd49b5c14a57572673688d1047cec63b090e1f7944ff24406e378d33f6a9a843ea565e47a0c9a281ca860ad3951ca5ee3d32bc658d77f56eac0a30c637b56f865d3fbdde3f932163390ce5b0f5c1e340d673b01eea1cee1d60570f90e0a8da7b186d6d490adcc732e667fbc1b949dbfd6ead8a6eb8997b262d1dd5dc161a26f666682657e0b6e2423ad5ab75c87425341ef57561d42c967cd8caecf47115c0d8ebc10756443f3b1abbdab8ea50af4512748f2928b3bbb895549e92897e9f33f9d032e9bd68088e20c1f11601330b3b798227998bcaa23f947e9df94c7a4c343348f71253bee2729d4a6921b55b367343b6b012a9fe4e07d404d2dc7398e60718a475e689d587ac84b8c1f8b5aee7e5c488c1f3bb4522c330dec0debcb2311a803a0f999d27ffa1382a737c150075c214f40a1e9cabea285d0f855e4211dcf4a2c7053d0b045b06353d048b0d547f022a3ff235d968efe1c6fd6e447e8464b3e632c29b33edfa7cc61332116fa3fda722e1282aefcd3c79b3d82f9ba9e728a921de53cfd7580f2c0926acb72199e095ef7a96cf97c3932d0c58d39d5de363211558afedd188270e3cf78281c0d7d28dcec60f828dcf47702ac192ad76a35446e1bcfe6d2a47f71b899afe59dcae39217153fe2a2701c18f2362395564554d27ca972c98269c9c72be2cdeb4c429ddb322f5547cf9403cd90e15281f2b86608a8b62edfd3b7aa2a83092a8f7d8a5582ccc840f34ebdbe27385f8c0c887eaf922077bf434a7fc69fb332d423210229bc7ec37df968bc48dee6053f3ccb52f6c0051c8557b979c8419b3a5f51984bfdcc47640deae3b1c1fd4edc8195ca32ba699ac4da63a301a3c976babc7403afa4c947733e36e4e3391511b592668fb97f6d9e7ffd61ca1ad7f727f143c49acedcc09be22b8b149c6b7d71cc07fe0a8a0a0bec1f97a6513e513d77c2d3dc468fc8d6e94c9c62f2d2a57455afa1f26c8ee3a5e6abf26278e100e7295cef8f4ef14f8800127732c95e46f397efff605a4ba773ead69a230d15a3e0fb8cecfb4c4f1712babf8e4988451f177c37fb80fd81f2cbd75527133a9aa564a88d6f3008e27aff443102e60d18cdf009c93cd54f1afb535afbf893b86b8bd9f73bd6e69f7b1aa47f2cfe9e7e6c9aab23cfc8fcfbe0f834c10c81543d77d8a95259e7b3707a233e015cbfff04cc4265f1eb7000914ef89b02e8606eaa4254378ddc73aab30956a2a1d611af44c94a483e78e838527774087fbabeaa924b438936b91e50c2f2b2cd449993df00d7fbcbf3df60d44a3913f4c542e8ac463685f38116ff7899f534d60de99cfe03f8a73c447507120da29ef6dc233ea6925b207f9cf902c408dffc32ac65488aed44b1b00b8efb8f5197849b85065fecfdaac83a2cbb45bc60139ef3a8092341fab028f435350d227dfec6dbd097b9eed089f60b1825750d3e5f7ed864d06930073e7d0876797852c75b0b2304cbe8890360653380a7cd7c05b17039e141e2c14a0c69209c6279c8e2d285bbb2525f5d1c54f43ff731e563441c26e3b988df2c1b6560823e7d9e2e1094afcd588bc1df93d55f79a93242caba7d1b4d428f693b3df29fc470d1a54d754778c124a71c508405e56c67c81f9e89c0100e336c10b2213451acf5472f75e8f6f549eb93c6dec380c7ac76eac994f6becd3139d1627921cd4caa691e03fef870fcbeb98d664d737e9f75344bd7092f20c6f979a917e0c2bae96b71d3d87e7e804548fea4791646c462e131b14b32b70e3f18395b219c052027598f4cc108317a7865297ae5a6e5abe36b6cf3049e5415ab3cf4dc5be1cdeaad957557651743bb48da4eb023ba0de4dc3122ba3be98620fd2681449b69cb168249e817bd5f5e60b8289f51a06514085f131e45d478c618e09fff7c9e002362f33303c7cc2aeba5f0a18b2ddc4799ef56cfc730efdb0a5fbc1924113c5910b07925d54efb70674ebb2f2b4608c2d2947449043276fccd54f04a96a04434577216c36ef0f3d77144f15c2647d4aba2916d6619b0da9ee5ddf7f898df36b2fff148253998cb1383c46de6eb5356b02c0f926fd46e03d77b273238489ab3b89df4b43ffdbe860e448072bbf942241f9a2dd9df486fd7d967182de50fd263f4a9df462a017a8d8a40dbe7ada99bfe585ad7d67713fbbcf420b87d9bf797d4707dae57231adfbdcdfaf8b9a274fc3335e5db3560319171a666f196889bdea677d8637e4a4b3fbc4741f9c57f0c4f306e15b94117685bbdcdfac7226f5458979e992ed9067e568d137dab9a37f4da3670cd57f7ca45e61539b6732081ea4edf12ffa0ac57b60d9c52fef1dcdaf4054ba19f6b5710d2a27e4d6b2624f039151c84056931d04241021009a47dd4ecd390d043dff915363fc44c8ba3d50043a87710b829c2396f555eeea22d780a22a2ff3e285707006072bff40de964ccfd39a9119bd2f03b8b0190d71413dcb3b3c60d94c56cd04ed382a9641c1c6ac6221473e2261463b518e44ddd32d7c7b1339a2c8e5617cebdc4111bbb1a97694f590dadfc33e667746eb730dc73c4b1a81b36cbd5b75615041d58092d870726cee69cff784fd9f92b23e34ead41f10408bf5b5fd0f22cc0c122fb85b9a33607366232e566ad1479d97f176baf1db59bfdb67112d2c78a55b61ac13282d71ac5fc7b6eec3fa102ec6056862d3e4c1f5c3c9590d14df0f4b55bf123aaf1fe75e2026dd2b74a31595fceba68dce293528b2d1db18ea63a3107da85a029bf29364a78ab2b2d80e9dd63caa9cdcfb6c6139ac0637d96b3958169a9644cb9b30f0f4c1c647cc8eed39520cd3533917d7bbf8e1dc7c3b7356678024fcc33e170bf14366c56a3153cdf703ceb6a97b065d15f237bf010e1ad894fb1e165d65bab17c2d7be4a8a7129af01a521b7a39b51135de8c85ea99115dea77ae318292011c6ba514ed3ceaf840ac3626f98a4f44a25bfc3d1208235f281d814a4efa02f01fbcb027d80de2633a1cd97dc05023536ea4861ceabeb4c5c97eff64f8add0e422cb08c9d35d3a57b53368a82c9717e563b02897533f43dd8edcdd052ea1359fb24369e431c7b4591e711f81dee31e6de805f83aa5075c015fcfedf475ae137e21e6985ff0ddc3ace5ae629932a7b75c086de34d14130fea3d25ef1ba63ac5147faa33e150a37bc3c2231f5b68a119a1a1cd67c8e9773073ef66da455faf8a61e6f5370124decf2ca91c483b9fd0559da381a60be73a41570f46f6efefeef89b432f581b8ea59a96a9d7056f28219aaaa6ba91b185bbb9d70f7f3345136f1f6f143a8fb231cc4f4e48dce48b8ff7df6578ece3eb37aea7910ef7ac4d5eb13f27475c64c0ccfc3e591336dc4a21d6807032382b25a3e37537f5b048c2c2cd1ded9a58584ad256bdaa82eaa54fb4ec5a2eea13f5141e8df914499ddb8357cfcd7170baa03ae506f8924a2de8b4822422cf05cdc0c9d4458e40d522c748e595ac05cf12a1432305134d277a1baeaed5c469eb3d8776e96ffc39387fbd8bd274ff8a2f79d2cb6f7b68744bf0851bf1f276352db2fbfdde49385b1b7d4dee0c60b646c9cd17749139d32cba88c135c10c4b24c771283f91894fd7cdb0d94af7304c25ae603d68da634541be75a92175ab99e7cd6fffcfc6e90bf87db92f9a11ab7aebd8ac428edbcecd2fc6ebee3ea9533c3dd01f9e78b5e644efdc0ef7f1e7e0bc49d4bd792f561c9aeca89ea7e681c377058ad757bb5f8c63867184e5b283727d9c1e3115f177de8d4fbcd6248e7cd7d2607735e9bca529656913e21d2945e669222ba94d9f95efbfcb9ac8ee50f819b26ca2b80683f958656ead5cd67035ad57b85df2f14e52a5e9f23208d083dda6c663ad75afce1f9c20ffcfee7e1775cf18c78f2eb2eaefc3c932bde15ef593a671473b69bbc1791181a3e5d7e26323c32ba0664b91feb085f0a6b48c3220bfa32475634e3594ca23215e3dee7b5b6463a73ca293f7fc53e60764436dbecea48a5f686649b2411e10b8abd8a343a9f13afdba157a86d3a5eb5626d6ab23296c399f603bfff79f8ad1cb9284325cc24e35d53654c7d28d43ff7fcf4b675b73b78d33e0d9ef980137ebb2132921b7c5d7b845822a89ef7f348d8aa33ddbb3ddf5cf546582c71c5a1e099153eb19ffcddb09737c0973e65342fd2aaf317bb5dccd0bcf79c488e5c69d6a76caff0bbab593f512f3f629513117d0ead7b128da06dfcf203bfff79f87d30a267e8ae5c6e6c4ebd4fd9072b2149a9638e74db4faff65015eec209bf439c04fc1ec8f8149cc87ffa12e6684bf0fa167188fc50932e5f6c583114af6204e434550a60bdacf4a6ca50695fe386edd869fbab45eac1ba3d8a80ae9895f66adc22e95688f450680a70129a171e5245b4174a351047e31dc6fa99ed4b6489fd2caf8e3305bf3a5c071a708cdc4cfaff26ba20731fcd1afa159bcc08f4d9cb6a14ad4c0d8cc43f886e6f892ec7eb457f9a2b1361fe39d16261f0d53c9dd07d58bcecc4b8f07539abe197d3c99ad387f3f7bd4913be6c37171360afcf949c40f2f46551bd49c62541f176f974ef894bb2a985b3fd9c3c4332f2f5941fdf6538749a9396a38b94d347f6ea5039109b5f5cf2ac31ed7209c1fe5eb9ae6141b1fc941ff8fdcfc3ef186a7c16aedc93c3e6a1f7d7fca8ef2b7d82d329edb8c20d0b015cf0bb415e7e286c9d8a5b23c27145868b5feebea28adaac5a9667ae827a519c48a0cfe1ebcf06824079fc41b5ba4f5a3a6011ade40475e494e51d2b5a019f8a1d792fffa30f958ceb4ff9c7a65a7bfa1d280ba5b5e72575ba8a9ffc90e4fe7944f7b257aeb28d6c115c709d40d1a2ecb5adef99e06dc32cf8f54493012dceb8b408957bab674535ec3ca1dee19b7a763237eaa2f48980690aa716ad28425237492462cd089d6a584555ce973b77a17a72bf6a1ca5b53c30204ad3960729e8b60237b9b257874aabefeae520cf4eb80425286703f9dacc36af5ff6077efff3f03b6a455b6eb46b5aa62f09a01673b04b393945775bfcd5575485ce1ae36669123be750f144ee2bf903235af38bc059d33a1932e527a387d88b499bdad7a9c7990f8260a4e447bc9ff53694ca549f0365b3851b875e79ab4553bcd1344d32474cc986dba142bae55021dd8b43051f37a4c7c782904418f75f2c37790c27f76450b621bc6010d697dd5bbe34fa8328ff7944392886d48a264ae332c99b9c2e64e40c1db2ecd92e2214403a3958727c1137f3810832e06476b42cdecbf1db5fa801774a5e462f7a68af89049b0a586a397604fa065d38d75807a33cf40ea19d1f6ccaa07d9b202cd14095fe8270f5dd88333d3ed47be5de703d7bd48c67bf1ce082549b502f5ca6cdea6d4eec0f4de69f87df6c32067ab1de9a7a792f1b7d732b46dfd0ca1048eeb842a1ce3c2ef88d5f4859320931b276ffd0cfffc56a042f2661c95a2bd1bd28c6042902e5e730778b31304bcade9c8c0b8bd02910a3fe7ac8bcb47a1fea3d19cfbb4d7761176e93bd12aa06ebc70114d7e265c63694d3da64393401ce7cb77ff0ef7f1e7e031f12dd6a1a89984ed25265cb73e7b7bb3a93d2fe3d85aab5972a93d27d6594a2bc1089a1993b53450f867ab4e88ad5f9f18ba744bb38a1003e6b37afc5c6a7aa317807ce290746cdbccc9ef0f289c4bf1250ad806a8fbf8193a6de8a42d3509f0af95a9e2b0d5cd6478590cfd40342c947faa401df3667e38aca37b7477e48af0ab33ff548499af336ffc71d099f1ae578b50eb842c012ce60d55536e43f164274e10ceae7ff55111054894ae0d0a80b2f9e11dd6d8f9b78866d60ab13852dfa6b8741db7b828671154b8bd31d1ac9d15a1d2b07a663b9c9412cd16d7818d163d99e5904e5acb50e8c9d7ea4e4e70e68546ba47763998f457c056211bfb146b9628bd6a7c79137d0effe9df774fedf7dc8e1e1e10100a7486ec4bbbafe0e940e2c3f66458beb0dea568073a81022eadb4455c0b1b392cf447133f2614338bc6b000080e697bffffde7d7f1d3bf6350147d56c5edbe248fc32af816a6b36ec4edf79617e8a612f0b502a57bfce90c2948a5de868dde4ad3914bf748ed31163c81e46410fd00c09b79f208ddb3da38ae1b5b58b2e12442e0453df1d038f21518c096edd5246fa949ed73043cc55148c17020d078bfd9d4a0ddd1c29c2c52184fb26aa88b4bc8d17721b7591f24a72489d7d1e8f768b3551f7cb028372ab184e41ceb5768bec6e445f2e9a1b5a75cdcb849568214de83e947de67a98ce498266f397431b8a3094e02725f85f7b355f7b17df95c52707d53e7c9a6dde9d346ba73179887724812590181c6f584fe0794330d93132ae255ec2bf958a95f7d9563841ce12aed486c565b141aff105a86669b75a60cb084f53b86123c9fd01acb5220059c6fa18f62385afbecfe315e325db5230d712c1789bad88e13c1245dbc3933ba56098ded8ef1391ca5ab859a310e79b25df21ebf942ff7e26094da809d46b0a496d0673ddb061bc8b5c70e395ab4b915595a3d35ac24dd24bac40edde5ba03a7c24263cf25b59aacd10a99c9ab1dc45fdefc8b675a2cfe4f1f00000060a4d8790f16cc48a8361c3b5dea8e400ebe40468f09fca498906dbde611cdc4ea35435473ba44fb8dabcf5a2be205df9cda0a9f2944bb4c29bc88d5e9376f6a1f6c0a10ae5c53d8f6316a2acd5affb8c7f2721b0acd45eb5bd9489234874d7093adcee6cba15c56f19733250aaa1b6cea947d0f7d2aa7b71660e0f9728acbfe910196f97aff3a5ef1bb01006f8c9ea6bc83c67c918bd9d7a7060f101d1ba5a77821eda0030078e760bf0663ac009d5068b60b3155400477a863036eb48b35050608a0c089d7e0e840c0d6d5155396d94d7c3b73b4e152abf8d4f66f879f6d13e7a96885e5f9710d0517fc8318637900a0e58f8a9ffc95fdc7d8df40add78f83e8dff8664a58f914a00b45f48adc91d8de1fb84228f1ef64216a7959349b73461521f71e9465fe66e75a142f70e3b7c8b164a6e62b075de5b5c895ccbae240678ea61e2b85570c959ae43f7fbb2c384580c87124a9491c783ab98372f14db250c2ac5d5b2e1b67bd8a966b05f9d1377963049a777043dc1f6599bff3fad40aaef62e56704f24a711dcc909b9eb46e9bf250bc14f22bdb441be500f5bb08aae9bb5a73bd20ee92f09b5021b3afb6b211d209edeca503738c2d4cdc14b6b0f55601cade47b5096190e16b31613b78683a108840458120a124382c00898244414640d855a212048180c22612d09028b414148b80d14048259498a2324c5c5c14810540ca7f7c7b52c33c6fe95e9707c56e92c5c3f14d777e15d29282dd770ae70e7e91463d8c46cad04c0b9a659cf47d1f4cfacbc4f773d5ffb9adba2bd7fa8ede2c8f259df69bb774003210ea9a4eb40a4856923d9705efc88bc50d9ca35addecbdaab1569f7b39cb097755ec1a345fd96f14a6016e8158ce347c776dcc882241030a404140417434a5a8124ac6c405071a4350c02978042a1a2a250b0b58d185c544c140e41c0c1566010142981044b826162e26248513808db71272b0ab316b5918443ade11030020e41c04411080844dc0a02b5b24188436056602b1b711b711b18120497808b5a43ac21703002060641ad61927004fc970c35b2a2236830f0e9b7196a228aafbdbef4ca00e9828fb6d3d6b1d0f6c698b56d621975c015e099b07dc375545f06d064eed968902279151071234577004712c45578c1a2a9e321224008c3df9dfc3b9efabb37b4e37dde71efbe124760815fda7eed5fb67ce7db87e5eee599002b6f88f3625af80444e7eebb85ee1c5ccb8d22dbe97a1b27c18338dc8e80ee2055d4ce24fedf561151ad0ed817be0f914a3a320d43d3c657a239816121787aef24eb80cb77c381f537edbec603fe0da8ccb6191dcc0570a0001c750076a0258003efa7ffc88712c787dae79329526b5329018055f5871d21efc97e067202d8f1b8a139fb80007271dfb02304fc5486e6ad220bd6d53317effbf851fc6b018e5c00c7e8cf2ba541486fe0df2b1fe9391d69e0c7c3724b82fcf2f39f56213ff42cbf8531ea5c55cc0497fe3ad4ffe4fb99b619c0513c00f3bf1e1180a7f7ae6a4bbd950304b02a42824a7ec636dc141e026c622b36b10d57e7f29e8ebb8b144e90e643ca77b9b2cd51c68194c864c3af3b5d6fa76571fe3b3b06703426e12c56a1709c2f20ea2b0186c1254441623692207151081c6c652306b3b1b616b3b2864a226c203650b0b8b815180afa4bfb83fafe62a1045812212e2e8a04598b82911024042429018122c54112087188980d4c52020e1583894b88595bdb8044a120a4a8a49895950d1c0ab3128583c0a2563821c08ed6713bd5aea1adbf3da2dcf6ad30125c79085b6fa83119d1396153a2b7d37c62b30e0641a3cf60619d1c6deacd3ee7ba0a05e24656d537b3ea1750f956e683992d3016fcd5c923b087782ffcb28cbe0e22567b6e288ed5d72f38992dbf0e972293de9f887dffc05650091b8818188c80d848da4090a236363608717184b5a89504444c54c21a02b282c09060a435040e43c244e1507130d41a0197b4b28689c140f0ef4b61754f23f8732bb5cb140000349bf5180ab882b71c8e0f234cb5e293fc4d181d6b6e5e2cfdeba442e88fcf3029b99bc7bacf7c3df3d3d874509b5ec5b5c922fb77199fb2b1a8b8ded97f793e3e383a4973074ac117f1ec47fa627be451dc38111ee777e6ee78787f4203310a2365d6f6c8fdadd20b9ea2812e7fc177361be2c9299c54d3d13ead6fa0abab69725a5beea49aa1d2e9933a1a4adaff2fcab32536a5622b7cac423f4644b0097d7552bbc07fb87b64a9e5e9e2f64bffb29dffd2637ed136bed5631a10f265792744c2f7a577b8fbd70ea814d36e4460d763765c12db5b7c33b49693eb80bde174e2b12a4949802d3e66541df059189de8a57159003a2d05852e5a30fed3ff348f42f3f25c478b53f7e489b004623197bc5aae03ae10807e9d5245c0bd075a198ed543b09669c626a160610b58fdd379146aed0fb06ddc7610de0b71c2b10e62fcdf93afd83809d8e2303d64ddd517f957df6fbbf17b62d92d7c17033122508e1181f14725dd77b3e0592bb72b9f94944a94e35fd0c61097bc585877bdb297cf170e189c85de8e5b8e61acfd2017eed526e59d5ef61f7c3e6ef914273b5cbe7f1df0795884535a8525104da7830a25cf3be6f513cffa893bd2812fa0850049b83549fb69a4dd5e50fc89c1bd552f6bba04f837d745fd74627e4285309a92d7859878abc857e18d89489c67c0f1b4c4b0514038928872f40fe9bc1b7cf085f8a6bcaae809238cf5b359adafdc799abd913839f25c88382fde4daa807a37db6f3a6de49d9c53c363d41e1e59c3ba4276282b23613736268cc13f4b9a66b20343e39f1a0f58388f2522044f7147302f9b0536cac7514ec21a3154075cbef3abf1838ea407cde9a65399f5c7c68f0d06f888244ffc9d36e9bac40a5a225bafd6948ebf64fb28d67e3b958732aa593b5213e1f1eac3e1e3b4a7d47eb67dd0c04077acef8b2122841ebd7b41ece85a24ddfdf677b68f09f9ba650d149aed02dfb7a4b0a6ad4adb5c443b9a966f58eb774f93b3cc6f0c844df6d876b1bf62905dc19b45a1d9dc4e57e18dfd3e92f284b0b586e36699e2d79a6113b06764725ee7b3ed1df2aa7e770f3ae26a50f86f1fd83ce05887dc5fdeff3f298b03978dc2f1a81ec315c179ff36c5552a2e361837ed3c6de73839920f4bc3755b5b02ffb432e70afa3bb504267cfeb0b59d4c3e3c39824ed50ca55152749d4a29e0b0c681439422531db66127193b98c8a6087a4bf023635a3a10bd3ba88edffa9ee7efc175a53a941615e51ebd5307e6369242cdd1b8f91d7ef89ebff3fa643a1ac67fd1edfcf3f8cdf76c25e1a3083fa9680fb3b2b2b5d2d47031d676849cb1f2f573b3b333d19640784b8869b8ba1ba9c3f534f4edf69069e0a862ed819151122c8114838211e210511b1ba8a4b5b5a4a898a80ddc5a140e87436010092b88b8b89818088e84db80c1082b712b51105c146e039690149394004b40707a7f12a48ff3511804470ff60e3cf5c8e5277706c68e2ffb188eca24143e1dada3b5f21336f4b02fe265e91499cde041180f341e389059c67170fc8b5e446cff199e73a3ea12121d9e7456d94b33c1f03fe33b5e89a0e439c9c9d39e07df3bb39de2479d905dd91f8fbe676dcd4e025bd19e3c31dd2a5518b4723daa038bd96ef2afcea7e4d17e2916b83d6e52d20728d014bffa9f30dbfd5631fbdf1eed3f5731db6af69df2b0b91adbfeb40ee46b9b9eb361fa771ed48f84ddb8f671ac6c527b09c3bafedf58313bb54dc0af435d91b2fbc4da9d68a19cf512a8e9572d1bb702f0c0f98f77e35c8c10ad19accbb60d059dacaf927a6ce84e2ab5cfc8df3fe34932b23c2bfd5acee1f49faba0fbab11ee63d5d74a6ca600ec76bde7e14a8788076ed203d0c0be891072264fd84feaac1e0a0dec1d0c21df67daf26dd6c7607ef2d29ba716c6a877a4154a8758633b1a46763c1f8848ddbafefbbcd1eabfb731aecba3ea7e561f0e889aef89ee8da97bfed0bdff7eba37700508fac587cfefd15805e41e5db917ef916543c5061c1bdbef750a1e889089f4d70ff36c5f20e4203df6303f9a8c8e7cebfcddfa087fef63fca381cd8788e309948b8b85fbbfc0472a270bbdc5827fedf2c813437ff2321b0d03f9eb16e2969c82f9d705f5bf2a2b7aba28699cfbf8c0a2372e497f555f62d8117951876a15fda58a61ad2c6de1ea429306762efde167940939034d02e4ed673408c7b02efe41bae64fbaa46f383e6d4caca13be2970e3dcb8b5504883ccd4b13bb6192b82ae809d7dea7bfc36714ce692054a62b3a35e07cb5f01b6d6ffd0e2aafaf2123f1e46076550d8d66ffceb67a215ccf6cb9fa10e6e39bd377af5766bdfe2251e15f6ff44af68bb162d7451ad6e997b0cf850f0c854e5d32bef4a0a5fed68d406f8a059828e943aec6bed8cd42a169567b214639b6ffacb28287a34c82870b18fbfe03b176efa145b55f38a838dd367cf37eba577915930ecff5a019ed8d8c8833f37a667a61bd38caab58e08730f60738ef47d99f7e14fa35e42ca1f4d0ad244a4283145cf0eb4fe0a79c04e957ed60a0d7fe7349066e7aba2177abd087afb0c5441f5996195d323395fde4dc9c725d11ca89d233b79038e841f676027977fe19fd79a61b625f5833590d78706d2ba93e4088acc960632f8e4ca554db6446887e9a86795a4a3c8f66bd77bb9d4a51ce3d3f603fd72cb36c6a53975bb937dbcbf97717e2d701e10faf1f1f3afd0ec955839d5bad001750bf321de0582ee1738c844553b18de2098635b3d0500478c03cc0cef3ae94198e488f6318dace4865e324741a0bd263638a9caf181af86e32f345cdd73c6845e83384f28a2918568430b965a0a5435fa945367d5fa467fab4f5574191a72bf327f442bb7062a84fa2f4a9c211bccbb820bd9d96450036a40b15b9f6aed8eaf6d04ded7195e952aec3cf8615b54daedc84be508867617b8b27a5562e055271efa84a1f337c5f609cd8af18841c946f3cd1a2b424f1b006cc538b2bd233b9be5efc0a3e70ce6c7022fb62cb75ee47f58fbae7f50ff58ed21e6e21ba50af6c757aff11c52eb4971145e9d5e6f658fd23c24ed679814d80d0c7ada3424cf3e47f02e9f19fa1d0e43efd68e6148d2afc463ca7ead338caf2b2389aad7035a5e354f4049f56ad0c9b94b2dd8b939ad41ca87d230ddccb9354a8e25aa7ff6bbef8d77c73ad91aa52b975475eddc2f1fb34c6acb136e0b8c79638e2c0aebe51a4116293db2ac957a237bb7e6581e4fae364f1c11dbf218d99f16ebee12ed6c7233d9413794e5bd819a7fd21673cb20349d2cbac05c8e182435842947e1eaa5991dd2562cf25cc092cbe127aed48f2844b6ad43f1d8ec6e1784d0f4f58cb027e4bdf50ace73bafb2f5da9329f5fceaeef80e40c9470541e2ca99bc16cec7767e93bcbb5b7fbdff83b1fd26fcf238b8099678bb2a54b1cbe7bb1d594df408cb198d2d55794f533376f7fc8fd1f902cd843b3fbf9514aecfffaffc807f23152181b268ee0063047983ed87fe56f7e7977a3c240106b70741af0f05facaac0eca1f3e4af9851ec78f8ef1ab4965915157c152fa9c8b2ee368fecae5688d9eedadf7f8f4a0c79cdc9e386a9b444e703fa4c75f777c0888fa8a494a5adb8014e424ad60086b6b31714924524c5e1e2c270e058983201045258818122a2aae88fb19fa17ce180cc1b6d62980f2885d7d05ca220286dedf796fb69ceada6ee031ae7c1f17aeb604512746a7747b73bef0e66879b11b38c6c67d12913f9e0dadfcbe823920e4fd90e1ebbffa8c0039c7975f38027ab63f3fa2cf0b667ea9c1b02ff1941e675cbbf2aa349aacaddd50c063dec8e14ff7efa3a5a5a53b79b0d5b9f1447724481e95bb1c2280dc0d7c57ef860db10bc4c7f7bfade7c3f1fb601b116814d985bf8c43d89ef1171ee1b1d57f2282ea2d2c6dda3770f2bd02b7cc8734129a50f88c40d233fa1dcef03bbf9de179f7e0b19bce199faf4af7e0ab209102773ecb13260b36bd3898f67cde7743a6e48e6f57d2b6a2fe693736cb4fc2f5b81958ec3fed10458c17cf585d168bcd59021d35f7345d923812969ee0a1f2becf883f53b47ca75b92f52abca803f645e027f457c701b9013f9b11438127c9c700a8ba15a0302a84887a2da30ac88d7b711cacfcfadd2f45707efefbdd96df7d7f74317cffdb803e04d1e5975e17c99a2f31ac85d056fb50818368384cdf3edf58583d7c938ee4523292a672c252d462498b3a20e6938e3700dfefb9c29322d2dcc079aa03750b8fdb32862e86bed94741be4a70c4e7294bda8070e9aabb8d34b149e99c6c546b30f7e20d820557322f7ae78b0334affc5bb3f40cb4bfb041546ed10f33902a24c577f87d10e74226803227f1b96e0448f8453ccba2330e17e8d5f7718d6e94ab16ec68aef53d72f28cb42f578066c300ec46c197f48e1eb7bfaeadc6e2fff4020092c3ba1848bcfa907ef9667cb4f053cda82b138f0a6a89d28d765b5cc6c460597f21d876ea102b3be17aa5d8ec9abafdf68793abbdb46eb96ac61f4056807461a69f68fc6501682ee09d2a2041187735314e423cf6a4d95d515cdf137cfad3a7574180c2f5e1a52cc2a99288c3fbb79ffacbab60f888b0d7a4f9ddf833756f7654b3bad74de4e504435a8797ee3de8e9ae253a372c38a5febad569e319fba2ebea0be8046e672836eb512709aed2f0cf3e4cf6abf03df161622a3296e313aa56afb2c087838eba15ac44137ca173e0dd0d1cdb08beada6181845fcb636b996d2f0d309b91332d518f7df950f93e0de59ad2ff30edbc3cbfbdfcb9206eee6fdbf19ec5f667702ff7ff8307f099f8df9f664f2408dbcb14d43d3d59f0d7946f75937528e727a270503bf674832fc5b4fd54ce77fc053c5dec676bbb0fd336ddf79a9b545415186bbfef4d6bfbf66979eaa53b818557f925e33389b23172c75bf5a3ff90a54b6cf7459646d7f54ab7291e9cb6d85ce7eed01b0432f77908b28b16dc1d457887f605caad4f381e759c5e7f759f4643cf1750d3df73feea9c2b2ffd83d5517be96081732a6307e3df5d1f65d0a1782d1f0c5208be09d83e0e49273eba0c7dedfd768ffdd3d55d8f0532ef7cd6906f7b00819a2922c248888a1d85710ffecbd542d5e9338cfcaeb282a168ba6fbd7ea83bca6d5081217db75133343dceeae3491a17241828ae801b1e3833816fd96cb6812d0994fa4307194a7be10344c3c9ece3cb8291238f5d2754461614acdc5d9d566d92cb551ed7cd239bfca36da41791b8ec1762f5d3f60525f40859cc7d94f7fc268ff7bdeb3e8f61fe03da2d78b860defd2f976bd9a8652f81ac96b5f73d4c291f758e2ca7b863b7a030b1e16915815668e5da2e2951697748abc07b51bb6bbb0f95ef80c2d19acce8361fc81da5af6758f61e9dbe3050c1721b32c39f5bab3b68233f6424d6cacffdbbcc71257dea39ef490c15a7788f39a7d33847d5fc750786452b5a9e7abb65b7dc7027c25265affcb790f36fc948b35564c31518728492e6bdc3a6d1d2e63937e7a450d5a8a368b99d79c998c3966e334fd6a6344189d529a57d4269f3141de7678cd07f0b6656db9fa4ebc3cfa33ae0e4308440b84125f542c2b497a350317f6bb74e5eee36ad7e5357c4a42186dfdd16902c5960ded85d9c1ec330c8c62f206dec7af9c432c877487a9ca3ff65a1f6184ef32f2f20f7d0b5c3d9ef8d4b4cec3534b65479ba3affe7f67546b6ac8d60157f0fbff9d93f26fc3327e23ee3929df96cd866dae5657d9caeba6a414aa9a279d599bce7cf02c5f755686a62116e07812d06804fbe89bf5e589a7bc4e86235b36a7f4f66b7f9b931253b119baafc1b730c4f17dd5666fa6c6544f131037bbedf7ce49018e02fedee37be7a4906821bde05a704fc7bf38ffb79c146d9b13da726276a093922724ad5435e41de17a7a0ebafa1e2eee8ab606506d7d391379534f2d6390a72fdcf8efb3bf72e9cec172cd132254e50176c79be5ab2fe7842b6737bd7f83c86d3b3a6f73f4b80e77ea180ffb7846cf7da142bd377df2161caf9216bb244612a63b62d6bc2ee794fe899c8bdfd2cdfe4ddadf27ddcc8e17514bf5421b954954c35eb06877fc56bbabed50c6f29714f5000935caeb993921579ad30eb55693e1b13987f18c66ef86b4ef273ca5526d6e733e49904fb6c1839a575ce66eff7b93f6ff7cbad9de9136d2c459de45c2d15dc1531e0e5137f6004b98f83b9a7a391ac929e94a38eb9dd0f331343452d436f5d47677dcbbe7ff1ba49b8160201004094388894ad858c1c01262201b71301824292629018582ac452520100931184452126e850021a1566008c24a540c692509065b83ac2570ab69856ba953acac4fe601054f339192d3e46259d2f591311009b15b80ab46a9c07bb280a3f4f4e28e6cc247eedfc75ba7167c4373695e2a41a337f5738d9c5825a7203bad57d8250b12ecb2d7fcaf72529e440af1e2ce97d33df97f1483f4eff7991b2b2b5ae795529ea6118965da50bc89bc3a9197d3affe64bd67d93361e8be9d740381e120c79b69feb98fefd277c32bdf0df2052e81efd1730b55348cb1f354a9b4afdefd2106fd8ff04ab8b2bdb589a4fa4915450852d3d758c9c55a5203222667aca0e0eeac25e665257e42dd475dddca58c34742f16f24068d048e98485b54bc80c4be7c4c53d5a960545c133af16184fb8ab980b570a66f2f4be7f933ec1296b2eaafc8bc451e4733a5b76a12f01c6803c95e98b1d4f12afc885d0ceafb7d3856c85cba3e711d70853c018526cda4411f7a7caf8a3ce43fdf09129be0408c416cab058f26fc0d4b05eec42b9b57aeac4982d5a915703cd8b0b52a22f87dfd8bf2b58dea34551b9d677959f557745f943fe3d711c0f1ecc5b5c5ecdf7c00b118e6f008e4343e456efd176c4199bb2fa27c9806173cb2719298d90e46397aafb41081650b24c43f7e4f8662e711ca7bf1d9c1dc543a7acdac3b17a532ca14f9825e312f0cdf020977729fe737b9089b6d796f454ad9c2d61962677cdc36a6df41a74949ea55ca656bbdeb4bfbf7aad39f80655a30e9149ce8a17d47ea1d9a80e33eac96ea3fe8f39f479fd8e86ff7dd1631e973b77864e324b1b3bc3b10b9add00651791647fe58edf0f7a4cf97dc2581490dab6f61799e8ef7de8f0114eb544653bb7da687a6e45935f203aaa936390664d8f01fad2203c503eb5ee35f137bf44a27eb62cf3cdd15c14b6f08ddf68a3eb54fbf51ffb456636989949c61bb10f1b145c814f1833e7fd0e76ee973b77864e324b1bee3f210bc6d034c282e9c6eefc9a5f99e4a931c65136f62afec42ab0b198f579e1a01b1a2e665f87e639f1063cd97c4c86b3219161be531b557aeae94a7107eea99d2baa3cd106f6155cac5fba9d68d789f7059378eedd9f1687f13caf1083af5b4fa71b4b6600b3ec320c87d972812f90f3f364a4cef09a8a466c98a3bb98c517422e1c5c2232e4449a5318db504aff3aae4729f4c27003715fbcf6c0a85de6f9b42b1173debb1a91a58a80c5b5353222c1f8514e3f97c2551e65e511d822e8163143584b4ae2e9fbb34bf2f27fed113790faca4bbee893c1f450ea23a9610cad05edf7461df701ad9a0e3b96d1194df7c22e33e0d4e9c0c791a9ec7bc288928e64beda1869e81758c33d13fadbba88780b5a86e5cf8fa22f0c8edc9e81bfdfb9e7cee5e232b98bda88612bfdfd595acd0ff30b871832e9774af7ada3f9aec38d12bfde5e375ebbb3992d74bee56742ca5fcc0ef7f1e7e0b72be9ec19f307138c4cc1645abeed70b585e79b6dd64fe2b42eaae2bf138e177d1502f708220fd625d40f2a28d43974a651bf0b15e821a6b4adc3591e1a92fdd9c8a2362cafcb32a979486f5fd1bd24f9195bb295ad5898ff3398a50bac506b1ed95245dcf273ca87d6a0dd98c549464e3f3092a6f44b4fdc0ef7f1e7e0fb9a169e97df1da27fd38602a779edb7f91f6d9ae79335e4822f144c9219cb650eecbd40058a2e4345f5c558a7030cdcd2737948ea13e0730faf18cf9c70565b937ea1607a268dfdeefcb094b4dbedd5964fa76a8d6df2d894ea694dcb5f32e1f0fc35ee1f74ac8477b63c156b65efb4a12c7f036c51963ddd33ff0fb9f87dfcd1ccc37aff7f8aeceaa74df519b28bd514f2a2cbedd64beb65bde7cda38f948e54883b8c00d4cb6e5c09726ec9e7d9f55d32e21d4d5020e2f32ac0dc9b924ba94cb77dd4088d81d68b8e9e24f3d5aa630e3d7d2a040d0691976e3d1337e652534252b6e4a05f516a5827a2f940a1c3b5960eb344184856848b1203405c6f3c7821487756bed407a4667ab6f0732abf53aad73fd20ea7f1e515fcb6cfd2cca7d0efcf870fd23f2a0fb15f7e23c98b717cab859968470339fdc7500bd392964e09cacee0ef76a9207db18e61c67233796f39129ecee78eac1afca5cc92c6cb5f6e43c9912a8617cc93b8fa7237c68613483e83a6938dbeb6cd3bd3ab4601f7c148499aff6a65fe1d199632beb752d04adfcc0ef7f1e7e3f74ca9884703106b9aaf2891d491a6829aaac48fa9ef84d783442e110b1eef2adde0e80707bacab3521aafc6a9297d4655bb4231c35b571863483fea114a0b88f777cb3883f032c6a4b77d5a2f525f873e5529ccc090eddbd52aaa3849f5d3b1ef845134ad4722904585036f7a22be3077efff3f0db62f91e5f7b8db30c7171cf3845dc9d3c42b22eceef89df478fde21963a17232dc4749ba320da8d79f3b8f8dce599c0ab8bac11c096679a574ee2f91f39a57dedd2a18b9a1f7d99ed9a55cda07761d4eb81c79e5724b7cdc0eeed15ff3e141ff4e84c8e49c3787ea8878d6e584adbe2d28bff6ffcde7d68c10ffcde2d7e0b915d97532f2418baa5b728980b0d1ad65d633ebee30aae31af71c1efe903fe3cc0686520bf6b9468671a33fcf1b3d99881a114526115bf837ac665a74529404da7f1e908ee20b957d1791003b2dbd7de6d2c3aeef7643e7b57bc442f7daff83763a2dcc7872ce741579a12eb8fb40f9613b75c8efac1bfff79f8fdbc86252fd66cea9a67596c7743828a3a529467fb0aaa0cddec5d509cbc9b722397ec4f3c98e39e6c0ab2a2aaba7074f694ab4129f0a4b9d8d08928e3f7ee66def80de7cd46959e2ebf795159bf3149c9e9ee7240e238538e4e369f2efb09636b7bdc94eae52e149a94e32cfad0b1eb958d7be0bc44ed86246e7b3c8a2f580eb8c5733a8f1c5f8b44e7bec5b3f33892d4cf5924cf7fed20a7a85f45d0b8073d2f71ac9a85ad1cebde77b97b9bda1821dd92ddb39ec51307688a748b3e9cfe642bbcdcbbf39a8bfdfe61f69a2209314ec359fbeb16bb0ae4a813b82e59726823968cca5ce7f0f40b92e3de4ef55be1bbaa52b1848e5733dda127c5d2d9d2d2e0e5e738a10603ef8e2565ff3faa54e0b0f69ee3c78f2a26bf1b7f832a265838b507eae71ec2211407c3cd2b11872a3d30da109048d6a578ae26b8939dbf758351bda732e7ca538c12561e4ccb05a9ee32ae81a34eeb03e43e57daae7a7fd8e986e453399c75c0de08fc0e6565156fbb5ffa1787d0bebefa732b507dd4967ec6df965db927c5a876547ce484bd8b5a44523ad22a73eefd8e7515f19afde2eb80bde1d0f70705efd4edd82b795dfe5fad480f9cfcf8d39f4a4526b4c56f3a0c611b27001f7101003475070074c600d0e201fc4d82a49d6e6ee324405e075cc1d7fbb5bd6915bea2b268eec0fe2a22bbc48377451a1bc0d1f22ce556c10332de141c0d25d39ba56fe7ea44b0f5d0dd55a5cacf332376a6fb4e6baeb1063c883e69551c4933b89d6417c54ff00988bea14e85657d6c19edd40edece6e480f11a4afb51ddcc516b95b941410f515fd93e3af11cc6ece6c5c2b1d26dd21b90e30bab48ff8cebc905ac68ba898f10b18b9b78334804bb516c44314b34d70f87a62ff1300e58ee749daed056c492bb856a2fca6d221b677d8c533fe8a637c5bb816fa5097f5dd6702ed635e4738a45fd428f71f90e7dab1d2e12f9d8484b9aa7ec837ff0bf20d7005ff2e0a4ddb8742f3b6f956e1e73d41794be2a62e60939bf1ff4c3bb73fab1d2e1f672de75144429e26372ad39e33270200250a80003c422262021220e95fcbabb3dc0d4eff7e5ca173d8b122d2150698d777851f4fddf10cbba2e0c48a13bc966ec7fdb9b248b263acead583b9d8e29108de54b448cdd26ff0410f72c4b9dce4461ac8d38dca5258faa82e43edd68105ca9e1fcdf93cba46c4fb58ce2e1fbb7e8ced69d8a78ed8e0a6f203c96f916254a3a34d8d7847a8889cc8659af6abdbd5ebbd5450cafc22ed64fbf48cb489d4dde1cdda35fa6b65c1cb4571601719a54558d607d8bdc38e65639e8c5177200daf2a65b055bb25a0b72d5279256fd6c1968c5e50acbc454d32433ce03eb3fc0aea4558ace7ad4d69389afbc810c07165a3d55db326b29157844699a3b9147a2047b26f99cf6564d9e8f4beda779e4180d1634dc91f2f9b3d58a6bf1b794383642467a6de0befa63aeaccc7d1bbe52e542445bc5f9c37cf53d8e029d0b34c7f6037bccbd8a931e97894fe9736d63935df5667f9a313136be7698a76149a4db7bf8aa251f1c04a1a8e4a374654fa40ec59c6130edec143be2f973942abda5b8ee163b401a30ff576aa35a19abd6a1c2cc9e62a728afafc0b0c9154e022ff237ef3b87c1a36f0c841c801feb68b53184afd7473c1c1f2ec7e70d1212296c7059ab38f2a09be6c857f8ce9fb90a0b3e63349267850952897592ba5b002437ff81a7c0b4fa42a8fb5b4fdc075ead1a670ce4a8cfae944c10f8493d8669dd2e31e5b15371e67a0cf3b88811c1c2c5db29b515def5837672ea5e13d09a5f952721de3402b3479fe3469aaa2b77ba823d5e3e5240d7baaf056b89e77500a7dff6510c774983c81e3e5891a238bb9ad7043920b5686567c6ed71ce679c2071273984cbd319a76e9244de8f7fb68c40576912de839c372d832601831cd49877b422d33fb243de36c6da302eae3b29dfba63104d8f7455502f61f34a956f368ca52a5066e9d3a8871e036116b94d5c4bb2e1293aea85470c4569b9cd4c4e85eb8ffc0bc2dfe8869775aced10bee0ff3ee33d77dd0c16426bd341fc20b3aa273e9c849f5dfd31f217be0b4153e7e92358e3b364b3b3486fe337564e42177f33318efcf6c6c8e2f4ec7f3a552e0d303121db3f9ceaa7c8ead70b32fde0f7814a9ca799b6e5b84b42be97d90490cda0a87bc947125cd273d16884f7996ed0defe9519e4bc35be1c3a02a6b5fbd371405c3a78795079a9d8cacc633b7c261e8db11c5d1483ff127e94a498767cc62085f62e0e7e64144ba9f3328fd686d7a5796e9e1cb8573cd183d88246ff18e7b0ebbdf141e84de929bb986fa601482e138222cf0bb495a5d8e0fba1895e7f4ecb28e9c8c290639c3e2b32aba43697baa42973d4de978dbeb081b30627a1fb1f6a70622bd791415c68f1872b75eca5d6f08c5d83f0274901c372a69b1e3eb574986c186f4c792555be1a7143b348126169c0165f44af3c63ce73f1d4ffeba158e46b470f372d445bea7b966ead01057925012791143a0b3a46151af232a7039d09e7ef833e4b4921c3546f8cd4399267dd0f53b25907acbb70bb724334e9cba5bbc157ed80640eedabb2f350070d849ce07c41252508fd1b093c2652278be37037dbfa3e3e6d0089ecbb5e213f35be10ea513d617798b3e79679c3ddbff15a6d2ac740aa3e49d4904da471b7968a13e90a15657b9e88576d28191ad703f8dc44c016ed289068dda6b1d0ad5907344ef30b26694c6ee082cccd9eebb796dce3e561cb170f7628bcc56785893f658fe958fef0d5475e20c53adb2cfc6a963e8f77530c5092ab7170e533d6afc1a83a15710f10e18596f51e7e2dba99ea91f183868af5c9b61dcbfbee88321b23806aa675fa26cbecc1ed36bca9214751b5273f2e056b835e884c88bf3a784f7893e05acf2fa1124bcc954da0ad7cf151d3e39795a3181c8036fc8db236821750aa324e17b30738b707e8ca06a95f983b5c7f9116bf75b31de8ff15eed99974f6f3f851d278b7fb9b9b169786399712b5cca82f1d0871b9b93bd0acdda752949c9d5d30f305ab17c98ef65397d2c6eaaa6fa65b351fd496fcef0098c984698aad97ecb76d288793cdb79e734e68192805a0c852336736ae02de71b5705d23a52fc21a330df6c3e8c9c92bece77e4d0322201737e7a5062d446dc4cad2c06fd9190da1bba2506d174df1c3826f0908a8c70a51a033fdf6476a53d3f7f940af8458a247289b0239fca0e637fd304eb48cac01c96f762db8c1925f20452f47dd730e8eba385d30693d0c818fb0986976c6c4507f45731aa20735acd370583731aaf6e2adab0244b478aade16374c1973d91fd4ecf83fef163074b424a61a30226c0438c8add06a22b05f3a4e40061163d2bb1ee0c7652257a8cfe2d27337c1a256f6464beeaa4f531ef15097b48ce1bbd15aea2a20ae5c557d0e18baff53f0c7a7207fdf21346f842e1fdcf29a2de0246da105ff8ab5bb7e6039b82315a319cd7fb34157c89a11cb6a2bc6f3ff2f9f533e6c518c2a1ad8d5e53e2e8c6aca3b42120f0d6e7f7797c72182526bc144d2eab4e74c934701fbf37ed798afc35c9d1aeadf0049e55d231a3f5b79afc226f47ec52a1f2d74edede0a5fd53f2ea6cb5ff04c581a10f56280fa69f5abfd18f44ddb2f883a089461b4bba6751ee0c9c6756de63c462c652f575103a1e287abc727ee748dbbaabf7892d58bb1bf4a97a21e3fcd22383a352d79b8216fe8a9a6df5b0ce1dda8d5dffdd688a77756d19cf840b949070f933f86fdcd4384ef75bd9b361997dbe4e5fb9b1ccb778a6a49b7c237623e93166ab16668e4b49e5bec195e79f86e14c300340da5e86f11f1cd6cb39435bbe1ba71f2626b14a601c965e3d159d569b1a471b7c43a57a6f1d6bb1118ad7193e559e2bc4fa11a5015671206d19c2137ed64315a255ded6cc8b34f3f2ec3707ef856a8e5699e6c3321a3adf0d4485acf02e6f2792fe528b3a6c64efc4d680006ff2f391a8806d9ee7bd7144c1ef9f85d9dead354160cfc193f9351acf05eabb855da4d9fa81e0f5404b1c358bf7b2111a6be22e9c97e5b9645feb92e3ed2fd0a462d19f3928b57fa339be246fadaa9490d4295031c5230221b783d920f07dd14e3485f4dd4ddace7a0ae28907db815be768353f52987461e85989e65465d074a5e9407033ff4df3b3b7882c90bc9c9a845fcb49ce5b2189231b6f7e6e0bb7ba75eade3970065dc8f7735ad9f558e8edb0a9f7b3b9d27973e44a54a236811cc6459b8c1ffe8c456f845bb53e3b66c86054d63895c79d9177db9480f61d86f81a0debaea97cc15c3862ac7e68c4203d2981e61f41fe2aa11d9fc50a33900d7f74ab618db8777888e1983ff5ea9fbda1e7f877021e0709ac08573a1c625678e61844618fb5dfca45eaceaef72d85dda2f817306c8acafb1153e0bbd8fe8b97a98a29345d79f1b951444c9d58ed11813ff2e1fef5bd43e4973c70bdc2d7ca3e593890d18e1b943b2da739fa73d2c73eb98ae8bb954eb04ebd06128977ccf2f78be577ed44114904be6c9a982b68b782588b1c1056106796f6e9cff9cd4c1728459eaa59db923c6f3df2af5cc341e0e240670531851ca1f8a7a1316f51243be933bc4b43af7a437aeb3ed74b51218e633dc751683feaf7c989626aeb497e739517098745271e89d72d3b7d578e7ccfe03d57833edfbb28bc8cf2fdd9466e8fe92c9db9998de54fdfb6b76598d178ea31b574e113cd79d543f7cebfcdb2f11ec098a563c9cf26cd2f41357efb2eee34b4fcdb6f7a549eb138af9b20a7fbb31394b6bc169405e05882532e8601416aa88e83215fe1faf048e65ffb157e33d41329b743c85f2911ef20e4794b94cebc098afc382c48b4b825587ababde4a6169a0f0b7afc68b0d3fe5b8678faeb58e2004c793a31f7c7d9b347a410a905b777a72a8be3e304b4a9b07c57c9c47ad75d5e2e4846a1bdb4cb49034798b6f0847975ab33a83322bd57b334d5c2b81cf87759417862262f065056c1e9cb33a28499864e3cbcda4a3ca9da0277e28554a5c319f1aec0c565637d6182e52b65e73faba76f3d2f49939d288c7a9354da8e03fd3b3362d9cb6c14f6214b5a52d5fc8f1b7c73671a4ddedc7bf6e87236d503ad9dbd8cb21e06e5e480f43f0ee491b43ac765966617e3d06ae8f7c45c9624bee3b797fe53a7437f0bf40fb7f7dd066de1a2dc696a9be53c4d5e1562328c6a904707788ceea3224340fa9b8524d866799ae6caa178ee5068a8cace3f2dcee3c99ec73a7ec721c8361cb5797e84fdced367ad8a845331760a189b10704cf336944467e13f528ee259f69f523c7e28bde6da5e89d1d91dc0e75ad28340d4f70c8d7f25c69acdee558ae28d2cb749ab2e8bbee563209bd8ef44bde3b7e5302fee7f175cb9aa810c280ce3f582ebb6f92fb0643c72792780abe0ffdd1c8593571d3ed96fa77c0ee1f38a557803128f4c1d5cc2a60c85d05028def7c40619befbaf58796e7cd0ac18621f1489bb9ae0f25f42178977ddf60999fbb33833e678fa38013e2a6c0388ee51a9b260d451d3cc8a8f5ed30c27dc51cb3a09b794f643f18e179ce53dc8e3ebaf2e10c96f9f438cedf6904df977ffbe5ab8c4daeac895fd13eb814f71b2e698afb6cc58a5a619d56edb56ac2b643d9a7c28d0c21f00056517078baee292a0ffba3e75dc6fd5b08b36a44b39bd82aee6a5387eb179f9be1285c985beac8a4787f9af7e315101331bebc6585a9d18ae2edb390e70c3af4de0a534bfe015ac397424b8e95a4a4d84f2a95eb20e2991758b139e3089efd6c8bffd91e4fd0b89a7615431f1065ed467e287de460dcf632c261c057feede5ba3c5c7002fbc1802d868e00d77ea87f8266c82be2f67556ad10e383e2f0aba8d8f52339f4aa351ffc4b03ec8e0e3fd6eb847614552c993a351faf12f9be549aa262e87e6b8d4156b305af13e5d6c5480b26fce80a6ad68f3e1ca9942bd247dd17884ec555a85e0e17a4ad546ac886deeb27e8fc70d3fad35164648b15fc4de68bd31fe24c2fb94868e3b9df3b77e32ab953c493c852b8bf26fa75ef739980492f92e1faf507d7716d6f826b6b504becfbb3f3fe637bc0dd7c9fbf80bf7285329b4ccdb19d7955ad7d54c1e6d36eefba6a99ca54f4c2061c2c2f371f7ed4687053c055144efd7c382185d9151249b72f7f3446017243839931a727f1409bf36ef6ffbf717ff058b584d61b591bce54ae285070e72742e64ce475fdad175bbc5ec79c9f3bef4c7ec7e0262bd1d711d3f3e55f2e0fdb09f9cf470b779b9843ab91b73b0921f449b8a69ad4fde25fd4e6f90efe455a9661b897e67206951c91779256467b49a219c6e794217645d84c5cb9e1f2aeca5aba68523c372808c33fe591af924597277e7366184df6e8da93bb0c9f5431ecdb0837309ff6ecc6296626818fa827ddf3ecb775415be19a36ba7df88a7625d268879b0ffad4c58dc5b55f61d82fc6fc22cc5e3f0a3171582f10cb5cb875f3a632469bd8c4157bb920d323f7399f3c9cd1bcc5502721e7a48c71607970cebca66a76276e2fa60069ba945f1a7b81e15f7c1686ef58da3cf946ae9628132290b87fcc5c0dc3bf366764f0982ae261df24097d689c1abdfb1919518cf539d3c65e8ef08dd657f109cb7ba7148f94c96862d8df19ec239b82112fed2ce6a32fbf8c1a3bac4aad89111686ecce7c9d183109d0b4fa383c9831f1e53e7b6800c6fe52c52c9d1626634895679790f187296be8336304bc741fdab878d46efeca0db9229d25aeb324acf6c318c101a5a78c54d3651e10cbf35d3cb89030bc10ade98e51aa9ac7406c28cafe93cb976883def86b5ff8ebe68730ecc72b2f4e8a51d3879c9bc925d957132ef341e4d35b8cef73476ad2f361d5e3dbacdc5ef71b675d288c44fa3082ce6734e3c3a178f419ef4c38bb45ecfc9ec6bec8c2a8b5226e5292714e53fd6a17d94585e02319b46fc35230ec87bc66c7c4d995357a74ecf15e9fa90a972f1cc8c3a057f17b3e9914da05d4be35c7b422b5f45ec624fc1f7b6f0196d596fe0d3f94b4d2dd024ac3439752d22d2ddddd8d94740812d248a8a08488744a4a09487783282a12d208dff53f33f3fe597394673c9cf3cd7cdf3bfbbad44b6ed6de7badbdee5877fc6e93a767e996a279e2fdafcc8471b7d3955d8814d99c43f081f86b3fa6a5ae26e3aa71e9914d86d34da1b42ef9e12220fed185531f29be13722f58546532bff7c323ba0ac03f2ca53618dc9523733bc8e591ab1e1a827eaabb0e909cd6c1875c37eae83cb31331d59158883f11bdc70ac4c7d5b396556782c766a45d8c96d1e04b07de08ed03584b0648c677ea738b54e784d01c71c8d7c6321cfbbe9da54f725c3b6c6a587b4e282b6f22b9dd5082add90b30fbb6a6adb9581b0452aa7f0f7f92c10c1db2d501f8dfa9ac94c7314ae84a1e1e48bbe87ec73462d214713c4b7f1a0ad96093ab3b6dbf87f0688eb9f9a3da8c4ee359ba4f5f1e4ebfd48d4e4e763d11698729a287b687409be7d7c85d45c93a947d6d3cc8f2b51489b9715ffb1481f8915b5d58ed0b8d8fef4b89778c955fe2b5642302e5cf8ea141b9f1e3e2ecb851d1a61a4dcec84f4442818e3378ea16fbc34edbae35ee1e9a51c2137918927e9467e958def9d7275ed1f03d604b5be3bf7e14e7fcb91770b2e0d2b979f25eb1ba05c5c0ae66597c665b2d900dc40f0207941dc39082ea0ce204a4f20bf6aff8581101fbdf7583b507cda3d15bbb9df85b22055460a903cc4fc6e85d66bd6c2fc3e89dac95cc4550687b7d4101c80fc83be9f04f9ed3dad8479441c21cd3d57d28760cc84787b837b8ea2af398cc7e73a99f254b9d242b23eccfd2172ce673b5a4195b9377a711c25d8dd35fd0a201f1715903c3c76f48bc172691efdcf89a41cb1997d402a00693cf5f6ac417d82416b86eb1b34c8f944af8b6c5e22cbdfd13c1406a47a848cb002641ee88a43db99a1ae0df9ef860a5ea269145576657df234f613eafa76601d87bf5b489e8bba5c5f2bbda7531b84223781a813cf700fe735c3174bbfb6c0bd3e3d3abe9a8bbbcbe26778104d139aee16217f8ea428527e6a64c5e4c114cbe6d80bd25c33df3cd9dc816e58a17e61893f29a66f6d0fd8fa0fc1a3dfd72c7b4b3fada1bddcdb908b145eb5a607dfd146beba137325f1f05458617f0bee509cf1607e24fc7587eaac7d9c73b3d2494776ab5c632b0993f01fad73c2346f4f61217d9d3196217a2e908d2798fd2f767e90f52ef5e853fc2f52f74fac0c7a9a661b1cd781d7012d27e837833d23c9e9016370c544e3533e5615006e247725cfd0315af0ab8233ea849a50609e4fa5c6a03beaf512e63e4b088799c598576d969fba436fc5e221770cadfd46a0e80977f77f2e12ebd24ae009b3645521fb03f072263f8fdef5bedf5a7573ec83089a3c0d407ce4b1085536a284e9f66e3a838c5a1f37ccdc7a9500055bac203a96b456b2f1eff465de13389d6e963d329a0a080cc725e0da5c5c1dde7dd07494e06229fe8d413e0f9584c4585acd4eed52db73ebe0e2931cd6b221a6e05e637ebaa84745f24f631b64d2ddc8320deab2afa9567e9fe89f70ce8c3917ab25275d2b1a23c38d2d8d0ee03fac737edd5c194f1cb0df1984bf0fd32b4ea4f0e01fa98859875462572e95b176ce88bc41495c7f047407cb126b1f9f1a9a3d11caa281c91d7abe226b626108e2bf0d86552cfe24b6ee6ba2994e9a9c0b5052165c02be422984a1f543ead67ebffbe150edb0fab5c608f1cc83f6a27bc62a9b67733f7595a490883d8cee8fa1ce084729c96b1c8a87a5b9cc7918bc3fdbac51609fd11107fed7550c560b3d6900d6e7b73e2d9206dd3922304c066bc2432940f5cd4d6d9b88b8181a72db739ca3f08e46f6144052cd31dd24e0992350e79a8b92c047f4f8d06ec2f332acf9278fdb8f199db9cb9a64cc77ed31640fcb0d8489df495a1ba66a9108eb603969174de2a1ca0fff3665b8c5dfc8586d3d89087b0b6995b56f25a00fb87cd0ee53e330de70acec34b66c6c50f7915b42099805735d1202672522864a49f6c8cd271a00e9f420c48bc6609ca0a4e566b92dbcb8949c6b1bc3da8eec40bc4770bc63f8b719e14312893eb6abe8beded856beb02e27750ec34f8ac24b9f705a6e2d1ba7cc94d2f69a38bcfd2df2632d6d0265adabadf9df7da0ee31ba8f10f03f2bf5c63eea3d3d9bc6baeaf11b26fb091f2386cdf0412a61b18bc973d14a6d990f0ae16e906e8ef8ce77a018e4cadba71a44ebf5e143bee9be6eb1bbc88d5776400fdce8d8131cee03e472be7c2b38db7a73ebd24c004802d1299b60a9f22dd635e0deddcd4477cc7e119fe0278bfdd3a0c02c4e419df1bbdb490a9b156490531d793b374664ee449ddd29e126b2a9fabdc8f42ba62e46f02fc9db1f53a5d4d3622d378b8c875764b499ef74624603f8c551a85d838619442cacc48868ffa9e2a4a5a02f1470f862da6f6b741b35d5a27e4e58f2f3f307a1a6cf2b7b6fac34bc1e857128e7fef98146b8f150c699ff76f653a3a6056915266ddab673bf74093e492dd04e39ed221330cc89e07757ae34f8e209febcb1d69a9fbce75ca88968f37c16f0604e365f9acfef6d713ee2e6ccebf672ef3dd792a2f55f40f3d806232915af0e2ef2ee2ff1972974be8b7bfce0eb9f11d1175a7e91faa01d5d077396a572ee06f5ee5c18060a49d0548f0a54b2d66d56dd948c93f3b916224c9bcf12e4f63fa914785c1f54531e76106ef613551d93ab09cbfff5db15f397d1e5342936ec5a6335ecbae211fb32413a19acf443adf4bcad8da04bf6f10100c2702810423b34320d52ae6b68c42f0ac9fcc9ffee86dd068a359d07f5459c00981fc0fe9476ff0e327b017a995fdf00974bd42ef7ef284ff2151185e632eeeaf70a3f536b7c59e5ac3334ecd6d6bda8f0a08ca1f6faa0ebe235a83f217055730e10ba56dbffcdc834852ab837c4dbccb39fa47e5600770710141f4740541dc25933570c115105fc0f4a58496b670a9f335bdd9b977fc92c625439658b1ee821ba3f3fc45b87e7e6d0f1cc3b39ffa2c884a74a2bbdffc8cdc502f3acf15f52777baf2bad3ee00fd4e41bc1afb2ca7aa24c7dc383a4d07c6c67681e9aad91f087d21810cefd41144bd6750dd66ba5c0403a4dffdd2c5dc8f100d70f33d3da276bf8db7ffb67f82b6c32639136d867afb378707dd3b8f120804f08561a7a2d5df2ada8bc3792fe17d1c288e16aba6f977db82fcec43f7e502822833127f1ff1da8436d468f259f43a6f69e97246c5df4cbe514d78febb5ec3faad368a42c6e04fa98d026b5b7138662b775c1fedf65989e7bae50cad4a3b28be00567a85282cee53199f4fee1d9f555c2a1f2e8ccc5faa0dfa536b7b518dea63f03e3cfc39dd70683701d9fd57e6ffbb8b7c6ffd3cf2ff2bb551a3615479c4f0e4fb7241579e9f06919f3806c3a9acdafddda329c3f917783477129b103a0cacf76e53de3fb2f424b36e79730258fc85d916115be6c5e65cb9064835de8b8ab6cf429f9fa51398eaa82a4651f05d9b88db54bd12808249950978e45e7d9f8d0c4a9e556e29ef8cbcf7cdf474b7699013b0b8a3e8aaac9bd4d71f69f74f257cf0116ee9f30532a2972db7f3f1cc837233876454e347474c17b5b2018fc7b273c76170d4d7da8db77c9c6187f2465559dd80c735e7a450bd34cab8bd178d47a1a5f335d9d502ec0a60e71c2972cef466dcf7791f194e9ff162eb9459f00a60b1797b5651e657ab31cd575b973d6f332697ee07327e9b27b12df98cbb14c890b15cb1f70326db55ed8088e515f7d4a2101eeb654890b4154b0d5aee44a1a6c9597ac9a0741866e373e77585f71845479e2b94663140c6f25c1f7e102fe4154bb2f1ae01c731ea0af9dbdc7c40d61bbb15178af14eba20d06f5a635c1ab8cb6c02342295ddb686ba7d881278d65da477f59d5684ff7b44e044e4418135996f94bf887ce8c353b9d1b7f5e27417d8a61ee3466d572f5506a676cc13065f0a59161d13024ee4c2326fafa3926dbd9f255339f46d35d36545cc064af0df623ed4916eb7a0d9ad77486176cea22e6a5d03da386694d085290b6f96d0def44c42bdd5fd0c7e1511c86d90ac48b16b167db243d13f33f2bc6f9f58ef9135e051b4d37fd5759cc6da5b276a1d8737dbfe9801c2029ce8ee0d69ac883c6f52fabe2e121dc56e1176e5e02390519d34ffce47987abe8cae0107f1908d79e021f9e42ec0b457dd6d565d5c3f4edee17cf2a9d7eb1693d765e0c41f0751c369df2440c6467b24b9928d5b8663ef447b96ae5afad6db5253c77b3bab2a6491ab4a14dd7556e02cbd6d3c39697abe8f9e2ee701bf8dfb9bf44f3eef0031a7dc249b8c4a1ddea5a8ac363d7fcde4433af20b807f6d0aeaeed8ba1bbd5fe2aa0fbf02a569e68c4d064cf57ba4a5a7a761988542081cb597d339b78375ea018f3286dca043349644f2c7b147708c32daa782a42480d69b0a1c089459d5110d98ec0e6199393ec2c07b0096db4999976004dfed0a36e40f576d90f24c6abeca7096bcfe6932135d7a476ac95edc1fc5cb2eafd0fa5a03e0b1b68cbcf339497245b32efbf87eb95187b2719134f07ded4f8ab810099bd15d783af9460dc74d384500f9e7b0c3528f5f5f2a45244365fbbcccd6e81ddc3890f199359d99908aa995afaf481a36808595e2d45bc07a969e3fd8bb30fff985969cb74ccb5dbd2d393214579cb3744ffb0f13dd778f042edd92c51fc45c5f7deeb5049c18d525b9aeed155fe78306a454f3bca389cabdf900408f3e390afbec982fcb711f5f362092dacf31d0261788f8d06bced159180eb93a6a4a5120fbd95c31c87b037c9f24d2e7f22b044f43bb4de93a2bd357173fb2525c3d4ba7384a201ac21127bc97ef4f7d5b91b122081205787c5aaebfbfd37a97d9c65b6723dd68c73b8352520403a06f2e7e9370a15f8e7d452005c752f5f9c0231e481bdd78b29a3eb42fb7f94c4d405cc12e7746db0d09f0e894f5e1b1aa7ffa4679acf7b448248fab0acee83d60fd8b464460ead6d3d1ca611ab2c5055e33c80f5200f8535c2efa3b9fadc568e132b64bc2155a23537c4da0622a394a70fd6087a2d297b8b7a257055b88a9a0d81bb0e77cad620e86c6db3a755a63bc5eeed660c70d001ecd91b783342ded8ee18ddf6ee3789b93a865ce0f007ddac8ac956be4bfa5d08710fb204345103eee77368356013df65da774e1ca5a49dc200aea4a3654ec69407e3e229fff4c8beccc670f2fcd653778796dd69e23e82c1d6d28974394f6732c553b660224f1a50d59ce4810285f5e52c0d5a0c0bfb6a4cdcd66e5107e5fcce77c963efb0aa2d0c97eba959c29f898b069a8faa51d1e507198ac84a92019aec3b580f5e1a1478be177ce01f95c40ff1953a0afdc0c7c7a9da896bb8a4ba80537664214d04fdc885870340e7a2544292dc8b6ef77cda40d803c2c45feade6248497bacba75ad39f8f0d3f8b3df502f69fdca77ce343eecbfcac3d53bd47397104ce335bc0fe294024cc7e62f1a9f14e92a41e362dc3e9bba5d7afced2a9a51a7b0ce63305278aec2e777d9e4bb8fbbe0e684de3e5d74ce6894ff674a3d679928d529e52c99814a8f8f820adf992be1273f8c6cb5b66d9c375f6161bea007fd234350de74f08dc26ec11a396ff64114f6d2e03c8cfd189dab1d5b8e7af5f1386682a1bf0b45da3f20080bcfa519f2f06b42fc7c6894187936ce5c7b03e6500eb531df9e2c43446478dfb9e64bfd5bd4f6feacb4b818acb179f4df7ae4bce5ccb26d21a9b645ff7de595403e46bd283f80765f3a7d4e4e19c2d3c445c52971aa7008fb8ccb6768a01beef61a0f6064bb79943b59671feedb374461108fcfa159b35b71bf85c8548dab5e1ba5b80474ae2ba6bdba3cb57b5096fd5066724901d6ecedc063cb6961569348e0b285c72f3f43767de99467a8ca52e9da5672649cfb2065e628c569c0c7e11e4d2a392f611f83e6ece1c90afeac4576b6fd2df9777cfb3a94ebd0fd82fc394454c42c31421e80f10dcbd6e25e404a3e7021153845a4b7572aa4286e7113aaa696c7efc039da3807efcc2c6985bc5a0dcd4671a7cb3c6e35d8a53491f5071244a31ef404ca375bba72b2ebb062321417b6a00d01fc7cf7c1a9f87de266d5ebdfd468c555d8894e213d0472bca0d9b0e5f2b9ca4e7692ee7ab68ff8576622d00f223fb14756ec065099b0e836279a56ec5df45a600b0afe233f82c7094492d257605df33e978264814ad02193e05a2abdb4ae858594ff9b2fcadaa5ec95a2fe802fae7b34f4efe7b2bb365e5038e6e6ad23c4f2d8aef00ff268d947f2217b52f3870d6d6e8dd84fb622faf0d54ec953767c80467dd3f3c7a9c7c9c0fe53184e307d81392c298ef6fd9ffe2b583582a0b4ff0933603fa8184b374fe8dcb91d2b72dd07aba056cf5b598dccd10120074b55e85db6b62e6df969103173c950f558f0cea9e0215bf07d7a41f20eb5f43dab83e4ba1bd8833f5bdb805e0bf515aafd6fbde138d5d98bcaa9e28b8238594b732e00f904b037e2b95a03a08aa417e22c99e3dce90d71474f591c709c997e70a6e1c2fde4ecf772a91f93a48926c2fcb616edc48bb585619e87012264dd0145aaa298834bf9c12c0d25354ec99e0f6d3a108540e521fde412e98c978c9d6c8cbcc99f30f9f1d19d93d79a1506e634e4e130e1e736351a8a98918279f18bba88998b809370fd488176a26c9cdcb23ca23f66fc9ea025c590db63e986c962d55017ae17c41c4fd959f2b2e3fff153a2c80d65f067065ebbe01637e1785d9095e9fd158b8d03b42a4bbcfa76300912267ba322182a3dce9b228b49eb71a8cce5f35adffe566db178541fad30174d95cd32ff87d605c08d791bf5cf53afb934a8117b6dc3c3ca3769b8211b51274fd0bc258b5bf32c7f51f5cbf727f58eeb75f5aa37f4946c1744fc1f01bc14dfee887f510b85c085c34044e59853e073a598933a55b739cd6469ffd965a87ebea41d70fd711f11ba1f9f81fff98645609ce091d307e2bfb57bec6ef9cd7cecd11bf221160ed965f7276fec3b281fbe1284456125f05988e75a31ff230958ddc7ccdcfb9fb5c982eb09c63abe63b50b88dce6c92719b0ff75b41a6276ff72efded77c9a721b070609afac2e0ca632afc209060243dd980800304ce5064a3c3479905c1080961901a04da4f5eab93bff299ffc0a686311e1ec6787858e9b080b14679572e072d4d21fbe9229509d1eb05e4a7d27840fad791c3ddf4c4c0a7b9f005ae0db6531f1719cdf801c5f52c96d0173dfc5e1f023f9b88f2b7fbaedc4ebc15bff2fc7f22d1d535c79f53c303ff30f44141c3396b97572d6fb8f82bcfffd12e394b87854df7abd875ffd9ebff5be49c18069d08c6fee3863f804b0808860b80a005c3c5da39fc20b2096bd5c320939fb94b62f7a3091a3644c35c7b84dd52cb2f14d944247c0663528ee7505130f07cda7fb29826ba69d37a6b7f6e601372d7d83177fbd6ad57920f26b0a3915f4d6c1e3be4c2886cbac314c0f0fb35e1700685ad3579afc583529202823db81321d54f4c3da2297fca4f7fff754e28d71f9f0ce29d1bbf62a608a089db24c4d68eab90a9157b9d2e8db23b4d0a37c17f0d08a2a7cb0ee24e407a5259614077fe13a70f9a0ee0df0604b18d1cd6c0b7e1140895fc8ae685155687a53961ddff2617147b43daaf5c606b7ed5a5168e0affbd19c94f26c2f5fd8d997c4ccccf678abc746b60c7a20e56692b8cda4e5860fa7f2544f46ff7c73997fc0d92ddb5aafb7f7c2834e734ab7d038198fc5c01c2fbcf216bf2b346ad9b34af04fc2829e5479ffe37e364fd959a47656cb5fe04c39e4012d2bd9f3f0249a9de83ca5644e2b747fc282be55f7dc4efc2b0f4abb35a5776ec83089dbe04d1d5fa05c3a9ac66371d2041432578e4efbc8404516624d620d10e5c2d1fbfd8c1f9f7e56c96f4a60d97271403d22fd593e7ed580abd79664174f0302e54a753be86fcc032c8f34074e59536a5c6d297d582bbe91de43fbff7df5e15085604d3d27fdeafd7f9c0ae7e39a333c16eba874ee8627e01581804b0ec23f8a60bf1d2bfff42f88bef8fa16a646cec45a569646b6be6fa07c6735b399abb08b0b1a9d8697142dd39eca13abc1caad6ca2eb7a4e54555b4352cd5b5f85d399d39ecddc5548cbdf8dca49c957534d44dfe446973c16405918b39fe2062ec9ebc505e63532e5e533313536e6e3e0e23633e731e1e4e734e28948bdf98cf88879f879d1f6a62c26d62c6cfcf6b6e626a62cc69ca6d6ec20be5e285f2f3995f6c312e99b9db0970b0ff39eb67569b9ec167ddeb514461cec230cbf3e4b1512a8c96ad18cbb04e9955ac4721c3716841a4ddd7adb1f4cc4fd3a55e2cedbea276d84ee2c96a90f79ea0b9fc1ac7fadbd855e2bc8477860f4b6c3d6929085d3f482e915a44bfb2931e1e8455ae7780b8755668222eff0942f377de5631c66db4ba2f4add79148ff0cc39d863240f229c1fa3b29bda2f5e877fda47cac26b21a13391c6d5f9cc4579eca595d1db5f119a48dca41a9f239f53e6a0f6477e851e72a35b04a2fe5768fe5f2234d545a146de76ce72923afca2e25e3c1696665ea61c729e2adc8e7676e2dcced6ee1a50157b2b730f6f39059bff5f094d1e4e5e7e76336e7373131e7e0e4e4e4e332887113f2737b799199f31b7198f318789312f948bd39c8793dd98db989f87d78c97d388cf84838f9ddfd8dc8483fb42f347f91fa1c9c7c5cdf917ed0f510ea3bc873e52dfe38c76a2dd1df9b07990578999d85eddd7f1395089a6355be2907828f796257e6be55bea1c7ab519e7f0464ea691e1e5a800b18d8e5823db99389842efaf4bd8332cef7c40c4b7f37892615c24ebf981ac897239705abf4e6f6dc53dd7e8e9fb59518d08a92104feb222909016dc224336a494c68c9955824af72d70a01a111980c06d5313c611f9e8476515cf181cd61a39d2cf980574106e771eeb72bcae47afc34507b922f912c15b2502d84ef336e652cd6cad3108f7ab047325a6a4bf46e5699ca5f30a0686a8bc329dfe1a62e17d188320da09a10312b20a4edbe81d043af183f3be192712a31db04e10032518bd46641b9a52b4baa33426a118ac9c1aabbbed00a6ff4d69454206d1e0345d3a994207b6fac852456920a1e21e9108c6c613e1a6beafde19f6578916c21b15801293a2aa7c398b075c6148ce9eb7ba378354781aa6b3ced2831e117ca1ea9d903cb88c613ca3d0f092503613681c226a395a3e56d85b2ffcba54e1caed1792d998fa4009f4d84bbfa2da985ae2619b5a4c4a23c100c1767c202f95d9b9fccac0712734df0c79ea6af5cd774f7a0780124322ead587d632e92f493a0d071997d3ae160d7e02f2d4b52b7475c4d4aab544b3f49e3a44e379a950b40110b448f202755f045f78f32ea074ef274eefdfdae203123663ab65db8ddc4c18a32d50aba22912bab9b70c0008e403c2dd649d89505c9ec6295e8e651abb7a5d36c069ccea36d47ce9cbd8eae88c9c470c954f3169801e006178a418481180239772185347e1f8acb636932d10f83edda277ef35716fa0b4b10c3fa274788eb1932d0e249ce50c60d4d4af2c2d1bbc3fc580e4a47b1d2ca101c22615dd5341b0e0b6da4d7e23013daba2c3d6e67de0fd5583238f76c82b948a4395ea9903999d92d39b80028d93b9a1dc2e7ae201fd6f1f474a25e4a4170aad01989ea0b79774f1ecf74535a6d0b1f8dd8f8d14492d00770d9159cff46b75a89b29945dbabc7e31c83754067029b40ef0f07d781925c6b8f49e9d3c0aa5e82d8221102a68ddd0ab8d2c89d765f59f8184d4378cf667eb03fd11bcaf670cb609c8c747044863a11d8e68d1b7c4000983c55952ec90db4f0291f5dfd3a39e2451475e59044ad46ee607930ddc764d7e7779f314034a1e87e1f004e07fbe85c62b93d33813280b5e180a8ada5f0b084e803c7ff805d63b7e7ebcc355fb9bf356e911aae5651b800fb9f890888c65f5530bef4dc849becf5e4effa32e206185390e315339794bcda3e1936bee4bf1af4c0f9f849fa57f4eafc46daa1e88551228ea0c51554236edac06e40f8b345565c6b15955b0e8e9579a8a309f34882fa079575e3c7e931ed5532a8faf743c93e7b0e85d949571964e8bc4844fcd41cc4d3ff28ac3c3745fc08a370be08fa32462f219d4faa39568522ae544f6046b7d67c05cd1298e0f53961ad8e79d7328d04f0950b7ec6e07d67f3699558b1a7520ceb4b8a7be562b3e4457541e089e1ecfcedbb8eb2576f8e137affb6e56941cb8f200092f0febf02f77d8157c4350ced57f3c30f4b4f12b3110ea9266f91a8ce3785dfca8dbd5552a52e864bbde03088ebaf57fcdc0ba34fc063293e32f2e7adf6b4b5d1248f878b3c2f43db112f36035c57b7598a419b389820958bf585ef69b2e8df3ed420c2521636cd81298ebd5408dfcfc3c4d3c3182541eb55b6d287f37a4fcdab21f50973025e8905697ba75106d218e6d4ebbb5437679c509b0ad5e85ab4539c84c880c6305f617efde46bf710d80906e4f93ed1aafd6ef7fba5194db2d35fedc3da20648e8e3b9b756bea43d635b3d9ae6f6f57b95f956832a201fd00b86455b6ed2dae41c6cdfe2b6e639b6c99c04126e22db146906033d73eb0b8fe6bc7994d89fa568f09ea5f7efd6f0c405870be65de6c57a27fa88e77ac33bc0c7628b18da3ab596939b89eae0764525486309bb1480703e687c4c971c3f6484372ce37038cb4abcdf800a40a0d7f0bd6a83e8fa7e4b39506841bdbcd971d40e054a98ad5e63467fd1d28e70cf83e88d71332c2b9d9403fce3efd68e63cb63d76fafdbd6485fd977d9e7fe31d03406ee2a5bc2e597f7cca8566a2362f1ee87416b1000fbe21ed9066a6dd71d6541bd2b2ac306f7d54e37868012f58492a2929759634e7cf29e3e8c7c914475dca80044b10c0e6d5b125b050f19baaf5d33961fcb476634000299ed446bc555c45bf0e6d7daa2721d110accfb4680fc8c9c3e646e992cd26c109574abb86a307bc5190b8090a85031bc827ef9e5bde7885f1f86e11ae82105cc02df7fd6f6ebb3cd6c5bc7a69278b1e48f8e5733f0f20087b0320b15c11b45e5bacb6271cc5fd1fd8530793881f11133ab4a067d89d50926dda4a52ad9d50a2302807d2549624d7b93eed64bae7ddbd78616336942512400c4f0d7017f6a558609ba44a8df03adc91a47277109e0fbef576979d7f1c2cd563f1c1e5eb4b6fbc8397613e04f356efce8ee7953cd81ac44c2fca913db448548403ed9cf7d4213b286d2f18487f587b49a38c86cd90391fdcfd9853e7ef3d5d0cb6f67bf4e5f53f48db60904e647b8efc0c9a3474987f52eef990562931a0b7d0450223e5ed3f2f9b2f74d94944b2f92bbabe56764d9588088c4529eba7f47020f2e91237bc65ec4cdd1065c205f18a2e125d5d2f191d22bcbb1ec2563843d1aab0105007121f3ee51a521bde050e01046bc0e49bae2ab3e2700e261a8a6e246b159cf3385acdb59e60e894775dbd800044b1bb2fe8e98eedc8bad18e4211b9e38d22cec7a2006f649b1e461947642742f916ac4d7dd607ed9dd5b403446afb4adf0cba6b8bf33fe2a4356771b01c14614601f89d4d0ed4ccb3d40705c66962d475def4bdc0121bc0fe0820e57ed0c3225b2fd827159fdc830e4ab8168502cab3d8b5a5ac3b76ad2f5c9cb53fbdab45a2d4042ab6b4370ee2b6ae11add1b6faf51099af63a109000d1ace7df34fb159f7d91e7eac2ce489acff0257f100fd88f7d628b1fa0de75f9f1d60fe5669d6597fbb52981821451ccefc60733d65887d874415eb1e39a92ea7140076d2f6fa697b5895bd6baddecc7776e8a7984ac2000fe880f84e591cd7d42d02194627d7d9fb0830fb74f00089fdbddef65be0daa0bbf1cd9dd8c88f5e09e9fc62d873f80c70e0842771f0b224a92ab81472408df0064da11e969d7187ff508d5e4b6efaef9a4bca5531cac209de1054fc8a6953702302f768abe28c62718c8817334ce99657b5f93be3431e4297a3290a5c9f7eff6c300f3237ecff7889c70f4cbeba3f10cbb6c5716c432ba0b792c9ae0f71f0404c18f09053152c157071f3b789edf1b10d9035043f4f27596774ba830fbbcd1ded0ba6e7c72a2759afae721ff9c30028bde74006f17107ce9ca5bb21a78d3bbfc03ec177393c0ca0d81bb068140b0fef6ef3ffefcf6ff1f9675ba06889454a7c74ed5c47ba5a050ab110f59a3b2c35d433169115e3fcce12aebdd1bf6127fd8a5dcc37747f283b3204a207efbd50d08da2de290175335784fe8dc5024dc737666a70bcae8eaa93eb560f6e4c475105ed55a0e32921ca819efbbf9e6adb6a8f5c2b695bdb2521c946cee63d1e461ce69472c9a3ea976f475020cc8ad3def72f4c82dc56f9dcbcc94cd84a4b58a48737eb6c2d1615081ec6de9141bfc56aafd0a860c291638d515edbec4afd83759f6ca44ad5c0c734945525a552b6ee58d586d8bded04b2cb5b0c0b6abffce2543f17493379dfdda470542c6d17bdeca263750da5a0956f55288483e77baeb8e3d7ea1c464b1861e4076f28701ddee2340d06e91fe73e1e655bef09d69722104c30aedc226060bd50793878effdc24ad196bf5f0b94640472aefdba2c703dd550f98160123466142fb6021cb2733bb96c36453e4fd475bfc1eca9fbf072eb153a4f28f7ddb2ea1d80861ea2f2dff96b18468fa27642cc1eaf70e2b230946aa2402ac8c25e0345cf19060e72a4606c277d4625299317d03d7392120a63af0214c727518fe36aabe550df3533ec698d859a0dffd7ea69586b93842a9315bc7e1e0f3d0c76ff40b00aefbf0fc93cd5a045391ebae419e611a3746aa91100098427b7d1f5bea8a7b7f0721eec33bb7b72bd1e918cc7fe5fdff696d78d8dfc5b89db336746687bae74444e1f27a5df0abce7b745cd438e48fd37ffffeb07277feea8c2a58f7ffbf6bfffc165c713b9f8eed703e1d19469e2e3c0e0cf9c00abf7926dfeb5f49d039fce7f6c84d7e01ffe28ff65542e1fcf5eca3207fc3341069be0224ac6968c8aee5e974f1aa78b4148c884955117639ffd4f38f395f59966f0a811c107dfe0d21a1866859f28d8fddaff8c62d37038efb2c792c631e046f70b33f62788d46f5f0dcd1742a3f4dfcbf646183e4b177f4d3fcc54b1e87b78903bee15fd00403a479344e2ca78d543a1e2dc3d36d95121a35cde76187e71aa108b8e1e7df9f0806043723535c68bd05c2b1ecad98138e0c69d73d815f9cc0050b43d060d0893b2f18cafaa550d91f49c3bee039e682eb87002483e9f5abc96124451c5a4c57e17ea7d718245cd4d13e7f3caae705e717f017872a01b3ad9b039d349191fd544ee4e942655ab9a15f477af3f9c3af615dccda82c9bf4dbf32bfaaaad2637dd21e4a892fb79ed77896b5b38657cbfdcafe63b6594bb20f9adb377aa4f63d3033ffda1715f885f375d5fe4f7bffc2a18c100a6aed4ec3900fe79c91e01e1004b38d6339423002209d10c281decbbc5c1ffe7114a3aa26979a059b37fdf01eb0bebf681e87f4c0927765bacc7ddc66f8be8424a9c212af255cd7ccc6ccb7bd7784a77a9c644e6834e30a465d53a4042bd825dc91943e8c7a455614d23c12e6fd7c3262f22b70da3e72f72225b14cb6beb969b5aa29e4cb9876f00b9f3f823ce53f9c7f00f93284ffbc35a6d3e952ec67de18f9d5c89deb1dc609ff5efeb9b00b24fb82f217e0dfd94da713e97e38fcd271f4f0e998426d428775cd5f79998b8effd1c561b2f11ef5ea49e376d169f97297917f9bd0bbe2335f15e957ee05cb3efa65fd87f3eccdafacefafab3f58fc714e32f42592d1b1189a4558f60f8ce703a765d7058ff1dce90a46ea433e651cc19d03ea9dd7cf7ffe6a6f5eab9a9c51dfb5fa9adc188bffebfb615a80cfce3b239e8738d733ce78b2b18aac6c9076a97f227f750a6ac8975b1745ff7779499bed2c14cf5a4ba5b9f5acedbf9bef19f08ddfa7b2fd48feda4fa7b720c3c563760ef3b6683699c59aa6d9cd0d4428eb0e3d9f5d4258a5eef17dd24440df7f6f5124e335f4951eafe3e82e21e6b8b49c105d3e9e9651da5de58bc1f5634a5fd0febba07d75c9f082a7dd8bda87bf24ff7f5d7fc194efffd55fffd55f17baeea478059f5434dde6126c4599f06ccea47c223c74e6ab22fe99faeb97f913e325c97fb8fe5afa8fd15f10ccb8ba01efff05ff201fe54c7e7466fec289bc0919fbff187c7fd5e8caf471eecfb6af8e9feaed2807adddecbf5c7f1927acbf4cb22b4559d7e710bc6aa8fba023b5175fb218777b3f4a3ecdca3ecc5a65636ce5753d8ac8f550e9814e64ee10251c220af3eafbf7dc48d1bdde698ebafd8afefa37f80ffeab1ffeab1ffe3fad1f30e7820eae0735633d3e166e1c8c61cf21be742300b658fdf1054bfeff2a7f52f3be1dfb0fd70f0bff21faa131f300417390be55f7efffa7080aa8324dc7f83fdd2b36e12d1fc83eb7fedf302916d4e8ba5c5f07f58f6fae8bee721f15824a42f597eb874b56970ab70d946bec38e99f3e41fa9ad25d3213de4eb0070991c077e526b4baa369f5ece3b7fbf4ef2e7d5e4ab5f8b8d49488ebbbe6b0beb6ea84e426b1e9bdd97ed17664f007881c01413857df0651526ed720d2964e9b3d81c0217f495aa04d14ce9dd9a83b261fd43af882b7f2959a037a9577102d0ef3fc1021bd135c45a351fffd96538c1b8ad20b98058f08c24238df18ac39d1f30812455fb54b7afb2b7be2076f0c43e62088c0c141e0fe78f20c4cb02caa0bea9cbffa12b9a0cc86fc519d99e9a869174327d87f9e3e692bcff68571ff4ba29dda4fe38e925463cbb2f2e874b3e00af16d6cb906424cbde6a934ef7ebdce666f35a20b5f8e668377db3cf2e4e071d92365af84495d5351e84bc261ae67b263821915b62b1f82cc9f0b1fd6de64fc6cd1b42790dce88c1925975499291c1379cffb0e9359c58e506f665545ccd0b61dcd8454d59beb26483bf2c8e8594f1ef3c2a5d4e16d2df93aaa63a7ab64957f913bd8d3fa7cf2a68a259fff99f136ab5e470081bf2f83ad8a08e903877eb1cb3258279a8feb0c4d8ceb2868efdf9ae8de6cf187353ffdad6034c769c6fc779fd9e1edcd95e1a4d9691620140f07a2681f844a88c44ef030f96a60c8282ad1b5ccc557690c2c8444bd641c19c2213025a7638bf3186dbe4bb3cba6d7ede03649c130f58aa29583bd5f866f602130f1cb3bc29975e8cc2b1f4fda26faaca07d51d5062412bee14bdf9de377daa335c44fdf57e27020345d9768a2ad41a38f7153e89b54c08494edba4e3c1fc22aad9023c24e379611ea91120f2c1a4444bedcd383cb8afa81ca5aea70e8db17de99a95399478f3e683fb99dd104bfaf1946619bbb5b42501f4476c3e27ffe544bd898c81a9f3f71ea9f7b0bc945f98a36c8fed11c91b7ef2fa84c5ad67dd2f1acdf65f8434a7d3f1d476e98f39a28e09ddb8def23645c236a3821a7fc68491b4cfc566401c80c6daaee294cebd9bc6ccff49116a17d35725bfe365059f294d354ce4fbd925bf829dc3db58cb4e27ebb0c00ea9be40a077f0eedd697baefa12d70893182bddf4601ef5ae61ab7863fa151466685a9e4bcdc307ba6f626c0a964395c3a375b57465fcb69d978f1a7b1926b3a00d6a553f1c3eec917579dab965f3bd0bce010ae172f06a0b019ca0b83f96e2392e92ea36839710b44477d2105323791f2528816f21d82753254646dad46a03d950a400663dc0839a2c16bcfcdcdf035f61187e7de6ed1da402e8a7b23656a30ebd418a43149533f7f67f8892c03101d73226b318e0ff3c57ea5a3701901bbf5d9e5ce4000bd877588e94646516cf557e2ceb9e366c3f6c3bd66a09095a327a8ef90cdec90e2719983fdd713a30c9103a07207aa2e81c115cc6b1e5df16d87dadf77c9c3c0098826775da99f3e5c7c5cdbbc2b39253cd4605d27220764813ab62e0c27b2176409622d8bce2abe877ec6ea03a0828b299f4febf1326ccc998b73647439e6c548c801d1cd18d924a72fdc37dac3ab032a78e6afe4d8ca2901951b647cb80ad1c323f122cfcc777233916252aab581e671cad065adaefd175919a8ea164d71b409377b4380ca17a28a773ea8f8d85c0436fafe86e3f0eaba0d464070e9a1f5dad30ce2f9313673aa450b22e13ad6f795002c65712f42c0fc1db88d5cd4819429da8790fc312b00ca7e5875bebc9deb51ba6ffb788243dd1a12c6bd0020b3b8e06dad5576b06d5961c3e9b54834cbf03b6d5e40edb989adef064988c5d24994d6f2d8724580efda1e50d9d7216cf05135ab391ab389f306d961e3bc20c93a90f95c5407c9756f251191b9c55c19f2c46837cbfd1600b58c41caf5e1da6ccf56b4f0bec8474defd7195a9140f34bc6324ca2a72584c2f34618079ae2e603c17297816e1ddd46e33856bd3b880459c6ae70c911631cc5e34066b722d60281702fe693fa684d0d09f1fd37757d1b4073a96eec32e76f068e4a0e655ded847118872af5f780e6563b76624736c2afe5ee6f7df72408b9522e866209c41f83df9338cb7adcf12bfd5ac624d4f87d8819dd11c8ccced5c13b21e956b54aaadd7f31d729f6112d5f11b0fef558316b4a2d4645b7e29eba0d0a6da3cc32d903d1db07fe1315ea010d7d92e412cd3da4930f7c99cc805ca39dcf05398d9786daf8de20b4b758bb0a73ee2a03907ee97b9bbe1a6e090aa551965758bd11be3cc524819ea513531e8feaeb7d57849ba59866c9e988769c4f040a602b3e4b9f24504614b7dfdfa0a376408cdb3dda019af365d6f73d5f6679338731f26254a070e82ee9273c2073fe12333abf411a390f0277898113219390ade9269039e493b57cab2a7ac54f2e12ea46b450308eb29a2675965eef91efecddd619749b516d2bae409e9b57360080321f4f7aff52d9a40fe24eeff498c4f626e99c039bee597a88c1fbee8fa8cc054e81d3784736a6deb92bf76501eb9e4ba2a45140bdbcdf0191fa4ba2ffe3f96e2aa0f269f185322d7388a6cf22deca7220d51338aaa43d1540fe639cfab27c67d2ce526b775911e61ba11c20069a93e993e5100aa4bb40bae9468465ea23fc050d16dbced2976e4299ef8e53bdbed91b5efb12e2649512f10288ae7ff3315122b34dec404854a2fcd4212bfcf84e0650d9fa4efd39f621cb80369709a5d523ead3391fe30a407f78dd8a6dab4b51e42ec31ec782eab994c4a5bd07bc7ffb02362c693b1612697706623fb86e9e2c2b590089af81eeac2637d8b6c5c558cd9e8efabd297a8fde08a45cbff3b896f7dadac7d6addabc05eee9233d4ba463a0b2d27a0f474e2948b2f095a9d778cdada23535b85540ff71eeb262ba5663234b41f6124b2d5bb31bda3480e66c3bd757735efa8bd4663d72a41c5e6b09430cc204b2ab4c3056555af2d9f55ba1b8f7d16cd9b6fab3d180fdb587be3a9cfe699fd1816be1abc15d327a87f97e60fd6483fa85d39c8b912752c2c8f719be3a4d077001951d016aa9cde53685550c8af295913df7e2a7e12b01f9b33dddc1f1ad676a8f4b6d426a86333aa0d5681000f661bc3e912436a298a36da7a8fe3216f7d6551a3900ea9bcc7871746524a223dd55d282a4cbc4e424690a68be8ac346941d12649c91c49433cc3e49f880901f03488673703275b5c3adb992546e9ef0ce40dcc145c30c700f613a78cc06a60e78a4910630215a84287e52c705bd5796505f9964dfb9727228d514ed330c5d9b48203b2a01ff9b845d3c2325c2e155b987dbc309b29e8480fd35c5fe11f2299f8d541c858d018f440af14bf20080fee1be87fbc5a7e34dfc5b39a81e573a94b870b913009234548b6e0e4cbca6807c94fc7679243394e77b28d04c7c6c2590a74be20b71c581c781106597da335321607d0b5e63f7cd46237c47ed2c516942d2315ff24a02206d1bda51896f2f24ba68bdb6717ede0b95935b8e03cef744bbe9c4a86f36ecec0f4d39a4481fc357603600fa2143bb6adee4983b2b2d75d030d65f6f2eea630470d2f5288be2d7c616c643148d798367d0199fbd430a9c94a2b9a12554156fb445238a236ab7d488a4d1af0e9da5bf9dc6ea5c2b5dfb40d09bda7ce810f9f44a9233a0bf0ec7058d9f4d2f19dd47d95513dfa4e67e74371368b5519b2df0acba38c919d3f96ea3b3e6e0bd9b45564052397718eae5cb1b2723713cfca5b1188248645cf44074239e57fe34f5d68ac20026fccc111deb75da3915a05580988370137770d8b5d0b4409ba47b8bdc3b2e434fced2efd347d2340abab9625a29bd55b1e12019197a0fd01f13dcb161784d3afa043a2a461872276621b119b08ff6037382f6b8a7254f0b2a6b4d3ef1368a6c89499ca53747b66eba9006cf2f38babd56387e899e73f326d0eae33b9e55fe4d06d9d2e8d01596cc705d5f876554a0b21263af519294e598f23d1ea75adcdb247dbdf151e0f991a61186c491de7e4f728e2297f84630321de900ac3a640d9f87e1e80a690208943aa67d869b5d10c76ef8fd777f4312671088a96eb37b6ddb71c1b33b4c7f362f87291f94cf8893d7c88cc3c8dc046a6cc2c1c5c5c5c563cccdc3610ae53482355e48385c3b5f8701da4fcaceb0618642d382db83c6ad3ec4579a5c7fcf67e7eeab26f87dc780207433df20d245f26ad300654ce010e6256bc47ddff296b652e234f94b463aa63bd278c53f3fbe7dd824cc2f00cabb649ecab0cdad147599cf188872607e3fb95fc095f1776cd7676c4150f8bedf372e63955870dd1bb9098f734fe02d761309f648f49d1e98d8aef06f03908218587483bf5764df68823b40740bdb3465568ece86fc8684588368daaaa5e6f7879de37fbfd73ed9cd708fa0767ab73ce1c0bb2dd385b2852bc562c645cc051d6b1de4708535309cf7c066301390c6580ef77e10c8efdde8d1bd4b432aac247fce777c40102c2909e35bc3701ec36848769e7379f30bcabde6216618eb8706e3fe22b0d617c67818cc020ba708160c1002ace458588e4a58cc8c279ddedefa687177c8dc7ddd8878d4f7fb074d77e82f38eaf02ee8e8bbf0f88bbeffbf79fc5f7e35edcb070423f9f4437edfe3f6d1e807da22fcde6d940718d737c6a2cc3ecbf0fcb49f2543d6ec062b0316e4005efeef38a712ff1fc639e546b44cfba266f9f355435ee6cf420f12bd60ecf3df8d730a093c977a0e7229f2bb98c08acff6b57f32ace8efae03788c8020ea11961a7844db5b69d7cf92145c8aab985a677822ccbfbcdb7885170c97e3b97811cbc5dcd6cfe5629f0b12ec284eb002e377460445871f33be8bb123477dea1bbf641b5a9e56f13edcaad1dbaf48975e5e35d711c678a40b8e3fefba374bead25ac1e6e14acc7a9530b87396bba34403d38f01e919f4d2f344a7efbc12bc0aa67356d2f78adb9e9565c15743fbb41aab736f4d999493b114423a9cd943493ddeab19d7c8a1ed5b512095571e226bf9a3b9132e9138385608c85a9756a4b1bbcfde2fe6c21b6da69e4d1269414a21b983be1062fb91c277311777a55efac54318a9eafb09e188532e74522610481065067b75b0b642e0ca1f0d15fde34e67afe7c262e16e518be4348332b405df6ff0d35cf3c980c1e2837f61bc60e05611e9de24174b896888814c896d4791fd0c100f58f0421032db3a55616b88a236ac5be519576201fca583702cbe0d71b4a883a9356fcbd4c272629911017f7f1dc26b488536efb5fcf2f061a5393a93e44967e0bcc69442f32df63313ff142947b9c85dcf9504566aa0b5e3ae25f2e02b46a22705b12e4a451e7acc79a52f818af9830f7ed475d7b40af9ba67ef0e5d32f7704be900cebb2d128602694defe6ed5e2d1af7c6fa36debf4106a0cd3b71bfd9a3a4115fc2176ff4ff5ac1e0f588151e686d771c2f59bf0cb57335fd1cbbf78ab0cd1d65b815a896ab6feee86cb37c0f41707bd9f098e8333932c10da0d97c98609a015795c2166f0a9dd51667629ef8f50f80c5f54d6f6bd0d5cad8b1ebc9c116c481dcf6661c27a001d9c542af7da3178de44d7e3cf1da7ec61acae20014f7a5280558d82cdd969f22d7fc22c7ee45a5d456a57e964e481dea4d78c88811ee7f15e70667bd30d6e378209b44b9d34d80ce0041e5f8fbe28b4c7473c69d8c32607d4ea8447b2a46681d4d46e14916020b98bded1780e75b89ec6d40deb36648f7130dd24659bb67790a00fe00ee0a3c3ca6892e3c153a96070e91f6f4c4242f017f5f098aaed4b2ff43a4ca99814f90991957d54e3d0089c7d27696d81abfb74329178e06afac5c5a35000a20cd1985be458dbf21db94173f6893a46332f5aa921f50aa93ae7889e189c94ae87408f10828741cd00fb2c167e99f07532374198993bad778880c1923723743558178509516f395676f5b6c85e644d6befa61406b647200a43f1792d0eb6a2ad9a2fdc813479c321bcf9e067d04fc1dd75e6b61e042653d684dd1444c07bc5be89893012897c185e1eb3c8fe393784a8319d83cbac479f4b901c5e67235b448550a273c4c4f6f54d5ec6991ca4c088094f2002e43626e7de664d516e9fdc1e59ebaec964a201d63d382c3e658962c44e070ded8e5598e7c3f0e6601c01fa6e1bbb9a8146a69db6951fc381e8737fab5008807be7d04dbc137059f26d83f64336ba2f12db69902fc37a595fc6a9a93e135aba0d31bfc5b2e4d55989b803f017e747c0b7dc89b92be2d8cea3ee2c8dccc243fd0bacdf68abbba8ad8f7a93b882c644cadc4a3514c8340ef348ca1aadc764ec190db180a85636e354f3fb68c03f132b8eb1e6a34811a4a313e72f51a2a04227bb91d80c9b1e4f6eef3239aaa573dfc354fec954d30d9965281dae0a2c1198d6e9caf0f90c8b504da364c5f62360a03fe2ea3f69a56aa5402190b31ac0f64b7bb5b8d8a7d80032303da1dde9e2b27c7e5d20186fd1e6df2fcf93ac0fb215e7b341dc763ba4cbc45ce41f15c374b7dad94fe2c1d45d70e6b8edfc17976c9dfcea36e4fe923e31d80ffc80359ac939ee8133bb7e3a63d2896c4a6de7a005834f702b1a097e7e167af9660f5865fbb353efe4006e08fa26ca53d5bcc8a81f6098334e811b60f566c2ae06f351473a2d038a113747df8502947d52f65a9a510b041f451447b2b189dcd0c1b320e53871cbd27c5d5807810346f89591653fae67396f9fb3b49b1b43b3770f6cfd2d7359b50242f6fb4bc404013b7d05b7f7a4a3d0facff1bc3142f863cdecf9c749fdc96331cde97eb5601ad17bb0cf68eb5c72c3087530c8e72af3f6ea59f9605fc6927dc51cff31235b65fe7bf18c4dda3d71fd5cb01fc7df6cf348aa81e46c743e5bba25e6a3ebe3251f77df32c3dd56585bc96b9f97d59fe1226649d2ffebabc19d0daba809d527e275a676abdb3d97a01573662dff325a0ee2563e47a055a6c6a30359682a5bc50ae2732c602f90125b35496bdcc4d19ae2fbedf510f7e4c55e8d40f1804a1743b89f36975bd94a184f5c442dc5725f7250024ccba6f08148cdd7bef07698553ca70a2561ed8606f03fe44a5fec794c5d105b5c5481f7928db4dd092f901f954d068c16dc9fc64a48932885ca6fee8ea8c9f0ad02d458197feb5f6624dbd78e3e414efd109b771022d10ef52d931ef551cd26b085c8c930eea691a8fb88d017c1f72661a339a17d8746306e6eae442148f57bf361602f20d79adc52068643d26e51e1cedb513573e350da01ad6ed5071f4c9b31cc1f4b6ccfc00429f47cc5216807d8014243ba71511c75e3e2eb1a0eb4da4b5231406c4331e155b134af9369ce02210de4bc0bbf5ee3eb403406adbe98bd07368670f254beac58237e79a0ed02e01ccad095b23b46fb6610c27079bcaa79daa31321224007fb47f7ca9f00197f3cec7b8620fe5c34107844f3400ff159364e5c7bc2ee44c48a5b82e302a543b86920b8c2f57e0bda5ff78c6eff34dffa63c1b0dd7fa996000892f90785c9dba6f5ed8ad40f8e65ea321c17e713470a45c33d37a18924766191f2ac8e2f0456442e3140e8827dcf5c7ea08a46177669631bba64510c7dec51a04c88f2f6c94fe4f1f2c20d44a3ed6ceb92f389dbd6203c8a77ea40612ab2ff58d8623254f7d092843104970816c5d1fd3b64fd1d63e562bc4111268b746a8b11be8ac00fe1f6ceabb3577ebfef5424222433c3d42dd147c80ff0f12d552ed274dae5a4c09c4727bf40cefad2d01ad395922d53f3475a1d6c6dc754ae9f3c55d8fbbc505bc5f6f5931a3da1585c75a64eaef2fa7e9e47cb0e300f2191ca877d2d5d7be28338b7b2a258c666c94d28f02f6e38d8965ed0ceed27ca6ca8f381b65ac093684b500d2e1de6882ae7197ffa36bdc114390441b4c55e6a1d0b374dc7e4c97ea9b2c3d98c1661bf7240c5222acd781d6ad4d1cb4888be44faaef5817da14f835f17c208d0290de703231b209dca2191f9a4e5bcd96715de57cc704f8f3517503109fd79d5aecb27c27e2efa5a8e9cbcf03f8570a7e50d388a441ec6dc5e4139f78677dfb947dc0bea74b8cf3bd2494c544f1396566ef7ab69a5fa20310efad35696b9414b6bee48c819b5c1d4c218106d105ec63bccd4cba6dfa5ce12bb2a404041f265791a1c740e69aad6a156b4a94fdde83380d0aa4ec5e55c50a3240be7e6f090bcafa62fac87749dd9d29aad2ceec7113b0ff6513d5a9ad3217cbca62c72fe951b3e968da6c48f786615b865e331781049171e405c01f5c22fc0738fddf5b9bd55c42bc38403dd2efcee2167b2a8ad583d2894f6557d0d475beaef47821867b0c48578fb95091db72c9d1f16b7f291c8ae3869c7aade3afb1ea3ffaf9bd7f9ff54a521ca52e9b3c58c361c8127fb2e82a3b9a5a9df3871dcbb01cc37fa35f10a01e890af29f7dfdd500f5280a66ae460a462e7f1481ed7f01ea35646f7b184b69f19b38f1bb3af27ae898cb5a88dfd232377770b1d270929397e0773272b2b1779196e650d7fcf3deffa2ed62452e18a5fb1700ecf94df8cc8c4dcdcda1c666ec9c9c667ca6ec1c1c5013631e4e765e1e734e1e7623763e2e534e5e0e3e4e73334e0e5e23764e2e637e136e7e2e6e7e333ea8d9bfb7ebc79f307f230e332e7ea8393f37bb1927b73107173717af2937bb19071fbb99312fbfa991b1091fb739272f8f310f3f1f3fb799293b3f8729bb090f1797a931278731afd9bf75feb02e512e8ce98e8e09a49a90d8f8aca901ffce2ab49d8d6b7a293b5bf08aeba5b1b42586b212ee2f9b8b3b2b143d11dec36db6ca570404dcf8a2d76e73a516714ead421556567f13fc018266a8ce3696b7c8ffaa060489a224e5f10bbac060891618312d58801ab0442f2418776dc4f857220fe6f18941ac930da555763a3d9a86e10866c5333020eed0ab7fff339fd0c6ce97c2a44af92e2f9c6d77fef89787330de730d5f8abdfffdcd8f696bbfcb9c33123ce699ff8b72df5e30b9f05826ceb745dcc4ce6e7e365c52267a67fe215be7cc03f26eefc09e10e70ccbcded6bd9b3c4b56762c815b614fb45815a4a165787ea88089ed02ac8988437d3912166b8dfdc3eafa3b7e680d42dbc5adaedf878bf94e0f6b6b2cc4949392f2a575133d8e6a15326d4f68aa1aa1ad9f098651b2ef0897cbd2fbbc2d7c952acf5f4189fd7aed57ac2e79d4d077dbb382648cfd6b95b974a7791df7420dfeb3ad2ef8f9ff5a5d7f92d56524764b9b83d7d598c3c3ccd148c582c7cb45ccecb6b89a948abc828a9bbba5aaa9b5b98eb1bba98da3910acf7fcefa8a8e14c7238fad5f7adeee163edaf5dd6cd37d782799f6fbe049ba00b2074a8a012af65192445b4ebd7ad5298915db3c86d4c01c52284d83d28ef673e57687680a7352985a735f2520087ed038181de9ade7ef63ee151ddc39d5328a0f84fbbae0a09f9cdcbd955bcfad5fa42c8ba46a82dfbf1d108cfea90512823e377ae307917c14f6feb7db439f11764cb3d3eb1e62993ed1c1e23b779b7d1d7bd7043f1276492acb99641e72e6de3f84403c804bf83f8746ea81eb3570c14828891430560200991cab15d1fc2670574196cb448e9143250f7d2ff67ce86a044c913772124a705cf32625a148b885149f398a9f74896c68c2b97cc528891238d8f0804587f17e8028b8ace9964c8752fa9afe6a2631966bdee9a612352c583ecb7f2d584dc60831ff4af10251f3c3883544d813df4a3abd5dd25e530ee2f0a335f995f73840c40d536f6d76b3c60efd5b5f39c41a765eb83f6e36c33d2008567ff9ab0a06bc346ec3187fd17ca59f5faa3c62ef1ec785c2b09d7efe7c954a3816b634180fa18601460502667bd33eb076b68b82dbc7e3d7fd68e0974c823408038c100e56612ad5051528acc2cc0be763fdfa9cffd4f1ffe997281c1c1c04b2cfb3ce6ffbcf28b9696c2c70952a17f64b1c20940604e190a206d10f89d6203ce90a70e3873124e0627b06010277a14602408ea84b5947cea46b7023454cb43762ac3e02e4125a2c120411091915ee123c02ca1f7a00ac3d7f6e8e68a33f21e47c3af7cd73e96197cee5c9c6682cab0bd16376022e444f5634be10fdb1052ca31af1d304bfc170b86fc75a8c849af19a82b67e6525b38d6d5f50fefdb5157157da41e4e1b762f7e0c5abcc6478ea163b7309d11f25b71766a46adea7b54b1a210ac230af395dddd05224f3177958d4c4d64b451b9f68f865a86a9a176b1705ddac34d642ff0e9e88908f5103e6c8f472ccd79e0f2fa8e8683a1365a2049372fb71385ec17a3f0e06830d0a9dc88a755753b3fc76354f2b3edec5c1ed9beb9ef178a1f1e5478dc64b9bd37d94cca2e4faef3c1dbcbbcca915a6b1f29675f1b71158e546fd170cd04a25a853f00e657308343d6e326e63af076ae05f19c4ee8a0cb08430fb63bed6aee3e6c1ead4126d320d4233a7615c85a37f25e1c582f7d5f24e043dbe8d4779d3bf836921707f1ad34244af72902fb317537f5f4080583b5b40e019cb9010e02e21431051e15190fedf675af89818d6f3e95528e7d3df7cba103d96b5eaafa50baa5e8c7e0396798b187c82ad8255b18f710d99207edd084953b45243da0575fb049a8db617e3abcebce0b61ea7a52588916a4eb02b61afb9b55855df12efb6345611426a1dab955dfee89b6a1695beacff0ca93b814be00d4c7be3756bce919bca9c6b86ba5c0f3db552455fbb992ab54598a9c660cae28f566258f33e5f16f62cb95b77856609b30796258648f2041b035bdfd5a35874ced03d9ffc8dff8dd56741f4862f221ce97b7093289cc5d5746feaaabdf158283cf5f38794f47bdf9a95afe54c77ff461157906eace279b590c834b157dc06ee92829f37f9b59694ef573b47dadff445ba39b271c97a89d879f00d688d99e9dbdf5658c6db1fb2e42f8be8995df85ab47d8d28def4d277ec26f803b8d47084cd46a61b3a6868bf55e0d03cd7ae810bfea6fa45e10f9fffff7ebb9fd3534a1623a681a2b1f51f5c67e9f78cdbee080ae4ab374dca52e95cef52f5d2453ee18272f070fc71e6875c30b88107eb9d618dffd19cfec9c3861e17108489521a44bf4657831e7caf58a3e7dfebb6868593850c1c1df4b9f044135fbce2506eeaba173d17102cb608a1bde05781a51110addd7100075dc5d1496d8ab4b9d2c093cc965ce5898a010625c68b19a77017c5b2ff0fbf609503c1218aca6d479cfd89bfaebb7c3dde81797a78d0dd1c198c6ec25602a017eab4c33705bd04e6068f0a560d9c5a24866963facdf39f01e2bdfdea3e32b7e5dd3b8f8ea4e7f533e1062797814e3192f211ebaf3c3eb9483233df31b78d9c820af2ee7409697366a5620415e37ffe16c1bbdabaab2c8fb6d47e1f0acfed904923c3b2f3c119214d1fbf6168213148adaf9a27a4f9e43bd99ff422ab2fa26a75db444cca545fe9231517924ffaae1ffd973fffffc79fb0f80f16ffc2e2cf5fdd47e6b6bcdf7f3a3b81e2eded58185344990df92bf9738dec75b1bc768af56c098d5fc75852d51ac3c6736f5bc5055bc752eca3277219aa53c52da60da7535e8f7b4bb203656707195a5e34a2ef0df35048d6292b8909ff59fce937c89593f288b75dbbd59fd21c8bde28b86e38fcbffcf95ffefc55fefcd57d646ecb7bf2331a432e2dc92eac82e5a74ffaff4afe34ce49cbfc7fd87b0ba82ab7ad7f78ef4d777787200848a774374897d2dd9d4ab780a0740a220a82129252827449094a48a760d020df38e7bef7fe7dce3df25ccf3ee7bdf77bc77dc650874c56cfb5d65c337ef330e5ea89e7564b09626c2c123aecc5d584696c4e3df678f639a3531747c3c08486d10968879ccb0635b696e9e562528fd0cffbaba885cbc1d58cb864f0eccfbf05d9ff3d5fa6cce7fa33809704587c28e0fb1f68cc56d8114cf31f75d6c364da2f8700107b2e5d0ab54d61d7fdc2741971363b870ff9d2177c101307dd4f659ffb27f9685f2e096c1a4078fca74c203fdf3f05b68bf9970d2c3b5cfe9fc990617c31a713db2fe6135502b9c70e28b45fe6b3105cdc7f01d58bfb6fae066fff7f0d99a61ab0fa1d432b79759ab0c2fb18b6a934eac104e8fb2edbc444868baa229afa58fae7d5a64aaf79086a0b9ec3f00b2539cede5490818ca2bc3d1e51f1c727bdf048c829d4f875c4082951bf539d8693a4ceb7040bcec18c083a16dcca7c42a40e10064724fb536b23fd716daff4333f23664a3c1a6e5a6935dffd6a421e6e750de482e1fb716dd66fa82a14597275ba99a9065e872e07e114078128c910bd7e5c9b744ce0d425d9978582adbab85fba3409cd95eda82eae0dcfe5c7b5d1964529c16c8683789dc9de341abf8f96ed49120451270cffb836837183ea8d634b0c092c9ab34b62cf1bc8cc6120490260f83faecdfde0da15a5569269b9b2eb62cd3d45dd2e599c3320471cc7efa55e3c82b905ffc3d11066c918e1f00a64a3c28b64038360c227aec0d73f301460a030f789fb65bd9ea0cd80fb677d639eb864f134b13a121051ffc728b150cfd1af3a160d709e9460d2e695dfcf6661f967251100cc6fd0dddcc032c3210e1bbaa44be86f50573d1474abc0c6df0a3b423cf8ad231ee212fc8e7888ffb4b4b44936a409987292b9afe6cda5f6b14710bc5e09be54d0117b856292bf8ee27437354042a055866cbee03c02f9132562cc8febfe6747bceb98cb5308256f1aefd0e21344201f7eb36ec50d838f73ff6a473cc4fff497c8ff7f1cf1f4a5cd54157c65fcf8e579e4e5f58504740dec9c1c64d4b8dd85789de5e404f56c9d1d0cacf8243d5d64fca5febcfec39bd4e24f70efb7343313e033e7e6e3e1e2b5b4b6e4e2e5e3e132e3e1b2e6b1e0b6b2b6e23117e2b51612b0e635e3e411e411b4e231b7e23513e415b0e0e635e7e211e0e1e3810fa5e72f77ef572b12dc6839379ce5f46baba0370f4d39bbbd92c388724a44547cb93db5488554a6dc48d1235782803f484c69c9d1389dbba9b6f95c581909f925f17bdfc4b0997fc1bd1fea1b05b568708e3ce4fcc7d907b58cea4d19045bdb9c45f329f6c767423cf472273031de8ab8aee0d5e1b09871d1b923a641cd654088bf7e824fe102d18c61505237c3529da02684316a16ce6b1b8c3790b9f8f5b45505fef8dcaf85f9e1445eca1be2a841bef1c676b7ec56c42823dd3d76a473c5bc73fe7ecf0a6fdcc7058fe9bf8cad750a7cc45a8e1c7d62787369fa48d36dfa29ac080fc7a389e2e71ec2bf459bfbf5a31ff340c0c1739a5ddfaf16ee8a2bbcb03e24e3f456d85814ecda891a9fe9dfbc39c34926c321c1ad47d865c16134d99cf5d845503512b05411173a72e07db8a013c344cc2622333f46fb91c75a9a0bfdb955f879cd0de887081fe32180301e0ac8a1876ecacbc9cb23c8f3873913e4c862c6bff8659c02ef71750ce79d0126efb6c277e9c0f2ffa8bc8e6a77791d9257f3c34b09596db6bcd55117ecd242a6d59498dc3dfdf4d6f74c7d16fb6630591dd941ef6b9935d39731972289610f1abeea5848974c2023bb91e0ad87dfcfa80ba5c67611217ef0a90bf33dd96b8f1afb11d506462ffeb447b62a3383fe2136dccb6ec1ab243dd32faf9c268c5596e2b2cf8a61212b0bbfadc6e97efa4197aacf9f5c83719d4fb6ad622256aee48e185915af0dfb3ac2a596cf7572ae851fa822de2384c87a7523aacb4a5e33483da88c7d58e2132015a46d4ea7aecc64451d4271fe985bd3fbfcb213fd7b4d5352cc8ecce77fed7b07eeac2010012b4b5e6e330b2b1e1e1e4e411e336e4b5e3e1e3e3e2b6e1e0b2b5e732e4b1ef8f63fd8fe998f86aff7d773ffcdfb07029fd0fee3f547d15eedd0b7f9313c1bf211d7eb68ad0130fe40e1d662ac2156a9edb04358db1ed339ae734e8bbd7d24028d5c6fcaf2c0adb43fce23e69b6a54bbf51c669ba392604167984691a3b2e88c7b7fdc02f39a4f15e15c8b9fe9a08972476a7c6c76ebb588758b8a7754a98fd902f26d327a258986f8a2c271573dbb6e88abdb96a6f0edd7deb4193f109937bea3824c84fd98b4491d5a8d27f2f69abed18bf78643bbcf9a73246fe15b893b8650a177d8cfae49aef5e7b50f7f59e38aad753adcab8dc0aeaaffc8acd982d943196be240aa5d87164be070b3c5992b475bbd66db62803246aa6785eb9d74fc9018cfb651e14de689ca37071effe9fbcbd29a8bd3cc5a8893c7ccc28ccbdcc2ca9adb82d79a8b879bd7c29ac7ccdcd2eaaf7c34593bf667ff7bdf7cffb9f713cae1663942e68ff7277256472d7e432198e08c4cdf33ef5d35c4e86a011d10798c5277c52ee26e5d7a59f97478290a2db549f8801702e2e5ee014ac714db776a5318e18881972af8b6ee4ee31c63bce5b310fa6c777d26c85833fa8ab8a3a3f799b390d41a1525533179ee2de8fdfab32a2df2e1aa128bd79bbde6cbaf72c743ab43cd826f7dd1b7c98cedc37a2e9fb98e7a1561fc1e75f06d6371ce995893254a8cd9d647a4b8ef8a0f6fdfe127983b738ae5cd135dfbf02d9164c87ec678db1e7fc393a885fe86944dd3de5d779699d1c519672d1dd6fff4fdc3c963c9692ec4cfc76565c9cf672628c06fcd672dc8636e6e61c1cd25c86bc107dffe61c6bb78ff74d8c2d9fdd4ffb3fbc751711d5944ec87eda35094b76ca674823e2c31d81ee2a3f3453ed39445fd8290aa1047c268eb1a117535307c845771ceee8430c635b9d864ea3aa6e52b6c669f9761ceecdec947911a822e9c0f4816e8b57b839e7bba7e1d2a8bd4b2345a9654f4d240663ca75e7f6a83727a14cca14a945dc98be5e0f07811bbfca95d262a8c39ea559c4885e3a5fb7b45de1cb998559aa5352fe326c92b63d85bd6d73aeee3faa932b0645fcb3ef40c58f6b956bc45772bce38ca88ca9f75ab8d245b34601e3bc826617f456bf2ed7833ba69be61172953eb86ff7ffafe811daa06a38461e33bd5c96ced2443e17bae414f7e2f96f1d7a7f7fffcef9f6d1996adea3bc473c43958a2b7b1bca7848576f913762f7a7847f7c9e7b7c28ea07bc161c48ba661cc53d7eaa16b28db0c7edfffd61f004606a8c3c19c65c1eab799b4fbf86e4a15297b270a4da74d8c32fd634dc00f87e4d5d932a17a8122c0eb2ae4b27307bc372d987af047dedf8110883904724d92ee77e70c91833c08d4f57a9eee579c62ba5ea78eeb6f62b8a482f30fc359adfe69fcb40ecaf3f53ffecd05870c9fd623c4b2e030ba3eca7ac4a24b5703c0d815806c482222bfdddc6bad7969bb5d4523eb9bb6f1b4e9e38bcf1ef260306460389183c1a0ea01900e82b36ef8eafe32fed0093fd987d4efd7cf15f1ff2774adbe0732febba373f9e931ffc9fe4cf9f9febe5da3d42e91e95f137be8f2323939bfc4c3c9c70c20bf800edff0c0cc3017abd9294ae92e2e8f2305563f7a91aa60d1f97627cafa288eadd72487705ad2db39bd80b55c520ae47e29e3f372a566716df4a5d6a0c26a21c996e2e25636e21825a4bc9369c2e8747a1b91673bf6a3d6ad71a4680798165dd79751146f69ca3ea19de87283f5c8ea367ceba30109d7850034a9af72b6f5654d5bd2afa013f17d3a3bea9b941e6ffb19fef9b9f2d024e250927590ea4124122868689cb78b8b1d3c1a4d1d2d19e97ff5978f605bc1bf468ac396fe378ceba8ff5ee33ac8d403ddac7edeb87e515a75d8ddc6a60230373e788debb09f5196b3b8e81c9e5c5e4aafd31f6e9a469ed2a9ec134ffb99c5002bff6759f47fb4be8ab2c282070f859914d6921c833f4aa6bb4bd19b83cdffc6afcc5e644cf61880c91880b1d3f94c9a307ef2a5f1b78f074d166f12665b6113513174dcbc634dc161b81f93c368941dc2a19aabb5adb0c3da28843bb78a5e0743fe96620957a6aec8c8d0d2f96704e19fcbaaf3afdd3b60597f5a613bc16194dcc7bffc29ba482aa23cf896ced1e724da0a3b42d30c0e437f7a3d8cd13aa91e4de6df1f30058635810c68bfe1297a048f80fbd14b79a4cddaaba667d9d3052370ee3090fec1647eebf0fdf3c13aa00b2d01f93ffd81aa04fee98536f84a53e46802d56aad098bbccea0e225e1ba62f10f4fad7fc5e1fae24f1293acd6bb895faa5f5e4439d6cd738fbacf5644c837e69182333762cee4f3ed2d74190bec70d145dcbc9413aeebc2a3dccaf3667aa28ff6b6196abfc826b7be2dfdb3021a282b1f6a4b5cf1e279a0be18662b24d0e56c1159f55ffefebfc7dfd893b10bf9c15816f78772b9880db8453e87b2e9fe95fc4d816d65d935484ecb9db567653659d452ccdaabaf404392c9cdba29755d381e86dfef5f801457dea4cbf1d84844c3cde3138970b996a9f13becfd27950a2e08eb147fd24ce1b522b4648fc274832ceff045e25ef67314fc30ff5ffefe2f7fc3cbdfe39c1433f38f2a74bea0ed85a70b74e971348e5145c58af2060ef0ea1c0991638a048a970c3a32b87e65353aab29143c7c9796fc95eee15764cc2b2b92d764dc34e1e1ef56d811e6774215e69f2154c1ebc302b229904118160dd0bfa6d539a2cd149fa2dcfc737676b18d816166cfeaffed4dfbe7fb09fd77d3fe76d37ecdfee8cfeed9412cb266a0a2d118d28729127f614c09542e0c9e3c6592daa9b0805647e4c2c7432d7653bd29a43cac847e7b0887f62edae4a77ac499946e61dc0e1a07042ee97885fdaaef4cdbbab9aed725c40f215b4e7ec63a5f78d1f9675d4a7691573791b8f565b8bbe998aca8ed5ec82b20eafdf752fabfc7df4f1866b5e2eb61294dcc0b0fd1763777baec36f27e545820747a302709be4b2918e159cbd4a611fa753b6dfcf3d24cd99ac16183a9f4f4ccbd1a2d39f1a5b9513dcaa96b4f79d3ab2957545fe51913167012e26797d24c4cb298f32d74b4153efcb31e155e070735b7fc73c2c64fb5f8150d113f360baf24ff97bfffeff1f7f6bcd71d5c865a6fe1f74f861f56761fe0550f7afd584480e925d0ecc2c5dfd4e511ad3e51547dae6bee97f5b369b0a2235f645dc3a91b5ced5cdbdd8616eacacd29ee4daa44c533af67baca18b0b19e91cd1d780ef413756c27dc2ddb94b7ffb3f8fba1917b4c54057ad2ca97ee8967932fc8a88d2412ffcbdffff7f8bb797958b0267bddb4a257e02d4afd97b383f5e01fc6b615472739bc7d06dff95d539594ed01d578f560ce046b208226aa5f2a3bca62007a7e9bf3f0935fd70c87bf7aa74bd554e63ca943c7d279e3149394174da50d8c39fab4f0b8005a629f097f14fdffc449fe49a9ea7b83c370d16c7e4d540f1bfa476442786a4660712bec502f8ac0a7d89c6dd5f7ff852c385828995fdc4d0afd1f92f0c7aedba94b405a6163d1d0c86bf4f1eeb49030d8f076380684aa3bb8f5085935381c192753ac1ef9cf782fc16946022aa1c7da928c9ebf71e8112dbb2b46d0dbcf1cffbe1beb2f3e4f2c03854638e11b03a89592190281e0feeddfbffff9f5ffbf6b02f30c96a8a8cb4a7c579fec978e4aa74d366a8fc689182278adb523b59a3b6ac5a96df225c6078a3548bf1c51cd9a90c91987758a22045d9654065ba446734546ecebbdd0e0b7034d89619c6e512c6f654ab251a9dc1757904a0287861e5b3cf9bcaa871acdba96f3244a74bfc48bf6f5e1e54cce39656bcc4408652096b1aef250a9f3ebdbb126cf5a4ae28b4b3658d82dd3e5dec5a2d5d979f36552909a585c26c12cc8cd782069ce4db7fb42b52a987c338d0046a2cb16ee97f799621c6d2940388955bd69f8268702e4d03f57a7d5609f3be82bca033eeb8860716b7fb9c7264344c131575d686b994695c3633ace11eaf25e4ec62e175ce72e7424fdc3af8a3b081074597208a408e0607fc65b31d7d0b6e47faa95cc2785adbcf400f9b3d36f5d06b8776f0ac148321011855855a7bfe470bd533a07e09abf30b2371c4d666352ad08da7a1dd57086b07ff843d71eacf917558f2ce13af8fe0dfccff827f03f985fdacadff8fdd77ffffee7efdf2f3f9f002c5bfdc0cd9c270f96e425c8240cb963a35715d499ca632779028f9af5d3b7aa7b9b291fbe333bf9b8fa244d9cdc5291903ba47a860b02c3717714efce0dbb36fad6a297f24e1f26e5cec067d1648a8777d204d6bdc2f913725dd0e5176ec2d2f8f53165467234ad721fcccb1d52a824aa8d74bf995fc077b3a3a64028f4219020d131b05739f6f164ec4ccf2643de43134537e74fb81e5e6ff1c5982b5c1cb346d22012212724eba591232933a3e69b0edbda4335fc7b8e92ce1c677ffc517c07e1970140201000e6c097310a81c094636311885baa0b295e27b2f85389dff22fabb1f6eb4bf49895c48ef76652589a69a222d20009eed252623f355a845ee50ca0b61aa3f93a31fceee98589db50f5f7bb7fb2f3bfe7033816c510344da7665a1546dae218815124e209096e3d82f244c1da9fda9e2541c268b20feba18c7c790480eb83df37f22a220bb6ae712fc7278b86cde4466f3f4c38d91f8c0ee625f0533a7297ada127dd3e57dc32e642cc189e076bc1eeec79fcccf53ac598a964426c124127d77fa096d1b2547c6d16e950f9d70043f8e07ca016b2be26b110c811742b388cfaf86a3d748943208408ce6d0f48f528f34eaed1c9f2763e06f7ee0d723601cb2206e5ac9fa1837d861b7a5ef7f39a2ee9f547c534511ca133e464de0519fb8f3d242ec964783cbf60e92f49d519ee5ed1fd99f1ffd3c7447961ecf92f72219c623c2890116c1fe05d417b1456646fee0b982922d9f68f681fcf5a15beee2da5bed2a65a81758afc4d02fea504258bd43ffbf0b25ada049ea89f5d155ae9406becbebf6c4dfafc42541858f06cf02f1230ced9f1df24e00b8373f1b9665fec79e6ee0fda493ff02a185d5570512bffe114df9ebed4a666a9f6bf98ef09e7e342cbe35ee9826c5591707f145f8daccda421b145992b2d3a8fd26d566b8870946619eeac58d8736297c7e1ddbb7f41c4cd3fc34c5812e12807ea4693e9f0917d46dbeba99e17d96f834ff8fe6fbea7bfb87e5445677b2b0b4fab3f2af6fd3f98094d557f3b0d3e690d1d6f2e574121074917075d2e0b5b6d3d7d4939271e3f4f0f43073dfdebaa8236ce9a5eceff39f32b293334bf768afce59953e3714a81325e301a5b5638da68f5a376a2a3d32b84cc8f0937731e9f9bea0656b961c8f93ee035692ca10c238e44b34fd591666576d077000fe5ff1b74ff538e30e68c8dff3de8fe3f4bbdc233f37a50d3ed38367e193f0967b0df100213788080880a418041d15090ff0dd0fdd0e77badf0d14fc9e0a257e2775d4c27bbb8fd4aeacc8be9524117d2ab883a2ea65383c136219e6227e34c2013b994e5063e1d26f16a7a899db23fd037a896157749effedca3d6f940a281d8906298b3d3bef822f7c9425c38a4d51cc5da5d8f0451d663d1870f7623637f7f84fedba7818ce63393ad99fceea1c171ae88684e2e121a9e38ebc7fc7ae55b77a81e2138d9b1990d04f7f397c29e84232985dfa874aa5586343e027bae2122e6a1dd63a0b172b9224fd59fd3aaea6ca3975466391828abc4fae8a03ad289d3fba45aea3883f8aeb9d84d4e94c11b0e015d35ce267ec49fc3a6d7d38e8c923969a345f332e5516ed2debb8175125fdc3879397dab29d9c3a846a22fb7554f745800aadc19f34933d59bb498cbaa1952ac8721f3d9329cd5696620f6e579c3ba7eeb116c288af0baba025530e46f0ed1ad346fa198f0ddc530884ad5928b7534d1616745da83303abb9025f135a70cdb3a23848fd7d52e13ce67c0a7d58506ff9907e3b1f63876b8b475da3b0f12b9edc95907d59c8dc51ffef2ff4cd5c51f1dd25f7c96239b59b8bb7878fce1f238f65e4eae56ee1c56be16b666ce363f91de1776e77d85acd185eb63edc8820376ec83a961c1ba318cf56d7bf669f34e0cf72643c3d4cb77c687844c07a17e0faf398ab8fd2ab4628fbc0fe3ab8cac93f9f2ed4be11f7d8bfd5d4dd40a3b828d4743eb62f486992090bfd7fd276d9592e602a6dbef088f282385856b3f22272f9cdbde660c1cd36b5fc43246eff177fd4fda2ab75a8d4e1c6ce947f61a0da2281dbfe954a4deff6158c8dfa7ebffe75b05ef375b85c3e95f2e9a1b9979efaedd7ff2566985ed4566e6f065e7ff83a98bf2ddd86d41a526d8112f5e7018ec48308ce06b493d2f225ae746337cc6099e56f8f61106081d7d1e1e49ec5f88b085371337e22033152ed6b3c6691e45d733d7a779fa71afee1fe72b7c4e973a0f3f4c2b7eac31587d44ab829a21551d387f6ac78b547354432ef37aa60c1f1ab892c1a1e02f292d8775f2da194b772ad6656337e8859fbd52a317d126256a22be460cf7e1ca922d6ee1fce3b32757c7fb6888c906d5f0a3ed5772e35f9406e705fab45c35a44f07eb1f7cf3030af50c81ef9c835098b9bababb785b49bb387bba9b59784a9b393aead979daaada397bfe2b9b15def204bf53fe67ce1978db87737dc0e0e61141e860214ea031683fb5fe83c4fb295ed7d19f11f80c0644ef35c42b1c4e586fdc905e68c12dc6221ae833a64277d8375622746d365b2d81cdf7119c78bf5f02a9ff62f50a02e63ed1d1982dbb7be406f67c2f11b126074c39c8c0959441bb6b9ca179525b3f022c8e09cc1113d1dcccc30a8e7b8e95d3d7c28a8b5f8e5f8847808f8f9bd3cc9c938b4740c0c24a404a50d09a4f4a52509a57504e50869fff8fe5a04631f375fc9908cbfff5f53fe077d7bb626de03651c6f27e7e7804bb5a84ec80749aa1639eb1afa6aa54a10daef5872d6fd478cc76d7cdba7f4cd2f0dae167a04b1f73a913c1e027662528eeb76708dee0ffeffa5f38bfa67fe6fd00c5564ea8f3bb3b90b3483c6326c13052685b79272e71cd338f59c7f9f0f46b617f09c1e58a98076aa895c5b2bd9b69415ec5e4ae46b638a57aaf246deb71a832138f3b0288fcc55bae8cacb3a0f418cebb21cd8e41e063d03f61fdf804f805cd65f9f8cd79cd78a4cc79acf8b9a579f979393905adb80539b9b9f9adcc05f804ada5e5fe33f7df1dcca4171b9f24f6dc5c6e06469297193322494408e01aaaea66cc365bd1758cc1b5ff2c7877754e50be2c2c06977cae9a9aa2dd3bb5235838dbe632792eb6558ea2aadefae862403968f65330fe76f17072f1b0fbc30f05f15fca73f1d9fb08f85a58f89af93af0703a5af3fada98d9085a0b9859d9f0f1b8da38d80af05af108b97b7372fa5a3bfb39395ada9b9979705b3bf0730bfcc5fbefb2297cf201dac5fb13ba850d221e8175501170dd86ccf53ec0e31b7c5f45fa94c02f20ef6a5c36e09d88223c1b55a02f46ebdbed99e9a09f2d1cd0960588e1c79a8915863c6e16631dc8ac125d12779d275306043ee395d90e2dd18b37b90dd5976b5c4eb5220bba05f0152178d1bf0b3be417ccc3aadb605b24f692b29d03388912ea220a31d77c21a6305773ec5452d672e1f3067889125d5aeaf27cc819b423ff80ae8b7a5e4af05130e09d47344b34408ebaa76a8865bc275243dac63dba120810065d82ade8179def87557fc4b2b5a30e7bf48e7be77b3a5562fb9ebec18bbb62b2686a2ce18fcee23fac0192b832e6bb2f34f909e65e155ccafa74b67d9ddeb6e3c1f7f44bc951ec3c6e56e94735845597aef019301ed7019c575862ed317a58e4ca5bfc93d367f508d5ef3de700844db3243e2bd5b8fdfe5bba570546ad04fae299d430c0be7555c138a04ca084235a900f31b02ca8bdf52d1b607f734345efd28f769fad16bb293a88d989683df40140e2f25a3f15cb1fbe47f830e2b0bfec6a421f832322c092c05bee29ba6fda9cb88b35c25cbca9b51cc9da0960307e439d039fe5e3ecea586e95ad7b6c8bbbf731f6bfa70b2cda8c347eb04bb50d33c92442b2bdb5205a0ac8d62ff848a46f60d9e25ef12d12f11053f7fcc47955d1efe942576267099b97100bdfe41e1749ac2821f518a57c4fbf56138b14b0797553376eb9614dc08191d09afdc6f77451f65d41cdf7d766758e23b5397d3337eca92a00fb4fb63826e9c911cb4715a6adc9841c4ecda39a2840c60cf99b22431f976acb5fd8b7c566da0c40e70bd6018f0d95c1ee02612fb293d785e5c7eb0f32a229b14c7abfa75fa7c04a269053c2b3c3dab742ccb5774b58900158d0f5f0697a25ef862a5888e4bdb94c29d1d397550cb07fea89867f43e8a8c8ac95dff2c49dddc5c4841a00e6c7ca20f679446e6a206fd85400015df9e991ad2ee082b1c99a7f17ad5ebbf49e5fc2e510f9db27f23e69c0f96cd3044d2bb8a4bf7b7483aa85b07c87129fa3156049b0e364bab15d7ede285f9eccb015d69671433c16e070e1502dbfd6c838f3d0b58ca57baec49486ef1b12ddf774678f36a1a108c79d9ea37bf923a24f35bf366002ce0f7fc3c54ee3461c5c52751cfd09860816dda3c7bc00baa9e4de65b63e7fdb0f3e7aef5ef00d4d06bb03f667e86e971b9f3e46f8a5fb5c775d6930cf035fe702e63fccb51f4d89b3c738daf5eac76012d7f62ee93ac0fc468a4cf7abbbb5e24e90d0d19c2e79d7ba16dc64fe9e1e33dcbd40d5adad1cc7a95ddd8fc7b3297fa90e00d87a47e5f10686f8d6953b05dc34ed05e3b5d7d1bf02de8709ccd537319e23e1231e3ed0971d87f56ba42e00209952ba6fc8c834dff8b0694ed17cadd3524ee78a19407ebe771bf911d1a928553e73c7412da981e2c3b42580a62f53efc8e2f22ae709da33b68dfed6894c0d4bf2d1efe90ff90fc9b2e7dc7a2cdfddfe7cf3a901f9b9771e3d806e926745a267be4e8b5b58bfab85de6927c001d85fe510bb560bf2c75f07d1ec76cb54d7766d62595f00e8d5d39fb1f8535caa63e770c256f7eb496bfa58bea73fdb97bafcb9d05122ef717507b9c0e94e9a00f7cdefe9cfcbb2491f2c7ed1140d925cd67c7deb28b2951900585885394a5fec7c6bec706740dedeede5d13396eab2efe97565e94d4c8697c6a45d3b573373af6d6d20a8197d4f6fe424caefc79ca4d89a93a2d98b4bbd1afab80470be352a656c250abca835d04c16cba2aaa2effee60b48e5d558c973c3d41071ba47432b78acd89042432e139067a6656f1a954d83bd02699667c4a1fb7a6cc5bdab80f569339213cd96d2169fe64830b5e135261ebf6d0ba8bf03b372fd5247d55e9e62d8d9eb71c418ac990ec0f8878c293bbbc971c336655eb2d6510a137e3002fa3e8ea7f9ead5ddbea3ca4a407b6cf75c77c8329fbef47bfac49bbe4baa51436bcfdedf5445c6333de0ba19edfe3d7d120d9a72debe47147645bdcf11c54b44bb13f1d1f7f4a528b1cc8e390f13ec6e553db6b1320aba8e68809e75ed867c17c39ba60fb0fa064d0e9dfbd8c9e5f8806087cd98001c0cd82014e99cac6eb86053efdeec3ce07eda9a7861d72d5c1e49ace53e7bc39e5142428201e0efb7cd4ca67bda74e61f3f53bab575bd788d8348e7cdf7f45d5ac1cf944a8f7bb4090c0515189e12ed0a180232097c9a9688981db2ad15bbb4bafae9a36916d5d544c0fd70f0c2a8ceeb72b47d1fffeed1e5d20e2e64aa078a3f233ffd8ec5070401040a922a0c85ee623aaa3288fc08e2de815507426fb9988e7974311d1b24252d1424d51b0e487612946910fdf42448ffc540f46b20cec5a82120e3b300a95f0964fc2c20f3fb0864fdbf80d0413098509b40c607e26983f40d44fd0fb23e849d20fc0902188b4d03c21f20f603ac1590fa41cae3ac8294ef02e1cf3390f9055120609a81ec9f3990f5058117449e00191fc8fa6180e8078841743ba820e70f1a48792c2d10fe04f100c64e0099df0d103a8862011324e1061a88fd01f51dc8fa165d4cc705c9f1870d128c8e9e0332be2090f501b9ff3010e13b1fb140e6071d647fa281ac2f2a08bc286a197cf7333a881f041188a716ba0d48fb0f40ca83e88f31c074b3f420ea5db02c7720ed2381944702290f0646820c969007a43c0a48795490f2a820e5d124e0d22f42d025e05c7f30ff0190f29820e53141ca638194c702298f0d521e1ba43c0e48791c90f2b820e57141cae38194c703298f0f521e1fa43c0148790290f28420e509c1fc3b243df775c45d1e41c8f86dc5d11769aeb45869cfc90891d9f09f3c8a92ed4553abc2f06c5560897b1e5ccef628625dd37b6e95a12b3ece22e8354cffba61fb3a5d389c402e924c9adad5623b501c8ff157e11f9e7248f13e7753d41e0bc3fb7a5543f35e738227f6f3f8c965c9cec065f76a84e09455a4b5b5261fc5933721919947491325c6bbf0b6afde28f9a87b2a3b2184e1faa9a562e6d65ed0fa5234d90381eb349e6d0f51999f39c8a9220dce28a79d45f525d7a37ee878859cc0802d8b2e277bf2b649a6b69b0edef64fddfad669720fed635a28cff36e93098f6c3e65ccb468103f7b719d5b586de6ccdde9e896e0a3ee5de8dba5c019fa4ef25ed49149dc2dd4be86b9a131bf99541b78db4f09ae1d5e45666dbf72eded9d39169fc6eb6f1aac67ee4e48952520311b766e74c1d89fa27c22a79c8ef4929f6a3318fc6ae8a216bca1b1e34e5dd72d8c79cb281a4e20084985d6c4bd6eca6b88972b1bca958a38a59f95e44263fc25af8bbad552967fab3cc40a1efef070e74c55430e3dea98014a90bc8ad1c8ecd9f3f2a1f0a3857edff47178c77faf9dbd77d8843d87d5634f0d03bb33e7a09b6d56a82a18c5268ce7cadb2513166245262f0aeb8a39c2f7c6657b5c629c78e5262f9f6ca66c6ba53a945fff1a1e056ffb8108f9218b38f79f8e8a2431bd22c97cf1cef82857b21e1d2b751dab40f8a36b9bdfe1190da77bf957caaca7193ace8250daa75a977c68643ee81ce16ed0ca129bc33bfff85f280b453ec51cf443b2a46dbc163c6ce44fbc562b72867176cdb830d9be46693af66159f1b55f8dae2ae5f2b96a18d124de330f51d1b442aa191497b7f25682b7fd230e597311ff4d570eb6b575dd89d6b0324f2fc7abfb44ab8b387a50d38093189652120ec44c09fbbb88d4a4e83bf6f9e134ee2b56ee3ef75b037c4d9fe0592cc23bffbc083a09a577a1568299aa13cc19647e9d7e78c571e5417415c7925b3141660bfa9a42541dc7b02e766a6a35bd05a8fc18833da74dbc6251812a7be8c6e2183fbce30f9e75de1123f2617a5574b8f78908ad9a5982fa103dbedaeb7917d9c370a117c23e5b5dccad495c77599c0a9cfd06efd8e4af4c3bde14907123997b760947f7cd0abced6bd1b13e786766db5f7e2f3c35dd995b3c782c57f7a1bb6d4c4c267d57fd63b23e31d3ae321665e7a7308588bebaf2cc950fbb8db67df10f37dbe5e426ed783c0de06d9f358f2960e3c4abf4e9e3b4ba5a0ddd922e79661aaa570f5c4eb2598c86a9484e6e1d87a00976b6e060c258024ca19ec722b8862419cdd7aca6f6e93ed63a6190c0db3e874171c68cb938f769beec115378835dad8d7b240b5ff7f4edf1abbb6ca51bfeb2df666b713ff90e2e7f20ae6f5a23f1e6f37191327fdc2c98bba3d3b6edc7088597ff5e708479d638a07fa85419cfd53b23a5474cafbf9a8752e749efec4ff14cf90db1e7bedc9a39eae09a992e63c18a754227d7a616d3b325df2288cf185f32764107bcedf7c43bdcbc794ae47a86462cbe72e3ac05d1e12a7bcd8d79e36da7febb5ecf2694216c3e8387d8d6ce1c6ccb0cfebdcc8c99d49fbe9ce235dc09f942b38e193432026ffb66d61cd219bcdd8ed11ff826fd3fa60ac859404f353d0510503e9db96d0e1bdcf2ee3da10decacdf7b877a3ea8964c5cc8ace0a43ab33760b28f7f14242729500a6ffbae762dfe5a4264a57b518eaf6b3673c8557824057caf60db274c92b4fa44dd4f9265bb374337a8b28dbdcf405354ff99491857ca1f823aeeb4eafe055b2189fb05bced732c63177371698a2b41a5dbecde2bbe9278e691ea3f30733f6931ebf3722e56fcad384ca5520b4266ec6ef4b38459a78ab737754bd04962774d750aa52d136aade1e5ff13396f219413f62fe2651159683df68da8309e23eb10ac009a105fe8ac5ebb9e9a618eabcc99d8e7789243b91165a157486a775ec450cf5761648c8a4c4b9a7c8377fc8bbb3b66fa6e8990361f266a35f7b80dba1bec150ba7baeb8f15d5bc6bb76df69df8f1d964429f3c4118926fcae925ac3d2c3f33ad2da71fad1ce08a50338c9587b7fd10955bca9762dfdd7aadeed55820326b1c3faa6815c699958335bc29b0689c8ea49f732b75ac3f6aa4d3dc66ffbe6e32242f8f9081717daf0dcd768e27fa0e6d04bced1f6ec9db79eade4d4acaa4ced218a87b158b37d5a16198845c9a3f3049109cff5214cfc8c54b4f56c06481e8caea0b1786bba5ae5aafb28590ea5c7b0833f7b262e05dff6ee9350e9b579650912daa54c539b1113134f9facf1853e76d39df389f142cc431152710f8d2af3a1d230e3738386e84921821d319f487e87246de4db7346b088677fc6d6f8278da62754ef19ce6cceeac93d38f2a75d773290a47a54f8665a5f9df9f64ed7176b18ef5296339e1fe48a972324f85a61157f8f9e14c185b9874a1088705bcedf3952ea9f08856c24a2f07193c091e41c156509129cf29d7a97cfd4c27207bf01dca962bfff917ecdb8c880cb70a9564e8efa75ecbe4d85ad2759c9f656d5aeffd08effcc73f0c60d8e5b81736bb4456685cdc4ac693ef36b0d061ddbea7ac7e64dae25dad9f6a3c5bc05033fcb41b571f45df832a92940009ca686b2a98e9a8f470e1e00cdef1d3d1e657581cb02c703ff5b55dae328f48195ce5654e7ed62cceb0b2eb374fdf417ce0a1eeed74b3b8a0e5dd79f7b9f250c965ab3128ab465f634efe554a715b2678c72fe39e9ca97aa858e3b54324d9f5a4d589dce94dd7093f0b82c1804af39c9fe898c0f917d13513e6a14be265c5901401e1aada8c3b13cd5fc4f6d32b43d4496e7d86b77dc5bd3dfb75e5b9f19225b772f9d0c096b563c57b853b2191593d927d136535cf74b9e5c80e095c49f8f60894a64dd3e8bd154f505917f5a99c9953a25675daafc13bff8a24e48cf3f783dfb491f01786bf51410bc6ccb0bb2c4d7cd82a9dc855d6e797c597eb3370e530e6251d67779135caddf1bee80d7df13a54a6b945f322699347ebf08e7f6894e85ecfe1c45129ea106bf9317f27a1a837a359cbd7af4e7851f3e9fc01fa16b0d3c64ea71d77ea90387463b5e4a043d5af54a7a2d98d7a0408b8344b37a5e01dffc84c7ea9746c6bb8d974fdbe70b0e4563aab03ef74af0b3f35537bed7ef8f8a75bd558268a5dabd718483fbc923dc44a9f90c35388744e68095941db5742a6e5fa99f0ef44a7ffb5f06f281c01e080f0ef1d2297e537830a11081444c78eb82d2210186b2e022232122a1a0c8a00f96338ca70857f0bc77cd6878b7ea7027a317de2c2e042e14403c20be977c3a82fa4279b9fc145bf57120d0f1d028120fa2b1f78ca7148f516053dda5841c6a2152df432991836bca1b82d6c6044be15c5588a3d85caaa6c8e77e873d770ce1b75168128be79b482be8e70665a9a64eb01462c5bd5d6fb5e124f247e1735e58080cad3e0f1e34c33070beab2f8ee2fcd0e5d2846ccf318c79f91d5de7fa57ba74fd9fff97922d3c0f4c3d3a9b8d20f23e2604ee58846b778d2b84ce4174fb7afdabf7e86dc9846e2bd6968b1a6dbea1f0f4b7bd1f890c843ccf5720811c140f0abc2d9127a928481ad8cb7fe2bd2e4cf4f44af0b1a88398c1378bd581aef4ee0e5f91277ef415057636559f9f455350f9b5e1b7949296cfb2d6b2e9ba8e349615db59d436b239cb7f27c88482a35e2fa8bbb49c2b89c3badb02374ad5f016422a0ac489f21f5e83253119fd6e0646b305b00582c1ec09714d36ecebe60ba95396cf78a881f01747198d61690529291f086b3a84b37ce53645145d33ce6fe1194bc969fa9ff773e305b0d48ff912cdd62cec457d8f7bbab4b6e7e280eaa566d635f7ec783e3ffdac1fdbd55ecb7391c25492369f8fa80b8a8a126a9f3cbfe46a8fbe56f442c0849a2f5b212bd87ad8ad95aeaf2b5d8a8b8ddb710edab3d08f80fea4bc61f091a3011b2b7f46f0611231d0ff93fcdeef9c41ac2b728435de932cec4a3f09aa3470c026b9a1b99e699fcba31b681a744b5fa9920e946f0dbb459c384825552350ba4543daa7de7d079554e1fce10254e47ce72922e7d51ff803157057e48b9818f43b50a04e1b6a7c4033106b6a711eadc1a9f121a5f8fce102a36f668ed74a93c596a14eef7577dffa4a96ce6a096ca37bf657af939a7ecc9a64161ba242c556b389cb8c4f11904f1a33e2d41b42e2d93dd2d26f40d94a66f2c76f71a9ead5cdb2be14c5e797229499552ee9cefad67fd8b4db1f74a07030c724ece165bede8a782eb950b8e3d41137720c8cbb397f1bbec6190ecbcaf75ecd4053538a228e9e7d8e277f3e94aef2f92338cf9e626ac48e64f054693aab5bb55d949f88dbd2024f8ea3c6a73c4f0de353ce30b04358e4210efb46fe7e673b6baec7842de20fb2a8e6305253d4eae0672f70ad4b42279b90462b43a3a27999db0909575a1c55afb9c8f446e6a621fad8d31db51ec48a1183ef3dde7924dc9fb7ee1b70bd8c77a34daad3addd7be7c342b79da2719edef3a8895c3c43a3ecbef7a67522c867202c5255149bdcc3e38f6d36974cc714e358a1b1433ef260951d9d40993d6c38e6d6bdc4076b34d39c2e5a307a7c864c64de6df58ada4d8bb4d0d0c29e70e3d3597b52f2b6cd015106f692a69058d8c646cb5a9ad7f866241d0f91c2b7d4a5b5d8a45876ad602cd2b3e7dd31c212252a70c8ca2cd2f380f22ca71cfc92ee819dff8e471d7b0e46af44cbfcf875ceea519514308af5d08db04047b673b45687cea36c3d637cd5c942bb5bd75331189cd88e5c53144f99e8738ee0abc3a0afd7369445b6d4d628246a77c5931592ccada53c5bdc43d591cf9a97e77a138511f77ce422bbb092275032db943d49b7b236fcbd7249ed2320e6e92af095b50dc4a662ceb22bdfb223b0923d7ba4ab04bc829c3d28015e7ea27c2eefbdce96b2b10dc88f2a9bd4d253bc31ebd53f314960e5e96906915561a48a7c2f6e76d766e7287cb67ce74973689dfa068e92856d35f7fcbcd7c7e4d8cf29ea5a44a4098544ab41b04bf4477f29666fbbb6e95b7644aa77dcafc2e69a61d92789ef63d4faf78921e413e04b92bd5dff1136dd99270a20b1e5c43cf4c4e4cbdeffdb8fc32d2a5f6b21b891088b523ebdfacffe41fa0c7db65fa9a8b57b2186a8e24c9e3af9429958b8dcd0484508d3ce2287ae2f6ab50d26ad50695d4d1fb9b43356a6fb40a04c28c5b2dac29832567992c764f0a9b4f49a96314c3771c0ad181809f12c8a4765cb42abf629306ceba9e9f9f1f427e8d1c67a5fd9f7f69201064a2c5ad5f718da082d92f20e8e3522edf6c1f1d44956d75e616ebdd2ca1f9ecf06d2044984bf0b2ebd8de3daf768de3056e79720281e70504718c5db72a4c79eb3edb995f12098fefa6b693ce5b5d9eb96736a5faf5fa47fdaec5d402e254995292e7742a472b9aec7a9d79ec442772bba8fd6f7cfa93b07bb58530b6f291afce6a2bbfe7ab36c256676142ceaae828bf1ec76251bbf1fccd9444c1d57d21b1d646a8b75fcaf857bdf94009951e433b1c63ccfa13bb3bd54f7d6106fcf6ec3c425f7d91b074aee61ce47937ee6239d4e846e452f164296a41fe9a0fb6f46169fd0ec82f8181e942e12cff43d117dda0e471fce544803f0951cc88d087b77ab13ee80574f47b105d2b978753f089cee0df8ff006f68af5743d265d4f7f6087c69289cc42f06c11b81031d0da91d5ff67e6d7129f5b364c12ca8d924bbbf3f9859cd31931f145305e5096fd8a8b6510eaf07f9a7fac96c30a6f75d99864f7f6882699f84a90ee81cdaf3c0cff9e2dd5b93c490bbecc8c59abf4e7e82c2c92ae4092f6506da63b371fafaff3146bb785116a66c65a186ab40cbd4b915a15dd8a56a3acba137cce50a751d913c8ccd54cfb9812f26ff860f056d00a3b42f48d42c08bbfa3eb66f92b3c21f5aa7d3da265914af62a7cb225f4a7f8dab55eb1f20ea434feb9d9a053f56bead8c536e1d8dffecee9b3e0a289579929dad3b9f54db82a9317f32d9b2f7c4f5a88e4fba536ab275967984c2ca7699ccd75b8846a837c3d230b8fb076905290ab101d8d986fbefc8aa2604014c0685041c3e5f06a4cc5461a23d541a2a404add0885f094e3d00f42f3ef75ae12b7f31197cfec1d3448fbd4eaeaf7b6fc6a782832dfc88d6519a0a119586a42c0e2d59142f8be82d58166cd074ed207406c0fcc076fcb0263384f9cec20391c466caef6121e9a4c2c35fff027f4a2e696ad91f25247fe6082bca6ceb137651cf959f73a9ca97267396b017700dda71d7c0e23f12e649647d9f5922bf16cd3f4c5fd2d512ccb73fe330499d614ee300af1e8c246282c0ec7467e12e12a6f1204dc2b86222fffaa7470f64847c02fb0b5c4f476fec914f5cbf9dde9d3ba52958667237ec46fefdbc45c768971392c73d8faf174981eba1fe0e89ca22ef5c2773b3a51e907e183af66e3fab82ea11269abc7eae85a0c64363bd917fe138431af9479df5481dedc13d48d4a82f239bdda8ed22b92eb7ec6173ad4d2cbb675a330dda17c44ce20947848ac2374d405ca41df54aa5e1bbe624976874ae21a51e6ebfe6dcbc385e018a04128f0186968c004287f56972a74f7ca1cde9cf38157470be1a9ea3426de772b3ccdadf9e64642600953285505bddee945a27e0f649bb83f886bf6a526e46af67dabc33b6be08462496052ac58717fb4725073a1f5f9d5887a078ca98e611179ba4128985552907082e8c6466485761bc79fdf5dbb4cedb92c32f8bfe2a096d512f79d7919fcc5b71df17eb7e7c83328cb6570731bde91e5528b4668be98a1475b1fb714ccdeb6e598bcbcd8d8b796be863571c28160cde9ae8dccce9bd1fa9d1ba9ee92820f688cdf36ce57901597f3e658cb9992002c47e2316cbf99556633b76350eed95c53cb96cb38f651d13c4c92662dbb89452379737626484ca50daf1d2526614a0c7ac1c73b9d2b7e6680a029f5268b684660d2c6cdcf57e75e7bc4c9522b2f6664d942649d7995c09b1cdb38331357b2f3d11f45166e9ddafbd62f204306e63d92093c8e98c50b4bdf9b4b85cdc1794af8d727994060bf7199e2c22752a2520a370de6093525d7ee2af1f7df4b6f1f506a184948d6f64689de76dee717f1c35b49bd23594b2efdfd1a3b82e90e0e0f81a503ef1605f75ea92c47b4a1f5b61c68e3292a5ec6e2437aefdb88678ee6635cdc66def3639c7d98462e49793c4986e2237974315cbe74b85de7add591fac77bf51124951c0245c8feec715b5b0a59bedd3cfe1c445e9cf87ab40348c30a75b95a21438a2708347d31d45d721b3b8e5a35f95d0e7b73433048f645d5e3ca6f2234c939261a038fd38539c285f456fa5332e63e8aab1ab4b04d38fb19f82517d957038c84c25b7cdfe7ccb2fdf3acdfce927a76f4d81cb1fcbe929f009c849b39ee8ebe614df70a3e8e75c7e6d24aa21f565514c52fadad163d9f7d1da2b4b887ef9cd4d5f8a0faa892cc31a156f64b6b10b3d4af5e7ef2c2abcb94945b6336bff6d6328f4a3bd386b0bc55c861ad303521ac864d8c880250d6da9e193e26f1f98de71177a5f6e74f3fb806fb2fdacb2e1cc198d86d551b42f14cba76df6913eb3e5d7f286c3a4e06b5d0a9a94f0aa8effc83d0af6010075fe40deec7c38e5a870fb976a8f7e4647f507fa08c50b60e8196fd0f26f2fd7e891bd437c9d29664569f6969d40363e69b4cf6c0d1d1cf347b8ca4733e95ae81e5a4f4524b692f482cf6f62e6f66f0e75e8f37fa05bd7438bc21ba9014880aba30ada71a4d4f4dad7fc341397e6c79eb44737fc8c70f3f1773e78e56a90e683e1144e7fca96f207c60785fb61d11f891569c12c18f5cb651c7c04630f0e47c659e1a8ff6f6e8cffe6c6f8eefb416e8c23ccfee0304ae1b17acc0e199aa374384f7080064296dc6ea17f98cda6e886a6afd70e6dc1894733c038e5b95919eea98ddef371e46a9690eb0d5d1a9d8780889e537faecebe17de7ed925d23d2f55429bd891bb7abea7fba5a1dd6319327e364c74e44918b686fb752f1e10f1bdb13f7bffa8fe060457618540a4b7df547a2897fd7b7a50e62a6b67405058b10386ef3a245e7fe7281b1091f876782a6fe0611417465c6fc52d76cf4e4e450340562bfb159a9c40f1a99478f3a40f7d5fb2a1dee64b80f1e1b1d153ae5eb3c8a444c96078dfa9cd9448fe4a18b08243766b6dac9331730bce9ecbeb376c8faf880110d950c712bc1ccfca6e2554d1c6a1870fa5e9277601102f76af28939bbb6f9a982955c7eaa81396924d67d67e4fafd76aa126a97cae1526db10a530dbc7894e1da0fe3dfd31273d4a80814fc57da49a75e9ae079e7c5f1c01c6cd72f50d32b790504e0dabe120cd5b264f90272601d8d8db1f1e50a1313befbd70cbf0ae680b6f4b99f6066812e2640b953f15fbb76e1937108745b9d5ddcba205440cc7d3062676205c7d79ab72f9c58bacd481afd7cc003734ea076e4f436fc96f1461e2182d5969ea7e355980674ba0efb78f3726458b0417ac646c18b40e08e7f500117fb92d11d5593cf87beb1ee25bb27d2f69b1bca601880d25832d8d8686fad05899a692e703188162c90880ec490a0832dd77d16baef9896e23377185538f2f445efa9e9ea94b58a215819f8a238be2978e361ed72a960fc8f540e0f77857c1c0e3dd8adede6283fc9e9b375528607c3a4627f61a6371daf111cd7b99d96f5c92a5390188419d0f4b761e09cb4a5d3d6b9cd3dcdad494e01204443ca33e0e795f7fb5aa5a8cade9731e428faeb58411201d9a4c325d7b7480b878f5bcce40e3a4815afca60c0011278e7427f86dd86b76fbbcf5ac9538e3ad65af2e40ff3edab47f633ec4e9327dcd5228db537768abafd8fe3d7d5ad05680b3815d59e6f294294b345b4dd38c08801ec9c0556b1774f7e93aca206bcab343ebeaeea4270071aeb6f77cdea80ccbe8d2908d59a04c5eaaf6f3f7dfd3f782db5af8d46dafc6bc0913bf77766fe275b32a0071e7de51d436d5ed984eb7ee435eaa8854687b862d00aa2ac7c0b8c7f63cdb4156bdec55e1bda383e9a4b37e40ffe8ab73da8ab0c267e8283ef4af6c56dcebdbaff89e4e2fd29fa2e761553434d7706e59d85d79104f0688c8d32fd5508b100b785176db7a134a7fbd220a790fa029c398e35fbb4370bf41fea9427b7bb28933835103c0c2790d93a87d995c839ebf58e289d5dea9459a040bc04adb6da8dc46b444b6fd4d9581b7265688afa0ff192021c8dd8d36a3fd9dc4d9904bc6e34bbe7a51c41a8200978cb4db37a2d068f8f5d7ef9b9d1add7126e1b00d7c093c5f17ee8b8f4097f0ce85f47ae2d8f4ac4e0b002f64097171df96a3d5e18768c1fcad82d20f03ec7a0188087633badb8527f88dbbfa972c3850bfc4f53aebeb7c4f7f13ef64eb58d3a7fd2df099f13b7efc573663c480f3216938171df9e6f5c6574eafdfad139430ca246d01ce774e7b7fe90c5e2fbbf357f3beeef3631497d2bc00f78bc042b62d1bbef5b9e4921a93a31efedaa6f12d93efe9fbc7d52c4de7f59c61e9b26fe7628a89833d9038bea733b76e13979611125ecd2e79d249e47b6b6cac0a80b8f40ad7b2bd17b767078591b99b5fed53b42c6e1a00f146e0125205769a5ee8d96ae8e0687cc5970f317c80f2b76e1f446e9511735c4b4cab7544cde3be2f4f37f03dbd8aff3509ad69aa58e463b6216156d11e9587cb00c43634a7cf9efafedb0f71a57377223477eee1243d05f43fa4b4615f13135571639ca7579dd121d783fc141051ee738e479da4f8ec9363afb715eee6fb76214b5ec0fda7a6abd04897fc8e3bc0beff706527c5d002a704803890266cf7526768175f207baa7dc41e81006d0a1920c5d0ba066ff490df76d4edfcfaaeb1aeae366037b8f87b3a79c5a74a86059d1562f1a6508b973d9ea99553f6dfd37bc878ef8bb0ee985fae82d063aa7238dff60d04585070138bad67198cf7b746a2ada51c2af7b4e48c34bea757ebdea9ca1fd266ff2a4e2de133b0e958dd4a0fd8ffaf9e6296209239d72cd030e5687ddb8e15c77303202e30255634985c53d9a84fefd08cfb86934adc470ab89f4da25b46587da5d9b84bf8c811249e5a86f16800d21f574733d939f76699eee21650065e4bb9a16b1f0600c94f24d1e62b504269f4b83262493c46667579761b8068e4d67ce42f37a8bbee2f6920954f601e11f47433e67bfa75a75071749f54571ac1556de5356ec46b1c6c0061cb8fcaf87438c13550ffa4b55e7ed5338cc23e0f70bf5eef976766d048b50d9c5cf4ff106d04bb75eb83ecf7f45153a9eacb82751fad0a436a3e6ff0dc7d2549a709385f1c25ec92df348b4a72ec09cd3e7f128a43de0440bce836789a7b8f55e4d51bbc858c6ddb90960f31278094906568a10fae32aacdfb8973ede56b3d70e6422e012082342f7807a53fb2bf4fdcb55f9e1c349cbcfbb8b2fe7bbaf2f5e7a91d06c8485dcd4bd20354a54e62a8d180fbadb4c5fe73953cd342a62456352bf6b33c8b9d9b00f955957ac2b1d5c0c6412f3b6ee079c350a0c0011a80ffb22358ec5017b34b1bee6384ea6f0f1c1fa5cf01ceff76649b4f598ac3e5ca4e128cf3736a71f8d37880e78c3dfedce67da4209d04c5db0816cae4198805e180fb9ff4e5483d2f35b435b95c38d2e83e61b9c58383f0efe9ebdb38dc44480b7d4a254aeb12aca5ad0bfacf002f00310defb5788a994fdca59bcbd8b4f192eba1e70044b2f5f1af374a3f2176875dd9b8c991e192f5a1741e80086266c8a9f172b799109f619cf0769ba91befee5340443d5799ca001fdb13cb4ca6444675d18feb584daa0044a64ce957480503262f69a5f547e22ff94a4813e502e8de5e36ded10c61ee53088d26c90d4981487ccdcbdfd3c549ce55ef6f855405143b1e8bb978ebe3148f02de0fa31b6fd990b379ac763ee97f3310cc0848c8c70464eaa2f4f141dc624fea704f86a8ecab27eaf3f42103ceb79d6d4cc1bba7ebd432efb41f1e8c6c4dbcbbfff02bec4d14d4e1d9412142fbdf738605b7c28e90f0fe91fda01e09912f4557054e6faefc8b2d0e57c4c19cdcd021c88870bcf9e145b606298e009658acf567d406186c16d95a82045f9fe4f80af4ee33690da79d478059d52f9cdf945aec9f99ac3e029d73d452c1c641533c1a8acadaf0e64b936b17af1f17317c9602886498eea4892bf5f4a1e9fa93d3858dfb238c46ee517225d86bf84c5aa52b635c16b74e956fa4ce5617edc68426071a93726acf2d276cbf54d69f65bf1d8d23a0ed019f45090a57da630ce9f51fad3fe2c1ade11ad1f79d1ef0695a41955e52bfcb33d0afbf9b26f1777f2c05d7049a825405f89168d816379264796a470f7532cc52d05536b13209a47eb23fb3fd9f98df7fac5f2bec08012f380ca5ee66185dce513d02e26ff3d2d79c7c6b4857b0561f29ca697fa0315533c2a2ce0aa7955de2afd684fe7b3fb07401504449e52f00010f2cf7ff6d636f9526c223ebace8b0c00245cc5e9257c4203ef7ccb8000115a96f353aabcd53a34efd913e7bd4e237c90f83171f7c023f3c7863bb181063409ca061972f19fc85132cb9127ab98a496153dbc05b948e659f2129fb7a7de795cc058ff74fc68b1aaf6b1c7ba736ad729d0c0e7e762d6ea7b5e3717ec464a2345ce2c6f03ca50fdae66df48c12d4a63d1a8dbc6b64305a09818421231c84d12a7a00737fbe8912dafd7c50a4f18f3c9dc1bf94c1d2c2c74a1071dcf8ee874730aeff6747813196a56abc8553b50aa78336e8955084a9d87f71c21af4cd0b00c1a8fc97b244607fe5feb276bc220247f1f053175fab9f19df94e36df1e4510ca550863301a38011aae1e07bbdbf2d0226b6fc8c58d30a1b8d424f55d674968084c1469f8563a02f9efec25e51927ea7aff8e62161b03715e118c8c7c3bfb01774328a32681057b1702bf49d5d180b43753db4a3ab6bafea8fcb85786f37880ab77e6646d77726dfd34d202447475b287f4e36e7130eabb87f7109afb53f5c9ed2618f89f782aa29e553b6ec40137bc18bf84ffb575f22adbfc947fe6522b815b613fcf7ff1419eedc0319640241ebdf6c3a7c587f814da7d751ba336633990b5b7ef4846ef45eeb12cd1b80cece14bd2bdb813a9d8499b4b27e742a3b067f0a02d0d9a810b517a770053f7dd1f56db44754ee91b4241700c5da25eada78adfa31ca1de469696cf4d7dedbf7df00bcb7b4cd604531a989b74705a4b419834ea84d3ebc02e814f0dd5706df06365214e9366f6531eb5d695d1404e86cc7aac7592dc55df15b09979c77e65f13612394004ca496a6f25858b2f21eb175150cef8a54b759d7ae0250f0cd856690069c4c0da2cf593ee25f0fb947d100440986cd444bc75d7b62f4944d8e79537df8350fcb2a0fc0044ffe2240819937282936751bd6db110b8ba301e8f4a5086919659d45b43eb4a6b41833f2667230f4037476bb385a9be29d78cd91b3b5e58a6f530d51e5f100f547e1de81e6108f14386214877ca1d309d0f6dd05ec1a8d083b83fb97885b49aff1b99ecc0ca19087840274225e0cddb95feb437048b851358e8faea919957900e60faa672f55721017e4e8d2ca3fd470c5cad7560ee0d5fc9c9a55336dfbca800af2c74184e88ff8a1141d009120c6dfe09e858421414383fe9b5b771f53e248b1dffa9e2edcfc5afe3de647056397672d2bee8f1d2416f10028c255e8f6718c2298af91f8b9f39bb42b28bd1ea201748e77df94a808e33bcd39e5d0a53d97fb6464466f0940b146d0cb34b79d3eef3a77e5a68c6bd07dce6f9f00b0aae261e1bced4aa7e3cc34dfdb59d878656412771b7029ce6a31e52c6c35163bdf76ae63a3998a987f1d08b0c9098bb2936ceeb4a52bd952fbe845a9c7e92c6002501c65ef3df6d91872b14fcb1756365b64bc7ef48ceee07bfa08a64c8757ec66a05815da9715feeb57d5dd5301fb6bbcc59234e2055d0a8eef52b2cad38719064d2480803ac25976e600bee8f7d71b5b5ff9e4cdea38af4801f8737e53f48ad7784174dd4e66eea283bdfc79533840e73c7debf0135f5c5bbf800645525cb4045522a68c02c0a6a44536f3521b75f441689aacebc87933fe5e0dc02677545f8e384877a4a3cafda0dac17e27f9b2522ec0261a90b8954015e49bb73f3c5562c8a026aea79a054009f57879e4b9fce45d7826f72509ca6f210d19196e009d7ef72b1f3e216eeb654edf8dd3631b6decc63d29000a2897ce8ae8579f4bcf4eca4a8d6ad0fa5e514ec501f81fa1ea534bc72e794158724d5fb76b645783ab0c00059c483641938d6566db5103df81e0ca4212affa30c0e3a66c00577ef79a2f713a0e2e522ecaeb86dc947d00ca6e2d66f9ed2a9b342289fb0ebbd1314df52c255a0094f0a39275074d8e330adbd909e567fb8a97af241c917e4fd7e53ce71e781e386ca3a9ad7b4f0d5feafdc60ac0f9c0515348fa45e19b4dc33a3394b28e5be7ad8456c0f6058f63b5278d64fe3ff6de3aaeaa6d6b1fdf6c3aa4110494da20dd0dd221d2ddd2dd2d2088748a742a488922a0d2088a740b28280a88d220488884c4ef73cf39f7bece7bd575381cdfdff9def7ac3fbcf7f0ec3df75a73cd18738c673ce3ce6668e7fba411e52c0a5d60fc8eee8465c78d6d939d953998ebc4bf1c37504f0bdc5f535ef682f5a7d789a88cab44e777b6bc5c9116019f655b5e8befd977c144a2bddb34a1bbf41228d7de032aa33370022f576173ab8fcac3e11e9fd6a8e89be1804fdd6881dba31f23d64cbd95e76024346efde69e0be08b4bb16058a95e69882b64db32a4329935b9095303c657ce39d5b3346fd23e24ac9d0bb4784efac1ec9921261013246cc2487dfbc2923e43ae92292c41cdb9d01050d91eb7ace712de57a8c8e0651fb3b1277f30274aa4018cff2fd73e8d1eb850686e5ed21c38e3b6136ad603c4cc65f8f9f8fb8a6718943d3b5a799e11df533a711650c93467118a997ebe3eb774e1d68d4ff43985116422802f4956275ae8013c46773feeb2a87c9e19be842d187374a116e56ebbf5e59169b5cd60e5945f4bd8db3780af450c8b393192d330ec903d9ac043dade4265f522e0334e91bace3b1fd58ec14d73a6c0efddf5ede9e05d20a1f88ce484efec0c23ca6b5cfa587d3e1ba6dc7b0d800a38e7b6c773ccb122fefcd724bafc21985e3229efb60163e14926d5271d568595ba350acd437acf838bb2002b8f5efd7a8098cc5a5fb25a0966a2421cba9d9309b0bf1321b97573bb0be39739f63ccdcb465b87e777b703f6039be17d14fd5ae3281b1f24d98f67700788d581ac93502e826541526c5cac36bdc70df7285b6ef15400fbc77d97a012b4516eef5dac9058a58b691166872a808a2b3cfa143a9e4f677aa857d2db879e9b35bbf15cc0fbede8dfe226901fb9f1febdc501cbea63b4fc203640459cf45c7ffa53f60a02515ade3538f7a98f7777a572bec6df85636a9019c4dd4f10c665355e8e7f4139c20da86c931c849abb2cf1683529e699eef991e877aa690209e735f45c35d7f7903f917b4abca81c2276b7a0700458cd8698c19b779e9058f572a0896d5d121af1674d040878493c0c87b697ee4aaa3eec3389571593bf3d3b06561939f9b9d041b68c8474dab2cdb0e103ede4ad6060ff523d51a82cc4ac7959bacb9eebd042e623c761e3f2d7b87c361dfea971ff29e75331097d0c5c73ded12b807de7bec2dbce9ce2d77537965075261ea5e2f42013b0ff970bddbeffe0d5588186bc2c7fa586d896ebf20050858678f569f12cad8cb3326e989e8d9de31b9f482540a5b7a9efb6cf2603667022258e3a013e9961ba8b0310b3f69db77e5aa21f2f20d122802775890feb34d10c707fe171e1cb2ad28cbb49c1f693468444f7f952303f7d8d3ff2e0dbe78b7dcabf13eadf5a4ff8c8a9427517e89f126bc96aa651f5073b01a335d958d3b2fda54f25bfc6971e8bddc4eace7ecfd0e3a512f0bca23f78cb1938f8ab64f562aca3c023b35908b5951a4b58f7f17200956f4e587993140ba7c0dbf3631eeb81450594f91700ce02dc76dc298ab6e1440eefc2ad8ddd2fce85658c2100e7454439bd3d5345980ce36ddf45a9727bc1dd2420e66c8abe2f336e1c30e23e62b97fc9f9cc72d4e208603fad24505ea1bcee64c9626937a82e24395a3dcb07ac3fcdfe4f0663eae0f021512726ac365f1b5baf7240f020bbd7b75b75e27d9fe169e4ca7b0b02fbb3a97c00019273b24d82dc6f949add2bbc3200ffdd2791e13520a6f62e1f215d143930778fd96ef4ca875cecfcc04a79f80e36c1af9906d454b03a6c14f2f4879350955fa18e59c73c0742c487209477e1338b50f19dbf76e54328e562d019353433dd24c50e2fb54a9e0c61424db54921c88350b686a11e2fbe0051390f860c71e6453da6b2340cf927bb09728f37be212fa07f96e42ca45a5f2834e468165b0df75cd77391d020398a2bff0f7cff98cf0fe9a791c1bc781bf095d956c8e9f17c71bf8ddbf7296c72baa331dc6a4ff627be3f893fb37fa1f09ff06c50fd0f35fea651a60dca8fd207470f85807eab45cf9c917a05b606e266d5f9fc81083bdaac28c763add1f0f7d57fe1f1817294f1f107faf6ffd4fcfc03fdf35f373fa1fa008ae301353f8f6c23c0677e5c9d06fe8ef42f3c3ea0e6276465dba66f1ac5674fe39fb8ffe8358f82ebbe6b698e5e4c4bca6eeef9f574a9c3d0edb4a23b6a7f69a31819e7f3ff25a3d89e23bdbe41166ee378fd8e3a4bcd1649183b1bf6df46f1df46f1df9beedf46f15fd628865aa391a14a16fe6d14ff6d14ff6d14ffdef979641b0119e7c7875264f4f7ffe54631d283ff318a910a144ea2e949cb9df8d2ee7c426734da657135a0dad7fec2232f92254a8c7842b528eeedd969881f0d3aa6d14cfdf38d5a3e017e4173593e7e735e331e29731e2b7e6e695e7e5e4e4e412b6e414e6e6e7e2b73013e416b69b93fd4bee9518ca26f5c38e7ad1c1d5da8755ddc1d2d69befdd286229053533784a8615fa76c20bf0c97e15732bcffaf3fd621b70ed255be3aca18fcc604f80f9ceb8a85d9ee5bd9baaa8792170a0deb37d0248b59e1dbf0385a479edea5ad86b754d21d1c321fbae64ad67aee4e8655ff8078fdebed7e7da93f0ebf2d4b3ef4fc790eaf70dbe39ed89bb7a188e150044a28292f38c41a078718aef049d85ffbfad967080c05677b2b0b4f2bcb3ff87d3e3b576b0f610e0e7527bd4b5aaefcea9a2adabedc6e17f43c5454a434b4940c3464dc953db4b9fcd4e47834043574ede5dd35ce2b59fc75fa571255d57cbcb68d27354b00dd4f38df6af1d3a3329d65719f8d7a9b1d5c39558b1d5e0bd9ae31e37126fdfdf2bef7ef9f578e9ab530bb3ff5123c5fddbe13c37683119a63bd83b4154ea15f7df299d9bf72b290a68fc75dfdb5b9a33c674be7f57102e3727f0c89bc8427c309e956fa5d10d5bebd528ff37dd4c4c6e61fe231591110b7fc57e7ae42561d0056f57fbefa9fb3aa0b1eeed6d7d948a9a5a6de3e6f94ecf3c55d2768b50395ab4069e3d906078142351afd9bea54740ae217fbd6fd9f7116858fb2aacfbe660cf8cc5d4d8c60ad69993e4d5f27dfbbfbe0ef55fdafbdaa2b5b799a299b79381c7b55b77453b4d1d2b41352d7d43457b67397d1d2d7e132f0e595f395b3e5363faf67aee5a5a666e0a3efe6646575e92fb4aa9319b895d2d19e637a3b4d72b658dc33d3963be8b1be1c17e56b821ce65cec905b3245a8f0beb01decc7d4b934c648bcfae17ac3ef4e1b3565da7ef0d07d6a9268fefb04267fcb77a9fd8f832a21d744f5a6e7cdcffd76d2795eb79ecf9d775129fb617b7852124df0eda1a050d4cd77b05034b45ba8b5adb9a869df9b07f8bd673ff9bc3ae3fead2d82c9dbc67d369f96caeeed600d7002396ed2e01e1577aa6c7c16f3d9dcbad32f6d2925a8276ffcd0436bedc83cf12d7aff0ecafa3f17459a41c73a94e9e32f8828ff61b5eb943747169d3f3fd8686eb7f6262c78f23c3f2c85be2aeb9e4d4badf2b37371d745ee949b2a7a04d73e4377e838b8556b7e940551bc9d92e67c0199bd63a15aa745afece9a513b1327fed051105f67f7c41c4d1303337f7a5d6357374b4f23cd68268e0ebc5abe569aea825cfaf2563edcaebe2e4a26ea0a9a5cea52d2775c14947d3d6ced9c55cdd56d359cec6fe4fb4358e792a9538dea91e26c57989cb9c5348884f40d0d2ca9a87cb9ad78a93cb4ac0c2c2c25cc8c2c2d29a93cfda928b4bd08a4fc0dc9c57c8dcd2829b8b8f4f50d05290d39a5388c78253908fef781b9a95b793b0202f1fcfcfda30ec57c657d3f889852b03f8a9c52798c216dd7b1225197c12b33e3e6ebdb94fd67376c4ec3d5bbe3da6cefa7ddf924001060a511ecdc0c0eeee291e3d349b4c7833d486f1cb7e41488119c24a2d70acfd22fa75530f7e69108df46f594fec7c3f21ebc954d9cc96a2f5c0f64adc793eb97aacb097964b00eb9684ad0fd3399dfd4dde65ccb5e19898d35407f7803254b4f471ca6563ac7bb824d572f3a62d639c171c0980e14c174875c6ae6d09ebc0295f4aab344a5d3c06605debd0e5dcbfc892a8c653f0c8f6535c03c598d510a08060b75f17d5191374b1e7a339452743fc4b9935524089e31c7224e905d5863cb5c1a0d5ed3a6d26f3675d465fe3f7ac659fc9f09b0628d3147cf2d14a514b364c0766487b17d68bea975146e51ea33aa75cdd49f3e3cf004a6b746afab30a8aa5fd3770e583453d2e85dc1e1907589d97e8cb2eebc77fb87365c3cecb6fa3ee7d5cba2e9075e21261f0fed4091bcbab28285131761699a68182c55fe306824f25265f7087ed3fd2f6e757b5beb71190012ce1b864380962d22195b0fe20ea513cca36bdc2b313c0fbebe93c3348d976e7a1e6e506ddc6c03493f5e700bb2094a21831db219ffb221c8bfba450796e848e09a08b9a3974cfe3068ec5c174aa841bc95a9c421dd59af7d7b8d31e15ce739b381f91978e01e96ce23373397680520b95ddbcefc98d76e3a494d39ad61e1c2d4d39cb8012dba8bb44834992e2e86d4226fa8fe209022ea7cf02acf8c97cbaaa5c564ca5f0298317160fc3ba05e963005638a9f9eefdaec4c32dd4f68cae46d6204fd4b542202bc283dcaaa52e5532c309f7e5680bbbc76db2ce95b35fe37e7c7886167b6c3712c3fdb4f20359b2da333232bec6871a5cceebd38475c7ec711bd19995222f2f2c0089e6dd8f248dbd12a6bd1fac9fe4bc5b72f33e73de16a014861af6e0f48300ed018fd1d3a2d8c9c9cfa2a30f01a5b0ec6cd157671518cba3ad9b4c93f1d162a4e94f004a4abad2eb6bec18f21ff977d9833607b490df5347797d8d3b9c5c78d7b38d3d65da3e5e237b59d65fe57a30b083b2b3298b951df09fc81e7e381acddf7833e3c62890f51767c0a4a96d10fbe2921f068b0911d7994b9f8380f74744ac199e8f5d27c5687fd2f8d402ec1db7c90560fc56f493d37d4a1a9d6e121dbb9a1910bc6edf50c0f235ee96841018dcf77dd0c579aa820b8dcc7e56e92590b5e06e77bb23e296dd330625f6469cf5a89d33e2d5c0fd71981cc03f697564ee658ad64a3191084baaad03f3b3e6d9e51d148b8a8e3a4a6d9157efd39b5a9831815a2e4fa7955492c91ef7b917a87b7b27d13612d8e8004a5e0e76ec0773eebbd6e26f2b2e0fd3c236db0ddf02fdef743847bb8fae52c77b96fc2d4ff13b4cd6105a206b4df6a5e30971d378dc8d1bf00435d7e10f2661b50bc0fdc3bd061d23e6f9db6a6237c8abbd0fd92c93805a27af3ec72319b2383d0f9649ecba3d573af5eeb51320544030ae57ddd69693e0662ad250cc4dfc81dd6c0460785f8fabd611542ac72dd3a1addb1227723c471b05ac3fd912ed35da4f6e79ebde5066b0517a91b5178202ac0f634228b61c8245aeb1436f5e9f7ade59f02a329bfc6bbc449bda2878b4c1b79c2cb59261663bebd1a9da7520d013accf705185c5b3fe19f2dc2446dcad3bd8ca80529698b4249560e615e357f528966ef4ee66c4483ec0f8958a34313fef8e09bf431b2efa320d3bb6c1fd3190f567973ced1c40ff129d2470d4282da4e7be45632b903575e1965e6886c13b0e26d80685ee224d23aa9a18e0874be0b1e0cd784446edd010208e961623e42c1702640d8e4430f0dd61523a21c8656cc1ea9425c56f5204cccfc2d854de751db2e61cd778a6cb5f5878d1cf940247692a6487765385145be11764d76f7aa4b16848ac014a99ea9542e2a7a8dbf5bdcfe1c1e596c9ee7dc0880294a8cec4b854dd0f24a69c9eca4a8de01422c0fb5c0164ed75df9dca920ceeb0474c9dc2b9fd31ea653ae901f0febc130375d4a5923574aee735677f6ef6bdf1881cc83a29769c5ee55e3b4131ffa03aaa63494b3c5424b4ea6b7cbba0c209ef8149854615cddab557aadaba296ac0f8abb99e4851e52b1efa287a95b06f80256e24ec0e903538ce36fab0de7e14b543fb49ca80b4554c803c1160df3d57d0bd63abd62f117ab7d355caa881f7062a121db0bf49a0f62759ab4b0a7e9e2f5139176591d5790e900a28ae633519772dc79b619867b308384c2d5a7d07642d05aa05526e862feecf93d0e0050d6a8870688b03fbbb27b9d89b3eb42f8b4636ecfb94abe63d25797c09c0efd775177f6920b832773e9792bff259ef072c7bc07b913a2824721ff5de053537492a4fec4bbdf26d0240a03fab60794a89a69abb8b639600c550cb31b32600583fe46ed724d7bc5641be55ede099701a3da567e51a90950223767f3ad3cf0c2b95f5b0783c46618ed52cc1f535dcf796f1b20f61c653c6453cd9ab6e85bc97bfc8028194f31893bd71f597b0553455bb67e6373e922aef01597d281bd6c8eafc745d437a35f7e7fc8a2c7a1d328123e83e43d8e043c745f9834c228ea43d61d5557d5c60ff0821fea82f2c7052eaf0926819fb4c85f9287518a0142ce064e5dc7fc1ca1a672ba36d5c75394870640b18ffa54c82b331b1221a46871b2d077bcd28756fd1017b97a8b574c6348ff68581aae409e3b9f329d4fbb9404d20b8937ae2556afead246ec66c338e60992bf67140d6a53e5fef81b3b62faab7a5f6ae83317a9e9556c43be0f913d0d1664771def8be17753daf4eb57d60530d6845e7204a4794300e07521424bb9c733e5e7bc36707b84483b1d2d9bb15130448f55f2a551a2bfb34fa9203597f4d012d9844d5d42fb71edd7db84a61a84a147c0f605f1689a0be346475e14bd337c474c9b53aa131a008646dbdb6204d2d3c5db227e52676393e276426fbbc14a0f437f5416f9c1c8bd6f072c56cd23b46efc21b8f7080df4717959de99b51bbcaa871efc6f2f500e2d53bd640d6dfe5f9db7976f73c16a4d0be54329da12e1b10b806d857170afcdcf8302844e4e2895fa2f9f62f35a9b600d1524dffebd4697238f9ef39f12a6bcff176f45ead005407deef86122ca80ab20f76bf88b5f57c54ba7d300dccef27e34eeb6812feefdbcb9f7fb9ca2fcc258aa60304e2935513522aa5b41ba82a4ecbbf5122ee6bbe2de305df81bf8840b4d8f8ab224dfd2b060a6fa27a858473bcc32d1c46cea26c6ef23aa11951baa59a9994c6c479b1e16ac6b6ac763bc79438f63a1b21c4d91b4a6926e8cf3c89b2ba5b5ddb16a5ae14c045d3d772898bb91077caf6bb1ffeadbb7edc220dea4f76aea09959b8bb7878fce1efe3d97b39b95ab9b35b5db2b03573b639023b0029bfc09f8262e2c75e48263ca840db21c405751b03270e96274a1f7f8ce25ea2ab1f6d7c63b44dccb015ec5b28ea28e2f6aba7f8b7e84fadcc7add7e2d60491d9532a3a4d9d504df816f85139d728b56fb9fb8127c1a11e6d0724c3710d4488663631c6ba41c53d30992f10a7c7f8dbbb14e57d0a6cf7d5dcf882726513c4dacf60714f95fbbf328333df4696509affaa5fe7be7708cd430ef3d54c6ded1ffc3ed5fc1991c9a51a63be6f31fa9ffbfa92df8d5d56c475355b712e423e0f9dc31943bb687b6a5f8d4517e0ceafb7f241ed1047ff6afc9149a9a71b9a809bebd15413dbf467afbae6308b6c34a08457b59ed74cde40bc0a339da7bce758e534dec0ae3d34a74c184f4d61a93bc3fbad861798afbc9d44d7daf86216a234d5ee5d9fbe957bfdb0295f483914befbe2e1c02532854e0783b73afcb7adc4492ebc4fe41dc5ddeec1f2f6a5c2c4df01d24dd70830d7c3f0958087c47308472d1b40e49465b8da9e827af024892d25a0aaa2a269ada6a6a4afa262a925a0a3ab2265aaa8ab22aff2bce6c20d8c32ef3ce736b581c4e182cdc4bd0444e301c0b1605b86b11adc5508a89d2abf1ceacb22084c676e9c60f9effd7eefc85a255dc71fd8ce9bf7a36a809be83b2f5cfb8d53f85ee7e4eecca96c1b211775425280bade174d1a6ada89320cd5ecfaac0328f835967d6699a6556e25ab1c24bb362fb6565ac18e22e2247895d4dd768c6b2b3b487e4cfab14f94d0cbdfddcd9bff977ecea2f1dbbfa1383f97cf2362a661e06ce966aba9a2a8aded2ee2aca061eae0e7c32362a1ed24252ae5cbe6a5cbc3c6eca7a8aeaff55b12b336e336e014e2b7e014b3e5e4e014e6e73214e1e6b1e01211e0b2b736b4b1e0b7e5e735e41335e736e013e7373730b4b7e4e41737e41735e730b2e6e2b0b4ecee399c756de4ec25c9c3f6d7c48aa19478e9f286169da291f5fbc421c5dacd7565cb159c84c3323a2951920d1f2dcf5a3dc65ab9a04e92ae1f22d1cea89c1d4a4d58345a52774b4d158ac862fa987a0c90ec341a1dc1d44d6bffc1309630c264a7e6d72b209be16144adc7878f8cb3f057c5d043cbf9574e27aea3e73e7ec3f6b43a0db3ccc527cacfc8fd7d4b41d13148a9678fb5c6dc80f06065555b601f561d0f7df2b4acd56118950d7ab6f5910df514cfeedff32797fe5c7fac7cd202b3a04d486bc198afa1e258aaadbc991dd2ff50737f3e82403f2fa05acdf7d33b0ce7fdd8cd9d7014293a0d07f1c3543d13961b05a756b47663d38678e2acfb7dac5a26fbc33f3adc67960b07f40674ccfb2963eabf242f8593b12bc592436cfc86b6dfae62f70e484f07ef317108d73f0effcc23fa06f91419a7690598342388698eb901184774581d4816ae132473e7efe11a73591a87a19fa67efcee1d71fc518843a2741b52fcecb4db07a3eb052787d72cea31e899a64d68afc7beb258a6de6b296ed8fe7840fb90154654494bf925be11bedbbfd10d584e5a2cf340d7dff031c30582f2cb76bcee85f7a79b4f8df6fef260ca6fa7d4b007e6587e76478bfd68ac5d3996ff28cbe353cbedf1caaea9a17dda28af52fcd7d6b2e1cadb97f5c5ecbf8d5ff682e885bd2e3aac243a504cbd7c4bbd41224397b4e4302df9a27040404845a2b0fb57caae36b2f8e326d09a7a206ffdb67b61f46e030a6e8a389ea8630ab26d51658fac4527df7067efbe4b13810f004c0cb0755bc312ce0f5555af473f5ef3e930e3852057189ab5d30d8813f8b2051db1abb0dfb95bdf5e738e896092fb43addd6acb57eb63751a2f885282979aa2e1ff1c568387649872677d2f5af34932ea89379a3625d4cd8dd36a740b23153c5abb871f9bb1ffeadabfe1f77d011fc9b838edde9778f384f3abfba57557f6507dd0e5c2928842a3bb90e2e1345a52225ac827082b1c8aade8a663c93e63574e7885df5ddf421a4e7eb64c269905620ae8d724d658cd025d7a09e534c1b24b7bbcb66f1f10ee3eee1f66339c8d7cef151aa6a65eee34da750bb3c944dde3d285c1b91769e4e966d6311d69a4db3f06946538d4972ab9f23c2f60c984ca1b927425349da50ab682eba327be39493a1fd153ecb1dda32e4221e0e5db3ae8a54d4018c469dd3537d0137abdb45af6cd96ac85e1e708da013ab1015dd24510abae78876d8f498349db6fae528c6dcadd0aac783fdd84fa56d1094274f4ea73f2ce1bf29bd8e0b23601145a89fcbb74d6a173ab8a19bf9691cd1e2e3ab32603b581364ae30c68186d17df5e32bbbd02f6cf6a91cbbf846569b218636a3741a83fce5cfeda26a530973330eb17d18390be608d9b86b67983273d306769e789f44438fa012a55d618ae4422ff10d78447033978dbc4258bfcd9f88bce6c1c9c882b6779be49cf4a359ab743049ab44895a0631ca424c7efe97aac4eba903f3d30401f218770d7689dc9f977e099cd1cc88b1cd2c290b1c7dd8526cbcf179d6204e30bec30bd5530bed44148fb356fe398c6b1497023ab74cb68be7ebfd1b4549fb1f6f7b9f60dc3497ce1049e5c168227bb4c75585fa7afd71358dace2b5f7529677da6d2fb7a46b467e14bb6ce27978fe1227d7aebd74f039c7da6dc57f8c26ce5a993ba8d940759f8b41996177c99833689607b1242d3c0d2ba6cc437f78ac242ace836fdf0f0a2108d80a917120ac2d1029f74c38e6594a1306437a7339c037e15fff8b7345dc3a1801c3b176641687516c5a3b32adc1607d11e8a28c42ff1cb9ce78c804feec1f2f0cdfd7e6fe8f6de9df787dbfb1d210f53f8195c6ad57c0bbd6ba5227f330f775ec5046030ebc01d0daf5e2fecc3743774895ca6eb3728ee25a873966f1c0d7f8c8066a129ca8fca5ef357e5fb6c3c672ae4c1cadaff1f9e19d65efc60cb4fdd270f653e1dc8605fa64e15fe35886575fba6d72ba3f2b2e27ca19e3f13b29b30dd42fa47f8b6c235e1dce793eacf0b5031697d6608c6d12b09f1af9aebd09279c8e1ab34f3e637eaae353716de1d7786cf1f392f22f971f4e4a31dc856335f99d99bd0c84a662def5bebad45591dfac42d4b739266d8612036a6d270b893631e66f49442edf219f4e767fdf84e10bb022f0b5accfe1e5ef540ecb9d32ecb89fd58119420fac9cb4eaaa22bd0b57cfb6de2ed6c8953f9cf2d445076c448dee6ade7abbeb170776aeca868e687b176edc036ce8bb9c212f3a8c24da8c9bf02b087df6b3712de756bfc67b4e4bdfe0f9b2dcb3bdd7b08629fcd6b1d4d91b882abe3215e7b219317eed3f42a5ab853c6e217e7f05f06d95164ac98a8977a95e1ba48a5f1c4614af92c400157e93486f85505ca389ac228a4430b65946c89a7202e545f07b9050c6ed2c57277dc3fde30e3f34f09e9105eadb19736fd96be7cd7cc87bab9c96cfdc74682ce007b09e0a7de764689cdf382a63d533beb0598ab3820d025aa1038ed10b2be2a5ee37a98b9879d05bd285cccd80fbbb5412564e7075258bf0f55505f5b7556ecb67c40156c5c912f508d3377cde0c3611435fa66384f6bd822abfc69f64500e16cdb0892e7cb6bdefb87d4b5ef55637a0b53dcb1c286013da7020be7d8ef2d5e90f6d077acf81fe2752a16cbbe5b4e4f219cfeb813445123cb03ed71d787f5f982f45d6f204cfa333b98834514a1b11c6030113c9d19e32c1dba256c6840fafb558a7a1dfe82f01729389af68f1f833dbc85c643fbf833b7ab2e2724530b05eddcca0d3b3c6d43234f4c7d93bdf59e87c21d405d82e37c788ddfc90d69444444e99bf4966bfa93f8e0668957304c99f11d6c40c8c4830a22f402778ba933903b0b26ef4de08930dbf47eca6fe76296f812afa133e0b707ee17cacdd6bb19f234bce77bdfad1a4cff0957a6780b5443c844d3f22c9a2b7518f8d4d9fc5ff799fc810608d2deb3d8c4b56946d0fe13abfe58f32c349665d40f4357e6ff7c0a55516276f4d0b9e9c2afc5a97505e157071d02fd47d565a4c9f965651d159d4baf95995fe1d10ba2cb6b8d8dd6326e8bed4ff8c9a37f7c600f20e0c90a7b0271bab9bc27442262756cfe9c4c64d2cc467074a14cd122f933c6ed3c390370adb6fbe77ce99d04201a8cb86fbf4d63d55d2850f8979b22eea22e9359f4c2b8133b2753bea2efa49cce11a623cf984974f09c366f2804ccd47952f13fb323751e78c4aaea50c766e8f959b02397e21671737a298fb6f979c46d23e645c0bb9418e0db06a84594f66bd7552e8982e4c2a51f5da3d1d19220c3c9f8736f640ed6e23d9bd996a24a79393562c855f80fd63d0faca041d8aef95011266373acdc72a6a024600eb90a876ef62ba976be8eed8a38c36132bf782db078016b1323af34d9298d6b3cd516ad4a46b141be73ccf005aef697749232ecb054d789db4c291de9453fed2ae0ae4752851567c10cea7ac25697c779b7480fbcdf283278096f9bd59279f96c0abae6c7a791b175c8b50dad4dc4bbec673e81df2fd37717d0a499666ad37e38a90e05e407de08ab465d66b78d6b8772f1192ed7e285cbdc2cc09b03a90099669aafb1abb530cc425e6572d3d6e89df020ab4742c9589798645a5eecc455c8cbc44ff6a45e12eb0fe5b768a088779bcd2fb18898248a27b4877b7eb31f0fea2a5a754b7c218c58a0f67cec639e8d8db60c500272b1f5e7af24f340e21f8ddd81443f71617cdc86c81f1139b1de88b3621729658b755d72c0c95b1cb440060e5be37652ca0db2bf188b8d7f72e2703c7a3c6ce076025b7c1d81f94bd492b67b642cc6489984f10678702fb7760bf54faa8c4defea2cba43bd563f57bb5751840fd6e9efc619732855b738fb98bda2adf503ca1525900d667f58cfea27c7aa625b22612fecde2b5d73b59d5c0fad0a7e9b1ab2adaf7c8964aad019b29ecb4da1e1bc0fa661275b5b4ce6246bce619a97b2a60503838eb63f035be789847dbc971510d5ff1b69cd19b6dd9edf079204aa86de9c5e55cabfa22d1b666d629ab9a0b7f8d1b60bdaf5947b5db35455f254de41b9858f892d232940bac8f1f4507a28598c23d63985599cd1ec8a78c5033097c8d67acb458149d33237ed1c257ef8cdfba675ba704c867f8c6dc4cb812c1c437d14758574ab22739b6600db46f1634a31e41cc3a78d227fff4879c16c56d13308f523fc483c7568ee2156632eda98f5f88a43774d081fd4b982e3d7729e75cf6bbc34c3382341776149e3e2014a79b5589cf122b1091889a69937199b06097e725b0bfa3d8f5bec6504e2c1522f264a68c882f7f3d95087894ae060d7bfbf444f5126ea14649f6c43bd92c2f03fbcbebb5554756d4cbc4388c2e21873ba70b6236520097cbc7278c3ebcf757a88b9ea9d69ad8a150736b6a00f57f909fcd5b06d72c3d3cf334a83b492a8a48d7563606086887e18ef909d08c45adb9ae0b8cd3b0e120bf04b4ac3b140af6631b2e8e644fe8e045f994a95e7a9802b0d6739a837cccc8c2548b073142ae1a0cc957e00b01ebc3742d8c8e15559e3d649ecad8b72d8291bde01510508d361bb47c5d60b43ad12ee1ddd1da5b31dd8902e468df696630c2256ade2391c4401a8bcf55777e9208acafcc7e12c2b39adc5d98e2bb58ecb996fc0726e6807ded85a1dfcaa99b54f79a796da5beec9ecc8e120cd8dfdea739e29ca0329064b57f10cc811c61ac78e910783f29fed22685333a03af7c12e4b8fa5d6e0d453301acafd9e8fcecabfe83cf48cc4dd55962309aa46f8802f60d65401e5142c6a5bbd23a1bed8143b4b832cb98408ef990a95027e7966210616c0a4cd69ad4d383ea3a383f660e361b3fdf7c7a9bdb231e077bf5cacbe472801a95a7e88e11edfeb92bf6d374fc8ad5726c6e6b1530c0575c7d6897b0ced20f0987681026508acdecf767ff5a4b154f4a22e44cad631d2a8af7c5e02b47f176490b20dd480a6c68f418f782b9bf72a49cb5ac50fb33e365f6cca5ecd41fcdabaa9c55582ef2b26bae5cc95585c148ddb289fffd6bf148df22797ce3e7a98fe46d83331880f00b082d700340df1d86df092e323493109e7728d9bee3c68b910a16ca8c348f599b51e2781e4bc873f191c688b56d83f3a28784a272a18f918f6a792749b503ff313b094a25e73fbc1f7498dba84fd58b2e87d121a6356e686c4f8b5284fce8f9ac721b7e9637b4ea068ca6d86e6d59d89aea20364bf283b90cc3e97ff353a8441fa5bfad1d5983a17ef4f4a4f8fa73eeae2ab9fc2d15b654767bc704beaed472ec4ca2caea89e948636352c1d572b100d335dd809271d3f026b60b1aed418edb5bec2a14e8b3f713713a8f501f57ead0b19d4753ccbc1173fcd55294218d21c62c3e56eac358af9caade7c9bd3705f5aa3e15d26e1f94494fbf5baf1fb06daed940265252d7d1df60b17aa190e8408c728a1e2c1cf237047ef933249c042e0838ea1d8709ccaa026f8b67a50087cc03714eb40aff83fdd5f9b62054dc28936b4bc1eac63841c4362fd21b41b3f9cc683cf4d01010df853fb10a609cd9f24a0a1f968c4484f4ec6a2a0e284093e7b1c29ded5dcd7edc684f82c8ce64b6cb0a8cc99abe85f8ceb33a7854a2743d3749c7f4039fd4fce8d1266f8c0c6840825f3b3c5ea3cfac3a28ee07013a8880804feb780c6cfbdfe3cce8d9ab2b6b3b301afbea087a2acbc85bb8fb7ac3297b28d9d3d0f8f9b8022afa28cb5bbb2b5b69914b7a496b9f45fa77f25c7f7c74f1b8ef61af27e200e4d7c4a3898917580c5eb82859d5106ebd35dcb5f62cd2512942755a99ef948bcfd20a7962ae38c9c14b1be062ef98eec84edf9c3b8dfc1298940b9ed8ab83d090bf5e14b8685c1b5b0a76141bf704a485e3f16ffe59faf3925ec6e1bb8cd23fff4c7a22dd856e3673307fdd2d24824866d9fed0bb2205828f652332c0cfbed88d83f9a1a8e906519bbd53ef96f7f1e0a0ac5940dbb1986acd890703354d60de609486fa45ef9a1f4869ca532f133d9be8bdf94deb89432417fbdd6b47b36314bfb28ec06a8eb0f486fbc3bca43c160f478daaf3ebdf9762013798a9fe44beaee2707a9688827403b8ad9d2a997cff40455642ece5ca8b5805724be91469ee5773cd47f9058f4e11ccdf82bdf26b1ccee277e8fc432bb9ff84d9acc764c50c8ed574db5a186927518c734c3be1be52313bae68df47d6d3cca6565f15ba2bedb74dfaa8efabbfe341c1442a7a41c1f0c87595486c0602569e1d68eccac3f313883b1af50f5ac691d9e1fe6b0f8b6d330e0da8371409b33de50c12debfc7595760f3fde593af7ce61095cc0f95f2d3d2a5211d66ab69937193f579b924a5ad50584c7d404b15d0cdfc559c455eede5ff6769c2a245203d8f6bd6c1527d6c3c819890b53afe1e8aa5923bf7d018ca692c289cb5b42e4eee24fc2efe2b914bf913fa902480a9c12fae0b030b3f061f9bdf945a413f035f7f770c07919303b8632d4aa181c3d59c5bb8ee7253f7f3d0f08ee08bf9c2767bce647afa59b56da51d5d18717ff19c0d7492e8c2663dd9b8cae0e3c4439ab68f14ca04cf46bdcdd385a5c6c1e45b456059daf79c1be1c6ddf0b4879d478465653606befa760429f60b1cd79cb5a8c1dd804eddb6722150b2b544cd7deb7b1a74bdd6abdb200d026de0ba1588bb2384be5295cbd40dbfc5139538b11708e726cbe1c222e160fa830089cc426fc9cd667292d0438ef377472139896c89e6fa4cf6f7c19d00d5f1c03fa6f9b7dd675b5df7f636113eb0b5e37f61dc7795d20b8b536de95cb941f4ed0ee497f8f4b9e4debedc74fc0f8675c4a9a1790d48fca6e279af0610c959d5cb4040a79ee1ab39a9ee90e14bb2eb0936c1248aabd6a130e142a33c8a61611c34973dfa868bceffb969b70c4ca19708ea645ed5388c0b159140515ae178d1a1729f64700ef17ee5b11313aa6feb19a42a0125b7f9871d51e01cceed84399d7e3c1b42d14f5ba2212b8063237c5348094ce67574b5c977b0bcab9ec74f3a8b91118e2dddd8024c6c1f517caded7cdc6f11d7b9e08048cdf1e9b5b06c836fac124e1cb89571595c7141ce6e45669946f1d0285f6547d5eb917e6af902a4d4cb6efe1593d546cf800f0fce75a2852b0754de6f8ac33b6ef6db06fa5a0640329d306372e6196056bf279f821afa0e3ddb6bc22f60848092c464f598e6b27f6136c1dca264f5f17f19b6c019c9f2f74c56286589eaee8cc52943d328c72cf6154008c50b717d9b39f52636e9aadf5b0993830de9f4fe5010ef24fc22ee81ccc47e972942271faa54d3cc0e58a7ffa353e91a9771e91e67995babb16e55660856e20652de07c4af440a5d9b52e3ad03c91169fd38e2f377c510a20ae2f0e4794f9f6f353553f3989af74da989831ad681008ee7d4aca7aa75db014f6481fc7f9cb4eaa5c230220106650eb7bad9cb3095cb2e6c3ae7f776f04c1fb0148f94536a076dcdf7eeb5ad02a9041e66cf0c4d1b719700e0659e21d6a0bcd99d9e23112c93fbf4a946e5600ac6fb7ac2e1799a313068cc6f225d3b28cf909599f04d6c71bc54959f9f9527ca2941de783bdc8ee07b7bb03ceab3b099bf5cece35ba46a8f64fe8924ebd930cb30082179b21219972119a85019b97858b494dd2d8f9d400e7b937b6f1e94f8a81987588244abe4b6d6279a1c41c40f0b5b76ab5ffd30315b9c0ee7c0e1bdbc0b0f3760059058b83e93e45c85856884bf5c25cf21b43bfb0052085c6862bb3f8632825a5a92beff0ca80ae09d68d7e4054ebb1e9e1939ced4e01ae65157f838c55da894b5f809d39d7afba5013a66e442e7b75fd15dac80b3c0c27607e520f74e4d9df5cee915f1ca3ab276453f8d47202581f189ed1c6541266e30eb0a3a9c5f0e7a479eaae01f71fca70f39a46627d735e5681c4558428beb51398b26f7f73017b29a8ce519524565d025be851c1dd36a010b6e4ad7d7c2fb8193a67ccadf4799756f660cd0f8036bc58a1d150e4eed084153e469b927c8185edde3bc0c111eb307307659ef033c9dc35fed4f899e2335c8f8142a6dc1c3bee95be2f221e511cfaa0b8f7c55e105e00ac3126867a439da6c3345696cf4341d37c681b850f81f1a71655765209995b41b43879b940b00e5b1f090eec2ff429dcf8812cd20b05a1cf4ae2a5939771d09481e04451bd13495b3715b34c839a313d45840415031d103cc043795345529fe9e562f74c03ce4aecd7a5de0abc9ffda425ee7dd132473c5fef8c36b2c9e42d380c704ebb6cb2fbbeb4bb60c5a86f9f7ac9405744e1012950e856d1fbf508fad507c97dca7838d1253cfec392694036e3eb3a2abc898a5a9c47860834ac593e6307c31a607f6b11671e36ab96e63c1bbc1247f40a595bd0f50630fe6219b76457d691ac732732f70a5fbf431e33c203f6572f62bc691c4214a53cbe941c5d5dc7718e043a405243b3b5d8a76c3da94653c6447808e37a581c5b1f40d66cd61d8dc931db8cde6377a5668d6260c8a322079e5f10d7af3c8ced1d3e9759edfe838c54c974bb1751809380de6698b331bc389a6cf246fc4ac46a667c112069d413a2ee5d1cb3226e14c2969bc0e068f8cceb0590283bb16afbc45f289dfe89da0cb2234161a462de0980bc116e1d39f298e3ea2c55d659e50ecbfdfc68a977c0faa3c29a222f441ee57df700feb9b4970cc500cf16581ff96aae99a5676c4da5c77c4ebe8c69d7b16edd0338f7d7169443da5ca2d81fe11a3eca5ddca6270e6403e6079ee85992e449cb16cb2af786654c3523a976524620f89671d72768f6320e376a4dc26464938144cc19e0f9f2fd255f160be68454849cc060a05f59742a3b094886d45c464abd7c7a54a4d7019dbbbbc38cb24e7e3aff6bbcdd7797f355b67b85b34b5d5883275a17e7012610bc699125a6db56ca1ee5ff8cf471f051cd0bbaa76700c9936954170caf8fe793bad54eaf3f74f3169cb29702d6bfcd35bd94e1a07b665f96adf33e3b900ce287cf0092046f195e30bf9a407ace76337f8ac038eadd3ba16a60fe0b14901c8e3dc58dfa92a670edc0568dc941cc05587f94d44fdf2670d177987893a11a624f82564a9e0e481a84fa589d649cd651c621557ee0ee246f212ab10a489ebc529fc7651fb3731193d35dc3564bfe348cd103d8078c6a2f9257f12d2465c63a8817878237e3c7df0384514fb2958f86a4980ee2a783f3cb19723f686502758a619f04d4dc90f4b823ad9cca36da7546125438da3abec6cd9f0eddaf4316569f2f0da7d6a92d925918ae03c84d6ffb08956eaf4b6e12069daf56b6cfb7cacee804c859f7ad8bf5f8b4adac2f1fda8b2cb8bce5b3f15211fadfd3d7b588b710ef273d7fbadeff407d118d86dbbe206d2e35f6053e525de8997d054146bcc9b3e8fb2a0519af3b692aa65a2bc88fe21efc84a9a2864e31975e3b7bf3113b77c519855036e2bfdd83ff57dc835eae667216565696b2d2babc17dc9ce5d4acd575342f98bbf2eada9ab9aae93bbb59c82bea4ae9db2b496afc85dc8389cd6769e196ce13598fbcadd98ccef67dae30ebdc0f17691e766ad1c8c5b5c6e4bbab63d93b2cd72f1ff15c5376ac2d5510cf3724d62cc0433b58364f6020ea3eb46afa8fa309429b8302920348fdd787ecb1a260be378b77b47f58ee012da53be8b7f4a4f8bdffc7d393b0717c577920521a7c687ba261c78a93fef7a52751ff40e705223d69e01c699ff4b9d83f299f0815168ba9ebdd13f327653bfd62bd6efb74fdd9e9493b48384121d4c1c6754828b90c826247990e7fc0350c30656759d8326303dc06dbd6cdba88b3c8a93f607c248588c07e8f4baf8596c8f3e4ca0fb4c971824c58b2ebef85400d48f8ce8984a01042e3f110aab1d6ba13a1e4e90f276df1f327efecdfe518eea13a79aa5f8530d27ef6666c754950ce659f271c06b4500ed69f5c80f2c48fa7b005ef2ac40fa0bb7838b978d8fde1c421f17f7c9f8bcfde47e09285c525b34b0e3c9c8ed6bc976ccc6c04ad05ccac6cf8785c6d1c6c0578ad7884dcbd39392f593bfb3a395ada9b9979705b3bf0730b40348fdd741433afe35ddbc7b93dbb6674d58b510e3c918f145ba99f423d3f040e55a102024786787fa841c7bb3d4c08073e56d0ffcf66d4df052c7ffcf37f17b0fcb9cff6df5fc0126a8d37df95fc0b8f8fbf0b58fe89fdff77014be83e38baddfa2f65913f383fd9be4f3140f3195bd3bf38fa171e1f479a9f7fe04c70dcf101f57d043b7980f2ff56fffe84e7871fd3befdffdb3e83ba7e4701d4edeb4121e88611213498376a43494ff4731cc524841a333c2fa97111736b9862dd6f9e58995a97b148d37eb71e7474071d4ad43c0cd6d41d1482b7c317ba5f952b06df2153ff5795a83a3299663d0ca8290371242085f83a1954975642cc080885470439040ec1fb434038fe10a721f033103815040ef1fe113410382d040ea1e0894040e0f4103803047e16026784c099207066089c05026785c0d9a0566c081c62fe23200438105c103837040e21ce83e085c021ca242120123710103e138420042e04810b43e02210b828047e0e021783c0c5217008af3602e2388480a0ed222038dc08884271080893190151d41a210f819f87c015207088a2b10845085c09028730c710100ab30855081c22f90e01a17b898008c2213421702d085c1b02d781c07521703d085c1f023780c00d21702308dc1802bf08814324d62020ec2b044451658439040e51da1a0151671b01217e8fb086c06d207088e44a841d040e51d310011111473842e010ca62086708dc050287909243b841e0ee103844ac0401517c12e105817b43e03e103844915e842f04ee0781fb43e09721f000083c10028748ce4640843c1057217088444f04441811110a818741e0e110384451754424041e058143b00a103110782c041e07815f83c0e321f0eb103884c01a2211024f82c09321f014083c15024f83c02142d2880c083c1302cf82c0b321f01b10f84d083c0702877069216e41e010652310f9107801045e088143d4764040888d228a217008bd4bc45d08bc0402bf07819742e065107839047e1f027f00813f84c02b207028ff5b15045e0d81d740e0b510781d045e0f813f82c021541d108d10f86308fc09040ee1b24640b8cc11cd1038440526442b04de0681b743e01d10782704de05817743e03d1078ef8f71586b2b4ad2d8d77fe89368fd4c9a9224387d4556d8da22f1097eb9e191683fdfb8646c49a4013f4d7900e6e788ca8b99683047219600f9730e6bfd107e5ea42fb09f73214105ad9afa2330de3564ad4ac0424ff759c182e0db9a415fe5b7ff275536bf40edf28effbbca174452561a6b72e3572f44fdd0898e86c119d404d96a77ee42824637a27db91afba14a7cb4c083d5aa1f2e4d686c57cba15b55e8a3881d7913579bf64ac158a4f9ee16a39034d18f5ac508a91968ea8b10d6534496d4bc1c52781b23083e1281c1a53e1ccd0e0bb1ccd809715fc80945529f8f6bda813b0685b0e096d4c12d13ca56c6207a9942d6c3aba5aa2fd57fef63835cc1a7aa33a53b4b8ed7f883b7335de8aaed2eb124188d1d88dc48cf2b61465a1c94603bbb521f1cb79ae080cf187c6dbc14e7c2ce06d46bfefa3fc6fd31642d83d63aca30a4e284d65e94f414644145cea1988366f0068a0a3f0df6379477d1859b2b26eafb2e46bd3a7dccb189929a71b9e8ef767f6d17e233db3d4121cc6f2b6a5bdf9e59074468ad6f34b70c73bcac4ca08bb5af5b0f508af01305f644156bf3787f9efa6baa8e134557ded025a9f981723cdc1abe6f1cb7fa3e45debaaee786becf94a42cf8f60c4394bfde5b8bf0d21663fc9bf93334cb1eeb62c6c993f725c4f1b290ae0f42c4346041857506f59c29d38f4c8bb6ad77f0e7cb4db43ebcacd9e9f2992a94b6242e5a7384efa0b807859c9c320dd5bf3a7eb20ec5f2c4c438c4411632360fc58dfc71a00ee9fef71de9c652a256069095f200654a874b4ed849e9e542f98a82e1adb49e5d790fa58140a04ad645cb42a1e49bed945188a6d5d6ed8abbdafb47c1171f90c8b04d3670273c1ce9720fc03ca31fbb07dc8ccf45a92b1591541da80d1ad8dedd5b6eefeb0b0047a754d3c7b44085214bb78267c26cef385fbf375702d6f0aa57cf6999b19a65a376397db34aed344227d90143264b89798da0fdaaf6d35624c3d94f7585ee6f0e088f8243f5dfbfbd3b635f8e1ff9199ce84a4d7ee487f116f932f4a3d78ff541b1b3e018ed6392a9b8ff687c90dbc44c2475fdf1fb83fafd5f6f82b2bb45b177ad014991ab48f689f72e91d07ab883ed1ced338b65d72194eba41303e2bc857daf6e1d2ecede2e3c456a918e5a85a74376ff419977d57b0c5871de2c1325dd6684d0fdc8f8bc89e8a2b2dd66c1a7e76c5e796d33c72c7721f82d6f389e2589b23c1999e770263fa296c7e5edf9d8a112734b728afdcf966b4b2c1d38a6f68bd9752c17b537b3eeb1e4207b6dca9e77d7ed2ef1bc496a3d9f27de7f5a2005fba42462cebb683e3e591d3f5be7319565c12d02cb646615b399568e8c174fa84a2ce5af4cbd6a09423fd3b8ef9b2cf07031843b0fe79c58e660737f829ec399b4de53d7e3e6dc16b81ef64986724cb1b796485f9dc7ad5e6faf28647e55ea2fae5aed6d2bdd17b4d4a3abce86844438fd5e5e98c78f502d3f7678d0f5c1b9f4574b4d3bc8c2e1532677b87261bf94674066fd1d4b3335eca75de3f72f3181b39657d54deede757beecca90fd99d9703f570995eed1fa54582946756db22c2bab61bada9cf730d4624592d7f7c7c3b61687a14bbe2a897b5fde20fc3173646ac13506df0e3387ff388b66660e8d6716e840d0683f9c3e96009b79791e860ee5b307b98b269f18bb9d3fa595751e87fad0302851fe3f98f9b18f9a3f651cbb9d0ff7d55ca73803d5d7d0be38409c1ce12c360bbb7a10aa84a8cca6fd54cce5179f80f495bdfe77160ddf41b79bbe97ebb50d555a59a814c4564eb8334ecdac4b2c317d16b532f8675b3d803645da40d7370e974681062bcac1cff7c07f01de4074121841498210c0fd2ea900bba82bc84a0cc0088be81187bc8bfe7f4f2a3aefddaa675379f5d3fc32a97524f83fdb44378361c8686d580840447c680a1a061a2fe315e2fd4dcc1fa1188c37671f2c77847d831f11fbaee71d815248e87cf041d0f5fffa16b0587cb03eacc83f2851a9ffbdab5b49105cacbc5383b3c89bc7e455fee0e60aaeb513020753c0cb5d8a30e9b815d35512b59bc8453fb16c95b626a9099d041b3c53b4850e34eedf5fc81ed089777899b8f1285d03d38887243af9e5c43d1f10f265fa2f7e1dcc4102bcc5212fea4e76cff68a8cea730b63b411c097f4e29b73721f9b4a2a8b263ab443fd4fd3176163718d0708d3cf952451212ad83961d4064be3714be6883b9f98687e8a527a721d29592a20e41646b6d8ca1dbea671e143d1becdef721fcbcc231176872a5167f7870cd62e089f4985a3b155b8fe2414c04b2b431def95bef36e8422e9cf353b1bb9d4ff9cab4d6990939b2e4fdd487f4ec128552be273d28b30341c62c76510b4df01d54f55f272d2bb5401daa8c1c67ee2bb69c24fbc53ed480c00d26495fe6732f648a969f7b99cc62d13d7d427f5b9efecb310f8ac07f7d5a7a6b6b4064a2f485c2ff718c96794914feebb51f7d3bad3f1e82cf0795a2768cdaefbf5ecc9c97387fe7f5c756a5a3181cbf2455d2743bb56a0c45714905e56e87325b1d057f8d0fcb6e34461fc7f9d06e66b697f4b20b760288471ca1587cf4eba61efcd2201a284dc9c9636ea891e8cb7440cc182ad3f9b8cf08d53e21d744f5a6e7cdcffd76d2795eb79ecf9d7751293b4e1f418f71c8217434416d00a285c13e8159cc08dcefb7d40e8359c072bfd4f4a105ddb845f5197bcf60f3ec66fcc849df967be90312a35fbfd56fbd866f75fdb786ec8fad24c726cd8bbfa422cbc3e4991f31689d54150c9be8b5bd9366b65b20f9ad9ffde6aca01e697c72e9f7de1073f2e07715337e5512c0ef9cfc450af33f5d8e9d02626f4b9d8c4c4cb11de8d1b742b9dace52fdb02036d2c2b0d49fdc263257515663137c07d320e2b29e5a045d10ec17c179ea9d903a4c39135d6da6e3196e3fb87efbb9e3ed21301cce4b72565632bece664e761647bf090cd3a37c1a6afd38c2f3a1f9cbe1d23425e194fed1f98bbb4e9ea1513201ac1fb203f24f7af0b5ebc635cfec985e8cc73ed11cb671bc35169d5152db0a885b4215eefc33fb286a299f9885eb5ce4f1dc7d90a9c4a6c7db8651a0f6319463c68c248ed2ff7f608e111ceffe9083fe4cc70854aad291c617d41ef7ebfd1f173fee1a073bdeef1fb9c83da1b6b39d878f99ab0ecff1d6e8df3b3f838e6207ce244af9617d58609b5a5ea1229b24b98c1a4e6279ccc33554ff7caf6e7bf27d8cbb30dd6c22f4fb1f59153247a36367aee9c16096471f813fd371f63b2eec63ae3f93c79cdf12c75cbfe147591f8eb43efe7685a784027aba04cbcb4b9f453ee2e60d0db8356ec7bb6989b57ff3ac4be52fa305436fe64346eefe7eee17c96f654929f37e82f2f5752f6ab36b5ae194fce225cbbeb2041cfefd1f81e0cbb5578c44a895af729a4f78e55fc98bfce4c6fe1170647eca555e9d6f5a9e54d593ef6a1221a31b879f015ca8b4f267fbdf67078d47c7100828be0dabd0df3a092c77a629b96d146689375c97a6ab5b029aae34beea0558b24b6ff53946269ff97219e58f904c46262277bb035964bccf6bb2bec425a261e43f3b84e7b025bd18b602588c94321a090cfda41aee05e537885ae49bdd3d2b81d91ea7584e5618f9586d38a637faa32ed3e342edf7401658d5e5f98994d6c0439fdca4b08c0dda35179d7460367c91a5a56d39595ff5ecfc7e880a17edcef487526051a4c02440d61975bbf6bc21cf62e09eedcaa3a07ae04d8762f8160b45c64e7a7ff944811d173d14b9930eb0b0dbfc6a4db5fc42d0563a9502a3483f6e9cf431005c7d75f7bb1442b52966bc66b9ef73c82af12e0c57032c7a7c362c937182f27394e246ba8b538f626b299b003faf507d7839cd947376d93e078e238378bd431615c04224889bc3be68a5fe208674bb5b78a42df6fea92b407051e03475590ebb89d6b91daa4802133764e3ad4380459612c95d3efd308cadd46b28c285f35a3c7a95115036aecfa40ac7536a63b077b0c7c33d2cab72abf30ec0f21e3c171b8c9c4ff13c5f33ed50d9a40847039d1fc802794fbff09ea56e60bcdc7a0c85ceba27803b721ec8e2d8f5fc68c41478c30ceb44632d2c0563795d951e60d94d98179d5fbc38f8a9342c54e409f9e430fe9814c072534ab5f2eeb71158e55ee044abad29f2f5224f045882cde56fcc95899f21c61712780eedb9fb03578780b27daa17bc4694e35b7c03f06f96f7723f6a8a749d04b2fca2d4535117b7cf4e7b0fdc14df9716b3bc12ef06b0207de1a883b76cd3ba5a3f7bd3ca4b9ace93671402caaa653192ecf8355b59895929b2829896446d6f4501868021673493477b0bb15098ffda298c4b09f1f9c400cbf3435021ae83a7cceb331dd5671fbb271bfb4d57002c2e07722f1fa402b8b1e22aba5c9845b1b004e328902544d718dd778f35042f381d3732239c430ecb220060f99a765670eff65de1a2c08dcc71ce4b7a1ad1560dccdf53b52225a66f77e10b2b893ea777f5d4a7bc99812c27a7d5abd7b311f3d76a4bc7ef2d722c8b3ed32805b2a8f8c83a38bcb79ede3ad8c3a5982fe84dbd7b3d066039761d7c8007d409c75de63f607470798ffb502903f00a7e4c9840817d14ae8f9647ef9dabe04c68b3de05b218b2f93c1c602eec3b2534af5d2975fb6cd25537812cea012fcfdae7d642ad210fd4e54caa8d28d10c4e002b7dc8d3488be6ea29d5aa413c25f5c8132e97ac3ae7bfc675594ca56d27dbd2b219547745903868455cd08153e841e7c7cc9b6cfee9237111a6f324ed89ca927b3b40604f1e638fd66d784307b6f47cea364ca9d9e002c07266122e42964d307ee8d5c4f706f341d97ac6854a4039f6cc16ee704898765b0c0bcb4b7b0b5ae6adbc7a8025bf5e602330f47e07b5c7bdfc74c023e7bd9b4d0dc06e5c67e78b6eea4626497fc2d8ba66ee13e58aa21590c5c5d266583232b14735c189364a288aa55fbbf7006089d79c3a417677d60099b076533fc5de256c3ce103c0f2956be2637991e1fa92e44a9be0f32b343a7cae448047cd4becce61d509cc72eacc4f7bab77351fa824bf0458daa6eccbf9c93255f3d353d451c6c24612396cd1007e8d6d18d7f9c43e8dddf582f3a46f3fbcc0dc6c03c80b3e58a356bdde1b569c9462ed0b9f59e003be968001f13cb53cb9d1cfae229896ccb3b26b3407275a0ad83f84df13dc8ac29e703eed43f232f40c7bf116a502701afeb86740ac90df27a637d8b54d74f8e6a6ec903cb0bff7dd48545e942e929e3f774f688556cc73f62016c812fc7c6daf07ebfed35b5e122c9acf68c63a398cb4802cdff5b11d9477b372cd78bc3d0ef3f21d0a3a260c004b3a34eb368395a54976be60326c5e8589f7ea5526204b4fe5744435c648ad48f20841c917a667bdeeea56000b570f29a96b092ff2bd7f405daff8a929aff757130196a596e78890307df7158f0664b91acc5e5efa761830bfb5cd9163562ca98aafac23d0b470a3aa6594a2802c9ba5c08b0278586bb4cc99edd10323bc34245d0ecfbfc64d46435fd44510a74e9f92e709474a9a8b51a107b28c883104ab694df42c0e1454d805e7eda3f113350115803eb970dbd1e0edcea9b216c61796cbe23c38db4016a8cdfe0b0661a9fbefac4c5bc8f97857696b7a5f03f68b4debdaac04521a1ed5a6b4b25c93d2e407411660fe68f45a32665a4844e4078434ea249809a8a96b2f7c8dcf324b85f5c3e24522e1e9e747b4b8fd895a2480f98334755fdcec1e9131d18171b51827f2cabeca79803ca42ae47af0e08d9ab3c7b9174f23d5ed74395d688030fecc7cf4987744d5b27fef6c3063b2659760970dd07fe16703addcfa09d6c4c80dd429709cbb96763180b308572fc79d1a791b92880d5e9199c861b7a2cfba8035bcd5deffe115d7827ac2ed4a73a57c75c73236bb59c09a0ef4bd86444868896668f2588ef19de583b2d7c0fcbfe3fb8a5f6274fbd3260501d6d39c4805865d2960ffe3ba1febca3df369ea949520539a0ca38652ad3bb0bf3e228b7627645a4a533159bba272cdd48eecba3490855fc84a8b389143b9fc4c4fe63049c4e3a2714924503675c6b2d3be9d27faca3439137d2817cc9de80e2e30bf5c09df5f282de3c5651e294c9f939f7e7de1791ab0bff536e6e1f7f28eb2be2123e723276558ecec9200b2cc58b3f199ccd5528da478df629c6f6126cdcad300b2e0a5553cb9c595076429f905eb4a70b0bdd7917a80d348f983aa4e2ad31b03460e572ca78a422da9541e00f6e7e086eb93213e1c7189bea7b367acd984860b338030fd43b3976902f72c64585da6c6101d819cfaa3aa80cc469ce47e6bddade6c7f4cf98344a3154384d3d6e0359bedd99b3a2679d1ed80b59f952bfa7eb5540aeba0b64793c3c89e56ec654a976add3d63867c5b3f2b1d91b208bd1c7cab8ac83965645e04e08e2597e0017277bf309f80e3c2ddc6003df4fe27f94afe1a188308796639e03a1fc0470ece39d83e13ff99c0a7c7f8dbbb14e57d0a6cf7d5dcf882726513c4dacf607022abf76e7517cc1328dc6924cd35cb8f91dd209ba07fc49e7e58bb68fd53eaa6afa319f1ff687fc2cdf73a8201d21e2f8af78cb5004bd5e56dd3bd8bf466650137c07473d28042333318456c0a00e4726b854a7e798477094e30d6428ed4d348837056a6be2c832f8a79a609d2c783eb29c739dc828bee382cc311d4e10f70797b1f7260426d398cb2765e324d6469f2a761dc27a54a6317386b563062e2560ffd517a47edc7f04f6e6a9994cbd933e66d9e949b15c481ca75c78c3f45dc113a63c3c8e37bad78f7383922ad9a96a36282e0b096edaa255d81a1b3d153db317a79344f67a78f5165f8461e106b77277733964177e0c536979192d1b47971cf8d8e5ce722d1b2f7aeac9fee7947f524fa588f39ee9c7e00a2bd8b85735e24778f3adf7f33b7f8fefffbef16dbec531cd9247bd4e31a7f779efbaff21cfe7b7df15fca178ea64a3d279ac1b9424755cd257ed6a255dcc86b78b2df60d0f3fdf0ee55cab9296b94dbc7da1f53285006b9038339bd115ca10e3a9c8790edf1364d1d7da8bb0433031c5d09c49fd9dffacf12dc3f745796704f755e315968c0c29240fb4b667be7f8fefffbef1adc7e55520fbffb1f715604d7feddfdbe80e09e911d2dd2148832838103670e87616749784ca8a364689988888895d58a08298888a2858a0d8dda0a2efb50d9029b01f4e9ff7b99eff6fd7256edf13dffbdce773c7b94fbde8a2c42c5c6fe0b86bf7b2fac5bd631e58946e90a02120c7d58272979af28d17c4434e6c75a39bbd71b7aae211233cbf1ef846eeaefcb7bcb36fcaf0497227d63f593fa79376fb42a38d92e5896bdd128615a136d35e6cf63329777a9c98faa7f0ddc2f7fa6453b99177cd32f1e97def1c84c86d3636ffe2fb7f0fdfe2d70bef5591c4f0e597d69ac985983bbca51822c7f64fb4143f1a72a7bfaf6f57cddbb7c6f6154f80836f64cdd26c58e52457f4f2be686da77375fc3285171494134c49afa4a65d7ad0ff32bc21c6cee141913ba42dc4f572828deb83c7fd4a4a5cbcbc11d6cfef3f7cf8673dff9f185470393a65dfb4365769b1ac61a5a9ce2db9f8a2c246f70bf753966efb57e8fef784ee50b22c0d6918fc59edc4698a157181cef2bb3d633a0fe9a276b3bec973277425972c9cbf89bdcbc4ce90d67a31697d6551f78bc55b1bba1ada1a9af9afb860e2746ebd3e7dfdbcdb7b5f6d64c99237273ccf9cdf4de4bd76bb669bec129fdbbc1d1a4a7f885373b6e554ca079c7fb433283728a0696b26398cb0f85f7cffefe1db56bd8676d150d3b4f4f01df91522060e2afe15639e52519bd757a00ee36ed07b5d53197acd9c4a11b1cfd85d67f2beeeb3409b858a9eaacea193124bdfc68bb9402ad43cc5e35baff2603a6a77e6f550963a342ef41126e51c5cbc41b55379ca4a6e9c26b6d39bfb10242aec441455efceec43ee6feb07d8666d468b8c7de7f06984f543d3f3a0f8c371b97da64355d7430979e74a396d4e81e39e3ff4bc133a5d5961c545624f58fb82dcd9bb8e9fbc9bbb7dcbdbde7d8ffd3ede1e99d9bae342180f0a6f8817d9e17739e5c54ac5cd59455cc67d39a186df3c19118208fffd5e3f1e622d26635be5a2609e14687fea5852fb7967a91ddf3629ef8d7fb793385d011bd6f3652e095385d27b5eb202bf0ec2b7fee433934362d6917eb37cef173e591ac4f946b91197450eb1feef5c1639fb484768b0a73bbe66af1846d27889fccedd5de9e09e9f71ec86062bcd7be8eafde24d664674ebe32dddef6e6c989131ce91bcbf5e16b9f8e0f79c49a7d237d1a25fd67fbfb66ac693f6d330ee7c9a7f2f8bfccbf5ffb9cb22032cfce37101c9712e9141789b00026e7e50649c5b66b8bbb95d866f921bc1c6c7cacf2fda6c062a3cd5dfe5bf87bf2e3baa5524536e3f7d765826f2c499a3dffae7ceab49786f7b6bdb8c930ab70e28567f96c8905ca8605b337975e5470b42ffd575578f3facafb8548359929f9e5324d58256e228da7dc8dc9445b19477a77ee8eb0fa782399c9554f37c5cd8e2f78737b29b809f6f73b9b6f9adc5f9dbfb7822ae855ac009b3f6c68524c0388f5aaee6a6771d088d8ba3396c9db198f987d408bb962f86783d07fae20284793e0e4daa670384f998ff354ab6f3c94cb6c7fdd077790e61f69514bb18e6ce78686f4bcb87bdbfafe0a76981ef953113e952ec8347deb85b6bcc75b2ed136afb8b783f49477138983e8dffb7cb3be8c8d950c65142f6373192d7380e64bbb9f34ca1f0bfed9771a2e00fef63826c149dda783e07aa58ac284655d6394982bd2231b71da96edd5be3d055c53691ac4fbf3e707e1e1f7f4fc62aa71b015b141bc3afc635c2fada582574ef0a1e6a8a6d8869e1ce9072f441a0e678828519d10a1746b4b1b6c65b9b5a58e24d4dedccadadad818d0db0e5b4941e9aee20a97a2eb2c7fae8551a3f7e56d69d98453e7b05b243e76d2cc55e949e77a2a111d66fb57eb04daf91f556655812d63900ebec4fc23a43b1ce0124ac1839000b87666b99999a1a9a9a9ac2677b478e762234390081e5a5937110884f311d07e1b545a21123abe1615593327ffca37918d5f031ab291eae0683e50fb224c9e220906a14863430582f89552f92592f0e62e5844516dbe120905646e10f58b81216ae8c44237010c82b1c04721f07810a0f3dc441a0523fbef32923d1242c8cf15228fff04b114834c6828ea023d1182c0f861e48075088309d37793427578e8e206379308c1ce440f2a859600a7274eae88561f2f420c4e8a5e4c6789c5dd42db07ab65c88cc24f78b6a6d356f690ad3b32b343674edbcbf6f727c3401d58d75f66730a99887be81d912b23f1d514c4720d1d97ede59e31ef88c09c0ba607903b05a98817f9459632833060ba3f0d2115828458e8483c010286c3645ae1881c6c22992641e1c041686c24160441c04268244a359d460c874041913808563e083f461b070ac1a15ca3bf4934251a2234a8a998dc00a52613808ec5c36a741070507812d61742dac2397cafa5e8e2029d0e44a11a45c399a7c0902812865fc6151304c0aeb0b0ec203c741781471101e191a0e027bc9ace913f3ef4b24fae7fc38088f031507e18130bfcec641787c2838088f0e124da305e220b04a249a128460bd8ccecc138983f0a099dfd2c8341a0ec263cac01b02894693c992b23808cf1a140ec2538c61548341a2b15032948ec0417816b0608955a34831723d67e43a40c6416031ccca4e30885d844493a158780eb39ecb28321dc1487a8283f074e7b092a95046bd598cba586fc55028cac33cc64178c5310a34ac200c07e17983442b90a872c58811a9ba5446630790e86204050b8351e4878ae220bc6e141c04da8e44870f75049941282f068583f0cec341a077906846b6680a832fb6cc22e9144a0e0ec2b39ed9f37404568d2a858592657010de1d280c8d51ba1245c983fa33dbc1bb31878283f02a21d1b941081c844f86c10c059a1c0ec27b3c97c5070c9ac2e421ef2314ad00ea4f09c24229d06204169e032d4360e12c4ef23622d1b95446363e0166361c848f07ab56204761358d1a844063a158583e0c9acfe83a069f98ac02a632241c844f1f638779f78da9292d49b2002231a89678ad2501640085b1c2762b93f08988c9b55200528e22e14ff2120ca5008430f85506c0184f9fe43c699601b0dd2812feadab9bbe0c80f530be0edc2f9201b00114800c20d1ac6a6400cc0245c22fd9781e2d03608e2812befcc62a17190043a348f86ac37d441900cb182a60bce7648714805c46b1beca00e84e14096f0d13c3ca00e8791409ef7c42424106401fa148783fe9e4f73200c633581637706da90c804251243cf4fbd94d32006a8622e1f907eef0c800284091f0e20596ca32009a8d42335a0ca04a585e92350f194a0750b9623a502221d180470189c63afb03a82ee3c945e60f86267a8706d0193ff27b16d381b20323ffadc1fcc18c2724b6fc9180f7d8606a5a311da868b2a556fea8ad8c91ea864403bee983f96b7fcd7ff647fea6623a509bc7c87f138986a001f4c18fb4ee623a80539068c0bf80990683fe48fb3ad44a017f569aca701a6c72311da8ef65a4a5b36880e917d38146f7481a600e407019aba40710ac1ecc17504c079a126cf9f040f0dc606ac22f2d8195fe78eb5246d900241a082159f5eefc91b695914667a4c5b0d2ceff483bcd487b814403e182c1f7dcf885ffb02f3ff27f2ca6032d03241a88c833ebe2911f4ee39166a41d67a41d62a46148a39bad310c200fe0c947619dfdb3054f65548def1c6079a180279b0ea6840c933980c690a84c2780cefc8b816368a3bf670c133bb268e0b022ccee3c9039ae839f359e055290a37048972101de5a14229b21517ff3356812e03d804200defd14206a8f440fd92f2c2f2910f09ea38329350cf7470d8a85938a7961583814ebec4f27339dab92623a99c9180a23efc7c1bcb280cf0545c2aa4902bec928008590a881bc306a4031160e2d067c2e4834055a4c1f34ea68722039808e8542e95838944495027c5a280a9d07ab26478116b3d42a1989c662b1305220e00bfee7d4f0d17e507369909ada5fa9b9c4819a3d3f5333c81dc0771f886e40a247874ccee88f6548805f0f8500fcba39805f93599e295c68c01f31017cf363e840db7d04be017f6a0ee30344b722d14396889f06240e31ec1d8d464790256581800f8a46a390c99280bf0605f837006d1cb3b7b170189469f8007ffda0988e4e3e65f4c71663b8a0a7ae91239a77245724abf03ba21b1c89e981aec96eef1ba95a1299dbb71a49e072b18cbe03022625c574a07d79b83d3524121070a5039d290c1f0201f8737f344a2084d928209042a1308b939808082061c8010820693de80061c9fe547f1920707c025c15d84f073ab7466a0daaff483f050874a1804027504942a21999e8244a00090b674090c2244116080aa2c80c7ae01418c99a6a43623816083a028da02330689a3fa3130469282018984321fb932581a0210a081ab0fa815513d43f87ca4af24201414ff624a6e1a40c160d4701c130b674323900812001c1852cd6d11140500c89469086bd332058c1e29e601506487522d1ff70e84247a2b17ca4402078fd9f8b9f10cfb0f809f9b1c44f68ca2fe227e437bef809998da20c80500410024ce9a15281f46a569f03a19441bc53a92cbc0b5351182c7fa09b3363ac038702a1150ca0e9ba31bb180b27537978186f730e20f933de07c728d311341a95212042cd2820d4f4ab8008758d2b20e4bf2220c2889f0504cb8f210732f80d84a306e5848e0842204824209cf5437084d25882c3e8662817360f43fdfda2c3e0c2329034884311b5dfd29d22441402881072800866a4ee14a99980948bacfa59778aec66e94e199e1f6a46a49185a50975330f10359e0025a253e840f7ce484a446dc84cb1647a170cf7fa6f9afda1512a2b84c1d297a22f2742ff033ad08b19a92f47670b6d2c6e89854ee06d62b3e940eff3c8b701312213057404104b25e7313e406e31d3e03135ad842c0a33a867c5ca5040ac945d99d2119841552bb60705c476ff924ac2d0d8a15cc48dd3c818b80371bf09b458dc830ef4f347b4984ce6c19072d8695afcdb3491e94108f22ff5fd7e1b31144a2010ff5c4c07fab548740902c1780182619024c4870d92d87c241a3106207227e47464fbd8dde26f6fbc797a46c24efa4064a1a6bd7fe1c6b645217357382ce6d33ef3e94b5e1e43774a2c666849fdce1fba1348540f81a50c890612bb867ead46fe18c54b348ea3a080c41de6c06c0c3d05242d27d0cb92067460103ca29791139623c989e83fc9557460f0904d8e24eb06e5080324eb73c0e4a4c1efcd43df81e4352aa62027a780295f340c299fbde70bb8038d94093b68c600c2c4d8c2d1b8161430f021b59d810f43de11f8903a39d458063ea4daa8988282820230f9e9103e10ff13115ff20447e093842780b249503a305c350265242c1493f3fb30e1c27719ec012c8bddb2b2e3b1768cc667cb9b2eda66b70d083c2e58e151e718726cf9ecb790b7f012de8273062136913166540694267d648f8e6c60216582c22c93380136cb44fc6214653287855926f78730cb940c0bb34c35b56058983124cc4fc25cc88d95cb615839d989e83f59839fac5c0ec3ca15fc29abc4d02739bfd457c89dc292ed66575879a3172c98907fcb5161151632502637f7178525974c2b2ccccd2d64292c3932ad90f119a9b01840fcf97f2077640c1002c546245a164c8e428d210ff8a30a0bc87979b9b9b95867ff5c3cf319b65b89d5a1f2d0f2b252068d4622483416cea092ca43c6e4cd7953c2f0066481fc541402ebecef885553c0aa51a072142809eb1c80600cafe4ed18e57290682c140de47b5040de1b181d45a24978069878807c0d0a01e44381d14d249a04e4c319bf6a19e3d17f0c37f955eca1d20134023d81d237e8c0b86b44612627b0dd4a1c9c6d3227671c4c9e48fc6732860e4c22d8da30d4ab582c060b658b5a4ca6a3c0e465e3452d261f1f336a412163982ef2e46b2830b97d9468433162301ca620343e9a140f8d89a6b7f671602c3429cc1a449349eee86852281c134d0a7923d0a46882020acbd9d0a4f01685000a5b07d1a4b09ff1ebfd84d0a4f0841b34296aff8c260627fe049a140f4c004d8a7563a209431ac3fb76901a9e8d75f667cd81930658ff462d40911bca0f94942940f1ddf02c29503224032569249a2c0394ac59d3644834866c454700252f3250d244a20703eee4e122183250f2651589fca94826192821878b90b0ced41c1e6959a0b41c0594ca81e233249a41c3861c4a2e1428d9315538160e94f6b11ec4300327258812d67b5a58efa104b07c3e3805ca784537860aa501a5e3c36f614e378fc7e8e1e603654532507ac8245d598b9d74652b3250961aae140ba7d018942b23504079d610e5ca7369941c2850d661113ac81859d9e1fa33c94039ec6792954b99242b27fee0a5ff8ff9f30056aec114349561c395db50240ad49f4c8306b0e64481f27e241a43a34103863c6086c051a13f3a56f9030528573268f84ece01ca1b90e85204509120e760e1b950a0bc1789062a93c940f926124d83fe209ef572c6d8960a0d002a0e641a507ec680c74f7c5009460115d4101f54884c3e0c75d8606f0f4c6cf50250d94c61d2438692c992c35a9322252f0b545a5040e5344b6d32350af30d40a51328de63ba512a0f2814a0b28e197a43a231234acb02555e1450e519d4b9700a8c3e38b1cccaca9012a06a0054b56940e529832d6d8c1a49d4a19ec0c269302a3487a1bd55fd5040d517a8ca22d1682a73f61fa8ce4791802a3e9fc26c3d2d000ba7414b11836f00aa2c133fb47881c2e82369a05a8e02aa6540250e894693ad80ea461a85d92baa0a3ff1efa7765c4001d5f3ac7600d56b2c66b1358642a6da60a854e8107fd4604075800654f70c378b4266228846a341a564819a2a0aa8a900d51d8c1ad5f468941ca07a0f89fe995e350f1450731fa6572d6888ded76cf46248186c556e3e031e6af351402d6d081e6ab47c4a014bc0cb9902ae563efca00c81c03ae742add8aca4da011450db0f145f31e93ac9ccac4a66401881c5fe92f93e0aa8dd1bcafc6a38334397907ece0c974401b8c46066b83233b39a19ab19e58892727ae910eb4954ac1a0d2a0de0ae28007761359e624547b057874101f83c00d763428b0a2d46501908a332980bcf420178260b2c833a8cc1232c36278fc122f84a1480570eb228e767320fa200fcc01099a7f228f95000376270ab8cc9407807f31103728c663a03f823d603bbc1a61497214a7e110475311450171d4b1080ba295037a001f8bbf185407d360aa807b00981fa021409a8473084c0ee5721500f1a4d08d457a2807ae530a8d4b70e824a5d950d54432643fd2c0aa89f196417f52776a93f4401f50783ec527fcbb41eeab983e6e4e7cc1a935040437a30b3861a3333032f3f4ccd38fa98cc60056588674003cd669380460c950a3402904c30d0d995ea209735965280861ea3ec0a720ed0b0602a668d0d54a0e18644930615bec65514d91f681ccea7000d804457208046331568a4b39ac45c6004346a9168444e29c3008caabc35fac834a041fba1bc81a610cbb8b39b234d0da639d2840c9923a069c6325b6419a0e9c06e1635679281a6eeaf165d33020534c387045e33f5278bae99cb7ca0e9c4c6661206eb4ccb6516df84029ab583c5693f759866130a689e1aec30cdb65c4a1e4b184a11a5accaef331ffd701710252349d3e241012dd810695a923f91a6a53efc808db41fca4ccb1d05b4dc862bf0ff499969e1d89519d04a663d88195458408b36fc6054a5a4b51105b46a061ba8b57b0ca534647ab5aea280d6956172ba59a6d76e84d4647b04b899995959d9c3018190444c4e86671293e2e191c9f0b8f8143880a76128d0806c10134980c7cf8f23268db1821a4e810630213bc50dab4f65fa10c50830c58f02b49e301cd41fa06607de940432986233ec53fef6aa4430e508fba24430a51505a65c0453e60faf48fcd1543c888921260d3532253e9a18c7681db3a9c424787c121c242424c5a71109e33555db186b3cdc546d3b0a98d23e5e53b5d164a0ad31a1a69a0f37f5d7a58c40fb08d03e00b477d380f632241a68af60f8bdbe3fdc73edf364a0bdfe8704330903dabd182a3487e90303ed370cd54303da2791e8418dfb4fa8921aa60ae8985098ef2c46705eb8c90374000ae86081761fc3a64087f9340282787c7c6a5c4a321cc411e0918464780c312e3c2582d92ff0d8c8e45890828f807082a0ce69acde70bfe85ca6009d98f1fa45e71d19e81cfe75f127d09501ba12acf6019dcfc38c24ff9ad111e8da5280ae22e3bbe60f0dafeb47c901ba3c0c0dcf189903dd79141ad0351cfa4da60fd9b9ec59f3e3002e86680f8f23ce67c919039c291144963402260758f239ce3686610ee89ec16a0f7340f72a05e8c68dc701dd0f64a07bf4073287fb83d1058cae00b1ac6e6175079c490d87ee18418d1e12ab3b4c8d1ea000ddafe351a34725033d9f51a849490271c961c424784afc08d61058bc199f3523a9e9c54ef941cd2b0ad02b1c8f1a7d2932d0bb311a6fe29253c3c222f191c4b814380ec480383c111e16cfa4266998564ed4e8278d5097fa0b29407ff2b8d4ac2303fdb01f5805fabb860410e81fa132bf9620288c22248617a77f1505f4afb0246ec8f03bb355f886e158300d3f99ccc3500701c0806fd819f8059f3f69ce08220bad98a1f618b862e1c3ed31f0a500037116dc476f8f413c1918588fc2ddd8c8b8949ffa99d1c54cee2673dace33cc5d8336acfa0f6a6e528041da78dc3518200383a61fdc1d549986f23f54a6a1164b65327e0c316798e864624a4a645cf8a0f10031f0e4149092ca221a1e169f044f26c6847124da306784fa32a45380a1c178441bee2103c38c11ea6b4c7f1418dea600c37a462b1e917380e129a63b69f869e438df0832ca38bf8ac90623550c159acff4fd809101830d79cc1f0cdc383328317224e78ec01296f9cc9f0d5f24e6332299060caf329e2d2f1f0e5831e80646d9a346528c56a280d1d0f807186d62ba3146896c91146cb7f23083781ca56481d10d14961743720246d730fec06817e37d68e69e0f774749601c8e0246cf9921012caf230f9697e244f277c46a51a052ece10463191402018cc3916812054a625f57666c8e6250cf4a657004caa82b07ca4c9bc54c632502e379142c1c9a03cd65061e2883cd669033ba7e8b8b8f331a4c60c22780882746a631242f362186184b8c4b61d72d3fe063dc84b51c868ff1250a308e1a0f3ec66fc8c0f8d0281238f865f8c549c428223e85c8d2b62c9f2999b3049a048cd0fd267329c0b86f3c6a4cb2c9c0c4738404b2a06752f9037a26b53fa0074cf632a097cb924d934606e658823a0c36932bbf82ed5b352132212c3958431a7e3fccf2fe814f49d315ef3c36a6473f41e77b6405ce7b78442960afea85454ebeea84e4f818bc1b4450166211919292906c6f62c2286a1c19cffcdfc43f1619199218986e9782374bb39d931692164d9c19183187901c1136c73cda17e797929a916e65e169150b10b6d3c2226388712096e8989c626565659c1037b829937726892a138dfb755366c45ecf608b2f499bc42fbecfe9ee6d39964bfceac1795ffeb8fb3c47ab92e33ecfbe99249aca45e2283bf5d0b40fdbfd349bae5fd818ed5d74be4627f966c7b8875942277dd2845dce95365dfdc019425db19c44dd7cd784d4087b451afa5133e7551987cd734b651a47aba3234f6defb2d63c49049598ef4a4dbed04b83fa3fded2d8378344555bddf72b2b2670bb9ef083f864f1581a0402ebe74dcfe3915abc04994860ed303c2a51cf4ba899b9fa1177c781402774719813462cb6e9b2983c2de261a9e59b491e1565c9f53fe7f9ba8b54d3716a656960d7dafaa39233af8f575f588c21b7872ab8bc690f3a2efb7e85d5f3ba857babc945fac10fcb21f336ef5c596ee2b93a20c4e599bff0abaf0e77ba944f2b3df5ebf63cb047e77a839377a4c307c7ad071c75deb43ce0f666424e874e7059be91bbf2e32773e63fa7930d2070baeb32b939d1b3dfa61ed9358059e5b944cb5c2ff4c52985b6cda9877d6b1daf002ef7047348d764e30fec5586d8f54a7bab01da023ea7db3bcac4f882967383af7f804f97ced727932812531b0e6bba66f27fa04db9a837592e66b2a99743686e549044c0e3983262254fc47a85d7dd4a3a459b1f391e0b0ed755ca30507ffad2e3cd4dcf25fa5ce2cfc56941b3b4b9f7da63710e4f6bd75cdd4248dfb044e67b7f6cc7b75906d1694acbc41c76365de0359cb15244d6e5b4e8f5eff0eddf1292a7060a95aa9578c846ee6d38ccf9d08f8bccedbc39b040915e0889fd74177f1215d6d64213fe9e77e657257d563c00aeea50690d14fdee36472a4cf28f32db37de9bf83ef4fc278f18c12fc34f6b95f756399cf5cdff29bfba79b2b61409ee7274e639e8ced88a1e0d4b2baf563c41f4a3936485f1d290c926c5133962a479c1805c40d4116783d567f45da77d5a79bab836e6df2346fe8f1c31829b9948880ff49fed9396199b111f186296199549344b0a0f0b4bf0b24620431208335c7cbd62dd5cc243ccfe8b8e1889e47b84472f2b5d0796c27c13b51e2e5eeb5b58708f9e38eb601824efccd6baef91f315f208679a5533834f6ac5452bd86dc26f5ac0bb55ad39b89d2f4bd2f35609674532ae8768adaf2daf873b527b3ae4e963f85b35f9861a39857fe421f2655d1ac5435cdbf1586bbbecc577827451ddd7d78b88cfa75bcf19cb065dbfe176cbc11b02e987a5e443c51377e3e87568aada6ad37a18e1dfeb007eff3a8021564ec44381eb794c42dbb444ef7c376b7f53c34c0723fd8ccddcd42ff1320acf65fb397d0cc6b80ee01f9ffb3fd2e40d5d44f19f3179f886bb7b8f95f29f21c94acfacce38b5b81f7374c7ac8f53098b6540a6e169c10b133179e1f465a149317bdf07231b82377d0a78a6f83de2f9bf26efff88c903e9335d131293c04c542021c22b3d33788687859f79a42748f3c9f0b6b48e47fae3bcecbc2dcc6cad6c23fe8b4c9eaf67c73719bee7b74d0be6e624a9b59e3af645eed85cbeb565f744f4e0efccece36759e1f136d8598aa4f4194fcf7fbdac53f066cffbeb9fb191e4d2868f51335f19703e55ab9ff7d3cfa2cddbcbbd68f3fe326caf78d269d6943aab2bffbdc1a632ff750b34b215e53e78df5c0415c408af6ddd96b56d4f47cb3cd27bdd2c35ff3c9fd2c7472622da8a7545413e1557eacdb04625dfeea5f874541e5affdf2ddabc907f45fb0f8936de356206d2cb226976020e4f48c08565e2acd32cd2632d7121a8203fb30884953781e865939c96e09e90fee7e8e736ace2cc6558c5d534dd2accd6c60c676d656a69614bc0d91200c05be0adf0566116040b3b533cc1d48a40b008c31170c022cc86086c2cc270e6d6a63873eb309c35ced61ac755fbf98969b1f666a67f4ff53d4d7e8369498df5770e994f583f7f8bf2e692f42bb78e5c99a2256fd3def94a6ab3b8d8a1d574efa05217d925194ac18ffd0d739213ed0daee89f58d3b8f7b5c6613867d577894415c78a502d5f99d196572ea8e5e0fe17de363b4a9796d990ed7020ed3cbd0eb76756e7468eee7f3fff7612555961463d7f0dcaf543f84430338ad3e63a6667bc46875cddc391a71c42a97cbd510691afb9c435a7d39d3994e7e352ec38de61edfadb80e7c0e3d1d2dfa4b465ad2da3559e52293099ea1e79665fb941d2d84d1380bf478ba8af93b288937ce9dc92717675f42c8f869264b765bd2e58cf2bea85bb2842a70fcee652afb850367f10feb02afd3e88809e71ac329675887977494458f970fe32fd940ffa5ef28e7b9eb6e53b2f73b151588c91d823757261e2805ffdc3bb451ad737273d5d92af3e9193c5394daafcc6a40bc7d3c3b72f47dce012c35c0e58a11c06d450de43c193a7714923e977f9c851570ce2f0370695ae100881d3b5f17fe2e33edd79c68289d06f62d45962753e6959cbca0fa96fcce76dd59110adfc4b768b5330457822954d487f0ce226ce8777aee6a9bb97250a8340f9f21d474eb63dd94ef9b0e976cbd2a6aedc939293c705fe2fb378f505ff1f66f1ee777d0816125e2b9674604a6b30fd0bbfdbe2d9077ece33c159bca55c769bcbb6ea255fe7616bf6d925f604a33e9eda52fe708ec2daa5574f7ccbf63a7bb027b0228834f7d08174f4a57ae88e2e697fca91070e7e55ad10e73b1221a6d3e740669f53faffeb6efeff9ec55bcadd700b02ef3aa3b721c25260cbd593eeb36c05445f1e9ba7612e76bdcff5a1f07989fe0fe7c338d0f7df3e8bc7099f2eb00314239ae72b5c55b4ebc9e7f5b3a3b61dea8a2475d4f3eab64cb73c310d9d6450351bf6fceeeb24297199f44ebedbf654df97373b559a17647cb153d0bd486fe376164f26d7c424c60ab26b115e8affebeaa50b67561504e7b7c46dbaa77c2c60eff5eda7e2a4eccea69700e280dfee7cbe57e7b2ce3dac5db1ebd6d5038f4dcb67864d9b15f00f8eee87fa0f3be4f550f720845eed5f0e6c435ddc02a7cff2c3a0a6077abb07b8a0307e2e81d3911e98c059333cfcfe0342c9a6148ddd7b523e5d9b0693a6d85f906a5494bab618cd76d3d187d675a765f2435f46e60d6cfb64105a26ea14b86acc9aa52e14675267bb34c2faf91287c734f57c044fd3aa1bebcdfa26272ab61f78adb170a345d040fdee60c2e14399e7126df8f80a1745369473a729d879e26859e91510b11add78d6f4c4ace3e1171af71bdf19aba4b6e1d9992b78f8ccb8b4241251a9b109c42463623a3e02c48513278a747dd374d37ff8f9fb9a5c4a4a4a3a50fd5c6c53c09502335752551f4d9f3891f4c5f1ba6fdf6c38932d515ab4fc4a2abc3355f54ce7c8f4d16f041ea37fb2bba69cf02370128c6eeeda0fdb2cf04293ed6aa503f63b62acacad3b62df38141c769f72a9c751f2f09f6c23a7fa27b01c8a238f0631ce65d8849335838e762f31541b02e98340ce4120bbfce1fc4a79d2c69767946b6f9e045f5d18526b31256bdba87c74af7559d4f1974365637ea67ca9f3387d63492e04de71ac217db47e2455c5f89dd3192d6554c918b31e4e72f48b9228bb2c33b625ebdb45a2427d54a83367381faa89dda6d23c113bf06acbb9e8f38864e8f2feb957ae1c540d998d468770287f86bb0e709ad108ebc85b735e107b6ede6b2a916c474d3ed84f83fa3fee6cec872d2651d51baceb61b4dd6ebc33feb213caa93cdb8df9bec975070d4eddb62e087bd1f67a8f0c0dba3efd1e87f255e33b618b2ab8343db40437b9071cf21c12a33c2f98bc64da995aaf68efa6daade5984881cae4ef991917ef145c440bc345398d1fb92c3fde87b2dcf0905a2a2d583079e5bc27f7df163dfe22d7216b6dfd78eef2196a683e390f120ff679eb17ab839487050bb73f146b5b37a3fb9e629de2dda7bc459754562f5859ac24dde5ddbbf009cc85e7c675832a5d095dde36d32d4d6fa6c47c290f8b92aa348f4f2a9b7e2fc63bfcebb6029da97987939e7dd27cd88483dad282cd4f9e6d91d9e8c16194f42f1cff341c8916ead5af8ba95f67eb2fb079342573dfccb2aa1de2ba3e85a8bea4d7eed394cfe973f2b6b82c3f2e1c2d62841eadbe115f5fa3b74ecf634074f7a7c5e2fdcf442c3c0cefc0765fcd212c4b55801c7f9e559751abbeae2def0aef856754ddd4456545e987cc578b25086ced505eaeded66485cdd4afdb7257a733286a86d92305e2b1af93356ae08776bc126ea5a87584efc3d71476a83ff2544b96f19a59f3629afc4acaf48e860f8d1a71a50ba4390c5afa76e74a59dd91d14f6160f2500da177bbcef805521f7339a2318088b1b97f95c2473db67f2a967ee89ef995ec26bc2c108560a5282e1b8364e6a2efb23f73d5ddc53c28c95d5d62d542aab25a3009d691c73fe98bfd8be7bdaccb55be26d0a0fe4f708c815d701e9428dbd46b7573788e19eafe4feec35b731fd769b465c0ce42c3f30bccc97267510be9511bb5e09ae3dd7eed56ff0734b618a80fc4352ee3c4197502afa4d5d43bcf9c1f5aaee3727cc8edc21b17d106a74dae3ee1fe976da8fd155ecbe11b128cfaac9cae76f2bb499f48b4d87723f52854e69c65d73bcb67c7a6946fb120626e55e4b61a6f733fd08381ce9cbd45ee1f5c8a85181e33ff3a656597ef6f537af4fe3a6838bfcfbaec53207ba7d5b897d34e367fafc2a1ca56b98fa5a901c2bb26cd6fcdcaff7078b1775fc7b8a1177509073cdb92d9a101fedf593f64968d079fef7ad4efdfe3e2b3117df8ddf1e6435537315be5ad7bcf1c0f4bfa38cf0316a4baecf3b62fb3eeacecdd3633e0fd3831e65f1619cc14ca6d7b77c74159ffd2d303d553bed7b6507231dccd23fcbb7ee82fd7ff071719009b404f4bdcfc8cc09989814909d18484f8583f9b14ff68cf28b790d4b00033331bb7990156be7e78f38cff1efeba4856593f47b8baba9b1b267fbe553c7f7bf281ed36086b88afe94eeb05655e0fe3672d0948766e356deafb7864b5fc62459f86ddcec1d18d1b7a7d54ab94b31f852e53e64e014d6431a6d8d3ab1681622d2a8db07e983f892a6a05a1faa8bbd6c37e59383b7145cdf1de374eb3179ce46c42138a885897a06f4bf1a6ad9539ea7a927baa64f89ad8eed2ddf1bd5d49d0b8e5ce85e285738e3b659bf4b547484ca4fe2fc4d87e61a18403f98d62dd0591e82f738aed9c46a66314aee31e366be60824e56465155b183f3071d59e48fdb0db9b49df858eaf78e7571d9ec7bb54a14f492a7f22e9135494b084d672e47885459e9de4280883a0e29dbe872a334fb21ee63ea7177a6b7cf7dbcc9c4b5f8ddb59644eee7d2f6fef9c9d9c5cb891e9139f9a85bee7a68dbc9704b68e99e82f6df4b44e70d35f547ee3cf128ff27efe5e172af55c78f689bac391eb946bbbca0485abc705d52f3b8d2ebda6097f75c2febad85f2bc6a08a2a7f5e50b2e09673d0b58ee2cc6d9704c61d565d3f23dec8a14e1153e9bbd0968ece40dac635a1a2a469e76dc4fdc765888f2e96e18cf7e78b4d09844fb950037edc4ecde5e5a27cee6f67d1c0443ac7fea5d2b9b565e638a9c080a7e71c3765f76f2dbc3891b1d344ca8b4df1bb69b1706c1d2d2693e526b9ed1684bbc0f67fffe5a2cc156f835dce5af1d62f1441a26ecc08ac17c2da4f4b369f489872e25650b0aa6f6161cb1afb99761e19e9905733ee46079a663ae6df349ba7e69067267744cc8643fbb85c7633c1e0c3ce90958f6b755dbb6c8fc7cd9d2173319872ece6986f5ea3da1be359913a7e8d8f04ff569c7c487f017c527c72f26f97e776e6ec3f3ab3f50797f60d6f62e74e01706a3f9fee4467b638b5f1e5289f89d4ff3b1bfdc76cfcea9722e0ed5b5b2e7518071dc2d33edac303109ed5109e5c08cf5448d5d947a1de83cf95278d31bee41187c03010e82e4895c083c62b6313b3180245f8f3af37ef3a207d33b4feebca26edaa0bea732c35fbcf8eda33bcefe5c36efc76d3276fd9459d57183191910ea7fefdc5a94d3a513f110c73c2dfefacf573818e5a8ad75871a12f471e8ddf5f90220864d6383193d56a2a930f5f1679893ff100f255cd7cb9c7b255fa3a55f52ad723949de1dd6bf8466beeff47721f4a0bf6b5d57a32c955c5ea18d65dda9faa9519162375f3a90caeb2bae9cf93fbf6737418279dc20696755067e32ff70efe50716272d3a799ed6a04a3cfbe46cfe8aee7a4d7c79b591d1bb556e7f7c41cbc34fe804e8fd3289ac429decd898c36b16f2feed41d7f5560fe4cf370e7b19ba17d32da9f28191ba7c63824f6f3990f8540d55697d5f369fd813d56bf6007b9f3647eadb7f7e563b8c837377328dd2feae466382c1459eeab2185acba77f46db24ef7b43adeb329ead55bf4065ecbaa4e24fc2982f8dceee4aa7bfcb95b7ce86b65a3d85caf6ca9ffeef0e7fff93d56a2010087cb80a3404c0c3185abf0a77ff0fc396973fcdce658674487dbf978fbb8bb7a047be3435252fd52fcd33350d61136b3d36707f9842404b8fec111cd441ccadf88d0fc833d56961636783cdedada34cc8e4030b5c0595998db104c896696167666b6784030b3c18799dbe1cd0856c0cc9268658e2758d9e16d2dcccdac6d715616042257ed1722a6c5da5bd999595afe01fe3d44f50a1ff691fb70f3a01def6ce101d12b396f8f72804f2f275b1ed91598f2fc62e5f2fd7c8b9dbc02030c17a9a896aeafe8596e8f483298a65c9128f0f8b5d374d5a3176ce43da6d8638288b4e6d2019920d405a767bbd63adf5498cb697e8b355b69fa6b6079b4c5929cb437ac9fd78c44952e59425523a4d5f36aedbe45ac814049af2737ac21379f39bf427532987bead57ca7eac2f7c79b30565f9c6edcb418973aed4428683ee9d0ac7275717e2aa2944a4fde333d26ed946342b94285b67fd575c097c6dd8802c661c4cde3cc18f5437f7bdcc831c408fffd90d07fe4e3cc5dc4e2f715107f4abdbcc18ce9e32ad53da782f3388dd895e77f0c5b5ff5eae641cacdeefdd5113d875f4cdf2dacbe7080f768db9c04a7b090136f16fa04c6263e939b2ae7ce6725f604fd055b09ad96f0fbb2cc8ae7ddd77bfb3e87b554f1c9a7363d2b0947dbd5b4ca68e8bc5bb11c21b140bdaf1557bc94825f25faa0ef69885560a44d44b227f980ebb4a2f5c159d9a1718b5678cedc5cb4ec7c2ee6b364d7ad7472629758adeb72f94aeb4390d5d4e0335f1b3e1d3927b16ea36170fcfae48b172d1e99a59cbfbdf09300da3fe6727db1985ef016ed67f999d6ea5738b5cfcc71fb96af5e7255daa6eaa858724beff92bf98f9659bcbdb765dd36b5fdb776c5985a5e8ccb78225ab2724e76a3e2c5ed940cbe36cd55d975599f3cb7ef7ea7ae4d10d85a1af84ce8a0b5e8c91dc78b6f6b57df5adcfe29668ec8d2dabaf6cbda5fc44bfd6cf63db59f1adfb43433567a53b89fb4f3b94752113bbebc80965ad6ddb9beae4a4070bdd76b45df58d7b288d372374e3549971f2a12b0be7c04a7d021d90877297616f6a39d68e77795ed7a56a4a4b9e2abe3427def577336dd09af733ca878a241bbb11f1a43a22acbe8d54309a8ab6b844636cefcea915d1754d4ee1b76d5a34ebe80c76d4edad53e1e331479c30438a50b6e17ad96ffe8e45ae2c2d3bafae3e6e8b0d8aca3ac4d4335bb270adeecd0b4994765fac356e55317ac9f2e7a4efe941cdbfb77e6b7111f2d587c52b354a7f9c5de1a73eda5c16c6bafa7ef6a53c2da9ee1eb7f99b5a88e7f81e881201b6feea63c3845f0981b903efdbc52e03f731cc1f98dc6c64a06985cdf7531670554e75cb5db1ee7521c71acaf100b3c1dacfda527e22a671fc77a9dbad26ca5e723f4ed43d18b8cb67328f17f5de5ff6a57f9cfad1408760df625a4ceb74285cf4f4b8df7880ece00ee2e3371b6a9565e5e3e21195e3eb1445458002a2a31300af93fe52adbe27161441b733b608eb7b40061c08ee12f875903608903449ca58d2d1ee0adcd8085998d8555988d8d1dde82686d63696e0d6cac2dedac6c2cb98b7dffede30810afbfacde6e2bae27f77ea0e9f9f3156ef4e80b87fc4cf7eb8b417d3fcf4a9f7a8ff0c67df7ba9a5858235d523e5462b66da74a8b8733d8118a14b3edd823a8d7f34ff63f4993a862f276349ed23c483d9417bd3085c265af713c39cbc2c2cad60a4730c39b86d9e189c01487c7db585b106c6d0916e6041cc1c28a606943b4b0300b33b52112ccf0b66136616661047302b0c5999b9be3389d5ce56c43c4d910cdac2d70006f6d8db3b109b331c78511f196a616789c951dd1342ccc8248b4336520218c08ac6df16680880756443c81886350d308eb174390a84226d95455b9d3f5023532718f7cb9734a0538304d804ba7969ff6bee26eeabde8caf9e2af6e8543b45694041ff926402e8b2a17f4b9731a92959156a36acc3657792723f9fd9cb7d3eec52fc90eee356e16599b7f043f91376a54ec38666b93b71e7b5b6c5d2282ec9d56ea7a9fbb30adf9d83b0a042b6685df3836b5700497a403590136fdfe1fcf5ad444ceec739b7a70475da7eea98f64c4a19b677107020ae5bf6e79a110d7531421781347ab7089cfaeddef45c61d4eaeb315da2153049e76ddbe3c7fd5ad66edbd9ce8cb87403e4178f68f7cb8ea8c9bc894f9fbab238e4c0de5e48870100cdec63142ebd72110c8bc9ffe990c95faa22bb4d1cc3db9470e131d0c5d0a7fb14ce090f370f451df0e02d907a19d879848422420905c48fc1fc81e16a3af6bf4c8f0947b4acef51d180864b2495624d87af0c5b736f6f65c2e6ac788947df059be6cec364b41cb37f2dcf3555af3d3a0c5b9ce21fce8e6d2d0fd92677e54f3b7b723739a06121fad7f3a9b66a3c6f6800e4160856346e4616110d84c0864cff811f941da5fe24f3c80a23d3e3eddf178f14d7911b97ba4c6d895ce95c905a309e23fab8e7425034ae933ba6d6127fbddedab53c2260dbeb7b323de90beb64658472ca5d35e5b99aed56b108697fb9b7e6c9d92b56cd5a7cf8d55fb158e0593e12f9c9de1a33d35202ab29de86a259ca1f4d550bdffc55cc9156275c6dab6df377dfc5d2d215077a9632e645bd7d85a625de2d3ad974e33bea29fee29874020fe575bbbba2152a7326cba49109ec71042588c7e0e3793591008444969f767cb950f6fab6bd7a5ed9b1fa9357ffdeb767c4fd1dcccdab934f8aaeaacd7339acfd4bea8a00abfdf126db8da63f63185f8392689f73fd7278642ee36db37d3b91caec0cda6fbb98d3b26626e381b8db3ac8d6ba3a58c266ebfadadb976257f678aaf9f6f4d47994cfe7a7982eab52b5756ee7da159b17cda9d6f27e73576948dd6cb8d7dabca4613a73e1ac705f89c32f07294be2afce485893d762f9f50372f11b0715f90e25fb1caf1482a7d6adafe6f6e62d6c14bfb42ca469bb3ea73e554354f55546edd34fc3d6c7775c09bad011eaa973a31a9522dcb834966ce9d25a61bd5ce77948da6d41b3bca46f3081a3f958d068d9e2a6bc1b2f50eeb37a8f3879fd19d6e54bfe49c133ced828dd516f8867388b9b60647fb6e978dd68b7da7ab4e46168b6784cdd865ec57bbbf90e8b9f99df8860f34f1eb6b2cdba2739eddbe9ec6a985b0aab0e5f76736bf5c31d985e7b6f65adee591e74ee81eed38465cb06193f82527a7c2d6aa3df88eadc74cd59fc32e866f3eea7b424bd091debc5db0534d15f6e4f5d99069d155faeb24458a792a9d4e56da1b1d37ead826fe71ce8c9339562b634dc2ea2ca405e4aa204137955df6e818efe8d708bd922a317983e3a5ee788fa8b9450dfd6fd3d1d79dfb56958da677fb68559f5abf247d4a3ff72c0865bc38668ef73ec78182dd77e48eee5ce37041ec9be79c812a2199f2dd51ab63e05775f9c48e1619451fb07e1f173cfb565ba4b15df28dbb29e155da86a9262216c7445d2c13851c163f144c9f79a37bdac375d42282fcf70d79c7cf57152a0aba859f15904d5ea11996a3d98baba2c5c1fcd38f7f5813d23a5d2fea4a6b5f66d9685abc0f5fc57bea70616dafefb416fe85f573c54af6bc4cc99fb1c17d7750f87d5dca2b91738255ae4fcc92fd5fba55ed304ded22284a4e2e409d73c8b768ac557ab4e3d5fa9877dbaa22bf9e6c922d322ddadfdfbe744160976451e3a9f592fbb3db8951a517b20f9d037d47cb46b32a7d1bffbe54c1aaaed434b80b8bcd095bf4a9f0b14fda34f3f2cb8aeb2348660fcf4ff7bc9cea50ebcba906c1beccb2d12c561f9e63bbabf25c5cb2a977ae84db4b4bba0bc9e51e4e7bdb3c60a7f1789b7cdda263e56b737d38bd9bafef76d96816b5ef7495ada0a9e8f6d915522e3ddaaa31b56573f7f7261cffaaf0d8f992f1f1cb47df251657096cd272c37c7e7d699efc7c7ec8cb3d3e5234ab3c7ff70c9e2b2b2ebcafd994feb60a1f0f3ecc9f932368f4d62a4b61d221dc1dfbdd93f779583cde92af71a94e23a19a2371555b059dbe779f14f57257d841bea0668f8b36a5152ddf9120dad601576b87a45757f924a9bedc554cd87233797fd6cbb3beca048d3d414e3eb61708c9af2edfbb3fc705d6778544e3fbd003a1f1f317a51e6aaae2ab18ebad1257de089e5bd51d3edaf05f2f6352d8ac84485848c996ce3c0e3663429382a3adcd182f7f588cfebbc68b79f0357ab3841a99d3eba4ff5c1c114e0f9fbc54d4d365eda96e9ceb47f1cb8b2f9ef6d95ee37f41756755577a9aa382def3f8ee4756f64dcf37f2af4ecddb796e227144da897ddb2cfdd35bb73b8a862284b6eff115e90ff9378ef87f248e0870569981c91e3e38a45f7ca6af851b8e90ec9512e2e591ea63671d6b994a0cc2476706795a2547f9e25cfea7e2881638739cad05016746c49bd9d9e12ccc8185a99d350004333b1b3b3c914000786b1b3b1cc1da8648b4b2b4b235b5b209b3b10cb3b3b5b50396e604ebffee38e2812de9e64bbd88be76cea1d511b7c3e7ee5a764af3d5f4d532ad5349f796352764aa7dde344bc33c1e9eb263565ce595a2775ba779de59e5707ad57c09fe888739f75b38c7115f91a8c2fa9554c585d23507f663a68c95515c217fcfd956626c23ac5fc29f44156c8ea06a449ad64bb853ea90e7b944011f770e060f87101c3f87795d210e281465a3ffc3fe906620922ceb19c6b73428b763edb5fd1fb95ce9cbf990caa8346936557d2bfebdefdc52c363f3f71b23a50ff3e9ddc269bfe152909d21ffd31f4e7bf220bfec97e9b220fa16f1afd008a979fc74931c3ce736b67dcc6330851ed7c0c33c25b992f412da4331ed17ebe66e881b58c493f1b8c2f4d9a9a787793b969e8b7f810c579dba655a43f129a91af51d8acb1ba3ef247c5c92207e74f5d747c71e997566b818ec106c51fa439cf24e381ad994e674f2edba6b1d011e98c25697e4a3ffe2fb7f0fdfade7972e2d5fa5f2947eba69d66279e4d565cba78e79f388de2df19eb3fe8e746ef0bd1167d2b02f60cb52d763def744b79bf6aeba9ab67dc56138287010bcfbecabf3e7c0180f227451ca55edbd27cf4adc7c73b7e56a5076a3a3f769af992deda2953be294ff10a7282bd7bfca2ce7179e6f201774ac5fb70ae228d4f22fbefff7f0bdfccd4cd9be8ec0d730c3c735e2953344daab9f8eb9be4aaf5a4bf1a321779e9a88474a52deb2779438f70dadda070b15dfe4bcf5c9408612248a50dd6a04e7c0b07d1bfc2a8d14ce9e4d2fb4fef490e2b2797ecee4a957663cbe791c2dd3d4eb95fba7f4f7456df317b797cdf1922bd1af962bdf104be86ea8fc4fe39bd35aa27ff1cd3dbe27dacf504f6a0237f8cebf1662f89e5f5eec44ae7c9927c1e250b10a6e5a6b6091adc18195e82daa0662ea61c1e8c36fbf6e92fc942efe180ebf503fbf97fca44f71f35e2b4a614474a6eb9fc2f7cb172e08b7ce4ccd9533bf214f1c7c22d2bf634defbff8fedfc377d08a693a3e1f6fcb09ec3b5f61fb85b063cafade27e3d6501b4ee506df2bb64f756aa2c6d1f5bef04cdfbdefbe33faf435d9b4b9294bc2bf2647dfd97cf56ae8dd4ac8f49b37efae5d7e54663dde40b45c2b3d4a758ef0039de2a8d60793a3e3b9f14f1a61fdfc2306d5fc7f6250cde579123c6cf57fcc7ed5597be5f68775d15d794584e71883c987d7ffeb34fdef095d4becc2bca3c7de697a9e2357be7e2dea94f972d798fdac57ebd10911d9c595d314bc33e0812709637cb2f7b2a3fffdf5ef531b3e465f574bfd16ed7ccfeceb97a34ab6d559654171b8cf579b94bf85fa9af8c52c4ddc78e7d6fe1d45620f1b3fbedd64fea70605f7c0396a4c0632413b3b2854d2554dfdbbf18d43ffe2fb7f0fdfe2d70bef5591c4f0e597d69ac985983bbca51822ffe6a0206b950731d3f7753bd2d9fd72a50256b7fed9bd1906e1371695dffb60de297d316481184f4a74c65d2795d5cda1a1522907339fb77a1a7cbce5511dd0e9bebd7cea5c25ae8c4a9f2f49802a2e1d7bc8fdf9ab124e78e5740bcd974658df6c126dbe5519847575f6af47cc3c5fb7d3f4a2614f3a7fe1726dbe2b6b745e668a8e8540a84c7979045f0183ca8e3ccd6e3f81170b765217065ca016c574d3a0fe4f641afba1cf4954d5678beaa1bdc6366459eeac19fb11d7cb6ecd5a35a342e6d0d4dd918af05cd28bf373e9d72792cee933e7292ab57cddd129a80b79054795fa8535d7ac64bb6d7b42ab648c65a6241d1d27026e2cf5b2e3db22a389b4ff978fb6f2b857ddfd93cdcc1cdecf71952cacef32f35a765a48b1ace0a12647e9cd6cd71b4f683b31cf62dae184f84f5d8b8fcd5f8d287faea7eda5adc067221355a0edb94bc460b5e8e3ea19f312bf6b8e23b787ab135af5c6aedf631f6dab4ec338cda5dbd37b1eb21d6429e1ea4c85f753ffd24196b6df3f1fae0f77452c5fbec93bb46cfe178ac08ca3df9f759df8e212cc9b70c745e9c441293eda8348cd333b4265273ddbb8721c95f9cbb4b2fff1dc4d1e8a57ae5e5d6769df7cfcfce2b59bf6d0b9733dff3dc8f22fd72f383d2e8a884f21127eb3fc886965628a9d192a213109671d10398390ea4df0748d4c8bf7b68b730970370fb0f60f48f40cb74a4ef022fa85fcf7f0d705dab7244bef3d7240fcd38a84a2a551b6010bdc34675f5332bee9b5f2bb44c383daa0c34153f78af2ed6a2f39c99352acb2ba0bbe6c55e857b3ddfd41ab62def9cf3ec371dab45ff40289aa2ca3552fdae4aed6bf82cbb11a5b3f0968db2cc9549c9c761fb1f364b3e91dd52fe7e25f8d4ceff6df10e2db642fb5a56a19798fe0ad6bd22fbad8968928c93cfdf6929cb4739a619bf36a8c8e9e65f90ab6892bc579af377c3d30a562e1d7e850fe376715c3b766068d4c4ff4b227464ef5f45a2bfde138ad3231f8034e5e8e2db09a436bddbd5eba2f696788db229ec6536e3d996c13e0ef5e5db8b53157789ee9b7454b37dde0a7ecad3c6c3c327d777351a2c8c7050b15bcb3ce9f5fef7b4be3a519db58f75e056d667afe93258f5e3e1114aade71e893ec1237b6c00072fee5a8cf7934de3dc117f883ed325e5411d98e1c092afc9c5564555afee66232f6549b7d6064c9a428365d093d575cfd72a11044f8be3aa6f7a36b7f610ddbd1e99f717587b392219a0da76e3d5ba1b06da1fcfbfd8523d3af5fd95121de294cfa7a4366f7d962c245a71e53b603338de5971cb9d19345da7eb77473dfc3a02afddefdb123d39f48ac5ab32dd19a6f7beba5dc677242569d07cfb39d48d9f0c5a717f6f0d4b9cf31198181be0317ef1035d96ed43faabbcb6feef4011eaa5da66063dead2fe9ae9a6cfab4f01add08d7f1ec9c6ea77c6492677ee38df64cb60b46523564dfeb202b883a7bf4d6dc13ecdeafac51cba64f370587de5fb5d62264f601179b02d98b9d91355bd9f05d5629b4957c17ba5bc7aaaf317fe1bba6bbf6166cb7136eb8e57651e8dbba60edf7fe5ba5f51a93a78a3ab16d4c9ad4e05d641bea16b04e525cd8fb84360975e522db040c2c1437f9fe9d8180f46b9148876ddbc54fbe9dc7b66239f13356dc7a7dbbfac57d1927b379ef19ab2675f58f4ca7d6c4165f14ae0fe981e81c145eb90bddacbe8deda0c305069debeeecfebc5da51dbd557199ec29c2d746b65b109c7417cf890c39bb5a9a87b76660aadaba4f898b5f8c4c6f93db107dc268d19247f3ef112e88be377c2c59b89dcd951b306879b844d27557130fbfc69af81dda9b67948c4c1783589daed9b9725b7ad1e659eb491ba73e93dcc2265fab7a24e38bef3cef8468be2da3ac9b9a1632e9c0ed91e96bba5b0ef2b4fa2c5af7f94081ee2c0bc149b21fe7b205869b6b7b6ef76e71288b49edb7cacd5cb941cc886d2cfb61c174180416adbffe4bdea16d17f675376d2e64b3b7bc07dbdfd9c36e78bc9d9afe267492e2f444bb0dcf46a627bcde7ac237e2e0f7f6256e0efd6f2ca4ee7b1f673b8aefbb8ca5447d57ed13858447e78e2e9da47cbfa0ef2c5bff38566ac02f5ccb74754c47f4571d7ca1e8bd507764fa541ed1f67611a851b9ffec1739076858ffab166ce76ed695bcc255e7acd973f4c29d957653bcda9edefcc4766d410a59ef7a497bb34cfc15abcf777b9fbe69a37e620b1c0a6fab6d8f5eb0fbfae7a88849a591629532f7ebd9ae1d98f769c3d1efa5ea062510fb872657dc059afb2ad86eee75960f9a42907b6df15266ba89e20763e7c8d07a367c15de407ff8b65e61959d56b7968da09ebcad5134dbce891b97e735db77df3ca2e2b06c9dbcfe913aacc25936ef5a4575e3daa4384ab4c2940ea776a79e4eed6bf73247a65f7dd9f3257476df42b5f767977aa9fab7dd0bd0f3627bffe60fbce8673df4cac604b96987528c664d2e64f33732340360dab7e5b6cd53f7d7b8c16b348dc776df9a91e915adbbe560a5c97147b5491f6f9b108567f24e7a3e32bd7ecbab4de1a1b27a37e5e817ef06d4f352bff4b29d7bdabbf974ca56176758554a63eb72914a8de9091e5923d3674ed51ba8e8a26e3c445f29a41934273e796968d9c8f4fc3d530bcb555bf8be987da9406c8bfa60894b4e1c999ea3f4fecea46f1a298e464fe714f2e62d9c1b3095cdbe3d6d3b48766dbfa2e8d1f630dcf3dd03ad6f72829123d3e30fa9b5c5bcd9acb0285ffa2be648c7cdfef6709391e9971f5f4f7cfd6869084c8ccfde2dbd4f4a37fa20db3d0d03ba5985b7e2f2458f174a94f609bc0d352ad65f38325d2e1e8ee3d77919927862d772d917c04677eab3d523d3112e6a922fb08d59d3a6c92d3e94728cd7575a9fed6a921cdaba9c351fd7b81cec74d8ec281c175169bd896d9f9214feccc637a7f7bc0dba2c443c28bb2f62aec21bb695278ddf2c361edc36e98eed9b03f785563e7fbd34e109dbfbb3b62cbbe61c672cb07a46ab93efadfacae9e1fe6ce5771e3fac58397f8a205dfb3954ca4b6281b1fa5436fbeb56ab15159876ace7ebc10fb30a162cbdca67bc8b6d8fa56af2fc2bc2e7de8706d75688247bab6edee2fe94bdff88a670b75ad5c752d5a735dfdf4a2b9bb5d39b6d40dcf4b9107fb73cd6e146f39e580fd3b7c481f41d6cb134bc6f4baf88366c9a1f0e051793ea3cf93ad785ed96d2f458fae90327b473ed3fc2e707752dbba9fb298fedacd92d7097f9942c74d9c11e4f9cff8582ab8d042adb50cfed50b36b338cb8dffe7194cf866c1fe92599816c1b07ab64d16714a10d4e9967ec6e877f733f577c620a5b8cf459faed38dbcfd20157fd046b4f9fe8c6605b8cd9f8afaaf9f973e92d5297e1fb940582899ed51d19efd8fc1f0246fe88737c817865d8f9b4835bdbefd44716b1f9071b97b56ce814b08a6f30ed489bfde41eba34588f0d9f7117456f08fb79592c31b35ebc6dc59b53dbfd9cd8fca318fd9055bb2d57a043c52ff45c7d82b8d0e2f3e5dac8f4b3678eec96b9bdef48c79625f6473224bbe12a30b69b37aab4b527cfaedd446cbc269d5074b01a53e2dcca761d65e1d4552d364fa5a652366db6badfb143a8b5ee085bfb9350749f986341ba0b32e6a5f3f4d7fbf8aed261f39fc80d9b420bf905d62c322df7e6fb723169cee606b673ab3fdce3975505ab06e687943bb67cb9bac157e0359b7ede790cb9f6a4fbcac5c42bf3d26e262ff79e752d9ecdbe675bc077753e09689a54334fee00757749f0333d36fbbe5b3570b3e857f3b57e41c65730affcb5a0551fd8f4af7bcd3c58abdea6c06be6c8ba596e470272a7e9b345a79af5f164e8ebea1bc7dc5697cf53d6c06e1668623ba6f1b4788ea7e15db112a441e7fa6b028dbbaccf5f7760f3cf1e770b2826b8e1a3854e97348b2bc96d6dfdc036def6d69cf7e2f6359e3bae9b27ad3e054931b3ec966593af2c0971de7bdec2c2fa1bb1fe1bdb2d7bdee5f8b2f9bfe22adf8e6a474d956b4a06979f543caf5cc5bbe2e35f1cce5c30a8d0f59a34c75481aeb7869fffd04d311549b61be2ea5acf9e0a1345a8bb28eebb700ab228b004bd96cd5c2b3d4dfd423c72af7a738b0b7dfdf669a4f27d1d6ceed4ddbab4f697cd0a1efef146614d6f8d5799bd3c11c016ccbbe2b249e1448c46b3febc4707f7d5dd201fbfc1c6ce8ed90e5a4f451b176f5f21f1b0fe0a4ae9597f375bfd391f4ed043328f596c7e29734bde36aa7a40d981eda6a65653874aad34f2717193ae1b2127db349d166e602b3f0f91319798d8f129745d7ae5cec6a774e5fbaa5a6ce664579647bc90d8d59ad31ef077970e20707733d9eed46c98d5977df545846421163b29b37f52d403cc45b663c216ccb15d23aed7b18b5efa6af9ea1d7d987d1f6bd886833133c80167fb1c5a947a1a14d66daf2889ef0a610b0666b506bb7926bf934821fb690b3e35cb0f57ae60e3cf8b53e58d15170b057957746a08dd0df836c9be812d3ef45864d5a6a389a9c202ab6c29d868239e49445db673ac3a179a539ae46fbd100f3af980f88c5f7f5153a4cac8f4f2c0f2ddfefb4ed8544e9a1fe6d0565a2df92283cd9d38b12d63f5c21af7a467b2027233845e69182e6e6733b7be4dcdd46f21d70a4e3aae1488d20c877f498860db68fa2eeab05feb8687472eb51172662cc49ca5d7a94d679bf28ada389300c90d6d7fdb135417b22dec8b97089b3bee3f7789904968c57229b057becbb5a775172991cd9d5698d7b05fe4d1c298f4a2dd9075ef3bc342e2b467b199a3a5fa4d3daf8cd41ce7e4256c8d3cd6db5bedcd361cca3488dae174d9f884f3a634adb579ee4ba71054edd9ca7b95ff3ff6be02aacb76d997ee4694eeeeee6ee9ee6e50babb435a4250a4a405e9169494140404a4431001a5a441b8eb9cbdf7393c7bdf4ff6f7a1fb9eb3eff75fcbe582e17dff6fcccc33cfcc6f7e33c6cb402cf11c9f2a2d1ad22e1c0fa309708785935d832f695d1dba60b962c5de1bf9a272fa022376e8905f5a7661f055253d2f8c37bce5efdc74bf161804db8b6f9e787899da6e94b82fea5eb44a0e69ffe602d08f9347625a614b2328553a446fe1e7df51709f6203cb0d52279edda9953bb2d0919ebde63d5997334a8039b3c2f19004ee42a399bd531ea1223f967ba7860dd02f873713cfdca351d2943e3d576f8ddd1d2d635005d211e5f8432be7b51983a4bbb2dc69a664423cc65200ce25cd4a381a3f9792c428b7af4ea5982ccbca968bfcaa9cad4b05229eb83a8ef563e291d3b382c0fd3e4bc05d37d98f9d3e3199336633984d46311aac7e33850de0c4ca902d9a820362841e094d4b5dfa8ccc1faee401cf1f3b82a025dbb2603467680b52df6314b2c29517c051baa812a23ca3535452379f7add597aef5d511f179079de61ddadedf7377f6a7ee1fc92b018866f74ec40e1aa1c9dc7cf5e837c66847b4fae79f5f1989e4d9c02e03f3f731478852cd1866df5d8c0ebe3de3b8c62e11c04b643c6c803c342d6f0cb5ffb3ae5a2690d42e20e80a1f24e8270bea5bedf7ddf89e640117f2cc7984cef01b60b64879f37d71b29a40664482c968b9e6e8e89707001e1826b5a024ded4ba1a04e1868c16969f8faf570a091bc9fab76a2904ff0b97f862f11bbc5a87e5da700f0fc368bc858ccb3a23e49c147761919b7370d1abb1c5d957b989de124244ce0b9b77ad0f8391cb9302f1800e19a2a5ae60e6223c97328de6f944f39a674c75e8c00ccf4b73a50d6058e383c5e45273e89799752bae0f005e0a5268959837c2862156af8402de1f5c46b8da3c94d80dbdf1599fc9314ea8bc1a83b5ae3f5b8bea80c2a8ec076f9429dda7da365dbba65fcaef28644baced17d09201de6ac10b16c0a81031df5959a66ccf839cc94d63320dde2141a354ae69354343fe6f2dad4346eb02c2e1908775f406b9bb14840cfa5abd6d205ed98cceb413002f6dd80882fe3f7f483021db7f55cd9b3e5b721a9d10ec0fb4168ce134b6fd76c497bf6ca53cdb6434f066f04f05fbd6abe3ef2bceef20ac8f0b530ef05dec63f0586c6dd4ede98c37c9a01dbb016d2ed55bb3faacde905bc1ffd4be54e5eb6594c04ba24960b5207e1baa7a2006bf4d2a7781278f63cfa5544a9dddc466d1b970864603bdf0e9349ac30ae21c448c698e0e7abfc549cc6230fb06f755b830c4503d934bc6ef2a15725bb08c7d24065683e0b76693b63e1fe913eb7d419e4a10887950110ee1a7dfeb228b6edddfc0d65765ad4d41c42a14508585f1bdbbf4c882b7e91b6799dfb21e9fd26b1debd516090f850f170bd6d886d2bab476673f640c47acc56d08bab72edd03d428c77045eb01f02ec67e1ce4b30543a80f53f518d98a0a1de83618532b0b42694b83ae5b92aa07f2927c8b2dcb73713858fd3dfabac86cf23597d03d687e0f6d55d52341c3436db594b4dc31e3b57aa97c0f5bfffe822a5d7d2e234d1fa6e6fde12bbca99ad441d08473f402506f708a9426ea5592709f3597328e603a560c36a9ae67788df32fb75fa21f53915554edfef03dbb940c23546bcd7bda1e64677f3a2159173544ea83a01fb8adcc3729f08aa4fff8c2dfec835c4a1d6a60b48c7cc6c999f417990a7b14d6ebf718cc7ad9e7b75117855fecc482881f52d9f9ac3639770cf6282e14feae73157e535bb849ece4c4deeba311e1b64730f2db53e3600f61f91d7454ff12d77cea98183cc77fa38dba3211958bf9394be7b65c4d5a0d4ea481ccdef9c40eb87f301743155df1f98144b9b9b26d95b07df8dfd6c6ebc38007c3fd45e0cdbe867ca26cb3a12eab6f5de9eaac343c07fbf2c9e30aca2403072f01e08e1d752f96c98fe1c48373c8c3bb3cdebc3c27179bccbcfb7aab2db8c7a09c43fda12be83b277127d75a83b096b32063ec47f0e8d05e2dba781cdd6fc5f3e8c617f67e708343e1f0e4f002a2db9c3dfbacc04565f0616a94255887da0bf989704ecc313cf5d400b7adf513f72a915d67131bcbd9a1860979badb16cf2f4e5555209466bcf7cd22f9ef4051bc0532ad9365b8beade823126e4bd177747fe6be66c2a906ebe9532d86898714477fad45eba2aaec85db1b504f00f6eb8e3814f7c0dbc44eb85a86b88105364b1fb8580741af4de99404f2cdf063dc1cea3d40f8a0c0a04c0764bda93b738a1c7097b2c32fe93eeab7dbfef01e0181e9e8f0fd966bf6493b47db485c98a93896521ef22f8d7f5f33291690f57cb4994e7cdc748f1e743953e947c01cbfbaca5689a1a57bb7830e8c4fa76035232c48060bebb11515a578df9ef29bc6a3ceaa0937386b683e26b2f0ce496cea036b417f89f5d78fdb39ff7a7155ef5d82d2d4c796cd975adf4d435ece56dbd1d5ce5ad34eddd3864d4e4645cddb82c741cf9acec2c7ccc7d34157f22a2ea863bff9fd0cfcbcdc667ca6ecac7c1cbc76dc5cbc3cd636acac3c1cec5cbcdcdc16bc5c6cace6d6669cec36a6ec1ca6e6a6561c163c96169c163656ac5c6c9c3c5c366ce677eb38922bfbc9f172226d6c24136410fc909a32a897108024f5986a84c276380b607e9c0e9022d80b8cdca593a2993920bf2e00dd5da57c8776d89fe495df99ee6daf0ebb023a11ed78f78fd97b9bec602d2ed92cbbb812da61c2285f90db6c45e165e35c9c119fbb75c250809616bf2caf80b97e8f2bf69c0877c73dbd789fd3daeaf39d4b417d91f8f68b01da6a107f6b21aaf6bdce37f93ebfb039c1648bf25409cb617bc88fcd7fa4923063aa7dbef3134231d77e18321ba655f74e6a7ffcb5c9f9904bb8ab682ad988c99331b9fbbba37bbb49bbab7ab35af92b6a7b59aa9ac92bd92bbbb2b8fa324afd93ded7f2bd7676a69ce63c669ca67696665c661cec767c5cac9c1ca6bcec3cac1cbcacbc5cbce6bce6d6965c6c9cb6e65c9c9cb6bc161c569cac7c76dc5c9cec6cecacdc6c377b3a5eb3f5c1f2f2717c7af727d3d0f9a35ccf2d690d86523c318eedd31dd1d91405013881da0871cbd6341eb9b258d4aa897a190375436a36690106594f2b08992950b39a000ad8a46533c8075f77ad7371a49737b275729f4f06f9e2fe89a81b32f7c156ed3c02f56d71d7e9f193b568f366e46bd9603be0dea0472372814994e20940803a10972e585731c503f96f1ba83be2b1b5abc380b8dfd117ab6be76fa7df235e7cc49f71d6bf3681a2bc8fd28555cc3727416c5955976ebfbfd07f82f2248cda94a6984a963ef09a1388688154f61d854dd31799a9bc03569c7af870597603de53832cac47b34c3712429583163bcc08cd27f04cd89e993f5cd8af675247bc11312ce6b340312a6bfb4aba32d32f66bd0c9f6a5f4049b8c2db5cf0743ec3eab957a51a61cf24dc51eaa85c01f9dc1ea3eed7543776f4afe2f26a6fa80cebaaa4a54184b07476d7b8ccf96a29ab8830ad256a345271e0751b95b462ee5bd0b3ae7d6d6dbd699cf5f095408e99f05760b128e5442c00d6028e714dc0c4b7d82a51a148a841b174ad162dc84f5331a74e06ee630a0af59b0e0ae79e088d7683aca350e0d1db8bf8d5331df2a2a4b19b9cc6a6db6650451a11591e81b3adc3f1b287ef9e7f7375044d5c3d1b2859ca07925dd56dce9fa4abc9dd1fd9b1177740f05cc83cf3733fa11cbba61cf3b4f956d2a207c5c97a521a0bebe8b851ecc095637f324fef0f084845dce149e43f396c69222297f1155493eaab4cdf783af902267eff67c1ad8b5357f56839079b82039c16ecb707b2a4bd87cb4b7e127dd2ebf7fb57effd975faebf51bf26947f24cd443b57bac235bf6106b19298a2c9d3f3c43914ada4df45b30ff05bc0b2e14a51e3c091e692a8b49b79f9dcb07bac15df7c2182df8e1ae13ca0c87c93bcaed3d709a8cd61e30b8b393b15d980fa5a8e56a14479adee464f6ff2cfd965c4c175f2e105e65d196344fb295276fc67b05f9a77efffbe977a4f76c3fde3eae69519697643c1f83170f71c4c28fbf02cae726faed8799f4f242aca9bb40d9952653365322bb3931e7c967461f1ed859e5d4ed7446ce2ae2cfb8054f97dbe80edc0769f0f84b7c3b96dae58c397b9783ec9537214d7f166b806f5c15766175463bd28b4a454ed99749629e44cd7fc627ff7efa2d4dce32f6f912d6ea88cb631c8bc0197b5d3dfab7e370e24e9518c21b25fbc4d43c3c909f311b90edd1845977af12702077eedc6e9c1e9f96ae735e6cfb60997837b5bb7d7daaf351f98b5a5e6d5c9215c35c7972214fed8f6fdd31785e2fe952fe2cffcd9e8ee827de1afd1c5aff4e7fbc05c732815b1aee9ffafdefa7df0aaa3118b80fa43f30b11192a032482738d6621cffd6c19a5c255e089f6e167fe72e84fac2b9f8a918f504432a9c3dcba6cd694da3bbf7015d43ca0e87f4ed9367723d2b2e2d66ccd0060fcc5ff5a5600d626ca10cd733b4e711ed1008257fd6b3fc59fefbf68b1d2fee37a31f9402122712deb3cac9f04f27fea9dfff7efa8d5ab337d4e8eae7d8984503717826f6dd7ea4b0fdb70ea67b8635c6b4f9ee46fa3d20cbb6a4cbc76dfe928756066957c963f26e9b6b050a8fcd42d8791f172c3d8147da3425eae50b2d1bb6fb34e4ba3ec49e05c89facad9252b00a42f91f549aeadc90f5e5a753a9c2dc2c0b0773cdf9e1af79a1c880516158b289d1bd7d354bd069a039d1217b4aa8f88af9d71a2d34ccdf1b6dddd94573baac95f2487e66c73395a9ba113a65fa5f6cb490ffbb8d16ea3a26081831f96f0faefee63a3e83dfbfb183dcbf891e59dde7fded59e59088720ce576543fce6dfa29fec2072c4694c84ec6482a401d87826051b13f7bea8cb74f89a0ac3444400dcd88086db78174eb9997ec269eef29a7a8de9ba82a6e3c5b17ce75b63c8932d52dd817e376b77f56d05843c2363a56a22b70af8f8086afe8f36cd60377953fedf3dfcf3eafb3bfdf3fb507b4cfdfab4756f7797fbccee489fcb61ca95a64c1c08de057dae722ae1c8bc4700cbb530c678bde1dfff4a738f9f0452aa3facf930c16df10caa442a91b4b8fa44232f76926c9f3bd25a451ea3b8ec81fcaa01e500cc17c53a415f4b3ec73ef43ad6727aeaf58a08a70ba5cd801c588e1edcd3fedf3ff3ffbfcfd1b0f1a8c9be891d57d5e9a1f9ebe5256feb744f409cd9c0c309696bfd23e8dee40e49aecb33698a53dd78f3df13117ef61d1d2ea525471640c7ebdeaf256d54f11f5b1c1f283ea70f91986cb89d865983a05ba8a3892c49650ab8c431f11a99f659fab46b7d4a05ea9ec0d76bd7b2d203515d1cd31fce94ffbfcd33e7faf7dfe5e3db2bacfcbfec3d3237dfc4dc40754ceaeb525ea2ce5afb44f3a8f26c7c83d2ae3684548dde84b4e084838d3685259d531af42a3e092b09932cad5e00114a895dc34195887fe85ea3c3c78bbc4ca881a6571245aaff7d2312e37dc544363fef7a6fa4f83f97f6130d719c4efdf108206d3f99124f8d963ee30da114dda52ac8f9e6baa485fff68c0f94fd13c16e4f7fe4a836958a270ae7f64f751949cfde593fcbc578c61d8fde7cfb24a3e73f74d2a89b4ce110bd1e9c159fb4ac4763fa48b4dfee6906382228e361bde281469c7b1c9cf4d44f0cf19c5dfe04b7f1ac5bfa3519065e9e0d0a88639c57d8e9710ca97343f41f7f4bfc6287e330b42095d4aabe1774d3230e16eeeaf348a69d6628ab07c7e13cf38bdefd89f7c6df1cd79dccede8eb4e951c0dad6d3f0850752af5657223f99f6271f0de05eb22c8b8cb7bc30a2a82cbd9b649dbb6bfce15ac2f9b6fea050f40d9db0ef7539c250c79541a1907244a10af2a28df9f6a544ddbf274edb2ee9bf37a0e20af9e8c47074b48158575d5fff3a32bcde9b5987b0421bd4480416ebd3555188d0f44741a1c50b2c7f05bbfef5a77f6467a5121a77854814a7f2a03371d7f730fb540e7797e93aa7f82ffa96e3aa08b43e9b0dab7ba16445998df9162b653fde7f40b8675ef3fcaef33e0ce013a685dd840eb0a0ebc43b402afb7a98d719641af59ff000c8e6cddfa0093d81d9fb5b9b05c953d65fd462a155d11155282b3bd26a66bb3b131eb258801147d4f0ade6039b19e4fc98070b6e456facefc359ea79ab5abeb61a8fc41fd05efe638b8578db765ac0dd510be7fc617ea625d6e9653305e26b5cde752ef1cfeeb25ffb4151333533f326d536bd7fdfd2ed0f1cffdf2d16da9a72ee1a627a6a1c8a4e5ef6aabc6a8e3ce2ba7696d2ae325ef66cee1adada966cea3e0a6ab61e92aa62ff5ed32259cdd8b839d9d8392d7978b84db92d7979adf838b9d8d858f9ccb9b8cd2cd9acb8394d394dd9d8d8b839f938d94d392db9d92cb9cc39d9adf8f82c38cdacd8ac6e74ffbfbcc502ad6f7d2a4dae2fc2895987e7de035e14974debacee8f5354edc792edf777f936084c0b05f7d1d71e4a39952be06d31971094c1bef5133b95c627c07d5dde4ca97cddc60b6a34128528b9a1b6f12f7cc6b49f1082da4ea047ffe60809499efe227e63b64073d3d305a9a6ba6a31b902fde66f318d5477c707e505c943ec0a342650c4f753910287ee8f33d8599e8b9e9ab3bcfa3d8e50583868b99dd62797abbcfa311c9fdaaaf6e383d2ffd98ef04f7ee39fd66b66c1aea66aef6d23e7ae6aefa1e1e1aeaba7778f4ffd1eb7bc87a999bab3359b9dab85ae98292f3b9faeaeb9cbff9ce72bd6f34655ec99a284a685f1521599c8bc1b9ba6c770559be8ca54618d48f522c12b2333d895e83750525be40cfb263818a42ea7a671e38cc8c8fd9fd68a5ec78a255d6be8c74e41a184c8e5fff1afd1a27b5ebce586aefbc7ae1fe924f6af8ccac8e5bf80826c8720ee898c56a35db29ee5207fc89dccf1e9e7c0fd7c3c44c3ee7b39f93090aee8f1a3c8c1705a2cfcd6abf2e6fb31a773e9abf4abb39cf1185f12849cf8dc01c6d2537c0baefa0fa603bd5047dd17c899e1c71f18807444843dd32de9366d46675cb5b18e61c1c891a7730d4060a38374cfc6155ad16d4f654e8aa54afcf331294081252297b4847fab1f639d8ee8c433f5c0126b7f06a0f891b2fbc0c1be41a1da9483c507ebeed40e9d800f50f010b64956400dd5de694b3ccc2075b42bcbdc5b5fba2a67947e94c79cccee87395e24cb09a7dc7f4e150fec0c6bcebf957daf7ceeba6bbc204e7f828df6dc5d039817e0cb40c1409856f555b98cd0928901bd81bda71da080d0fb70d0e3a6a6ec21bfa07b51085bf4d1c55943f6aa5c3aa4c1535da202f2e29d5b7870cc236a2ddbdb40d4c1a9a958ad7f28a2d57c40de790b1a15eed5374680a22664ac6158382aa1d48619b674aac592b24ab407d839b39eef89d604afe5bd241daac753a0381b905401285c6a9124adbcef0e98d5101aa3348ff13d3140a202c0937c6a47707575f81387632e6110a11a92513ed00078f89ec972bead8fa445614e8bcef959207ebd383bc0085ce46bb55a85f234afa2f78298ffdcd72e3c9a1b5821f9b1e737b66add2aa1b106c5e70e9fcccfae16031d831125b319273ad65c6722997caf9976e624de5701145bc26792c12d8f95939779e8688ef88d6b1e072601cd29dd16a9744f4c2239339f5a920a984023d8360f021476d14353f9adf77ceff324f004849e57ecc4d3a303f7bfc6b4c842311fcbc053de4dc5373c32909292ac71558e064d1ae8ceb4d82c3358fdb1393ac899e41819d83f6be16a8ccee3bd9f64befb042f337cf2a5bac62190f910c310c5ea917d838fb4685e818ac070979eba086064ad8beb3467519f21c07c7f9fadf7e1b34341515b80e2ef8190b84ed5bcd57d82780f4cea7eae3ae7e3daaeabf20a689d184ff4a016538ad75face2a50598bc6d01464b1f6a8a5971d1c1d4822c290c4e7a8f690ae71a407f3f40fbee70a4172346bc96598162e98895c1c9bf75553e5afaaead1dc736b6414fff32d6cbd2025b9004a03849151e51bacc6dd2a40c61fe9c78f2f2893f4108b0c243b5628c1d6b0ef33e33f3b016447bf90e9d9d1fd875563eca585d080ab1d73aabed68d140ac57982304ae4f4c3955707cc896cf138787e2f53ce64e46e668f055f9c35871d4ad2a1bc7505da5ed61c4cd4597553f80a2e9f5e1f38d73991034bd981913864f39fae308ae00fe05672a786f3873d2fa95d00e1aa4d1f4c35b35920023ac5b16329d0b24cd8b81fb19b81f3a3032b7911fcb5d95c3721ef3b16fc0d3b5d718cb0e50b71badf2de0218a333b3e79513dc463965c965e6f85b4a2d13d3d03cafcaf56f735caa3f58c3d7f289c34377675ab877be092cc6ba1cf625b77afa3f866e4d91e63a9084c2c6cf0014502aa6e8afee287bdc3ec8f9e8298948ff550c010e1885a86810fb5a6eee9281d5f89e0932a3182f2f1492d15539c75b71abec2a7bf387332fce7786e210488e88809c867ad8865d67038688756609594d20bff094460a10813e290e79b4eceef81ca7a681b3b1355f2d7f741ce8a69df3728615e96d9e8a10fea010d6714c3d4d69095008a2a0afbf86b913660289edc7c2708768ddc4e83310c2c27dda44547a37fb80de172eb54de844f03b192d90bd45c1b2ac280b41935b97cd4c89518f7c99ce7a02b441df0a12958f988c1ba1e2caae43be78e9d5ce400a5004ad3a4b8c88b466304f506ef03e2c642ca2a9e900f4ff0df40862e7ebf8451c9df9d0650fefe6c7397000a3f5b3f8fa56ec0a4dfee0a7b0e85bb036d98192d6c5402652a2bb69e120e1b8efe8712ecab97fbe28e124c0687adf53c8d778bc3562bc1527eb7bf66ccfb7325ac0fff8cc0815d3a9a1730a6abca6eb75cb922696607a7e556ef6e0de3ea20c370ab313f97d66ec7b038505e60005625697e3e6344f2a56364cbe7374418391470e087f69a5301dba18224d853e65739d7b6b618ebc5cba76551ec965c3bd28423bfc9d92c88255d39f36316816d04f07690f4e63aed73e45c18f5bc3d3c8f5377a9801fc9a0ad7b2fea3effef7f2c86c29f3452d10b745c681f7bb982c4422e9267bdc41157ab9638f14ef537c0e5034b63d3ecfdb7d52df1ab321e4134ba6d22e574d0dbc3ffbb60ece191425d109af8b4c47cba52244164a32407f9e08be600a72350a9aebb6b1ab45cf733f76077630c19c4a799edbefce9935b819905ed553287e3901deff1c6952e5e283fb025f19320808436cb7e2a31381fb6f30df93b8cb5346fa8e8fcde71953d9e314ac5433c03fcc1b9d70419411e1a830d3c7c167d43fde250628d850ee9013f9959fe160dc3ed8f418d0b15857850518d7b1372d5208eb3cf79827fad8f2432bcf2212f20046e1e71e998618e5a533bae84b0ef5419fef757fd6aabf2a27f252cb9f1bd91ba82c90494a6171c1d5099d02f00d79ccfde1456577e531743e958c20680f6f384f0013198615d59c159bd631e82abe25ceac503cff28ef080c276a13c793c180f8e220cc6cdeaeeca945f43e980088afc6df15c7a057eb17dfeaf2f81e177777f6bdf67be9abf2eae0dc898c731cd34fbba210ad0306073981ee807e5005c7196cd3222ed52e264d56da1ce65225e601148d2a76b76a2ef26b2f5e728e9c55c4d1aac83b97036c2a9cc75eaa2fbc317407d303dbca04215512e07281f886ee9bc42deb3166ee275d072999c31fa02547590146ff76950a1231d8527753a4f0cd37f919886dc8d90085d80a7b40e4c17c32fe570617e2864f1f60a6d12c00fb16dadbb03f3bc779417e017b3a32abbd44a1dc0ce0c79ec0aa2ce07ce5efc47238a8f03671c7577ba702e057d6105f856023975b396df141d46b9e92581409ba5f95bf24c184c5949fb7314fdd503f4074e1a6b9c420b92a778483a6944965b7cafa68f178d6fe8b2e615c22503c606bf13597a27ff39da2afc38636475c4fb86305649c7f2296e797ffb95857fe4e8343e839bc0a391435d4f67ff14ee433e3fbff369e14f3c87e4cca3fb00d6afc6f99f6bf1e16d606d535710d81c57f7ec8dfbb42a363dacfaf1fd6f2f7c4e4fd705f64cfcfd0067502a9fa5fe76c8294d454a12bbc61e6ed3a72074831098dbbca4ac6da77356425d5c4b48d95c434ee6a49196b28cb4b29fd0b328740669d5972c9ed685c040a2b847f10b30d1f733c561f80dd5b272618b8dcafd9d7d17aad5374a4b6897f69f3e5b75f5fa737b98ce4781bd4f07f3dd2b0478ffd0adba0c6233935664ca37320c290373b20c2911726842182fee3d755cf5a98bb21feeed73fada8a22e29cbf545e7ff5eba198c807a287380a3154abf501374025516144ab68fd004955f250123ff4b37f8d71fef78f507c1bb6697663ab3e715a3fcc1e23c0d139bcea4e6d71c9ff3e3526740de35977f5dee78058a42dde13a2d8376e69758d1b11b62e37f24045589c2d320c367acf9c0c3cc5218bfcceefdacff35c7235d2357fdeb43460e818070078815b438bb0a2edea5e37decb28f28b19c4b91f9e0200301e19e7df56f76cf6b07c67dad2742d3e875eb8baa963fbfa6f9f41353527e10a8e66c88d210cd88a4b97328acad10b6f9b0028cf841ce48df45945fc0ee9abea66f7fe5c227da923e3b3254838f5025855428c10ba1362e2a92e8a51240785d4aea4f75fd535d7fa2baeadb9be810386165ecf845849160f78aed97cfc857329fc03dfbac25bff6e580595aad677028c6c133642a32ff65cae71063eda375dd0d8a4d9e1ef7e22f314458d7a10b4ea0efff455da12da8587caf53378051de093fd5c9072ab12c26b1c6392b7eb51a831be1c793cd61d8795143be3cc08d13e92d94b927db55f83cd5d816feb1eba58ff7dbf9076ff5914851ae93dfacb20605542379e79db1947d247d2027bca50a8867d72fef62fd255c478c2c8168acd078a5eff08572e275d9eee0cac661503145723a84e3efbee7eb4d18f586f23ffac133da7a51e769e5f7db2e601bcec5d1f34783b32127f8a22d7e61fd80fc9394d4fddf166f33306fc95c1bd2b5ddac4806712b474792e4f60c7f707e9327fea5d0e3c0076389187ff48e4ea0628342c996689aa0c2fea7ad0e8aae2f1a183ae7b81f587d7db7537d2b0c32d76bf986abc335b82bc8eb48ecc29c246eaf5ef3370edf7ceaf80367829f24954b70c4ac2094683b3c6aad68c42ca626153dadbc18bb0efb75eb86c7ffe813f22429ab44f022e3844ed95ce942fcf644633bdd09b6ba2b41cb859feaf77011221b7f740168429723ae1e1e27b90351567942be57a855ebe81d5e3aba2b79d8f284f78f71abab989ee3f673c985d1eb68604709e63049633651fbd6dbc8463497302be3be1262be9c798a3037922ca842ede26b5b3900471fa78f49edc2d671fe50f31af70f75025d15148a156b194a633fd8049ddf17e4ce77b32d1494c9b52557c89b602a01734e31a2a1113b173dff76878d98c1551c02020ea90b0a021a01120e161e0a06f10f7d81c90da20934968eae9c9bc8596b4c5b7e2cdfb1b9899ccdc400f32672f6dcf3073792571e5cf7fc61f6c638ce98541f5021d338afedcb46f5753a5153563ce2dc5745d5e37db1a1bbcaff82f8fb3d88984fad77313cce347c336f391ff5053c1c75f48cbd4b9b76e764d4231491f0f15d099d7881611118697ca6eee6af7c0b3af653df6c65f6f491775a15974260e3aba959b7c75f4bcbd070bdb424d57cf6799e4de7ec590e8268b534d975d787194e2be1b704b998403cf34a1efd74a58ec37675039765de9956d7b8fdeb990def62abf6325bafd2332dca70285fb408ba23d22c8a6c836c81bcae31460e63b2007134eb2792d66f1047f574c861c260693abfaaa875629e6d52967af31eb7eca9346c087f1bb49961c6d2ce554a764f52df971b9efe3e0cdbc3ff983fb751d01ea04efedbb8ee5f83ebb33e52556a1c954d29905b45d2d4db5ee528f0f41ef3ca785d5be41effaee04d50fb6df1d004d8930b8597b239703a413f8060fd239c2589d52bdcd5c2d8c812db7625cbdb7661d5e37ce7662bf59fb8be5f7c7e2465799d3f8eeabb0a675191d494f372b770e7d2d273f2e1b097701797f411535355d596b3963375906357b05751e57294e7d1d67393ff79d7ff3f00d7676566c6c5c6c1c3ca6ac5c3cdc16369c5ce63c6c169616ac66bcacd6769c1ce69c1cacbc66acec7c961c5ce66c563696e66c5c965c96a61c66b61c969ce67c57ea3fbffe5acf18f20bee895500ac1dd217bdfafe72c9fb624d098472f30e3e314244469608e02afbe383ed918d32ce9daf2fae865018a68045cee635a44bccde538149db6b03b72d7f7871cab04fda7cfa3d3f06a945c930bdbb82a66c56d3f8c4e6f2d94e8e66e7b6ff05c299196c92aa5977e4c8e432b818892aa1a525758fc35d37ca54cb669ea9778ea9a0935c491bfe27904727f019ea759ab4f1c6e1df6cd9bdde07511c1446cf9f31240451ab6b58b717bfa545956898bf936d6f32e1e9101f5e0872ebde4f5340ef692cda4db193262b22395677b40bde1c990128fdb76016a3e66d0a2e6501c37db3280077ad9caa621d329ee22f7141e0bbf4e7169bf79032081bcc7e89f43c3ea59c9ad15c633cbf1782f561e13a8875a540ee7cfb89f62734e04687d76fb0cf1b0e212c0c3e01120c20868423225dfc7cd3024969ad8e2af900242f350e8b07e01515bbf8d2d41224954d1a67c7f9dabf2c11543f64243959ed510579bdb0424110d96188006480cfbad525da69147a33c7cff6d8e2bcafcc20d60e7473b36a037bc75469a8cc6385c7cc73b23299f0f58236cfac2da078331bfb5632a0e1e65e86d1e78a700789429045fcc84d9668d976edb288dd4cb3c4a50d8a3c08e28cc9d9f346f9a439fb29d09b6c1772be8d419c855a1122a0ef77f5749369a4031f7ff5c57afb30b0dd4cb4721b654ecf2b1b837c3606debb223266e51a8034584a25702ebec7232868d67932555885e4144535cc0c820c48f5371b65d8ddffd4f1b795fb2876894c1650378066a280fa2c4ee97a1ea46f8ad17660dc62ceb39c0c8391d2b7c4ea6b05bf3a9ddcdea4b6704b4eafbda403d78e0ae93d950c5cb3e6196dd3779cd083959fc2b4032405d6e4c06ab5325123d45a934c478f7ebaa421a809769a2d9087a501cec69204a4234cacbd98982f715488650fa775f3aacbd65ff9291ad5512af0813daae068cf42299c724fd289dc1179f71e74060dc983fab7901a837b3abddcde7e782095d38e35518a634fe7c3a6702c4f4f72bb228999d47697636eae4a933b58c2075ef0323f5d08ce25cdfc90f1823a7661786779be0f65966037815dee8d0a581f9da3730542ea9725b7634126f5f00f63708f324be7a54cfdf7b88f07622ae1e0cef1a1ae03c934c9f6937a38a4df7a34d1e753dd3a68aca1101de6fae5af298429350e0590dbc02fd3b760a419259c0fe61f3740a1f573bf2a66a148f79a450863129c80221d4ee4b77e5cdf90009e936c2d1de24c65bc35feee05f95dbae1792dd516273553bbb65a41501a3cb7374ebecaa5c1b7e9c51c4d9c0af02556fecf4b2c433d16c12d81d54969384575898e7b0d89722224c2fcb272ee602885d14c627c318f89525f692915ef773a57bb1fbfb013c444b88e94af04796ec5116f5e9d689cb3c9e7c3c60c26e9a2502149efaf11ac6db2db877ade135a2835380fd8a95160c36546b1ab12e5d16cc0a58d10832b101781b11e34d95eae99d9930926f451e4c1965c20af85857e5c7de9852301ec218b4f9af4687a24c6f4b5adf02f086bc914b6428bdc7d3f0b178539db37e7edea9b781919a6f1c9ecade421ba6b26df4cdf8cc96176ce28c0bd463bf7cbf77be488d44bd30a15fe3c995df9a8fe103f837c3499f3a7b39672c1f4d0a27e1a57c530e7c6a60a4a9685cc74bfceda66fec501e4209f5f8a82701df80f5e11eb66e309d0069e0a5f48e957a1b3375897608b0bb96e3bf6c8cd6b158f6532a68a97abfd05642e90dbc5f6ce688cbbe82667546126dc2bc14fe38cbfd27403d7a0957264d6253a17df0498e76d96c4a7731f71190cd78beff085bf8b2ab8cb20a610c1b8f9891e9fe3ed038905d6aab27e6c6f3ca9a6afdd881ecc24ef6cd298017cb418f8c53755028558017688f607798e01fce04f16af62f36fb88dedbd27af1c30c4cc2e27341840209e4264d374a180f63a6372162f6d94fb64ad32e4e00fdde985f348c8c743dc864d684256146ca1712206300ec7fa984f0d5636af8b5dbdcf75b5189e78c911081f7af29cecb3dc68a79c6a907794761b35e25cacd0ba86f43c3644a4db0a8bc0d8b7bd43c9018113300ff0c98a06d3e75fe01ab3490a69495f105711bfea1759b01f755f95317edf1fbba1b5f39b31dbcb5324402bf451a0223f17a28450de03658680a8f6c3f64a1673bba4e0402231b51d7030228e797898eac3ca05dccec17a2b5c481efa783d97331b21b4b1aef7c0f37e7aba237f9791cb0af7c8fea00f500328898ac1d2e47ef340552f46460a4ac5f55b53adb029a265932e56ab7f2235936c61160a4db2e1e8bb9735b8de641043a96c5ee876e78e93100cf355b63cb7227f6224798281227cab5eab0f6443fe2aa9cbb636fa5e60bc90802a229b104f4bb254fe75180a4b736eff4045d0b55fd516d45b4176efed63db84260100ae38581fb8a6116f2231a633c6c36aa7b8d05a24039398e4623a71e661f29d9d1e561f4c7b254e2a30f80ffbffd506fac981a651173249ef08d52909c72ea43e0faedd4e31ddca6bc5f36b10c703fef334033544d01f02a7033630f9e087c355b49d38dd1bb80a20a20cd07e29b4e78c62c6d35cc32a36a325cadf34767ebd9b5001e287d7238d9f602db6469d7cdafd72c2deeb5cc38b03e1636847d55bf6d13df60ac90dc51e20de9eed20c14f0b502e0b03e110e29ccd73c3930121ca098964e05b6282f3a97f3552055a7ce04b6e73f9ceccd06afa600f64bf59617057a8462e9ed6311911eb83db23c7c0560a45fc626e2709ac9e4aa696815fc426edd202bd10ee0dffae209bc36cef3fc66e6f4cf2c9b752fc47ce3a901ff54934bc9197678afb7f6a157d97e7b51536c0df07c2c310a03149dfb3e56b7fa3327359dd215d2aa01eb4724ba7640ab420eec733101a2cc7985dea2f56d83abf2bbba8c13c1fbe908a289c1f756de5aac3f7a7017585f3913d0c60f4ea19d2c64db20bfd93970d6dc2a02d6d7d8c9249b0b85f68f15985fe40dee627405966b02000872148cba6a726eb15431b4f9c52fbdb03b4862dfafca3f668d8d199ed0559b553b3ecec5502926e79604f0769c9e6da799f863c6e46f993032a7bc9ad82e1a8091988304631ee3ef76a61d236ace666b62b02ba73381e7db6da2d6bbee76d1f269e70c31fadc7b0a8710111879c8c046635c58cede67787b389a6c4c705685290b18a9cc9a25c83ae32921f30a19e325e61d28c3b952912940ff0e108292c8c845fabd38be8a7c31527fecf924eac7381c9b1a691d8e339722b4b7fbe18b2b3dad1196e75210d76eba2622a1a6b02ad25cc2fe0b39f2f5b13a7cdb09145350181cfa37de26284a3fbe11d69bed9eaf2d067c828080c0f8ebff7ffbf7b7cf7ffc1e98361c292a107ef8edf65af3c36692c2ce33fff7889a32643b25ad53dc775296df75a840ed85a3b2e4fa6397183ef4507fd7378510becd0c01d1a8bab58085486d15bc8bf6be9d4178c7545812ba09a3385075e5e3588214099761069556f557369652724e5c6f6538613dddc130359f134e2f143829d2f205522a3e42f966f421ddc3c40b1c35c47145bc78417efe4ab3349c7eed7242c67e0c39cd2e26b9f259ec25fff442c7de7def4fc81c6def3f0a3ec79f70e87f049f48ea3f29f9c709c3e2a0ffe306202060813c957f992c2ae937f2d16d8c68dd7ccbc13ab764e7aebfcf62d31b68bca12247a9be7d3f652e99ee154964781a50537f93faa87961e4e3e0c074193e14dcdb4ace1cefe11f5d099a3202feff2dbf3611094d75d8ef342c16eaa7b9181a6d551006a9ba5ede067502a31a14caa7ad15aaae4bd804f333e845a16ea68bd0c0f93f20ceb3466c28ab7fe5f31e7a41b574e952679177c34cc99f33077ef9e77a4edfbf778ebb35e6cf4dfa16b952dba4bb483d31561ee18f3effe119e073496f922dc37180df3f8de89b1a283694fbf4113a2a5a2eadda1d61d003e6ee7656d1e4436f65f1cfb4fc1fade3861d89199e8b6771e61e2be818e007ecf769db9cd08efae793fea427f5bbeffbe6453449e7c8e6d41bd9d835cfbeed2fb8960dfa7f0ed702d0c0fd115c0b07d9b39d87a1e7eaf47e3c6b543eb50a2939e568b472d1dac72e3b922284fdf4d7c96f965906712dbf59c887540a84b0a473d43c3ea35d496fd479d7320d37a5593d2092f64b702d50373cfe870efaa31bdae40fe4546193e24f7e20c7ab3f4efa3175873b6eec8fc4b0b5657b374ac5a7213bffe8fa3a9bcc6fdfccbf5d8f6b091e3335e9c238b050bfaf9c584c11f7e9f5e91ed91fbd9fbf64d1e50d7e41165dee9281d7ae580ada352ab3971b82c7bc852f018044093cfa30e9161afb4187183d3d174793963928a01cc8824eeb12230726ddd5ddbb736ce8d26ef1a5b0142842730e66ba419f23fa5364ce7d3f9e4bc9a7fa3809745d521439e9b0d406ea37b6c926a3e597c7045927dcbb2a97518146cf1bd2acbddbabb4a2566735c98d900764d1b792b269709e4093f64475e98741208efafa4f0059285dfc34e6afd2bb0ca6f4beb1894edf8b5fe3ae0123030e6c7c79831f931609b1b1e4afd82c3493467601bbe0e8f7b31a6b887458709e42d64de10cb9da7921c02ebb5856a1f2b02979bfbab4031aaa7f31a8e0cd8135e0ee9cccbc4adc1a270b88bb8730215da95ebc154eb92a3f13c1dc09d699f0b7f8ae3d199b543018f51a02c892336625cee3ae1db13e929e41657925bc6d88ba0674d5f4efa612a1e06baba2b48c7b04a53ee789bafb06d8a50e3b23c69e2de64d23747fafd75338e4d5e83f35bc2abf3c8889ce125773dcc4a1cc2cd3d608c3cc1088bb2a4fe4261963e57fa616bbb8a6b76a84c925f77549fdaafc3bee0bda6ab87739df06662bd9101d2e6d4572812c59ddc7bd5e760bb376f5058ee2f5f4be1e0b6ffd79609dd66e5963b7622456b0f84c35eb595ba29a2d0a54119804090d7285ad8a62fdd613b15bf857a845d580aed1e45d97af340b18883591c6cbf3f1546a664fd080ae20d2a8577185d46ccafdc768feb4a323bcea69df010c464003ae0b67c3d11b3c97db270baf95d0f12df9802cb53585d9db53a2d00457e8231498161b1168f35300c3932b2ec8686f12115b5087c514f53db45e506f1ad8aa1d1a6cd6b5baf6e86a13ce9761bf7dac5b79f119a80234a79bf1233cda1aadaa3bf4cf1ed714486e2558bf2ac7fdde91dcfb24d5d07aacf0fb0407435eb6f79a3d90a5fef4cca89f7446f2f88357b0516733823eaed737204b19d570e15b65a444af3f43e0e2d3477972bf1ee87af1c11a41107e8b8191c2b42ee3686399d6178f0f64d90c2cc6d85566e23633fc840a961e4c4abccfe906b27caeca4c2f445bbca953f7c9233e085433206a22035592f704c754a3e13dab5cb3b2198f32596a8229f4802a044282197a6ca7d30794d72e9dc9c61aba4309db4016d2e4d2304289a62279e26981b62c3aac30762304c0aaf52c7eec16ce68d7dd2486e9dc2f64d6ef3acd83813db776612bf244cf5a412c53c703fc5e4a23e47a69a00a34fe4033667bd472ee3e52a654d5a869ab8b93483e90255a4a2e5fa57d89621977e6ddb69d48c2451d055c7f80cbb0d4b07911daf4dab368742c9d600cb215a0ebd34ef0046d963ae375394f3b13ea685c07e99c21d075ef1be4fd709356bd7750a80216654b3ebb84b701c8c2fb54b1c8059a55393511d7f56673a57d6584e807b2e02a9d7ec478e9301d4e30ee613885f8c97e394480ff3cb07f082750fdc5e624d3ecc460a5d00579d20dc8221b66258eeeb6115316e424571b242e990590de797b55fe4da07a90d7d24611772d0a1b7d27219a83a901c00c9a1c7c13e8b6db25a3a3f1d392a76c132469e506b254fb99421f2d432236279c5d1ff38b73df0b631b0230a7da5dfbefa7efaa5860873292aeb734c204d4e1005d676a15adfa16f4f4be5e58ded127996b3d5c47da40e8905366192c518c72d1f514d19d6b711ac9ccba0e58ba3d5ae196a34482bfc9be3916af55f154786d1908ecbef9cbfd18e010854bfc374369c46df70a842abd8054c7b09359f8c2182fb90e76b89816da739ca6116b6019dfb8ad9952aab123f94977061225a504e6f0ae3d08724fe2e46dd46e1d4580b4a964445667c03e6631be2aa771ea7a4cbda5969679b6d4835becde88570e035471239eba673fbfcb2d831ef5681a0f491926e8c208b05f85ae8a37724acfcc5b1a3966b664adc78b272980f5c3aad07be3146d66e9ddc5989bc1c6c3449e7213c07e6ec95959aa182a05e3f776a2d7f5857e0da3eb00aeafbd246f8657d37a5aea21aa95b7c68372abde39204b8e87c873f801a9aebd05a14dbd68cd789ec41f1e608587363f20a1ae70797aac4df4aa2a6565b0160a09903b66cec4f74514ec58ac58f8f8514761c4d24801d913987d1e8fbba4152a497e7e787d4ff8a6588821802ca4ca6ce0d673dc7a59d78656f8efd21b8ff372941000ff68ed460d6b219746b39589c27228acb2ca560ef85f6a3f47faa2b45cf476d7aacf9fdfad72c61b0f03f1819635b1d0f68c32b6e260604d19fe92f4e62b32e0fdf8123188d269518b1c1713983abd602a89794600f8bf5ce759658ade84f3578c85035d53c54b2c649880ff165f9a7e4e74719b76fabbdc097b87600cc6df8db8df6aec8bcd176d2d200dc82e8f7a091d4a54fdcaf7aa5c3a9822d76e1e3a66bd29860ec64ec3e929711c300b87c93283cba76cd38c457dee201f351d65aeadd21bb0efea37f96facca16598a38b393a498b7483e040219ae38866ed6b1d15485f87dbbb9fb32a54c2b1d4740153ec6d72a5dd8d2d333a1344a49fb9232fef9e416109f5eaa40b7bca5ffd81da7f68d8bcecb5c7bd4c819585f2238c2436c3e7a8c920a43e405ae71a1ca90de03e2cff84aff05cc60dea26508f387b282a88805046d807fb78931f826cbfe6af99bc43843c60cc9ec11920dd0c628656020be9d38ec5414e2aea81388526dcf3603f03a3941eabd940b31f894f3342dcb41286e0289f501e09f7c511ef70966d7210ce869c994bf8fe410279d9b01e23f2dc9c20f119429ce7235707c6c71a48243b1ae4095be4d4266665eca3957d925a10a49a2b9099f0418a5680b41164bd358fc9c8dca329ce589db87337e5c208bdd460fb34159862270bcefaa83b7a42b1047e406dcbf86b53331157477aa87afb56319ff83e98f99a94057f28b19effbf26f29df2d68102514771520769bd300e7ff520de166bb7fd910b8513653669799553733d37155be197d8054b4d50c7be7bef2f93c3fcd6d5a185500d74e1f1efa2ae2f0549ad080a7eac864788d32f93ba05f43915ae473778965e0b9d913975287d568c702f8ff0b6744286ff08f6d8e64c62d285a5e1be8f36ea435dff728ce93a0ba0e7fb49d419fec45fb6fe812717ed83f26e6b1d8e6eb0fdcb20e876c259eb9e78eadc93a2a95ff708b242c1f724dcfed2e7b6b9336aff55b973d1d038e98249134e1c63bbf7536d48d310e0dd41e22005bfab7cedb5f832d254db4c68d479116cbea5c34133f441b81a770c92f59a081e4723099d6372641ebd17165d8e160a9ce5ab8836ed5b5e3fd8364d23f604bfd1597ba036f2d7b65f0987914861631ab163b5bde6cb7fc27b6f4179f1f41d1d2cd54d1d4f5de1f3cfebfb1a55a3ab6527c3edea672f2ae62ee9e3e6e6ecebc521a8aaea6b29cea7276dcae9c5e165aceb6324af666f6f7ac7f62baf986298f9f802db5e0e0e3e662e7e0323565e36363e53037b3e26637e3e060e562b3e4e1e2e4b234b7e033e363b7e4633565e56467e5e0b630e5346737e33333e5e433e5e6e3e1bdd1fdff726ce9a94800d20314c56a5625b6f20cc3e726853ec139dffbe737fc25a6ba69eed6ed5a340cc063ea236ccf4d47a215ae3007533dc69bfcdc78a2b1e6ded68374eac74a786d99f33fbc2954bb5d28ddbcfa3fc7607079cda7ed7f1d5db3e2efcc9b3bdc346f7e5df3d50d9bb37e4ede5c646807c2818b29c4fdf9c7c50e51c56c9bc3a1c5b0ba573573bf246f0ef90bf3e6d7ad722fcd74a53b7ee8660ebe5dd31418fae339dd8422c637bac46b8e97603e0cfcd579734ed70088539e49d7f0421a16194ca6c365ddd8993f9c37ffcf7ed00dfaff3ffa410baeb9fc9fd10fdaf8da775c9cb2b394a05911e629cc85ae00ef3b777cf69eb6d8871062bb741369d78de345bae1f13ffa8448caf615b868b408eec0f6bf0bdd83c4b610ba47b734c847745b00c7788fb3994680f3ded9a54350faa90feb975ac3146e3786ddba7d1523e175dd6f394fb0482909f4362fc58772a210ee3e1def268457e2777f9156b4a1dee58695590d5fec4f51e0dd9e240bb96b74a17567a8343e2299bfd49478d621b081fbe276e48b71153f24827fa68c2341f10bca38d9cb2dd6cf20aca86ad2ef176caa662494f11a0165809afc938b5c0dd6f8ae6fd9065a670fa71c577aa2aecaa7ee2826eddfd3a0913371d4903f5c4b7460ad02c0a443c4bc0c189119e22a9b92ae1775393c0776c300b9dfec7d23f22cd1728859e5cb4beced879176c62098148bf63d3e827177f30031a213bb8a0b1b5af816c0ae6636f89925d0e3039baa66b4ef4bb79532912f1f000446e8ab37b4907c0877a81539578d2ac6e1aadc238088eab46426ab22ba76803bef36cce34076bfdb11e800b957f3f24b712911a4db5d58a977ec299bcbdf9c1b0a5e9573ebf22d48d83ff20f9a6e113a2bcef994eee5700ea45160bfc755edae0c09a230b1c69e6c4559417f055ef6106df2808eec3c44a2251c37d33dbc6454ed1ea0cc618dc34c0fb16f23bd5f727129c54f832b43e009906bf69e7fd9b8b40f081fddf2904d425941a931d107c267bb675a9589a3599124a8054b569d2a09d0242300b95949b4e12dc85aad7b7364f0a522721ca88d4dfc807cc2994a61d87202e26ca7c2a9376d723ec80e15e8768aac9f896af22e1de2987e6b76401c3c19a9a8018031f7bde54f199775ce52f9a7965eb00448d4f72301694e9177d0ef9fc8d562c6b03c29cc76a3b0c227f102c0847b35f6e3b2c7b70465ce24c651323c3afb9f3a01606cacda2a1d02128fc8195313b251f4ae4645b12ea0d920bdefa2435b9632716de3b86000574a1b014e0de009b9e7d55635251f9c67711e2bc8a3d958847f1b1bb83e6902a75eefe339293ee5612742692f33eaf74c40853a2ae6a2da371455ac133e63afcac8d543c54313001b921d730ea375b5402633688857307068bb4a9903e4bd459f091cbe3dee8a8ded77863069c1e0bbc89305d2b4e11131069bbc47cba7dc2f0c8731dee65796180115683d45daaccc64dda4d95318aeae59d677d67d48c0f9a132c9d3ea5b08d3394d5e7cc4a6ac0e72c702d99b1faa3d40e2c84392d7367a6858f074df2599b217281370beb09a511a09f732e99abe252427441bda2c0da44971fc453c8eb6e56eb3b3dd0d5b8d51749b226a05c8455f8c3013da375a1a470658e399d5bb446c423102e90e44046d8a0d6892f30ab6e5c67794b4e4acbc93c072d961ce0d81c0b04c83c5d05e1736906415cfa60a94219c710ce0be4a30230b28380ff6b722c048efe001694c3ba3c4c6cabb50b4f16a443aa60fda72d9cd88813431b9bee4a0750463f4d31aebf6693318da788840a08cb815e96ebcdfba887f7ff97425ae32430052d51b2047f5513e6fd64d9bcf7ae382e8a6c0425300456c0f44fbac7a21c1eda8f4ece2a251dda32e3a0729a28600989c54b8db55f8e54bc52f2444d9b519f38c25a5f940d492c8e9c02ff3ad72922d9f6887c0b46da071211628b3ea40e1509f25a0dc5577849d82596d74f624dd00fc9f04e9ec20ee0b1f2a631bb9a7fdd156b644a91840524383f6c9231c7e6e5f0d76310f411c228c7753b2db80fe0531457bcc4475c4ee893b1257e68e8baed802080fbcfd7e9ff55e2601f802db021b7717e74f229700f9ea87b6ce47f20d51ae78a50d7e7bf1f9e7e8a5a680bcfec87c63aeb1fd363ecd6cf777c1dc8f3e262a7857e57973db1a8921c3c53889d96e66629e91698598c052af3d17da2a90501f20f21979623a35935cd4e40228b333f93abd7fc32d292dab7a87436f1f2eba32ce2f0128437fdcc131f67a9c535eeccf7ba9ca7cb634a82502a459e9f9bf0e08b0bd4fb3b28ee65bc9ddcb7b0e0b94811f377fd5aabeb5591f59d5bcac1ab2a66a6d06864b0d3a8132d63a42b07181333d986e90af82ee4a00a82257d57af10b963ebccba14c486606eede47a8f1004246eac4105d5ed413b6410f724d6961e1a519322ef07ee937189c165fb9df2187e7a752c0177cbebef11628037ece35849ea63e3c19a072abe8dad533384ffa044cc4df257cf2465f47de2fdd819e1fbd9e7ea43a4a1f28137884f3d32b318e1ed1e8ef8d3029a6a83ed25c02e287976b0f4c723bd5d95fd2cf96566c18b02ed7a002e17231535b26aad4f24639a57979c25b6703a1773ea7004c22c7530887718aae9e8c536de9a9d8a7b2c75e6c57e549847c6eb7b96a4d5e85a2d6ae95e8f0f44fdd0907f4e392d17f464f5304234526a9b881819d749c080845632d682cd28f8de4ddcb33933b68b4d28f14107aaeca99eb91d4378f61b11d0b6431523885bf1a576e02fe6b7bf814a25bc555f3d5943ac98cc11b628129e43757e504fd08f45f9ebf3a78211192dcb45c370861d7f014f09f4224db2b5dbb7184dc59096e1db710457c62001800e3293b8b8628633c12e18e51f4e4b382ad0c34e0fc99678a77f10879119d695d44de7f5e5f13665206602ac9a2f6d8b4d410e9a27b8bbde973e5aa8a6c81c0faa0c04a187e917c316f448159cee250c21ca9960a90cbd755ce1ff3360ea785713d677651307ec3aa9a0934233a8534e1ccda1bbcd48a7a56df65e8aed21a3e9b05c45fea25be07c190a3e91466898d8628e4419355c04ecf7b84ae941c0722a99b246b555f516eb15af28929b0be433274b9c8713d7dfaac7c28e3798550b24b3ba03f70dec27eb06b2faa1eb951851ade854e7eee9c0dd84f9840e9675d3fa918c30e8e8980058a6defa11aa099e1405a2a7b6bff91862be6a242c11ca6dfe37141607d2b6ab05cb67b98b8fa6adab65132fe90e3352fdb7b0006a16fe2d11961e17cecfd587e7b22a35204470e9873f4bef290ac23a0ac5bcaac9406d7bfbfa978ec21d0ec93a9295e67f31aa14988235d34f041d52871551c403e9ccd703a9c7cbef096a8b7b22a608540f6edbc3c50e66380860f242b3d69cacde37a6f95da253de02f0b34fb24e01b10be31349e6d76d414de3be935bbe0c402f81a84041e77d80c2f383a5bf8db6fbf697653c4ed06602288eaf772b9bca37ad75da969fb8daa6565fba881660615336781447d8607c21ae7044eaa700ed9d3b701fbd4cf7a5c38fe243a5646171e958819cb2213170d2057cd6f65ec5a7bc6956fbf63b058df6e7fb9cd5a360d7502e315098d191ba7e56cf197dacbec46138c45bec2d3b5df9388fb033b55e02347b7aa541bf7291da759c22892c4d7635b3f48efefffe6bc32287fa2f349b2c67456530b86c2e48f77d28cd7941bae253a15cbcea499770dd7df926887f40fb64f8ff9245415da09b92da21c3e134cc6f9c08046d8d05d7a4006bab54a631afbdcaad3dfd464cb201ff719e42e1d1eefe6bb2cc2ffb7e972c81ba6cb216f22befef95f572e8220fdc6a39714f4823689ac22719cf6ddeb0a4233cccddd12a14fdae228218b2edfa4afb93ed29b3d3f0a90d77fdb1b75f2313fd7f7303f58e1b9f2145458cd4737d1af7f423fc50e1b0c4e5439b3798a37c7be70121245d4abb9645451c59beabfcd88999568ba43e88164454bec06b53a87d7d30bcd2dafa0b635f330566a47354025074fd0fef88683f5c53ef88a3b593f79e1c98b402f7637253d3e32b405b6203fc853d165bf423c81f9f21e8f33fd4361f15c31870dccf1448ada2e73dd1d985b0c9fe732191778378595fe09aa8acaff2ecee61bb37c055ccea0bfc338a5072c1f1bf1bbb7b544086ab59b9e80374366347faace8bbdff3cd38ba7ff905afd5dda65f88fbe0e977d1f68ed8c75a4dddbcdeb0d444f8e7934ea4e3ae54edc3bf51b87521fc6d3aa4a92ec475d534f4e785f21b3f2498888494a87913da2b635a3c9f0e2bac7dcd61f148a71dbe02fb32bc722cb4a9c6571822042a1c6c2c29091f8ca82daa08e5583feebc77facb46f5e220d62a0e2486969cd4a0ad4ec2f0a27e836fd5601eafd7786a7e3da5e106d5027242341a152a9dea1c6e10f9b48ba7e423f11dacd72cad0d70cc282bbae387acdf5215de36e50ae59aed0ae592e30ae3167ac6beeffd635f77fe79afbc7bbe6fe09aeb97f22e0fe7da52f5bd2ecab4f140404e7b25eae94aa7c9cf4bfe172735db810964f7c7ff39a7300d414efa9526bcf2898111e7052146910d36c8d611912921c093d4e2c5ccff5301c598f257fd68c4a69fc8877c03fe2e35799a3faa200589277b17b30a9231650226b016f3939a12f0564bc948bc94f3edd8942177e1f7d469350e6e2fbb493d9f93c20962c8b0dcbfaa0a452ca8547804d231b9e3aaacf964ecd17562aab81dabc847aca5576285c8a2a84f5457f00164ec538cce9c5543c16af5a28bfc183f8e7f9ded47bcf5b931f7d9a17db64834f8f60bb347cb8f4409b42e5d67be5eeefa4e9f2f7dbf1ce4894c5c5b59c97f1a2728a5162c208f06be958f0f890159188de50288f2f4031c05e36bc2581b5995cb595cd192d894e297b836fcffdf48ebb1a8e7070cdadef234135699bf1795f3f6d1363ce4ace0a3f1fa3430fbf94f87ea8d298a61d451c5193f0ba44c717573ec04abb23a6b56a2d136b916a92edf3532b997b4fa35a3a47d1058a092a1d22e14ab1dea054cd71a832d8b9ffb36bc3ff06fd68377f6a3b13dcbb44ea01dfdb5965b2ea174f0c739d7e508504fb17aa7c7a4e8217f592fad4f1bc31cfd882b3bf15bb088791a4cfd39391ec25fdadeeed61e26771ad36e5cf386a72623b96e7d6576b383a2dbcc434b55520721e1e7d8279bccbfe8239c50e492c45d5a2247a97822b501b373e8dc52ef43eccc793cc4776ebb96684ddaca33bc4451a1f3cab0b2c2e993e8eea70455c206cc01584e8dc3de5e0f30f6e771f725f6ae0c225d48db6770adcb3aaab181074f4c3189a927f43945954d835506640d7f656fad0349862a8359388d74834424a2719ef791127abaa4553be0226291a2b29ae3516d2f9e2fc62529fbc8a19c179ae14acc9fb04eec27aba00dbb4ac4418e32106d86e685659473cad9447769de5d149302acff8e6db0f9ab38ed982e611835ef2a64012a6ed5a2b1773da7d920b954afa77d28f226327cffb381c5dc1d3eb116f038fff0f7bef01166596ec0f773741901c04c9597268a09ba0e42c417206a18126e79ca32049411451c94a1204949c332882080282048906324816f07bf6ceeebdbcb323bdcab877bff9dff7199f79e8eaf7f4095575aaead4f9d535ac601e2d54fcc1267136fc1df502e58177cf41c152fc858411e358f8d9d2a9eeb08d4d2c5ad70d1a4895f0478f476524e1d82fb2706b0778df4b7c231e2a97ccf40c7f55d89ca244e7d57e8b2716c46c139fa1cfc69acbcdd09aff9277666e8d784f048faf7d248d232ca0e4650af8c68dd03e616db16ad1f70a01990545aa4b07913d75ef5d1fb6b2e19c335cc1b7d9ca9efca4c9944bcc7145e3865ed2cad799957b632dde898e9418585c911fe3957b5a1e46f54d57bf9a2e2d865dc21a89801148cfce85f7ed90b2f824c5166d4e5f267a4216ed3136509fc88df7c17368beb4ca6a8aa64c07ff25067be1c3d99c095a5be9c5a988bbfbea443ea4a5fc12d464b2694dbdcccfac4371d03013fb9ca1832d907b4d7708f83f1e0cf3559a1ca65f1a531da38f8823d5fb2bf107995186e5eeab073bec8379594bf0e5294ca2e7aba8f8835ba61f7d0f611484f30ab78e8f7c53d15460ece2b02f0e6b2b9692dfb8d255754c4dec24cc65a61956ae2bf48df4cfd5f361fd48ac434190f3c8b7c97c2ee3709e27ae6941d267ea6741f6b756b3fc352779715f1475acc70e70167fca41e44c10cbe33f229bc79ecb6c1ef95a3b50915497c093eef46ef04a9abd74cf925f473719b97c9edd67cba8f5cbd281e7b3c6cd491d5bc8b9f50cec94c13767a336dcd570075d8387afbb6aaf95522cfa1d09638e59783e1b76d4f10e4eb71d0ad87fdccb7edb004705b38c89880bb3e2def856009f9adc4a71629385e7c0f64bc17b37954158f44f284784af37b0b32d6985787c921adbaa21d031ba77fd4e12def38e2262e6444c7abdd4eeecaa8e3cfeee95f707948f3d617c1e3c9fc8149fc970f7dfbcfd57e28f5b1d9e251695f4bc8b1f46e6ccaa0743ee71600aa3e28f33b18f9cf543893293ef78678a644e305dbe7e8d361139c57baf3c8ca2dee605060e6899f0c2cee1c3cca76abee2f94726e215981708afdc51fdda4caddfe65b9c4e54eda3de9f12101b967adbdec44997a18e4b0d2ea8016f33ee82945474e8e792aef1a59de394c0e53eec68ecbc93a7dbcd86bf672711f92ec89971e47914b8e05ca7a922d67bd7571378abfaa6545eaae78d191c9cfbde3949f77ebd324a6f7e8e3ca7cf07a318c69a9d02befbbe61ceb8ffbafa93dc7793732fee47c54db2d23611b6af9bdb1c6078ac691219d021c68a4993bd3da10a49c646e85caded9bc3b584f7e75e6a721aa8623c240cc0fd023792263f9aa003bb7f3d7369214af8410b82a1114b1a874fe8c6188b67bb5c1b874a0ec9eb44eeda301b711a82c7742529047f25fe18c0a7c47d4a41dad7b7dfaea6fd10ad9afa4006a57d8a93aba75ee6d8441cdbb983dda2bf212f987efbe64848527d64bad9645e507e1a980f7cd63a5c34ab735723db5ce0cefe32db687259f5b5426cbb7d7a57778a329c1996d2a8ebccb75663cb4cccd5e5871bd3587cd6af66a9be2acf38e8937a937e41bfd976c67130a92f9a739ed5f8a9576969884a2b03fa25c6eafe47a5736e413c0e82116df7b5ce465bcd1bc438ad2fc934f045dba8d79c259ec77e84193016c1c46f988213fc7198b63f7a75c04d783891b575adffeb7a6fafe6e7a21afe230222dc287f7e9f6dbf504cbea7fe074157be107ebe57b27bf479c9ca30f49c66712373c0c0bdcb64c685f782ce0aed5f782dc46bea7805f31cfaa03e9ce42689c4c70dee33636f8274321d1788f63362363615bd2f60e9385c6a8d3716330a11209b7af357e20fa3a2db3ca4ce7d043d9e4f548914b51bf2e9549a50f10766f879cdd72493d7a4299cbd44a38ff25b0e1af245f8559abe0ca543542ff42972d47c2ebf484317bb36ab1706419028bc76678a9693945f2ce20a8a7c7d3bba02443ad8572fef04f2ec844b75e8970bdfa351ee461828bc230ce37f26e27ca04187b6e2164feae56a9ad6121737e0bf6816fe72d62e2df915c643c5fd576151b3e81fae54b00f3fc56d0991e0d599bdac3b1ccbb95d23f0dc14a30844c716636189c6e937f439c45bb44d3b8ce5966c5186307ac8428f07dea7878dae9b4157a9042009b5dd55c1ef686a7d2cb10e666615ba9f92c75750b099ab9827311663acd0e144b6e4b8def61afdb6bc82cd8dcb65760bfd716647dcc71cd5fee6924dd297036082a0174ff1ee53b15271d9cfe6045edb266fc4353026be94bbd658575f4432f757e20faecc6af4d40b821a0859beb7d90bcf76f83d3c1b51f1073eceaa3e1fa1928addd702af42fa599137c48c2607f1c8ae82c388a9bc9dce5edce76f4ad4eea0630657b7b1b9978f9995c5b1abf378bd4b970c9321d9be24a8c27bd5c580371659bd5e82739ec4d476b874717f4e4934976911eed8a75b4b20866dca6a5bdd93da4a0e492b6175651a3c1f92b1f5d1b908bfb1e623dca97a330b4796cc2edd7285d268debbaf2c92ba9f91e90ea2725b7cb4bd84d1b002b2b24d9fad4fa3092b2cbff631e0d07b3ebb5f7874991e2e1c3ed14ffb25a6a66ecb07df38b9d45ccda86aee5223f8cd0b97744fca8ad72e63e6e4c834eceeb45be7f719d4134326a73eb25e25b093d1526d5cc80f1df3be39161c4cb07be68cf3c399355678b4e68334936a46b3e1cfe05c0a11b4c70b8f62cebe549d0f4d981c9c2935ff2bf10713f9283bb5deab4b6fb07a3f461c35ec3c609d1a46b9bf3c84adf3884fb47e7b917c57c7af76b3ebb15d920351b851013f921153e73c29f952bd9b149419c2c2eacd8eb376c4e7353de3818e59c911f0e2cc8332193a6b5cfaf59eeeb488876ae7279929b0d198f4337bb911cf37be9dbbacc8bb6cec62c9497617177eaf9a7de79bcdadf864c10596e866da91d7afeb2c75de52aa3a47e79257d2dc3ddceea236dfb9752e42f9f363b30e8dace955562ff45ee2b003345b69da0cbfed2258eadc5a965951aa7632a9aaa9d61d211e11307552299e66a4393e19468ddd0d2d7a8408a17b6136b51fc3bd8656dff47b63afe72a7802545de03a045c78ee09581c72b277d36fd2a4ed0b2d90d42df2ab1416e1497baa30543b4ee31708c49eed2ec6af8b9eb4aa6870beb05be9f19c575abef7cedb42e7242d99e9bf127f5cbf856e72ce562e9bd858f012b6a244038c6bdd04157f30a87be5af96aea8eeb45f7b8bf8c2972147d8e29f18e27bcd94b4fcaa836b6b14b5142f2cd38864575a2c60cd7a2cef6c56b7a93c779cd6c3046b95abec0a8d8f2f9426469d7b64f0eddc57b9c928611ad74623acbbb451145fdd638307c2caee0a60f3f77ec585c5cf3b84d4ccdfb538dcc4593ec770b768bb3adbfbc9ab95ceea36ee8dfda2dad8910f65fc4fd7b3b2217bb6c18e1044b4a8adabd493ba35c2457cb6700c3c0d486b67a3007e0818e37dcca5eea37bb3d47e510ee2bc34e7c79f6761e6abde70133d23529213b2f3226360922fa6e93a3ec1fb5dfd6afdaaab7d41786820eb4bd28561413e38de9f29c1da24f29c32eedb4930f97e4d99121f6e3c2dcad02e477c6df5e53d756de9c95a3fb1ee84ba800cc2cda7196e0b119c1731d5fe4afc0156193f1a5f3ad08effc21af155b427e17cf12e04157f9c17a1c198a139bc22aee816c2b759d473c3f85cde54a68bf3a307ac038f7d53482925589647032ee64b388690042cc825d99b62d374a4a8110fe16aafda6f6ea5a1535c50662a5249f968442d4c4f47730e5bce7ea81c199573e5f347377d387617d550a9449cc97d05cc6b77cfc609e07f696980cd3071d4cddf1fdf33f14e7cde487ede23ab4d3df8b6c532dea51eccd5a9d6baca83039d8b8abe92c2541eaf5fde22558a2b5623713c527bdce99f3f9d142c6828c35939ea7d0b534f543604bd7a9baa42b296adab8535ca1003eef160b6cf903de5e2135c6b2b112ec58390f2ae39ecb21846b0f87622d9d446925bcbc70a213da98f60b62b06efb6a822d178ef2cb392553346967ed9bdf024ca25653946f92a57eb05aa478ea917b7aabbbd47ea8aff4afcc11256cdf4a0665058c49c83659ea75f496522831f157f10e7f191aa7ea9ec73e55b99b2e310d8c1b0a1676dad1170b196d34c4b7961368f46a2ff291e469e29524fce7b1fe36c4ff695fc202242d7b94a12951dcc8cf9b73443f44dcaccf733acad969c2ef4f87de67351369cbe73c42d414c4898dea61d43f185969f59ce3123ef7ef53bcba120250ce91584206b5e9ac1f40211c9d58a19507d977b59a7bdcd8e62585abf022681607cd66144edba023dab88d24dfd20c9f3e878429f2d7ae3cb3994d453b6a7537c6de7c7fa096b982326f5f239a48ad278ba5f9d9b766138702d5d2d24a641d4bdd7be93908fe8efabea71a87a12181ac231b2c21dc1436dccc83a11b74233894d3e20f906eefe6edfeb5e63d775f09331fcfb480a09ed043295fc89f4af81db19a2beac05c168077ef9e2668a83f5b58c7f25fe786c2c7f1b8a63b1d92b0e45cb5fa9c5ec9de05e42c51f1c0a2ebe52d4dcc13324ab83b5ed5eaba689322636968f60b21fa965a6706adcc97db95535627b399eb3f290f0c915f796cd92bd51a09da5a9ae93ecd7cd0eddc1f13df451da7a1f15f2d4cc42c5405463c6ffb3fa5a4fd2de2b8a4e2f4be275393450c233f391589d711f2db10cc276e84013486f235e8ea2c590732d03434a1967cadca98ddcd7c5dcf0e983b5fab4e5fd271b9d19ecdc3702cb83908c38e11258317e57f0f371081d174d6f6dbc41a0dd46209c737299cee8bd15f7883dbb868d892906a955f416bd5b348339d6cb992b76836dae062b371e3f43436687389993d656eec187471a7973af76779ef30893b6ea77bc53b555be3301f69f0c1c21c2235f0bb308ec9a1d5b67e30f256cecbed4a6efa434575070afb6fed5c65a7a43dc5f893ff83a78a2a26f5f18aa5da6962fc77d096326e01947793eb7d7f2b107e782d7da167560b974cafbd48997ac12abdf16146d9a198c37da92709819053ce7c9edc97903cea6574baad5b412b39c39a4b86b948ced3bf090e21e3dbc0a3c8b2939b521cdde34ae3b21d246b687f6507309274c61bd4264abd7178d0d2f9585fe01f9f9c218446e605a7fd4d9890eade151262f364da1eae097f69c5d03d3bb82d13cc9cd42dba52ec2edd19609f1c1deafa82523f55e187e6c08261e79cd16e267abaabd43e5ec0881ce0db52a08e3c207af32c04217d1152cc6c06efe552cd38b2659899b6f5ebe9f1ec640b86e18b07d86d95fe3363a423ead6370dd256f40ead6eb61663204b909dd98c75c754086bdbeac8443abf3a026809fc56d5b46bbe9110f9121afa2ea5c5a94d0fbadbef74583cb15bd2e6abe07cfb14f4ea7d8c3d88924fc36f052ad3d9c5a49e06fff6a30e636a9b82b512cdb292f6e83324fc77960c0d1373e665565e73ed6c899875fa2f95a6836ea7c87237e84fe131d38e958ffbfa613c5970017bf9b0f9f06be0ea5bd6fa3535387eb8053cf70fbc5cd13da3f74e9265a02fd898ff48fe74efceac7ec94ef9f12aa169889f863f3f32fad7fc82933edd05fcfe295fec24c4110aa5bd83fcfbfffd21372ca4c3d4b50f16accaf936122f6ab6f262dfe2cfefcd1b4b4df235bfc84fc32374386afa3f9602b97e287006bd1ef2e8684d381aaabe7c25ecd0352b13da6bd47b2c72b3818f685d5892f6eed316c35159cc82207582327d3b1c7bfdfc15b481f999aef5ebcc7e488873ca2e62a3f718c2c6a275e20463a7077eda16ffc0397850e54fd8b30594c4219c4031c95488b93ca3507b21fe5117e6b9ec4f52df2ad9f2432ea298b2c791783231c4c53695ba30ae1864bdc3d0113e49f3159e8760296c3b754b0a07e700fc13771bbd21fac164ea719ff0f93e517b78fab698e40f8fe7cc5bfffc164d144f8f83823857c75edbc5df41444ecf59130472b2b98b3958eb22dcc51cad0d3c746c84956d95d44d946f7cfebff7f00260b1c6665c12fc0672ecc6f6e652e2c606e69091711828b20fef61fbfb0889508cc122a0443202d118248b830d4ca120683f2c1ada056961670a485e5e9f054b0acbc1c4585056102bf883fa41202f4ba9eaf366bf76555f0bded922ee817d45fe9bcfa8d28af33871b921abd60132f4064ea3f9243ea89b443f3092562fee227c33468da34b0ffeadbd768a6aba82e1140f6d0d643c2a9258dfff6af066d6e3fe521e0ce1c1fd580d55a598b9dfecbb1eb76233ed2b359cd8f4e67cea35cd3a95f668dffff82e7c19920104ed54d92be9abd3368d09b6835f8345ad1b41ab52a7f8714fd3a70aadea346d4cf3cddfa804e0b9a2fc583864db07a5f13fdf9cd9478dff60316358b589fe2470a185397bd5813c5f1372faaa8b73d1fe4d39196162896b9f35450bdab8c0d3d20698cf2fca50791ab0d48d4171770aad6329f7d4ec3a2fd28a4e4f8c494d1ba9098e46ed26f02717442b97dea177c0e9bc4a79c5fd029e71fc03fc3b93629a1e54ecabefeae3e577bac9feab2169f8c32e563bbd9bcfbe53a982e860b391344104ecd5c5b3d4744da8b0a1f1f804374899179b6cfe8e3cbdbed1e58d4db5ccc5f73a9dfffeca8c0747c6f7b54e5bfab15c0342d62b47ebbea28da2745d5a73d8866a4e117423f495038353da80622dbca16aa70fc0be7fbbb22ac59486776122b0d4566063edabd520b3df9371f3d3dfed7ca1f3cdf7ff7b7ae9c4e5582437e84af7eb07f332387ed6c27dbcc5c19a76465d0afebbf41716d7312fdc9fd47449db6ffbbca21e1d474a9d5b21b3587d528dc30e9ef35c2772bd8e3c5240874f2e5a05209cd813171cff8c17b619387762b4705726a2897bf19b287de1ff23f3093edb0245d9553465b50201071baa2f256cf8230714fc106e8a78b16a07279d050a86fb4cc1f8916a12a2a842a9af24ff39b5425ff2393f59244e71b56a170dd2b33223aaa6755118d2c6f3f9dbc7e50b5536eff52a4cb9a7a9c8e394c0575afc3c38ac6b735d09387f468bbf11733036ebb503e32a688db82b5a7c6766af4ef77c57463ed2b243c2a75e4038b6362a9e4dedf2e7941fd4bad33140ce028caf9fdad9deb2dccd084e1943a03550739ff9067c09b7f5660f687a29962e14bfc1852c5c9ed2f686f412c855de46e3e4385ce45714a970f8c6afe0108ba9023aa70f63b16ff26045d63704f07e46916638fa0470c66f764d9157912517e7d3521c2d158e22e4ab19d1f89d60426a6071419f6f1583324b09c1dc0fae8aea85bf67fd19affe868cd9f87a0abe366e92dede724a360e92bad6129eb0555b657b15454b3b57694b791777477d5b330d7f516d017d2f294f7fb4b456bacf89122480141a8084c040917b2b0148009c104a07021413802ca078589c0042ca096020270b8b9103f3f1fd2ca9c5f0421c82722286469250c15b4f80f47d09d86ece139980b2db59fbf78e41472bfa699bda2b8e1ed94cec2ee5b7fd97744d15645b52424b90f92b9b4f7cef6375e1e74784bc4d5c4fdf65b4b6bdc8c97d67c326a04ddbe907fa8bc88e47b01b9cd90a128348ed121d07360e07e0fb21c12814930315203993b7de150080ac60017fc5620f46fffffc7bfff7efef639a01a02695a55c9f06d3756053413c32597333d95bbebc451e7ddbd56f273af7d748b02cf055b67be3dab2c2289597940b1eaed22da8e8707022f76dfd0a3057fbda667f5c96875ed009b624c4959bc8f4cff9b6bc5e2a16e921558dba446128b6190c28a8d353f54994ead3f0b847e6973b4c546e0b9907021d65b0c6a7fd5ec50293d421a512958888484a389c5f6b060d3876a35becb0b6f8338e732b0e394aec99594f9b7f116bce6af8f88e35edf6e0b491ba3269395335781ee7951216f65ad196d90205b76d5a32edc7f7586bbbb061a6a797fa111a733e5f94ef803347e9d0097ecfdd1c6330cf7b4d0e432886806d7acd337307d361189eafc2e035426e5a257e6351fdb2db1603cd24e19749e5de43eb87ea04ae0ba97c0cd29b7ed1be4d8efd4df2847fdb0037b55086f24cde3a3f2c2ce6d353d22ef4a7e554fcf8b5475c1f7709ac6a398c7a5ae3df0f262341747c686e2dee6fd9420422b6e8c7353f64cf4e375fa9084d77bd8fdfc52e474efcf48f19b946746d85d71782030422b51f593980ef1687f5b001008747c61415db94ea2699411e366bbb97c6d4b6211455fe7977eb4f0293117fbad029d828092d12d5d8f6c24f1350e7eaf937a423910f447b77c7ec354fe28f8af612a033cb59fc1544605607a4a80d31fc254169e7425bee227eb071ef695cba11dfffc4d89f8b7e257d8518f41d525da8d464e4bccc34d45eb3d730bdb21f94c49297f354c65d025e92fe9272641a4d4139c6cfb80b97c7ea9e540a672a2db95f2261ad5f49d1a53f9e20055182f92715c5664a521c313ce65784db8ed6787b3874e7a1d4c2f98a481252c174e977abb061dbd864fe8148841e004b208df1728ac3b14bb14f11714efa3886ca085fcb45d459d475a24552df72391010085ead0fc4a39aa9f67c0407d70f03f8f86e52b0cbc54ea848d0df59e1b76811cdf04cbb951bc3f753a2b00a5fb812ab2447a4a2123fdf131ffa9efffa73f52603004048af1d533b073fe7dd0acfc8af921d5697534640f27f1b70a3cece23e353811bdfdcf4c4e17ad4395db838982e7b05045ab3cce92fe5161206252519899ccfc57721d5353b6de591b3d429cc516624ad23f8a973062a1a7621c3568ef7e657bfa02c27756945eef45a298dd17f28cbb4db073901b9da0c9bbcb03efe61062cc3b0348ef37de623fe2729e72d73d65341412723a8f1b954e451552404789b2f7b4e5898b0f59ccbe62c74661a574f4da70fbe4331531d78678fe9d387e43d54d5c3aa55b568e8ed51ea4c36264566977bbebcdd91748a02f3d1235a3c877ce3d392dca199b4b2ad2c3be107f7c3ce5318489e583ca849badc5d6409c08d9ad78e3e6a8473a68ef9871cbde77c9592bddb1b778a8bb1d8166d77b1017b2f49822effd32f6c22951fea4c831e57083835ec7064a5381d8c4858a053f26061040465c2730d83844e4f7dcccfa1eb614efb62108149431c03409da0437eaafd8782ae3f5f6deefeca8b88c76daf1cb900d7c72e73c90b49b1e0abbc6619199f5222d5dc670c4cd78c1867ad7a18a818abec39e84244cf7aee13bb9556d2fcec094f3da773e784ddfbfcc328fe0dc2d39edf849c53ed580f688a3de18760ca151a75994d76e4ad8c75cb6223e17b73f6bc6080e325b4a7c5ec23768dbcc31de39bf6b1dcd24de2f72d5fbd5996eb97459e117b9a71d7fec33f2c29532cfaf4657aa6736446fb521680370d667c8b36f5614ba1c861e0432ca5c57426f081492b7645482c7b87e3ebafc966fe86914a1ac78b4c546d7a8f4697fbfd02b4125d76f082330df20656e80fc597c63688d5deca4c1ae2ecef348379e0a15264b35d78dae2f925a579d0d31efd03cb0f5876f90e514403c47acbaac8d284efbfb5b36c9f3c62f98c3455689d4db249daf9dddb91b3642d1f4f2b9d67536b3dbfe60cc805e5c7de766f3afd24c5e862a8634c1897c549a709aed437a59fb1009f1d653ae3fc06660a624441358d9e739104e7f0ecffd669ed1d2297b9cceca079b2620b4bcb75a347b6d2cd315f6b1e70600c0556733aba953a74f6799ff2a9370d6e4a72b9eaa8012e47a2fec407bdffc595e1aef59ae67253626315403b212a37077274c526f3c95143b74b253d0da1c26cf0104acee809de04fcaa7ce91c35ebdb9c7119ca346d600d0ef772e8f7b289209960ff8b41b5cbba8e6d0e2f70990ec91f279e0ba87b0c867bb108cdc0a6a6f9c26efeb009bfa3ed25232823086aa5534b5577d78585c0b3b1e90bb7f3f6ebf4f43a26c6836c340f3b9d98d1423b14e0040f8fd0ed60492e5a32e156376af8370cad79a1e10c0fe993a0472a2fae663f244df767584924b5adb38edc1717ade4581548962d6c6f70155dc69201cac83e864c09583a6ecedd8bd2766e397be52afa2bbe357edb3903203020a97bbab9b1f4a6e5d7b8df55293c6724047ca35ff38bd8f7842f0ad4120e6b7945d13cedbe1a3433ba39ac7e9fdb8e1123218d318a296f5ee713a68a19ba1e80080e0cd73b48bfb54a95113b79e338b9e7f5abb1a779ee4387deb49b050d8a75cff6fd9776dd458fb327a7ca800577ab73f8b3fbb6e153f3ff3396c2f8d532896f14edab99fb2697965831ed0c68ce77f2f694287ca3ae00249c745dfefb84c97a2e5a4849d5ee8247de77d2dbc2acadd7738470ddfa16b7ce079f70e3b63eb7bd613c5bd3ae52b821c78df7b9f0b6badcb8c6646dcee3bf4c970313fa80699d7770a1d81211e533cb09e550b82ce3f2ed7a5a6e522b0b9dd18d6f39df18b283bbfd1a8e74bf84e2a14182d938f987db1bec2fd3bfd33907024a0f414bab9f09d484c073b9e992fb82538a7f08fe989fadd3c45c481d6b5df69ff627451954599f7c2b3efd00394e8c38b6b2f5c2bf80e5d65bcb282675596f67b715b68f3d562f89abcc6f7729ef1a43b96bf2edc30e3faaf140008d16f4639fd5e780d049decfa1a00e0f32bd5b7176f45aa87e8df7d09dc46be53b1714d4491357cda0b1960cb4af110bc53868b4ee9ab8001ac0f7641644df27ea879303bfac647eaa83f434ff87ffbb00b30be4ffa97a4739eee316a091ea993460eb60545547b9fa6f166c81ec6df99827674b30603ddcb342cf847ce08648420926dd8db3d58e76edd2747625ff07565bcf2671e3ed9713ce1a15f45545438a9719a0af268ad04675e0181c85d53ff499ff2ffc39bc2389d2709640810e402103f1c3c7872000e620928b503227c0ef43c19bea0183faabc93d3f223f8d1e76263ad534a8de40fcd218a353e2d8f206dea9d16dc25955573bc8dbdaf943c3f57690f3fe524a1526cff849e8c96ebf8adea4cbea90f73c4a3920aff2f476d0f044f1adff8b0faaf5218156920867cdbf5655124dd51dc03a92584ec059adf9517538bf991f9463a70e5a2fa51c3cf828b2a1906250a2cf22d1f60ecf6390a6df7eee759aeac3eaccdddba74b71b8e9b7205a9160fee7952ebaaa136e41ccc73b6713eabc1b506cb37179b6e00f203de87f437e34fdd53496eea601cabc70a9792b0ddfb03eeb9451d92d624dec8451f2fe81877ba482f685cff5668eddb5421f9769007bc6b4ac136595b43edb27332a24ab48f1a9552ec8ff4baf87580febfeb3287fc17b8363ed36fe0daffb6ac930b6dc9ebcf1faac0328be803f78c280ade1eed23729c24b1b16f7d2a94548fc8e63ca84ee179295e651ff704bb22c93af1047efda7ac93cf7765ceb442acdfab920bdc956f97eb53226835386588e8ffb24e7eedf3e7659d582a2075b495ad3cbc150c3d3d7ddc2e7b29ba09ca590a69b86aab586bc1749decdc10087b6d6d294f9897e02fb2647e6253f813b24e8411420202427c823073a839dc0a2a22602e22081340400584a0560873183f1fcc42008a845a080858595941e1e62270380c0613301736b71280422dffc3b34e1a1ea41e7144ecb56a70c77dc8217b30ec465528e94cb53b505b491dc823cefc164233d2666bf66899bbd0005d3d084fa34d49d73489eff1d33144dc07f71c0d6fd459277b686f7faffad0da4faffafef944cfe2a685c42b72459a5aff238d054c067edbcf4674caeeee90f2b75dd112ad4ed38f5d16c8151503164da49804de3c7263f811d5479fc87d40fbeab1304e8db7511b9b580eb1a67fd87fb6ea834cfd9feafb93549fa19a8f80a28ba58bcc65692d6f5d4798a20ff4b290858b818187bdae9b8f9a94ada52c5207eaa2a22267a7f89f33bf520de10e4a1f649e648c9e490a7e01a50bbf16f9e08ae9b0770374fd8c8792d3577172261f4e91ae339642c31be969298741384de19d4fcde3c526e3fa4a6febbd9d462dda8351a4540dae8ecdbfe58f45104d3f04853443763543fee7ef3f281af28900f2801a12c9991ee392370f0dc0dce63a09bb0a6d704224ea6f3f15ea2d98333c02fca9c1a840bb82b94b82bfff346457d637d1f2f79fc64a49f141b57fff293888d93de577dfed8f24e64b9d970485a7248784e7bfe70df9adf8cadffffae731b1880db98312a459bcd8cd3c8dbc101f8a3195be7bf8cb815bbd21e7fc8f8af67f4a93ecd9cc94db5cbf59a1bfeff8eecbff4a6eaa6ec7babdbc8e4ae5a2b98acacce9dbbd828a268b414a7185aa1444aeea447b21acc4298bec06c751e1fc68fc7dbbc60903813c01492bba82ed3947af532866db1d230bac266e2b8c382980409e803b40eb07e52f87fcad87c3ef721854e63d9df9d4c4fae1773f70ec9a3db568c2bfc984fe361cc64d33e82fe04c0cf5bdc0185f5e944fa2dd417c79db2c3b79c9fa5a4ae38fec234a72a2c23b39a217143f253884ac48a5b8493322fecf84fe8fde47febc6bf6869adace06ea8a9e425abe9aead297e1f2d2ba96b26a507b2717a8a0bcac23c25c5a5a45dd4fc0108630b0ff4b99d0483e7e41217321045458c0dc52841f692100b780f22185a056e67c968296082104d45c4408c10f17828b0843a116704b1854c00a2a0c13b4e41784c3ffc34d68f36cba5cada8822792ecdf9619d46d95e22ebfa39f8ddd5fcd5eedee9c3b9f6d6e22434695b53f3633e9ee3db8e66f3192197733183bdbfe6da4a9c43da13e4f41d4d7ec211a7fcf8c91c5ac81c8da7911039402def202ee2535f935656cb5d6c2f7e6aaf3a296effecc00f75fef4195100992c5d3e7059cee1954cbbc70f0e8f681144e73e239833e45c9e05c3ba5dc9dc853c7ffe0a5832aedeff5d927b482225d49a288f7d3419f4e2bf4cd7b90be28ba3df7eefd105038355d6a0da4996e048c7b3a5500016d0ff1aabd2ac0fd2643f9081b966fe38948d99e752864eebc69ebfc10f3fc2bf9d3b11d38e4cf648288c26ea5f341aeb559232b0be57a139478ab76a9dffdf2dfa70a45ff647f31df629a5bb839bbbbfff4fb04769e8e2e566e3c563e1636e64ed656fffa9b18d82f2564123e9e1cf265ff86a295901f0103fba3e735ded1f2e493c6d568fe45a6dad18631e35dd20b3b61be39971c2eba42f6c03e51604b19153f03ffffbeb50cb68cea4e7a856a07442c7d909f3456a2a648e9b59a460e06446a9536b6be8f2c2ad8982bffa4b63d71fccbd703a96d684b3c6c0467f308a4da258441f71d2fa21af62977504ca89aaa9283cbcf6b93be57be6b98718d4d8c7e17df7c69157824346ef604ac9bcfe6541e8cb18aed282a36a7f679b588247eb6950fabd3da5670cda06f98fe2169a3907276cc7a6c8be2bf74b5e8ef53fef7ab45c702de7f23b069f6fe226bdde6826503fea85ac803cc7a9adc2d9b4b0f0d93cd8c3b83a73c8b9638bbbfd279c56140a8cc1425737239f03c4a3ef08afc88b5fed82246fbc2136cf41ecd69f3f247e10c368b6992ff67adff475beb9886566eb6ce4e3ffdfeb16b9686080f4b1755793977293d75117998be94055c0e61ae200575d1b5755345dadb4a09c8491b68fada0a43ff52d6ba05d41c6685b4b2b0b41282c19150b888a0255210c16f692580e4b7e217b1e4872391fc50045458080ab5b01214b412b212121484c1ac848591fc027c42ffd9d67a7c2ae923cb214acb060f32cbf3b6ce3668e18c0a63565ec1fc023df99aa9d32045151632322bedacfcca475992837267e74649cdea7908fcf30cd52aa3cd4d24515716de03eb45329b8b748780c271bcde869f4f56ae01cbde574c3e220721a722c82c41204bd44b8fea727db97fdbb2a3cbda958f06cf34531f7409f9445b03a032c34a94640363cecc36dc69c0d3fda229a529510b009cd1ab02d3851169a84d421b738c2c6db0df2d110312b214f73dd99a86c954df63bfc71f60d6e833782e75c2a2fc36dc13e85ae77d2b664ea0cb5c99f439a1420a8ee7e3a0b6c1df2266589fbe84f32ea656cb12a127cf1fffd24bb84c4fd8ed67505756363905b12525f734e587281d01c81e1af93fa07dfede720d1a7aa1522f2aa0bb5396a047951cff634643b17ba6179d0a181e82490aa50a78af5bab749517c5fb6751d1556d9d3ce8651ccc6d1d7f4ad030a03c7c3cffaa1c2b0df36eb5d969ce316097d7c3de352a8f0a9601ee592e3cad781791d353c3ff6d2bdcc684fa4b58e4bd09144da2809ae1b8fc7deabf04af646ae3e1e1f2b79dc2cdcaddd9c1cb8ac7d1cacdc2c18ac7c7d78fd7d6c5c99dd71e0675f5b38579ba7ac22cedbdad1c903e02560e3ec2425e4e3e7ee622b69eae8e8ef696ae828ec2700f017ba8a09080a0133fc20ae6e3e76a2e6c813afe235532ac98b95165a2b78d1cbd6b48e25c52725ef890364e6dfd95200909084d62454cf5c53712e17782c3425acbd95bb41fa4d1779f83231aba056ba32e5cceb81c875a910d5ec79b7a147e36a819140e79231781832dc3f7df25c17ffbf39fc3d6c16f99c429a042c9c55937329f7d70a624a08f3811080a348377a719b2ab1572ec0afa3fb72ad6edaa4d2a3dca70b7fecc5aa6d82aeee2b5beb4135be50c788dba5512f9b4052c9d76e777c698194784ba2a6cad6b27a2e05fbcdf7d3c6a5f25fd6745edffb426b514c1c62e4421ff741050251df2471fedd1ce46a1b5f05da02d16ffaf783aed60841d55e7f1067f08370b2d3ee851274a271985054382e27d1214924982a203248073d1248505d601bccc2b0f4b6ecb2ebe8eec92d8d6b9784a0b0dfb74e34775d31165b17a14ef9f41f13eaa62f538282c487c14e323f8215cb71f022efa97f8efd41095a7454c97fce9f1fdc4f30bdacf3c1d7f83fe099c2e13511c936a4af438f3c2c10597d261e39e5836fdd3068a7ee1fcff1058c11f8dedd7ce3f4abc15d9eee7370a7e5686c85bd1919163d43f549563c12363b85689bb9eb4f5caa787afa36c191f443b9cd0bf04b2db3e473f32c7a8d6f047e014ffe8594737d37aac1b56be974fa51d2aa29a653fffcdf0ffc7f22ffd9f26ffa9c449490918ec1425509d4daa4d328237483be7ff45f997fe33e5ff8fc6f68be51f55f79837c991df8dc292f74be714f02f7e0fe7132c93762182de072cf66be5df17e394f20ff8e852ac1544afaf62c36de7ded64cf907f5a42c4a8c5f188194fcc5f207fac563fb4bc9f74fcccf5f59bec1b20ecf95bff1f69e7c490fa65cf68be59bec47e618d51aa2c2c9fd95f2fdab75cb2f18db9f2edf3665f2fa025fddf2f07b37af4dcd7535445a1dc8fdbbe4fb27e6e787e4fb8fc6f68be71fd509cd1cfa9c61c98fccc1a2bc8574fba0527d8656bed5d0cb047d67494d5437cf010714afc8b6933c35cf969278bff2bfbe551ba7b83b8c3c597f38ce9f4cb75dfb77c9e74ff007aa23d433e63e0e3a5ab2323fdb3f740b2b87d3ec111c7c3e165650b83c5c44400806e3e73347f0410584842cac84a48585913069296119416179615938fca76e349e2dfd91f9fd89f8192affa6f974f2810ae90d03051d1345fb58a8e4f3579f709fb6440eaa87fe94ba03f467beffefd8bfbcfdc274badda478f15c1c9b2fa55144f1d9ab3cf977ed5f3f313f3fb47ffdd1d87ef1fca3e2cff689ba4f808bcc8ca363773e149bb7a0e7556e77c5fb26d5a8bd00e3b9a77f3de0fb82f6a9ffae03c34549c96b78949b04e457a46cabe5b70fc5787f8847bd4c3e9145e89b7f346439c862acb349a97eca7672c11a98b3edf7487cb742a5f21d428dfe17d7ff87527c7e78ecff8fc9df4fcccfff13f207855522dac2ac5e5b697863d35159fa5f59d520427bed00763a5c8f954fc2e3fa913954a07d25a4fe8e2938a97a56a4039ef220dae7d17733faf9eefa13f02878137e8f2ec7ef68269fad78f37f71fdd17f64fd7f64ecff2ef9fbfd01f34fd8707f9a7dfe13f3037e66aaecfdc239737bc8bac358f699d9d3df7fe1f7f4df12832d6a9d2277f9fe3bc7e75f4b0c4e9b458c72171c8a0830ca7f85880b96c476857c7c1d1e3d24f67eefc22b8d7940f16ff8700f124dcf82cb02a758addf63f93e65be7f2c8af64f9d18ccefae6ea06efdd34b21854779e6aeee6a68e4847be67b3b6bf1ee82fd99999b1d71ce76d86bfc794607faeedb62ebf692606d1952c5c22535b91a15bcadf2a0627bc518b3e0452d8cb35ca873487613fe27632be2c0d9e7e49cf333de34c7ffac94eb7acd9fc86553787755c9a5b0ddf92071f5fcef5f41b533fc9ede0cd9031385fc0f1fa01b057a84fd08cbfe84492e09478820113001412b24824f44c4dc9c1fc6078309c38590e6028230244c445008ce276c05e7b714114020ac9048217e989085a0a5b9958820020e1741a06adf42444040002a04b71016b0e0835a22cc05cca1824861a4391c09b5e24358585a08f3f19b23a04841a839524840801f011711b610820af15b218505cce1cd90e12851e67376a374afff313311608dcfbbcd7d517f3f10bc06e6c0d8008540de446e222559d02e4518d8f082220cc2f06e85a04eb489fd508691bda676b71d66db1492006bc9dce28f3c91afcfc8d3a36eb5f46134397c74087cb149e2e2c1e12732cac766974e6a956809770175ab8e53be83436d6934096d755635751f29031b19864f64d906c3843f318387d766514e7c68edbfa0d7762271d71cc71270feabe594e4901ac89cc2398953322b30bf24599efcd9a840e77c674b1966e878f1ccc5c72980a8c1b69145f27bfb20ee1884cb6ca1837f696cf106cff7dbfeadbb274b38aa1297dc76a78c0f3033b105039021b71ab8ae22dc8c92c8ed48de1da2aff28c62c4dd38cd1e8f4caa12fbf9edef5f98a33fb9e0414e5a276f266c959e79148b2978537f57f422ee696a2b4a3d301ded7c174d6aefc9db201277952598b92c734591e85b6ae1e81739c7c8497e0a8dcb57e36ecf2ede8f6a191b28a5cf1bfe28a78dff382a7fd7b9e913753213e3bf90418df634249cf0f954384bc8e71ab4472f423c454eb793a24caa0281c0a799734050ea0c34203ad29a74e9e10b150605963b4b208cb339e898603408d619103af6cf1d0fa002173b319b17bdeef5d4c9f4a5f8d3d1774f4cd840af3fb9c4357afd45c993e9f14b27d33b6127d21b324ecc16466f744695288ebe3800b65a47fa280c9b2ffb7b9c7fe5e2728405e10d038771c6f6a48e746e6edb13c2e2f7f17b707912e9d091fd55da4547b620b6bea7e14777259097bdb7530356b46b63a3ef7a35e58352ac7af50ee06e912ffa5d84751cdb9db4afb8b7b67c645c8b6d180b9f6b2305cfe115c41dfab6508beab3934fe35efbccc9f0c4231955ffd02b524b74c5047956c69e1fe2658ed8e97d92bd7b7deb62bb8615bb783ba980a997c14d22ae83f3948f11161ab24979a33d8a3675d8b7df6d7accdf7f116b7aeeb38f3032f43149945ca3d5559e72f796caafcf9cd3d6cd8c1fe914361ed02776ec28868862bf3a97bf933d55e49e45bb3ad64b9193f93430742be74080649027f26ef35e734f4838b5746e4d73bb2cdd5eca8f0419ffc0000028e06b58a646308f682d3cb829f17ed32b587da91d208189efa21207d568f64dd90eccb909f93035cd331d809b8aae5aa9e4db44baeafa07ea6e2aba999f27f65901770de2faf5262b9a73fcb84a30ec798330a80b22a70049aea63e841b526ed2380e1be1995aef441ff0278ab21ea7ef1eb49e3730df793dfd4d43c7b37d214b267a0050b46af630eacd08b970f4563ac7a7c912785972bfd6d871fae82b78189147814edee30cc9725a7a825ed94cc05d09ddac4379d0042eae5018a143738e7314e36ee57d8003a724b69eb925648626527cc1b70b37e261d37b00b4f10db78a523ceee97b43eacf2259b68f6e101a39003c977ded21fc0819e4dd317772f9e5b793f6aa690bb3c7e99154df6227298ad4e56ed3aede23dcb974db0807a08f585498440a25aa3e0513470adf76bdda385c250ed0376bbc95d5878f2975d439cbd6ee68bcedddcc0e0104b9d85503ebb2b74a3f04a45709b0f4cd573084dd02f0471b1d9f48dd811ed296a640f82d9c90c44c7c0f506834fbfac0fd4ad95db584f7bcdde5816ff69a442d01870c094b9c44b87589e1fda0bb0d8a955c59f0d90c00f8efa75c179c92d1b84881bad9472ab18c34a4086f4080ef0b9b128d22c2858077d45fa386bfe53ee233f2f038fdf1fdcbb965a6a9e90988e0ec41f8906d94ee66df71ba037b2951f1fce45e2101d7216e8aeda0cb320450b247b5c0adcf31d7c9547655f5f53934952d13ee89a1e3749a9ec2ad41627f66082fe54795b749f25d578d0050a6712526a5755daa1238dd9c9f680543b71b73cb9b8ed3fd2abd52c2acc5769f3ff1f52c6cba54da5ec90e285ead400c46c25a0bdea3795e330d8310cc336b7a000e3102c9228571291cd2355b5b161e3ac27d9566160037c537dc6f115c4f463bf420a815616fefa518591a0704b96e932f61753fef5813e1166b63637d6a577d910150da832fed6630ef598a489c80feb3896f60ddf114f2801ba72556b70fc90b0337c789c0154dcf1c3faccd7a00dc8798eb890a7a6f64647cf4708e5a941fe244dbe803ecebeacb104f51c130d2c03c176516e1db2cf7e4ecb28fd39f7c7aed796986a124602713f359f45dc523de48a0fb12102a3b3a29f381460b591221c26d59a69402583f85f7f01b79185b4d5efab9737261e977c536656c8ed31375185f5c7acfd6b67dc31392bcebfb911b9207d08fe7bea18d30f9c752bfbefe51c4d9a5a254c1e900b0614aeeb88cc57053089a44e413e34b62e4a47a8900dcdc07b1796b16435f725a127b4bd7e60f55024d3f01f6eb9a74f54dd78c0cb42b19eb0e8d165b4f721f3e9c04c8f7bae7560d4efe5b1222e13daa3d319cf9bc150034fd4866d80149a0ec6e33331698305d05ed5e652e003c9cec915ceaf63242eb8c8b7780a916e58416733300a0004ae5b57c44f91ef3ac156862f335de58425169d071fa32044d5ebc5e6679b4bcfd66a26f59004b8617005a5fc2c3522e7de85ae12e3a3a1306da3d09a8a21f607e5dd87726b86a3389d00f6232effbd6442b1c2d005cb642c70c99c3e7ca8b6cfcc57e159889cdc4eccb00fd2ee4d18a77c0201f351d2c2732dfdc5517491f0538c0deda987f45eb690b2f7662599cb0c7e0dd78470a7000ac1fbc35b2c4d22f2e299f8fac10b4529050b695384eaf2d99bb8711851721ce7e05e7a29863cd43810e007678d674669c8f693511c3aad917d3622ba9c4a81200c235a94221bf5324df7b0f42a69497e60c4970f47ca5e3f4fbeff240ec3422984111ecabaed1185550e803400095b6783e5685cdc8dc396a1cffe80da7f9fcb9a780db6db11516c2b0cf7ba08207328a97eb2f1956760e01f65f919c7433553e55424e64d605f4eea3540e161ce5e374ba9b8a0d6c431b039379d7a69fa9e0f6f0e6da03e46f5577c3afbde6cbd848ab6360ccc41686c54a3ac00162f1edee703b1365187b09b2403af4b19c2d5a1060cf33692796baa636d65ebeac2b9f82b63681e39f0208030574cc87855ed67b674698a5813dbd21deb5bd02287cda317da55c1c39d033ebeb221e5e67172904170024e1652774b7422888ce1a233ebcd7b6cf4e9e30a14f07e8f7dc2910c1d617ce3e7fd2667ef567f2f694ba80d20e0a85649693f89ded532c1ab2e78817434c679080f977c35fce2a7b09de79e370b537d4bcec89c41949c09d28dafc2cefafbb1f3aaa6c31723a468b03c9566c8d012eb1e76c58fdcbe0071241afbbf35e13259a3d6b0a001c32900714f0f7ccee5806cb501c7ca0abcfaed600006b3139385de49024a330e50dc4dcc0e9c7d754bc06b05f7817f125da238265f44071d704ded2957e6350773b4e4f3bcba79c621f15f8ec92c4aa96df8659ddcbabed00fd9a7c83038d8353edbc7986f4cb6055fd6f36177b8ed30f303eb06cde4e2e322c4a4cea81dd381c4e4e00007e7f76bfcff27e266a77924497b6bac187882a3000507a42fbd1ee8b2a81e4db3d2fd9159b4cee3c17be3002b0d737436e0ad73ae229d4124c4ad1c581c26d823000f693d7334f19c267fcfee65f8441c52f03a837886fd71fa78771c67139f7de0d463cc1ef0d8f2118ba347711a07f0c9c75ee5a58380abfd356ef3d58bd21e4ba6f680a906f70a350ef57964285ee3d6221c6049ccf6ad800fb819b7e8674c9a4a9ffddd16aa6ebb9d5e11a7fdd2e80fec0e96e71e3d1dbd279d95fc75dad07a9e210051477088fe16c8f2dea4e2756d5185f105b1ef7b8ce03a8be579fb03116f4d2bab0b110511f1ab3ca9c693509a8c8e8b8f2f6dd2a84a17c801f7fce23b42bbaec190120c85e5923ffc4bcec2baf5bf1c1a572f9404b28c77580fd0489f9d4ed8b1427b3b1fb7a2e82256daa0bdd1f70008300d1ea243ddd1b1d2b4cb8102bceb51b9b2c0fd0cf0c3192371f93a705274e3cfa98aadd6c6f11a50a3860d555eef4bc21688cb71f13dd5c3186b7a18f9cc43f4e17fc601f13623fddaef576063c5f4605ee8c7b0190cf82cf61ab415a695cadfd9ce7a7f9f29c32a9f200003a0fe33ec266fa48e7a434ac89de2b2bb578915400ca05127ee4c7102ff54d6ad2f170f259fbba429fee0e281df24cfcbcbf23d3f3337e6f6e8b8dabbb5b91563d0524f05c32147d9ef91833e20391d2c25c517a0115350b607fee7dba40e5a4b6f20e3ffd75955204ae8a9bb234c07e3a4f68017d80173a16c0d2dac313a36f366f7a0350da4298c85fd8905f88dd8cd13e59cd497b25a06f1750c794596bfc7aac9d5b63686adcfa82ecd32192ca5200f8b623eba2654ed0c57725911842e53dc22eb65be5fdc7e94e03bc31bd7df7ad2f3f7adb7abd55efee672f24e050e5820257179dc10301a530b501d5031c98451b1540be481f7b3e4a984bba3c49f759b0ae4493f2ea9a35c03fc11e2b38d00e1645178f27ab69ab7c7430308e009cbc285f1bd385a99be57b405d66b673acfdb8ed0f00f669b908a4f1e63dcfc4869c59cd6ccd4f5f3d5ef803f607d28e116c95f28f9f582f92bb2c1b05cbb8666b03d6bfe1f575738146cc053c575b34235f7d6e8e620ac0fe4d80a0228fc2358b3a90619eb94469b47bd6bd16605fae3b3dbe4e599237dd11af32ecc769da79bb4a1cc01f9df6ca9d8b0cd26eb65fb0d0ae6a63c0bbfa5f03faa7704d6c5a29b567857588cc56be7bfac9ec083dc0bee3b55e95f350168528e89e91a0baf3cc212bd70e700c02a266b77cc5e4134fc2c13655a21f4c26289ab40bf08fa5088a9358b96f6f64ed7d6a30221d97c4c507e8578116063af10f78a1e96f2c3a66be5c670d45b8018ab34c2908309f1da9a41ef06e47934b49d80b8c65002072de1773857ff0eaba823e543110c23f5d9f11273d779cbe37d0f78d5f7ce22bdbee57bc76ccdea8beead60b00fdf5d22233792a8305f6397c5180649c79137d0a7031bc2cb1d879bd2dfe9399da131792db98713c2d94fb80fd1307d36c97d1297bef66d9f39c6f329c3a1701e62388304505b7e92656b294137e37425d76a887e532403fe99de55df8644c7c9d7ae78a2971a6b266d7b70b009c56b790f733d629e1c4ad0111fdc49bead152780b00ff6ad006edd99416c9b8fc6af15763116dd7cfa6e28cc7e93267ae1de674e506f97f9c39da08b917b6e87709607fc491c2b4d59278eb3a7299361aa1e1fed0226380fd428508dba3ae494b8f9096c0d22b985e58e4a407603c2d97ec865456be8a6dd99367f91c7cffb11b9f3760ff08f43629902838e460e96f6d98a7b810f761c505004ee6d5e1420c7bfce5dc6478ee13760b1dbea5ea0940c109460a57abc795d2fa65c3e0f8b0b36789f5d93c998ed3a3d31fd64d3770b8358abfd98c783e3a7deb793f00f43290574cb7ccc1132cb37e1543b5e15be34c512040ff885557e9e3f564be4fd037e0cd24bdff6011f21650ba4a054a5381956fdcc9755f22f84b3c728825af12d0be79cd9329deb3cdae9ec956375363ad7b8207810535ced3f4c40683fa17895af8679de86e74d6b027ec1ca75f947f13aaa8e993914827814c55e4bee7b9b304f05f82f01b30dafb2e7a7e216562328831f51faa3007f877ea8e2c0d8bf5197de4c6f1238e067cc58ef5e400fbbd41b1ce8ed5dc4f40f3cb0689322e439b6ed00e0002dd14bf74d9b63b815cbaa2408a171ec8a569cb07d00fef797c41f2ab88cb6c379a744d4a47bb8b9e355a1ca7a753ae76219885c7fd64ec0c7c24d0975af221b78fd38d74940d98def2d11b53e30e1a4fbce2e9df1704f8774396ebafc2d82fee9fbb99b188ebd797dea8eb0200bfae4b32510f30115ae9fc14de67c41e9861c5630a88375e5a08cddff2bd7e3537f84a2ee5055d69c8576a4092e8dee5644e714611568d07e17551c6c851eae47d80fe240dbe47e0cf75e10c9278f70d6e2389ada171141d40ffdab93cb4acb29c61a3e71f1af7e82711a71f223e4ec78abe6aca2294da1da48cc1aee2a4a312caf50180b476db6b9526c55b081adda9e6e2803f28b5d4310300402c9c13cdb21b9a345aba5de8997805f23ad7970970c109596f986acc1529f3b086f669d84d9dfb1d44a200ff6de5db0d4b6edb34df1bdfb61b3282cd3754f2be02f8c3b3c13e3f98ba424c647428b29ce819eb872b4580fd2d2a6b534c0777463946dcc480c943c24bd4c4e32b803f469b3c2c9847f6e49a11c2bd970736998b4c00f6c3d0866cb3f0d346e71d5a6701aff5c7693c62dc80f9e3f9faa8a7bf788a2b4348deaf3554dec4e68625c0bef26239a22ed65fe149bbd65cd3e0bf9c454510b87c9ceed11c8da19c4c11154942ec8c2e25d85dee1d05905fcdc07738f35f2fae29db08e56c44e175d06b0903f8dbd83b6d4c75c97c7b1374386bd6472a747e460e20bf66574a29089b880986e22d7bbad6c74befc5b8911fa7e3b9544f6c785153ae230298b5882187d3f3ee00ff4cb29f1d1f16c16ec3ad393a31d895b96dfa6815b0bf9bcd7f54448ca7f1b3068bbae4eec5a2ef10db01f61f2f02ae0097a5d905f067ec87de71767b2bea970109f467aeb524efd139efa764094fe488602a7a6728ec01e203ca2f0e6a8dddf34208d4835f17d75552ba6fea01decfa6c1fb36b1d3c73b763da157a240a377571b8099f26c124cb5f16e4ecf9d992a6ff2193d2dbf8c3fe01230179692ff253a08431da91f32de97c45fc35d09609f46ddc8d89b83a97de31c7493d8bb199e1ccfd29207300f38ac6530321160aca75d6f70ef5595e3977800ec87cda0f37c23be51126bb6602e0b2aeb0b2e965c00f9f17a3f7e44f094d57343dc7665d63ce4c51c5acae7e3f4237d1133136bbd226277fcba84b009539227762dc7e919c33c65439dec4b1936a4f41bbe09d718aedd78739cbe439156346f7e35f9223ff587d1a53c872dbbf780f8eec6f28576eb022e2dc50fcf864d6194b8da2c8900e067456abf84685aea2f16627adb1a32a19b6edee680fdbd39fc91175760cf57c94bf61df56a507fafe669807f606b10e88269f72a46abeaf3fad91addf3954b7200f9dc52cc9797c796a7bcf1ea966fa4aafd43117749407c5c5971f32d6bece57ba64b634df7a00c0b36440a80f579b24a2947d56340b08b299db67ba67d5f320e1fe01f765ee326249e15dada2a1899324ddb7e66857e0488bf84ef6629445fb8b08ff18687435e85e84c8be647807d794d55e00bfc5c409f094665f7573455af39d832203ea3bb5133e714cdfe468ccfb3f485d952c2537f174069460cf12ec5ad5d56c8e21b8aaab9cda8853e5d770062d9edc892879306d29fdd82ed56dadc6673ddaeea02fce378f2a179b9f65e3229dfd18486ad418b40461c807ed89aa53b43a02e87a4656faaf1d9563ebfa7d30b90af570ce099e0d77c71181f1f7c099ca45f0cafc007d83f8f882567123f68fa423223244a720ee90e556f03920cae31b19043f522223dcde6116a90e9300ffd5de0259c3e0d50c2b300232fcdbd569d590ef448925a4082a38e44633ba78a92fa5d183959c20bfc072942dd80f3ca1149f2687715daf30d2ebe77c7183d85cf10f701ec53dbcc9a829bc36126394a9b52f0181eaf58bff6ab00ffe0edcc0ddb73894778ce7dbdb91a8cc506956900ff3f810ae3867c781be515922f523e15fbbb8958ac80f859aa99f1753d05a8d28c37b5ad515edb1d250d4c80fe640f9608497829b2324f801753f4f59c4ff60a1c9004a2f461ec2d930591884f90439a084b568e731e13e07ce60909f4c594cd1388be4f103c6395569174f213c0fe70b5f77dce5942bfc59c5227f4aa9a67f19a6517805ee57b8e26e09ba4b0ff5bfba78ef53d7967b65901c50b3bb2c3e123ed1a0d3a18f3b033cdf37c5b11e04180ff8637a55c3ad62432a6f6f128a45e99b5f34d1de0166356cf02e28513558e4c993fa1c6fd127838f716a0f8e3da00d2ab114a75857eaa62fee2d29b16c1497780fdce28c7769e72b6c982d10e2453f436a85d005312603ff8b5659f7b10c5bb28ed5adbbaf0fecb594f612d4069ce9dd74d55d11e77d60baeb8df0f9d7f7bf69e151de03c3aa2476748a5424e11eb51196f3876bf01665a380009ffbabe7a8cc382e338575e647c9141f290d89d0f80f38d827346e4ec93fdd26711add00ffa1fb4987ba000fb2b1e2a12371d06736beb4ce7407bb956214b99f718b0be3d8c6753f86ed65b673d99ca0a591f677b4e00b8c099339f9b76d3f0d3ac2cac8882bef091a3c75a03e08296f97e683b4647772d19fe2523d21bb3fdd4e406db80f325cb8b5c661ceb3c6e36ee41efb9134503783700fb074d951624eb3353df536b02342796ea0cb43c4680ff71fed6e2fd7ac57b8cfa3ebe1b06decf1c432fbf0480955a16a8645c66e15b6d0705f1545617f3b2240a0212b53e0a973f91008579da0becdda91f29182237fd0af8fde8febb3211247b4cc217799738af53de6ea8b1009c9f528650c341260bb339f118d6d2a4a6ae05a3d280f82a641137c7c218cb63c8f0aa09ff0e8bb3f5a032207ebbde87787b59ba3147d6574b8697f9ce45217c03c0fe6827a6af1ff79e500bb15fba56bafc40850ebf1ab0bfd474f3803ec4862878e36f5297a86c64e6251000ce7f6f17896d3c9f4bdf5bbefc1054ef44a9e029ff00707eadf554cec7e47a7a477b7e7aa1f9a50c34a33bc3b78ed32fa7b94faff77b91f2bfa5ca6efca423d220aa01a06b933f6acb8af4b068a14ecc54e00e0da9309b00e85f8d5a2a6d196511c7d59cbef19107cf73c79f2f03f6f758cc210689c8a3a95bd061c533747a2ff8747200fed19448f899d2cd170b566cc3a34a47f23db48e4a5a802431904c914b8394899d00d15a168dd0b6d2bb2b80ccd754638f33cc959beb8f8d933d134507b387322fb01ca77f98c8af2fb8ce6427281e3de6989b9a5856f50690263a6343ba7b5fe859d06ec18da6c7e6680a1616b2809cd73dafc2fcc5a89ecb596cadb96f5a85dc4584ef5d07f8a78dcbddcab75fdf4dc09022874fde7039476008f0dfde5f936d374e1b667d2ad43593b951a6ac1d7a1720df5d7d601609eba30e8a9b75c14dd5145792e87caa00fbc3b0198b2443e9a5a36ba0c3e16ee35671582c20fe89719ee33ab8c41b7b69b364fdacfaeb9ca391bb80f3b5a7e6f27a8c85266681bc35445dc3cd8de17ebd5980df176872995e1f23c37dc3887cb2d7f68ce0ffa3ea1ae3f2ecc278764bcbb65bb6bdb08c6db55ccbb66ddbad966d7b59cb5c58b66bf9fdfa9ecfe7f7f4f4dcf73917feb84e5024101f8ed4575f33a1956657dbf510c258702c17c91a0081579346721a89cc26234750cc6c40d30fe2eaac3aa03f6e5efe966944736e7d8e8e02dd272611c2711b0ee03b3f63aecc55d6efbc3346441c1299214d3eca2b00fc9388ae33b76d57c557731f6cfa6b4e6dba59ec71a0bef8183544c171a58af2845c7dbe7b13010fa75c0df44f49a376a6c25cc2836a97daa1bb72bbb5e24f3ac0ddb91bbeb83ef9cfc9af0f2beab0d74575c7cd4359407c89d655b9712da425acc58060e7b77989e2c20e03f08f605deed2f86b748dde14e63d4bd2ac2166f939801fdc0a80b971c986a882455890b84ea9a84994df002e3ba4d97d8c3829b7e665db47e463ab9460af265018fbfffada7d0a0c4d3fb3f17ae80a412a260115a1843880dfabaa3a8b4e776b9bc05337792b7641647d224f06faeb21b5ba78cca049734285c1305d0e5dcfcac41f00be77d619dcf77d39818942bc14cf4f3859485ad807c82fef6f6af648eaa07a3dc5c717b1c8681303fed901fc8d3c0ccdeb454f749570f4e43bcf5e9cfedfb73040feecec7faaf1a7e6aad477367ef8b590c0d5f18d11a84f843e8888bff1fa9e728db37d2b43130c8c1c1007f433ab6d97f8dbfd86a42e513f09cb28bd36bee9dd010378cf6c46fc8b57faf06c762dec5e9c374875a0bf03f927f705e3383b99ba08b632fa57e8fb45539819fcc6ffafff2118c4c8f86a1299b2f3a22a6fe81d9dea2406c427123ba115ea67023b3cab9e2d6cbe8685b97501805fd43e7b6033fc7e1cd0ade8b6bcf04ff7a264c703885fdfafb64c105fc97a4fe533db2ba07d0fb3ba31008d622c7add95e250a667e0894aae6f83d3f6cf5d4d207f51656625647dce2c5e687f698f55ce1ce0784d03f055158edab4ebe7dbc6c5544d2ee85697ad5c5b5580df9043a137b67c843bcbed904c29b13060bb1e9b05f56814457d34f2271ea57130f782e526b67b7581801e2e8659e733f3a48b1fafc20d2b6a00267294b005c00f880f7cc13ad05b4b5a194abe46d12545cc5d2505a65f560d1c05542cc01fead975f972e4437073d8d003f8eba69f0de5a39e949870c2193185f0bf95ac994120ff96aaf8df06bb5137e7fcc596cd3dbb9c96aa3f07eaafc23d1d47b9f5c09c85270dffeeb443ce6f13acc0e777955829eaecbe8559ffb9ef14cc5611917dae2301eab7419bf4370ae4778f8db67fe713e25cdb08959effbfce955264a7bd416250907920880a839934a0680bf02779da92a438abfcfe3f5b5d09de84d27dc367e3007ef6ea2fe42dbb24ce2424547d1aa7a450f8e1bc13905fcbd237b3a6b597e1d8978533ca88481be7d61c017e53e033cfbf2abeb51449d6ee0b8170b27abe5c41001fb39acf530888f98b3f4866eeebb67ceaf4b46d0bd40f3577d2e406f94f558c3426e3f3a1e493534d96c7ff5f67ff98245a9319aaf4979433e89ca88caacf520088af424c28193f9136324b69bd2a6eef08e959f07001038fef71164b374430ace983a346e08fcbbfbfcc5e01bd68e66535bc1e2f2bc74ea6cc5ce7d517f69c7708ecff5fb74561ab4c5ebcad70d082f4140ecc7b299b6400fae78ace6e05115e07c86b1f718777d78ee6b1efb2815bc2650d3255589977857f99bca7fa2ade372118380be457e924c924a1dda3ed7adba55a4fd8659652d30580ff45c32f30a4a53289ac9f8df8db487ffac56c2411e8bfb803c2b8701cbbae2b907d2583f1047f1ad26c03fc5c7520b426e7267c825680fe7cf4849313b35806a0cfcaa482266efba7e70025b3089b1c05311df1a702a8df8ca364a183dbbe8db82ca22d99e541d74722f501f5435c3242f3be48fcb1bdd6c03c592166def3f92740c4ce5ca2384c0365d24ea67a1eace6a2943e9ac601c4772363ae93d041c71f818599f7788e7dd54b10c6407dd4a0226aa6bb67f4dc11ac5ce64514b84750ce010c3118e3c6892c33f55eb19ca5ce7d920de8d2299c05f01764688361ec942261b9f296cb0525139a982571403f80a021e7c5caa334c967c6c317e65e10da5c2102f00305d70cc31c660f2f1429adec11c3ef20887517017d596c54a77c7d96a6057233a99d652a11e56e1bc8ff5f31b51e279831e48921a89ff88ce33ab76ae303dfaf8cab187cb6d111398821f47bd0a46b304b3a14b8a17c705005eb08bfc1079d4f4a93f85e298101061ec8df9c5b4371e573815f90df2b6c79fd56eff1f4834f02f52b7f8e6e48f74ca79d76dee4e28feeb0a8b4d7febfbea83a212d1d40116155484ab87e3d55a6fa8f14c05ff40515c72eeb0ff4105b3d8322cc44a53ad5c580f9bc2b7165532492b026b73ad34dbc48249a0ecdf0a0fe6d7571d57cfc73c61f5f8e7d92a060c335647e80bfc82069d058a90ecd3e7468c8be0fb5b647fc6a0ee8c390b563b1f30ff4c68f3d791b88bda978860ca080fc0e31da3d6794973cd9a98d798ce095352ff5b72af3ffcb271da8ee63b578e7194675f368d581848e274740ffd391d0b04580a3b58c6c55d09012dd264790a10fe47f4142cfe20f74ec6552456364ffeed2ba24284300fdc1eae8abc99b7755a6efb0a5bb530505e6f0c7bf40fcf3aa28ea12711a56cb0bbe6de3971e9031756c07f02b486dc7dff13816913618c93248b766e2d175dd40ff7d28a32ef0f2e5902333fd8e829f3b796b848c0cd067c4df770e85db268e7b11f206260811df0f490c02e7fb98e05a79fd36adf43b59dc4d708f4cdb7ae7c66fe0fd920ccb8cfbf58adf3cfdb844c57c733c525703f4a14105943c77ae9c6577e289c5f44a5faef8297e14fe7fbde494647e3827bb6944a2c540f64ffed247a67c02203e8bb43d5a12775292a7e4c145177bde3136e802f9bf2ba54e791d45939eefbe6b6195ad616b771a1788ef46db23a9e2dd669ca75fd69772f9abcb83eece81e9c7ee09df9f02db7d57cb685c42040fa1040f76f91681fc53de9027279bc4f2dae6e187c0738814a48c0df08b93d6c615abb3926f22f6975ccd7a0657d2bd1e800928bb96e1e48dcd34dacefea6d8ac13bd98f8c804a8cfc42bdc6f940615e6aa602aa2855188da4cd7f001fcccff75805ec7ec604af0c136ac554a55cef46218783e2a785d9c0d6183083d93a2dc9cfadaa479b3f700fea58c1c7ac6defdfce7b35f3f8fe02a8ce9e0a6e7f2ffd7fb4e967bbdcefe662e188a65465711a7ca6abbfa03fa830fad505c35ad5b50e7c7b3d2f93f7a7e0b2501fd9307e9d328df9a766247f9b73f5336819dd8833200fe6b199518e93e69a886764ebda4f672f4c3670817e01fae74202a7d36622a020c5ddbba5d740fcc496e00feb48309691405624f2a84e12732a5e37e75f18f7780fe39f68a95af4fae7c0449406c949eb116ba782800d01f0edf17fdcca7672cde3fa6e8ebc7802a39e74803f4a97befc5d6f0bfaef264652ca6c78a406b363e5001fd4b2e3bed14525c0c6aebdc566ce87a5190cab439a0ffc8df444acd4ecf905b835d20917ed435477a6700e013645a7e142478ac35870bb67869c1e7161d96fc807e288323d8c36b76a5a8d6a7bd8b529e57dab9f701a8bf101b4b5227166dbf505d84fa2c782b6d315bbf02f1070663ce336a7ad9ad46d37da6b4a3bd1975a11b985eaf5c8ca7a1d86eb639fef77146d913c73b953201d8dfee0f34d4871f642b2c0e90e9388da1d827374880cf773318bd4c877b329b042d325a0be2e7d7ac3703f927d7d9467b034be7766ee247b6dae27702c7b05dc5ffafa793ea9f1dfbd73b5b70696fe16f7ed6579add05fe7fba2b6cd4b491b839f4b3843dfe410b1f61565d003f556f72b1a127875dc4fd993a2d19961ec6834f09f4bf1d2f42b2d72a37ba232adfd7b94798c8fe083c03f8ad206c91ce5782e6dfdb1af63570de92a22beda8c0fbd359804962f5434497c87a0b8a6e68da09246907fa133e0eba1f42fda4d34eca6a584e8869d999af8300bf9887ff55ee43bf977c5b4f85182565fc5ad5df39407f9d6cfac616209fa4c00a3b06ab1886485bc7ba08f0ab2d3073f6babffbdaadc44a3317a84cbd97f01a80f889432a2d53d699bd19e587470b95e867d09caa0e7cbe1eaaaa05c60432b5b63bf4c3fcdb42451dcc22803f38b95d411c0ab0b89de8649c8c4467971aa97a00f889a6a6dc51d197111e15d7beb279719966dc1107c00fc4c97fd5288aeee3e6c7131ba5a011344e909605d47794e507cbf7b2d40454b842899aaaf74e6995c340fdfa5e366e231e01416706dbc87d38640a1f975a18d83f104d860b3c0e4d5810d9059b83e407d898a64fb780bfa0f4d8f2289cb1c2e94eaf483f830b25dd4010a89fa25ff757973ea7655e1fa02819ff62f49a093403fa4fb37b173c0b16713819c2e1cb000ec532dc2e74803f6c95844af1b9e03dc4e6e034a942190a4171e607f4edb9042e1b94c8c2e33f67884e3a616adf89c0c700fe0e4814329c56af84d6bfe8044bf7fe0782a55d2f407cc751eea8539471b6e9e30846e1b4c58fdc7b7901a6b48bc289a93588ff2c51fd40e9d81b8c50a58bb9080c88401569748b2567b1645474db5e69ec955093ee00f8dfebac7af58691312571a7f7bf25fb7a923a46cf01fe0095bfecf3a7a668bdf24eacf5125a44392f2165805f91506079b1c27b1c90e4fa1aeb47c46ef5438a0e88af9de2423b50d07faadf695d0c571ad218264345027e42974a2bb7bfe3b5afa41f98c753a064692bf8cd819beac6d04ea87c9853a9b3ef7a7d64fe60ef27527f8a00f13b9d422c9e6c5d16ceeaf744ae6951bf54c981fd236e7f7dbc6c2a92ed5806ad2211ced0f02d0e15a82fab7f7568e4ff34cdfe6e349f9ce432e65b4c540d089c2c344e17427fddc4956c9a50c96ed0f7cc86e856fc7f3d25aad721f0c43ba4ebcad9ef6a234be8a9cc1288bf3aa895417c0548061619b35d8ac249c1bf364c81f88635e00af916d57369e8d51eb8b64dd361343dfc13d0d7d32c7460dc466dc8e53bb8618672f37ced2b05ea4b8a6b87845f591666215f67d19ef1d677137fc202f83b6e189485b0f39fc46dba9f243583e9b9495adb80fe0227258d6b644ee2b2edda90e098949f8cfe990dd027c7496f99df93f3d851538ed8b326f30569d08702f806aff7e1906053cf21e579f1bab156df0c4b5437803f6c1544e735ce7fd1e2fbb697a50aff371c6f5f1fc08f156a084d1d3a9c11c8ab935906f2257d47209301fd63ad84e8eea039a2e662b7f4ad4a8c9afaa3912cd0ffb13dd086f084853240d065790f95b804c63d0701f11b1567398278a283093595ec9cd75766fb1e2115e037ca82f6c693a08a575e3069045d9bda4d27d01d81fec183ceec6f4c296744841cea78866e09c216db0fe09286e4e4c38fc1493244cffa5999e3694cecfe99c9407c60308fde2ce1ac999b67e0af4055f063dd8de305f441e4e52497bf14d00a1ebe5a59c0ebfb4743bbb302e70b39c295e2c7c6c557a8c22e410894b77a614e43a03e3ad0b8f8cd9aab9d9d1842769bdf1cfb2cfb0d0a38bfe7ba9fa80444714baf074dfd8cca97eab48ae701fc4692776399ea85af4e8760545cddfe3456dc6218e07f4f3b6ee63f2d1938d7bf725592628a92870ef200f8c6667b81440b1bd7887dae21bbc47bfad2f34e49a0fe12e38fe651bd36bdbf5195892c7c1c33bdecc002ea3f7561a711591fad2a948e7418d41fde2503fd92007f31a18d9d348564c34fbfd5c1c42e70f56ac2fd0dd0d7bf71a305ad32dfaac712cc35dc31bd57159378b801f42316112e2cf24698ef4d43634f7d5f7d857c1842ffbf8ed9ea23219274f756682a5bf3b52dd1d6258a16d0bf7278f44123bd21223b8c658c6733d42d72529703fa92405f82ec5dd63d08f7b95f786b5e746e5fa93481efff50df64e9045534f7ef7c59b59da02ee19c5517e0579e0eb62cdb9986cdc4288a377dc40c995a4afe02f1ddea9848ccfa42302b8fe0e3bb5a1b1ede4fd786803ea3b6fea1a70c772ebf53cced130e541c1f527f15107fbb9e95255853aa3ba908f51070a3ff7eb4874500f8eb2f7eb9b97aae0656bfda3ff521e9a143b3bcff0ebc5f35f390ec0fb79a263e88539752327e482fefa781fe7ddb00e6ecee3c0e822671f5c95d2c40d8cc751de03f2aff95a3eacbb03f7a50c2c47195998ea9440802fcaf98aba74d48b0506e571ca456d5ca3799e37f4600be5d43b3e31cd270e330aea3ff51837e08a19eac1ed06fa3359d2b48696154ccb13211f84b2cc2e7393803efef0056e541288cc57863f24bad244c9423b3fe3b005f69ed68d430b1b120449f5ff8317f01a5206a3505e047d342988be9ac038594abfaba3f3a198486645800fd70c41d57093e536fb5eb627d91eb99111f824133b07f8fea9b8b7ed311eb2685be79d9b5ca50cd223b00fa8747a126f3618a158d6ea1907cdd669bca854838e012930c876f398d0748f3270718e676c1332161cb2fc0fb319fe9a832e8c267f9a340c08114bb5c468a46e50cac6b670bed1659c25b0bea25a33cc0f248f856e103f5ed2cc7ee33e34a92f5eac578c3b03c72bdf647405f06517f7e80f8d1d12b9c9272ed7728bf376cef2bf0ff29846608db98118422d2ec77400d327f3e1e6b06f403f38bad8ef707894441ff8a8d9fa5256f72df8401fd828fe8ba95f85774f594985b0fbb2452c9cd0571403f8db399f7b1ac3427f1f8bb93d3e6d2166bc5f9e96700fffc11616723d68de22af5ef5a756b71d88e430dd08761484538e37ae2303d2619fdde9a11ffad908b0a7c5e69d8120655599578dfcab6337506ca91ddb61918a0dc94332fe717a0879670d7cde0bafc143ec9e607f89b318e5ff17973213f58b0c265bca2935e8ab72b00f802bb6d4444a69e191466367efd4729f4d0d5b23d203e644b19f5861904f4da9e5066fac3f55e667fa104f25f20a1a144fa1076f690f8f7bc9e81f4625cbf1580ffa78531b84b56d0d9be0f7aebf1c6672066b0e806fc5f3ab6485f547bbbf3aa2edffb7a0638a7f4b22b01f880f0d73226f1d4800eb81325fa0b27ecfdf82d0ea03e1ccfb6a3b85d10d02074747068db4bdb84b94404f8cb28fc4c2fd188c7ac6f353e9a6247854d8f2d05c0fe44b5a2d083b13b5d19b17015282bcb23234dc904f81f5e3fd48b010ae91f2c3f75db1fa1213bd3b1c581dbc7ca4ad36625d46d1c53bf9f8e65ec5d782aaac202f84564b859329d5bd106445d66e792839d93b202177093b02abf15cbe1fb94fac3ef970261e656dda7d57b007fd58e579679bd73e43ba6d25c7a5062be97719d03e063d5ba7e569b2e5e3bd5642dddf62dc29026b4a03f5169af8be16ac9c9b828978df0e5b2e9ba72e717d01ff12465e62f08acf70d8439745cc3a17b50ad8a01fc43024fbc44f53554507eeada9bfdfcfbc7fd5643805f5d81dfe8972bf32d0d9def29db1cc7f529294f00e2d314411387547ca52a81571a0baf86740895ee2dc02f34c5f7391e7c0f98e728de7f8e532339413dde06ceff678ecfe50c0115b411e5111c46d3bc7ef98c4480bfbac32c8e46f915da58d04b99454b96927411d20ed85f9f8ef5f7d8cc4634967e77754328abd9fd0c8404de0f0bd66313f5077aa8f131b2fee967c8aff503f8007eae77e24a4ef479979f5d38e28b700f9e05cb1acc35a0aff464aee0b7bbb0b5ae620f3a8f14944c562502fa7f2a2bc6bc40dc3104f4f035518df985788ff229e0ef2f59cbdaeef393a5aa50d3c53b9e8a696d287000f967fc0ad3f47adb44678df7e327ae90073c11ed56e07c1241bf0b2bbdc3e5bd6908b0d55626486aa4f607f4bf84020a9893f4dcbb7021743f31083850add9e2017e8422c177e9cada2dd3ff2705961dacd2e8c1181da06fc67ceecb269e3fc3b4bf250f621a7a654b0dbd05f0837ce4a3855f6bd4de2e5f8ced31ddb58f5b6b0a41fcb09fc5b5b889ac2119fd0f7f92f12f04bc0e59203f0efb41b142228f9c71ad8a0f7442deec9dfde907fc65f322ed6c15ec6f0b964a2a616bafffd8bb6705017e5247b5c0c71173c3e464f937c6cf3401b784943e607f10b5c2e554711fd19509c373d99615f16e134e03f5e7cc3e8da78eff929e90cd64a0226ae5af43b465c07f2031aee25dd345fd10facda7dc6ec039a47b9e10c03fe9b2fe7cb9e7798c2f3bd6f18fb81a1c36a03802fc991b2efae392661fb085a5b6a2b46907263efd4b05065ca3264b0ca963536cb0ca1abe237fd5b6a5eb8b01ce67a2eae8bbc2353ac9213812aac568dd7515928fc0fe999dc9e1ac7cb3616723e3c7b5fc10e19df2e52f807f677d8f1220ad9fbb101b5524d21e0f21e2182b01f083ab19eebfd707795f823149af3044705dbc863b01fc41d94a4182ba96a3758dd613a231127210aec21a9897d2ab79d2a63b8da1e1587ce773c599fd279a911ee05fbe2ea0bca3a13879e83acc41998456e8ae7e7703bc9f80959c06ed0876de666f7ff4a5d71c4459b55520bfec9f1b7f7a2780f9c1f568c17e6fdc1972b3ef17808fa1985104b65db238a40ee1e7f47e89e27e2f830ee807713b0e2f6e7838d57fed5a05b69222cda5a1c900fe34cb6e69ced6fcd6c1ca07ab6a416f1e8ba63e3c600054e8da3fb361de504a7c22ad6ce5aa8903aa305aa0be189a61ba534aff345ebd387af7bea31945915f1b88cfea4571cfc8316297064f92ab6cb98f013e1fb8007fd05034e43849babc5464026c220d22bc0837013ea09f79d3f6469b671b0e09a85d155c2c87cdefade202fce9770e4c2f96f18eaf95d5c1333613f90bed45fc803e80bd79c5f6e5ed014fcce7a9c0b04ad981a5db0be80f62d0572b392bf7259ba04bd6b1ce86f48255e481f84bbafbdac6869abd79d141d38c24eaffbbf4873ad09f4c84859572b999d55eb8f73bcc6d9b54e753b0fb02fbbfa76c24e89148e2310bddad70e5b642234301d01f0e084fba6e678fb23e9941969d15fa0af420af01efafef37839ef38784dac0fa7e972c44d8f26eee0860181379f5044940e62d5d950a256966b01566e53d15c04ffdc8fa0e413193a452180145db454bfbe743b40180cf09e816194ca71e7a194e9e7484bf89483ee5d501fdef2fc2787a16d8b5e0ae0b6deab841b98c840413805feb863f40176839530e20fb473cbbf045d15f3d19c88fd54ad1c9d5df8a8b60b2af7ebe797b92fdfe840ed45fb9c7944ed95285d75aa82bbeb123d0aed9c1af003fc5fd2dcb834511cef2b1c66dcc8661b0cbdf211c989fa04e584c000397d8ebb271cf7c51bf1d88287e0ae46f725d5e9d10723ab39258627373e8352443914e15607fa72af7e9ef53d7603ee5633f3d6ad64db71e03faeb289c1eed7731ba725aa1347ecae675988a41e280be484aeb78800fcbe74467afd9daaf6abcd2e2a20cd0bff162be835d1a09bd86b0d6a06c26eb53f764e0042ec852e4768d1220ae3f88ecb6f06a173042466cd702e61b98d3063c0b8bea84b73650e2f125360f31957302cff7b7074296a22aee394be904094f071e7183cc35a0bfd745d1aa0f0e1140b9c2daed26daf06eda7d7507fa87dd1c11df381377afc66fbe0b490a7d1df4a54800be951de55bb171b8cfae7649ceeb71e54e24d31108e81f6aff2da4b4e8078ec77b6af1a6b12474f8d76b00e763e3e09b5c26d2c977d22d7d933e31e7b453027e203e9ba19a35cd2cf1439a334835449fda342a708a00f8b0cde3afcdee0f96392e49b736cb6fb14df6bbd6803e98bb3b502974e7bcbe8360bb255adbf89460e41ef0affe29b56c836b7d8e835682e6c0db1d747e5f950ef85f9ef1c650cc0e2656b17fbd44feb428d05e62cf04e2e337d53bf965ba7484cef56dac509d165f827fa5c0fe4aa57e4ff1eca2622840e9ddbc271b98f6f3f6cc05e087bce19fba3e6f953e5e9ff006a3413f4a331802fd252e5a4f29bceb725f8884de075aa7b2498edf53a980fefa7ac098c3cbc984c7cff727e39ebb4e10542a50df4660bd7c49aaa2fa6480b363a8b8699757588403e45f5dc255a829394406e37f2c7afe4c1783b6767df6803f8761ab5e6168830702930922e59bc003960b0a501fa25b32a92bcb538e9eff9c7a29d0331ba1b9c402fbdbcdd46833e1f5cd5fbb2e8b7eea5c8b1a421b803e09693507aacbc2ca2ec23260627912959bfd681da86f38e053456f1b3f7a40c4c7fc42549e848b3aeb6b02ed0b3666afa20ecb786aefee301e634576bdb200fd4f922681c8591aed89c1bfab2b084f4a66b13d2c603e50105a094d03bca158317b7e0e49e054741fe40cd01f11ffb5a2aefcf4059b40e8b7f1ce095946ae9602501f591d2cb44e97bc630bbf52ac4284f786ec2d5d02fa77ed6dda05a521df025bf3f07043416125d8a808203f3bd2347010a11957b3f67f3ef0a0589656218101eac3e92826e8c7f9b22f495c11667f155b344d28bb017dc6e1161ff1b2a271b253f253c6dc6daea5120f1480cffb10329dd0304eac38ca64fd1c2c7ec0a3339401fc6101684457082df40876864f68686bab8fd3d211407ca9ec0ac24668be740fb650666a831ffa16d604f2a3ff2695e382b7b2967f69fd6d3f0dd9e49eb18e04f0497f18a42362f36b2a6e1d4ff374ab747df4d332c01f2d78fb341e2b9430a83d8a2bf6ab380c0e8fe00db847d8ffcaaf84ff3ca34ab47439c9ba5738a483e71ac017b05ef73314f557e0895bf671b8a71e2b634d02007cac3557efa5f66cc36dc5fe308eee4ab3ecd236aeeeffeb8cd4e2f07fca14d1dc5d427f777faddb0e5aa705f8d19caa4606747ba53c27beb8feadd8be0b8746f04ee6586bd7e10e59a642e53c5a339f1ab5430cd51b60407b6cf7ca813ef608055519f17192bd3dc1c57820e05f7c3b3485bd58844424b16fc1b71be7df4f9f3a05ce6fbb4071c805843e2ab79a6aab1f878eaf491d3e80efbe8f57b7ac88fccbaa5f90bcd927fd1b4b153f15c0175374e238320fa01d0f1a046a259c610e8406e581fdd16e8086a0ff39c3ee7789aedf3fb855a2a8b770203eec28415eb57df6d95618bd7e15e0bbab94d42589ffffba61494d8f4b6790d6175fdf90ccbfc27cf797ab003f2af022921d7ec46b1e9cad32e6849620b127a905f0634711d05d796b26ff72e5261f844ab4addcffb64f00fe8316462fe438fd006a79b12502520d1d1cef3b40fff6362643c21eb2a63cfeeddb2879d3b75e13c71d20fe50441b3cd89b262157965351f2ab26c9c079a180f894ba4c01966d4302b491ad3289a0727245342e50ff2c4e1a5d43be2ff60fdd436843a3d098d3eb1402f029372ef8e5c0d3b2d91f2abc90c109f03ae61f16007f1a8da167bc67cc877f1f4c04f21c684e9f5d33ba007d1a214d9ef8b043338c0b0c5340d2705b79ed381990ff3117606c933bf5556bdef9c590f527ac14691d03fa054ecdf866abd1dacb91ee337c53fb80664c856f00bea4c680960187f641d334b1abe1fda9c047bec833e0fc0f4b116e9904baa94bf58de0a799a90ef4d18b03fc87c1fdaf2592e16b869d8ebdeeb045062fc58365c0df767a5b0c53a283aefa76404ffd7373e5896fda08e0efbe7689055f2318d964f9664c5eedcb4e0d6a3401fe505353e6be18e2789d470361e21043d4fdd9ac6be0fb210b69d8e58cb8c2f351aacb253fc2570eed4502fa001e9de06b5c5af6802edd967e9335fe3d0f495c809ffed23fd6bdb558a34c47761a64569f0ae1007f0fe00f8eb42770fa137081c10f2e355d3fada909fd4c007fe86f2fd523be7705079f368d34968f14b70ecf12007ce167387f8965871451d21f9fb5b7d68b9c574b1800bfa6081ab3784140e2bfce4b648584d19a9a2fa901fc9b41a17a4bd644ed956884c6f55f05d7041c137b80f36339fee1a1e07347fe215cfb028cdbef3b5a816100bffe26127eb7c7e231a6a34c9d59285bbc99a48b05e017cec1d2d90bcfe4b4ef53583f0a6d6e684944f303f3454e5fe7fc2f12d3b818b11ae71f3757a163a32901fc2051534530b8a94e85e78734ab5e76364a773814301f4dbaccaf6abd31f7e7d4a7faac66ec976247fb4b801f67942e5848175e7aff2efc54ce956293d217fe18c0bf1aae4c6a0b7e207b7d0bbbafdfcb40ce699ebb00f05bcbddc71582cad79dc198729f48de62f4fc7b3c40ffc723e2a87ee8783ce9fb92639ba3ed9b2c54d100c46f38b4335ebb4f8af0366fddfede657abfde191102f80cd1434e2e157fb4d54c5a1d6232abf7641be90a501f66f550e58b1c31b1399af906fa5a53674e8f4102fbc3d95090b678d6f08f8ae85e3c9b4e99f1e29b00387f4eed70bca39fe7844581a761a83eb1892ec01ce02fbc574c0f3be477caf04745d87a7f0ba690e2df00f50da284612fa5419c9a4e7eb6cf65974e3117293180ff72cdfbce9830ab55d2b2bd0ae54cfa16a3e1dc02faf0e17c2bfe37a61842020a88cafda96481fcbc64a03f66f38c25839785d0b0d679d331f568e04174cd06fc0ddc777e5142758e056aa3e62ac39431991d6b5d003f74ffb3b3757bb2cbefe893c62a4bd1502e63dd1d30bff2ee44d4cc9d51e720434ef5020fee6ab54df811f4afcdcaaa47e29150a80bbaabc4ee6cce95f58501fc028ba1ab3cce76997cba3087a63da9a064efd047e0f998fa8d69aadfc315505c7c6d6bdac26779a1c106e283dfd5b9a22d8735f386b6c8967e06546e519808e02f984fe6cc29df684b145556b53c1ea4d3be523204fac749e565553299c39abda1e0f3e9422ed1d46d2240bf99339c592036bef1234eedd3af2acf0b8e32440a40ff35acde6aad3c6d1cbef4d410b9232a41d34f7f00e097ffba0cba1f537bc9b52f7eabc96fb4e655db50019fcf5460b8c418f4d7e8fd05f975efa6b5d061f515e0e7ad5526bd702fb6856f43354f5d262fe12dd4b200fd8abeb14cdb928ab93b469e42217d72d1af38466d203e09f07e53ef4f5673765489be86acb9a278e84702f803fac02e46cbd95988eade096117d60a166d1901c0bfaef8e7df70f1deeb9779d1e05cce7f99ac8e074cc0fc8a82cdda598801bf077d4f5ba32a73e900f6674c805fcec9a01b465c0c4df344e62432b499dba085b7073eefef431342f207efb8eec4446ecbdd66809b980fb800d3c8c2193990b00f7aeab4542adafafef73f1c6da0fe71890875138619df861a8542ecbd930877c78106ea2f0e3477f11b39fa51915959128148bd63e6a81680fffbd3f9bb4b3c8d6c1a12661351dacc670c4eaa06e8ef912bc85b877cc861d2d24798595b25be1cd02200cf6f33c9f94af9cb846b31c62afa5805c6b706933300bf196ac0f1f5aefc577781d92d9b9b7df1299adb1ee8efc8e8a5b0bef20c59565f2b35feea561060667007f42fc2134ac279c2eb508c454df00eb166df05bef003fcc84200ebf55b117eb924dab4d7dea894a7b15433501f477c160d8dab9446f65e64b06bb554d508b55607e2bf674b72417fa2b337770649af495cbe389fe7293d90dfb0871cbdbcc529968273679e8b56a564bda580fcbe0ccfc48ac0694fcbbc236d9fe27d4eebd8a403e06b18fed149aee150f2ff727fa03e7ef458ab675b02fc7b4d6e7bf4e27ac92650e6f03ebb7c027a2f2f02803e87c25c87f8ba37e9dd93299ba36ec33d95848e5203d01f3e0c4ea5ba5b4018b0b5fcee7575d5aae72404f8ef6b2dabe99aee645a42c80a0aef8c0439ad4e51409fecae1de3ca3c3364e7eeff72432b107916709b04d4970c357ae5f199977b48a5d4dc3b144603d3ac4b40ff8ff57b7c1873e0e3b68ffd5cce1bdad3cadf1367003f16fc58abb568d78e5fbf7bd8b21e6345b91bc700d42f7bfc0afcb546d3f43c8ad0e1660d62b2583482c0fc86389b8a2221f88fc4f457cf2bcc8fea07c913d9c0fe0a7b1c791da65b20e0375f1df71f9a599c4de303fe3ed6a74b1c06aa6e2dc7e60f62978b7c262a7e7f81fece44a66edde8effe283f7411dff41bb4d057bb6100dfd663fe06d1526eafbff7ebf5b3c97dd1001b662a30bf29bb8027faf6ef5fd71adfc9e5777f8be748bc8280f97a6c0c5d3ffde55565be7e657b253cb25059319805fc6d2a699ff93b1adab3dc0b2543d21ceb75e6db7980fe371856b37eed7af463ad1fd53ded0b0729b6014432a08f85e98f3c15975dc965f82ee9306970ca965501e8a7e2c883c77165360dde5093f0c712b01dc46bdc81fabb90f00a03a3ed4e6ac155c4159dbbb8d77a4300a89f2fb80551afa48b55630226b1218e21a4f3b857017dd93ec72d5a6c1dcb294f9bdecbc5097b9e04371ce06f85f44edcde7fbe5cfe14dc763e999a1f13cf5c0ef4bf949cb9577ca489dfd9a9c260d7ad26de5f9ba33202f8c2c710ac447bb83aaf56c3a5c656956adbd475c01fa368f6845444e049264fef17a54befea3e99c903e0bf5f94042a558d890b46be4a058c9d7efde1755306e0f3fdc3717f3074ab3d1144f3e3bbe7e3d34c3e8f00f1c16605538d92f7e6219e561442514af3b12a6a0bc0c75fa51043961fb890ee0664e370463f1afbc78d03fe38cba10af35f317ceff14e3412c6a993e0f8b46481f8ff2eda257e626b026641d5b362fe3bad4b582e3f501f6ee228d1c67c7a9594a5ded03e6f89e9e6461207ea2b191cd92cc96a05f8be24375f03bb27174a2359c0df0c7d4fcd5ac6f912f6a128549acc69414cd15a00a81fce6c8ca75edbd4a944f2e7e7ac45fc2a292d82017e660e36e59058868b46f9057d1d95b2f67cfe5731903f140726eacaef545f319e341d9eac370bf9053100fc57f2c5347691eea116a98346f17e8addd1194511a8afd74bafb3c27f669d07cc04deb3beef475d72cb06f2a7511c43b0d0971cd22f84c45f6bf73dae0294c800fd3ebbb6a80ec777b5e7ca3f4ee857fd950d781607003f1b3e03a5e26c9e30fedbc6ec3a7ec555778f1207f0ff96f650a810f162baab3cdacb7c93b831df3b8901f4771cfc1e30a9d2d7475e0e3d6f8e0153a8b98939003f85297446f2c064ff4819f18211b50339c39bc60ae0677ba28a6edf54612a1b92f2f2027e6e8d0c94a601fc7725e5fb8bd43a9dd9020b5fee01f3a0ea1b2858203e2b34ef3a151d555b582b3d3653e04746c1dd5703b7cbc6b1a3ff334be1de12a277cf3486371b5bc12c00e2838d3fa61627d6fa2fdeedfb94f881201577221420fee960bd8ba93c5c21e7e5fd2264254cc6ec381802f873b66d0a11d55119c4c45f229e1f08850bd5b8f900ff11b941e5e7367da86f8b4259e26bdf6420ef64c2007cfb467a05a9b1663c1bb6ff7ba18802e2ec7bc4ef407cb271dfc178e80e5f37dca1823959fefb9998f20ba01f45a5bc43a11a8cce70862caef933c73244937e0ae80beb6c2d7107bfc355cce13c70e6e45976ed337b00fa58e29030abcd04cfd8788f573445519359e99563a03eca41e64281d9a34514faa0b9da34ac5f31aefe11e87fde66977c0ae482f1cf9c1909c69918dcbaac41fc32e8f187ee3a2984f69b8e8ee5d8dcd33a866d3de07f64f6a2697049e0ce7d661c7bc7484440f0ec9f0becef8764d1a0fd9e32659b75124d23915ab8e48d4e203f6495343c1ac5d8e814f5c9f60be04213bcd71603f8c95d35b8f1d9f265de07b2073c34d662e29f0ad7c07cf6abf72d8724bd563b1abea31ac3c8a28fba101e40ffa3e8cacfa7d6402c935a267bb894badc406c7a0ec4a79a6723658c65e6cdb6755cf534ace1c0aa692360fff8747efee1a49b73bab5658a751fd9c0542b410ee043c8ab65028a66388208692bacd96b2d5c8128ab00bf46fb8b5913a628505b2ce5f44b68f9bdb8b7c16f80bf94f597bcfd598720fe6ded529ec861fcc35af73ce0ff297a6b4dd0edf3a681c42fc9e96a778cdd1c8f05f8c72bc30a0bfbe209c4af22f5738b8b8dbf146b8e80fdfdf51302f66c82cecca81ff75956f1cd148a3396c6ffd77d8ba04ba3154e7f59b006c6303f0a417540cc03fab1176b0d44d91657a6260cab1477c984c6ef684a00be419c1cf9f76fbf747ffb4ee7cab8030aaa165101c0bf1d70d5a658207ede9d35cca1b930ba95204a6f04f8f912a70348d87a4ee12bcfbc735b88d3a357a64f407c4517bc8e4178e060e16a193353b970a66af92206cc5f32d93afe147e1ebe9a5c732617d11b50074f3b09c4076cfe9e9aabdde8c7475927e7ca3c9d7dbb02a400b07e18628a61cad8e709459ed0155edca4fd230dd4a7d1cdb2a37f248f3c957220f0fa2e56d4d50ffd81ef6fed0c675d87f438cb717048799e0ab54dff800ce01bfa9fcb09760a8c331c72561a3599a5eab3989b80f37b1739d1e5a0ed12843a2b79d50c2df2cd5c7301c01777d0e4b5bf961bc8b32fccaaccb7c2b46d969301fd27fe7761c8bb8feed8efae67682c6e821102580d007cfeb9b726bda1a606f5759e4aaee4c3967a2ff423303f6a68f8d5c91aa35ef5292d9a842b6afa6b046f0810bfd8a528e6a068ae793d52e0bd734da47c910d7e00feafaf74c45567a8ed01d126753f512ee0fa889503017c5abce73cd5e7e38cb1fdcf297ee62dd695bf860a007f443c8a7a3fa95dc31dc417873496904d22e73a09e0a3f2ff847e2f8ba1058da7375ab8b7ef7a0c21ae01e75f0efab3df7b7c7a09f729a2087bffebe2cfb476c0f930896cec4ab7fc585a50d1bf2a8c87e1a58c1c0af45f849e92de42468b5b5631a48295d56b4a2565ca407e350ad78c3e91a0b6e236fa4254a1b3a4f3b06b0ff88fa0cba2cb59327eddb63032a42bf38ebb3f7e959500dfef76c2d4447f78d37af4e081cecbe3797026501f286ec250eadbb3760dcf1e91c6906bf420942402f991ff8c70342789dd10435df56854a8d8f7a12c02c0d7235d9878c5716ae3b20a46a3ef23ed02656f2f01fe86b799e7777096f1777b61bed8b553eebe346a79a0fe4f0c2d314f3d1dfd808f8aa3fbc7f30b2217c315b0ffd8eb1371d9b848740448bf7b3767f2198c20a701cf7f57f4ac332c34956123de919d4190f8e937ad3fc03f73ec2377ce92f3498ffc4e539fb3c1914f724f07f007e53ec44a61e428f5f6d3425955d70e653a1215007fac0dc91863f3afa4bbc6c57754f6a81f3c644d02f21b3f49db793c760d22ef12f288729cd1624ae40af0fb4e031f070fee951f338c9063b2698437b8382d01fea5136f98104a8db35dea5e9ee26664307684dd0fd06f99351e476291a74c643c9d0d2e88c06aced06000cf57a08d9e2224204b7cfee67dabf6ec0a635f541d804f99485087145c8b390920274e7da6f2690b9c2a00fc0bb398e64165cbb4e43ec4a68291eff7b398f0f281f94454741096af13de1a7d4ab07ccf0a226a371367c0f7872e50b7487d93d768c00a49a71e453ebce0e700fa0b0f7233484d182db2d7a990892c97c2a2e40342603e406bf87c16917bb0c54e18d76ee9d3b4569de03cf0fce792d51ad1836dd8290de68339affae3f9675a80f92f7ceba5f884995dcc6359cb847a75f49c1fb52280fae8504254d1ad2a0da20b1963a15987299196011fe8af7e653549eaf3d91d490989d07c3e21e39fbfb907ead3b513bad715e22bba92fd182544c4f61ae49e79009f9d50320c84cedd3cc3b3b69afa1ef61333353b16a84fd20946cfbc6ed11192c7e6978f4c9545d1930e80f85c176600258496d412ff21cc6a69898ed3c80c12b85fa7af0c6214397ef578afdb83cdf953c23cc77905c0ef994c1f887fffb87614431d957f8ec3336197f609987f18ed652f1ee0ae5c2f38e9b4a0dc067d223809d0b3107a58352f66cdf17f1c2eed5a669dbbc3f7b174007fa92ea92be24b7e63339e249f8623f42c6bf2482d305fc6516f29ff6e5331ddc7912d334ddec7f5592709b8ffe853e29ec033760317e9c0394b9d9537ce8a951dc01f782edf8afff0951f4ee5d177a3843280618c1f04faa3a5fd3bc365d6f50cf7350f0c7a7ff3aecbfd6700dfc42bec3a627630f6b5188abea7a265f7a0702001f41becf999192e7031d6b1123a05f4f36c77d34ca7c0fd090c1178cab0252c070e02d3430c279c1c1ea479c0f3872f6c54c26bfc11e3aa56998def9d881d98470ce8375a76dd6ceae1775979522f067ad742a06bca0780ffaf557eb0e28e06ee47fe28de6a23290e45bcf236505f5aa1dc92c30a43b82773f0d3e83328be69452302fce00e895dded9412ae19f6e9cca1ff691288d7345407ca0f270ed91ffd18840c81adf538fae21f327f4049cff4d91f890d3c24249d085fd450bbd68f4454817d0d7fcece8d809e58a41254c3290c5fd8763bc63220ec4071e721768136bc34e0f4777cc087259b9a58815e016bdb071f18c570dc2e79e37d354e7b9ba12541805a0be58dab386fe600945da6ee2c0e88f4d25b2135d05cc9750b32df74e17b92655883cf4aafe86019d7cd705e007beb14abff4b9d30742f5482905ed975e1a900b81fe30b7325fd2cdfe83b1f57a9bf5e72039ee56793e20bf71c3bfa5f2c77c0ed6a27019f43ef1e69a6d5b07ea17d7cf58e94e734955642472b9d3a2be4b658375c0fc839094b34bd9de71ef2998a8f7687ffa3a76a3a480f9becb3b091dbdb20223131032fa1af4d6459fbd60017c93bc7d5c2c0e6616d704ca59bf8bfce6aa3d0d12c07fa94d10fbb29addf03362b338593719fc8cd402017c1d8f18ea5c79a6bef89ca3a0d57f27d6775f9c07889f52116f6a9f488a6e980bb103ef08734ad23efb01f50b1db2b00e5968a8da54e89e8826f4178abcd12600ff7f9430717b7c16bd608d9326a53d316a2b449b00f889f81eb6d463796eeec37a7963d6b9bbcc1bf801409fe025109e80578380a5cc82e1a21e79d24dac5f02e8233ca8e22c1cac2321ffbde7d339d4f34925809d7103f0f10fd370e16490f2246def151646873b5f110680e7d78171f4e65d5b9ddac99a9b74eeccdfc9715a07f80775ea1ae5079dfc224fbe994a292875a03191ce00f91df90b09ebc2be6c48cd78b8df088d1e169f6234309f748b99c48d217cd1e3d39293bc338254dcf56808809fa51c3477f016179b318a7366fda87cf71145fa03e0df3cd64c1d2460890e648cb84c8909a59aef525207f409ac353c8b45a52a70afd0f4037ffea53e39f7c100979c7ffa97d093b55af3b183bc70d451d90fe294a30ef0c7f56241d3d8e7a2fca8f9dac1a8d975858f4b1100f8ab2fa0c65f695a4d66c54748756e044d4dafb10880fab31bb567f085c4cebfacabdfb9e159952caef3179000688b090cb50f262d77cb1572a23d9b855cbf1502fc2782589c0201f4f4b93fa4b27f5e7adb8dca1b2230df76a6053eae5de59d1b51b6e038c9540352d640150680af1199b9bffcdb81d641e125da40f5d57f352d00f839df87e5a8db1b291bb64b4c9b7425d2cd94a03ec0bff9c6ff99eef063d6885f7b5237c53192e8ea3114503fe38cffe6c0253692106f716c8b86cc42158f65f404f40df9fa3a11a9d5776e0a7b7f09aaae386ad17781feeee6969bb334128621f6d555d29a97f7374d02787f20974f3de9b7003ce9919494a958824fb9011d84c005b7b2529eecc5517d565d47dca5ceb365e3bab937c07c393f24be3721ec345c9ac91eac63e8a1f88c580620bf75c51cd0daca4e16703d3765225e32af8f1adc01fcab2dde054fa9fcb5b14a7ccc2213d369266a9b2c509f6b67c3b592bd6b172cf0ff6cec5d7d21653c4b0ce007938ccf4e04888e8d26de41c63f6b4d1ad0c91181f90cbd2cde4e7b50152b9f6225de2755d237328ef001fd179b330d54e5a45c95463fb9e80e55b2d833f62b80df40cc6cf31c094f071f69543990ca9613fb478d01fcaa07aa4a2e83fee09b8d66095d3e149dd8849212a0ef0ff4ce5d8af479f79cd9a28b282d1f07d1fca313e86ff61c092f35e4a66360544d5bb897e7992a06c0eb68312def71a4183f224eb1def69b96cda1a0955300fabba0993b0dc398630e5591f48d8ee847a8b8931a00df80fe63556e233d8b32b9eaacf2dc999e8b085703e023bf14dcea315d68e7d1730dc8054f4d935cadce00fd5d65ba7ddbd78e8a6c11158544ee146a84cef15b805f4c80bf2bf9255437c10b79a85f3f443fed5e810df80b128d427a3bad258ba3a3e1bb274fc6e004fe5603facd589883e4402bd37e18694717cadd4f54943103fc80fe319889b768fa6b71d298f99c680614d74e713ef07eda1059537039087e93d6a2be5ffbf72051ad8e0e9c7f34e6333c893ebf3d5e0b13087cb1683c4df13a407fa8d9d586d2d97fdd76cfbd9e9599c18f27315f0ce01bebee8e375fae44feda46fb6aefb00c20e784b503f72710152d5094941b4198fdc5c5652121eccb642b01fce3494e4ccf1d9b7165f74687249616b78c426ebe40ff67577076a5277b3cdfebe9212cc4ba14ef3c3b05d4b78c87aceff8f8b068ce083f424b20d7246b944603fe24223191a7538438b132dff1a6e9069398e3da13a07e9b911e57ab9537ebb198b9ad9cd8677c8fb46505f87b92e8e7a03214880f3ae6f345a61feed564525e00fd1771a7fae70cc904b2cef96d75048349b377a527c0fe36e85ef99791edd0a133705ef659dfd1da923b01d0a773a30947f11c1a4bad39f217929fa1157a1daa02cf9f46a1fdbb573519eaa407d22095d3c5b13d953d101f243ec0a04c6ebb9123e9d02631a5c370d019ef02f1e13d8da38f5b930bee1fa1e674b9c80265ad6a4360be88e4f8f5f7e4eb36db40e299a1dca977b81c699540fdef0f51675cda7d96be883897b1cd9b21379572cb03f03f0f4d300467092fe92eab48197322d0446aa07e77cbdab3569bdf24516d31e366edc557f6555010d0efacb5dd2de32665e38abbf979dfc2f4ffe5f23a01fc3b7ea67e88f18d5343df13887ec40830eac1ecd001f1a95198157937e734c2de643584ea4b35c7fd2317a01f9bef961badaaffb16a4122b84ec50e9f0db35d01d487e9d84f15a96cbdb4fcdddfe5b2ae6879ea10f580fa1bb377e8309ce5eeefd6eacffe60e2ae96294853e0f940ddc093370d3708d220527b3a97cb6576fcbe05f867b59bedf0862e5e621acef633f4b2d52ed6ec5700ff1d8e24b2ec1383c6aed442fbc3997da0bf7eac08f4774eb6272f1df6cec7a8b92ae9843f36ea2e75e581f9540f67ad57d49f337f2ac28666ae51a6ba54730803f165e7db169f83f44e1699e3ac5e6e5658b380aacee5ffd7cf73bbfa37246d78d1326ecbdf0aeb17f60c3d80fe57691613d55f8748cda2e9ef68742a9b63b0bc09a02fb04ac0c5fbf1abf2e3d0cbf1ad5198a3ada97c1c501f7fc491e02f1d475d949f6c2d2d6db0a942ac1305f4df6e2e28ab6c0f743c3e2f54661ff18c643f7f4206f89d623538aba5238c36972f9e9c721e8bf12d5b2500ff87109082a12d77f341abad945ad8ad9abb975e15e8bfa39244e722e5e0b71e54aee2c536d236eac61300ff99fedc987d2fdcfb2df5755536ae1a98407a6e4940dfe4261709b7f812433874e0f3fa8d682288030602c07ff218a21c32c235f43a2a0cd9c9f58387a22857c0fc5759575c8eeb2636964e6bf75b994b40382115985fd15213b7c04a0859c3f99984312c6ef97a269f1ac83fd0e2cd3a50e1254f81076e14ae93ceb37c09f0003f44134cce5561f04c82f0087b72c0945404b7f909a8bfc7ecd660aa77dc563d7247709ec9d6f5d4701881f9d42f9c91634d619e50d4636d5b1b083e1047abc980bf82693d6832f4535b499f855e9cf02dc5589d4d18f0ffcbba31d530884462e0d6e411596a894c718fb901ef3f71a080ac53b282e54f6b50bfd5ca0e1c7a4309f0fbbcd14a2cd988a30ddb0e36c67f649bdefebb7f05f49b77ca5016397dee97d5590cfb51a3937a1febd4017f30ad26a12b5d952136062aa5c2c1491673d52a0b804fecf4083b0a48a7e567bfa2c61cc7f33536907c00e2fb734b6e5d44fe25aaf96b45db4142329c406c0bf0fbf485d216fa26f737b98a1c9f9fb007c95df79700fde5bc5f6a914ffc84182ada8adce22a0dde08ee2ba0afb3e653ea10309621837a9043c247a6f32e333d00fa3765529f6b77d9f0a636b1e793436bbe3b947904607f1a8c33bd5ba17ce4e67ccc2c67cc3ac1b2bb4401fca356c1ac47507cdb823f9223b8fcbfced15419e203f8143f43488d2cca989e92500c863c6b6c590fd715a0ef6df86188551565b5c29e96da3a237588e0325406fc7ded8d54c44bb61bdd329cec11c6e8642f5f7653801f8780c81f6445f51ae5e910d697be28be2a3bee05eae7d9e62b1ecf06019e80817d3e9578b2dadaf71f80fac455a8218c7129a70f9f7fc06c5ac6637ba35300d87f6be40b6bc12151e1d712d1a9dc305c59a2026e407c929911d0ee7793c0b814f870abf1abf4e0b9771f985fd556db935e206c17a7b9c2601dcbae018944e703f89b427eb475b71b747ccd29c13a37fadbdb5d68b50ac47f77b256ddcdafbae7ad2cbe138e885478a974e0fd8d52f83a2db0129065efaf8ca1a7a6f9daa2b41e80fe75621d5725591a9664beae620cb54ba6d05c4a12d0874ef774e3a4f4e462dc06a7a3124f4be14c250c00fe3441ca7ed228a99396d01e417b23683b14ac8105e07cd172576f27979031783d51f7c8ed7990d7d7e601cf27b3eda2f8d84abbc26e98702df4c95d54d2c91ac0777d069f42a4feca9e46d25044e23ff9ad98300b01f18fdbba9c409ea565d6d11015e5e2aeb6c593d8d51dc09f39131ccb0f9f1a1ae8a6d7cf7a87c952880f01fd5c53781eae29ac218d8a6a4b65f6deddd7b2f41540ff698fb8c8d238e4757f47a0a1b9e494f5dcf3900fe0fb83e893cb0b4f5add064f30977eb1063ffa49d7017f23f7c2b829b49611931172d5a769a7d30c82124f403fa12b1c89d9339641dbbb60996514b03c81d3a907f08fa2b63d470e151f1006140ddf74fd715c8ad14e80dfff49917c81b9b898b81ed6e0665d23b84457780298af9a955b75efc1361af309fbf7c7b20705ec0a8125203e191d4309d760f77930956d70c49fb1ab1415a700faba67edd4cc7f061b0b90e799df1990165bae0a6501fd9056dd5c807d54c627fcefd2586f869a39ebf78bc0fec1216d143b305c9af5ead83f454d4e38da3e6203f887a33f97141449d3cdd107c9cb3f0fca4607e53c017c4ee4d00d41c7a8a664ba509d1793c68f5dfc1417783e71d6b30efcc3c44b0e98b62f45daddfd57d74b407c52e90a29962298999dcde5e41fe81a8bca29ae03e227458f23a7d67339059abe3f6b60a86f4ae7a72ce07e872c01d69a0d4d8bac50e2f2fc6c1dd449f86570be38c5d93a61014186927feb3645811cc41865063bc02f9ffa6d5bda4d25d51ee9850a98b82adedf28c703fac167648b102c71fcc9bf86ff3acf9c7448b0d2e7007c0bff8ef16454b3c32d4cce6feb5ad51cfff0be05e0b784f94c7a6ab1b03aa162e4505befd3b56a928980e7d3d75546e37f74596d99dace6b1db54303a3920de87f90af6b9a825bc9bde03f60aebcb7af77210b9103f0155a4bcfb3bed55b422e58d6d2805667e4190d58007f9ab0faecc93169ffe4c46ae496d4fcefaa956a1cc88f2698113ef57d579a44ae158b6f6cc8d95bebb8803ecb8ff6b44474d93ce5bbc626bac07b9bad3b627c6990dff9872183e63235848b95fc55edba09999a1e385f7337f7364b6ac3fe287f8c30db37fff88f27ef00fd7954a991a010a54cd4faf3f4b10d892333857a3dc0df7f978310b771ef1d26378641e7125c3f16dde304e637c9c305d311b63116acf520092c1f969cb1b9ce03fd69d7e7fb55b24f5c938c465f14b9a60cad6fb7bf00fe6d5f9c385b6b2a0d3b37693428dbba68c343366720beb1abf57256d09057bcc0c662147e63b145f6d404f49bcb38581c9c7e141f1690279e33581ec2ac7d02017f6adfb3382e7aa117676be79f408f845aa6a4e73c0080e0ecec53ad3bb02354c6dbdaba7d706dfadab303f457736c308ce884f41314b4280d3d513c90745d95c0fe313d575959721dd4dd26b7a1bf12e02da33f2004f489ef20d2f5ba967cf5dd841f3a376267075d07a6007cfadd97af2934625c8be5dd617931ad8d510f8d7b00bec6f65926a0ab46568b68d8dbaeebf8341f158a16e8bf59393e2e22a74ffdd42bf42396783279b757bb0ffcfe887618f621a70fbf85488ff1da060f58f1715881f9af35f9939b778c4298dbf8ad9fbfb4f0767559b802fa7c7f67821a6c754ba4132f2412284d91d43b3f38b0ffc49cf24dfe57b478d5166cd2476a36c511140acc77c627a69aea5696db546256997235ed755a751006febea351378567c7b59a151a4f90836445c9bd8e06c07ff74a49e8078d07d49026166eb965376a1297a003f37b3ffac5f11eb4ee44a58e95194b716c9188efed01fe4e236ef548b8396c989e5389ba85e4825442b77440df40782c3c61602fca88e0d923f9bebafcb1f1d407f09f2607c17c0d3fef3aac37cc9ef8c16aff67fd733050fffe869cb35410c7c5fd9d5e84a9a82c62a3168f03f40f52a34de3ee6ed335b1b6bbf54915e42b283e2140fc200fe84914670c755b7a5706f34dc2e9f7d5e401a0ef45d6326df6cbd00d60da93e85d7f1ea69ff3ec02e2e3a52aecca1d0ffd30f768bc7a7ddb2fd5550752607e9850c769296d20d40169a93677c5df6ea233a60ea0bfc6be9110558316a9baeef4692b10951532436e04f25fd64b5905842fa191be724512518147a6ab610db03f652c7dc974107ef1aefda1dde1372396777a8f0dfcbe05d2792b2a3615578d8025ef3097c906c62c0fc03f7ff26a0beb2a6f2b6aeb3c76b20bf1517e9b6e1bc0bfd54e912a84ab85f513a778fae95811fff425e502f5b736ce3ff87808fbe92ee4dac5cdc1ba9d5303f07e2e1e8b4ee420350d2bd29d7799d4adb90969537c407e7581e41dd60e3a9c0e3fcf5a6d24d4f7f422a606f02fbde5c485d8cf4d7d55333e0b39e31e4ba3634e003f18f338293b509d1cf8f8a72c6da05e2abd00961a882f8ff89842ebe534f9f7f6eca1ffb1f716407976d99a28ee103cb80587e04e700fee96e09204d7e0c1dd83bb06825b70b720c1dd2db84bf0c0ad39d37de7ec73e6ff73bad367eedcaefeaa52a9627feffbbdb2f6de6b3deb59cf9a921216f0fe4602e0cf33e95784508f878ffc3caebe5fbef964492aed02f8b2e68eedcfa922d517e9a73e4d8aba43ce741fc1fe9a353a9047a4e382d242df6db7f6f2cc4dede76180f57f549d69b8efd578ab640ff5fe652a8fe7ba0d11809f76c9418f1236aff11262c45106c695f60fade901f90fb72117e961c9d94428b8b17ac3a3d134163b0ec03f9f882d0c0b7ea3fa3e843ef39af246fa35c41b42809fd5a67f5a1146054f6361e2bc2962356f0123b307c437e49479a8633178efd285df1156f6f31de621ee01f97f92b07bd54a1c658a576a0dde3890736e56efdf01fae343ce4d6f9f1c7bafe5595cde7a7dc4651877583201f2f3e463e3829fce1fc69d1b350e167bd8aa5a4501fface0e89b1661e9e19e9a946cd15b86620cf4876300ff32621112f7e4986c917714da93fdc624d81aeb02f0d7b88487853b0949d41a42220cee2f37c8dd3f71e183f54b31438fcc6517ed21ef11e796162e340ee781fcefacd1d42af572040aaa25d296c7ccfde0cb7917801fbf7681e8c443a158ea203fc739f21262d264011f383ee80dcb13856b613b059937da831ad929b187b2407e85f9a5e0b988025301a71d6c77d7ece7be368353c07ed7e4da5fa81cc82aef8e877ca0c1e7bd0cdce306f4d1683a69e47c779b99c63379463f0d5e5477325302fa96440834526f64b60cfc3808168f299caacbb7b001fbdf5b99cbd8ed4ba9479ccaccbb4f0c796e0ac50df8af0bc1ed14e1182e8ddc4838dc6c687ade053872c0fa36858d7a466417adfdd634fdc8602e4bd65bc715d0f7ecd91d7e21fe5822e4598ec68e9730b57e669004f88793844ef3f1fbae931052542f0a71605e3420a902f9c7d1f72f30524bc6e2052cb6ef621aa0066789a301fe962d1dadb49b7e9154348b215e70c84ab46e7c05800f8ba2c73053c95de05506d3196be4226d21565301fbc7ba94fdd5b1b2147c93100ffc0f384a2b345f25609ca7f1f95b5bcc8b891f17f42a9b42e86c16a8b100bfc4b3c50613aaaaa92bc2e4e510b29c7f0d0d4b00905fb6d0302efce6c48daaabba3bf861f2f60efa7904c00f98141ea3798984c0bfec17e3905a69eb60322806e497ac9e3f5b1f1c874dcfb9c9ae0da5b8171633af03f029ffd2ab93f88d746c2f4d4fd35a73df3862cb7a207f611b1351396be688dac61583ebfd8ef6752b6c3310bf32eaa3ba73f60f8760a8be5a33cf83a140b5c302f4195af7a84b26bf1d2f96c168312ef536e238171f01f88e1fecee45d7046dc2b80ca3f4131bc69ec9567bc0bfd4ae72995ad2423fc9799ac2b4f9fd9bb4a3cd2570ff630efe4f688bd55e6f608c0d201d9933d7d67f03e283a8e0b919fb43fa9ce1af5222df09690da44a4c80fc331bcfc70a88a7ac9eae68539551cfb81bd1116901fc70ba2ed684101f678726fb4dd03d9967350bd203707fcd6b38ae754d248239b183648b265f6adbd2d1017c3cb75dc72a4222ddf0482ebf2accaa642bd1d80a88cf9f5f63f2d04793b934dab7ba380d14974732d403f993ddd2999bd628b2137e7ea8bd77cc5259c84805c0fab1770ef18e668d79664be755cad9d78740da013ac0ffdcff60db4ec8482a6c5987ca4d86fda00b9b7508e45f845f671c4b5c9b40b3dfb7b07d9cf4c382e3e603fc87763122d26b3ba3a5f6d7dc094ab0f92841060fc0fe1a8e3b64e68cf52830cc75d1ec7c7c4135dbe700ecef86b1b079ddedaf6b128ea15dbc7d1d2dcf5e3901fac5398d7524e1fe3cd7c47ae74817d9e355de1bb840fe465d43de0a61cf75c8c64d230446604477db6e15e04788d9b2522b5022e3636fd08b22d5bed736d27909f427225ab9a208b18fb8842fc7b452d28ce986167503f01b8b33825335573ca1d271655baad2842951c512a0bfbd5a1bbc78f953db25095925bfad82ddd195925ec0bf8dd025a2d38c3f4cfa727e42970af730c44b0d0fc4873d16fa48666e0284dd05015c0d860c2da6ba2a407cad3d6ed93367235ebfe78ad47a2c032921fc3a16f03f5fceef0befd71426123e3a4ebd17846059f07507ec077bc60601a2be3ca453c255a8c16a3d29a2b00ee01f6037e7ea1d886d5e4f39603a137a235cc1bb1003cf9716eddd24c674d25b74f401c4c30b0fed8c0f66407e3ed3c297e73af7125ba5725cb3ad5b1efdd0fd3590df49a45ce94f80ef45f35824c689743437e8544006f0d7a2577295519b456adf17ae22bddae5201092fb8d007cfb3b82387e90acd66601ab0c6337b7db59740fc02fcc2aeca7c6655fee507aae4c2059aaf37113a61cc88f0b493947de21171823b1947de8f6598dcfa4d706f43b588a4af86ede375e6ce13d0677db9f395404b701f3eba4c0e875b3dfc6334f9f57a4528f245850c377003e68a888c56fb3b11c508e6d5588f5a2e1b94c721b50ff5643b4ca4c973313aaf0235dd2a06e467452fd15307f3079f5542f0d863c216bc7918abfbd3844f39503e6cf4cadc6fb49672959855c992a0f27488f087b6c407f58a3ac178e97f51ea6eb91befff4566be690a506c0cfc8093f2979f1487a63320d903a91d2e89eaed103eb7792fec1e402a5529c63a48a8f8bdff4ce03bc1a10df76a98f0cb7e316af31ab28ea97f9d899e31cb700ebdb37a338cb9af998fb33eba90e7d0a2433835a15a0fef849c37e37be16a7db586e199efc1ef92a039127b0bf7a2d0a9f6c7ae942048ab391acd359552950510c00fcfee3173dd0f8225f1c5a902a9269a7ed58dc9fcd02f9fbe62edb2bac9627db742aef0b2cebe7f0f82880f822a12f78c48621c25e19158efb299958f53adb16b0fe271caf6c5f322042126d1bfa0df6a978915de003f83ad2bea4816723d4ccdc7e52119f10627b23d33ab07e1ba719cf7d4823594067dc6d4c8fe623dcc8e005f627413468d95d725386c9749154b1923232027a3ae0fdd3e74dcef7abb5bfc778e723af7b467524fbd210b0eff23981d96c6c13c4f4ae4979fa75bc3005081ea07f1de31c291774d698794cf2b7f7c9c91a4daab1d3c0fbd580e5291498e9b918bbc2798a1a90371b87050fe02f322e3d85fce46bb8a882ee0151d61275c2ef00fa1e045974d5f396e5c0647a0551aa3183dce4b1d607e0fd4a1f37a0d508b6afe4ef9db812d321053c81d701f80f930366839b42dd5d1f7a908c699f9985798fdb00fccc26ad6f2cc35b96eae36feaf42b1a33a8b1c57480f5d1d76faafe79c7a69852a8b79ec645b608aa858f23c01f7be85b601759e8ae0a4b9e579db8ee114b8f05f0b5221dc259fff7c81be80b770b83305b88769f6980fefd4fd0ef596945ad669164829f7d8be6a5f12fd600c653e88eb13489c3ccb275d67c745dfce639880ec1fa785ef8a7db3f0cc4462904470628e0e2512a2301fff232bbe089ee1cafc82dab0fdd51f14f9cb11d26607e1dbb9aff24f6deddc5f3337fba40368236417a02f00bacef2cbff48865dbbdf9316c2fcc2f2d5787e300e0b3592a8606fa492ef2013cf7363564f13c103136407c94f77c5daf62a28aac9eba493253650c01533c08a84fb65d17b2b2abe9a79a66bfc1d4258786974f0d01fccb338f7387855bc3c1e7b9a6e13953d9eb65363380ff382f9069bc5fadd53f4c5b93c68d7812a1ed2d0de053a40ba91f5bc672f3905ed11fea39228fb3337d05e6d75810aca01992ab52ab0bfacc49599ac666a012c04f3785f3e39736ecee89c887893ceea33a2af0ca01ea4f0f38dedaefcc1f372cf428b9f2f8f21c31daf702f521906c7578acef6b52fcfdf3e33e42d3a7991f4d02fc0b0e5a04db3cccd6d70c3f6968b0691562f95f7000fc4e2194644f9cec2d7efe6f993198ed5b5b4db7bd80fe1cf2987473116c408c2fcb677487b5b5f859784ec0fe62072ad9de0f86f3068fb4101320c059e3973102fe83e114ccbc698fc4cc935c0eaaaac0d7fa08479a80ff4a28e2d7284d5d85bb624addfa054fb43173490ae067bf85886f5167c9d58e42f3bbb89b0d3dfe91e002ec9fae225654df8e6c5f2b64ac45261d5a920e2ff502f9f52b8f8050c3b5f98f072e2e7881bb4f1093904f007e23128de07e7a2a4d0d9ea9c8caf45c38a12e943aa05fba3e2ce2447a0c2f552e0f59ba6451fb2ef1eb6b001f568e42b2a3eeb55a48c529fa92be04e5a0da740af81783522fb08664fb72a65fbcf01b76360e22ac7e02d4fff5ef0f3e7f5db2f86cedc1b5533b310fa320340d884f8b3bf630e9e033386efc8704d8558e0bb39c5180fa3a6624acea9a69213fd1be09d87571c1aefe0d64607f267b1539ac0559e31a7476d539b4470b07774a06d407ebab9a7995678522e5c4249c6cfba8bee6b8fb00fc3e471753dafc8bf264757becad753863bfddf43be0fabfcba5a5cbb3604262d4c3894977b37c952e0a07fbaf19995777a81cd33af088966324d909237f7c00f40ba8730779b052bc23e72c1f5f657c68d2e3c2f101e2439ce293e022bef18b7efd6435a5aa10d2564f001e80a8e5a17f109a0981b234e62294be55e9c5ddd701f2a7a2a7313eeae3d463afa20decb3290889d0204d00ff63c88551cf83cd8a75c7cb3aac1013bafb94c009a84f89d73deb7ecda9bf8692d738b344c3e1b22f2603d4c7c9e5477c0a70f18bc59f9277b4c665e1ffbc150be05b3c5fbaa21c9f9e9e43f9656195f98dccdf658502f5bf2fce21b2cd1954f35a6992586d60a9c6cd672281fa567e032758e6713dbba206aa521c8a8cde5a8813405fa1a4bea3ee9dcd74ca2ba24937161caccbeed07aa07ec471b93b6203d23da66dd0bd6952b52fdf26d306c80f4b7797b5c268b4a57dee098f0ee9ab1c40d1ea00eaf71735bc2c5f9f5513f3f22931210a969316185000f183b746923ddfad8d0dce463e05edae9ae60bcd46207e3cb24136764f57323e9f325d81a05a621d94f201f41ba8b0d0b647dff2e525cbda3edfb81b9b2616eb06aecfb923abfce54d358fb321829baf85dd11a91425807fe57bafef8bb8e83d063e274b1e6151bbb5abbd01e22b5762ce18a9a6c605139410fcb51a9f46af4b4f60fedffac941ebac953a360cc036556d97abdbd63202f5e5cf5c84b5fdc786c474e2aeaadc0df7e6ab211a007e7eb7da54de17e22f56f8afeba49d8539301d717780fd2f2a5e687d938d76d12d83538d50a799f88d781cd07fb8afd608464ccae66756522714c7637d8ae15507b07e8e164fe34ad957ad3b499c9c04e9d33706a2f103fb8742fe0257c6ac4103f2b7e342ed4c884121390e007fd447bc3016e49634f6b4d72d309f1d2a9978370bbcbf604ee1f1581f55350c72d4cee83cb276d5d53d40ff7e54a892e9cec15f2549ad98eaf57643a2ac3f0bd0ff42306e62a9f309b29194653ab42796868310940b507f51216c538ee4148b715ccf621927406f4734e40a5c9f920689760234278694f7b09fe52a2a94f3092e905f177ed82c9c46c053a4eab764e6a0ad3a50f20a048e9facf3862e2242cd29e7dc6446fe487822392907ec5f6e6419d2f68a30eb34633f05b7b41d431fa3c1fe2643d56f8471b8b4761a9147c78791cb1dbb260c01fb7ad1349c7b6fe2956dc4b397a7190bb18acd2107f04bfcce6ee6ad0f6f338b5262864eb8d2b6ca672901fdeea0cdda14d2c401e6f80c66a5653a0ffcabc07e00bfbcdf761ba3a3b2ad979e1f892f180cd26e52e402f8bb3903e70e85c9c9f85f8d36dc14840bdf56e56500f1c15778d361fb387d8af5d1509ba909627576433a203f2256f5c44160a990c3bd5c914bee4a2845ef3a0dc82ff1513dfffe94829cd896795bab434d607cbd131358bf3a25ad50d206aed121cecc4cf9f50376556aa680f8965c705587db32c4ac8794aa8efa3b27e6e9abd700fefcbc4767a48d90bb8bc92598fdb89e8086e8bb39c01f75d3d05d565ae09382735d721746b54a85cc4905e64f3f434dc3f87946a5626844f0cf938d8fe4f1c9007e2f98f038114e6398eeca58b7e952689032106b0ad4cf4b534d577ddb9631e2c855e9bf77767a6675430df0eb598c472f9f2c758908fe30bb7ca7f5a97f662f0de0c79709a91037d36f7abea7246bb163eecaf2206503f00583277b5b4fb660340fafb10f1ea207a31b9c1401ff5e8fd1f78276d3f97e8153e10d7bc50fa5b45875401ff7bc67646b0f4233d3d60496e095cdfd8d40f223e07fcb23eb2ead5ae916d6deb9d3af1409aa4b08f402f50b63b421325ab7a50b4656e33ff3996ad0822ebc007d5b33fd5ba83cba4b160e46331c313dfc398dd24220fe241178158ff10e9ec3f3dcd785bd453ec3f9690050bf54fd55604c041e566bcb3f61ef8e80ece273bd34101fe2e6c4f3c7bef5735fd3bd445a7cd2642e8d3c0ee447fa91ac43becbf22dbc925e78ce5e3879650ccd0fe44f71538702444abe1df443f1aaeeca5e7f38513007f4b54db9c65267bc9869ecb41e23c4886483d8d12b80faec82cb09ec8de8ad18836dc8a5ef42796d92d1d480be2745f4fc91a4cf016b53bc5d337e1a766eb0c030e01f9b73193e998ecaadcd57633cd49ebfb476175c07f801a6bcf250cf10ddbb1313be287bf30ebb87f18e020e888b46ed21cf9bb982ef3221e95676e6533f963e01f5471f90836a3aab8f9b949095284cb077e878649d017e2aef792af5ee14d643d5f3f6934d9c8a83dcb05160ffe5bb83663ae56e1315c3670a8655e295d1d8b32d06ae4fb0706928e6a57f45ddbb738759ae8d352567c03f71e8fc623453e06f66531eb3ed79acf46334ca02a84f97192c7b8f1e1ba10c7df131998a5f377443e519808faef1303a405657b123a041b59e7e76e8f50fa703f4d7b522dfe7aee32ce97846f73dfb5ecfa30c411904e83b5cee574c9c65737f0f5a5bd533852ff6af905f06f6f7437f526befc327c990999aa14e6b90fc28f02f017d475fda50f443bc2172f963d68b97e79e306e399c00be41f6ede46bcdc9485e71c029ce86ced071a43227a0ef72fd94a2abab1787db552fc66fccad9acd6ed115c00ffbd9ded2393b8431ab3424ffb89cf566309f9204f237916d9b851523be92b9b36b77bba7595e05d88300be562a3178dcda29a09215bab3a5cac75c8cd1430ce00305d22cd5c9219a291d9d039d4888e246f5aa1c40fd9bf07b9ecaf1a3db8b781b4195c9cdf0773aef0781f8d5e5cba2d1d669da82d13269308337bb53d71e3dc0dfbea9307a9a4ac0aa1028dea68d5bbf21a7a86400ec1f16f6b14ec3f34a3e655c862f7cec8be126d30b80fa9357712b8d0f4e7ae347ea52e664c52ee4a37e2f00febcbdb5b58ef28ffe47afecf021f504ff29af706ec0ff29adebfe7035356bc6cbf4f188c7919b9712810fe0ff791f54b5d7b58d3220befd246083cec30b6b4a0ce4d792b21490e550c5a8e2a64e85717dde20bef6c200f28b37c7b935f463e1bae773c496e612b564e8a84e40fda78a67df9cdea8ade051f35e78038a14aa2fe931a0afa17b79b84381c2802d8946635c57b2ec6884e500e4974d9c836de2310654fba5e1aa75dcc8e404db7301fb3f9d57878fa070c3bdc243c847c7b2ab4d4ac907f0d1bdf6ebc14e178f222d8cae45cd64934b8f2966007fff56b3a3cfe8a3611550a070f7ad22ab39b76e1788bfde9e3989ba7f0b7fefd1fac5220e2787dd613208a81f0f8257f82a65c1c087aea2c4d48d736e2e6dfe14e02770f0d62997efcc7dbb22853c2069d322221bcb00f8fb02280ef42c7e4dc926928bdb72125da7cac4a9407e97e059433656f54a691b7bc67070f8c547ac506ae0f7bb8ebd4ba6911cb9d21dbb1eac6104e1eb633a80fce79bf9b0efe3a2621d950a5f7121b436427bcce181fd29b8d31ffafa42b350fee9f4b32e7e2e254d21f64b10bf12aee4272e8f8aaa0a73ab85e72ac5f66205d6bf22045907818100d25ceed174aeb64f57e814b300feb2d62774377eb3b85d2760352b873fbc91f26c0ae0773a39cfcf8f8e4d2bbf7b221e5129284beadf8b0ff0f303476dd3c2de4a2f4848554257a77c0fd1d8cd02f8833acb9d24db4df35e68e7fa155170f7d78fabaea0bed582649395c9872c64b6133d02466e93bc67b2807e1155aa65e993243295c48b13f7c0c0becf5a648500be97ac266af1b8c3207ea0e40e19abbc25121a450ff8771fb87ce3323754bbdd0f695b636f3ca75c8f710100734b44f379afe2b4671c775e283ae9aa4197fc17007f80623c392095f8fe5524e6509ab207ef2924563360df857685c25bca95c219575819e7e114570e3b07c0f36d1843aeb4bdf58e2733df6e533074aae626fcb902ccbf0d039704721db36d23eb84a5d71cd855dc0c807eb2566c98544161bc5d13dfc105e71cb6474bd209a09f54dce37a16ee0fe731356852ffc6f6e78a0bc567c0bfe6b34df05552608c4baaff0cd5dfe4a11bd69304d82fc627f1c9e74b83935d9ffaa03172b36d02251d81faa9269313739a6e73cd6b4e6fe426f2d191c737e5c0faf9b6c9650349e98de100254751675cb36d40b133a0ef2a805b56dcbc46aac1cf58f43a26347497d10e7902c0bf32db5183a214a80a8abad58f25aabc98f07d01fe8e65aa232cbaad86d7cecfc48dfcb6fd7b1b390e80ff48ca87adf823bb83d4a5367a645931690cdd5106e027f51c7be88758b650bf1d2064916c8ca3449fc803f27fcc53b40122babe639073d3f5d763082d57f6d0c0f1f2fd946bc2fbce5498cdc8cd21ea4d4cb0f63600bf0d8246ea3888683a6e42a882e54be57de02e391f103f85e0f7c70c3779bd297ef76589423132ac8e2808c88f6cb94d084f6fa7e57d120c15edaa111af9a1dc0be4c7faf8346e94ed1417834a3d71950d261babe1b180f8f7da38ddb3803ff0cd009649476fc0eb1e88f73f80f73bd0be7b9845c7e31fd65dacde26924f9b15ef0cf03b64168dc339574fec98a553e9cab62f13a110ab01fdbc737f98a4d3e24962591a7847f72725c7b1188980ff6b94711721fa50aace4dafed5742a648f9c5c81de8ef73e6fe3c9d55276a874d3c5746b22df323e7232cd0bf1a82d4e36230888b53222bb61f7ad64b85502d17c0679d3696310f72043fc7c710e53917957086c7d383f8bee4a135ef94203183d952b6e335ec9ba9070da07ef97b9d6a381343af5fee8ec227b7e5f195cbbe61e0fd88dacd430bb96caa4427212dd28a3646763b8801f87c3f9dbd937a379cd0bcc4a286f173918dfd7069c0ff194f3a8d12401d90a1ee6bea147a1b1578bbe500d8e7b4d8708aea2cc590595d95564dd927a3d00a7a20fef10c8ba3ce36df7b362876d3cac3cb091d7b9801f00fa76be4ddb388e88ae887323b8a369a304ec2a401ff785decbeaf457778a66de7fd6848e0846adf692780df924dd72893a95ce1613bf66b5cb18adc65870903e38ca1f550a836186bccb7daf67d43d1bbf1b7d300fe24f3a0ac5b3234fa7d5f99171e3dbc6bae78671ff04facab864b54478ccb73dda15912f8783f1abb28d682fce5070cfe361f07a3a8f4042d57223de7a660a0befeee3566c73a34de2d1d4c0f321cc4c48fe5ca5bd07f19f24156217f115277c5f73c6f8c1ad9f45e02e06fa6bc4e81438232df64e6475e2990a38b4a937206f87399e16b4d9558cd283c8104c2b3cc31e147635100ff0f87398dda7a6bfa2c4cf066e687f1f01799f95e001f79c7c8fa7d28f77cd8f5bebc55194de843474611a00f1ffba4b3efcea159741af515baa3be22235da519501f847932ae2b7cdfe6a4c4cbf65c546cace1e0a311d09f3d7f75b8ae5254dcd738fc1937ff8db0c60b855bc03f3b5ff93e8bf74c284d656f4a48a1658f8782b812d017b2fa26a7051755ddce51f7f062a8106d20c7830cf0afe8afbb8272530fb4264f11396797cef8ecd46c80fa4061c24ae96641ba2a914a62aaee302e098df22e203f4188fc92f32ac0a552c8628b81a41739e2f9753b90df637014c5cfea9d6a488880af2d3bf76891922f00e2ffe7becb45f86f76be21b81da8133135ffe4c02305e2fb1c4f3f43e50fd72619650c99d42bb19fdfa8f280f869d36e98bc73f803cf8840df6cd6dba71f0bf380f9f94830f591676f916687ecd0152144ba0ac7e923e03f7db4caa75c25e69a488759f964394ca7e331e900e41fe8a458ccb286b2f5ea790fdabf69e151faa92702f9a3ec16697594fe7c9ad06ec10197933deb8ded2f00be1ebeff742cc8734fcfbc5af5939a9e6a9787551d70fd0ade796fe453243fd62b86f52e8499c7da96ba01f5b1508a16c804af434df4127d827fb83f72d163d302fa562ec7224b84d04f2af3358fc65ea87543ea7c3106f23742a88f06d87e36b9db2ce124333516a9e8f8eff780fd0daf8089e1e5020c6f9e018f31c156c6e1d714207f78129fd6d748fdf51a06a6a41af11d25678eab39505f8a429b225b147a93f6fef3d0b7d5cea634a67259809f766765ec3ac07ffb7dd29957447cca803cc62908c83fb91887b717181de2aaa5dfdc8d1fb5789e54c303f9050beb194d0bc627bd0e4143649b4fe39b679f3701f8c43cf1581d0cef58e5d0073be8579f50bc0ac38c017e80e1cbf99e490f76620919f94762d8a117f5c606807fd592ee8fc74a8e1443d444b2d36db9161ce20005f08b46f0cd1114d7f98ac291b493b5b66bee481597007c4dcc8bc71d5bd8ee5a4b81b65c6829f02ce02bd89f9f55d85f2ae458232752340d452fa7011b369f0cc03f6ce25c1903de94bb26df0da3b7a2555c701a3b00f81ffe52c34e6909e1e3a83dfcc7eafaaa83e0d25b005f6ef620511c5f7f0a2f90a87855ad21fdd65f0cc42f31f3ecb592da961197bef1ec6a7198eff75c6703f9978ec48358c1a972bc657eaf819f9edc44115897807f858053a1b86974852a0075fe0c4a89c96901af07c89fa0e9a3ee2314f0b17f474bff203587c074752309d4ef5187cf44bae474c2cd3d569c97977e3df586fe0ce877170c74b8404f47f8f6f3370655dab3a7cfc26d03f91ba2d47deafb7a9c419ff85676ee81696be1c326203e18309f6097fd7664af9a25b4638d59d1d6caa10be0076a5561a24226cc6eac8c469a041c6f239560e500ff543753a697e615e68115f43923823744dbe6321690ff092ac7a37bcfa3b5b068a297db11ee42a1a8b600d60720509e48189ed29ca933c740512e4ddd147d06fc931b8b7262b75569974a4beae529e7db55af311600bf8da498db5245346cc9a61ac77016962fb5c3d000f0b335e83ef2a38b4cd40e8fdb4234eebbdb44610ce0fdeee94c70521d1fc2303fd7494c7f5cdb1972c907f8d96379710773e189b369c1a37d5e05a75169dab7c0fec0fc6d2c40e1ea69d764594aa03fa92757f2da14105fd5ef6f4b5ef39144dca272a6a28ade8f28cac203f97d4d7a9f08e83c818a10182b995b132865889520001f0d1a162075d4cd5ad43f880ae594c8e25a0903da2f4384965970aac20d30ab59befa54907a32f30e0112e01fed72af6006e4dc0b3b0f147cf6f3217be90d3703d48f5a36350d4010bfba2c9ce828bef2f1dd9592e702f4637c67028e1f8c1a9016c949fdf463dfed976180fce32d797236ea6525852f379fd874033f4c48223d05f413a4496a22e06db6abca7ef8e2f89fc61b57f981fd1b3d7210c23ba946e22b86731c0a2b0e6d7a35f881fd9395bbd7e3124b98352ba93ae3ce812a72613c1f887fb1abbca073e306963b7519c834058976a2a49980fe48f1a55a623b9877dfcf3bfd5bf666e072760ac581fc5bda4a80daecb60e4b663f191f676865c0ae5817503f979f871756e2b784200f8f26edd1c9bc5d6c00c69f3a9699a46cb2a1fccec904d6b496c949b0827a003e3d4a0243fc0845c4a17c27a53a13b6302309b90ff84f2b85e7345e9fb3d98cd65e393d5b09bcc798e100f26b853f9a5b6f5513b3dcd7be8c3cac37b886517700f149855267c04fc4e4bc702b9c6ccbe5a29044c30f80fe6971d425bb77a65c689df7bcc6330e681634622200df1047b01f592a714ccf24c3e4e0d5ef449b924602fc37e6d191296f6b32d628df672f2d6ab7629d44b380fc6805292616cae9888d9d010bb95cdc12414cd00130bfdd0c1e8add8dcc11dfadaf240a3edf6e2e8c0806f0df3848bf77a7b741769ac2e63383a8d36ba13fb580fc5c7e20c2c2c6f7e56b12d966a231514d72ca9160e0fcf7ab15decb27abb0c4223ff7b74b540554ee7380fcb2abeda413d4d856d3098debd1cca341c79b253ea0bf085699eaeb6162ac1ade2e550ed99aa1e721c151407eaa8658ca2e251515062798876a4f6bc74cc4d519e0276766c40645f34576746ec2355a0d7951ecf50b00fa7c8a7d0e543b28b2a4eb128cb52ccd14e8f726ea407cb11adfca093955212a2814365460a0f844316005e0cf1becf7d614902aca7120b6a037eefc74993cbe04f46b0d9f367f0f1bc6bee3ece68663c7d93474ea1204fcaf138e5d79bfdee7b6b99cae17e72144e39a7d1a00be1b3a5fecfbd2975ed7bd306c1239d79033cfa00488af48d42033b14259e962d93d3ea19760b79fb7be079e9f752629633aa42923cb3b2f0d84be60d3863c6560ff5595ed21324f1f98aee0c51ba4e426a7c2be4c00f4fb136c55dd5cd4c6a0913f7fd5e181cbb2832e06f9bb79990375e138a8a963d861ca6af9a2ad443b6c007f836cc585d5871b89e767dfac99edaccd1e46ba1ac0efdbfd7e29b43e679f53359800d57bbdbaebeadc08e87fd01079651b8ef7dfd4af90e844496289d2866903f12be40d25c9a1135bb58ff344c9f0fc6785aae2b7c0fa50776112dfa6146805c9e899c5c34c40557b810ae49f0c8a43c5777b9e4604678c411d9fa1cd9ee87502f9c108fa9add56149a4faf0bbfaef23c607ef1f4780af0ffaf486b167638fbcfd7681ae7ad267f368e402001fa8b21d9dfc58522c43a98778aaeb1064973f8428f01fdb3651911679b74ebb45359d48e5ed303bdc7e543a0be6a65c7c80e3a34b6d629f7f95613d305d46c430eb07fe91074abf139e5382c3e202bbc4822e62dd3cb02f21722dafb3c6cd33fdda231c6c81988a044c5c36680fccdf74f7118778757eb56011fda5ad853b11d556201ff1185b0d6c4c63f1f3afa884abcdee1032743c624503f652198b6b6ccf7ac9f9c934db1a2865cea62511ac0efda081e7d28142687ca07e58e1b9367b8b457d300fc233afb28e844743c23fb4af1711a35ebd02b0406c02ff46d445f9a1ea00969c8e4ca534cc493d325c601f893020c47a4a26f3b560ecacf92e992cb8f4f9a7d80fc9656bfecd17efa8eda57565c9316832905225924807f4454ed93fb78f83aaf941ab9816de64c9f04951ee0277c5fadb8b9ca3a6a9638146fdfe59f65d4750d03f437b14baabbf797de50bd11968e511ec9a5d608fb0cd4f78d79ea2dcc36dc64493b5bc63db8a21c44e14402f9bf8692af716850e2d77e7c18ddf9691e77344b1f80f86af77866917c1a3a3638d858f62cd68893cfaf1cd0a78ab4f348ed581aab1d6c9bb1c5e6a4bbc56c4900ea232d873ee52ee1be99579f6d1d985a16d9b452c601f853b271c8e5abcc454eee0df4cf5bc8f1858f2d4c80f8eb49e0131fa88c16b3824382b9588798275999f1007e1ca3ff03917d61a12302428b0be9826f2dc4c81de83fc8d6f3a6ee1b45609c15dd079d033c0276632232a07fb515dca906b39e5f92a7f533df0879a172edea09a07e4e1b8af85d5af1f35dc8dc1bf61caba9378b0575407e295d8390918a5da71e8130ca3d5d652f291b771cb0af0ca3beaf7c6eb69c68105246e952f2b26bc210803e6773c0d752aee7385c9a7b85c2ebbcdab9a7dedc007f67af6e3f16e506e723d6f96387369abaf7bd801dc07f3d614376a4fd42ce17851d7658a84da1fa025116b0cf9792295b76d412cd3a02bc26ed749343507928545037b063de7e4f4485fde89e7c6b80edeef4ee87254168096cb523791bc84adbf6038d75677ad33ec58c7af85d76c80c065f802fa089f0b77fc8813dfb58b1eae5688cd1532a52f517b009d787bd2cfb667f7a3824acdc2f7e60f5cf87a17f310e35089ff321853bef04595afe5d82a662189bcb7536fc2eaad7478c848cdd01aa850c3831e80a96d0a4dc629cbd0189de79041ade55b738ea8745fc4e7be935cf2a5f425e51e4e26113688b07690f123da7e8af104d682e8cc3959e99f82cec861fb3ad3df66214f3246c5d46cedf4f0ddddd8fa7755ccb3af0530b6d1ebefc91f7ea1b2aa155962267bfebd2d17c3026aafb5959ec765681042e95ea836e6f1b32fab902317f4c4c5c491175fe13c7f2264b6b99b3559ab16832279c7992059a9eb80a966cc2370ae55c7c213fea909043bcdbfd6a0d8cca0c83d1753b9dca13df095018a8f018e19a49f83e482611e5bb36dea69f30503fdcdbaabd9e7a338db89dacf2c6229e3063eb4dde48f65bb5a082ec8f08e1f8df8c26502c59c7a4e5493806cb870a579fa148d6f91557ea45769805e44550d54d1e94c37754e341abfb79bceff0ddf8114e98ec19a0849f176f508e464efc6588dbffd16f55c4fab391f9c093b897dbf056ba32ebdede4b68035a8c0cde34efb5c97aeaa5247a8b4f961a0ecdc6d1e0b76a9208eb1033c896a9f2ceeab5d25da3c89fffe0e38bea9145fd54b190f8e5049dcd2b87644a14f2c78cbdb96a1aa1eb0451b87195ebaa8ece9139f2b5f6caa4766bb50f9177981f15adc4a4b54051e669078f2fe3027c8cb7b0fb100a7ffd2aa39a3afcc727c1b26b8453cbc6b96abd076dca32a3dc42eb8687c9f1fb675c32e6ea3c9f2a880cbbbd30c474f5ba0beb96a97f2ac1e775e56cf1a596bf7671c019f1c590f87643b3d83672ff64911855cb91eacefde3dda02994a214213b5a7f8d69276cd0023261f384cf867058db3899e34ffce317e3d687ad31125aece5484c9d1526bc346e1676b7c22392a49f21a571eef6f094350e3f63045cce3258ea5f72fed4fbc14a7f41b4958cd9e1ad2805b5793b41fe2383fe98bbfc048a2a2231a447509736836d5815fa867c3b8ea9c8c81557c0ab183f33f021bf8f33635fff31e55304f7a601da18d1df313f21db7ff105b2df9bff10006665ca278df23dd82dda87d7adf5fdc02505a180e2afe677d62faedfe017c7fbbf6b5128f8c577847ff31a219b6cf9f0391b747b327c0d7b68bfa3be34285e17b1c60b263e31d83174bf482afc8de787bdcd493a639b6befdb408c23b8155dcbe93abd04e068ed5037904f03e52450071b8dff6d6927bbf16b80842963b93cfae3b34a773ac4fff1283577742f43f3b9c42f2e0d807ba6bfc61a4bb508d9e54127ec455d185f1b21dba4fe2de37fc767f5575f6887ba8153f6f6438be5f5a3d5ef698013f72dd518fc4d7b87fa5b6ce97fb31f01e7e7104ae5ede960f6953241a0b96ef346bad6ca68fc95adfd62fc17f3014afc9d3326700f8b3617f2af3f32b6bcffc2a481d9084bb768447dfa7bef055218e29ffaf3cb3553fc336c1a104b63ec55cea285371ceb9fe0696507232808bde3a8ffa383d90cf11cded4fef89d3543a4bad6e404ff8ca0c81ffaf13e8a5d9d298c4d3f2061ba5bf4792cfdcfbdd7557b8a58ca35624a1b1456930cbcd99254a7d72b6b94e456af12f55b3151cb5439d088fe414fcaace939339bdc28f78620e4a5fb153d0e032c14ccbfecfb9fcfbebd6e52e7c70cafb765730f0c30481133dd1fb934ff68014cf0ce785675fa5b6e85c80fd6ca3547c63c6388f54fae5db7d61382d4c204819eec42f7332bd124d04769f2d710a1da06e8f8234ca63c648cb436d056fb5ec6429e1de48ab02c25f4d65c84bff1e3ed503788ff6353d1caf0a315d76840fc476c2ad0bf67f4d0bf304838e0f79f89bdf149f5b895c6f216c2b2e4231e3f232754fe3f3d293fbc72966bc6be314b0df6f3c896411978da850bffaf49f98f9d944e1eefa786892ab8daccfbde52c99cf84516d351fee9190a5e2dfdcea4fcf8836760431abb39250272b57c4eddd1d9421df3b3badc3c92bbc54f290eba52ccc4c5a2846f8bd9fc9eb9e9c2c5cd45d8fc576ff9ee93dc8a14af3b3109cb6909ff414f8a5f081337207798d7c3771a962d262f239474b8fb5f9bce3f9f7dab084babd0cff8ab09bc5e8889b97b99a39a7fabf44707d365c3d4b859fed6231431dab9d17a0371ad1aed8e8d8b557611492bf39132d63a342ce507afbd55a8d339fa58921c8f94a8a6adc28abed2f6f608a966c4c8d869cc638642fa88cdd7e5b7ff28a7ea5d8e61ccdea79ae04ae1862c28efda4b6cae648f7fd9f73f9f7d4bff6c0845f0c676de1f35d51141574de90c3d0afca38347228d3a3d5a7feb0245e451828abb3c5cd456671f9075e44f71bd703d6f2249f85651bf3b37ca50241ec3b98aa70e9d0f698c8e1cced94da4f9787ee8bbe18c2aaf55dff5c8dcbf5b77fc4da70a52d9db0f61e7dc8f793fad01525c5d89eed3ef45da10debf7a2322626a328a0afa9a326ad2e22a229afa0a226a321a12fa6a8ab2120aff9537fa9b4e1f30e998c4d71cafa684a0307df9be61b413604c85eb024ee5ba41fd1ae15dda3773cf172b10a7b01d1bc277a17f78668c2e570a29f1a976a81b98ab40712e39dd0a083fa807423fba78e30698ef63cf6a667fefd1c2fca7f5a23e9fecf8f3a3cc87664376a14f79756f495c4c5caa3ffaa45e603b881111c156e796f07d5aa3cb3b5783f73d77bcd00aff9369f06f97fbef3f8d7e867dc89ef8c4df3a60eabec23e56e1774f39ff1e4603f58bfb83fa05e808f58bb90cc6b8a2fff90ba2bfb840a43f1a409cb77af110f47f76e5d263a0b3c59d44570fb23985f781e8912eedca4bfac5210832d6ef4c8d1d4d4dfece9fe47c6b6be6c0c7ccac6c6524c6a6a429f75644cac88e95d749d5954dd251d5d5c19c4741f3bdb98aa1b4829582939303b78d388f9185e63f70b1fecda9fdaba5e9577b81288b8ba1a931b7118721afa9919911bb312faf190b073b0b8f31370b3b0f0b0f270f1b8f3197a99911070f9b9929070f8f09bb1987212f2f9719071b2b1b0b172b37ef6fdd3f82a9b3151f0f0727fb7f9349897c0d695433cadd4662930ef267b0786a783a2686a0c21f3e480f39fed484d63d4312954827552e77b86441e55554b05e5c4c03250b27b2573e5a258dbaa817cb29e12ff7936b256fbfa4046fbfc23ad1faffb4b552094c3940448b5239d31938e93a1b6d95c1c93cffa353a9cc55eba5e3fc434ff92938da62b6e21f7a4a557169ce03ad7fe829e97228092e192120da6fa0e4bcfd88f8a21ba0c443481544f91428ad20182414b3436949129dc63fff8deffe0f7d4d785f8696632e965f1d8fd662511fefd9d1374a24a945463c16d43af7e5b2c91cfdb3fbb19f95a1a6856924ac7ab6c987b93e99297d221a361123ab9f04e1790cc27253efdf6ace925dc0ad4938743f7a89bc6fb53da2fb6178e156d2ef797c2ddee451666cd7626116cf343965f7405d1c67fc81361feba76c5087db1b8e9a246f4b98663b29f83c7d178dc8c8ab586f0eddc839798fdb703e046d9b13387ad69e28a5c8a19876d289fa57ab25e275c841aee54c3bd5ef91999adb640b5f4f6f7c9a43c0df0f697bf515a1fbdc1ebd5606abab3cf1497a928efb278d389d8e78143fb29c8edc75ec161c914465b357a594f5db0d92c4cf1ede467135eecfacd161763ae45c06d4c3bdcda929f914ce37ae43c61238807cf31cab03b519155da39e5fd8f1c30714a710c7aaa8aa52a712693ce4a455a6f81daae4cd2a773c75dab8e8f86e0ae25a87b16eddca2b3e5e6d43a6ec1a8d82e92b074ad9b3315ccae4b7765982c19d5a6e0f7cb6e5e72ba6656e8fe3641bab2e16044510ddea182ed224566bef2e659f974f7e98d7ebe41e9e44417020104efb31d049e77a8b98fe8dd8d97119fa908007c2876c34edf4885c4236725dd4e473ef1b8fae24d5e063410f7dc747691716d6db7762be0296f55037907f212190b4c4354076e35ed102fb616a0149479b67fba8a3b6cc088abd7190826f20501803d14932531a6677ed10b83c60e238765f3d13c9f8a720b4258fc9efacc7bf3efe97b18b48dea14aef8699ac036d99399cd7daab5a7554eb40cbca31367f2357785778fe7ed7f797d1e3aabe1e0db76ce8282e6b3dd5db2c5ff09c0d260c98eaba6e50e6dc7f3376148121c80b31f966626daa323f2c5d24f8bdb180b37107a152d4b14aaa5c92742f88b5be398f83fffe93f0ad4c74296f0b7191b3af0502e5d030d5deed07446984e65fafb737302e41d018e1111a7626ffefbb8531c9930345a1fef6d8f397095ee0835adc4eae6e01f9c32536f1ee3137a369cb47b9e83f7ee7bec23b6fba2be5a3da7c464333badccc9fbf7bc6d2df0c33444a29d1a760e21cacaa5f21ecd2a669ebc314fb3a3989735352101afbf4d2e67f17f3506709f127be9b0d78fdee4131a1c3af69cf7b59558fcd2ab4825b625c9d9bf0ff5b5f06f2377d19c8df19fef5f3ff952f0f41b633fe15ae9e9b906e5780ef457b8b11032e8c388c6c894c8759d1e16b3edef3905f5c1fd9ef3dbf67c0f3813a76459d49e6e3fce9ef012bb85416870aab9ef03bf6f55fb04f9187e4d8569bdb25cec2b182a8cf2746c68842fbbd795b4c597ac7af7cb55bb11fa0cdea49769cf0b96920b7d6e5bf4e2111cd3835637d6dc595232d153bafc946faddb5c75702ea23328b57c253368be429a5ca1f97aae173d4b84fe5f94b7b96a0a16d61de31d55364ec338bc6617f36828351f4bea1d9f4d8d296becae5524f3f4385dbfaf5da73ecfd57e243de739f82c85f830dd78941500e11949b64107f851ceafdc5e44780dcfb929267b488f37da33e4d6a73aacd87b0b5a71a4ebfb794fd5760908920845ba8851d35083fa8d16d7f6408382def76a86b656f3fa891537fa47b31c7ffecd36973457b3971ae298e92c85e93390456d2883c75ff53e7fe64ccbb1d6a3a1836a3e6c6511002c21f79bf13220079655a10c2bbfd06eab9b73fdc934f980d50941ebc632cbf17e8fc72f1de82808040ffcbff7ffdf7d7cffff83ba06888bfc853566604ed2d864286649d8caaf3431af269da6c8f58ba7dd197ca504a6a86ef4dea12690e3ac777221f846c76157b91851b2060d7cc2e9208545f981a856d6637f6a8c0d6dcd80e3595b6303639a9c29a8adf079ff140236ce05fe2d6e3a6efbdc6700e55c51ad90a43df4915c119347197f291ed6622c450d5d629fb903f4f27a8a07e579b485e742ac15079faac57e3c92b7dbc4abb234752eaa451929ff1a29248f6f5ad317851edde3bb2c8c64ab264ac79f08d2f96fffe191401fd3f6ee03fd228512a3815d29413905c9cecc5c456badc3f4e5023fe47d4a0037dfbf6b386f7d764ee6f25b9630375d10ceb807cb2f53873e8d0488af9cbbc99cee04ecdc45d67b3ca3f06342e64b6f50dfe77f8e25fec25f5e5bfece55ff6f25fb097ff159e16b8fd5e782adbde10bb0e45d3d70e75035de9ed874988e8475d0bd3009dd7efedc4fb7b3e1bd4aff2edbf748afe865c007cb8b678aed677e317afcc358ceb74f620e0903ae16010a0a061e121e010ffbe1f32f87bf1c57f9ba30548067f3e4e9ef6a7e39ffbfed427142ee2adfaf3f101d3df1a2f3e51fad3f15285953f1d2fff6cfba7e335bc6f7ef17c6158f6db3293310424e932ebbfb66b04553174c59ddcca9f8dce7bcf08bea5d7804639448de40fac3a602f1efcfed12e905a183e8fcc6fcfaba1256651df6f6054dbfc74c05bcffe856d2d4930ce679b7a0ce979557fc32d0d42467da4706fb7db71a91923814b980581ed5c04544d8c6916a333c4a7659a64531fad90ebee64cb197f757d2e7bac1936bca5c6c88a73907d4494b81a15156b4572d8ab1eb90e7a4fb16f3bf5763f691967a714660de6e0df3d8d6f7b9569a7217a12a3949b791cadeb615bfa6af2999952ae1a648aa457ae767d7c7d5d0a0a173227b69e81f29503bb146dbd9ac76a5b0774546f777d1229d1178899cb16042b1bfbb2f3da0919b94dd60fd47f21d1a0af38fad1e705feff90449355431532fa49803a61978e29e1f61c0e03fbd0fd5f49d87fbe242c17d5826bd097f4a0868cc5191ac74b1f77ec30ca5f01a6bf132cb9332f0daac287c9de4b107c914943c05fce72e4425f3920d37dac67e1be4d14c16774b7983ad2de545dc257aadcf280799f18e76c5d10c95098b01a2f0b493bf28f22d168d91a5bee63450bf8af855a4d938b8eb66d9f75fccbbefff9ec7b7c2b75446f8fc79cd2e8c57200aea3606601cefe1fdb370b7e02f7cae4efd877c492af52c4bad75d615152b68fcc5ab22f51124ca5bd11fc96a4c1c1d7a9b87c8b7c218b174c588fbcf5be79ee264a6103e42b9895a19ce3df368b5258be285fffa3ec1b51aeeef9e4648d39257a5b1ffa5cbd9deeed97b1ffd3f6fd2f12e47fbf7d339b981141775406d23f0dfe765755e184e82416f0a767c8ffdcf53bf67d601efdb569c8c9b7608b207d6d9639829b5df97033ea5a22f59dad32ceadcfac75ed5cfa46d5683251a0d5d980dc4c11ac24565cc28258527b798d4d028cf297df22d1dca07cf3f623a2ec6f40e91627bd49fa4d7f0a487af8c5a05f58cac52cfa5d8f49b1a98ef71779b201ebc499804e563ddbd0ad144d06a24c5942de002d3da056d678306c9f725c85fbb92192427be528d6aa6715002e1db43f98c8ac88416d632048184a2aa7e4bd4800627927ed4b65b81b3786163f7b36d8d8602351b47980c4521fa99b2f5d55a8db08e1c97717d07b9615950174bb7b8ae276e67c769c8c636accea73e11a8615960a74e37dffd0f8849de356d68b2d0bfad4123a0953a0560f9cbdd2546bc8de0ff16684a45e284fa317cff68120328a86d094038dae4f84441b4ee0ad61b5b58725a066b2d5f2e0f80ce1c54feba835c174ee8b20f6822e408d456e4cf4fb117a2ac6364950d4e26bb7aa82ae34c00e1998b19851233a615eb93c5443c2448d0f7ebeb7fef7e36fb1cc083d68b710b6d3561c75b286bb9b14ecffa329a1603ee64dd21772ad65b408c025ba74026a388a5d8eb071d9936f3fbb68793c3dea7386c0240280e9f15d11b365aae1ce17817155d8d91e9f1de54afbfffd3887422b05dcf17dbe39e26ba71a521de33766184010c6cde87dde827aebcf185c101e8bac8b19a0220dc8d5486842b78e70e7a288a7e76ee0fa72117b8d6502dd90f38de19768c9998849d510520ed83e5dadf1d500646565978d48593aa99501eccc5b287496399b071540ed235eb729a7bee3c77c84e87be9ea5d64618e5e6b20888b84b3f1d5e92955e8e8cc3c8be3b98b0a343f04d4f23e734c73bf324de3f9a21ed72046f31c5f463914500365e2301dddd6dac6236b28a60ae520d036e0db05ba51328c4c70b5247b25f9c14071ac71416a6315f500dda25e3ab4d71a20e0df2735fbde50bf105f727cb102d84feb5516d7890456ca9791f303b224a215b5ac40406da645a8ce266c9f59a22b5da0f4c37e78239fdb19a096b8c751138221a6b9cb05f995013adb7f7a54eb1ee86633e3cf2afd2107d9a95aecadfceb948d2f6b1727809a0189e44ed6f87935b7b1eca47a26fa7b568c087640ada40a73f6ad757cc18dca0f0a86745f3eb7085c3fa09b3f5900ea74953139a3a0f9ebca16edcc245c915a406d5b8395efd8e4a1a2c3e8c69bb55f42106b12f919100f2a264e603027db5cb1b8ca293de341cb33dd3607f65b3e5aef73c1dae5d01871e388f9777ed6f4ba684062671d99505acaadbee469c04f9cc5208b7b428575e0fe33e42068642895ebfadb477c0c6c3ef22a591d03e593ad0ae68c3f76183facc83b5a315cd28f19b4e700dd923c82ba4f3bdaae278c5ea1100bc8304bf9e177036a931d90dcafddcc3a0be037074228aec70efc0f37816e01499bdd460f54673c2d3d54dc31f986a8d64a1f09fefd38810b1e7a45283fb17fed9bc7ba0adc672ea92680dac10fe570f2a05214add50894a2048f8c26886b74c0feaaecc23165497e428aa75926e8bcf1d7631ebd06d44225b24f30ce1ceefbbd15ca7798c2cdd0b926ee01353b2827e6c44dde7accb54eb78a32e5115954f662c03e6402a16c5a47429f2cde2d1e0d23699a700e5402ddc2af0b79732d335d0388d043ac1e6d534e5dbc4a00b5e3b8be69f3a2256b1e119e6624e7e656be994628408dc42cb172452a67fafd787d6b7b011aa581d4e92aa85605d36c1a7758f69c4efc05d96afab55ace7b107d4c30e0785a1aa13272e063f5c8376399b1cfed007463ab5c50ed3a41965f4fdd43c1c9cfe5476a877490069eef58f17c74a3d50064dec2f061785e60ea552ab03e059bd57c847eca90f949689d23f491159d44970ee886b006d9849c2ed4f57dcff26bd99bca146e746f3840ed2d4287bfdc428b86445983d5b926ffc285f1a934605f5fe678ddc2f05234adf33fa0aef4b670ccf91c016a119a569417e6af1672395a218a7084d2f7b32f9101354093988355a2a71fa564212534857245cb9dd1a200b51eb882c118abe6cf61be27ecfbe7f5ddf96be981801a330c2691b9bed5229a98a5644eca049ea357f932a0167ef36ac7623e26307bddf213bd13eb9563791935d0ad6b29d58c6b87b60b01c3edc78f097e4478050d7d80e509cb8d97af816a144dd5e5f45216455c04fd5335b0fe5e635e7447665f1ac4b536aae23886ad73bb3701f313658685ad25d20afa856ed771a7bafcf76f0fb9805a6e88437af9a58a44d193401c56eee6f659429445406df941352ab5e5e4aad57a040f26502e306b1e921e50e3151573a4ef59144999c8c116331025ed1709c002f28c9896534768c4f8ddcb4ed2d338b3a8bd6482cf806e5648cf3959b9bb263f792669d2fb29a50516532f016a5efa0e7028d8c994e5a8acbdb7497a3b46111706c9ff7e1c353109459438456d8217ddd1bef1a7e46da018303fb8943f4fef452e7653ad415f9caa914a2ccf2500452f2abbe3cbe2564c892c8103734d8bab3ad6555bc0fa87e87da24850711da9cdfb955401dd6de481d20050f3cc2bb8565ad28cf142de87f47d1382b99f3cc205c0ff333a6f11a2e42e2278d5bf84f4852dc0ee2de8016a4807b4735aeff29e17d3511e22dc3256ef10a0d403ddf0789c90212d4f92891545730f4d08b72002dd10802a770cbfa59f795a3815880238e1c3c6fae49bbbbd000e53d38220fe5ec953888c97d70e77b0550017fe14b87fa76d03ba5a21656864f7f96abce4816893f1a50b204ed03c568a57387c027532182ce0862d773b5e09a8853e45cbbd761e751d47a125a6bcec77d2cab43ea0fef7e3c5f0aa5ddfe8d65da1940c5eaee7a140a5e63f01e6bf6c13272feecac61983818a42fbb460a4d864328057861c0627e606dcba51f7c051c6571c04569a8d02feb7e7de508d6ad587d0605aee17177a8ec60642b3801a73f1de8ccc38b78315495a2726875b88f38d3001a066d382e9eabb5f1954917b39b16e19841537a9470894ef63299d1aef551dcaf1095234aebf7133dc15af0552487ddf115099b931775ca550b326e914e8e9b90900b548a512498caf671d95b8b982ee9da107396556a780daf87cf708912d9535f1f1f66653cae64cf3072516a0dba5dd4fdd6defc40876cda4645a027e52ed669e31e87fcb15a18d2dfad19ad4d78b23ea15001e0726eb72ed0fc78ccbe1b762394ed913dbd2360a657f16ce24c5ccd1fe5510e02fb59bfff709022cbf2e57372af954f10c936acb64eb23337bcd69d1bf60b77f3e58a2e4980aad54645faf87b7de5a361aef1a1ea551fc8f0e465dadad2e30f93d58b9fcbbdd3db90c4b035f1a1b4ec6cced0d1c2f7218c6840b7b666834a217f18991b5626a5e5c435c44e093f02fe4f697486df952d6b11aaca2422bd34bf9b9a515ff28d8ed550fcdcb2a5c43c72bd5f58b1fb38f73e9455f27fe65dfff7cf6bd99e245d00a2f11f55eec4a0c7299252315f165df1fbf221d9cf599dfe24e8924ca35111c680bb9b745d2c09dadeb2bebd70c2feef9d16f75672563357c2460e4908f73fa2ce655281b1c3da4fcd6f46d951e4362acf3d9d7a7c8578796fd18b8bfc371fb9f94fbbff2c8c4c509046dff6e93c668bece7dbe27d10e75ad150c771ad84d6c7bdbfcbf4e6d61fcd2e8cfcf0cf23e0007ff44577ba2eadfaef64fd964878903ccc142270f169c9914ed730773956ceb7fce509432e5ffd539c9a0bb3b1a0cc88e43ca6ac461fa882d91b20323feecf1188b28d3b4ffe398221e5b964aebfe1010ed379025de7ee43dad0d907928283c197ffe289d80f84883a33bff6134097fa3db2af0b3e9529cd4acb5d49f1f8f0ca8d99eded70c4eb99b4ffb25d26bd71654aeefb4d16ce1150d4f90693ea23a57aeaad33a25d3b72965300e4986886bb1afdaf611137dfb8515fd42fe08f257c4e4f77fe2b9104aef6bae4240000dd3719362d9e028071fad65486a60a3b91fb52eebff22d80fbdfd1fbeaa90fef8202ff993b35971b2d8f5051d16ada6ec5f9861347fc33c879a0c42705ff9d8200ce587a9e81d809cc7ef08e1dd3e1444fb89e822ab25c48fa875dffb067afcaf158e44a4690dd0ddbf5fddf89f89004f8ed6db3e0f8865a7c9f3fbbbc1bb28298c8b7715d573aae676b41dad046196c55009173bef102e5e97941c5bb94ac5fdc942f99faa1b719e91987a76e32892aea606231753a8494b5ce2fd9ec3f9df5ddd08b5fa7ff97605fddf7c7e141543232357324d434b4b53c7bfe3f8ff55dda82ea9aca1292ec7ae2ef1925b999ddd5c81535bde8a55dcd88d4b4de4a58a9c9ba9a99cacabacb926bb9584d4ff3dcf57e4d1e1b49800e38c455682157d7aed4dbedb8d369b4ab8f8390febc1fa5b29050c97036bbee48ed676584b73cbfec0321236b344693b89f854c4d76bc47918fc8cbfda6dff671e8ba0f0bf218f553a50285b988cddadd71d70e8c0cd2f45a97a0c14804811374a28e2c7cbc66b3287864c9e0e9a408a01aabd8c1ed7755530fd6721d9049f5742f83ecc37a002aaea12f3a3d55513c64be4a5ecd96f757c6d32b7d0009cf32afbc5483d1d73b5978b7995cf79ed09ca090ca02aeef8defe336f32c791b403e1fcb4e038b1a0071d8003531b3f23bbd5533d3c80fa918f7390e196b5b10cf032c8fddb127e5af5a726e5ef45937c314a57b1d3db04f6211de174c4cabe9bb80a1b5986639e065b2bb06bcd8e7bac97d889ecc73aa48dff87bdaf0eabaadbd6a7bb439010894d4a77487749a7349b4e41a5a5bb14501a410405290969905290464a404050010504a4ebf7fcce77c279cff95cd7839e73eebddf7e1efee17df6da6bcd35c79c63be638c77084465b3c93d3914f7fa164fb7940f6a610b23fcc493ee7a23be1b5542380438759b3492ce747b57086cf73ef122f1e9decee6ba04f060a3f898d99bf70d6bbd18a57dfa581d36f395140980b945f4d05ace29f944ee754fd173b563665a92db4081e113f79746cf988673adedb063184df27961c6c3c03e4b4da78f2430c6d3b4a72bba89cb86961ac1ba0ba84253c73bca3aaad327ac146b19e4d3eea70e5cc60754c5ab12aab5e7274f6e3afb24313ca4aafe349a980ec469bcbe2a9358df767ecd1cdbd2fcb86a22f2025b2b100712b16312197ef7fc49eea559e373dccfe0973e99025505c1c3add69559249e0673292ba7787abdf1111d806afd87ddeb8baa0a1a646d45e67ad10bf3e577e3aa80ae3bc545ca1a8447da9cc6782f0334296baeb934a50271526dd4697e38caadbae7f1e7793b96de9b05df1907bad6c626ede47499de6b4577af2917407e596f6f940154354c8a972ad2874f6f1deb0ee84824a9a65e70ac06eeff7ac5b1e11bffd630c31a279cb2877c4628413540d793a2e256154a8cb687daf40636d79b69b9afb55c05fc1892fae89c927dbe2a9ec00efa74fc8660d3d47ea06b9af52d573b4c7a4e516924f764bba4764637417c20ce1752c5c14b062b5a67707de4b6741410affc6807e0f9e0b00115f4271f1623d3b4f1d7993f9d4393be0ba88edf784f8861f0d84a1b992771126fd82e7eeed647404c203e2c3f314d4076c307bd84a9aa0775bac828a9ec5b9c20f9f45aee9e7fd5caa0730a7d895828f7983c209329778d9db5b269f9407d6aeb596126be40590a1cf08c0ee7303ace0be59f620d997f316ab9d1d0aae5036c21421446ae85599399914df0900b24b4cfe4c5e2011e94cebd6ffeed97b7415deef72604cdbfbccb647d07e4dca3293196d39405ea7bc6b90e6265966fd8d3af00aad1ba949b89c9fe9fbaf9dfe4f0a4790d4a882ca401fe441fa2bfc69c06e63e0a6e5cbd6a78ac4342a101a000adfef103b7962aaaa99a3361586c4c1bd241aa221067b8a0d5af413bf9b220236c30e2468432bfe597377bdfe2edc5740499b7b73861ef72cd8e46ced316206902c9b86f8c04fb88afcabc43a3daebaeff421f710ffe05989f07debb49e2a1d4c74206fe45372e3a6127f50401a72d42ed241559fd565aaa6b71627163968134fc8d40d779e7a7abf97a1eeb6212f921d38515fa8edb3b6091bbc5a52fcbc1c9bd389d118eb5d72722629c43e46080bbf636ae8a3ae9c5de650ed2691bd242ce8c0175e0fdaebcd0f6e37f7b2ba1d2b7f65026d6c847cd9506e8ea3c6ea28291f1d19ce4b9fc56d77b8eb5c3b07262607ea75dc7964c11c069fa7cbea8d67b27267509160e741d306d40c67d753aa51b60ffe24aab848d5b813d13e0e7376015a4698a77d4d83e4ca9484092635a184c000896d6a1ddcfddc1373ac604fb13cd19931d92118801318e12a6348397d8cb57655d8ff5fbb896a5f01c3880ae5c714ecfabaf7e0c8025c67b70e2f62e3607859903f717c4474d8279b4f571966ecdb2e84e3ebf832127a05a5fd2f084788ce8565e0b3cdf322327af6975c11b585fb2efc69867b846b368139d2ce63e9a15533aae06ba66e8537190dd52d9e3b964e57fc7b5e602f55b0b3460fff2903e2ad99a1380adeb2e3d58ea964f3ffe6c0f749df9e833dbf374944774b1df4bc9edbd56bc54ef63403ce9b5c44551630e328d05fc26e178787d81e75a0730bfb39155b02edc436eb96a3e45d2dc7f9044c6280274a5e30d56e6d1309022ec4736b37deab02b4377b704c8d38876dbdfe7b8f19e90d1789c84c4a8e9e88ad70e705ca124d88c428d57de1f421066b56ba54b91a96106f62786fb9dcee6ce9b49bb8c41d5ab74f8261fc322f580df6f37c80dd54f54295a4ffe7c9ebaef3cff801390a7418d3b3ae5951a1f2c93b0ee79530f7537d0ff13d075e070036f132f5051c7ac6fe226265709566aeb17c07f219b8ba90e4aecb068adb9e911c7646984ebc50110e88f5e6d33485e19111330f5267dea654a703d6b1688533d52c52e7d73403c8aa9e66c1734f6ca0d27d31688537046920f956f2dbc4b91c5a1c299f98a55447b00c89dd8bc937d41f9c9ad4f63e6860641e7d78d0bc2ef00f1428add3e1cf2233a52e648a3fad857f33d2e9aa3d3401ce3fe13b62dbd64cf4a2c629f07fcfe3983d9cf81388e98676ca0991051fda6bc53c4f6c5244bf64914c0bf0a6d663ebf10b3774a2f79be9872e6f381be101bd0d5d1b043a85948cc5f5eb4a2638e3c04fb9a6c411c10471e6a44978b930c98280c506959ceddd56e4aa307db36e1e20c5e52ce219a9bcc4a942ba89dcb4ded02f2d51c38c588d5045ff9300e7a72e13adcdf325d01c2c808cc037a5aad978ce5ab6ff16af9a84a3e70216002fca75ee22efe9b87e3f84c283c5cbb23093a2f78d480ae2ed814d8be0ec4b42ca3702ad7f9f9f8c4e8d6eb409c39f0fdead2d10ba63bd3b191586e4fee8f35457001792a2f6baff2eea9560e89a4d0bd7cdb87f2e0f6db1b00db154888a9f36a5edf41b4ba83fa3a3f096a81462d303f0292df0e3fed7773ecf0a49ebd2a327ccc4a7a1e78ffd47c7d965fda3ec9a7da9716d528aad94c867f00ec37c9bf42cdd8cca44e288fd330076e9365a2ea07f8670d2f912f465beb9ab0d0314f54520546b7afb003fe7fde85dc3b15be9b5158acf86555cff62e1a628a03949b7a5d473827d71ec5b255af8ed034625f939216307e4b0f03a7d449d54c6c0b273663487111c289271f238d44223ae7bf158be131f8adf702ba56602bd270048664b2d794c25f45bb035b11f751ae47309b06aae5e020848aa6f720d4a158b71be804401c707eff3cffe76b4d95486a7f8865f14ced90237949f57cc77c9d8bfef3e3174f465e798fd4cea24114cca29c411e1d31f15c98c2c8d9d8e5dfe73421f910e621b37132c33355437d974ff9f3f8427c3ff06c7c0b149d0155b8820c55f802451492343a061b20d8b1aceee2706db71d76c369a7df3eff01be80e48c7cc399bf7fd6fbff377fff977f5a91f6b402ff54061e4a38ff00e1efd9f64f4bf848195448e16cd9d16e85efb9fdd0762e7d37d64ebaf000fba75f147be26ac74fbf280921dac59f7e51ea57d23fffa2680916733fffa21c755d3ffda2f8b86d813ffda2e8ab8e92ad48af2390c40ed5f9cd7f132d08251b0f4508fc45afef1fffd4efdcc0cf0a55fd5573ebe75d5230f84d7f56e29f247af0350343088b2242d8f848ebf07f46ee0b94983fc4ce8202717d74889d111bd8d9321a83c7706fe3a03ce55d7eae5c5f509d4852def76b730b9051fe6b6e41f5e1497daaa2cd95a1fcacb63c8dc9ea21962bacbf38b700f17f766e019477848822a5026af440e56ffc78591513c159e6918d93d0ef364993b3ba1d2cfb15e2094988ca7f65b466b3153b37b850b882f2ab1b1f060ed6a32ecc8af1d69887a542f08b3913874d1fbdcd958f44c2a60a74493cf8259450e325e449ae5d5f7038456f8d9a4bd0bca0fab3727f901c9dcc9ef7b19b78975c2a23ee6873bfc06f29ff877dfe619f3f6a9f3f3a8f6c9c847e37b74e5b25cdec133db9da779f50c5a6fa57daa7e6464bc6eb9131db9441a2f68cfe682351dc5e3e9b97ec0c301a3b235ec30f56a822a4b937ba3513463e4ec4cc7bc5342d715d927fe31e7989d118b1845793fea7f54db83e89814eaa78548918da44aa4351510a2f6be3f9c33effb0cf1fb5cf1f9d47364e428abfeb0f3739bee6bfb919fcdd9f2f20d4fb95f679f0c00685774f7afbb620e66ba34733535b011215f7d5a81ebd6a5f3bef752f13956dbf14cd53a339abeed6aab47f4248834954419983f89aed4109d6644dc870c3cfeafb30ae7dee511f02d2c874ca14e7898588ebf9faccd93fecf30ffbfc51fbfcd17964e324a402755efdeee7617eefafb44f8bdca953ba57c4e1c6c636ca480cbad97641d347234941e1eb98ac011f741ad178539f285a1a2e3a5f334b96f025cfda793328ffb256478809a68704772ce5ed3d5beeef3ece37876a9cff80433514dd8b06617098c0fda178ca795c1dc2318049a01a8e845a650bbfbaa8f86b179d3f12f6cffef9f184fd1b474f08527a754e5f0d3ff06973bc9218c568fbbbc7d69fd14c493aecae83ae432ead08c28d07bcd4f70768e5021f5ba0529fb2dbbe7c285a96ba26658212f9d96a3600ad82a45b26f9b318ab3bbbccfe83a20f1c950cfeea6b4d177e96d36b49b51a268946292513a91cc88572ba5970d972f15f3dbfffd081f9f5f3db105506d6ebf0656cc23a42cf2bafe25ca99894d4777f21a8eac659e6b725ed911bacea9ec17aed077fa3aa741337e9c9ad0ada868e2043cadeab41db4f5ddb4648efbc567da13e75253a8e5c80672656bf88bb3bc0214b2691c79822b2fb67398dd6a86298c1785a954262b9ccc37ccf7a2e9d763cfa63fdfedf37bf8960d2fcabf6d10942abcfa82f56e315ccbc72fcdda6707fe9157296f9fdd5b0816181b9930443e5d488696269803e86eeaee5613ed967c16731cc5aecdbe86c961b79321e1aad37aa3b103a63311b8754c94483e48ead9a95de1f9d88c17fd6fcbee3eb2d7f2da439f4eb18cdb5834de1dc873a88a37fccefff7df37b452e4b910f473db4fdfed5db9f377c838d2eec04ffcaf9fd8e87c8cd5e8f72cbfc68ed002bda352ce37ac6112d36536c492cc36837dd553af494c622dd7335f0c9acf238941bd3ea95ec5ed6d5e7be507efd32c063f7912cf84c878adfea1f08377e41fdc387d31cb239cc50957811a3a5d482560ab916dd2fdfe2f0b082d80bc8fb2b9385b9b6770a9333d677fc81721f77c67b34b8226c86ef30aa0785184c830d0b1072bec50f3846ac2e15be6bef285c15c0bc9720cba27b0ae88055b7260ff42ef3c6a5519333283ff0bd759e4d12fb5bdc2feee212861f337d809dc8b58f6daff002995f03f9ef6a5b97340a7bf33f650f065919347ce87b96f40ac87fc7cc4e7f0ab7ec8129d5e98c3df1eec6fd5a3e0ae8c498c65773083c76d1f46c1c0a9c3876092457e8047c2ded0f61e61c0cdc84ef3502a6fdb407c9487aaa019d93d3ea7364c9a2a3ce045572f8211b1936f8aa321cdfe2f32143387acd173875b8f8a702bdc2938adefa010d50c24e6e86615e58bb5a822fa7343a173dafdd8f0bf821cad868b6561c430e26cdcee557c27d85dcc4d4019d32724a590ad7afdcd524b99b1a6ccf50b0de4fe1020980be9b84079b6bd35d2bf535e7f18d462f1bd038013a6d34a355f6e71ff8f57e1c69b3bb6c78d08efa0e0ee005d35c660d62893e885e2a0d7c7464ad37b45fb97f8b3b17ba33dff15b6189da78c29ea011e34cffba10a84c0a19f149c6160880b7a3ddeb794ecbfec4b724e804b8ff0fa1f35776eeea66ee60116d6f1499af1c5800e4fb5481807608351f995f2de50039cbb536b2fb5780e6407a1a25ac664971c39a169df55abe98cd765f8b38bfc58f7b11f38d942222d884072c3b781770d822e580fce5f30af4e71ca227c286e8ad9d902f74dfeec41d01ea4067d5d2de9921a40d16648ade544d711d8f454301827b5b4dcacbe676933df472c5e9eb3b8f1e8533f181f983157336924d4eb9646e41536c1aefcc3177e780fce8f321fb5a5d61e20fd3390eac99871336227c6701fbb58dba713b752235aff5a98e43a4d2853bbb0b440bc03a98958e80bb299b68b481fbce4b2c59039d641a28176410fd6012c767c31194f38237657ac6f8e9581a901f5b1471ce0f85bb27d3f3adb66dd67e91a17b0c3d90bfd610f51a2f7670c8997f7eb2d32ef75e74683637d06732194f62a670f81551d06d4fab59322a6e6da21ca03e6263fa1a2cdac64a2ff56ea1a5f4807eb5eede2240fea0ac65b2b53310794d16ecd54f0c8ea5eb7ca4037400bdf4e9bd96b16028b156631f5db4b6b7961ced81cc0637a44963b2f5f31dede9f88f82d626be20706a01e3bb3696abeb24b2295849f5d962f312b57ac6037f207fdbbf9468f8f52cea9603dd6d4edb35b13d17b77c607e593974c6b3245a0b195a5665e4a1f225f8675a02f54382fbf73db4f914b3bcab8b9fb17e64ea8a796a00ac7f75c6d694069cc3a2c2e304733adc2ce1115caf81bd7c6979658a3d1adeecfd34c2e592d2b246e4940280b73c5885658ab975d09c9e231c15bb8d7f67bed3ec5bbc147f70a4fc7a86114b75ae0f913855ce00e1abae6f716101aae85cc5dbca57df555fe495787fe37e771550bf45abdb8d313c4087f3c27f8fc066fe8ba5f047bb9a6ff1e7510623242457c7c7e68b389af4069b7bb6da81e097a80bda6cb86f66208ff1b57991c7b4655ce7e9817d58f7e00aee952ade614bcf9a950af328a940996da0feac2fbb5445f2e1db6cfb50822d2d5c9d4ef7e27a60fd1fb4fbfcbe31f7f24b9b687c1ea3fd6702988cfdc4dfe242f38e1a040a1a04646b86e2b76cf51cd0c29780fa9889a28c4c06220ae94f973f5e4c239f5354666f07ea6b6c5893591b9b9c1de7cdb3bb3cbb9e6351d3f93c00d6af6bbd8889bce4979e36a67550c97c557b5f560ca4c786b691b7f8bfcabe4cbfdf67b1ce709ab23c400ce417bf9123420b123b8873e7d5d0e58d36c93a104b0674b058aea1e1cd85bde27c2b4d416b6dd530d9172d08a4cf16b4b0ec389c4aa9661a9f8f622e2984bb1e1900f555cf46a471f0a9cb5ac6dc1f7032bef6f01c36a001d6cf2ffada638c564d6efdacda72b2e6cec26e1929407e3bc215d3b9d55158c2dd9559b162e537c33a6fe5013d5fab64abf878148c47effa6af03af4e3a2dddbe580f4e20016f4969c655ba566c702893e36034c7ed34740f282c55471893ee797694cae2badb51b08c354455e40fdc2be5eabf9e70b68af4c9064aee3714b5db650a9007418b5e4e8d03b32c51b0f2372f978a8aebea34c2b02ea5326ee4d5adeefcf48c71febde8d53f40d9d8ed14c009c9d04ded48d0ff6ae6de3d64cbc2b82958b87a4009fda68dcb7226043ae74786767e22dbc6dd86eda15d0c97ba66bf8da69525557b9b79d6635b3f6f6bd8b28c0fceb8d7d2057758b09cefebee0b048fcb2ccd870bbd0b778273ae56dd99bf7b9121192320928655eb3aeba007ca98fa5cd3afdbb4f93bd5b9c31169eaee572935d409fe332cb66fafd71218a754e17d452f590cd9b8bd731001ec321af2d57155b98a6e7d6d1e5479c84f225c540fd974de9ea68fededc55e180b23dc9b2ccb7eab3caf3c0f895a35adc36dbadb23c52cb29321d886a7a5f0bf43a1e765d2128d9cfaba0882f2fd25eb1cef0764104deaf32aff8a8432ab113b138e2b139c60c95e4a3aa2d809c1e42745aa8949d64c2e37850cb71cbe84b1606906b57f2659a6d598b607408f7a02862e4eac34a3b03c0be3496cba5662dde48dd2c0960a81b4c7bd9e7439907bcdf42c4cf27177750bd97d52fb126e9e94da95ab600eb5370e2cb4c0aa6f48a0066b5af3d81aa36718b6dc0feae58509af15c132f37d24e3b8ec6784cb9ac13d0c1ba67044735e56f473ec75eabb794afe065716e166826a239fd99919fc113f3d538e94a49b6c608fd33f10380cba3499d1d311ffc2a3bd6b388cb3334db7b4c0de8909e583a442e10a4f50e9950221ca9297df6cd1e02d2cf5fc9385a072f3235d5c7610763e6989a6e461102f58dc1700752e6052ff108abe4b4818b2a261bb16d807f1a135ee0ca28fa1a87738af7d38d1bd74873098801ff7e647cac4ef4b07a2d8e5d914049bb3796dae029d08c635668144ece724f8151d3b1368e63d5fa48ed2e60bf115a9bed698a2a8ee65f287c848b439910842981f74ffc4e85583b87d49fe5d13236826014ebe3fd197ca4bd9b1188c79a2cc4087f6bb7653d25bf0938eef27b83e9590d5ade1ff60712eeeb612c8f93ac064185d6a0a3337f5266c90f0be13418af952544b90bec45af04647a83939f72bb3331cb295cfeace491a5f200f2e08ab48f6810811c1790df1588f0974bff6bfb956beb6a68a81afe68b7f27f71bf720705d77e9d95c42081801754c377d35730bd75f67f3f64f5db70b6f60486e01f45851e57df17ffa1f4db7fed7fff9a999b3973c636c0f1a5f07bb64713ad4843e1445c99ef2511fe72dd9ff92b2cc6e704ce152120b40e861b6d11f848fe49282574354d1b7d1f65f35bed945fd319fe62a22d793c8ebc5476fb9ca5f40ede5067276b100a8ed2cca2bd427a0e471dd29a84c9cc85e9a690839464f259d20ff4dfe184fe4e3be5228b1cb1b1e00bc7b2ad2bd51d2daaa2ecacde8fceb664fc6b3bc3ff077efee768a7683938eb18c9c82aeb095d17d6d173d4f454d717f6d1f3d2e2555410f076d3d1e4d3b9266beba26e2368a4e5f11399cb332ea23fa3333c9fa09035bf005c90df4a186e2128c46b6361c32dc06d69cd0be7e5e5e112845bd85859f10b72c12de142fcc2bc42bc4256c25c7cfc16c2c2fc9642d602fc677a7e34f80d67116eae5f363fa45ac39d229edd5666ad184174f0a4b5121e53b94f6f2d7007b3e4c560760a2ea73a52c15d7aa709fa2186b5e684ae67fca2fd84ee8385f27696e87a2af9af97375cff7bda32bf845b6d85c307d56f2dc48d7e5ada7213da9bc5db480458685a8540d4634455c6435d0a8d64d1b9ed21855a809b21a51c2d8b64a7d7f5475b698a9c921df59e1404345269e9cbed51581932a916579fcb893d63a6e39f02c2e08bd1f874c88c9a2d9bcf31d1265c5bada5e5418d7bcae5c54a92a73a7b1f36940e3fea625579af06d8008bd3f929db5c2da49cc618d761b502062c42a37480db7990cb9655764bf77cebcc8d62178137e79074ee02bee5c90bb10c662be337f6dc5753521d46c374cba981c43f492a74e2de55f3a7ae923676812ca7fe68178201ee54bff7934d0a7fcbf59bbbb7aa294c3ff7e2c40f02671722be0b624d4a3538373765fd8cf84ba5d26bde307f8b8f9af00dbae54e4b3e4cd06499b897e9453c4b00f8a60845a401921d535f2f26a016193a06e73e271d04b41bc65be3afbe085defec2d44526f39d026ef542900de7f80f3a923816c2bb2d9f56835ed37e9e3ea09d2808b1aad87bd9590689cb8769f274963f9e2eb218d2780f6c55473688063c16bef7312412c3012cbbce65e232680db4b39e5ce0e44f3512c8a8df2decf7dd0ce430cc8e48948df8d1f43988d85736a445d983f7cfc52750c8875a587cd3f7cbefe518ccb2876bcf21566bda439baf1b7f8403739ff9a5d7a18792c5ef26485cf7cb34a0e505a9bef4c59dbdc46a647fd0947084ee8578c39b74203704f97e46b9570ce25f4eea9e1a3b24ce05c7959093402149c45bb57e7f6a845ffd8e461eac2c9a5f9615660fc0492096f0da2596e33217c6e8ee9465f94a1e801e62f313549524b0587c77bfc8a21698939b228b35da047c6d347e7112787d830651fb530cebf5ec81d6ccbd6fe1677bcc725977754717deefc8956a2a9787bdb0a2f70f67a4b7c50f6a0665be0895a752a9c66b7d1e4dd0a90eb11aa5094affdb8ad82a919bedc7b94ca4fc06a0dd81f4c2e5c946edc6660beeb15e355af09433e4d2ce06c39597107f3fcd3dc21eb8f44de998fbbae92e67ca9fd1657ef0b4b4b7815c39f122c51c38a2125f4404b1be0beca113cfc4290dedb7bbf310cdc3e4c39be55d64bf72d4ef1d504e1010517437cf7cbf1b695505f1fe11d6067c35026d0b5cabeaf6527dac17cc2ae9c3efae03cc05dc653c55e312a8ab2e76b76b3fc6c28257b627a1bb0ffdda1c74854ddce6e4e8f46a688c51d3f6fef23647c8be78c047596684eccee284586069fa8ca7864637103eb4f4ddeedad2cb3d787aec4cff14231f86f715bad03e38bd16c3edba8d83cd9e17f5a94d0777fb06e17f8fee029f68e4d9e40d78da163815cdf763a1b1c6d40bb62c5d0cfb7f50d87693c6170e73b87674aeffcbe1c0367d71b89dd4c1faebf938ebeed2790fae92beb2603a0ad92b7621f1f766138efd9a3db6c182e97bd85c646016e3d51f3ab7cfa533d8b9efd186e2dab711bd4b231a0c78156c95dde7102ffe5c37282d188f03021a327ac0037e424ce18cd628713cb537f5ca1ddf992e734a908b03f93f1e7a4a91f18146f9e3b7f6ee82142aa21a6df30f0fe0e6bb8df38709aeb63b38dde537aaa5f61ef0870b7b5ae22e316ea0fbca4d025ad67d06ea2583f1305342acdee60223caeb91d50eda9bf4700d7c1a567f300366a0a0bce2d1bac5702bc6a03e24df41346239cb180f64dc908ae4396e4a8a56a1b5206d645d93a4bbf3da04c993f87b300de94f9e5d11af3a30544cca74f29db000d765b436a75f4263eec086745bbb0d095cdd2d3b780867ddbe20337af9b81ca2ec99be6e2f226cfbc2c1780f5ff965690d742efcb1ecc0ed4a77bace66203d9b500b78fe83397efc740877ce770b5acf5d9eb3dc9752ba0474feb508ffcb91966cca294e238796734ebf5c0cf803bb583ec7ba2a7d6b012866bd9958031a5f0e8f303607f36d7659def8427d85cbf3e246eb27fd324ded704d03ef97a603c212bb169c331b3c0246170c7a795d505e8f9d88dde36a4ee3b7e0ef5b1a8de694fadf94b4232207644e8a685d4c7375a6881988bfe66fefdbdf16e56405a7531614518ceaecfb36978c3b6cd537f807cd24aff5bbc3ebe7a77dcfdf38de1151a056f933eae870b2880f6916175505e8cc215a2acc9467e869cfe8ffdcb36c0fbafa512af7af96280775c31760473f0c2fc588e1bd00344e271c3fe662ea1bdb235128c197104c1634117882d0ade2c2d0849ee93796e62bf44e596a3c89061083c1f59926a724a2ca5bfed8abfbff9d8bafbd4f30120b698147332573d2f2742896f1d7e8b208eeab8f32ed06381c5abfe9dadcbe2b156ca65bf56bbbc2a3c1156a0474cb907711302daeaee974c0e86d1e4a2c8ad8e75c0fe2813e7c9d37937dbf827e57d0bd4c78bc3a56b80f58b5376edc564396d4344eaab9a78d6b0a5a754f580365369fde4aaea9328e3c2f39899419c8c2d9916f180ff9294ecc380e65ca281a7da508d21b1ab3875bb10d02edb4847e538e0bac134fa75e3fd39fe234d6fe148507bafbd8e7d4bc7e9fa57eda75eb3096d9c98346480b68bb2715c6bf806679b3d328d0011b785f7e29e09c0fd27b74af1045855c74d9a4585ed665732b509df1105f8ac134f9c261caa599f5aced7c37970ab24c244809b9e7a7c574ae89ed2c022cd02b2674f5080da84c8cd6f71e397140b706d1576343a9deb1e61e22d3239d855406c4e5491268883ae60e3f633e58f433dbb0f3d8b80f75f802dafb350bf97fc069725bce2beb89f24be10b0fec90f85a5cc712cfb26c7f06f9a1b72e930bd8e077a18390c5e769f8f09b2ac7d92b110e6714f90eb1d2eb0bf6f0b4c198f79558787592537b5dd4de5482c2f03f6574caece2c7c13b3bed5cc8a72045a6cfca7bb9c00a74655a6b4c246d21a6782b8bb3b8e2fde5eceb603bcff7eb314799c814c27b2fe972ac85b6dd22fde83dc6eaef140fca7b7658db27a813b5cbd3468681a7bc0fefc41f3dcce2c1f8a1496a25d8efe8cd502111611c05d9b449d43ce2a8b2b2c1e78572249d4b0a4d4ef05e43650c95a9657619185a73bcd254a46887127de4b063a1e748a575ef8f8e6d8976c2567b46d3980531e65301d691f79fc2f24ce9f75647e9108ae76c3d855037959abfc4a5c33028e38325559be4491e17ac9aa34bf9703d333c8f1d49ecf9cb7da893b842d7b3d676c797f84c8d16c0e2f94a3181e19c9e113e96c7e159b5df834f13f9bc8f93f2f828ba1e4e200b7f284ffb38d09fe46e458c873c365bd75ae097ae8f3485b3b727b1818a82af00bc9d85e9781cb08aa6b3bcbcae8bbaaf13b5ab9c3adfe73c6574a02971901693093b062c34402ef3c296d3bae659e8aa4e58e099e33199d2d751c5e63d9be7ec0d784380c9db89cc7fda1bcf8227c14d3f72217d44db12eb7ec845041062ff6110903ff64d4ccaffaeb10519c5b9c5e9c915dba0f653516423c3c9682425c16706121610138dc864fd8925b50584898cf82cfc6d2c6d2864bc0026ecdc505e717e086730b58f1f3d870c1e11682fcfc16fc70383f142f8454133bccedf6c4a2a184e291b900ffd68e9bd6e55b33355c8bc98a52b7f12e295ea1a42c50cc7c3c695e51c6e96ad512dd55587b97a920e4b54491f1c08eedb504cd56a47da49be17cab2d3313762178e6d8217cebdc7548d64fee6a4c9c91c939631b1744283576eb486e6ba04c51fa1f906b10b8f4f77f03bbf6fbb780bef47d5cfc3b953dbf0df9ef7ed54acecb24ea74f53bdff745599ee4f8ddef9b8bd38f73dd01d81ce52c8965ea6b92bc432c191ea9a8388ab767cd79bf77fb364e6cf033d8330a112d5e34443071f0cfc1c46be6b51db4392664dfbd20fa4deaefce37f90b02dffdfecad87775c990a41161dffdfe87177510c315f4e7be0dfd69a158bb480c7f1f668acd681e4c6a5b336e3b9c27d91a2d11f7b9f3f8de77ef29a0e17eeb9e6ae09fb4ebfefe72b4a4e11d30c443b4cab01ab9ac478843d196c65edfbb1ca5c7db57bf2e8aa7fd9b665018f6ec98f83fb8d9eedc09556bde77fb7831c8b1d3a277cde2a5cf7ff77de15b4c3cf89ddffa36ce2692f82f8ab3b91d74bdccdf278d2548c6e61d8e09a0c0f10a50d1109270db1f223024c62419fd11f74c494e4468f7a108a3e252a253e09a54ea35693acb3fe26cff47e26c7a56760256f06b3e0636ca7c36701d036d2f3715597be91b56aad276ae2aaaeaee2edc961e8a8646d63a6ac2ffabe26cc2828216c23c8256702e3e2e2b0b2e1e5e1b2e3e412e616e1b6e016b416b2bb8b5050faf80b0202faf05979090900d9f908530b795b590b5b505379f059f00d77f769c2d1ac31efeecd331638758851101ac3a008be8c6486f4258aaebc0c6abf39d8a33ce4129952fc29459b3ecc616ef5a2d1a9d5782058c3dc87e6045b97a22ab67a049f9df8ab3a1a3ff82389b5c9dfb2ca2b81d055b9fdba246bd0e0c39680ee8856b284a76c2a57360dac28a4f6b03578e19f72b02049c900324c6b224cfd5edc0dffa78ddaf71b8a0c22d0ef0bc8d646584c2ebfa14e7bdb53e283ef618c7577ef32dee799078a38570613cef69fd4ec141cc891d9d0d90039f30a9a49fcd233bbccf48c19320343fa559fe1ae071f4f7ab46f5e3d95fd24b53b3ae6d47a25abe940234743b13e32a2c058e36da6f72df8aa6313f36cfea3afc16bfb698a873ba6f46a3ea6a1cfb9a9061cfb1dd09e8657a218d2ce65d4549ca9a2c39bf68de8e83f0b20be059e5c8852a0b6ac7d12db923f6d8f4571d22f95202e36f48934a9ffc6ec99ba5473aa6b324ffb07b4407a805967c8916c03b1a895150512671f3fd43620a7302802734499b582c7ce11917dac6adc855ce41fa40470fd82eac2fbb1ba1ddebb461c9336d17cbd84ec3c5e0067278798c2fb397cb8fbf8e686fcf789883cdefe6a40d04ad9dd31dd90967cb479f5cc74cb6c1defff815df1f985fded1d3a44c5b4b6cd886026f5cce25e87dca5b01e288adc4bb69e46f2adfcc93b94f2727d13652cd6a007e083b09972e2ba7b0eeb96cac3b6c8fbd6e4e7e1a06d25250aed8f721314f48e96f051d646fc0a74f52ab8138437b147d5b6397ebd0f0397d07d237d641048e928067acd153c875b7d9ee9a65de9d5407752fe5f2193ca057321935df7397ce61f7b99cf6ae017317ac10f63220cea6770177fbde959c2d1ac588dd9d17952f0959b405bfc5e954157425cbea67c44b77de8c7ea258cdaf6801e23c2fd509a2d390364c743bead499ab790c0fdd8c008d75e3c34592feab5df979a37b1e0735322e1aef74009e4b0c83f2433a4166506710a67c21c9454d618364c03ed06c390ae46cd81e45a49f2aca318516b0ba69019e3572f95b6b0d2c970b043cd30a465faec8d26ce902278b1b41cfbae8e7b7b30b6e623b626eadf8aeef3b03aeee6aa9e3eb8223e2cbb5dc5a029889b222f75533011e58e5f090fa03b9af4ebcefe489c9c7bb375514592f7c8b1fa647fa73d4b1afacab04199f3cf3172e358e064e0eee65c32b66c339c9c2f614b4091b57af06c7f9011976d177279abbd9ad027594101b6eb6f4135c66120272340b046fe6320bc963d2840dbb2b19df72aa31d70778be91c8d00f355225dc2889c752c114c178b0ec74a097740de75d4429a18f4f99cbb3a8fad783965709d9803872e8715a8116460559efe5f1828ca29df8f5f2af803b17add2f8bc9538cf32cab2a2e933613abf706d3ed8c3669ceaca23b5742f85cd69b539b3541baf6a1980a7acd3f9d47e787de57927f2fbc6a268db944e694cc03566937b50ee4e53f9f46d6bce853aa601b597092e004ffc5c7b4b15f5ab7e4bc2db3a76b6a726773447918193ab52a98abb5758e6734ca57977c9b6dd2cacb975a039604d86c6065fd271e96ba4995d1c0cd7e7462ce4c0c9d1cc2c99718d3a245236c0c6a3dfba750bbde308e811d1283497b7ce5bb53051bb388758c2dff0e286105063f2aaa3085b093b024d29cc787f3eb3f23069f50590836fa8f1ea111e5d92ed4724ec4bc40f8313521f8e023d783a3a5c6453a93f77a918ed785b5b5e35f0f2fd0cc409b2e6320a7cde601914a83fe332289f3b0e411a0634ccbd854fb2f30fd412fbae2d0dd415c89a8822bb007920bc1aa1a739e974d3cd3561683a251c66d8b485c05153b3cd24151d712bd7de162e596c9e51e3eb1409ec1fdc54ca63e84baa629eb79e4cd3b5f6523c51ee016a841e9518d5de684c0e4db1155869109c47700e4d016a0004734b892d8ca89f582de211cbed8aa355c51b03bd909f5ad6877629f54c69be8ae94e97b9ee42217e0a68e0e31ed98c0f711358ebe0660910c7d8ac2db4e700356e63f1182d1febf75c6bda8733ade5a489bc443e023534f86c772b736f46d8b7b8cb459beea1152166de04e26014fc39cfb47aad7232fb3ea3db3ecab2b8f448300be041c8a9fb7184076687c61a0d9f2e5a5fb4cc9b01246ac2503455460fa526cb0ede3df3c4149697ab93bef62d5e35fda463bff7feedc3275e1b17454c3f69f9fbef7e8bcb944ca27529cdab89e8d706785599e4103f120104745e75a54cda675512b6f4d2443e5cb2e3320efe00cc9ffd511fc6cbdc2f0499a9fbddab6b6c6e2a65c0813870207338bae46453ff015268e0fb143d2afff95a204e53dd85c1ce8b8e119bfa26ce3b8d14719ef88300506332b21ef8e608d3b2b0dcc1d81a5e584cfcb0dc0cf87e967031054ef8c4968fdcd7e58577cfee155808007930aa13f1dc5cdeab9534d3c45fc83a309fd7a73b03f333f76983daab9b4e03d76891b9b3abdb68dbfb9a7400ff69ae5cb84b42d4a12b7669da6a34bbc9fe8a3460df99cdbb077aaa6477eefaa04e3035e332e4f46e035a0b1f384f452e04a6d8b92d95b6d773abb9a9fbf000fe49edabab3457cc82852d67084767e6bcec05789701aae5ebf341cce73504eec7f179e73fc4cd3dcf548c3e078cafad1fc7bc6c5d303c7832c504dbd2546c310c4845dfe84c25c54027a88f3d2ece8db07bf898cf34970df09df56fb1653d277a9346dc73ef26bbffc950ff65e0bc5c3c3bcab3a36b1bafb2781275134f44bf62c51ac813425ae9e26cc04f2f481bac5a2b45b22c76cce602f60742028c94fc88edfeeb1aa5a60beeeb083394e4401cdac53551967e97ca084fd2053357a59a8fee8b1d307fe925cbcd5ffa90dec115b3eb8ae5767dcba2850ae459c0e5dd87c4482be3a8dfd58ea7f005e38e2b8c02e343e4316ffaf5deabb515c39152f5c34fe397d2c0b6a34ba1ba2eeb39f5c3c28539aa4ad692e4567302401c9efd53f3e56073dea6255a77ad0a16a38074a314a0c78b97b5d378fb1b66ac2598fba956bd38866c550c60bfc2e23b4cb2a7e2e9ddd1915fa224e52216768b801abe0d7bfb5b2374daedeae97105b1309a2bf1173dc1ece7c596fba57596fe1d2d89ef771899ea3f63e90312f2867d4425f4fd125477373f0e5b4578cd3df6270204241df4945abfea5412d31ad75177d13c8abede9b5384b48fe215814c181ba7e76efd274efe42f2cb3a14eb7cd5cc8f104717083901c41f6a7c6b665e9992c81ddf1ad7175b842947abc78a5cfe77da9447e581f963ede9493a6fb2eb1a0954c7bfcf025e82d2b6842a4b90f2573a1feaad404ab979e1ab156d63a8ea548aae14776e1883a15e66c7e31db74199ca3b1277707b34b1ad10c9879d26b9518c1c2bb8383279090918877277f5fd29ffbd4c01e2199902c4b3c0d0e30fc594215c5c793b1cdadc987cadecce8e31eb3909381123ed29015e43ea89280659df97ec2b10f777f16ce3470f8c0fd2ba37ee789a08ff71a81faaf84c69322eaaeeddb3ccafffc6fc942aee7d2aaca1272bf3f99ab8204ed67cd6e91ca1f810c5fb205b2dd29e12fe5c7a2a35917b87e60b639c2f4c33db9dddfc951419b19baa1118d8e7a4a9ccde19b59f71fe4979a56dfb7cc0be42d3d814ac6e5ffffcdd024dc253c7c69a5cceb06c3bf2493b14f72b9edd0eb6c5b44bee171e54e6defcb43711e637597f6577d74bb12831b4c40c3a10b8a711f897e07ead2ca66921901530d7dcf1d568ddbabf7bba0e115765fd907899c4e6f7deb5179a42b54cf3da442bd23e52df9f638b47d17548ad341388386763d49010fc597817a7e633aad0e3fa9fd66514a48d93e3dfaebfaa0cd79b735f3acdc58f8498875001b6c01f7929d897ac32b58588bf16657909f6ec306a0fde3b05526dbe7e9ab5332236533da4f46d8ed1b12c8e2278f35dc996d4db93ccdfffc5fc8a5fcce8a259585d73f5f8e74b19f01dae3bbbc1af71c0bdacec2c5c6c7f2456162b4f789f05ebfb76ca720a65e7a7101fa8bb18c43d597d5bd2bc1ec5f389be7eb269eaea1e09e36eb0f743312751f7d66ff360fe5c63f88bf2606c7735d56b8715931f2abfc7d2355a7fbff4593faa518856f859ce32aef45dbd972e15ee719159febc45699f3ec0b54b547e24d0c22a5b9515c87fb9ad7a2888e1ed1ebfb07c400dfb7f76a0e58f3c1835b8a7859a8587e33ff9fd6f022dca52021e701e6b1703191569216f3b6e1d3d07651d7d5d1d59212d05033d690f2fb890a080aea6808b2af77fcef84a4dac743193718a26b00772c8eddc9db2a85711a3c24cc61d7a6457a606bb7b3ed3b821df892963d396f92d9d1525fddc7d56c5774781a3b81634863ccff51f5d28fb6f6c7f9a8121487d41a1589bcd8d7f1fa8a55e6690aec572bf41fa9c27fa238b2c43cbfcd8c9f7433b5204ad487b6a81e8217844ceb5b29fd7ef209ecd05463cfc1797a0fe165b41acfc05b115bd6ace873135a8484543c27c97bbb3a6e5caa4811a954fc48bb1e43678f10ac5046a245b123a418c4140acf591d5fc73c77c1af6f76e7998e6448182051aa0fe46f2ca63d173d7af61491acb327a342f6dab7ac50039e6b59f6fc951d45c91a50a17141cfe88f3aca4a11438fb118bd5383f1b95d3bd9c6fb941f9e5a6c37c74107076cd79cc5da86bb3ed727d504475c998e32d79c96b807ba8aa105cbdb183f75199ffc2367d0be7f29522f86de0e8a860c0441e175e5dd1a78bfd9e1321efc47e11e0ee7645e7663df24a8b68a928b7ed39c8f1e4973f014969ecfd7707e7326e7315bf0a9b67535571bfc5d40364f5f0853062197b07efde8ed8ce4c7ae2bb57b7bb04700349f6a8c31f2414ac79c72350c82f5a98e6613c036a5816086bcc30747768233f6beadf649c8936bd1700e8fc3d2672dc2478adface14e99e85529dc66b73e365809ba37995c5dde37cd3bcba8663d5af0da7cf4c846df95bfc6369ad325363e2d3c96749d893efcd175554c400ee7c503d9baff6b276291dfc4a1cc7e606ea97f8ab40ece98d47cc78fcecdde6f3bdf1fe01859be1ada643406ce0ba0f3b9edae6726b14cae26af982a93fbc04d4ff0919bcd566ba732e8b50c6abd4b7ed65f7450951e080ec371327bc2a16c9ec431dc0c25b3adfc1b2610df44f3f11c2bc631fa673ad8e8ae0b007267baa60ba07d4e8a837762985a7a886c34d6df2a573ca7da35dd580bafd0b8e32080f58dc67b3930fc9af4df5168e764d01fa236e9a080b4327b9ba0f998b5ea8798d590f7f4d020e58b76dc85efb5f99549a6be1a9663f79c4d979cf05e02eadc574166d565f2964ad5df15eb01ca18be101d34a8cf6336e8d5deb122097e7f4cf43fcbce58ada05e867f0bf139992e3ce6856b04ba3429d8ac608962e036a6cba31520848e687313eed92d6edaeaac13abb7d80df87795694a5327cd8b60f0ad87b7a811fe1deb004a0ff3018bd55c8efe17291bf6344617ef76adf5afa6d4280dbec5771304fe52dce2f683e606e6b3cf00a32066a8c0625d557163add7575b8b53dfa1d43de967bbf04de4f5e30dd15e6f0be2b33de8e1c476ca4b1450c57f080f9bbe3171c53f53cb565ffb1bf78c914ae7cb329b0b36ac57f31b72a94f83afe6c57ea2511a54d43582d60df81c5b7891562ee714edefb607838cb13182fcb02d4f0503772a68ef10fcddf37cf94c08d50bec45f800ac4ce94e9c9d7995447b999a396d3190b33acf57099811a96f81272ad91bd9e297aed32ef1ca47ef7131d5821685f3dc8ebca871539f417685f6f927e299d9f055c3cc7194df4bc9c11576941862fc785197ae38caf80207fa55ea9c19023176d10a69bed327be763b35a4620369a16f7e9bcdbba08814178830eb5b6741cc366620fc0ede53ed92e5a696ffb3451ef9218c15ecccaf604581fc3962f762a54e90717cbdff2ae5f28b67d31f11ce036dfec1f992c0ea9bba4a3105fdd9cae904027e60076c78bd57b0156064947f3d9e46e4dac983dfa6bde803ed5862fd984a118f66d54cde003bdc278e7c1172740d6de2d3b53340ad12d442dd408c684f7a5b5735776017db6db6afc7786c48b2b3aa41f37251513e9eced5d059e7fb0b8298db58858669c57d0ef4d994c694bdd2ba006c2f99ec24a46773bf336c308b348f4484153781f10dbbfecae1c20842d7f65f9b66f1563cbc5c85e2c64e0602a257a95095dd7e31e261d55e4985379ed070d0920ab05a791e94e04aec74e737a38dff4277f9c88d47e607e0fe7e3c9e8526a582206d85d53ea4753c1ed9d05f6974b786f19de131379ccbf28663eafe5af1b6dd70364858e6a22d80f1524fb1354dbd1d33547b41721e601fa5cac0d03fdf14d766d6c4a6c9794cb63677a1c4681f5676161d5d74e370097ffcb95e462a55b854ebc0b407fe7d482074e84f94841574fee8c6d7e5227a739770d88ede8882490d1558b3b66bd5a8e4638f732e022f321e05f642b2b060cb3db8daa6785c8a40e896505e184030743cd3cc1a19c872cc8c265cb978a5fe89d4f443f01623f06aa0a4604ec9e74ec2cd9338a95b03558ca1be0fe4e2624dcddd2284c51cd532b6d9347dc28b5dc801aa1c52b5198920e8cf050946b6a6a58bc248759a0fe98c5e78b63834a36792bd7f91e66657cdc12bce607acff5fc5efbab1d7c579608bca7ba086aa7225adc181fee10c1f152e2ce3e4173e1f65239cbde7904ad54f00f827b70cfc29d30db2f457bb22fab0d595cab404fd003c680f4b3af0f8743978998a76b11a1b8506a903587f89f98ce9e5f78faf88567946d25f40916e6f7f03dc9f8dae10e95d27f2911cb88ccf7b7febc9aa32be1d60ff38b264412b64d257ef424cf0417ef7f1eabb0c20f67df316befc07d14aa6a7016a83bb4ec6fbeccfc781135a17cbac1531328c7e5ef39d5c786874f72d7b1580094012e67cfa71628881397cfaab898b5feab8bf3c50434490d226e16714fd950556c9d838cabc71477219585f5e5eb4a09e7f4fe81328f93ae04893f225cf3d6ce0f9781b0a103fcd2e7cdd7b75d1a9da636d3b6b5308d85fd2d2b3c650651152538a68c869ee3d934ecd03cf3aef67de12e8b3108861d0a6f1a0542c8770b07d02d657dd2617d93532256f55c7af32bdd9ae93595f8f81d8026bfd67f6cd4fdeb736880f86c77973a3837a5181fef4bd199526464438cb662512c82efbe72c3b161e0335a8a3c6dc2c4f4e5182f2b514d483168bd1e8af190359d59b5d1b0ceb078d649993f849599bb62ef8c7f781d8ea976b1bc806fc35c74b834fa470c895091b2c3280fd176edfb0803df2440549e64d03a3d32b76f4f7b6807fc784e8eede026f8a3a30dca352bd651e3ff8001dd07fda5be8d2a5a38bf1ebe2754fba84f0aebe869610d8df12de7823322cf5d29b5d980a6efc1050926af46ce05b7c9cba96c4a0a5c5e56d3baa7b26d689939dfd45a0468ba5e4b370fb351a7952a3abba98e9cc63f946edc0fa2fa52ae23563d4606028c372f7c59a065b95f51720b7c50fee47a3bac15c1c80546049c0a5ca533181b684b4772f029d26eaf9a134f55ff5a142913a8f80a4076f650bfe383b39c32bc9d3d4e5ac303663459292df3dd2fcf96a083fc09d6cf7e77491445e5db38f382ede65bb9a8c23ae93d18a3410f8973b0abd9be657d08a341a892839956a92df8af0b78ea37f3aa60d9b2010b9fddd7fbbe7048b88dcc0ff8e46221605f489ccdf07fffd939b9822edf5ddffe917451c6317fbe91745a095b9df8a341a21602c3b492089f04d56f66f83f5e541d7a5e9dabf1b43adaa7eb18889bffb37c2c3f74f9310c07fefa370078610953384d0d83eab4361a89886e72320be4a138854d0b0e10f791ee489b0bcde4983d219126e382b7985828d4af2fb676ac61b88faa613c4c4c8584b26c89c659b0c3662370d5fbf10ec432b8828d87ef901b3910e62ea41b06648e690ac24e2fffffcd33133f333720681ff66d649f26c41d77f3ea8285b1a3ab1fcc0136c28e8f0e475c7df32dc7014a65dc4a1c8fc0f117ed86d6994f7693e84e26854af79550b86f60ffadf80e7d844da3feb1abefdd541e1c57c88d56a4fa0152b8d69cd166b4f8f2d16ea45843aca5b7e31acf2154f62668e92ee3ca2ae94269d6abeef4957536afc42bfb8694a2a2b3c08b594c71ef568d483ea3d5523b67b628fdd0b39349f63479199c34f0f54b9f2f493ccfa369cb3838df84f8cb796483411d13f27f90fb3bac4961c3a59ec5e3f5f6760421e848be4e8f731694c6addcf3fd242315586b18fbfdb5446fdbadd8c8efe2e266726d4f371a0e6c7af78bc55cba3679da45ad0a053c6f3e8fc74f312d55671687f2ef2637b2da45e513ea3870f90965d71617b091bd18922da48038aeefd9d823a168ecbbdea1194e8d54cc85b18dd3463be011348705732d98bd6ec1a8e4af258cbefd417c88b60c8573dfc9ebc769c97420dc2583dbcfb94f9b493d93f09c667e1963d11a62f25148714bf4038d329a0e8d175fed854d469971aedb3223db12b5d849fa609fb2c3fc35ce46a62acd091cf1019db333ad1b78f8dfa15b3caba6ef9d92c4e6db702017f4282732117b5bafe0d01ffd6e61eeb1582601cfd1b2d538f661ae075ead36c670cf8379c35e01f105721532fddc7644ced15fe5589a354bc736e2ec054863e9cd236ea78ca1a8927d0b7c8b11be34603a97a5138154dfdbd71ff77f35d5e5f85ef3f8d32bdecbafc7f3ce0df70d680ffc14e8a9ec7d2bd41bdfadbfe19985e9da78a4f0a369857a23e95fa1e746f3721fe0f0ff843cd4f29f7dc61e7250ae12e05142dd8da87cbbbad58f3f5c1b603b8bbae33a4e2944f9cd01d68582c16efdcdf4eb40b0d62a52ddf617b7fd1c472c97ee053a7d3fe7ecfc4197b8f48c50d7b570e1126d79d2bd57523a2732976594965ac7ab4a97cf122fd71e3868d2fcc0955db73ce0ef7a36bcd320b7d22ca51350a0e85faf6f08c7419ff017c6e0732e2b18fcaf3ad30232ac34f2818fbbb389f1da37513dea47a60065a2375c1b69d585833fa1273959e4d8cd6e31cba187451e3da872c1d52e93b51139b0bb2152b3d3f12c764dfcbc7ada3bdff98e7337fdf3c06cfb0e72a8ac17f761cf38f82b19f563066c3edec25ade9784dcac843cec759cf53cbd1d145db4bdbebbaaeb7adacb695a60cb7b7aeb5b2a5a390fa75b99f77ffff09c28c027c706b416b1e6b1b016b416b214b7e7e6e7e1b0b412e7e2e214b416e6e211e3e1b5e7e6e0b2b410b4b1b413897b5b5850d1f9c878b9b0bcecbcb6d612974a6e73f6bc118307e146da8eb9fdb4eb80503982ed749a854c875f99c87983e505df3a48e8c8f560b1e94dae49f629ab465de8bcff1765aa14db8d072b19333625b2e2185dab7f7e26dba527715b394535adf5151530487708dc879ccbe511c930462b25428afe14f67f5dff498ff7c56ffd9e7548de3833f1d31e3a45ea81ebefe2fa7f79f25d72b10cdca9ea0f497acaf9f72c97c5cd498f7d1a74bad48fbc815812144cf5c4298e077eb90f3bb03af0b9fcd19843ca89ed5aabf35bb3d31ca6a982c9caf3fa5439e30e02a1a029260012a22123206023a0a2a1ae63ff50350cef877b39e1e8823d0ff5b7175c59aefe2664e73dfc73d61bf14bffe4009627c5182f3940a2fc364c78da39231600a12761f26ed442a345d45444d5ce47664abfc06dbe26fabf18d57135689bc6ddd0a7bee2e66171d5a39146fba17817a55ba406fec5dafb17dd86288724e439d1db778c1804e9d68ce7b360752453e5d5d735983c765bb11e1e14b6df40da15b662db1f4f26ebccf68bba6eb9ea92ae3d3678b9142dd1fd9cbcfa62ac60ea197acdc03282b50073ef1e896d589149c47bcdbef5978a9d4932ee57092f3a9fdeefd2c4cea91f724edab813d730f5ac2e7ac48a79c04b877ae510dfac585ce3a7cb123d2ee1d9178b2fd2242b77ca3f28a53cc41968a7c97673859a67202ee6dccb5aafa6cb578e19d82aba58703e8b2c624996fea895637c8f85af71135c3c8146f2cfde60bfea134ff5f95e6f1a322c710e9a2cc96f05af6d3bac2f7cb44ecbe27ccf1a7a144da47d60c0cc1be311e427e57a50e5916d1a9deec6c630a999e2a79c653d9993ac6a2ebbf0476874bcf1e0ff8cc3327ce33aff644295ebedfca390d6c7d3fd0d110097583969258a818f623d7ffd5fedd3ff1fb50ef07a5830254db1cdaa87fddfda143edf3858cad3a25a48b57af8eb5ffc80345e95b9f3258267b098e66b7ed6f601475a608bcf9fd9f5f346c143ac680b8e4fd333d23f2fef7bd4714bcef76fefeb35e15d625a710faa5fe3a4494014db5f1332e54f77fc428ca1f3f50f6ab834d698709237007b395669ade04520b24fcbcd24c22fdc7bbbcf95045a4368f15f4994c11ce762a85a6ef90f6d134ff3628683fa33ffc1995aa90414b299b3d396a18435d3db148721f2617ef96c2223ea325fed13ff5977f7ebc7fea4722be4803ce4bf96b8424379f0993b4b098833583df7e580a6f566a90d6c59d8523e33a1d7622db185ad67cdd66a2e39e39706b94e7ed4c77d3c3f75f7016cddec807e05f40a55b2f27a477e020a9e55d23d7fc70f13dca4bb3c707aa047bca7081b4e09fd5dfddfde6d31256c4fd8f0b0948c5156fbef88bd7e77bfd31bffff7cd6fd4f34537128994920dd4825d706e50f31075c6feee1462c9231a61ff3478260ed8708ddf3b92c151f6e565476f4dd9f41b81efaf17f5e23f7ccdf942acb2bf95abc3ed617094ef2e39238d2d890ee3641a85186d9ebe1c4b4691d503ba6dc38a9e48ca336d2afffa864754d205ffffefefcbcf7c97f26ddee0956e12d0bc2d987d39b8cc5fb0f2ee1fddf4f949f7c59350c48f92bfed8a48ff7f578cf30d61e087d7fd6131ff0e8b614d08485393bc41915d97133ddc629030c1a4f4bb1cbfb64fbe553aaec6992ce6eedb35659c676f4eb822637a638768eeaec762e924578c5761e72aa0262a3e6c9069931ccdcf58644ab9b525de6d56e85471a92d26262c7daad9639f87b1ca56e8bfa1978a7dfb6f930a3bf427b85ac867db0aa0820ae8807fab26f162ce9cc23c4490c6ec8364702e4c706ff5faafddaa9051feabe1551f9ed4a72ada5c19cacf6acbd398ac1e62b9c2fa8b0d0ff17fb6e121411507a148a96c45fdc8e2f6a3877b04042682b3cc231b2721bddfdf2a1928762e410cc1c3fcde5f38c052953271bc2c171a35ec1bcc2d8f9610b0f422d24a71ae9f204b994652f817dd47502ce24ff3b6263da0ad292c9dabd5af2590aa439236d6295ae894932189b8edf2b35cc9c68fb3a49f926ee667df3f6567175fe91b64f2acfac33efff7d92794fdfdb87302dae78fce231b2721fdef5e1ed3b7f2779f5e9981208cef85ceafb4cfa776dbaef8edd75a1cbc8c7c9138f883094b5dd33d4f6e50ef269105e3282b75e944100ce1aed25bc52e0d2baa3c300aa967343c37ea9928c928d1a5ee1ee5a1fab3ecf363694cfdd50e49ab4aada7a6a825c5d46ffcd70effb0cf3fecf347edf347e7918d93d0f7935bb80e7f57b5017b24f809be85f4f1afb4cf3a0ce1cb6c773d9911e692f22eb2df13c838c6f68853b4ea2c15f12fe72851e2666072353b4dedd0742f8cc8602c462a37deb29ed6805b14f06e3c1271308509519d8dff24d7fc6b0e7d1db96c9b0106cad98c8e0ce2ebe450f704b16933f441e0fd10f800040e413c300c41e0c310f80804fe1a021f85c0c7207008d29f0122938d6112027f03814f41e0d310f80c04fe16029f85c02172ff19e621f07710f802040e91a9c3f01e02ff008143e447332c41e0cb10f80a04fe0902ff0c81af42e06b10f83a04fe050287d8ac183621f02d08fc2b04be0d81ef40e0bb10f81e04be0f811f40e0104e1fc311040eb119339c40e01042463088ed0906e1d0c120e27f3008d20a06b1ffc250217034081c82ac804144b16110895c302c081c1b0287103a83e142e07810383e044e00811342e044103844fc1646028143a444c1ce41e0100e1a0cc2418341649ac228207088f3240cc29f8541948dc22e40e034103804e90ca385c0216a0b61f4103803040eb140c118217026081c42480fc60281431ca8616c1038049d0983505c837140e09c1038448e360c42dc0bc60381f342e07c10383f042e00810b42e01079ee308894649808042e0a818b41e097217088da4e9804040e41e8c0a42070882e703019085c16028728ca80c943e00a10b822040e91320c5386c055207055085c0d0287c881854108eec220c2a8304d085c0b02d786c021c860982e04ae0781eb43e0109560304308dc08023786c0af42e026103844d2200c22931806c16fc12c207088d66b308826a930888ead30086558980d046e0b81db41e0f610b803040ea1c4097382c09d21701708dc15027783c0dd21f06b10388444300ca2ba0f06917400bb0181df84c021f2ef60de10b80f04ee0b81fb41e0fe107800047e0b0287aa14e8e840490238ca3ec98e1db29424a1c55b72223656775a08ca8ca1b27da08246b276a432809f50e68fb91351659a8e86e024cce6af70d971a31fe29c8108155cfb673f90b226ad3d8121f8bd95a1c7d5f7c57f13871dc409c53a7af20f3a986e3b4b1afbdbf0ea88bfad3bcc9e7914a8c8b7f05dfa07bd0235e95f9d5ef743a9787ae19e01cec15bedbff5a02838ad95dd6e37e8fafe78e57f97f1c4f8b28bda8ab4fe5785f97c753eaedf4b04fb46407e6ff0cffaf12bacb51d3fa13535644d92b50d3797858d3017af859505b7a515dc86c78acf869b9787cfca86d7c2d21a4acf1c71b759ad5bf17ecbfe624a0c92488343ed2d6e3e6422f5748b8f183e217c5ee539ad48fb48af23583e63a61a06fead97fecf91c67739210f260f9cf21d8d388161f55a9560857a1bf3f0253232196c2fc687a86443dcfa4f95c687fa64535ae595f7679f242dcd8e5c42cf4f74bb5cf0fb8cd69f870b62fefda74be313fe17697c0ee7fff65ab5afe01f55fa5dc6f3df2e8dbf8f321189583237353f1cf85b0d21ca4bf4557a6097ad112975e217101873de108daa97850dcc5f26003af6acfd83cfb73854bb05a8ebe3b66c47b008451a931727e0cd163e97fb7c8de9774fa97f7994ef7dce6bd1d09e71138260e9907fdfcb40364140e644b8dffdf1ea5f4fba98bfbfa5212322209923dc477fdffa8fe2ba48620888930808f60808149252ffa863fce0026ef9ef6a5f20297646beb569c44b5cb37afefeffb1f7165059656be3f8fbd22d48774a7797484b2329a574b774883448770a8252525252d2d22052d2ddd202d2f15f77eefdbebfc7717cc7616e7cf3bb7b2d5d8bfdbc679f73f679f6d301ae9ad2e094083b4a9ca283768d62d28cc6afda65fade924ad41906c521cf1e62e467509f53db958ce8f486d26a4ca0839ed53f809f46d96cd7d73f8f1ded02a1668260ba3b235e5d679ba17c534a20fc24e36710e95787ef5133a41c34c0a7f9ee5b7e357e3e3613b8fe67183d957c75df8a933c42551f3ef94ccbe52bad1f972e81f27d82f6b7cfe70d7a8ff4d68acee868f60d4edab8cc73901cd781c6f71e070303e3a62a79b775abf2e0533651ef8c637f7ae3dfc4af7f8cc77f0cbf7e1938f1fcf77e7940d2f20f36529f574ba130572bc71ecbc10b3e99f4f8de92df7d40b291fa46d7ef9191effefa6f83de56620fea040ef77fbb7efcbdf6430d1cccf5abe5c0fe8abcb819e1f0f3bb4a0da78f1ebdf0f4f9706f30dfbe3e27bd8b755be75317f487010fd51e8d91f2d1e24a618e56467ea8b0c61f88debfaa96b3a0573d4f78f6acd7f4b1e02ce8336cf3a2f0590824890202fc9f5c2d07960cf49f3dfeef74fd30e150537252b1d6b415355471931237933736e39537b967ee262b272be962e8a47c57cbc8c8ced2549d4fedde9ff7fcd7cc21851894f82754d3e16133e4e035e1e6e2e1e4d4373064e360e534d2e735e23066e3e26535e0643734e133e262e533d237e1e3e0d6e734e463356435e66235363230d1ff1b84fb7a12e535abe9fc09ef6f64a4afcf6eccc3c3cec7ca6668ccc3cac6c6cb66c2cdcbadafcfc5cecdcdadcf63c2cec766c0c9c1c76daccfce6ac463c86d62cc66a0cfcbc66168c4c763fcef7d7f484344da21da33dd4a74e8ce5dc5a00b591f89e4690f3f187631ed393e98f7e09ec81e7192f683f3e19cec0695325aeabbf00b3982924be8b7ddd8d1461df172041a4c7f5fd3b47ff0845f67adbd3312adc852660ec24c7e6fef5e3f76b700e332189210de742cef1d00a687ddfb4e2da1d9a8ae9af897a548da017659f6e095f617e9353f74a388c31de0fd968aafe2fd5509dc5fdfaabd49ffd32ee99c8d1df90c43b0221d9e94efc31f7aec61f713d121af1a88d6a0b634075a7c3d1599404d0bb78cc823ff433b2f1c9245d0cf992e4e4887bcfda1638340bffc5743dafa27e4674108c5809414070d21140f0e121780f07c4810c8000a0457001a04cd1a1d021bbf09e1fdb120bc3fa4505b7c08ef4f08e1fd8901efef21795597685d76222720389dfe76a9e0dee2e8e39f91adbf33209562f5cf22b182102e08acc014d03c3f37a3e06e9ba6ffb94a8c97ae626da5c895f4e8767254cea74ce707039fc2285ed4a252e926f0f63c0e5cdcba7b5499eb054b7d2bf741dde9254ff510ac44630df93695fed9705971c4f44b230450f5d268340d05b3b444d161319551a885f16526f47c4851558d7439f88805df248d8c250fed761c7195387efddc19a5f4fec60079e45813fca76c620e0a196c7ea7bc102fc67a21fa4fe21c9f58c9f7a11da86556715eb42888bda2610e77a9a54c6c253887ba43c1138dbe3b85e2802336ea309d3aeb69ff508b61444386e4e1f11a86ba1b0d1d0e274bdde4a70f708ef457a1e8e6d45497716ba6ea88e96b8a8712d276f80824045aa3ef744b263aa8ee52b6c13ccc87657966af50a9d4561f1aefe82f3f444c2ed0a7aff6f845d204334cae9a1e99e6c250a02f9ade83e013e8de7a44d76984c79725f8e80556c5d8a5976a1d9e696472ea0bd5f7467b29e004ed6c2dd1047e2fcff8bf801f26d4bea9e4305208e1398ffd7da61ef12a693d7f04093f0817d9cf6bec022fa1ada6df0fec190c1f37720fab2418332a9df487cee275ad61d663628b6f6adf8c1c1acf9e8cec4feb8d200523ac987021c3c547a3d4e3a2c0140e1e9fbebc5da6da7f2999f7744eadd72c43b1d3b1ee93cf73e180a53bca14b7149bfc17093171c50ff36eb9facb1d337428703da0452797eec7ab968f6dc8a5686b3ad3bfe4412d9a241f04076917a6b5f68938e9aaae3f2ba5ba597a3ed04133a4597ec9184b24d0551c2e4bb3a645226f2bcb41d36f71a16191aa470fc50a076d1af2b9702e78c4a151ab391f2b6b9fe57dfaeb5b13915f4a51b17dce053da890b0ca83845b2c1f8d1262d2c7a6104f801c85eef1f5f158be1342432d6995277dc874484ee1533610db9f4abac3f938a936cad0077a053caa6fb2a42365b2ff57c28fb9da679c0e5283f0e4b7e8715c5a0a574630b61b21e1c7cd6d8587165d9e7ead6b9adbfc860a9fb6076cc8e10dcc262ab9b0d05f0b201ed0a578c493ed7c7639b5220aed57cea272e5b345a2e6ddf2335d28e71b1d6393051baca3442986a0722fbfc10091b394d5ae348b8fc42a7489ad05a39b5a91a5803b260c723aad19a199c4da25d38daeaef4c25c13db46b28acab443699614ac08ee9ca6ac2f773dc95788759b53b8f82cfe046cf760d8ff4de0809caf43d7d439dd064cdac3cfc85791ac61daa92162ae79bd4c5f2478eb242e67e4a1f2d149ddf01796f53a4bd1426bde3ab33b64402bd433360507df9f1e1df2749bb74f7a2b4ef5a4094e23f6c6ed9d0eb7f88a54c941a377da857a587b462d6f3371486df4b461f96014bb981a9ce4705a1fa2ea3b57662b5095bf5dae21984e3bd049950be6fc2be107ebbdc7391a1fd5473743e97c107d8f0363a1f18620e10789a79d00011b86cf27f5488ed72f67051912f947d53e6f519f98a6dff5248cd3067dc66827c450d29f512a8c485d5b7dabbf4e5e4652d96a3d9fb34e9b6e3589ca2aed4cde54aab95bf365be35f26aa6aa3f1f319ac91a4ba3aadc5b45f6923ae0e6ac92aff98b752a022903cba9532918434df04896d6f3db16a4128c782b0c8c481b5fc4d95d9cdddbc37db9124cae3a0dd23c7cca7a791ee9f98c660b63ef64129f9e91ae35515c7ada0e177f9aa12e39eecba547811a48b80d9e2c4c371771a7e3e1132eb2b7e85fb122e355f9b0d472487d2e20a6faa240e513afd634f9500e7cad06c26ebade43b63e16072ced21f877f0364b3dab464c5e15185e2ac65398e8f46ed41f381f04b9aea6faf30973b1b2f22381bcdf3996e9bbe89c7eca3a67ff2be107733e053572ff3bbb90bdd714e71e9baeb0d4de9f20e10776711e261eb6a1a37f8d96ca3ecdacbbb400c81b1f534454b19e4177431ee50996426c62b47a95aff513c1ad5789507ada2a52ac36ef893ce64b16cdd4dea8bf253a365c621d8332bb47d330532f27c660f4c42a0b663fbd0a6539733ddaf2b0974236d360d5fc06e705128d76c240008afe73a268c48f0c9b4db4e94faa0ba18969421f5b8be51b242777ce1e5736c389a97e71946ba2e2d2132a95f94c8056a74d7140a2784036d52e44e45f157f4f2fd19e3cf5f9868e2e19ee00a2c34e9b1f176dd4d2c80a8990dd6bba87ef5e3cafdb7086967bd8f9ac69790a66f2d65e8b2e139d62d6a03849edab86d9ddacf2ca6478b98f6834d62b2ae9f8989b2b79a74184cc982204f8c3b47ded99e4eefce188e223682f0326615bf934ef2beb98bc3babff2be14707ba934c4cca1375f578f93e6e76c9b8aab36d1848f871c318a1242727ef4ead4c6356d0f8d1cd0cd1474b91129a4163f78d37334071575885d891da1675fbecee3a4a6f18a68e23fd9c927c6e91794a2a2028496f8c13dca80439d20ba12ff79a230daf6faad11da0b4d5d73498ccce44d9279b7f36b26531bddb4b6684e5508ea59280f6d9bb76bd82f12996a4518eb408c3971066cd1534d435d5e3f205139b58bf7e39c1beb548b7bab05abac50a5d1cd11eea9c94038ce316a8befb04eb88a92f14259599ecdecae95d0e1285de19c95a527e90d0484f6981214a97711743f07e8e183f0b0d4962aa2de5d9560c5af50001c8d8e33113eab24e9ff289a0a7526234e9d37055ef606a22061c4c10577d03da2bf82c8757373f9b339fb1eaf23671e8c338c3c83f55472b267e60572541a204439250067afe57c28f19ef4d82a6fec8dcc183e7771b4b77bb452804ee42c20fcad5ac76453c0c47d77b6236fb0dd31d555f22b534b2ecd49f6cde3058357d9a433eb40a126621571c2aa82a634a993261d705631b7eb6814ffc50eca351ab810c0b4e454bd42a7d3635ec882a17300643e46266e2b8277e107ee3f21646de4a3e096bd21d51f545519be33293e70706e246085d6b262fdf5560d3b1a7af15ddcb669157c50f91807d53f721c06786425ae780b28e039b52e74bd4f12993891f4b306e62eefb386ce6429ac65c998f4ce68883c981cb3dd8594e9e1fa08ed7cb96343153ad5983f4110deb67e10d65e3df4bd11d29eebcf7bdb21b1657e28b7d9cf98e96bac6b7a805cfe4d9fd1e1d5ebb12be3c91f24baad431a3ab0db4776c2e7103b4a115d31bd062dcc4ac255a3a756ed9c4b051a11f6492cfb613f36eeba3a2f4fc95f0a36c4abc3fb16d6dc82c987d7de75dbf5be68cb72f24fc207788bb574258472b6f362f335b3cc0c7382f318c4d843a1ebe1d6408dfd0a344324e7a6160387b279451ec7d53681acdab2f62c2a7ec89a85f92242f9e23bca4431862ae407b1969e9aedbd0c38fba8d9f155076ee197554c978652fd0669e4c0cf660a000bfe09b5073667a8f20fa0631cb822d085509fbad91db0c46419ce3310ca81ebfa6c002bd30bc8c5ef6463af668fc94c68349049eb04e3d71ed92f84de6e59ad3cd0d13aec87542bae1d3819190edc5d97a7f395cc13ef069790cab214a46b3a38c2ae1d08d43da3de6e9ec5b35caea0b67b99562020e44af381a5ac3109b82fbb5ef7760cfd8836aa63eb753f90535378f2cb2155dd80af75a61ab3a5247516fb6b20582b3c6d1a11e0c0545285dbd3c9e16352145c9d16db64539e3fd2be147427516c3854cc49c9e95aaa28cb295e370bdb32924fcc08ddfb99b22437c3189fc8ede3d58e5d5fe2cad4a91167394a25fde4726684554f26ace8c87d378c98a0598da75e16d37b02cefc26cbf88faec1b5433f0a458f51981b44d5aca9bbbf1879730d3784a183a6b7eb04a6a2698d1de6be58b47eb70a5c452e68f29420fef8777bf7c4f3adbcb3c199d5adec5f2c5a49a08b1f328dd7897bee3f961de597ab929da7d3c89976feef14ecc180970e0ec26af2c31f1bc6f64a5a1d824f6c2a24753b325a87dda3b99faa4e0536f8c4e1322ce382bf3dad4142649bb02b62ebe52dbde1d6e560a13ff94718f75df784106e2056dac0783a3393ce43cbe11a2541837892a57b395734f3943c651957b87fb97caf54e3904b6d5dfe23331dadf379ed5f2d84c1e894112bc148a419e745de69be550eac50cd313f1fb2be1872cfce5b8fffee84b3d7ee1923d0f18fdbc48317748f841aff6402e92c21b4ebeeedee0eec12835cd21bbb438e7860d46767d989d03b5e70d92a0d586febeb533cc630aefeeec5b2cf1b73c340448e01c1e3983060dcb1e13e5d2336987dd7960d32d13eedf9c97782cc79f637e7f95354de881b24debdd01f84964f62ac907161598b3ca7e9f8ea4ca428ae1bc856a753e95e73319edc92e42236815d2e759e23aaa6d048bada336643462ab0dedbe36d417be49e3252cc185e377b17424f5cacf4fd4c87701bcb650ed624420bea5daac3fb486e5b1d04584dc46e141db70a26a9b2cd65de327b23f59dc0c23b0374988594572d427d795449c12514f5b566bf3e0b98005ebcd7dda9a719ffe5a6909f8f12fa69e01c9b9a137aa187af1fa5db0e0c98cd1cef59b3de54a1e08e2b5d067d48867a56912c06cfc95f083f34e2a5f5b338bef5d23049ae3466fa4638df45a48f841eae9184221ff417878de847433195b84eea810cc1496e1d732bd72c2f994be852e9ac5d9fca30ace613ada5213954587dc176e8991715d29ca212e04b2cdee20baa01392aded52e2ec948f631cd526d9dafab788cfeae50330b473389e23376cddaa1d49ae9020ce52a62bf2e14b2ea9e3f719d034e34d511720f914e83ed5af826aba846ecfd853907dec35e66fc4e3e390397682aec87ae6b98a73274c523b0e26a3ecfe8ae462a66e7fb095bf40b4acf8918bf574619af91df6794581be2b2198ea490c22057b95698bf4a536186d23d387d13d15e4740b3dce96f017534fb450e27ce8abcb714d85bb71b396457b97d7406c7d6bc68aebd98cbe9f309f9e53435df68fe0f62f267af7b99262b84b5a0469a9395b513aab8fa03dc7e1cf5b5cf82be107c267b47b175763f74751c63a5e3c530be41a40af81841fa09cc5cff4e6e79df0871c92305bcd709de111582288eb5759275098cfab999eddd0cba42bc40ef624b487cba5f273886de3ddb6c91cbd8b95fbac95c362bd630ef570f10cbdb59d23220a530b639fa20ba141cf041b943d9560f7a04702edb100c845a2a24acf3d590ff52afee5c891d6aefcc3bcfe47bd9574b6e4af2a39c4d6d0ee96390f30f042b90e49c5df785126056fb48bb463ed5c25424b9b9a5082b047e48c41aec85071c02f93873b3131f3fcd9a3fbc3d92df1f593ebcf44d8fc4cd9c9e8dddb06edfb6b5ddfac265144553caaeda35d2dad02991fc13de3f36d9d0b01f5c4d14b04d8104847a7d33edb0e797d41759be3390705a7ce80adb818477d3e7ccd7c6c8209946e9f3b27db36fad9de8c379642835ca62adbf041d7d36a89febf127e88b6bc9ef55da5190d1d5c49a042da19e1a9687081483f6a4eef6922be3b165735df1ac89994c9d9b8b9402dcf0e93ccc41926cb80fb1c6734748c5ab32fbddb37c0e948faf69bfce228530f6c02354fd181264ad82b66b0ed15f7c733966cab5cde835cfede8f863829bb1bf6830b2739f8dbb19acf626e9ddb7b533c6d7da5e248246d84fe1e4af22e88e26390f158fa2b65dffdedf65b4878add9628f72eaf4dea7f15a476db9996248cdd56438ef8dbbb06fc7f44255d1a126310c521f4c153a0823f3d9ce8ed0a4afc151f5e72f37d33ad98927691533e52b650aec276ad4782970183deacce110b22bf7bb9f844709ea4c1920dc7fc650fb4a437f2209230feaee4caada38afe27ea0bd7ba67824dcea1e11b7f82111a63b2d8de4e6db3efd3b0387cea36f32656b967746eee9448f3adfa089cb1671f92be1c7d81adc5dff6caade59a6ad8e314c8b4125a3c40988f6d3be0039262fe6cb958c1eb2f86c75d27555bcfb3e8fcb9d761f302f7b7461bc60b13b6158ddb6b32eeb748c2c94f01ad72e43cbdacc9e687e467c09f379d7ce8a2059447a6557540d6b71a7b7ae3455523bd7c1f5f2264884c29246a5fe7eda0434ec880c2ff4bd2bb14d8cae37a8f8956e5381760f3dcc8fd19a887a7ac5b9986160b46f3db12caec1e6df0a650f7c25eba9a08bedef1030c111e3385deb70ec82ef81371a4e9518853fe87ef954250c8f6cd09c9f3406fe3c69b8523b687d31f8265fd75eb7de7d3798d4c0f7d9cec5dbd83dc5ac891cbbae58d37d913b634b2cb7ec61710befe88f085d58a534a35e858fd2b0d15d48d114198f67e5e738db0f22bb98def09a7c269dac290d6610a2bc8ac8ab22e3ee90d76e166c0a693c27fba1fed2740265e5ed473f5b5e0365145dbc0da1b40e8850de7c133f25739e4e4126b4c6dc8b2e52513d7668c958d1239d29810adde7d6cddb1ff679f953ac5e6e742b71b850eb22b8124c127167773776ae922e56d3cddffe197c8b61a042e63021331404a1f3a9b4c412dd61994cb95e580e581faa8eb0dc5d997992281f9ebfa57ca6f6fdc3a763c4d78c85814948f6ccf9efba7f5f17c26f4ec09bde7ec46eb76bc04bcc3c3ed8d78bc202e625dc649ba9fce2987ed8672ef6c229736855ca5601d033e4634ee43b22a330ab33e164bce733e4dd74982f7faa84b8ef6b69f1c721f08bf5f1f5a8eafbca22ca776a01f717fd99f813ec7b4753b93fb819b6622a341689f8cfbcffafa3338e7e58c9e7f7e42540384e10f3567e095b72e18a030540a9222f7d276c294a504ced8e12cfbbbccc5ae48e88d5f312d2edc81fdd915ef9b67013d4676f7f4d331690bfa6cf9e6ad65afeb1102bcc0a5191ff7973c7ca05636407fe33c5b20bc68f1bf0cc023969352d4ce3d54dc772defec4ef8dbff304c76cf50a6fcc35e58c04329e8343defb885457feb08001f2275774a81398216f7f1c2de4267f4d27f4fc1a98d6c2847b63d78c85ba664827a44e6e7fc6810376538374e0f8e40345c4914b39847a9125fc1bb9d2a2bbc677fe6d072e7a20a62bfc0795aba27b1e27618024af79e00effdd07ee1a6bffcbf1e3bf04f93f0b3f9aa086831ed8bed94a6c02b6c61c0e4a13d3e899eafc76564cf895133fe8db592b137a8702946f6705ce072c11ee7d3b6b68f1f1022eeedb59919a9c350de16f678d260aa12361be9d95543e629d3eff76d622c9e622b5f7db59b59bc4525e46dfcefa160949b3eb7c3b5be65c5b64f9ab2733664cc4f834f7ed2c9f0e25cfabb55fed0ea9c62c4ed6b7b312e207d65b44dfce96a68f5fee547c3bcbcf36d728e0f9edacada4a55fc5e6b7b3d91cdedb68bfda3395b70dfed0bf5ad788d671dc82f7577743ef2d9397fa76566efd86de47aa5ffd56ac3242e9575fdeadc7b8aedceadbd9db2b43f14ffcbf9d95c53f8c2853fd76d6faa06862260a307b022d1004461f67b36902f991ec0fd74033fe0e4cfee765b78091e00e933a01536f2beb6f609cbfe81dfcbc1642344b8913fc74a0e66796c488ff607c2cc07fdf6cbf356128436b4484d1a8ea871720f2e45f536dffe130b1c4cdfc213c262b13d21adc28360ddf9ba735baafc7199a140802813ca02841d1b95b604ad0a3239005485e2f6f78955833d507e61ff5f720c1aff1fed7cdaefad1fa7083fc1fbf15335e58829a776741ac203e100d1608749a8b0f89cdd66218e0e71b1844936b596b624181692d14f11fca9af81edaf047d5f1a85aa2c23f5dedd956383f346a40ec5e6f5f886145475537c8164c662437244f94bd79f53fdfe097240a12ff5ffefd3a89021283fe764077de4c69823a8119f87be5009226a31a985649d68c31691922a20766cf9ebe55193022d5636c95899558505012610900e9ed106ced1a5c536b07fc257da9a45df8be7f6943890f1e3decdd78c1dac68f0bce08c14028480786d443f8c637e9e53f7ba2e8595d597fe7f86344e967c8e16fe663fe4e38a454f1b812847cd0fd6798f0253b8cd229e32161cb113f2e182524eb0be1f9e7ae79dc117e365dfebaef0869fd9f3d7810f708228e434421482cf3f50f542710c810947156f51ece3b2d93f410f95ceb0bcd97c8111cb7778549fdc2e35f7fa7ef6decf736f3b7538c7ff7af7f7508e306b07e5bf23d01bb06810d1fbc0e41c0fbdf4464b05150776c1fa4b395b66830cef4ea828f8342f20c4a88f3756887f76abfdfd38fb7674fa8fb9496fd01deb167f1af71decf04b7ec144e170bb0d89ec6b64328460cb1ac0aa44444783b3363373773f33f8a18221588aa47ae6b65abf97aa558f6ad8c8ad63e64663434375bc9a9731aca0b3ec2688c3df0c993346f306ba94b2035af7c2c7224ca0d0dadf42eb7a4c2d614844bf63bda7ec16a0421308cb89f35fd3d099c7961a60656fc6f7ca5fb6db5c4e68b20211194bb65ce6545d4bb6e2202950db1393c62e677f56e068ffe997ce5bdd1a3426d22d89b57be87481e773223882d4f7e5cd9c0fd5df37ff9ca9fc757200d48341930fe814eff64be12fc9f5686e5a7f8caefd92388387e5dbe02feed62ae60121068f787852f7aff706115902dfd416f930673ca4f155681a017b8df1afaa58ec65dd05dfab7d4aa388abc0133bd66af12f54fb344be5797e4cfa9a3f163be36140cbdd0e2cf4704fadffa1ade4d50c7f783305d720c18575dff87de568b5b1aca4010d3097f5b8cbb39ac6cae280cfa8195fd16139c9c00eeaa2a52e216eab8163ccffab9e60fcbb8821b99d3a106832803f76c9b41203f72d5223fceb7a9de4dc7038198cfd85e39b8fa91d7a255b71a2d1542a856ed947e4d93a63a08504fde43715e343cb3492776cad2f24d4e08216274c83f583c781f04fce95bbcfcd4fda5f5273d4a55afd6f2cc5752f6d3ff51ba1ffaeb9841a813f8726fbf1be6f7feee23a981cf52e1238650831892e31d2836746fc2a598dc610a6fc0baa79243e986e47cb9867dbd630db7f237da08830a4544bff56c6b66753d1f8e36f196e5393b3c02151a3fa7ab7e66d7e45460fbbd38c582d33bd51ef12d5e6713d074b62cbb715c2e2bb2c4da97b40bfd856314bc6d20184effd1c3a1e8add7563c77244e6f6e1f8f6dd4dc6743161666a864afe040a3a676d7767aebf95a2df6b6d906718d034df3ca63845d9b63a1b6f055ae7dfeea534c102c65bfcd5d59e116689a94a45d57d00294ea291f6d5dc492a81dc7abd921bf9e468364b4015dccfd0cd6cd195d477b0c799d77b0ae0a4fd1ef3e2a2f63f24e7db10382f3779fe776313af0367b56a0b9c8daa73f6c2199edb82cdc4e0a9652c6e9d83c707c9c877444c4a94ba8933ad865607ca8b3479ab58f30f1704dd32cb51f4e461604ff78bef5d55b2a2e9c3e49f1d984267df70af2d0a1572a8d779d384b41e8c46facd5d6d31f0ace9f832bb92e422492036f4a33b6a02badbd871661a3cda7d205178110361c76535a3184b66ede0a2ecdc9f58f7f1864764b5954f96327bb3abfb751b8377afe387accddab641eb95976287904b587873a39ccb04df96e6de13113a3462a20a476f6c92ea7a54de2bc5d93007cd675c9826cbb42e4ae4e8bc9f186247d22da7786cd8773b3d0435821aedb45515070ecb9e11cfd462565af33b4a2eeacc9baaa8390930636f254b9df60775106a0350f8bef0fe33d7a8f38739e4b375a00be710e370a26d97a1a8c535e7b0ae7ea1910383d38f9a169af17c544be58bea99bd55c110d845a855c19eb90cdd2b3db934054ae393cfe66342082f620e8d2773c8f42efe5c488c4e4736205c2b51cf1a880094b37ba30359a31c33e964e865ed7aa4a2d67f76d30da9336987de417de5a67a376abca3651ce32855e98074bc3dd8fe3022753f05b364c863ef6d9acee29f67c695e5bba8d35ce54c6143dbcf4f88be8f495ab378dff00e84647a7207c7421eae1f8a4017bf187aadc3b3ab8116db6d19c12c7cd1d54d36830ec55bb662a66e5852337fa9778f63ee0a92e9c6e9ac87354f8c51c1f66e188a081d1f57a2e4aacd6c238e03c87850f0fda89b78871c41b59ed795e72baa1b3b5d3aac3fa9acf1de9146d85e0891fcfb1cd722f3556ec2f08b53a4af7e43f4b4cdd5c076120bd9497412ed2184d5cd347534c0f19583bb593672d8bbeed65cec789c1cedb4d954029762fbb5647a72b19c710bd935a91fae80c7e82bd6b5a8eb58ff5ad5726c8c48aeaef892b806e05823aaadc75d951a23d9d5c34364aa2413edcd6ff101030c82099c5f0c067f4d0ebd5eea39c4628dcadec8249d116210f7af210e1b115a914a88c6ec54c60fd0c09a0ac63f5248bc3308252a792d09e7ec1e0246e56f63b880eb4ff2fed99fefe7a3f7543b8e137b4a949f81c8b329f1170f795e1088b0c55feb50b804020ba159086e15b669cc04c4f99b98147dcdfd1aba0d4bd0eb4d3e2841686c0ca5c3f68ac8a51c7cf2a52f7db858e309a39a8c4c67e6e8fbe27b5c00b52908044c09cbfefe97f8a3742acf989784de9f19a767ec0f3379afa061799e02fb5cb04849ae49ed6bd762972bd8eb3ecff32db6ea0e4252a9c72d03948a219b03050a11f76afcdc8c64ee7e3d88f214cd5814595ec99e8a24855bd6dd4b28cf131d437999cae0de54b070eed437e7cc79cd3c938108c5ba7fd3b6b2773c45474196e69eceae247d3a394f8b38818e0028981f0e32d21ec9e5a39a2b44121d1cfd3abd8fe9ef66482a1f657661fd45e2b129e3c2f25ba00c126dec184e6f1f8102bcafbf13c5e65b490f999918e6442abbccf67b226891a70e7a32795b7129e0af285c131100f85cbe891203c87d919d3a9786574a0accd1bad04866bce923a422ae332b5b36074730b33df3d1c04176c20a3a5ca48f84b719bc75560f15e6c4998a59a4c8e33a3f277f432bb1943c3d45b7e0898e879269e15abf41804cfe91adca1df651d45b069ba16f678727d2df36ddad156febd0d5954255dc24a3df9074d2894bd42172e55238f756dfc4f39a59c1e7e0c58e12a61a636c623c40f0021cc8bcdb78dde449e1b8e1ab18b31ac96328a75d3e620aef37538a76ff3185dc67608415c41abc17fdea6a24b16f33967275a6c42ddf408a92a6fdad16aa04e49008464fd8522bb518143ee297e5ec1fcdd2fedafef29d132e56c3c216a9ebc14d8c3a3558bdf0e319f4d12bedda3eb91d21dbb5347d382a448629ab1e2bf346beca2c60942e654eb2cef6e80eede290abe9d839661b5b1e374a1e1c4d223f1f1c5c392e0e44098b1071b1c1f8aa5d86e66f578b5bbe879603de8942856c1f6b36564f384695000a1a219cfa39657932556b8cb43998fdcb9c79ba5d2eeaa0c8f4a882fa361faae2505bc7d7f072da0db210dffd18944f8a853d37aa4c8bcc6c070444d2c53d9e0be33186d2a60923caefdf40558e56e35b3049a90538d3c7e28f1ebedd3dabb92048c95e9a258920963bea04fedabab332d8356a63e8f132db3f742c18450e651f9d3da5ce01b3caa1335f8b9fe9cc5881e212366dd99ee1fbc56447a5ec74eca1c66d31295e598dc14ee004beda9ef3eeaf2f4e37f81dcb53412e6dd30ecf62e579a5916e79d34087d8eacfbbe78c396ae16e5fb19b9f7e9dc5ab72e0e387c13a09f5a33753d3e60b8af267811748161b7d8154178c7d44b80bd43ba0ef44891d3ee2e3d2c6c6878603708e3c5a23c743de33b5fe3c68eb2a9888e888763b855037a6ea2024f1853dae03b4eddd20f3c3ae513239c15046ec0e5cd778de9ab1e9fb4d8d84a41e5534e067de6039b585165fe14db2687ccb60936f0dc1aa2eb87bb88e8720fa39ecffaee0a4fd0d64ef0b6b7bf3c8f01cf35fc6bd976e65f946d0b743b7fe427d90e32e75e9eb6fded3858709b4d64ef4efa6f065d83db61c705913cff14b68dff5fb60d0209392bd0d28612addba23af75a4b0471dc40987b741db63d1804359ae43708fa7b897062eb62ef26a813288cbfbb34e960e36aa060284ab3b0aef98aded7bc1e92ed92ecc07fabbd0291a029f3339a0a26c211fb6984bba5bdb6d20786d15078765531e56bba7b7e0c4792043c1f6bac0b475ebb8c8e5975ca32090115811e0b47e175f6c7c48af70924b784e05bb949ff8fccaa1ee71b7c0b0b79013d911206c921f21bd1c99afda26d72bce09bef07d25e711d0b9a3159f273eca698afeba56ddf2af7f75d70975ebd6a65f99df507d7f6fd284cc7abc5316012006d557bb8c57a7de3cad8ec696825eedede947648937d09e99337419dc0f27c5bed1696eafad56e617e6597248b32c58b409114497f3767207a8836403878fca2a0702bb9f779676dd79b3cf2fc63caab029b2dd6c0a73b62e26ecd3f08cbfc75b55bf1fa0722744b6c682f3bc4a2ef5f72c74addcd81d011f4df5ded1606f41f3efeef54bb55b7d132523057d2e776b2951451313035b6b43530b5e5b067b3d370e1b6577134d797b079a4a6a56faca4e9f0e73dff9fca50fe00b515657535e1e6d467d5d7d737e4d6e7e0e3d3e7e064376433e436e2e26433e632d2376033303131e6e2e0e3d5e733e0333036e0d5e734e26167e536e433613761e531e4bb9ede76cd6aae80fdc3fb642283281f8e295b5ebb2682cfaa75c91a01c1c98db204891a335a5d6868b2cf2aec559ce23e2d28ea9314b098c70867fb1824107880d193df0167661b4c5c19dcd35bd75fa5936ce2cf257d7852bf4e53a46ac48ce2c04a6afcfbabc1fee2acf8d6bb90cb5929568ff7195f4197a03f12cd284621d9e7474dd4f1eb8aeee037411d6b04838ba104e5ec63157ec60f42fe9bfd75c1852383506549314de01318a720e8b0568ba43c90ffb6252da806c6e89d862a84c643a0df2673ff58ab3803d5e4d31dbcec7832275215c92b9388e20c836df48754b4e9afca677b3321305d18c03130e69742590a768ff2e1736f70e93ea420bcadf8038f0c380a2720dc0f1283fba31b07914d409f4d3d9a8483d0381a84f487d9cc3ff617c2f5ded7634390a83c340432050dc17a040d49e8c4d2887079b9803dadf538aa9372a99a5709441534f3136404eb9a64fcdad75ff7f9ffcdd7ffd34713d4f1a0b73fec9779903f1cdc13a2ead60cd8c4df222a48fb12ee1a92d9aedf431a3a370bf152d390e5e49b37329e415054457fe601cf49d913242253e969326a8847cd8884c9e6d2607fac0930d034411deb7afb838541207f785610a85ac9c48a3e038af559fa8def3d0dd2adda879fbfa74f7380407f0391e8d130167d78e344e56e628531b98e6590fca2b5e904aad7db8f25b8be06aaf566be2020d40d52c805a41d8194840269fd3b9cec18bb525e15fc7b73ab0eb56032ec15e3dfea9fcd858ce2b6cbf1032200bff82cf165511a24690882b8fcafed66f59df57fdc3ffd00d8b987e107ed5dda4120c3dfd62dc0b827184c258dc6bfc4747c0f71bff7e97f89d2d82e5375a98cac7e384e77c49f00ebfbdbb780ab5a8ea9b999fefa975b7c0f337fef2d803f39810af3f6232f65ac81f22f158391bda6f07c5de11bd04e53dea1a88ae1dd34f75393adfedd322c7f70a6ebc21f968a7e21105e03101e1f92cfc5df4e0c6719c26faac2311d7cad916b42898b13d4c755ea5d96e1ee919dc4685ab35e9d90e0c51a407a07a46b5effa3e19b1c551769808b0cab812d7d01ed71b4fb66ce96725968ff6ac7db8e7035aa8d0c2edc506e388c8820abaa31c01e91fa026d60615160a260d6fcfdab6ce27a4c0c42988661c32d9d02927e537bbac4e3832b17fbde013d7082bd7ce240281d06befb7d3d9dd8303bcefbae3b6fc11ce1117592e5b86df801b0f346dcf75b589d292a2134ec3f01cb79fb118951d680c577ac34137e4c7b8200b933d5fc92e26ea05d438ffc92c8d3887ecebbf3c8bb3fba9c9e77eae467d64fe22f923f461aeb300df57add42be2f4466b1fee49abaa288fb422e82f35955f797b92c2cfb77fa7ee2e0c8410e0aca06e69a96cf9fb040b6a817a857fd9edd25eff88d95490baa47792a17a28ed0499239aa8830d416c21820ec26e804a5d7db8f0833bb06a5559cf424e99aa713d04c3889f411cdd4dde1884fbda20d6c9f0a72380b1d00cd64fd071702b93e1ad3bfcadb6b31e8242f7f8c6d02a8eeb1b9f718db781586e7cefbfc7aab9b18746942a2e35fc3b761683e2867c4944af984c256b7047ac13972547c0d978622f19817b83d89b23670a1a73cb46c74da04503d87838f24eabbe5ea45bcb5bdd07d350599073300cd9e090e939fa532b0fbb32bccf66527f0ab6f39de9afe1ade1c894a0b2b8ed56398bed0d82b14aff3519402e0022673ac5d7dbd925a14695f5f2250f286bd098c70f1355c693ab632528917978cdecb87bd1ae9eee39a5480fcd7778ee6bc383b43ea41303579295f165be4bb09106f583b2cb14e5349f469620db9a1b89e411f333502d015f3de87bd10eb8c5ac9d1cb25fae215499144956180d5b1b5087e50c1d005aecda1332fcc7422669411906665c87ef786dbd5f623cf804039aa423fc16c1f6e8cafe1a4bd37f6263925a0c279daf947de2eb34c13321c7c0d2f1d18c679394800cb935aae10dcdbd2c60b4d0568223b115fe42c85bf932eaeccea6febc914f8e00b3c40278c3eacb746101fd06af3a9dc1c374cb24bc75560fe1a3eb2803da539e080a2937c598ee44b19780b990a200cb5be8c4274a07635dde8d8da32b6ab25f4aacd01a44a459ddcc4210a271432ebea617bd3f1f043ccbd11c0f5d675b9e9b954f20c307d49e2376f348f99aebd01e0771f695c6b0a81634c44f4de86488a4dc2016129a099f20ed634cba6ecfb99356ff5c691a5a1270bbc2f0019dd7ca13b305ec5048eacc51baa28bc3a863056ee809e4e038fce2baafa2dfac3d8754f89357a92e80e9c10be86f31b072ff59c77f5c72a1f2b878dd49ba2e67600aa5beb48b3a4303f4f085e29a4ad79f1b6617f47f31380dc51327daa786bd3d3565c1a8cfb8ac7b81723301920ac929fbefc8c364cbbc78ab88e31be4634bcddea0ab093f4ae74bdc9f554898bf06e57272b773f303fb9c9f2359c265728ced47f387345363246281149c752262d1ee0231b17d8c5630d73f7171ed77885e55ba7dc985600d83fba6ee3e680e909a3ba868a8bb9271b576f0301155fd8f76c5e1b8e05d955c736df3c860b161a261103644483c94f44a12e44ee59e4bc103842c689a53a5003a47b5e4980fcdeb80978c6156016f0fb136770e90de07d0d0ffd52bd5520065bba333a44da7458eda195d404a0a6af35daf11cb7b1d982688c89e36898e058a56982016cda93f4836dba93da92b2323ae6e2be4a879614a03a7dad7bd75988d42d26fdcca92e9212e69edc6d4580b0eec0e11b332e1f7129158773e688bdc6ca3ef708507dd8c5c56527145a6be5a234a3a4ce57516240803ceb6bb84755f7b1a8bbb59777a67bf9ce91aa326cb80420231ccef0030d9399ee39b31ea5cf8bec8806ac94168030ce20f011be40feb31b7651dc1491fd20f1b437c13c40947a09fea42fe7a9fb7eb584288788dc4d40520e60fa2f1888273f8142ab9608bb234c7187ee51d733b6b4afe12d9f54baeeb789244c660addaab4e13b3c31abe6079cffbb8d145b06d38b7aabfd83521ebbb15e4cd300fadcb3fac11b0b191ec54059e8f61a3747daf2e436c0f4b8198bb6301ab0fb186426c15fb7392064dd8020f435bce4e64ca62374834a3e1151b93fb96c007bb23ca099fce5676a5ab7ca257d21c6d7e086e786f6b4e45580f87cf5c14c32eaf564d655e6a82adcf51739ccd8965c5fc3c3acf35d8394f2a92768e7832c6a5d88e0425875bf8623b66d703814e327db1b7e7ac1811702bb7fa80168238a65612980bed963f008fce9e100d74b663a94acd6afe19eeb14ca76488cecbc92a7c63e5addf68c29fb805ce2ac40a1ec24595f02b65bc11d63efa8ddee9556019ab57faa7747aba549d1e61e3f78ced439d3bd9ca547ff353ce78d684bf37b930c4ea2b12b33d45469e79a771fbe8663b496d2421d75aa581763704e0716116e3465d202becf7d97b031ba393cbab6fae045eb75d8077b4300fef3e2e14b4fee23aaa5a6edc783d857c4acb199ed00a3ddfba8f68513e30edb2456a71ed4de4ea2bd0a5df4afe1b74f7761b051db79184ab18512915cde9b1e5800c4c19507e6bb8ca8a55e67645507d84603efa7dc05160162b89f6a9807b77dced01b215e8cb387c154c8f700f47b90c91e6e79293da7f1f4839420d9eba0e3cb2180d1fc7d22f5992dd1e3320672ed09fa09fc2ae66a22409bd965d5b7538bdec3261a05be053b6e0a791a68c200faf9bcc59373b6309fda4525e081610fec856da97ecbd7702245034c41b462a6bbf8a4976f797130fd0af8009db8432469dd9ee4f96e60461c68bc4fefe5e4c4c634fd1a2e95ff76f269f99534986023dc7e0dfae5dc0e1d2047a515ff893c47ed311fa3532e0697216cdfe3e031406bf05b9b799fe81f05dec859d09864aca78ba1f54b0528f3f03b3e04e307b601849735f579e105f9ae69d000fef9001ccfd086c37d9c3d3ede482047bbb3446102c0af8fbcdcbd20bfbac666d945cab1ab2b7ab34b94a75fc3bd1019de66a5e9ddfe02b26e177eaa2f1246ba41f5357c03e1bdbb42f2b02f91572b91e0383c0ab6e9a7b5afe1f9ca32d4f3fd834f2f959f9ec7bbdd11f24f4800dc7f7f2de8d64b76e6bafcddd738b329f0b4882e6f3c01aad0e60b2e610bdcd1e0dd0d5ee5f873232ebb02807c464d441ddc3008a6bdeab08c47db7d4e0fe265035414cb407db736564bd69c99bb4007037d0f86262f00909f673af4bafebe307a65a6d507918490d087f7df8800e447d699f99672c68388298204ef453fca78532926c0f951ddaadffa02e7a7e03333bc758be458b7d4cd67f26bb80553eafabd8dc627359483aa49b84c3552a4bb80fc1d777b141b33f86c9a7ca5d7fdc244c4124e1e9700f9d81fa1c6c0e2d01a995dff4bf979ca9be4a70c126c5fc3838c737143c8e89bc37566268b1c4d9e9f65f601de7f46d9f8038f5b41a74a758e8d826053178b3929407ea952fc4045001fcb7f16f87650e94e128b7c723580ff833be1e9232e02e80236179d9f045a4eb82e9e018c35ebb3cfc3391ad84967718d5c43da33b35d5fe8887d1b906178e5fd8db7a73e7019c1c1d1d448df96d9d8d10cb2421c5f92d05bf9514567fbc0ae5b64aa8145bf9d7dba09ea047a2c083adc2babddfbff77be43b772c5aacb41584eef7af609864848e6366410dc753cd490ae85648e9bfbc35e99dfe51581e87cfded1bfce3735dcf3cf5c7d7c76d978c41145463bdcef7ff1d83e1bb2662f0c1756deddfc55f488bdef6db648715294e68ed22898132e2b593882c8b82b03efe9f79ff9ff9c0fff37d9ababdfd6eaca0f85fbcc9103a8e09827208a75a2603f9659bc455fb8bc97f0048e875b7d3883ed5377186ef9e385798f43810e32b2c5df3fc431c7f37d820fd330c3625b52621f7eb66d0039da85d172adf3424718357002e015139f64918eb2b568fac344cef01d970850700fb95ac84c367aba638afe439cd0787b83579baa66480d0c705e21722abf78a9931cd9a62ae9882a04decd8a4be86279f279de4c6e0517f282b53daf799697d383c0d601884304ef06a0d288df21a036c3c9d85046d3b66005b35950b8112ef62689f6ede719c10e2c6272caeb9178097cd75d124eff35e0f0ce0b588f7d6fcc8d99a060881cf2450eca9c1910e7d8b34691c62e3a016504103a862a96d2e7d83ae4fccdd9c2a9732ed1e763e161c3f20a1d5003bb74913d1c9b938e58db031e5c0736e264d40dbd7dbdcbe8cf45757fe0f789ce1e28fb976845a9301f4e211fe148b2b386eee3d19d25b85ed69b51cb03540a035386da64f686b3779216e46674ff448925b79ef13607f029e05c8c0b06cf471622ece13c2d293d8b90014ded26d663b99dee957af1e3de9e411f4339c3c1a0108d45bcf5896ad978a37963ebdae5ed15d7877a62a0688c5caa598717e96cd9cc7e3d44b89ca54a8e3753fd4fa6b38f71399a85e49d1054994bae759635d75222398808c9527cfc91fdd1de6fac056e614719f95205b72b90f60507af8099410a314aa452b12e34a92534ee62bd30e70d26f6f2a2db5dd4c58e5b3929621b3788dd522ac072854f174afc928737efb21cf2c5cd279fcc1edfc17590085f2454cf1a970f0fd14a2fc5426faaa8aadeea1e7625fc3e5be70fbbb7229887766ddb54feeb3e08e0de706186cde51713a54e68034f767c357cb34e16a8b3e1cb6010c8a7294ea54692661ab981bb73f3e75bab39c096df835bced027751365460cabcbf36a83a65e7f9cbe03e80c2d64f91b883630fd71aed365ce71e7efacc98ba8314602c2674bdd5274e24bcd7b23594e2a4902c9f550ad8bfb8c5c5e7563d20b1bd172c96b8028ec83388de0085192dbb392c8b60a5135e08ff84e4758d44d93dbc5900ef1f592c49f8207d7e8581cb4367f64abeb4200f2070591cc880a753cd43242373087993c02db20a02007749bdf969cfada9d7e42c14219b4f6ccecf0dc46c8c80b4abcdc5565e366d2e915c1f21da98ed4e2a27a09cf0d30f463c82e88c67f0f1569f7d2ed3d57288c100fc21c2a233303aeb78ea44e0f5e11c5de2e36b2f1880abe5f3b19b5368cb6851305c1d8f83619ebf55fe342014a8b31f0126b49b05b72b811cb98295d857cb6601105666682f775b5e4a73ac251f6c60daca3e45c78d0348a84fd86674f2c3e143fe64481c28889ea6b21dc504383f27fa2ff036d0ce2dfdb5e0ddc9a86fad73953c049c8f68777dd864493a37ccb8f50e623c149b291378c0f92420c0f17d69ea154b7c66309ee7df46bfebbcfe06c06ee83f867c2148378d150ef380c72b9dd293850108f4f3394e2cf76d3919260ed89ac7c7af4a38cf4201250623bde45de43bed1c0c9624ce4e3a59b1b7a6dd0106c32f9c06176b280b7a5c9a399a0c996f271aa13b01edd01e5387dee5ed4632d31fc98016870e50ea731d019c9fd72eb07d8f723a7797b0f7422222ee7c16355805ec5f4e9f2bd30a3cb9b86b13c7bda56796ca2c8b66315fc3f72c51663f1ef12ca2f1c4103063a19fe78b3d54fa1a1ef821e396e0c48547e7c73de73a7664b95bd3457b00fcc5bc359795b4765f08ab9836367202ef54680f40df114c6db78e9c4789db53197474e71b287c791b010649f9a1c257dbbe7507d3066fa2e4a659ce3e3c8ac404c80e777245e169ba5b2d83cdb391c5a0db555088003d9135c0734aa50a29848e71a67131a5c3f53c708e80da6bb7e9e5973fe131ddf2ec0aa5bef3ce46d06c5215204e84ebfb4331cfbdcf0973c2b9596f76e9c65d5605e02f549f1ddd4b76f8a52ca1973265f5de90f0a9a9018260c409692edc822c713353f260aeb637b725264801f451c6e7f6495c09ef79238fe0a9031aaf53797833c0fbffb1dff2e1b0a0d0cd9198e1d0ca4f5fb0d51f3e53ff1aee4cfd31c43ce5e0ca4dcc495a019bfb0522261a40ddc1758ac555f03cd6e63b781bd398d079dbbba8005054c5c554c76f3a7750ed2aa3e87248e9e49477ce00e010983792d3ac410eedad234dae8dc474a7773a170c03385c2cabc3c9c758758ebb188bdc70c65be763bc01ea4026d90add094b679c19943265fa9dccca74226e00fde9f1bac9206a97468d256458c238e7a0e6b24bd101a05f29d81aa4db918a477c7cbaaf440dc15f1c02010a7bdf9393789e57d8b702d3b7612acd795615313300fe3de54b3aa8a432a3fe7a71ef3e858f3e0a5b37dd2200f4074155c473e8b3bbcf2e9622f4887140b4fc1780c14683f5e141fcc6ccbdd3d6148ea94d2f0143961b00b888687cb8f57493f0aa8fd24c8ff9f951e77008401d7945d341f636b219cbfb8b5f70578318bdcfca00807f969420623b9d232bae941f93dc503195ddcd6406c81fb74695c54ee708a2063b3d06ba19d8cafdd5e3000e09011e0e4bff21c6370b41dc215d75b1495d653b0087dc3a62db137b5732527add35e5aee568dac7e667c85fc385d6834db2430e91a96ab046178feef4613bec00a223cd995a85d73ed2992db28a6430bee4a3f6ae1707f01f6dc187cc04b87cfcb7890fc8123d2444aaad350006dd1d74c2dae5b5971920675e14900da76e137231403ef5c197ba3fdeeea1e75f13972dd19b4b2e593403908fbbace305adcb4605dd3b091f38165cf92c219e0012af06c51f4ff285dfcabb113d27611357c6f2d14cba12108f14e61db591704f4d8751728cda88145a8cca2b106090557c9eea76ea720033a2e117d4945b1a50a3731f60f04aff42acc27b78afae945e2dd390ecdd0d4b1a4d00fd44e98627bd5d41c34a781c633ede854ae8680a28327b695ad7c8b56f7016095d8e8fc952cb5b13650568a7ed468e7cce26edfe591e21d0c6b1f6e1d3933c7780c1bb8ff9b99202de8459370d1e7973665b9f52c49ccfd7f0bbfa360d67bbaad40fc6140ff7b679b90d17c801f4a320cd8e73aa49217638b1e8857b0589bbd7a1812a607f1735b7e79fa9ddd470455a494e79eae24ea8a40b75023dfa6d160374ebf5b3187e1dfe68186978a70f578ab8d6e352691d8e9c9d0e0e29ba4e72822e55eb6a93b5ec28ceb05ad4ac453fe84c3fb5f52925baf70f0c21bfce6220f1d099284a420bc563aa087d49588ba5dc72d0754d43c83f398b016a0ef49f3dfeef6431e8abb92b39a989292839bbf1da1bcacb29d96b193829386b68d8d939b3ab99d9695a5a38de95b433977371bcf79fb3bf22ea8ee1075b2c33c4d91306549f7617b2407b9419f24787497de82e624f6b755c1544aab2b73edc924b3376ee32ac7f859837ac8f5d2d1421867b24a88b4cd2740e39ca7e30086d63ae02eda5e0ff585f7f498e4b0cd4da477717feffabb341f9530558bebba6dd0f92610f0af97adb75cd7afa10ed7a80eb3fb3d7d7dce7357dff684f43872334e64ea25035ee6f5ffaf7ed8460f602303a8a22591ce4e1732984f2d6801a6cc33889453eb86bad0fab98f84fb66b8a7e3ff4d5e87ba5b04020f04fd577fb07ba9ec07cf6fe5f1e04b3f42f30e6474332c623fd5b8df990701e1a8275111ac24707ff9431f7ea3be387fb1b5b25ff339bd583a9768550c0fbb64f0f8394b0accabfe1d6e8da8fbf1f9bdaf59c212091367f2ff9bce00a4e1d777839185e2db49d497a91aab9a204e8b98b908af82a2c352d63b2682dbaee9e473366ca65ce312de71826c75c91fe1d6dcc74364f94d409af75a6feb0b303e6624255b1d1e1c56f3750aa2bec9ce7bf6f7b4da200fe03ce0ed1ef3b3be0fe19ce0eb87fb3b303ee0fefdf3fbe4f13d47189b71f586ac14f4e56b83acbba80b8ed671e69e755b765cf3d0770c2c983c1c12a124d156d6d4d08d7775eef930b2934419d80f7ffa75d8ee39171cd7f5bd1fcbfd08aa6e9047af07ff4c46c93b87f918e48bec05836579acfb2bd9436c4058ac8f3b55ed845dde7cee392ecbdb7d855a5f7333a22e991e796df17390436776e47cea1b063d115e3f5ffea88ffd13a228ab2be81811bd97d7d2b2b63c76be9880672ea26924e8f24e59c450d7854ac55556d1d25d4cd2d1d1df81ef139982acb9add7b64646a67aace63cb61f61fa4230e78fb6bd18acb63844b2a6406230cec69321f50bfc8ae7ed949dc5ff28c31169de14ac5a78d90c43a911ab62cdf3adc4c9941f616aa6e4aeb1ba41816c391e01188d907bfa4ca3da9fb3f9e2a875106f5052beeb75f137e2973b1eb84b6e97ae2faff33a9727844d4c8541d01ffcc54b9ea7acc3cba0de73f3b550ebce9ed4732e6fb6fe847f95f99e45f2f937c1b6ca8dde9dd047502e31a048d1116ae6e6ff4f7be36c9a43530465972cf56af674c00ff54e4d08c3656406b3bec3889e3d047c7e3cfe1d98f0d7f55bafabcc43b6be45d4aacea447a4d1dbadce88f955dc6f7d7b4c0895cb0a5515ba9594d72d7bab3eb90098aa246c8ab87b4efd32ad9593ba9c57a161aed2d1ffbe2d245cab9536052c20f46558eecc2189482b72b5ee592b632f22d1381fea9daee7533579bae77fd8fc190f71f923008223bae32083f4ab00dce17ed19f3c73dd01baba57c0ef540bb6cb05dddb66daccae6672c883fbf7f9480fd81da71431d4de6e7baf0f784159a2e8e4385554bb80e7efd0efc14e930cbade5492c90793a3bfee4e531bf3a7c5b146e12576ecd61c007d18c55e304838bcfac88e39f64dbc5b70a624070ec0bfb01c3865aeb26167526187aca6ef7ae897f2288daac7b6282c3e1e9a9a9db7715a8fb9642df30a711a0eecfcccf734a3cb70d44984a3953ef0d939c08a39e8752ddeefc7223ee4b197ba688fe9c39999f8dc2faef286b53f5bf41c97f233db44348d559da5a46363f43797eae92cbef3b799345c22a2b61748e49ad12589d44cd877a3bac949baf3a0a877adc86aa67e1229afe89418df7c695476fead6bd7f55c86d55c762edf3864716b01fc1ca9d1f84f2b9d2c4bd890d91bc4b721e1d6a01ca3192f3951fbec96fb0ce4232160cadd63b570b6ee6fe1a4e77476548cc962798c5bdbf222a3cfbfe7d3d7c40500f26e3534e3c38645213655a09b1d42f7885e8cc001b89904c6fdf5c692f37e6719ba481419efa2704444009cd2edcc80994655d35c68bfa50c6a79d6422130b230045733085efd9e656349513755e996ada057a022f20687255e099ff69d2b2350dbc1f5ad3e0d082d8f97e3b60b79ae96e9def441cab8bd3ed3173f304f274dd61f81a1cd58dea093f0d7a18efd3f65129428027b2bc0b10b4b36ea173e331786c2140c3b11163d384c71afe00707d704f32cb874e457576cf52be8acdd70d0eef64007d59e3785ad5e992f99aedfa72cc10cf3bde0accf1033ab9bc930d845d5946bbb366d6a822378d8dfd38361aa0369754f145d51329e7223f8ef138604ba2ccbafb1e10d456a2252abdcb2e7b9e288c4018cedad55d0d2e07049d64a77b2663c948708badb6225fc95589ccdfa60464d9e9ac12ac5b8342dde45ac9224e3418c8d5675c0031dca8a6ac640ccc95ed56d65c1165ec292597dc6c004ad4a214795b62ad49684da8556da6205a8323fe0ba0b65fc5b0fb035c663b0519f54a14a309196a2ef3e86a8094c52e57806ee1534ed35f44fd416c4ff7ede9262028f565f1ee3b1abea4c3ea1b86d8cf43f00903d006015970a1a4c9a871bbed62d3d4ebf328ac0c2b4fd45c007d259ec12cb1d4d907ab562844883b4bd74a901a190154dd8ed00caf63ab80f0b33aba478b19e4cf458ba843bf86bbc1d389fa77dd97b5122159983f9bc843537c8cf3351cd99f429c96193b2f392ba6b448df20b379c20b90e513f9725e6e9a9aef928d34e6587f13344392de08082a4454a78d2e9377cc5854f2ae2ed43d527f590c4872013de5f33a342b27754be032176a0cf78e184c6503040de2d097de67a6608897d10523bdf5f07c15d7cd0d08fa9ad4e4cfcda9ae8b92c87df3a2e9aac3f315232089152476aef19e5a2891b27b102cf5b27d03af31931f200b877663411944d4171578cb6bf5a7c4f00bb19900b2a01577e749ed7930dfaa05d9f72c35270908b2e002b2008539eb9fae9456f76613e5370f7dc495e7f2ef06ec1f3d724e145b5b77f9e35687b2d478a6cd84a90840963454e0e213eed73796776a1205145467e5f49e3c0dfa1a6e03a3ea917a5e56c5a538e21d925eef55f3b21e50390cfe3495ed41ce53abbdb945dd3528e142c5b40d80a458f75984b3c80e5ab7d774988b68f59977a07c0d20a896f56ea47aa66ff42e32dbabf33dea9ee538c95940965da0c5deea8dcce0c6a1e0ba11f257ab4933b8629400fa2916f7def666c01d2fcaa45c75a1458130d23140d00d3ca7025f5ef5e8f1cb0e58f1b20cbbd35441164050fa56de7d699f20b915aaee8e73fa35a699343d7380d5415ad945aaf026e66c5d55737abd6b30688421eae3d7702d249562d740554d060535aeddf0796bed295e8024325b2de863b3d7efaffad0ae3b1e9411ff4908e3ec6b78c2cdc878aa16ba8e44b6656ede136defcd7c600d9166652cbb82d3324d42b57abc02f8cb983cd3f780a09d070ad1abdcf8ee9b623af39d1fdedf28684c7a4dfd359ca2d22abe686ce37603bff3cc5498760b69a134a04ac28daa776def7cd87a3b50a215ef04645b6877a503b2a095e11e25104bbb3dfdb2d3d9655d8b15f5d4d63cf56bb89c45705db8de053d9a781936ebc5420b3d6d162068280a27e609b29d80715b62fa299d31add4e3ff8fbdb78ecaab59f285e1c135b8bbbb6b90e0eeeeee0e4183bb0448902081a0c121b8060b091a34b8050d12dc1dbe75e7cc7c939ddc37cc39bc67d6ccb9ef5e2bf9a38bbd77ef7eaaabbbabea57bfe44e8b1fe52688a45b5138ae1c359de406cf55d79c5136780049879667a39c564d16687b9490a4b1bab4d82ad3b30014bf7bdac78cbeacc22dbecd4779bcd15c4405f29c00fba0814ce2e96d59587569f726016c47cb4062c21e507423ab718d7049bee415f64987303a7c7dfeebb1189d1fe5ba64328ad47bfe1554aa311e195d47e4b6432780a442d14567d9e811e5b0a548a9565154313539ad77cf00eb0f24df07fc9d00761acda669131a7a0eceac10407d157d91f06113a9179f278c94d5eef2a56ddddf7b03400fa91cb7a8721447fc9d9736e5d52fd3828e9245247f947f49ed37a4ddbd82d8685de3dd79a76ecc2f4593ffa35ca225b6734afc199b535de292a710e2dbdb463f807e2899a1240f515e475e122be7851052562ede1e02ecf793627081f5542d814d38cf62687768b4898a4b008a7025c5e0c34ee4ebdaf3432f96b31a7d736b2e7de71fe5b6c2359a50dfc1a4ca5f60a3acb781575e1308000a3a47501026c0f46ce465f65f09c7cbaf6c5e063302aa5c94f1753272d38a480b9f5e7d4a841aec398bf003a060157dd085ae7ac1d808a6a36962f0b1880e16a89101be02a6ce60b1b682b8cfa489df6a856a14f246a200159b65ba7a0c2ce2ec991c8f222ba903a5340d062b0149d94852ea4ab7851024cb4f6c2c70a310a0df6ace034ee2fa26473a2e72d24a9f1cc6aff59484e7a3a685009eab131df5de958e98b7a040d4b994dd57332b9b4180a4e056d211be0c57f611630f6d81f9429da271e53040d2b4b2df62454394fed005539200a8f60c73b5330db0bfab8556ee6593624fb7c0762e63ea4c707c393802b0df5c1cfb325419e14ae21f058b44cb6411e4c89d00a092f5a1ac6f5bcd6d23db110cb47e9e3ae82f520b0149872fa53831a0aa1cbb09a3ecabbcbb13de75303f05d8573cf56b69b174df5c05061f11b5dd13cd9ca65480e38a0e2bb45432a8eb66143f9d9468f6ec34677a0eb0ffd11988f41d7a5f1523c1566f10c70db92823d60b585f464886bc54bed1281b923fbdc4d0cdd7f0f590a1049c0b5b171abd3f74211f57307fea37e79d1487cd64ff51fecc84e6258b9333db5e74ca2196425db8fb326c2a2006d961d6d71c9c7b18fb396de2930fa5d5f4b227c08ffc8442392a88edf0d390ab3bb5525df4cb6747f600c7d3b456ec7b9111e3aa837e3cbd2e88eccdad849abc1fe5fd489b35ee78b4f9ef0ee286e2379a748b0a5d01eb3b8f9c566b6d78e7d4825b21b3585e7d3748cdd71d34f4ff27928424a5fae4b7832e202a038253920282cba0b01b21def606b8f13ecc9d02baaf7c231838f84398bb01e53727a645873e5f3ab612c24bbdff16e8f2080cc45d0d0d020783848182868380057b706cfdbe88ec2fae4727bbdfb2a2433b4728fd5efe2ae4f7f249a387c9e756ffb9f2b5f9dfca5da814ef0b3604c72f16839acdf230992bc599b5b21023276aa36a6c0d4e68e6f423c403946b3d07fc78cf172b557ac22d90f0705bc2869d8c24efd67215cf2e2db2f5cfaee4d12c0d7165945aba7697685f7cb76f279fd536681ce8a8db8e5b5bcba99e8fd1625b354f7c73adf1e475b8f438b68ba17ddec9f72d2b917c750aa5cfae7160695df7f54f251ec7a5f50aa7744db147eaa2dedb9454723ed7eeab6329c9e3eceeecd9b1c776975b78f1ba3814c27325d072db82549cab94ef5a2bd51b754a36698ea453de6ae84507dc3116ad4fa010b0f128ed8df2d75e765604df380c4c1a9beb21888989bf2bfd72ac9a0a0e9168ce63b5ea76d8f9b580cd6b7fff6361379ca3dbc999d88f931645f07fdfa48589d616cbd55a35e5d7b3d430add7f90e060dff11020c1a121604050e0103f7df3f69858a87057f2b6f81fd6d7688508f69e483e4bde9bf8dad0af5ab9bff5ede1df04f958fa3df97300469acb1ca9f44945b81183460466c15dfb5aab28b290b5372b3f54c582971ff8b53c733225790d39c5b53fa99437b6948f5cc13de5767dfdbac7d91d612b23ce3cc6fa17115118dd066ab160e3a9d66064d4e20f1a5d9f80c9d64bd7b1c82a4bee67881699e965e783d328ebb26f78353aa9951b67817a47ca9b7834eaabd995c40755fff9013d5e4c164c0d463dc3ff6903b40fbcf2bf0b43ea63caa77f91c89de6158160eaa0ed213501ceb6db0565dbbf9f0d1994c8009079b1409ed6dc5a85ca368fe7797fa57d7d028dfb3fcd5aa0294f2d2b1e1c9b313799bce41bb2ca5214874894a62f9500aaf18bf1f0914481c9b85837fb5b424b57a91dce4702ed949a692fa01b71db417104c80c5f97ffebdadab35a4fc7d7fb1c5feddf588a0f04f703d1a22e0b49aa7d098797767ac9263e692a2a7f71a00665364257211396f9f0dcc7b2428375ac7e24d45bb1fe5afa6e5501e912d7c09f75ebbb0a64de2d26517d2036c6dfb085e328931d4e710b373b8324ebf4bbec605f84608552f21eede4772494abc0989d0af57d3111e2cfe51be5a1fd84d5a642697020da9d0fc0557b921001bb035b552eaf5329bc193d5ad48ec581195280939ec02b8d6c216a46bccbd38d1bbcae7b18739869fada72b02827e6a7c9b66a6ef2023fb83b488bb9aa46506cd360078bec78b2ee476358972dcc890cfa0631c9f779df102b68e9b6cc54ccc7a79d21ba15e8527ed343e7b660b8098278a45c6515d0f01846a9d8c73360dcff736c77c005e3664d0a233fe6ad85ba4b327f006f6491d727c24c0b536501e13c8f87849f7716e30fbdcd29ea9ba1d3b20a348467f4b6238f6f557011e11820d2d1190a14d23a0001fd224dc8274f58cfc4a424595a5cd65097eb63ec0401253be8d493228b83d80e826f2294f61fa9ef50df0fb946e13a4d1b0f570517208c996c6c87749e1d503f867239cd3d8b6929f123fe32a19743f3bf73ef300d6a1976d50c8d97ff37cd6e12926f4729e75e74ed637806b81685e91ec91f8214bbbb70ff7a30e36b2f91c5200de0a14e4f20e71057d7c17b36db9f01d5bbfbced1d2063f6a9ae84eb5a3fc9526cf8ab34e7bda269969d5c8081fef40189ca6e2c316b334bf59637141e72a4ae0dc014b3644ce16e97191a13373020b84ff1ba6c334402a03f39d2cd4ba64ab70c90fa7b03607807711a1f1201acc0851112c433eb8a62901f2d5743bac03cc5ca5901ae5307b5997a3bed46ee27e6a3cde2e45fd4caf26301aea982a84119b07563b145aae43a362e51e3641c7f806b46721f67b4a95022d30751e3266238e47dbb646cf58ff25d2af29bf21a5b5cd980219ea5c2dd7d936074209ecbed6815776f5d2af9041396dcad645f60900e80c7c4612c6f58320f20895b12663f849e4554c33504e001f7264b4b5a828283334ea849d357388dc43ec001f07e232e7930cb200e0e901599a7bd551a483b4a00e09afb34560f23b49e5e51ebcd7a7d7d14407f940d018899f71dca709693adb4b9fa70a60ecf6a4aa257df005c47cfde8ce330aaef18f7a17b5ba27a7e0a877a0b0b708d71eb9e323fd2e0a9454e4867965b4d6793671400b8cefbdf9acf42d0f03cf52f15bfc319a579e2863402807190d552ed8a476819c3906a270b8583a5528592000a7412692358d09e8894f446e0ada4b595817315db0242074c188c3c46b85cc4668689e1c93bd61477cf6801fa49d6df2cb080d09785d8be39b744b051def684175060ae74372a017a013bcfcfc45d67d172ab4396af1e603f325af1833395b308c439bf91d24c0f49b4b1d703caeb2830e550912b38e2a2c8d462a4a70f87665d150032e29eac5262ce8fb9c22cab667fd1e35d0be2667a0bc05b2673717cc3b71eaa9cf12bda05d37517c4b79a06902a702abf6e5cc1610b757bc71fa3626b8514e8be0828b057490e8ea6fe5c6e9a7f038c630ec6a9c0d6a400e05afb34cdb86757b78a3ef724d0162a7e64018f421d9042d4cb6247bf425a6a6157525d36c5dc90893e5ca60af87eec9a61f309485491e56fe36058a153ea65fd80d080844489a28893172f8a93146526ae89a5ae2f386003973bc701356f3964e199ef4dab039f04c9dd2d0e983f3702c8efe50c871375cf5f87151f960f3517d0015c2b0bfe57aa2d57517478d8cd8ba6649e098e518200fd6f39a3916a730a4345fa40843a07ebeb00415e08086dbcc64effdedf855bd5fdb2c1941522327aab200bb07e6aa61ea15e298b495c8de470895daf979e96970b03d65fe5f7d4389a9ff60ee04cccb6cff4e25cb6220005322f710820dcb5922e095f5b7ec81dfe2c1d970fac376235dc911d5171d9ee66e76179494032dad1c801189f833e14eb34ed63d8b37d45a38ca824314bade7003cf70ba2e6358a989de226b9774fb84f52890486df000ac07186620e5c25e5fbe1b7c3b40bac565be1701d01ea99e007f4385d4cdf9ed0e7d0248f3ee7328c9590072c10dc29294e3b04a8b197746365d7545f16dabec001ea2d3c9759bfd40bf470a44d5f19595583b6c651e600e4d418e0e7773ce37ccd757357f385045d2dc2e18d761960b7c7501e971981671d5072edb956c9d2b42fad0c3800744fc1bda05925b9c2d6ad0c248fe5413a636e05cc7f53995ace41fcce0cc11483a684c369839a5e5a006bc667c470faac82ba27e242b52970b1f863ec2c0600fb4f8ba7ad6dbd780dcf0c9376056a47f2c03e8d45fc51be2f8611654e51bd04057587bfe4e7e08115fc1d508f80645f40275989e90a893c51f84c8e1e5da14318e07a8b3366671c08c05f95ef25265d8dcfe49365ae31fe513e1a3da73ed1a1df8755e4f2f258cd8273aa494cfa47b9b1b92e1572f8eca3ddcc469d3ddef9fe7e5a41806b7ddb67b9433c39200e344cf5151131f8d8d7475417103abe76e5134fe6a04e91d0a00e23761672a11d05cc2fe4507252f1ad2719ce4da249df35bee5f0a22bed005cef2adf9f63851d501cda2f427659ce68c1974d03bc064a6f9353f736665784bead5728a01da449396900ea454840f280177123a0deb24cd24780ad1f0757e701422b299cc6703374ac52eaf6a547666212450a82118024a2bb55eaaca6bbb57405192e7abc0c9fd73352cbfc3fcaa365a440b7041064157b62a6ef5ed7294053ee01ea0105997136fbac7df954de626586b2d7ee9aeb460758ff120cbca71fb94d7d54ebe4b55685726ee8cb1c04ac0f1eefc8fd232a2e6f6645537be18579f406491400f6473f88e1181722f598a02990ccc4009b3591dc13b07ea0b2f31c8f0e3466227f248de90d43f1ce8dd0061ce06410fbb96cb6297ba8d86dd275b05125600b3b00a1856fac645e3edaf316318fce2cc765862c676dbc0178f9ee1e5bec491e91fa5145b5c192b7255514e8e780d0120c8bc719e7e9cbbcd8ae77f994a99d58e968d48003189519c126f6b5ff1403bec85d21a54f2bbadbcb999f73d2949703da4117109aff01ba44ee5909e654956c84102b4d529a7ae06907e281296df73cffbefc74b010f4efe3801ded7d598a4a7731b7a74af40748486b9fa9bb3bbce2a68eef012e2234fcdae61dd6da532180af58f0f49d8bddc903dc356611ac661affecfedffdee3a74ff7d720f5264cd1f0b7f070cc5640483b173a6113197fee3fb6544a2e6e7fe802c01f9827752d4650b42f7e0c7569a4f7da7c95f09aaafc5d06b1d7096eb8335b48c7e9f16469ffa80c425483452e4a87b52ae9402fe634e35881d36de00d4e5ef46a1cac90ab5ff47f08069f9ebbf070fce3dc2e5709e17ed75f207430a3d0bc660136b3093943b0114c3f69231e68cb112d7564c9c23aca0a3a4d795c200acd5d0abc2c1c17d96fe1fca9aac3309f2671261e173eefbfcf60b70b5704ca5b3b90230b01084f318b04670317525dafc7b6e7b3061acb0a89ab4a282a1aaba92929cb6a182b09ab486b8a19aa2acb8c27f6d5a3dd0aa01348a496cc9f56cfc09082de8713f6a3b1eea78b4eee71fe547b19093167cc62617d9d7f18f6b2fefc43fe071fce193ff7d2c4163e1cc698f336f02c04210b63ac042509772c102da4163e1eca543a2aad13fb7d225437d8813fab995fa440f97a9f0e7562a83a8a304b09f5bd978de5c3effe56dd4f6ca323615bfb47e17e6f7e4f8b99562a7666837e597b795d0ed7e0bf9a5bf629b83fcbf3c97f9db60f35cd6cfaddc9a69b51fdefedccaa4af7d9d22f6732b03c65af9f0f0cfad8c2f98f7ea137f6ed52d7bd9544bf3cbdb84dfccbb10fcdc4a2f0c3e5006fbcb1783f34fc968fddcca23467ed148f2732b0dc53189a0dccfade40b75b598bf8c3a6790304917e2cfad1c1fde889d36fcdc4a39f26804d6e9975127f6a474fb65d42912ebd49dc680ad17904f034260f747fef65f23a4595099c6e707ce8e0716420062c6a7e0beb2847d5754dde1f51a2ca35cba73a935cb7d60b2e93d7b0e90988d3b1ae01bc870b2b1619fe84ad12334907e71d2684a87ac1c7d5817ee03eafc6fbfeea3660413833328009c540eaa4d8b8d7a17395fb54b7c22f140594dc2fb52fc87b1cb7d5dedd1aa07755098adeaaafbedb21d6ea54bda5241162c196d656549fca728d0e1b6096bb6ab1003d3e2554662e4a2ef3006aab4aae53aea867c42e5332f81deed468ccd0d01a3fb0a1bfc97afbfe7bbffa4109cd96eebf5eb7f748efd09630fd67e2e171002f9a906ec576655e1962f4477d1bba58c7e0cfede419f86f2319efd36cc0d9272be055de0b3078440248483fddb7f8df8147f8211837fd804beafae06f43def87bd07588b700f7017f91e2540b9678b857ecff761ddf37d3880eff396b86b4eb6afba90e3e39fcf78bf5aa2b432e9fbc02dda7d469c82f7dcf7fa777f611e0c0fc8c2b3bd50ec41e9289895668557fa0e57ee49c640db4c7c2690fa327f33db5d7f64339a2ca70989c23089e7b36fd8ca8ee4595d811f14c1f9eaccf5a541663fc7bacd71b7c8999c773627759581643746e5677bc4e747d06cabf25f0ae599851f5b686b586e267e880869af232860707f57d366c284978a6b6dbcb19e4af4c435f169641ca9ec4a12768502b34d7c636ee447455853acf708c107bb7e3cddf677ea438d2e7390c555d5eff11faf1a60d9a58b6fc5f38abf411f7c8dbc31179e25281fe563ea0049f1a8360fcb3fd2a2509a5347e88d46b5023de9f6e325d61b170e136dfbc7d49f7c87792952b2bfc16d5075c17c5097235003ad218df52272f1647077f582b49a5c0f47bbbf728c2f9fef6e0ba12a3b33b6a05999403f720f53729169dce5383f7aafbf3f12297ec69be101c69f03227d5a0cab2c8ef0d6c04f994b7004f7308c325b9a1f6ccb6b71126b3b2c547e0cc551e09e9ff8bee21ec261479150becf12e20eada60815fb49d297629d6877b83023eb1b57cfcc38ba5e38f923f9774cd33a1a5585879ee3ac7e91f493088691af3a1ef13f99d47ebe948aff2719e9ff7efd050f794b64b775cf330059d2b03927fe7b2f9feb577186265ddbeeb38c94f979dea7bf70d9e5c40cddd868b51a82bc87dc04237b6c03679154faaab4da37c6c36750738416db2944cce7cc5009de9e2ac3319fecd6b257dec6a532f75ef564c2abdad980b5151c522a7cfcc80a83a95e3ec58550c15cbc0fe95c765de65b6879a1007503433bd8cca43f1838d4f175433a3938e19d97a4031fa5efe3a7b72e049e03c33a4cc823b4f977d9df031e679c9cf03707e6408710d50d3d86e02d26ec02996cfb40e67ea7ebe13e110dd6bbf663435b6d73e7b6767ec3a81734a2f508815e86bf09c95644dc80ddbaaa4693991931ed283c28a3aa0be530cf5c41d2987aeed58731a8b00690891d06adf6d71dd6e7def8868a60ebee1f1a46edf570b4716205245e60c1ec7da9252c2dc8e11cce86d9e5a786e93d02c36809c77c19cdfc92cccd03f43f573feeb76f16bcd480d2a535509fd723d23eb82a352816683186afdc0a2f0ddea71f8f0c434a9df14aa74490f68c46d21b5eea14e9dc8605cf529ad85917049aec95c18c3ede4d2470805f83e3110eadb3ba222ef75cd063abdd67c2a182b0f6a7f7c45ef854bdc33f773e6a5f7c25a7d94597663adda977129dcef6c9522ed39bdb9611762d1d199ca62c6080343e759c346d827a9d74663feb652e6b2a847d237f2e7e8723c5edd8271af2f7f2d835de6ba13a316c7ecd016431bb4bd7e7a4c22335481261e4254977e1e603ef6b1dbc5b946baa0b0b7935e81f55edfb8b190ddb74a0f8a4904a329d8fb6e47bbf9c2a4ae9538dc2d0b0ecc57d81316df279a26a380792c5329302b335fb6be8f3804b4988336d95b749c9ef54753e0d998b142dc54c11d67da22ac55f39e5484848605b6f7ddd87635d9d6a5054de5c1bb15c13a097f150fb7697176cd4605bf1540f17649bb72708a34d9635e7e5ee7550b06b544883e0d0e1e30503a3d27d0bb958d60165cfa8ba6c31773afa42026de68a36ebc803a2b0ff7faf7dfb2faccfc70a4f00aefc0ada901277a7138226c4e0fc714ad4323c6ed7fbf4173c2a7d3caec400cef8b41bdfcc5d0b0585306f37fb024baa42243c3595cfc40e7ab885b5dab3b55d9804338740a54dc0bbe402b9b4f4806c3897160fe2bbb6086c0091b96f2ff96e3f5617ab919d75f28b6ec1568d84ba9b4e5f9961377c331c7af3856b212270065d93716c2d5203756918df20b67da54c393c05d2a8344a19942c547d43ae3e621ded27b3a9c57a4fc23facdff4ad3efaca98e9374fcce3c70b03f1fa70adbdcd29f32973be555eb3f5f1b55f9a95af32be7d9feee39e1e46415c3e5fce8c5a50bfcd537613b4e6975c0d90d3adbdbedab19e37950e5006c7dca07c976436edee27fafe272fc802277c36f69ac6d9ccab596a2331585855855ed1c0d9c01a791f872c8e8b488291677d029ff980123648292f8756ea0efef4a5f943f597c149ef1b48b6dcc358b768bf34ca803ea45c4cf452b75236c3834332f2d4a852608d9bf916ac9f16170a555d60000d81af1b79b4778a78013b6d3ea792eb5cf55f597f2d1423dc1e6a7f2983a4c7ca828b27a55df5b7fc5b64dd5c9bf74dd760cc5e0c612d8abdad7b9a44d769601e6fef065bde8f11617eeaea4bbffafa3b4b1b8467c90483902995fa0bd8af2102f1f688ea171a3e5dc2884618ef33aedfb7179176799b267f973effae4f9012e59aff796d66c362d954e5db524b534fe0cc478ce72f4cb9b5a0b114dac051f2f5ce0251262083d135a9dc641106cac74d3994d80d72b8b01c435874a237a84efd5152c92ba210d55b444ff39653c531cafd96a33ee48df2c838bfdc8089c7006d6b12a10f4de2e01ea2d14d67cd9ee2ace1aa79fb38d362eb7dd4106dd327d2dec7f5978ced30f734e78321f553fc5e24ed914c454d82874d803c9d14698cc8fecee3c052c69a378ef63c0dbb1c26b17246e422facab73f7f15a1427559f0a1faeb85654d1e6ab935e9e67d6c9387f7a6cf80de0b3fbad7f0b2d2feeb454cb7c08605d13be28f8ceabd2ab64905326be9850af1c8ab213ed08395a4653913cbc62604ff0fed2f63a20a0242fb63570592544ba437be5ffb6ccf16dfa7bfd81f9ea23ebbd56d452a03b1a7b06c0f2465377df398324a3eff5268b39471e4031fd4ff298220b9359b499762c83ccb988154b28d82f98439faade266f5f5610c220cc28df59b19dcd27726ef105d35d3bcfcf42c12191d263ea54b8acace2b70e3247eebb17cc61d326646f9e875babfab5480c363e2dd13c6eef392a145a73d18f0b8fa8c7326a9808a2eed450f2fcacf2e51087b33a13dd38f7d8e0d3c314b6c8827cbeb281ecf8ff2bb0c33e39f908ee2d0f22ddc214e64f0e2494197e866bb52a5297eb1c1eaa5782a17b67b1dfba2f6ee6bf745f53ad98b666abba8d340cfb347a9d6e7b4168e43cf5bd5713624cc4ce6d593a08e863475a47a08b8a94213faa89fe8e1c4499dd0f2704cbf32d81add7ae57e7dc862c7c4a100c7aaaadde5daf8bfdabe6156003c33d37572d77e9a1cbdb828a2fccd8c186a29876588f7e9076ce33be678a25129848153aa633797f09a8926960cd591e5954626620ec9110bb058ff844bd3dc150bce27ac5df419d132e64fc848984ff3cc630767a05f704020872b9ff332a4aa6341be9fb84352c310fddc86d294f1a812f5e0006a59ecab12711597b2b8ec2da3f2a08b9293d379d010ad204b1d8fcd54cceba3db02dd7909d0be306735128ada8050dc6cdf5655b8f18b75ed89178d37c7b0a45ce7f03b2d3078d526efbe59726864063eed527fef96867a7246eaa182f3bdb9487db52a478c5edcb85bd92e77d929cf9daf2ace7575e44d7d6e6ae7ebeadd4e7374b3ed527c1838853a25c5f4b39976e346b5582c424a91c3f9d59d15bcafe0e7c2b39f2d44b1112c4a3dc8c95fa158f808f80f3df745f3e7fa1095dc4f1b5ac9d740cbb79ff250fba6a085d8d17767a19c5aa8c9d273bb17601c29bfe6eb9035d58a576f557badeaeac5c010e036752ede615c2ec360701293518476b614120e0fcbe5a75abd9fd943f0afbc3e53f301a85d2ed03b3504184babb5e8d0df4c396179d33caf76ba4f7f715a1dc1729fbd82ea317eb796a99be4fd08d74d338630aed889095b2a8124b1074a5f08aa5f0ebcc6391387a9955b2f4f89d251fc74388632a4cc186f35ec2c8559c620122731da635c8b66c5a78d8cc92e2487b6179459d0845d9c28dda99c80f14dab3547a61da9ca3069a178db96da9b224dd68f7f23684b7ab1a1efbca61eb20727b2604ded2dd9b8d956f860a75db564138bda086ce279add2543e52411d5c95f823bcc2c68d5b58eea24affa1aaef1aed0770b45dcd790b0a492a84dc10f0582c66e156e58d076347b11c8c07dddcf2511318d4feb00b4f1af85a8bb0f01094b731478d783f6899de4679f41f2be62c8838ca1e4dd19758aaa94e206cbf85dc38698bed1e1ac3b0d21cb1b679d1ef654a5f80b346ab808e02cef9bc818ef4a1faab014562dc55ddb04bc79fac7bc8bad54c5be152ee692f538705b1e1065b169644c8b616f87a05a91e95911c8a4098dc8abb2ec79f6011876cea581757afbe1df6ffa5f5d941eb1055ef29541b62572d2672bc314dd5c5a9d17dfa8b46c36ff2f9c2dd7dc3355b8d0ad6245f7ff038f57c3de0c61ead9d677c87ea1207636c5716c1d609c44014e023b5829bdd1202c19271a8ba3ebb8ab69a89f1089da4b5a5f7e0e32626ac01d39ab5995a85e9c4045e547814c30b7fe7f18b99f73871fdfa9de4fed93db3cb4dbaa3cb2b947cfee632d9ece58798e901db70afac18110ae5fc74c09933c3dc1bebabc6993674a9d4baa0ceebcc3399baae4ebf1b3173253ecbf4092e4c387b93b34eafb1f632914bf54507e32c7dc53bfd6fd742938f547bd11d25759bdba05d656135ba9b513990f75be4731aaca121db43b5afad51c8f50e1d6706e17b623e3abc52255eb4e8473932990e7277aa9d9a72f77f594c9466ad2cf74a2dd19c12fa8bcd85f9abcd5e9e618b6af4013c3eb86693351becffddfe1f54f687da37e8e8e51791fab58ecdf698acd52b9a5f4fd1e1c7e66d3ac3e5bc4e3392faee5e3313ce3b33327fca69e67df67d6d96fabbddd30e8b10ce1d712218763b411cf8565c597778b42f15667929c3bac9d4234859af48bc77f0d56c261d1538df17c15dc0f45a870b92606e042e8cb1ebdebcba04211550943f557bef2f1622dee5f2e516d9cddee54e9dea1dfd2b0cf6d2a8316d293ef0babede79dfc39a4ef2a71fa3b849eb9eb5e842d8b4a8aea1ac226958abf4bcd00e5cdf044f7c3339d4a7389b89cec05cc4ab73922d134bf5c12c2fa3dd122e76b514826134249070e1454da3102f3c54f74026ce587df598048a1b15b44e4b960af31a5b8d4d994218e734d8c9060d9881da81e9e3419c48db31c9d22e11b65e3c577e489795e73ada91d2e15feb1e6adfd482dbc859aea3e72299aa918df7cb56f759ea629af874b0a644d2d48825b03f72c8cea2ec05bd47bf7a79b4b6d191448524b257988a3b42bddbdb787309bdd3f890f3f3df1293529202820b27ee7e0dc4510a8c3f057b2942e94e6be4a6eb6eb2f60e5a9af18f1e45abccf20aacc79ea41d74aef56365bb1018a4c58a06317b2703c0a345fa0bd40c824d1aa3956f90bd4f6bd79e23b172dcdfdbd10808dda0975775759ffe2d6d91cccc22a0fde7544695f48076d005f8ff9fcaf848e0309866aff4afa4a45f929210c0b19e7b88e847c9ef337bc933ec083b74ad8ffdf1a3ff369cff0e8403c5ff1380705602c2145fe5db181ce642bb2d9a9c162f29de02888f98262e9b87d6f19d9757bb2cf47a4cd995b73f0112d191742888ad0b68232fd70d5b3872b633ab2cc1004097d26ba87cf72aafadf7feaf28370714882b5d51014032fe0d6a26c1692886b655b11c9b1bd7426d7d952f3fcadd41912a3732d1de8d0efe26538354a377a615002095d530931fb89fd069d4c0e5b96079f81b62d57c80f52537662121a1144ba029fc902520e97688a4ab08a811c6c0745bfb1cfd40f0f2e8805c2e5a68a8d91d1c50a303294bde37e3d5798c990979c3e372d688984d36405a266de9c1869422cfd332a7bad769da617b1644f680dad2c409b4d696d075223cfde3e2b77541a965e0e780e8a7e77c07afa001afc22649575dbf676fd665de73c0ea92d084b75729a878346dc0fed9cfde9b5bd7400c40fcf4de07a98925126bebe364544759278f570401030008763d69ecd6b534a8e03a1a2be99a83e06c8c3a0680d73b5ae59b164aae280b55711817ebb94e095e7f03d430721f51fc320bc387656545a7e08566933a1213df0c58ed70f3504d041fc3534c7864851c899fca1d9e026a288d7fe34e43050f153c377997e9e979272546c407a8e1707db4f24c2fc6ebc5909d478c27cb6ae0013c2480e9bdabfd4b37e6ad4b37f5d947ba1bf1f9d250635e00d3fcd9dedb2cb54708a34c9106b693b699d7e852c800202777091c49d3cbeb24ec68f465cad943831551608d1b4595450c8717f6c2821ea3431868b079ce6ffd0189e24cdfdbd284de14a88d6a71ed7e56718843e39600d43822ac27bd259cfa2874c7844eac6ded52fff4ca1a40fc4aff244c8505033c56595e2761d2b13d78d80f1d0064b999622dd18eeb4ddceecf8e88e75acfb9b88c05d4f2ad0ab3b34da98a980b4ce445ac5af37e39c4d2094811d106fb5c3b87abcc1dd62061378f54525b6f3104a841b7c830a36f85d932b7985a36992870a386b65026fba33cbea3b996759bffe44321e90caca553a267b82260755d4b3a8325eaa5a78934ad8991aad3f75a51e66dfb51de349657a01fe5e23985ed3d493b42aea890e399fea35c16ec09357ee039d3f6edf254ae6e8c45c5b41cc0defa52d2be6b87ce4edc91e5dfaa1e101560f9140a00020e0aa1e5b9a069e7a64d43571c319a9ecd082f3cfa517ee57db6affbe2bd4717a1d00051e1e2dd96bd3bc03e41ed89869564c1e557476e1cf959963679422d00d693d38223f1f0339512622ad632e93e8cb9cb2f73323fcaf3ae91bd7a3f1079ca77a2626e0d46491b25d101ca3710d5ee1fb2a8cfa071c14a6d9fb012dfd60b5b014e0fc847574cde5e61a54e8b98adb3c26c03baec0a801a398eb77d843e39f5f2efd295f688bc997a5facd503927c4efb6f3da01737518cd7bc224acdc0eb603a9a0069e76186af5d9aeb4e97234f21dcc1a54629bf0a9301a86b5a786f46f7bd16c82d1bf8a50643f73b0f550e0129323573ebb9beb39418bb819e19d45c2f15ada1a900487d45222491af4f9cf3ab710fbeb1865546dbd7d90280b219971be9dab1816d704f600fe5146758cde8da4b00f32f28261377260dda60cafe281c1527ce50ba0650fc58145d1eaf0c336f397f22c029ebe6d4ec568311b05edac623b28d44ab21f26b2e6f5d7d435396d46100124b97adcbe41e24ce8f59056f07eb09ee22c0ef03805c4f8412cdf6156ce62064324b98b2e097d9be9202f4afd4a73e34088254c8377cedd1e856bc475d863d00283b89de7fcc35238b1b6c6ba81b3d59593ae1f119f0fbd3bf2a0f4ad9c12be9b7953c7bbef76554e27135a0469318cfe0bc6a2da2b3eab565bcd2e7f755dfe38135d2b27a02e4d7f130a7c35fbc7a4297d57c90371b00206e95a56c2537cee7662c49510c61a1ec65bff0ea03cc5fc474dbab018dba6227a5556b28ccccc737ef430079eec6c77b86ee01ddc39dd43642ef3b56110f243d00723bb3ad9883d9aaf7c732da72227dc15b538740e2cc7666ff4107d66aa14f2ae431778c0260b07ab9805aebc3cb73a8aacbc514c1a59d23538c0608494d7600a0776c0bb2cc861195d3c72a9429c7e40a54b1f6112f403e96336866aeca7f1ea258c557c23889c8f24410407c9b31a9da593fd1ffd54cf0c9aebe356db035473d607c8e3f424def783d2efcc42bff1eaa7c041904e17bf4a33cf89009573ab4ea236d55ba3d45d20baae842180031ede1aca83f94702f184efcd45359a3d9485e9f38807e80af596edd2e39c1b0144bef5c209e052ca1a5006a84b2b439f646570fa07c56cf71d6bef3552149adbaf851fe262b00c7c904297b14dbadde524c22bcc4480cb0fe9e92a4e6b67e32b3a155c82c944c443a59793c62fda31ca6d692f66bb1951a7514f59a149d70541a5210a0069e7090e4449c887fc6d78997b54d70d3ce98e03280f9c9dc9eeceec89a7a9a4133746cd7049222959802447767113bf0db97f9978724582043b01452f62dcf01ef5747eced140c68d519f378921a2a67ebf7dd8d0b30ffc6f9930d5e5fb7131f66464af610a309dc22f000d6d72b5c33cfaf72622a359a0b17f327272abe9c6f5b01f383e5f1a8113a542d0fd8b8d5b7eee7f0ccf4023380fda1fe966751df666980809e77a1ee5bbb29ca4a807349df3f457c4833970cc9248c916faf2e96e14817607fd3ce1bcc5ceca92d9311e4bd79b95eccfb2d8000fb5fe9cdf4357c6298570ee286681ec2d128b7fbf1801a89d669eacf3a19d55557b70eec1c35847844fc5700352cd5968749f51c23e11670a46bcb7da0a73ef82d0080b091d91992f552220a05a499ceeb235a1f7dd3c201dfbfe7e048fe98c6adf211073d0436c1ec32cf730f400dd1e65755ac498db2a18c1a77265611c6baac77af00f7f3ba2daa93c41d2bb985530eaa4bf8b1518cfa00702c5f19cd728e1a3cbe8e1abf364e627164214ca505ecdff05ed817e2ac37be9bdff4e4c6e5f2cb51a958008c6f9ae81cf98ebd60312d816dd5b15e4052024e250008daf26689ee312687ffdcc5317cb891bd6204a123208dd6781d21c90debe905e473ec33b68257b9471b91e2a00b08ec5f406f9025d2031a0ff4dddde7bb0978e04913905c9dceba44f1512e59b42d8915ddaeaef382f084b6f161b9ad60f0f2d60eae24a276c6d6f6ff906b018a95898589e5bff8c794ca1f91d2f3441462185b9fd73a1150a3c56203f206bf57d6ce84e4f537b2dd9d045be9131c0585a5cedff3c87b48f5e85cee3be9de731958b9ba3a3d7dccccec62fed4d1cedd9cc9dedcc5d4ce9cc9d3eb19b3b593c353665b4e56e767d69c6ece6e9c6636c666c6662cbc16361e4e9c5c1edc9c4e6cac26dc76ce96d69ee6a65e2ca69c76566ec656f616aef64f5d9d1dd8399db85cee1d32e12e1ee257a586610804538b11d5ed3c012ee4588a7e4153b8decf5bda025349e6681137580d7a9e818fbba6f0077df6f3f47cc68f9b8ae0dfca6b157494c09d2f8e7fafb7e50294120e5fe8bdab1af0230b27ff0adfdd0375f7375af7b7d7fd3dbaff7fb990ecac2dac85cd8c9d5ccd5d34d8feee4e007d760ee778384bab6c1d9173487896089e1bef2f8a39ff1ef93fe0e5f9c72ff897e92416e9f7bdfe37ca8dbd30b00d4cb677b679fe6658034a2fa43ea7091edc2855424739fc9e178861e17f1381b9b90261559a3d4934dbd6718fe499e383aadabca89d9bc7673cee068c01e4069c4298c47f7a4aa0b2f25fe4d81dfd5e37c7c3798b70b56a8cfe86640a45589810040bf833dd99391478a70c7faa8754ad20e69df4f2bf4d2c44e580609385d8601fbb904644b13f01a9f0400a4f887b562be87bcc211ca07fd699dd8c46e068cdb43ecd6269b78aefb21737fcffc991905fe05a738ec7f2fa090c2d1eb54c1a684d50b4732654070f9b5a7fc1b57e567ea4085e25a471cc3da9e6784e5ecc6f7281d0e3c87f389f0cdc540c621f446c2ddc4aec00036aae497cfa222e7e8cc18cbbbe2f81e5d1363558bc706c7f2f124cae914e495043c7d679218de050e195296194b6734d4da106144ca988362b2396cc9f9569c881b866b1a5c9cf90272cfc7ac3cead5627e4a0ee2ffdfed7d36f98d1a5eef42dec518d493255f0e0cd76ab5bdd3fac6cb8b54dba4bf8f6411d14d6c93cb4167c43a18718a5d1d34c56ebbe896c1247bbc288dc7263117e5d22a94ed26f30caf5dde6053e4727dbc0236bc99545a5826e4d459ddcafd29a082c32fa7f56a643a2d596075d8702e5955000d1fa314a6f2ed893a6bff4fb5f4fbfddd0054a34b567336377346ee3dc5b176cba9186ffe866aac4e7e2ba490fd36fcb3e6c591b169ad869bee9245955257a6124ac5b76faf7f2f6cfe5ab996c8a3195798de7260225e61927c242a0042220124f0e23b792500c9aa46f376fcc3d5eff59f6bb506b60e7db19b43b27651bee74cb360896955dff2ffdfed7d36f0da9cf548918f2756477c3849467b01fb3d7f23fddb7df7f887e57cde752c011411f68e8b82732c8a0be5d4ae9b77c642abf64189cee5a12cf3ccfe389847313541812985470b123abd73e7f822a403ac88789bcbd3de8f27968e561991c178f7e38543cfa330e15500f71b1808141dc73e880be67d2c0dda3d08880feaf9b4e630941ca7a92b12642fa5af1ed0d5d3178fc35a9fff526751e75a2a565364e910bc9265a2d555242f3a681f81fdd9ca7c4bf2dcffa20be5b61d3207085406238ea7614d767646b1ac4a1b6fdf531a10396067651e1aa9f6f38d0b52ae9c44d9e448fec695cb6aa37d34b84ce4f7f118988b5f9be20f15933e2cb9fb568fddf52ebffd2ef7f3dfd16b2ec33ccefb93df671902ecebdfe581847b4bef647377f45b07decfeb01114de26e150072fe440bd2613fc00fb2c3eafb82106ad8ad8a24a728d452646b62e012e38c8b38be9239c1e8888209336160baa991e8792bee18da78deeb99e6af2c69fa5df1a69161b92faed9dd61f75489313d66414a605f6ffd2ef7f3dfd6667b5196b8766ddcc05e7fdcaf9c4cc9acf76e20fa34488d6272fe1d61f3484c2a1f0c8a910c1f8fdf9a1cb1e60a434a2352c6835537ac6cf7571603929e88b421973d31a96c4527b739423b1091a55c9372d9a105266903e0efa2ed5e0f4d82bfd59876a4dfc3e959afecf9f098e60226f4ef93fa79cde8efca5dfff7afafdda22f7b6aeda6ed83266b88e07eb84a67ab7e70f29bd39527ab65cde3fcc7e77c244ea4c3abcd3a16bee7eaec74be9686f837173be449a1976c1ababc7edb7c9d634f184ca05db430becd17977fc37c71c5d1e1b81ea8937d754dc870d31c47f1abc8bd4a4da5d366d808a6c668bce645b319956818ee22ffdfed7d3ef47bb8778d7dca7b9117b21a9cfafd21bf618b730fe99876a3fc36a47becf9ba52c3c532049c7575fa1a639b2313db6d4182662bf3c57c2da73ee86fa4898ac7bbea6f37a2fd67497b5fd8a2526ae182769b7a6a44a701567e621fadd7e5e158ee8390a210f3d104c8772d2f0d6cc239af88fcfa37ffb4b76368e7ffc8d90ba006a83ddffcbf5a35c36db93fca3927f4d75a79a1d9b1a5bb2deeccb1ad005d448c0df88bd87b81aa13e7504f44211c1b684b53a135987b1d2b49d20b36e4c7c73796d413568931d3989fa383448e081531fe028d853ac92198e33c1a62456e7874a3adfe962d9b2f8bd0e42c9dff3827beaa041dc23077d4e38a2583b14738d95386ecaafb120d11df16cc7044f817c9af9eefb47d541d31bdce00029e8da120df02d31a9342db234a9615774734d92ba5988a2f4973b3305f899491fcabcdbec624f7657ae5bd54f6844212ce79deab3f340d3cc25d4fc69a69c883dcbd1536293bcd9a70bd6a3fd8fad8ab256ec464f9ed1165df95ccebec7c4cf9ad6d08b7527b4e5f7f92ece57231ce27dc5ee422f27f7aaecdb075faef26639e2adc946a45390228b7a029ad6de4ecfbbe8e291481d9f649492514721239f0502324837c1cff2634aef3bc26c76c98ff7f0e2877ab6f50c8892f502b715c7f6f2d1460735e73de7e637135e7e437857fe85bbbc5c3d5b702b0c6ef3ce9d91283d04dfb2cd090c0fbee8edb030ede914dff0343afed24d0199b987589d345ecb39363bde92d5e1d7c8168d9e8575a64c4736f2418c718e4e070536ef83dc0412a51d4282a5b46556f7d6399333bc0dbf0d5f3a8b882bf08e01763aa1e952029a8a5399310a19f7b3475bddc881fe1415ef3109115fca11cfc07b04ba1d914c9b7eb4aa16c5f7529ae374e28ef3c95ad1eb9747b4570c1861d12b9ee36c5bf7291ecffb46a5a61fc9f5d6c51f35baf1a5b6a1fbac82c88c505b4aa3b499bf249f2eab24e9ca5b7c2602a84229e255fabceef38667c220e7b4736ba28061fd70420c7bdef642b7621ef2bc8cca77167c8d99b4866d938c56165738df1610e28498aac50625c64762f00be3bf980eb7f2303d1e08ece0ad2aec16e4553ad3b6cbb0d77fc45c7445bc30049e20fbe8b5a19121f327ddb4f068f78479e1820e315552755a8356fe9e9dfa2c827eeb152f07c14dd132ba90ac5b59e372140dd401aa504f9517fbecc6ceaf46eca551b6bc7932c62fc962b7f20f8a36b03cf20ed5862f2ac25567e046cf1d470bf1df62b917a7b6ad59aab253cae1c6c194586236442c7f450ee393f003cf55add851fbd2f0f546cb373becb972f655995bad088efd12ad049199c84ce027db13e877f8ff0c23ffd03a96802c70f3c75288ab11cf5e06f23e6bf5e83b25c31750947b9823f1be5c3ab0109b1685827bfe46e8817d04bfde0e2bbcaa4b5939299a375145bac44c9b757e0f766ba481c79f84a171931df380f1c358e7249e74ca75096a24c4145c7b59c7e935310fcc7cfe27a2aece43df5a574712adddedf48c5f8d9a5958a43c0544fb6eccd912c94421e755571812334648b1aee1ce00f42326e1b1a59f37ec5d37ecc625c75b02516286ba01f410b306bdde28667a6c77290879245f39f752717a00f409b64be4324ffaa612b54754fcf873dfa17b4d9a64ff282784c6d0df916b956367a47a51d42cbfc0b6860e60d65dd98511ea196696cd21c9b9b1982332f0795507701c98168928f4908f5f1b89cd5f7f56d49ebab394073cdf7ff4830e96e23071f1974c1fc1e806470c813a40c6a37b491dbba69b36e9909195576e9488be0ca43aa0ff5f3c440dd39a13c7e34c08363c6c17caea286e00ef376457ac470fe50c8352aeb0eb058d92665f9f01b644d8319a2772a7bb661e7b93754fd2742db238d1010958103b16827dde97884a4f29cbdeeaeba486192203d66b4183bc77d2631063243132a6d13acdc66670f380f5960393b2728780d1e16edc8442406b2e3113011690355bb8d430ecd5cde444f0823b5b42be1b56850a0dc06c4d9327a71e15f4394f7e271556f36ea9be207d1580397f2ea184827d42cbe3bf44dd97c571f638b9321850138d592d665931a612c91b0f5a592412b7085e9ebfe84779f178579118a256864f8516a2059c8d8ee85901803eea09a564427a5660940cab4318d3768664046320003500832730413c76cb941d154dede8e2a6421add0a20aca8260dc9c64b9cbbd68efd44295d38a9e1d628044035b9168d3917a2a9787ccc128f69eb187650e96204d0a3d07b46296ce545b38d9aaf961db526e71786b001de7f858f9ce2a908c6909ba372ce70ddea693bd40d406de1f882fa8c1e198a5668ba8555f6d2cd1c31d301e6df2729c2d76d90cf52b3f02627b40e896f4123d7ee3fca5f4cba40e15179b012d2d4ea096ca32ae9ba7d71f851ae642e6b7c158fc519078b8299bb86b847933a06407d71ce3d95b25f8421c4097f245a7467a3cc1be80e78bf3a02092a17481f8c8928cbcaee3dcf3e9e4a0f60fc11eda9f33fa4740c11b93f2f7477b5a608065503beaf0d0563adc30a3abe121c0793f7a40c419f93181065f36cce1b90d224c89f407598d24d5634657b5d0078bf88f6c418c554d9c5c6f57969b3a796a324342120eb3e58c7380c9758a63a6c78c2caa36d978408ed3340ff355a96ebccc86976364b191189fa42a73f708b03f693d3da2c492881b418c4c520eeca3a06d557e7958038b642d4510f8c645502659f60da24c8f61b340321a0ff338ec7f9fa465f7dcbda2d6dfc5e51349b490a00026b25955da05cc6c7716d998b9443a5d91d43dc1f00ccedcf60277890d933e2044e25e78288d785f76c32010763a7a65b43a9feefe45b1626f1d127e4ce262d6280f923200d19891bd2dcc2e18b82a560e0e1e9145c62fba37ca1a6c859b4600807cd514480587670f8d1483f80d9ff4347b7394cf4a746e31edae441b9207523d459006aef13b4c9fe292ade4cd5c47e049248d9079da96140ffe6b28dd637a8f9755e55b36919a8c2aec1af0402d6ba3a2d612b7d11d89217a96125bd8e45acc287830054d270d8d72855dfa737c2f484f85493d0d138cf6d137e949f661b6ec0613baf9c44f4846b9d39458da0d401d68f987a5da1c73aadee7c54a51baf262c36fc465801f45c711be6e70c550c021285593e467c055cdc7285003a4f1ae31e3a774484d662ee3eaf51933a6e1dc73e00aa4221d974c547231061da3f377832bdc5c8435f09b0577098d13f38c30cc6fdd8961526d7b8ab5e5c840e2025c624b352961e43af6f95fb7059dbc5122fbf3b0540fd0decf166b2db1e34ef25ce6da424085813630800d6e7c400964c2fac04d19ef57826a35417f5e2c02ed41fe5b94d0145e390aa29d4500144b36d2c6595effc00e79dd33a373046a6d8a2954057083657742f4ae91a40feb1ad0f7e1f6bb0e7d3d6dad2d0749bc7b41f7b9900a8571101e70e250edd81c18e174b42468b866ff3df02e8977a48675e68277a604220c323b260e3d6fbb7da02ec8bf6e6fa36a846bdc22b8a27ff26721d75fe25378052e4f35cd6e41e548b6644ea93888ff06d873cbc2900fb3699332a8b7e45eba62f472325b1ffe98841c107908542607be8fbf495ec622a4154df47fff92701a4a400fe9b5a210f252b4f7afb008c06e88ad64c86e7725d00a7d0ec8efdf1975bb54778bb066a5cc29b92a2675b8080cafc1db6f4fb2568a298096cd53769a0ee0a692a00aab3529869c4432ce2151b5f6f966682e622e2d623c0fca4ed1bd3745052997b46f85ed1f495d14bab8c03003d6718784184184448f665475fa6e4654acdd45a25607fa0f826d31ffa6b408449ac40cd5426a695b3c922603f7c69af60b75433f2f56cacc7a9b6d7a3d0525bb613601f8aaee0be4d9a35b4ca86cd8afb6d37b95272008aea57bd6ccc8ab0b33c452ef3f97a80e54a9f5529054065210f950b74c07a1ae76715f59ae5d0bab14351036a15bcb860a00b5407af2e41567626b9d39de8218f05e89fe048abe913878241784a1725c93c9227f1c78a6a3fca2d59872a9392aec0c72708bbd6914f7d212d8f01fdd390e1b31c183de875c393eae56ce59f773c8d33fd511ec8c234093f19c2edaf6d58c648bac1a3a7dc06907f47e30c435e2dddf282a6ac50cb654569412203a03e61fae5e9685cf15f653193893cb1ab92de37a404d03b6212e1bbb58b924cb1c2b1389ebac0db4493e601f6675416ac7b6f2022e50ecd3441dacab434858d2a802a127af6ead58b83225bf0d98525e1aee9db10975d00fb65028fae283beaadb64434162f8fdfaa32054109200ba25743009f4e6c26d132e924ec81df847ea5330ef87d42a8db184cab599ecc8f06198a5c4521d4298e03d69733158618691c7106a681d037aea524182d538d00549fddf730ddc7dddf7d2d724520b12718d2264688010eaa804f6454a5e98b612621f518359b5474304c6a80c032c3083ff1e7539310633f6e5442216f77fc2628c0efdfd1f978b0bf34f482ed73f3359b48015ce19d28c0871511f44d3052caf574b534c7005ce3d108992486db0f9ce9488cff4d9ce90f835d00ced03ef4de32edd71f22aad7ef74834fb5fdc1a0e1dd40b09060e0d020281838887fe8050fe04c873f8fe3b1fccdf8c05f809b6c8aff4eee7eeadbfe3b7904c36dcb43e4fd33bfa3547fb0fc6ab7d129e137f26b34b48dfbea3841eee6d8c37d5ae816b7f7e6c45dd230089c0c127f4ca2d74887cc2f20e414cebb518cd85f647aeef19c89503464f1a9ae712955b83bea9ec4ec6c9ec9bee6a06823131ab2e0cc689fe78cc8254396c06d3989a45a41a16ae109539bd7856d301d7ed3c7191704dbec8598e3fd2269b62f1083d0e4941b0b7e060bc7262a53df69d6efeb9fa1d0734ccdee9df479a8337e75b1f615b87abcedb5ba142486fe96dd2e1afe1997566f5ea5e3a1118d795b8af0ed560e891611eab0d7ea8de9ee5567236e09b03c7dcc3804d2a8cc50e6d5ed7589e04ae1fe38f3aca4974feff420bb60f4a85af9466b617ae168b4371b85e73bfb2fab3e61ded2274ca39aad901e38de745e7e68075d8078232056340f69d2f5e0ff567fc7f6bc1144f10e21eedbc3949b27e27752acd05bd436e7bb114488dc4a93dd0b372b0afc0c79ad0614e342d531adc45326b84878c7874571c003fecc98ca3a0709efa81f9b6ce815dd7bf40dc39c2977e4853f344bff3e9cbf7d202cb5d23f390c04636ae2626d6669fe8fde0f6b676d61cd68ec64fd879114d005b85638ea472feec580ff04dafe5592e9e7924cd03532393acccfeae2612d5d6e4646cb5a6a86ff38b8f1efc3d90eba80980c13e392d3adf801c3fc6984bc66ea61430bf18b9cd5dfd4f87241bcb1b64a58264fb7e9883c71357a8d8a945dbe4f06ed5905948c66f2c896da6ea74827b7ad031c64d46f0862fed6dd1fafe8fabb50f48f9e0521b6bb8d77e369b29b635df7702fdc47cd00bae7fb40f7f89def2316052dfe0f8fee42fc939f0f2b6fee6a2c6ffcd4f61fbc9fd3dac9e2e9636666657b35312b59cd67c65ab6ca52e656c672ea4aaa4adc96da8ebc26e23abc8e6c1ada5aca1a1256f69c6a72cfd8b9ffe78cafb091576b1d2193899d3364d7d49a00975e6259d08174f287f210c4f7f89b5ae59486ac0daf0667df7eb1d746c134eac7e98cb4bdf35370b1839427f0cbf159e4f3b92f38ddde1710fc68203ee4a6364bf05c36209868d2bd414c5597530f70c05f86eaed9be12de37431232d6db96556cea7f963b657f250d714b27db0fff330026a8b5f01ade52c2f73e8f05c033fb4eea9d23f7ad3e879d3f1f5773ddcb5a501fb3788ecffd90f905c0437fc42ead608a179d860df369ece0922d517323a233cef43fe6d30e9314a473be85c2b02fc1d885fce3941e13f89536d4d654cfe9e5034c08c974e7c0155a5c4b7832e20b17fb69690907f82b5fc652122796989138b28219cf171d144e41479a4d25c3abdddb54f66c0679cf613b4d3723ddd766289d08e9a0077fd2ed312e9c6df632d0960a925ada4d78c8239f0e6f6c8dcaacbd73078ffb296ffb3ada5b4838db9a9abb9d983ada5aa2497b51987999aad03b79ca39535a7860b87baad83bd8eabbd9985898ab607afbb8d369bab9c12bbbb8aec7fdff801364a0923fbe23d071ce4d55b337aaf6a2a50f06b6faa7f7f3be2ea7dd656507b9cbc0c17d73ba6ab9bd0c3faac6a8878f2c055c38af559012457449ab028ef35279b319fdaccdae03c931fc6ec3b6c76ac262b42683485fe97cb7c09df98efaf7831168ea2597a72f60b1b2b899ac5ac6bfbcfad64dfceacb3f67f6e459789352735fab9153e88a775eb1706db47bee6693cbfbced91c4e8a7885fde8692ac898fbbf8732b1a4bfa37a15f987c31229688067ee10226ce4b23eb1afbe52b461015fa7fe903614d300fc9cf3cb1d0ca01c1ef9f47068f9de036428bfd4f63890ddcccd5610f527b3a4ee3a2f4fca983a96fec0efe03f7f67f6520fef3adc6df9d81f8f7fcce7f460662cb2be2441d18e309f92e2d0feddd4c14e63b32bd64c4dcb51e3e3f3caf5edd6e0b86e8374b47443e31f4f11773fa2e6622e38a3b1f571122d59a1003ca8a4f8ffeac0cdb96163852245d93fa352f2748fc62189de28f391ffed2ef7f3dfdf62d4fc37ba944514a8e8e5fd8d1e9cd37620afe874b632193de029dc3a4d543f43b3290cc9d627a9a3d0897fdd47ee3c3c4f107705af972d95acfd432f34603b7652e37cd82006c21814a61b8e79631fafed4b7e739abb897a22ad233a4bd173ee70fcab0bd0095060493c64e3482de568a42ca3e703d79a8af06e047e49736b933d19abb2efff2385084bb7e62cb99c4f49efbef293ae6b7f4405d590591ab3adc37a7ddc4b722564dce4f425a9d39f925f6832a8b917d262dab02e65f7b982278c7dfe708c7b847aefcef830cd2000373036452380844c8254d08a93852f31b2f0cc1c619b4f8d18381b965fef83707d7359fc7bd2d278293e9b4eb0a2a9737daa8d7fec403398f5ae4422e9d0c8a061c198f1ebecd0ee9197820b4384f064cc04bdbe60f39d1341548297a35ee4494ded067484c699ac7556d8fcbc024a78ffd82f8a70b0958eed3e5bfd4f52f75fd13d53573bd8ad6bcdc160a3601443cad6e43d49179bc20ce10860b13f32522d227639e7d3ae8e589359293aab0469e3ce8b40bd1f80c5f1ac970589bd09b870b0b59eabefd2d6828e03f7c1c2149a93ef9eda0f19f8a5d86eca4aac2fc8d2f010e47c125eb2fe7fc1f3be70f5c87bd331243523f124632f38b59f7d4bca2ff4d9dc6bf0d673be84b38448cdfdbaeffaca118007041c19a500433e5e2fd37b9a0e42926e19c616829deab0622d345243b9c3e4feff90ca9931b45d6287fdb1dfaf5ef714125d8e7512c12728dbe815cc8b71ba4d5f1197b9af9970beaff1187bd361787a8e43349532f4b5b3733050e3579b967aa5ef26622165a8eecae2aa22e4a96f21e4fb5e5bd246c9c78fe855c50a1c9aa354565fc62445162dab08d5744ad9ff91af7fb7da4b738e5896776a6d9c1e33c15875317ed627d7905bf623af12f98fd7fecbd0554555bd73f7c38b47448a8742922d2290dd2dd9d87eeeeee569090964640014929054949414010944652baf9c6bdf709f6f5ea79147d9ffb7eff778fe170b0e7596baf9c73ad19bf395f7d7a2ab4eb3724ab8c7eb0230a5d05b52fed8b18808e6d5523bcb27e1fe662ec15e6e877d55320b7f46dd72f9441dd87820d5fa8a9c82d4b889963bf503dfda5528cacb5493811f38b1a7a6b0417bff82db99878ac36dd17caabf514a57cd23fbfc57f7d2f3a5fe90bb5da47c9c6eda43fbf4567fa902124f585ba4ed70b13c7f28b367cb41034c9fcf35b3caef6dde70c7f7e4b28e6cbb5fb45bd146c4baab05ff68daf2369dd16f8f65fa0a10f897e3a0ee945ab541416635d51fbdd260fb7f7852564f6e26208ee0bb58229b55103fa988c6f0a423d51de8ee99d8647cd5d7597eebcdf48c4d7ead0d0b96273e61bdea093ec32db9a861c16d83ff83d6208a1696492d8a505dbb588478b459cba3a8d38f7c6df5b0cc181fe1f17438872a6fad68e3656a00b8b21351177291b53214143332535262b2621c7bb32ea4c8aa6aefa922cd61a5692ec56ee2a66ac46e69caee6322ebf48ccfcc0a913daa919dabd4a90c195cdc888c388451fc262c8c0cecaa20fe1e464353660306467626165663266616360d66736d6d767d4373436303632366061646383b0337018711ab330eb1b185da8ff0810672b2e46865fb63e04f027d8de87bf7e3adc8dd01af53c38a25919b7f9aeed5a1aed43775addf6808776529986975ce3898d6c8f5c8b04358ccd2b898ddcefe16ff927d0090b6667db735e85ae76bae51b8880c1fcb2164ce9c939c070b109831aa0f8db9d11f31ffffff3df3f9fdfde03d22485f07307ed6ee12dd4ddab23c96b39f21a4256168d5740f788226928120e4cc55725b166a2edd5d0c5fab895adf8ce49b04b43311404aa51d82b3d49ae6c39a57161d33db6ebf6204cccaec9b04a1475f073f25eb42cf23772b1bd8bbc404a5ea2b4b396b2234217f6012707ad5028ed046b0c9df0b1b661d7d506e5a6c27065b19e1de65403e40709945bc75626393d594f9771fd3cf09d43f20889525763c6029f140febcd2cf5b492971a2167c0d1943f0da4aee382e13543fe71456014ec6f1d0081e001acd1ab580c8d748b7c701d335c3d07f2bad231ceaef5cf9eb0b45a4a6d54e4a8e57896f1efe36e349284042502b26e222e3abda6268aa9f65aaba03a6959c577e4cfbbfdad966012da32ff154bff69d2579ff27429aefea70af47f9f110ee0187d0370774b02c832d16be128cb2620392098ba20b2184a14697ef71c214fc3d5bb079a78d40f4fe9a858909b20cd08df86f2a7b681a9c6ab10edb9a1c6cf7f9720a5796721c7e166582cf96beb4681872dadca74320fa0cc2cb4d07c686edbfc1774dbd6bb20abf6fd2f8b62fe8b59707e5c94717011dc2b25bc5cf68db2f04a0e1c84d044c5c62bbff0f7540345e4d7c8d6b41aa5ccab19b6e36a70d7af39773e326cbfeea47d277f55a87ef3ce2bd4c81debb8645e541d9742b7e5c91a297306cc235eee24f93d1c72e79834d160ee68cfee47a9433dc393ecc9a6d4233efe287bd3119d35d85c4fe6f92dde8fe917ead0c2ae5805d82a4fbdac6f15161c8af0bab5b070f370e1b1a0aecf1946516a74b79106661e0667f6a656fc4b131d2dc57e03e5670fd7c5d5ae637b96a43fb67d47715bd43a481969b67ee35958904e4ee1baac5f3a5e02346d1f82008a7d84e659776a28d3f69ed6ee9596377a3373a2727be2e92fa87983b6cdd5d96da4b9d6df69622911964994e019aff9ad478a7c78769bd43ab611d3241134c9ab76c5532a7b0b1443f98073aa785c69c3b78cad9d2e587581ffe46dfb02bfbe07f747f00db430f98e728fbbd403bed9e6ded2b07622f755b3d85a324d830338656db8ee5c4b836dcf9d630936353138aaf71c7b78e9a554af177ab65052b81f07b9ebb06fcb3ed54e5fea9e74bbc71b2ff83dc46c6b6ba24c34f210de6b52c72ccd07302bbe0144f622b530b3f4ec7e972fb61e1790ceff3549ac1ee16780e82022125aa36da12db3c2a4ca7b9ecee2e00d3a641f7108caa3b92d8a756b775a3d72fc7b16efdcc8cb1cd993298b1b1a975f88e179153dd1da02841e0afec57c7d5d5fb7f9a047f71b1f43fe5c60680793f53dfdffe2c1598bff16f9ecececec82db192abf021fa0aa87c23ccaf669239e8afac38b3fb4bb1655b81aa336f762730f06d5c494cb19cbeec0d52f121a8e8da2cc1dd41cd27f6cc917997aabf3ea3e5d542194b6ff8f7ae93f799df9d4a60a836c36ee03e5b2136eb9291bf157e7f69fc3f56dddda2ff7d2477670b33230b3713073f8b1f218e64e56b6107b7a88aba1a9bef5777bfb4393c3c0f983b6f7a0d1bbaf1fcd2c359fe6d1a31b9ef151d0ad9c2a6b0c7d8facbf71fb15ba4c95251f04b776163cfe525ddf5fdae082e62cb8fe19b4a7bf707ec1dff3e3efe26d7f2c50e15e8814fe8f9f872ae749b7bfbe05ff6a8f406b23b43d58b3e974b457a8be5d3f979cfc681d9b1639dd0f005da14c91a44e5d845da736a1d18f365198e03a42eefda37bfc8f67fcc3f74cd1f7af51e079175afffee2c8058d071a41ebbfa599b1d9cf5aa349bca69c8bfc700b314d0b468ca36e7da99b1b80d0d3362b7c8f77e99fc768c2c48e472de98449bb6c23a1543f4b1cf410fe427c18aafae4e7aaffe8ea1a1c6ed4c8d05d6274010ba07b3af47fa0b4fac5eaa3d6188bc922682a362874b4af362e6b35399756f7ab630c9399a9237e2feb42bcdad80902ffb3ee2cf52f0dbd0e8d92339c1e5772d9432a9fc5d1f32fff77d730d4f9bd80f20d6c346f5129f7c3eb0706738cd1faebdb03264b52a2099ef2f07b5a046dfd0bf6186d0726bc9f5adfdfb2953ec0d76e247d4375b1f1852aa729bf470668e4eeedcbf4cf8d5f92086a89f7ceb25064bd3df44b650c128ddc0579b0b0b3a819d1d7f727975d5482d1e1c5e4e08f3de7c3950ee02e87c2dfc31e925a78e61488b21f05aa8583ab656087f971de0d138b17d4ab0c5d5ff4ad077b134a7968faa81fbe83c04705291790b138fda8be0a3e62733e3a18dae7c9a0f055606a4b6feab4c54dd6d38e5cbc286402c5eb5b29939a507ca561a09d8f482f285ba1e9b3702fb84471bfbfcf3fb5fcdffd118081810181ccb39a74b5ff1c54ef1b1c3dbf19017fc10ff4072368f707d8820272b559027fbb501dc00efed3089dabcdf28b02970d630cf97af1c588ea3c4ee53f2190311d98549bbf7bbf2d6196d01c215441b5ca5fae7d8805b3c4b6f090fd90ea4ae4ce3798ef17066871112e8ebd5c2e6ab1c5584bdf3581247b417283ffc913e85fd0ffcf0feadb0faa82be81811ba9aabea525c4f107ca9f0bc57352b76015b1605591311284d8bab988a82b73a8885a9868d809b3b14b09dabb7028188b2899081a336bd8fc8d76794f6e1fd90d22d74071f8c80a1757599b866209efe34e03078e89f832329df10c6bad8417295333d7523d74cbfda76bd39b162a6a84c96d0ba5048c6712d85f555e83aa654490f70d30f0d60cf0440efa1bc68a3dee26307eef33c2a81342a24b67de44d6af90ed77c153fdffc5d2fcf2e7fb6369d047c2a7337dd10c1ff4a533e2a933716ffad37d35b1ffcf8815bbec37f19226657fd4705f09a50c86b40df7060dcca9e8e5768171329414beed51d953759ebacf92bcc286d5cee938e21f362092e6bbf73e92ec6fb7de1a476278feb362c58861e5152af70da8f8c31d0937afda4f07c41674fcdffafeffdffa163406a907f428b08289e110ec3905d4af9b457e2d9f080c9c0ed1cae37b99175adf432e0191abf5ea93d32b14713a707a46b0052f58e6929fec1ab9d8ea7a7f421a96bde21b64ec623127923ef84691b87197abf3833157eaf17c22f9dca00056f8ab0ba5383c802bf60d202bb1ac85cba1baed016dbd569fffc3f6ca035b77706c71446c855d7af45c39261b52c0b7cff9434851380efe5628b51144a5092a638a0d2e730872a407f7d5ad18ce0e8809e20ca6a1d12fa6a5847d76fe2f8e493b6c5977617798b76e22b9c4134b67e2d8f27ffccca509542df2b950439a8f834d814f2bc0e48e9b8fb4343537c8e6bbfb0c9d07a05d880e9eff5197b18fa5e931fbd244db5f4d240183e0d5c793b688fc8d8fc3192630cbfff8ea43a895dc7efc0de0129287048c14fb5f6f5f86552d844fe162fb1faa810877e5453abe17a5f295fc558bb65315eb2e7bcb8ec2efe03fd7bb3017fd750b3f8f23c8f7881522bb258eefe96bffd5bbafaeda8aee404c077c403c6ab7f8e47b62ba7907946415bd7b6df7a3319e36020078a12539f9be79fefdc9bc2023fe2972063c14021b68aca2f84126005bd637082587dbf1771485ff29b02e95272f43f3c4c4061a0ccc3e8f07f97f58bf89f4785e5accaa689bdf6f67d68c5ce0b004d532f3657fd8c0c28ad6a2e8d4f7dc79d5692f87d817525a77cfc14424eebdbcceed4468f57f77debff59df727c6fe985a40846dec186dac649484c424add55cad1cd9259c3414ad0cd90425c5c414580c5421f6220216726ec67fa33b6fc5736b8865a74ba3c49320460dba5cddbb0c0bc508f1be45e283ad7e3ae19b66e406a822439fe62dc66d9f6473c409e9d0e11d7ab15ae42af45f36e56cbfac760d2a58177830e4b2257ffb19acdc3fa3fa7edbee30ae21308675d6c1fb0cff8af583310ae98aeb85761f4e9b3118bb5578c2c94c7ef708cccbf224a2dd77a13f206c9867ea80ba577e0e60a56abe851756dd384835265d5b5ce0d3deffb6ad30154afdbe173b9f8010351815cd15d97f38ba4b807f58612616b3630ebde65dea448c0fbce4288d7c64d3c963d61cd2e73ed1eb716a188b88f8bb153366628612588f4ab2e85e3e97ed528457c3887243b73c78a803ddc91b6ef3bc66f1d784b6dc2253ed2b97102acd9e8c10e1ca013f96d37ce319aac3ee8cbf5653f2b9164e079946a8039df8de56e5210a275d07def4f770598281f640132adce9bd7b551a9cd3830be6bd327e7f6f2efbff7c68cbcf03f992b35570356475771316325457113377d1771533707656345614911776611412545033959351b03134507317fd8957d00b6ae97e42680b0733132b13848199818315c2c6c4c9c866c86e680c6133603482707070400c380d201ccc2c9cec064c2c10234343430e564376564e63237d76560883be01e442fdffe5a12d4f23246d6418cbcdd1296091ac50986e3cf7c94e8b9fe31b4b7fd11de6e579bd54a458bb5ea86bf1ec7eb499f9dcfacccbb392a316aba9b466ee25a35c30253e07d490fffd58df009afc82dffed50492982450432970f75bc48ccb9580193de3a9c5b9ebe4dcf4f9e496a4db504551d2a5951ea84a0df001f667df80cbf4c60164c78f6bb167ff084b4080c9f661931d2fe53cd0a71d0e3dc255d7cd4315fa2873aad3d5d05a8df5cd2a9d1961d6953e7d685b6bb8f2900309d501c2bd34539ab248466c73c34e96f2f660ec4435944641390c624159e6d82018988b00ca63925e709bf95eec9e8ef1e1bfc526fe2aace0e30bc90c04ed4f838be9338afefb0d820d35ee70df43fff24179fb4d32ea35938b751f990930da722844d1d15bc4649d6944b579c2d25c53d8b65f2b09bfcf92c1cf7e1703caf04211c32850d82c4a66314c63063c87975f24a51b3565e134998104ca9365d7b714085754b0d94234a19c13907dbf677e0f98efefb52cda0a4775a825333cc27f394c17d8faa3238b3e1ce01e974b1a79c1c32bdcc5ca4373738085b23f11bf6b7fa1d019a62a72e06c17a5b9b277ed522bf6279e055d707f665e50916e445163fffe7bf6a095c770f34bc4ee2c761f35b8f0dcfd573d716c38dffac05497b0faffd499efbf30bed0f8f71728c9585858d84a645d56ad0a83618c82be99fb81b490bfcbf87d6fdb2e3e7e7050e8b01d88ab146ee7df5471955ab2b2b1bdb5facc1d56274cd5f79107b3ee7bd600342524b4fa7fe6fa474f8d4218b0be197f41110dcd15ebafa248c0ca203037084c0582190765762e6889fdeb5cfc758e0a5302029d823211e79abf86dbf00e044a078164bf7db954bbb964b566f8620e86b69cb6c382aa41385c3aad59a0812210a290dcfa57d3f7f5eae0473917983fd853ff561d2808f7bea819d12ef9a7cf050da84f86505ae5950a7db49ca5ea710f8c5342033cb3c451bbf05bf0100560fe325c018efe8a9734f4f5fc8dde8340a0886ff61ea95591242daaf68fde9354f4598b7d5e2803e32c548d7a4921def119f7f8abde5facb99b8716c6dfa377ff422cc36320f6946db935ebff257320b590fc500b956d187f783ef38b5816f8579d4ffe33fe05f59afb53afe977c45395e8d67538b03e12d22da04536f36831ab5f5076fc5a35004c832e34e5e22f6edf05cb4353a35d544d02356c7dffdc3881ff6a4cd39e44de2b3d9c434945373348e3b3cc41cdae383d4fbfc96f26efa6eaa0f901750da3a434f491214421062081998d61957d967122d810e555966ea72b131d7f61f483b6eebef7def6e77549c47fb2b44a2226981eb39f87c98ea87b9c87cff91fee0fffe9d7a54b72286f0f3e606aeb7338d14c3a6de4eff189d7bcd154e64eeb55a7dec694557a745c21516b9f21288f285a5b8152192043e3e30666eeb97c3963c589e39a1f22438161542bc50c7fd104685dca9c0af15567829f16ce531cef2d08c5637c3bff3caf15ed2da3f816642b4feb3e84648121cd2bd7fdd940bab872fd6955afaefdd2fd8fc052421715ab761afc7927c653433b114e87412d45e4e3a95d395de47daa6c6761d58e5244cca6f422e68826f7d4f671f0a8d0635e3ab0e7c261e8903ea7b977ad0f7debb24c37cab0610238abc13729b957796f5a9553787f925c818db1812e91c66a4ac91f2c5281745cae464c026bdd5f9a7d75f552aec9f09d937b49446964e3d91f72b3653fa874043a3ec7c783712bf013e3be838374f40295a048a8c2c43c33b77519a2cf8a731cc87f18ebf8f2d3736919412b0b15efd8318b392fb05929b4fe99444b187dca6f97fc6c794be34a17b76627b1e64a3d8de29d2bc6918f44e1d471d0f0373eb529df3d42d51a6fbb45a9760f768eb5474f63fdc8dca16727fb8144ac3d229f195fd1e221b7e5d0ee7463c6b30971b1c9681fcdb6bbe8b9db6df09897480d6e1b73387654c96fee5826a149842f1bfa32f353d6b08e67e20932b9b56ab9575ecd1fe047a872d5ceadc8a9bd828acf6fa59ac83a487f53e401f7826627995d746c375bd8ee03160cab44be29b2e8912df2db73cfacd879c9176e5077a08afd01ef414272e7b77f35c2f801daff5e10f0bd73205843451058431b04926d07b13e76833d6d36ffd678185bd28637830fe05c4360b122a354ec8c7e8fdc267ed0540b679423950a2d799cde05d90de071b8019ab56f1e4dea25bf257fac3f8237fea1bcfdcfbf397eea9bf3b6e5619cd2bbf4da7a4ca9916ff78d6efa82d77d01e68f8416bcd61589411a485a18ef836f1ae39a984853724a2c81c28b1718ae8e2a50c505d851ea21365d23ee458dbd22de28ef1ef638583afbe0f892642242c4936bff5dad39cc058f13301721431f7fe8e2ee648e435f46b7fd69e0eb7902698c47922967079c0f6a8fab9b65064b143bc91a2e785d8642a7008c0f78dd0d6d24998bf524d0139ef77d693c1abc72c245d6d77fb03e05ac40e0659027dc93e475ff02869286febd15af07c3b74e14e588d0ca6e81eaad5015f49230df24f078b35123abdc08e0ce7ffdd99ab0d41ddbc946f4e1816c83f1d50b5a15403b498a2f243ca73609f113fd1748e033f7bb16220f24af6f1511d5df0d82431585d56bf1c755a71ed472e1e8774ab51b35062bd08ef6875051c78d86d5313e828e4b7980f045d624849f0059fb6540b39b111e1797abd850fac8de234fbf3eb999a60a8bebc7f7b3a47377991ad30f788af2ba4b791a173d6ce411e7587ad4bf7192ffd2a0bad39bd1861baab5661672f278efa6563c2aaf52ca775c247e606b5cd4a00a4f0afa7b3fff8bdc56243994e5996cdc5d0d9855f51535041d0d2d5ca5259d5519455494584d0cd4a484151494ee5a5838580ba9fca29be80fdc04f82f56fe3fc2126466666662851873b232b31a1aeb331832b33333b3e81b3070b07332e9331818b072e81b32e9331831331b4118d858f521cc06069c460c4c868c8c9c9c0c6c173bc95ed0e0fa13facfc260c4c4cca9cfcec8c2ca68c00931326065346464656266666563d4376062608618b1b333183130ea73e87372e8b37018b073b273723041d8d9f5390c0c18feabfd872a1a4c85cc1f868cfb5812258b444dea63dad23219c95ee7538d3df22aa8dc507990eef29893f67ea38492315bcb1ca13562e9660d43d648d5bdbdbc9949f0a2c7474be86e4fe75d9cfe851eff1fb93819acccdf9dd412bf4698d403f9683ce419acf8b4f1e5547071e1e6ecb345995d8005422dda257bfaf27b0dafd80e8ad91a0e791065c8e4058f8dd0d60602239baa92f48f279c142897509678a1f1a14b4a07a62c097691edf50da497d2ba79060e4e9ccca337b85439059d16bd5a6d5f7e98d64abbba10cbd1dfe1f7fc46953ce589b0f2049e264106f4b1ffed96f1affc887f362a70511c0811e0227d6aa5c432dfab76438def4efc2ac613f9dceecebc5c536033cc019c53c865a9873e711ba040ee875da05a38a3163525ef1fd688fda3ae5743218b7197b1f8700a449fddbde259b3313c18e9983391448efa4aa6667448048a4500b0cf215c62a8b3a1eeb17e9cee8d2e5dbbe4577964a5bec14363f182ce50a1f059286b21a7ec87d9fce5c69749fe11efa18cdfa51f3e67fc637ca194f7bdd839049a988716787fe1c07adc7e943a62eaa0cfaecfa55147a794b1aaafab6d687cc75ebc70e0f845cb5fb4fdffe5f2bffc69de97f20d24ea8180be044365d26de74db094ba79c2cadb95f6307f3c72ddc1f29b8b39874808bcafe0fb6f48f62feb5c5ec400a75c0307df4c0fb7cd9f63f444d8a5fba6520a1627ed69f3cfae14a65673a3193c14025765f811ee4f50fa3ffb53c13efcbf7f4a326f6ae1e99f31ece10e24e69f7c816c0f6b63b88514fae7b788aa81b485d7fefc16ccf688aab3e68b7a6595df561b7df1dbf4adc1883f61ee8f86c0b4352918f2830261d6d51302614e34f90261e497779af7237c0372b5596a02f53d3201a2f82fec5f825feb3dda87aa8afc3ff9647ae59275371a2ca68ee820c7b547449abed435837ac9d8eff60d204b24ab69458a5f8516ccc87fc1cc46bf3c73d101ea6bdf80ab279f6b515b85490e922ea8c3028cae9ec76b75b122a6cb02a02c8b450c8a2b6d696c00549dde5752eb5563be901cc377e1ad736b496ff3290eced39943e07575c98ce2fbb3a76241cd2378af9d6501c33dcd4dd5c7e8133a0aaa587ff0c9dcf99d6d4b22cd797ad4e37b45d8eb74ce9bf5ef7448d2c7ea0bbdb9b901e5fb19f0af0cb3998a1c595a4bdca9df807f6609807d1ee0739c839cb90eccb04fc35c59c13ec6150e025855f5c5b76de81fb0ac1b6e705dbbe68b53a779778ee43c5d7b7d8757692eb941d72a3eaceb31c51af74020607cea604db4a44a5ac813b97695f933e4c9f656bb002e4f53ce050ab660f7465c8b7e9d4194e78b5ee4a5c2e7e95ae0db7dbcc80d7e16951bb2749b9f4e124ecd001c31342932f954b93b8d62ba0d4e3a57ae553b21eed179ba0bb68bc9addd6487c9838c7849d89110933bb8403f511fa9ebb62ce19feb178eab6b037b72f0f1ccc3ce9369488ae9cf1ca787e94e7a46ad3a23046a9e9300524589dfb42fa1cb7d13924bffc61285e192e48b206e8af3744f58eb643ffe6b4d38dbcd04ad70ec453e57a4a3011cdfc1b33b87b8ebc863a42d4fc5538e3081b09df63c7d2c3eaf86669b0af71a97ec13a4290a1531b1a4a6f3f4eb242ba7511628d9697239acf604f75751f8d7f3cfd33b613aeb71aa3692359e91624da0983c908d6d03c4e6217565cc22cba723e2776748cc1f8d5fe1291502c4d1c919af123fd6fc541a75d8b4b596610a5e19bf72749eeef7a0affd127547a1d3abc729524ff7dff1a610011099f214c37a6e3cb70b62a95a5cdd4bd9195fbd8a0f7027e9444fdaaa40d227edf954fb5165170724ac100bf87ef876c5e724fbcf740cdcc8bd3bc209b64c074580034ab507629a68129f14fb8907acf515e1ab05e6c100645c760861c56ddc7b308efa792b0e14cb0f140dd80126d7e454141f6f986a8aac74f3a4103d843d9e3b7d80bb48a560762ae6f58e20b2faceb80713aa48774b0e01c84a1b0d3b3d3a8154c55c6b83af650ae5548b1543d2cfd339b0c9b8bc8c8e1f1cbf33598a4cd9331c54a600e4acaf7b5bad691eda0cf72a13f96a9da6f63c2d11070035ac2c38b8612dd21991f2b6f9d93dd094d2222121c179facbb0ed4726a884e810a9f21e5331b3ecb96816c0015392cd698f1a1ccb93b259377e28579075870407e012ca4ce964c1b9ff09c72d367b93c6d71f51c2aa030fb0fed765a91787689685ab62d1ef4b76f874bdef723e4f57a6ed1e2cc8c57aedf7fa151957a91a9951592f607e22e2caa997e25cb53356191ecdc4b8ce71983b1d9fa7abbf75b64b22a77d9d4f9ee48d594610ebe77d407e9e9ec86f726449f18ad70b4270ffb6c57edec4b0ad1dc0f4eaf29933b8a1c562b3b117a7a781714029a71ab0be36d81aeb5c706e6dc693355e8fb7af5ecb902a173f4fa7e5cda29be9d41426b5d42bae0509d9119d3d045c00abb3214e0cac9aa36791f91c9864d7622a8c9f2f01e6af0599d2213975f35e8a960cfd5ce372e6480840c59c7d1c9195869936083120125d13bddd5c6303ccf7d6f09434dd00216758bcaea43bcccc896f42ae68f83cddb282f3051e7681c8ab55f3d0312644940fcf9334cfd3731fdf6e9b0ffdd4fd067ce0f0d6b2fca41e240b0892a036574d5c849d9b4e2e4078924b523e8ade420870397f5e60e817e4471306238fb7fa211e0dbe8ea503106f55f1396d7be51097fca679fc630ac483f078ab4ac005b17017a7e299daa26b2e5378a69e6c8f45747820003889a5c234b0493edee3eebe11fee8c2e90b86fbb900787f5f65b7e16530f7e3ced7fb071a8fc5d5d6e15f01500d45e5f8efbbcd354958e63ce0bae4b3362fda870fb8f9a3749ded3e5335abe4991130db082f271dac3dc03c4fe7eb5790402a4678b2ac74786030796359f4d212c0bcbf55a7cc7859843eb127c0cdd2b78a0f8d533d01a06c7970c83b76c9e5005539ec317630fc72c3a3c2b2aaf3742965c7943dc3727d5be296b6dd9cdbc2c2ab3300d33ffba31067897c30b9683e96905af783caee875caee7e905ddd4cf786361ab3c216bf49c1e140f6c573801c928c5ca603d90a9f8d6f0360e7106831afce90b5200f2f1488349b86337d0355c443cdf8a6aa9fce0593160fff92d7bb7f255ede0b1e587fb8a8d5bd4bdadbd7cfd3cbd6f28fbe5448f1a269b65841dc1786483fbdc2740e4d5636e790bdffd354dcda87c3f7b8a30dc89051340bc3ed393678d5d55ecb0731d11fc580ff57ae9c529f501e78bbbf897562870aea9600fb61f9ff556a646bf078c2f7dbfc7204a148d608380fbaeff83d6b15def876f00f3ab727af2d6e13de721e486e5911bd7fe71061880af33b69bb048418254924704ca4dcb6ed66bec8c01f0efcf216482ed9733782a17fa9befcffbd7a94701bd3b687b5468cad8871f42e88ac3a6730cd5625a3fec9da71745f43535062a76fa397207891d9a75b8efc7be3a4f1fb9953ab30e1f4a4bd6a50c0e56936ec389cb00c46c2766cb9eb9de1af9588926c836f1960d1fd59a109096a361cf65b690e7b58926dd20bd96f5d8f250be633680bf315555c9721512aec113b1a916ee6bac60d5009073377d9e2d7338d06ee5936cfbf6f5a5de10413a042a38729b5c4368c731d4155da8c69cc369de15c103905f7249c95f0b3e792655255b1d57bd9f3e6978f90e40fe2a7599862c9207bd33d4b382a7a2733ae2adc8046442a06bf49c7a2646f7b62ff9a9352279bffa84d234c0dd3493a97ff06ee2fbb523ea91cdd51708def0792280f3e1cdcbf9f7641f39b9dbbfc373260bce5a0ecc08093f4f7fe1865841895efab0c0de4fe966d41eac68d04dc0dde82a5af83d9ae05a070b58af5dcec90ff78b394b00f27195d40882bf1da71b3e6870ff4e6a24ad7d9c6ff179fa3b0fcfedb9b81bcb33c3f6487056f4f49e77a500e76b1267d679633de572678208d04e99c481ce1b3500669a4a3b77aa6c64651f49d22d6541837b651cdb090044d9fe0f7929da8730917c67262a550ee96be52ad8b1e7e9777b5b77d3ace9c4745f386cdb10cc494667da0a01fa2f976f0c07bbd12c688268cd7a3249c895a116f86d5d28b4e49e80e7cec7c6d814128ce6bfc89f1a8230fce08443caed9fef0324123c9bc10760b510641487d102b937ff528183855f5ef703481d0df777426f1a3d74ca22ad603e3e510846c3dd5efa1eb5e3f7786c82fed19c8ba9c0bf0f73ff3bdb6798dbbab20cc5f2fefa87d5ce3ff07c5ffb2fdd9e9cf5aff876fb0d9e5db4fdcde03721b8010f2b269e8202c03d098197b630587ebe760881f705ff4faf14519728f6a7570a7bbdfae7b7144907b1ebe78fe9b3b74b3fbd52786d899f3ffba0044ea49f5ee96f55fdfc312561defdf995ea18f7ffaeac9c62957763f8a55a58d03db3929fdffeb07e82e68160e43b1faaf1500251f62d4c0361e457b0f7bbff509cb6a60ae2022e7758adfea8a666437cdce2d5cbe21396c86b03748fa1702168a8af5f4f3d01234037d87b1bfbcfafcffef440a3ff3b5b5a86fdc5b2a5dd4a88cda2bad2fcdbdaecf70d309c9d09f0e2a6a869ed93971eb9a0ab01d464e7e7ffe0d9c41b6e197832264bf24cf9a5e9ee36f9c22585ef29ff97b2e20f8dee6ef12fd0e8aac70cc8dcb34dba6d80fb3819d930f9fa9a686ff9793a1155b3bdc298fe95111181c6a46cf3154ab23940f9a90d36bbed072b038f9f53e26d1a16f4c6be7f00184f679ce0a88107c4b8a9eb448727c94a76683c758013e938cfc0ca93624fe4f6dafb95daeda19cc7032f07ced34d4014065d30c1ee2f646beb730a954a25df7c022ceab01692c2b4defcdb2815bdb31a4c3925222fb3000ef38fe83238cc9028d9a2abdb3115d0b83ade35f2be3b4f4f13cf74f286adcd4828bcb2e61276906a0e3308d894c79f517331d2143730e2b98509ebfc560f49b1b600df77c6bfbe151c9bff50e925f94d753da1074b0880c8f90d0526b2554ebb575c0c082cceda7974ae9f4a01df7fd12939f9545a67db66d921066eb0526c62e17ad7793a013fddadfbb4c63ce68ce13055b11e2faa6fca0334ee56ac37044bc995540b9d090a3c1a6890f5d44401f3af2aff70dd46d17d609d4ea3cdc9da227bfeb23920b79f05d3e7b072913e964403ecb0d427dd8f549c79001a976b8536ebf00aae2dfed48a8bee0ad1dafc64a5008d43f250e9f03bbf2c2e07d14b3c05b7536fd66885fb9ca72f41106875040e840cddcb398fb9c67a8bc4ca011a015f1ee3aba3c570979b0f192f05dfc15e32dfcb03cc5f49dd0bbae76c912509ba53222dcde478b9c4b580608181db73c78289bec80ac515fad634a07c1ca476c0f72fbbe332a504ac160dc9c4f0b7c825be41f0580404181edff4bb229d3585c28341197b35d47a73c0f11300dc084f23e344e8e4605a562281e349c7c264c41d5f00340e025e53ff697592bfd9a5ca22c692693466a3308046548afc8441bbf07684b40297371da73e32833a0d402378e4164a46f5f430a254c14cbe48a8bebb618b741da0d1b4b1d3f2311d294c6f50bebcface0a7d806210a01119dd8e96ea66ac3fba9c873b559d9553ff662f0c70e3d6dfdc4d9343a45d4b50d4123c8351750ef479aa05d0a812c11d53bddf0ace7b15fb7a6dd11f6cf5ea55db79ba01ff04ad68c5ddfe0d9767818d68a4e926adcf0137e2a5a1fadbeeb003aca70e22cb8d89f4a99da39fedcfd33179928c14ead0a3e8ac1d63c47c26151e463c03cc3ff7f0096560635383144ddbf807fec40812300ae0eccdfc548b9fc040d9842d573c34da1e6936d407ef1ec02224b8a8c4944f74c0c32eecd6233171a26ecf011011f7155078e1e7babbc93ea65deebe1ed24628cb05d85f6230615e1ff5ec6f10d9371dddaa92d1944aa80a05ec0ffe2d11d976b8329654bbb275789bac5bfc6eab80f5e1227f8b702dbabf69a72b183c4a9609afdbfd02605178bf88f8664689c0fe5e52c88d8e98c986d13ac0fe657438dc8dcde3b71d878b60745be242e21a0b59394f2f86959043b0be3d48dba6232b8237a042a9080740f67077e2d75dba7357732fa12641de77f956897d02803f6632cfa6e58e8011a341adab447726a3399e5d01dccd304e2be2fb441771cc825b33efa5a16f7d805b07e490e07b689ecafb2e839de36694219335262881bd0ab03f4b673f53cdcdf74ba5a2df62b2bd0447ea9f430ed0b8763184a815faa04b13b52e0c199ad778789c0cab02e6bf98af72ae7b718b7cbc83d1716cc1ce68340ca0b13b7c3be50353be3209eb26a6bd3bc42e65bdc10c3854d09ddd18dfba8a6fcff59e66fc83e522ac079cb53540bee184ef765299395d8e2d3a78ea16e0292a8c01b09890516ef58c700566d5b6a2500879b1bc21ab880420ea068f736db0dc28de2c27b6ec699f5d09bc4f530cd088aa379256d882858be844274e6f3ad7ebaa8c9001e85eb294c65215cf2efbd4c4776c166b807515f1001af34be120aa1879662ddf3353d6bbbd91362f561167cfd38569875d649b14ae29f87f7cc889cdfb2cdb7211902be3706c72586eec30ea5e78e8a0d97a38f9d0ae2c20e264bb963020916477360cf1b8bbb2df71883a3104a0b1a3a1bf9b35eb5783531da1b88eb118b1a8d1de03b0f8ed4711e4a02759e0c5eccbd093469721f1620c0302a00722023bdcf784d010d9f04a244df47a3b5b47001a1f9556eb8650920055b4b137fda6e238ba1988e600d7eea727fa06461c92291a9d9fcab08370c71387e8013e4949d62cf05e524d7e9368d6638ea12eab2b261a00df6f06efe2ac7c46a167693d96263ced59f97936333ae7e92bef842bbac99d93e07897387cebbd7924db6fb19ca7b75a0e8f0ee36d5d46a0826fd3edf6b8fa4cfd3ec0621b69b4c153f290fec0f22667dde4117abc84c4110051717aeef14beb3c63e2c5777d836aba60aa12477d807c11137b69fca87d3bbd0c329e8eb7ba6a47b30b1203f0effdfa6bd3d977c60b255203ee8818cf9bac6102dafff6a393f6e873b1ce64834f04658cbd7e8a15238044c8775ef7b15eb59d2b6616f39eb731bba7f9b9d715a0315539388bd48cb9c7cbf5e98c462d9b7c69fe666b0480ff1071d28b6389dfb33814bae35d3acbda6bdb07888ac0cc277ebd5d83d5deb78e88a5f0d16e555acd0e601173bae94f24e246233a0dc9f37dce8d104d5c45fbe93c7d9d6778e4928171cbf2b4e2a05f2f1cce74c3138017f66b5c3ee3d5c1a07b47b3d261283efe414fd264002e75c78c5936879aba5a63555ef7da18b31f73863d0058cc06cced5037748694fcb539ce025899ac7a0c54001af950cf2dd889339fe454c6d24f8afa49eff5c9d480168df7bd30b492c99c5160a9eb8f99c76e5fadbd0ab048d253b39cc5c7b0ee3192f973784e57c3620e7e0494cf769df4e439605bf6730273fac2c8d74864f402ce577865c44f2783dd841be5a5a9bcd2e9476b56a401f239b46c885e6071e0e63c5e997b31e9f34a8d87cf0197bae002c279ce6df7c42661bce6210d99ca6bb94500fe9facad34c9fbc83b9dafa0a6309339ce3f39730d0052f2d96322ad54e2017d21693d1dcc029af12c8212e08ab50f96cfb87da5084e403e3b9cb6a8a1e0f6953d807b6f6720ee147136ad017e7e03fc2ced083bfec7364070774fe88ef365c1278a229653524f3ff2cedeae88ed07584c5136b72742790f7b305568eaa362415ace4ff5c10730f2beff464d145696bb91f7abef6802424ae2b232baaae24a62c20a02aaba32024ae22a22ba4ab2922232ff810af1a291607adfa3b316637c5f254797a3ddb8ba13217a8354bfa70a1ee5eb556b6c113d7c120300a0fde7d0fecf00d012bcbe9a7c79725837e02eb678b7872d61a9c1cd2e6953b499cb4122b8d4fccfbe1147f06524cfd20d85d3a809d4b871bbce8f8999eb9196f0cae8df6edfff01d04279fef744f2285a989b40cca4d44494e54525d4f405156d9804d48c20b68aaa724a82d6824eccaa464aa2828eaeeaea62f67f9ff11550bf86907f92ed54dbd31795fbe206172c6f44da67d4958005fd177b0d8d1f5e1dfa2c481f3a0e0f207390e8f33be8980ac3ddedaf295589573feb3173ca61efe4861ea4371c9caff636dbf75f1b3b105bfc55c4cfd737e242fe700fcd2dd8be3dfb4bf5a52821fd983fdf15363c31f31729e68742c0cc39e47ecf7ee9a0209069ffaeaf861dfebcd6cef167e75af80d4d355ba55fda00cc5245d29fdf2bf098dccf37416d7ee2fe05e612a9d8ff1d951258ed65feb62610988fb3dec07de186cdf9a928face176f59f4ea84477fe9fa21a4c3ff050eeed2cfc87edf15466376cc52bfb4fd8887e1bf0f2b58d3bbed92e59f0710c924d7d2f20b5f7ac4143791ea893f354bf18f6605a14cbde5fd8b764dc7bf30b9943adddd8bde0d275bc37c5f7d4920ee9b5b081552f4fb1058bf8b9738fec2179fab776c9afffb5a70fd1693566c7f0d67d7238837618c88a6bf886adbb75ac0dd941c0abd56e69c09eb1bbeea5af9f0a8b17a57085d116ae989be55abd8a70c999f3e897002cf46a137d5115d7c75723ffd3ad7ba59298c3d9124c932ea374dfab057f9279b0fc0b77c031130d8f96bc1949e9c030c173b584345e798ff8d25ffe3ff7ffefb17ab06814000305487a3412d5e3fafd6673d69a0f5dd519e9ee9a95d46c858f36cfd7b72f810baa50433ac6cc9e7f87c1d2881e2a8cade3df6de9c192038382c91aeba100bad170212541a0d7ce0b8b4430fae8c81fb5ee10a843ba33388b59e5241c95bb39fbcba4aef173e5645c8a6b5a9f0d8908c54dde7d9654513b421025d4509b0148931f938f52e9e947dd91b61afe12ea3aab8b5c7987102e258e249cfc05d1e1f779ae8fca8899acec1dd013c9e7ce99afb47b55d0fb236b63bcd919eeefe38444714ec6f1d008140001776af623134d22df2c175cc70f51cc8eb4ac738bbd63fc7e1d16a29b55191a396e359c6bf8fbbd1481212940850aa605b382028dc2773ea76456826e52b7971a072e99b78f8481bbb767ff1fa002cf5bba9b6162c1c462223c825436905ba29229b157e9d38d169b0f03bbbfbd5e502b3ce40f8b0ce165a79748352f73857123e9a9bbcf9b88c5a3a973b5dd23a835fa45d9b5381b5228d4bdb1ad8f3d133dd1185a92f161910553c152e25137a65f26e8fdc5db6fc5abeafbd4255809259c75e1aa357aeaab7a36161097a96f2e4a30ee5b72f0fcd3dc4e473c56a6f7e384d861826f53f14f1aecbe49ed867b24385f77e8874ddf90491e826367bb11afb7869804d1fbd4da7675a153186cd7b1c7cf278271e2f97693cbaeb516c0f591caecaf8d96d28c870af841253ee48108fc8b1d1ceeeb8c563fb122b4d0e3d2e3bbb01b3d017332149897397c0f5e4ba8ddad2894c98f0008e47ec4a48536400e96d6b6db447cbbb53c8b0af8a1bf0984be09a9d8ea2d1885c33fcc3e7cef4237cc3954d452dabb0d972adda91bc99fcc298d9ac056062a9343615434a8ec54173e0b7f8094fc2dcaf6a41f01a6fea7a3e0c79d7467c59226f64e75e72762d44ea6cbaa532e6d2ed6386d9fafcf1385e2484095e93f6589fcc8f62340bf51e662d05da5bbbf31a511c31ed4ef08e4a086861ccd64ad93c48d1575dbd3af674f70b16eb3c1aeee0f736ee3ba35ddf31104ae64e60466a26787ecc5809ff6eb3b18a4c44327a5ad0a8b0cdd4b3254931749dd753d7f14ccc9581f1d05cc89fc7b266c837807c5b35fa0518645826e482aad46e6c491bbb5f167c9b75c4c0d237802c80bf26c768b698e6dbcbc069e182aa969b40186aace44bf522c57bf7b0e785dd8ffd842ec528a9fe2391f9c457e2b80f60567c0388b48e6a6166e9d9fd2e5fac39404f3a03f7231c81ee2b33ddac77054591c7736e85362c7d0f1ddae3ff445cd82b1c71a6e141039aca968280025f1de0fb82dfe34281d5e2c6fee11b8a2bac97fc5c295dbedfd3ff2f1ea2bde36f7bb2795f30f12f744fc23f9c1756f57e81f382376b1661bc9257debbb21c9a149bf9929da6c737cfd30b7c28456a0ac5eb53df5674332fef58ee997902d219d92c82e36995594ea952083a3eebbd99e1c77802d0e674f2d5e180f8f3bd30037c51f3dea7fb8be99c4a0094aba5cbc7943a725d143c67974268611886052fcd01ce2e5c04c344b9a2faaf351b5de46f4a283f0b6104183f794ee6f5af368c150b8133df51fb3885ecf61602a03439f23c7775a76020ee063c56ef396e84a975a812039c0b28f312ccd9e8856aa79def5ddff02a23126c0268db56b3484f410c52aa0f9f22234fcadc7f2185400a189fc5cc2e4785fb333ec98fec7193104106be68645ce7e9149a16f91905db1b239ce1013dc395f27a550180f6c97bac2ded57b7f1739854e6dbcb37c63eba950b389bc5981ef55f6de60bc82cbbb41c74754acc66140c18ff56a968ea4f560fe8996fc8db6edd82645c362d0718671b11b8af3af9cade9c79bb507fe330eac05fdb0c10ee65ea148f878b48c40c59b8a14297717fb1107cb5fe3c5d822e5799ecbe3dd6d84ad41a1ef2d557452b788ae7e9335cdeb2b7de1397aa657a38626fc2c5603e4000b48f4ffa60f57d833cc4d49b9a2e6050d44bdeac06c05d434dcdfb84193ec12ed6cd8626510a3ac43d5e7d799e7eca9201ef09f9608047da4a1429817b0795c91580cefbee11024e7842bbdfb2f93d3226a9ab5188f89f00e15a3bee4f06906989419fd8ebaae52ebbdcd6cbc101185f6598df9f5ceaf23ef860d17395cd779d64692d07607c10dc38e2909aa2beaa18ce46d85f27619e4acb0238e6efa4329450a7c4b8f625bb7454d56f5f41f3c10318a7ef1fad2134d6d3e63de3c87ae62cb18cfd9a20c5fd3c9d8530254971ab2f8cd8ac50446e177f8b070d0fb03e3e5195bf3643716fcceb801026a4bc58788cb40148d537bf9c3d09d222dd6de15adeb2c465d3cdc2ec034827d497b4730e8856e98e4386530dea494722fdc400e3446f5922cc9d390bb48e9c18bdc5932e97961a51807113456f76de55c541c13c3689f9d593c2e5709846bef3f46a7c7dea81ea5d67c2ccc541ea383be4d4245980f393bd453a8ae63579292798ddd25151be2122ef34807108c3c78ededf352e08a2af7617a413e594a2e608700e6aa5b3418cafbc15c515e1149db5d678196334057069c061fe144e90c88153611e0ac11a704554eaee9c0030c3e2a7090d0b0e5d98dcb2a5a5156ac3602e0bc0fc9653e6cabe0fc8390e1677f77ebaac8a3227ec08707e78377e25e8e016b2f38196a3c6caf00463dc8b5b007539dac25dde7c742d3d7e1c5de6097f9d947e22f4edf3f48a92510b614ee5325b24c7a1fadd2d0a02fe0480715cf0fd4cf1eeba52914e4d6d916ad5e611ebce7580f34d7bc9231e9fd50f32267748e0b06caab0c7c21a78cfd3df18f0b052f7d41d3e4a434a494a79d9c3b2070b18ff9996b88426b7cb8cf423191e8b1f0f9fdcb4c906384704043d633577fa24d9c51aa52a60dd4c61b4aa0ab8c575d896295ce1f4e991b60dec52b9d9fd34b2d118106e30a6b7e0ab75c2bab0d67c1fcfde2c1a81486aa3e63cdd8572a1c7b4844cf78427397667782fd6a9bf13605cacaaf88c27d9848947aa3d26417027fd29687b06e09c53bdc048f4788bc5b2c8e6ca03d1b499febcb4eb00f9917e842e8fb126be743be54d2152643366479f16803f90f2dddf377b1c2fcdf10481cfdf4748076f421f1030fe80e016ba69f2e91bff10b18589865d1bcea0314680554cd4934c670b264590f585b0afa66c4875b206c0792b6cfb8a3197925d6dc35684fc88631d4b24ac1fe0fb510e0ebe13cba629d67ac24c971cc06e1803a7807029087ff2ea843b0b250a36ac5d70b4274601241ae0115a753afa32fe4ef3672b596da2e796e149149d9700ebb329a24ad4b79131e3c9165549c35ddde0ad61234038deae4989a1f21db62cc9cf4b671457eaeaf9dd6b01e1ce3e33ed2aa1a4a536319ac98b7e67e2473df4ad800c1e6e95fa2e771ae17882d65c4eeee156463ebb3f0e70ee0949a73bbdedf8ecc98ec6ce25255d8fa7c292b7af9ca74f5a3570cc972f20d1c75c9afea468604cb82c030897acb8fee49494437a2ec4b3e1acd7bbec6d5a6a03802e6369e3d15b31f5f1f2fce9dec7ad2a896731920043dfc73a43bf3db074ba06db6cb107e11d4ee5786cc0fe7e33f15a42a4d35248d34bda3b8ed240e2da443e309e6bd3f295f7cdce33c83c42c718e766a99c5919e01aa81c8e3b3a35133794506d588753ca412c9bdd0570ae1abe693e547bddc47da52dff03ff68f570d2235381f374bf76b1752f5a5fc3fba6d586bb780f5f0bddb40184636345ca918933c6d608303e2a7b61e76349d92903587f8f58a22f6bc4e0f06cf7965b113abe89dabc0602a4af1bd123f4bb31fd3a749e45cd8de7a0dea8da8e08605cf7a137de1a9560c192bdf9b932a13a8934b67701703ea93b919d9d7c474d94e1693f83d857c547df920b901f731d821893d41a8d621a344592d8778eaf4e6101e607253280c087a42fc665936c1384242c42e65f000817b6cfaa70dade1279851e5f493e8416e6e32cc7050877b72b16c2c573eb99c009c8ee37709f68cfc39c03e00ed6754d4822a17c2ac8c86116c2b7243ab95dbf0a987fa2c2fc7dab2c5e7e4531c5b41db77b9e1e0bf20600f916ab8548392cb8a56871ff2a633343411a830d607c063abaf34ee53c53581d4539790fb558c87c0e00cea538e52c0713640ef645c14a92735e4dbdcff66d01f299a286ceda47469753415516bea5a9e3615f7c0a004dfaea471ddc11cfe086c3b17e023bcca646b9d195c6f3747179266aac01d24147ac4eedfdd47403d89525807c3dd0963b9d67c74427acb919e7f25cc8cff7d800e07cd8d15d5c59f8369ad37799335d99c5af0d39d800100e4caf386d8f681543250a96f97c374eec76567e21002ee04d7bd7f1d6c19d83ded3971ee26fc39a51333e009c4728c3c6e4494a5c2b3c083c95aac55f23480667029c4722088ca9c2f0cd1e2995389b962da5517cf4f1039c6f700902de7bb834df5bf38913283a761c3b6c47a0fe12add719f45f40eb357a3e4b809620cb3db73c5469789bb5741321bbe6cfbff94eb4dea50bfa58081c6ca3686962aeef5339f88f7929dcd2f26bf18e3a0c50425bcb38421365ac4612ea4f7273f779f3282dc7b4654b0cf4d15bf669e1fa350ec44eb5ab0f1fd6f6b2fd3f8ed6bb743147061069ec46cf0171afe1d23d733318b8173c4522199e4cde278af55322024cd68f2da1adbfbf3b5a2fb4f529700fd9c58cd468bf063145602be7e507f75d1264c30fb7e2d412da0a2a8c2d5e137963300c6f6261d050de13a8d27fbe374294bf268e15fddedc2d2349b7566118e3a268bda21e453998d3cd0ebc43e608d44282d76063dcb57c0a493cb09d33f2723d3442f4084eda1e2c54b7e1546e467b92667ba83d2ce6b78f60bf3edb288b60a3be7ffd3f8305fc8de75c1308fdd2a631540e3391945d9f07aae7e63fea243478e41cf5559f0c11c37bfec2dbbf57a91a02e357c9d697c0f7ef20db1b0cef72be56f2af2097be5488d3c83577f906600c0e079e5466f202a024c17b7b013782d07e0194e4ffc66cb985ab4755d27148877568761aae51bebb130af31a71d5231f3708491203454155e2ebce5790234a3e969fc18fed5b1567cc3f6587b58d1a475008c1ec877d3da5f41f203cc30ef8fac32e063eb8a63f5a0bdbca65181c72b14e4363143017ccc20615b3101aa3021b193332e81b733230eb1bea331a18428c990c598c199999580c8d99f50d8c204c8646cc8c10560363083b1b9b211b03338b21030327131b1b9b3e3bbb3e0733b4fa99383958bff5036616d68b20bf427369c3b920fd82d8c550370d9a82beb509c49e54d149df1ee2fefdfd471532b5377320bd6b09718558fffcf183790985ae03a57eb8b36f3ecf73be5d9ca5fedbe5fbe8be4d7f7bdc7c00b7f50f5fcfdfe3427f510e6c536aa306f43119df14847aa2bc1dd33b3a9a47290659b8051849294b32728fae0f9bdada2355a9874c5ff74b8a523bfe467eaa2f1d3db7a2e1468cb9f50d0eb28eef73551e9e89bcb8c272b155fb7f39b07f71fda80afa06066ea4aafa969610c71f28ff6f474f2543414e0e25670b5609130d2b315b417b3b2175153b0935530141470d075b051669378dbb101619516907999fd7febf410e6c56234e2623164e080bb391011b0327b39121270b270b270333038711b3019311a7918131c4989d83818581d1909115c2cc68c8c8c4c2c0cea8cf69c80631e6b850ff7f3924f9f3b4e90355e496bdb1fe783ea56bbc7bbd32a0f2e3ccf6a38032cc423eb0be3dee8abcc88ca7986e4a7c805e0c3512245d1193de3ce5d575ff81a707e4414750b3b540f5ea791d228c753255ce1a3c0aae85a76b637550f0f5ff668dc9eafdcde003f0a66f00de8c5ea0ba9f837b2d78166df27d209496645eec2a0ffef6528359fa3a5d5bf00e44032aba08c0a8cbd7768d4c2c87c0dc3257aec3f0b508d1325a24404b8dcd0993b7889560dba6877b16b4b84cd4e58e0400d5c45c6082e77dea16d7a4ec68edba71b4469aee000831f2c20be64025b44c5778f9e253b6159b9bf8f427002ab78050ae0e72f5fbd94e05211711f385bc9d2d2cf8ef693ff0a1d4e7fca64d3c724eaa647dffeb74f619b538f46fb0022b8a12dd6fd50f8dfe47fbaf223f709ddc2291815978dd9ddab3a13b4030e7f12c65824c39741403f90e927265bdaf7a3ad56975f21da60129d16cb1c2abc58eb94e1fbc56b08daacb09cf5c5398e8e69c163e21c9451f5d818052e0b7d7473e9ba5a69f8ae5268ad14418c7166d95b5bdbc553390c73a4253179e9af2fa3042b1d844ab26526f99e9d3a76b2a2f468e821526b22d3ac9094725756d176c5ed27ab51a18cc2822916bab04a05034ff4aabfc126128023e8d38a55da50dd306bf94058f1600244a99fb325c69abf120ec429ff566e7687881ed530048640adf6b127a77996db8e34198caf71f7372583d01218fdd11be0fbb664f9c6e5c13cfaf6cc0d1501d91061c32f9fd503bb7f33030280b48555fc113f34cae7403ac1a76e6f0e69c37139b8f098c1cedd1dee19437c902acc2642fb1ad6739c55d5b6c427a22e97daeda55d2ca01b496c7a8316144e4a9b3a778becad62be97ed6d30010b3776eab3e3bfd950b52fb9a2814b08ed21ec837014cb792c8be689e49bceddae88a88825bb85def44ea096085f509d285e8f0f2933c1052e6336a687eb9c3440918ecb82152bcfe4fea85d38b8847826305fc377501900ac81476147cf9458c214beae5e829317156305c6900ab61b85d6f5f5cc2423ae4f8f1785795e44b1f7a80569939747821737e9d33affe23416aa43b0dc58c2ac0aaa0bb469385503805cbe637723f60210bddfd49084034869c351bf66866d53467f1f0e2d7b6671e68c0799ea73f19bd2eadc9646fe2d4374deb16d0626c677102c8ebcf9cc693243637da96b3e5a5f79aa18c93031b1190577d934364452552b80b519aba88614eabfe46b9192064bfeca49df29ef4a1ec758b94c8d502fd5288d41380ab644eff8370dc33d4d53c7b2239641a2eae55062c80d5122e44778637a07b731743eec31e3c6ce3bba49a1600bdb4c68da9b0b2e7c699621395fe2c724e991e40f5abdfd16e28a51e8abc7808de0b2425013f143a04ec9fcf89ce38b1aa112b4848cea59e7a2ab08eba14007e4c7823f84ce7c471654efd4951629d57ef30a1102064d31dabd53fd39122315fec08b655091bfd73b2c5d879fa1a05d28c688acef6fe2dc2f0a327fee119b8cb8090c3ec8774c657c2ad6c7dd76b73e7b27012c4d096004747b06ef9a49495a557314d3bbb5bfb23a7f99b9500ab874a9b0cf6a37714f96eae4aa722a919e5620ae600af14c128eb896db9acd42c85a1eb379291e61e753a02b4fe28bd1de84a0d26369a5c5df915056cd577ee9802ac2e8644ca48f2f3a090072673b2a88a8e1a052ae200488ae495c34c05023ab0144f9362027561d311be2dc02a54ee6c1eae8754168ed08442874ce23551a2a70e08597da0c689a849d0707fcdc2b6f56e7ba2bb603806008412c9e63a2356b9921555ee34a18e15ef6c27a80f1012dc7ae51ac395d5a8227ad3785b5ace27d4cc438180905ffbc7fd98cb8291b80934d39e37e517685b88b701900eb34ac6217a2103f90b6ee1b3475bee2ba6fc320037e921fac521af12abf94b1bd24fdce6259a359ea302d69779944ac8b2b082ec03b1e2f5b55ba33c72834880534ec4cb5e111ab4967757c7c30671a7e19730b6e70056cf93975cc6cf072ca6f14718d611711558109efa02e0d064961385394ab9795e9cecba57312c7b20c09d00c2ad027772dc48f05d90b2f2f7d3d8fcf782f2c654009017b389d4f08ca405e813c3b71449e48b3621837c00abb323c7dc44dda888aa78df7207a6315c5ea65630c0aa3f711a35a11893c58b20d8abb13f7c19215c80ab046095bf9320834cbb40bc63679068ce6809c396a008f03434aba0f1a920649f23de258748de924e532a08058cefaaf3a9a2cb50579dbd242a4c11f653b6c78bba0090c37a4332bd9c86073bdc974e22bdc71dee17931c0140909196df30527eba7df8e20335115b36bb4551af2400f35deb9a84db3d7e1704c238cc155eddcf2ad897150190304ed953692c1fe31b3d561ca66695b30a8a41af01e3c3a6ababad4a6de55ce6505dbfe8c6aacb7d830bd0feedb0ea34c915c7fa1971df519470bc5c814125007db63eb8a0de2d7f42a9a1a3bae7f3861f3d553660fee792c64b8c47d59fbb827c9e6a5d8e644722fa5c0938214dbf7f573a9beae4fce9a4dfc7ca3d51e9c00c709e2bdddee49dc821a2d3348bc46d7b84f9b26fa91760f5e4ca064f8d9dbeafbf4dc47de91606c19a3a9aac19c02b227ee8fe147bf8b3ae377c88144a9794257bbc016a6044ac7e8b5d3572b33b6c23d7145d8ff429aebd07f0472f2f41bc7e1e74b0a79d38b2c55ba60fa401a3004801735b8aeafe887718d7aab19965f1355a8429f30059687ccb8b6e99c83967f5dbd2ceb27b8894e1166502e008b73ff96853df6da21c0de3de56ea36be6d2bd209907f1afdb19d2b2481ef3d0f932898cd32c1c326d4006de019597e3adce0316e88f5ab10c7672cc38e9a4a80fd61287f7f9fbb0e9b022d26700537b59661f09d2d0044b471c9fe292eb3ab988352224c9ba3e9ed6e6c9012c06b226f5f77f2b349df199dd601376cd58372327c80936919117a2f562335a63d3626aecb70645b8ba421c06b6dceb8ff7ee47190b8cb2324e90d16ef81161a17c079f983da89981a324270b4a224024b853852eed5a780f3019504bdeea218483276325856f3b40346fb351d0012c63982e74edd99198aee35de1bea28fbad3a3a20407a22953db4d2928f57b4f82789411f264783afb4fa039c588d991e278460177615ebbc17855f41e5f21c0b0178dd7c127597df3e352242ba2fd33f4f7bd74b0f5301b0ff72072df1c7510cf40aaeec10318d5d158fa8a706401278d65285a16f1fec72bb373dadbeb299efcefb14205f055361455683633884e6e22fc54c5aa4deb21905c83f769acdb0e8e3948e1e5e5529bacdc7f7ad368200fce92c4bc82d2f9f3f4f993af2ff63efade3b26eb2fe71babbbba5bbbb1b011104a4a51b04a43ba44b900641414524a4534190ee94ee94920e85dfebbbbbcff3387bef2db78beeb3cffeeecf7f5cc335d77c66ce3973ce9933efb7fa93e997018de4b6c0fe73a41f0dc17b0975673c5c193b9dd2dfe6530129303fda08378e525b8510623b91a2b70a347d33e10500c814db816c56e66103041cbda405bbe8e12e4c522a206349ff3881efd3711b29fdb2c67c3edadc21454f357034fce4ee498f8366fe2bed2907c1d9b5b03715db4240d5da561434319af63d0ce73899e7aa63c51db1494380fc0adc2777ee217b725c0b51fc6eb8d04b623e5209c8d2d44d2023ca40edb140fbddbbf824ea22881d800b545da4571222d6372a3efd287d77877fc8e2cbc45dbf44a07f589d9cb9000451ebfb87515802b9b01c5588000b14557f2d85806cc8729c8f85e394ea96748d5e1612d429ccf17fdd7e4717a20ca2af52ff172545f537b5794f2c5ef012a3293d77f4f35dfc8c574ceeaa1cb0fd2a91435a063f12ee4792a2c89078916e92fa11cabb6c1ecacc5b12f6adabc37f2645ffad93a2703a664e560ef6fff4f7bfe1b1947377b7d0e152b374bd63edc02bc7cbede2e2e4e6ca636e6e6f2a69c6c367e76aaa28a9e5612fe5e428edfe1f951465e736bb672ac0c9cdc3c729c06e7acf8c9f9d8fdfdc849bd75c80ff9eb929178ff13d2e3e3e334e5e5e76765333817ba6c6fc9c267c3cec665cbc260266f7cc79febd93a2f5fb7c89e6f21a41af21ba06b7e469ea709106212e6948586a671b833ea565d3a141ec327b320d3e24aa110bd577a578112aef5edd2ae18e86cc53f0a18b7741ef0ff0346263fa07213d4009a2ae7958830d1358a8d975cd4585bd5efa13e60aa180bfe20018f98afc1a3a307eecaa1d9f69a8a707ce5831d521333cbdfa674d9fae29f45791dac158bb620196b7e2fca23645de5c752037f3fdd35be315030caa8cd7d44bf16bbec3bff973157121248c84d23e70b1d3f2b3ff975e4b5ecb98b8e05d1ef62c8677481440b1ac9f9eebcd7a9c53f3f4b020ef1c05944efc663cf82b7ee3e03a72646ecb6ff7fdfeb169bedbcc1113fe0b27582220ad2ca5e3ccca745ed0a3b1ac6f5a12eb32b8b636f48578a86befcd9ecb1be1442ff01f92f8096374b09bb9e2941886978ad29c3fea79cb761f338619d1b6648ce4270de4b0e2ee877bc8ceb8b2e6b031771e8e668d541ccdfda99fff79fa39e570a0ac9fc0dce056c1aa89550bcb30654cfbf97afa4987711d3932b7e5ffdd3bd1ae87b91c2bafaf38851b577af72bf5538aaa1b710c2da339ce77211bd1b132f8f0d3731779afb766b0ab75080ea2cb9818eb065409bb2399b45694b8e933283d3d22ca8171ca237b9b82e1b29ec92b673f4b3f07ce5b35aa83fd1c9dab3f9b63ab250bdd4e848fff533fffd4cf1fd5cf1f9523735bfedfad2261784a4374c47cc514e4e576fd4afd9c1a871dcaa65277a9786b2409b96ade112aedbed9324b27430f2d31dc8450204ce0abe56678d19776c46c3699f4be7f7ac01e726a7e41e72e0d7f92b21b5c4303f14f1ac87cd379776ca3a8a9da14596855bdc7c4cb4d34c53ff5f34ffdfc51fdfc513932b7e5f7fbbdb67ec6f287bd53574891305fdfafd44f870a314e07c4faaf977cf461ee753102b149364e1c8be344f2ad06d00d4453177e0eab28bcdd9fd409e663e9d66d767b3be2eeca3742dce4e751cea3be9fd3b4fdb3f413018d27b525a2a4991fcae6c0fccbe2f1659b2bfa9ffaf9a77efea87efea81c99dbf2fb5e73fffcf02bf5b35ce0ad95e19d8f53d9e7c6a43d8e985b988529ddbd6188455fd68a586389986f98f66dec473c0d572cfbc816569f6be0338e9efa283df4286a228d3e97c26d7cee3afad908750a8df9df2c30357f2accff86c25ca5105729d4550ab3459cf886a93af53da12857fe9b423cbe7c4babab14e6bb797aa82e3384df6b7b5da36b611a6d15f82b154674fb31f42b075a843e975759db19ac05905b71322e18229c0ee5fd5d3cc8ef197d118c4410c80678da45f5313e9cab44a351480de1ee51ede5dda3b34e38767ffc0732c5f003ffa314f02d3f21537ccdab24d04004bf26361e63cb3830b5d46f6188cba3e13c17a819f9e74efaa761f851c3f0a372646ecb6ffbddee1b9efe6e41314b07f9d757269070bfd230f82814a840373f13ca397eb7524a70b86368b0765726b764c5b0aca00e6637429d9566d96cb98d757be6592ddd56cee24b78218383179c49c432ed6c4cf9ddba933fcbd35ddd2ff5f5715c421dff128b34e3fe3c8c183d06f64ffdfc533f7f543f7f548eaedab821f288a87ef797a78a571a4504fb7fa57ea2134ef9363cc94e93f0cf8caed1afa179ea7914a7dbc12eaac2ef0fc932aa116d9e15c55b97333142b72f4ef57ae4e65e78cfe42459f8c9416f5d96c2cde5a19b24d7f37451d4fc8310c5bb82a8fcc76b50a47fc2a67e5551c71502057d8552c35da1708820819dec6d71f677be18ae7cb1e1785f7ad71ac78ef17fadd18192fe7ba3f3e3027f6da3f37ffc81bcaaeee33777b792485272a2dcf812aa0a3ef2c7de37c2acc6d8b87fadf0f60aa51dad47b6598132864fcdc528594ace1ca3b2085dea087d4522a1b97746c83fce242800cb9989f5351fd234a5800c6dda13e1087bd7a269d75da826f73e4cfc3bd39f75fc326e2feab4654f728e5d4c1a32ca3483a3658a14f1a77cffe7c9b7bc82be53a6e3b821826c1a3f19e3a9e2c3e8f5f3dffbf269cfeb4855a9b86bc977b3398a4812df76bdf2daf30c3b1ef70142a692c1b0f26d15bb62d148f7e1d32d9af058aa7a39e867e6fb706737f0713bdc49044ea905e45934d008d33c8849ce7f967cbfecd64187a32d98868f61ccced3626577125fd4fe53befff3e45bab44e462f5031bf6938e3edbc306bc60e19904ebdff7300abe28cd5fabe84e824478f0519533073f86e19789008d2f5b56c3c45fc96e6ba5256cc7f6b41d13f56acef4b5d4dbb743a2a17ee45df30c632e2628d66232a356e88e9395b98f438efdb382a29603ee07a5e6b9179bb0708d3ec423d6078fbf2efd29dfff79f23de017cd64a764d639f1be827df9ee8e527beb25ccaff44f766987947a9a6eee352db40704e0e8bd2c4fc1d57f127498a8edb872fea626d9033200525db6d8bd1a899e1ae2e10094be61788164aa17faaef599434047c2fcbd6ba6cf21c71eeabd47fb52f5f27f78fa5a82fb279f5df1cdaa2bda517fa43dc951ddd35d63001a39bf4d871f2efb3e7441abf0ef7e1327d1c588535bf7f7db6d183b64d996afa79957aa05ce0f8df9e73f467f6469f7fd83f174901b83efba4c50fc14107e905de34f10febf7bfe0d40f81b4f21d542f0e55dd7208248305afeb59c9bea776eddba79f7471937ffc59c9b171ec4a147f045366bd259a643091dc44bd65832dfe9fb2f53f9570c85259f5f80a1a0773744f1632a04ed17b74805a115a9f4c4a5180059f72c554cc1405faa5a8495d7b8b6c20d9138a707405e6d8420b197ed5651daa19d7141d86a1e3f58ed03747401961ed1b23e1cd6b29db6fb4dc4b3db0237c0e53e367ca6c4bb1d67007991df009559484e20b00ca4d8389e3cd4a5f7ebc628438d5ac15189b42396b34afdb6bd0f460bcd4ab8c41ac2fa963f7cbe0d9f49d23ec0e3e9a4cfdef37a5b2e668069cb9aa93cb264135d1c600e38393deac735bf749243d0f8d0f58c379951660bb8e751de1e6f0393123f48c9e4cef0591b976a32f90bb035993629179e7bd3e724c231b4d5d3f79cbcea10018acdc4579f9bdc57dfbcb59ff942bd247d17fe8cb4fbe8db76dac9035699ea1d5d3df8d79e34989d47a49359800e974ae261062fcdb620d575580b1f32111bf0329f7edbaec1b920d1d9e796f78ca120a61d65f0fdaa1e25708490a0613d4d6c7aa77f28003de93646b240dcba2790edd0a9d42e85d1246757cc0b464ceeacaee9b3b003b646aaa65266e228171378f130ebed80ba7b5adca800ed3a861a1681f7f69e542d0a4db3f01a391384ab34303fdc01e3231ee3ac8bfebe66e9799b7b31981c5069dfb6dfb4980fdd958eac59f3667b1fa5417086546c06d0560bbb2898dab80a26a8b4bf18dd7b1e07e3ef240120e3536d7b796813913e4f7bbe499790c069ec8e910adc51949b22eca24267477841653522c31ec5c8c6d90014e387c5d7f865d93d20ff405dceb71ad11915e74301d0528b397f31f6583a56168377e5b158ef8728dc7401e497788032380b855bee5133e4b852f1cbcf192db9408a7bda50f730a5d08510de9ab0def03ecac10a032be87aa03cde19416c97c1c4255eb7a6174b54abc507e68feac5e3cddab725fa8d6ba3ea971f78a046b0cd01e4f0bd1526daf782f2795addf8d2bd5972b49a378a016527ae64b01084fcdae98324cbe92725f1acfbab1f8031a24b85d453215bee6ff0256212a202bfb237761fb8a3dfc21ea2129f8573f09822c5c2e12db42fb5c1f0836fdb37ccdb97e0c47591b3298632f7933ce60fb63000d4bbf8dd9ee56257e96afb3eab079c45256ef4544480fcda591e7c5635f868e088c1af25c54db15aedd6fdf737e9e0350414850c3a83e3b73a0e789c9b8d01fbf59e3fcb90a23cc83a076747c09be0bc11999613b8430e7f17f7c5f329bffba38f6af564d3d554aa25a180f589aac33ad5d855265822a016dd7d7f37dafac5128011303af01876828f34e11226dd00762e1173f8563b802c6f72a7d369d5ee8eb3a4640ec2c4e173537bfc20e00efe603be2d7ad00ba9bcd85ce586204be67480bfb809b46ca18d92ef342286339dc4b89e664108a40a91388a86ebbadb9888c27bb502994371142e06e0cde8e013030f43c3258cc7489233559c4f66a2ee2a657a32001b764b0691d91792b3695f504d54f40003f36ec9c1c28e649ab7f3f0f3d2238da8650d33a7701df9e2e7819f46dfb6bfc2945e8435ac8384ac312cf94e4734f2d5560bb6ab0def97243033b95f2dcfe209329a5a14fa8e2f8db76aedd7842563c0ce8f99a118a9361f1b58fe59635dfb69361219bb22e625fdcf03789670bd8c1c3681c07307c9a150fe8bf5ee235b1d1f60b7ed49e7af7e88c1a70fdef9c50212cdeea7719f1b032fb2cab9f96d5b10e604ab927f93f6f48e18d88fafa5aebfd050c8d836d15f0feac72d31224fbe7a4273b6fd7d0ef7efedcbd4ac9fa6d3b67cfc06ae9717f0166d740a3d683b637de387d40bca551f9b671e989f22de6bb85c5846c4deeab93f4c0763f7cbc4d822deb5ba35c523e92524ebc7e38810d5cc5cdd97806d1d613bc76e396a94d5d9312c2a7112dc0fe3caa7345538fb5a3313fcf67d6585cd1cfda1703c0d9acda884ee065f6a6c3d72d4bd31f33ad6a4aeb01e93ca90dd95d7b3a6615d3295c3189932ef5d5c795c06585de677854abf6ef91d9d878241a6a1e09587dae03de9ff811fb8142f770d22cac63d046f5f393565a5e00932bff0612e188ec60bba621812496952e4c94e13850db8282adfb325f092da133e491d1aa586ee55b7ea341c03e8b29bc961412f62e4dc295d44474e56143ba6ff96d3bb30eac91a3a273d00ce55611ed57fefe6e1b040039fe92dc9f11fe2215d5dcbf03ee8908ab4de41b4c004307bd7c2ea076ddd061349cf5a56419568696f336b0ffb95353a1397a87912da7d8a9408c77be22df3806fc271a96397be501c39bb5cbf0abe1d2f78d21b18c018c2832f13c894f4fe1571f57a9723de79dcb38440c0730244c2622c3613dbb9b9cde4cc8bb3e2d512a9d7a02646dec7b46f38ebd060bef17e7b443c044a7c1d32901f0e9bea666f666ba22a213a12fc4e7b399a13f50050025042edb73b047eb15128a2c7c831e81e8accd462b00860c47c0e1ebf9add7af31fc3a713fddf5bd41aa6904600cf8c4b41ab5a00b7a47def2c9f7ef3628f5ec830268f71fcad33946d6690f57fb0d92c132636673bfa7068e481f90b6eb9310d6853c19bbbd34d27ffe58c6ee1440ee877b4aaed265caac1a7394f7b151c90b273abe0cd8a0d85f79418721b2ccd91978bb173763d4f1be8507f61792de7ebe237cabdabb2faa78d1e78a2c5fe5b701cc424de2692c76622259de429bc21a1dbd39371f8f02987ac3b0da94488f5e2f057393bc20b2ede0fdd2830560e474faa36014d832064f4f664a5691b3493f38972600fc03a7e189a5e991c7e7a816c92b8577df943e4407309eecea32780c097c38933b6e308468fae0ab4b8d02d99cdd6af29997f2fdc473c6db92215b4fdeb52dd700fe5f461d41e013ca22c90d46751d7cae33060b593d70fd2eb42d6d23fd12db6c432a629feea9f0627702182242d413b6dbf862ce3649ad1afd0d2f02180bdb010c85497a4341343409eb511f17e8f2e8e10d978a732028ea806934b9ef98b5682858386ed2a5f13cedf233b0ffb2d16ea3e6cc2a2df421be165adab41f7c954b0d60040d39e40b071b621de5598dbab179f3dce57eaf0a248416fc26eb2dfd5a5d5334f82fb4d01ae36fbf91a4873a857ce31f04c38b13840331570399abb30439f5fdb89523e19a21e47543bc2598251d406eaf2a5d406636c950e7c73e7895e9ced77944abde9f7c19f2237199e0fd78ea44e64a8dec2efe627a4e8a26579b42a4ef7d199a7cf0bb893868e2d27a886b65ea4ed4fd83dd78122142a0349097fe0126e649a657c64355edf477ea2f2961315ca9ca0c5e7d6f55a0eedfda58f9e9bd421830a662fcfc5ef58fe2b87e7eafea771fcf35420d87f2534f788f584204437634fb0743f93014fb379e42def40fca3383f9d72650b41434e4a56f4b68fd680ae55f9a40b18b47def078b54fb08a8fe250cd119d216b4369fabdac1b959cf4c829a4ad7f10a583660da4290a0a7fd6f747f300e08fb01709bb99342a7edb814ef8de6c1f42bc41832fd315c7269adffef5f94b79d78897c5685032e3ddca176f16d6ded1ad600d3d417f602333cd7da7f6142a643e626fce449118af3fc459d755984391ef0a281688b96be68e9920c1725a9ed2dadc3b6af306166acd79e76ceeabd07b3d7ff1b189985c5dfe2963f1d77cd582dc2fc857b15e5ef6b0490cb006ea840a0bd27d20c1a024bbfb6d7b11e64d1e0cb507385efd6385fbfe447a943928000b81868bddab084fd2e6bbe7aa41ae36e6b0e41a2e40bc06b3ea2c43e68286e54b71cc2614cdfcf5ee163110cf6ab022e34fa7b07fec94b31745c5fca84bc6ff1ec88730476834937d2d148630c973ee4296d3803bf002c4017124e77d50abdec6d31bf3d51934e16149276440ad7749168ec72819620eee8a98e0e1a7092525486220672ebfbac1c6714e29717c538345a12cd56a926d06b0fbeb7b05789eda013d8de5d421d91258bac98dd65b803f522e973bf7a6e8fdfe5846e2dbdc583e0eddbb008afcf01912994d4fe4d36afeb401ed770eeee2d60d803fc054acdb655f29d2c98f9ad03286d135c356033500f86b2b79d91bb716a25249dc193e1674cac5066203f1ee3befc064ecb337aa216c73350b0abbef6a87c281cb9befc23692b2e3c64628132ca00312e5c91d8ca800c66ce2d941642fe3a4d2aad8c91bc1f75e3f7c8b210e60f2a557ed1ef579ad9135c98e140411189b645891004c4f2f7b64b60a95e80b9266aae7a02a9c4be5299b817ce3eefee102566cfc53ec64cf01af0d8d75da843c80e94a90b3f456cb079873ad4f184fde592384a957dd6d03fc5d35128a3068ccc5ed3e796daa0413eb7dfc270013a54ecbcbcc4e9686702578a1e153ff194588d2b2d86fdb1df390bb93c6eddbb1bf12ca312d9eb2a06e5100f9ac8f2bbddc966263823b71e91e9f556a9bbd5a19007f99d3a38266fbf9200ade09ab9dd56a794c919ed12360271bda59d3124465700c2979f9c192783f026e0d602a7c8ace7083b042a005f940666821989eae6cab0c60f2aa6230bffd21e361a93deec7f3bb8f4f273a254f802a391a02f9fb0c3771240a895b437791cc1feb40ba02d429c502d316edbea8aeafc907b0d0615f91613f4e07e211169427c7a62a1149c99ade62f64f37a32a83d601a656744cec66c6af11f2e7ed91f755c8ef2ba86c2e02f64129fd62605ac5c25f4689098a922f6c9ffcfc0570f9d09118b15688e60e3bcee33dbb0b64b566ea0900d210e2b16c6b6ad1857a6a29eec38f4d0f046d8f47ce01a63a6a53a8542267b8c8b88509eec0cca2ac19db45a0ba8662d6676c8c32d28609f76849a7e232fbf9076b80e583134fe1d10c5bfe91af58ae4d399bbaeeea3111100fdc237cc2428234f14a09b99eb577237bdc46441ad0bf143ffd83dad5e9e7b786112c1406ccf48f6f910398667bc19f70dc71f972c66ad86e07d027600e513e07ec9773a26c47227f2592667a765db1f52773093225209e7a3189dde533dc540035535d46a45e3ac5ca9c0f8040d17d642cef988bda14f124433b3aebe7af4aee9505f2f1b938a73a7e071ebaf592b11745deacf2437280fec0e5d20b28dfbcd8505a52aabb58bd1565542009801f1c102262c6f7909176bef1eeeb2bef1e2e9c0a03f2e1fe77bebc5b6860406326339c38b1d3b3ddee9b05f463bf9937a4cb8d5b7f421db5181ff56594b5883180998a40488e5b803bc3c0c5e3106a5e951a8757c50ae4239a84785006722cb3dcb1ef8bc8b12fa58a8d1f01fcf96a2d1ebc6d642ffbcffa893e154a1fa2531f860198d286f59b2682f8eb0a726121aa5cef0bbbd6b2f1807c4aa42e6763ea89d363bdc19729b628a481c55118c05da2581c2bfdd190f83c93e719eeab28a55a5fd10b80333fb7cdd768d31afa87afb5f7cc2366c3dc769ab570807c3aa4874dbc17134b580221956a6e291fa4df33209e677185a88cc6b7ab7cea20cd99acea6f82bf1405ec8fcb1f14b7722536496476c8cf188999a6ec8e5400a667ea053e9764e799cc3be778f976758564fa5f5501ef2d90b7f5f3877d95e80f55534ce845032f5b450d01ba8e09eeae04f17bf22dd2460aac26a4cbb03b25d2403eb13b94da56a2af450b52d3e32101a3670fb49709c0e4aa56c0cf8405eb1e72fce0460e7ff23ac39a535707109755bc35911441c345327676eac01c98975540043009fb9a363cb8df1fd8b70caa9f693d0c7aa43ae20830f95af0b9e71748d4cb6d454cbbf5aeb138997696e800f9fa796587ce788f6863eab52db4d9c9c7cfb0a5817c53fba4d1087fd5740ccbc777f4b39c5e28ac4709807ddf35575be3c51edb54d35e4b34dfe8591bf0e501f68f9421c88f1e559378eba5a492a7c2ee91c4b93580fd48e675c36621d15066c34ea67e339a95c30dbb025c4b1821b78660ac71b76532121530f8fc041df3660320bf6893c125dc507b879fa255f3ac8cf70954e7ac01fbf5a856638f47212f267548a89a40e999c9d7e9a059209ffee26803bbb7cafc5efc2b592f62aace92c85330dfcd1ab74a6b20e9a93a5c4b840e0dd1f1aa6577eddbf695e0d6c898ec88f7fdb29581084374f64ad628802f8a68ef0847bbe7aa2b1dc3e08e632de7baa09c096046aa42c417a2acd00bf81db44afb7487af57ddfae4f66d7bf6d3fd0f5a2aaa5b8c2d6afe16b9dd08fcb1e0fe67669b999bf2756eacc8e2b954f1180e74ae7304904fe69dd0bbb0aa0b0abb1909794bcb082dd15c5b03b89c1e535e8f74f4ccff2c705d14d9ad03723d1cd203c8e7ee76b0de4c4680dac4b5b53b8abd2846291cb404fcbfde52610c05234b4f617184f6a7f0c1476f6b7580f31e55d71b0e07fee9a2823e4fe32e5e9541e7d65803e711059f9fedc7c5b41b43bd8b5b0b082e384ed7b800d6ffe0ddead3bb819dafa460f3a5d0bddb09cc4fd500a6638f3a9f4727c79d956b335a7c3dc43be6f6cd0360be2f56203dbbc89c6388503e55d84a632721d304c877e630214cbfd9306f9c25754aa54b9ca686732b02aa2c6928160ff3176a39a860ca5d2456726af0281000ccd697c255d3b796d3dd34f0d260a6192a30caceba00903753d29c1e035ab70b4f54e9d98e38b8b7eb7a4c40bece00ae35456d363075893a8dca99603ac5bb830260aaa56979d9ba2a88e6bfb096f160d1508d902eac13d8e0a75e53725417150c7d52dca113e4e3951b386a07306fed7879efe99386231c9276054fca7bb8b7787f26fb2d13a765d1ff021367acc29859d8f0d13abb120b8ef3a730ff2da50ce4bfff9f1f64e2dcbc66e2404286f5a02f7df49350914f18f4a27f0de9d8c5fc884246c1a8d3f19e79035be44d439c0a8949eeb758448378af421133ca0b1e0c3d9931ff4a51a40d95fe44a586e9ffe74c9c57ccffd54c9c136d1ba7971286be95c40de7a4674922a56634bac63b78de1e9aa7e27cd3067c578cefdf9d89f32af9943812528291a1a976923b8229c47452129b60da7ce0638a9d11b1c65b091b944b2e7c59a159721753324fbdf17e71b68ddd79aa94564f73f2538138221d34d1e729d765e2e42cbf13ec2eba585c66232caf22c8ef445c3f0233c7fd16b2162ef0cbbb6d081545f1d71911d6f253e9dd9d5675ab6b76595fe1dd83ef405b0c15134c33ba2fa2fd81abd4df305afe7785e44f67b4bcc873dec934182977e4c00decc8c010e66fe444bfa66db892d1d2d1d2ccc3c3caea9f9e7b096f0911256b8e1d91c20b1fc7ac2f49681f3a1464ac2d67dad22853ad9819e288729777bcd8de7966f4186a52289aa61ae3c6494f78975ccc27ecbc21679e5e21fe8373af5f1c8140f00be71e21e371315ecf4cd8fb9dd7d345426c0e6709ad17ffee737f2310965c51a1b649799bab1e81b1481399361c465c14bbed9175efa08f9c942549c9cd5389a8cc37de2223b3f6e5d8f41c41f5a4baa7e75b02a1beba21d64dd857cffd60285369fb467d23c47fcdbc3f40f4fab373f16a68bc863f3fc3aff42263ede7f7aaa0464df3f37b154ae2a5f83fd56b5f18ec32156b1dee796330698f19847f5f9842a39f85fbabb7107ffd1beae4f65f59dc8231e79ffd831ffdb4860e954e02f590292bc2f1c5328737dc1173d17785520411a2f1a777cad917f4f33bf58b13fff99d92dccb6e841a0e85e24af1951587f89faefdfffa298b29b5fc6f3e65bfad1c9dfd9b4f390793d4a37edb43dae06d9a3ff8bfbfd3437c205522f8e9cf9e8430168fbf0ea0f0cee39bbf1d40004f9ff66f3f4d186575bfd68bfde34fd975331bd87ffbbfc31b868ebff99443dac9f81f2cda3f5ccadf79b790e9558a3fba10bce5b73f5eebdd388aa342f87fdb6f26ad01c21f1dc36fdeede4a67f3024e77dfe6a694c9824e0824617af5477606229c77d3a7a1939914d05e74ca5ab2ea0406ef807e5e973e7ea11e603a4535ec83b1f4aa470a2c61af42eb68feb4d0663661a4fa107ff8b338204a3a506bae5fa7c11d0bf6937893511ebc59727adf5ba50db80a3e4946160c6604255234ebaa37f7b4b7f9df63d872abf42aed61765fa4b0bf564f9ef40f2fd962f22eb687ccd5654e594a4511edf6bcda1ddd610bafc8ad005e27aa1cd75f922a0e620febd9fff4324ba926677a4ef48f1a9f1baf1de95d2e2f2b074b253d2e457d3d2d4b96f6a657edbcc4e854bc651fba693a6b4d5bfcffc4a8cc9a850d363b4bcf84881217f4b5ae3c9e3e8222ff79658f852942c4be9f527a2be564ca43a84ef79c5a9151af12544e009cb68c2bf48143937acabf02b3e7279793549ec2dffbf6483284e83aaa5110d5e9401c7fe9c0d355afc163d4e7bda7a5c918fc49245abbf8f2601cfb4db08d5e7ff5fde6e7052aaf7f346a8532835ff20a8e3e32006e3d61a28e97072154941151a3b082619d59c087ab2e40783f93f382fbf9b72825cea1a3a797f25e32b5a6ab6aa609084d426df7b2e0203917ea53b37d41dd5d66163dac5d086ce3342de2f2d2fbf64bf804716132127351de12d4c7cadb020b8d9c7cd0509d933525e33c473bc3f8da966913e6f3482ada86fc57ddfb82e016f846eaa6eeeeeda1a2d4c732b2d46b6e30a75dce440f44d682bce3da4ec39449a3d7952e117e24738f8870a2fa311f41f7f6dbab4b6b062a18c187c76978fc38fcba95dbd902d9f789f4882fcf550e067e84c0c8a5759ed959e5a6c461a829fcb6e4be77f7c0febf9f1f03e0e3426a49bc23baccff98b8bdb6d845fa6b92d2409a06b7cad612a04437defcf4916bfb930473c1f4f25b0f3fa224128e97131f9b9d4de6a3b368c3fb48f3687c0c0aeebe3234536c849d2ec6de16c39698932920b06d74a059c4fef21e64d13880954b7f7faab3d45c33a94478615b904656699ce295915fb0e7b87d1f6081bd6176af4ec5e9fc1685195d229ebed4baa66218ff4ef27fa240893e46c5c9e1cd59eae643917bd82b0caf04070e618f2b2b659a70f6d18ca63edafc4657c4171104123da3b8c82e04c249e71d8f99ec1e30c31b39bd4d565067a8b881f2280a23fe3f336a58c52cc82a4697eaba577738a7ad88ea8b7a1cba5bc3b3bc799b554a0886df52ff0c6a314e57f41afe1a673bfa0577f42c15fd0abb78cff2fe8d5932afb17f4ea711ef75707298c631fe60fb88fa7909b7fab455bfafbfbe00fad6b3498d1f55bb7c2c409c790ebf8de8f46fefec60f1ded9bdb7a6529da7ffa7d70a3ebee6bfff4fcffc19cf78992fffff3527fbba5053838d1d094677ca6d36bb7cb0f466fd0b967b17e79c5f3fd4d5290fa548a000761a38506d3fab8ca0325b12b39efbb63c3c8e96f843aa90afd9b28fda5677a35edea5c5d1d53fb1fd9f8cc04e55196c23ce302043cdfba751e51118ba8debceeca1665a39aaf8b11e43da67840ae2e7b691e53946dbc8d6140439f955f36db9df3baf12f9381d6be18c4a32ef7dbc9f8cb4335ec0c8d8e6937b37e542ed816f9ecaae9fd7f0e82f67fcfc6fff809efe903e40039d94f8878d1ea639865ddff40078f31ba9ebbec8a0beae04de3ed7ff05c4fcfaf4a515e7dd3f81ae3b38fdbe87e7dc5d1c355d81ad93fd32afcd8f8a344f0337cecbf3f7ee3eeeb8eff2fdee8df44ea6fdee84fdf8a8c92727f45afe6e58d90a7300f42a1a35aac535e42040ba57542d4c098366b6bf8fed3b3f2b7bea6d1268ae177cf1d7c89ad3f7aa8ea0b8c33c13412ee8cb17e95c491f64153f7fb7ef72061d98f1921c838bc87d82d57684de38f68f50f45edd05f89f6d855c5ae983fa47f3aeaffdbfc5ef17dffeb6505ae0abaa1afcaba5c05237a95d5c2a9ab78b4f6cc9a1fb298b1065a844857dbd1abe7d60f180a9c6b1a9a6b7fffbae3ff5ffefe2f7f1a4fa1121ec2f39329226f0443278442d440057ff2589db89e3186bca65379955980fa21f46da758d159eb0a3459380ebdf768c79999e9a81b8058b5f3492eb30e361faabe4678f91a1e7bdd818efc10e29fb63bb010f191dfcb9542f145bde5daf891fe134b105e41686560c397ec302ba48d47442dc700254cffc4d5adffc5f9f97f0fdc15fc80b0baa75051fe4194da313550c16fa46094ae3abebe660871d5f71dbefd43d9b9b08aa9799a37dc7cab7fb714271832c77de13aea626eeb7b7ecd182cd8510aef0aff18a23a068912330d326cc68da8e7a1c61b262b9f4771cf088f6b66e79db9959bf70ca5afda29aff9fdef3d8114ddf3af20f34605028b20e0059eaa176e3960506c7f74df7edb5fa0807c8b4e96371773efd3e4b0f90dbf2d6b9c428b77696349fd2f83b55a5660e8925e74183610b7cf04e7e9e7bec4e7f30cf74d8e39e00839ce6da89441ffe0db84c58369ed3ba686542eacf7b57c22f4e4918b8cc11a821612a2cb126795313647a0087de540fc15e7fb50a7b0987f8d0bc994546a60615c0d02fd7ee43c448a0fcace192be834deef5907a1b28b7ffaf926edcf94596bc642568a1de38a0a7b1526036e56f56dbf6c550808fcfb19bfd93f7ba1fedb4e7dffe7297e28ef0145ab03360f7fdf844379be0703e576f0fc83525fef0acfe58af15d977f1b3277bd484ffd9a08e7e2d7cc1dfd54bb666e596fbfe12caea49ce7a6e7a65adc8e5b69c37bcd49baca2efd26349209eb628f7a77c7fa3e65da7b27310cd54a44d3ef9e2bb21523fc2ac7a7221382f2a5d5e72d4173f28ba874894d63695ad2bfbbf9a712f123f36d6ecbbc7fd58fd679a66f85f9c87654d219d0381e7abad468edda3716109b2ba9a15617eda33ec3cf4a88214b616b32cbf48074563973fa28f154066f91ce466c2047cf0c1943fe07caff242f5b53f0d2a53c9dbde351cedc71b94829ebe3138dc2766fa82e99bf0d392610da35467a9492fb01cb442dd391028a723c823d0637cdf2ad21d5a28d4ef43efad33a92abe2ed9338ff20c471b520aa31c8ea607cd45eb69f29ce6abdaebe260e2f8ece3ebcd3c2c75462f8da96fdbb607c116dd430e16bbf7c488daa0ca66e39dc092a90c319720343e3a4775c89ae1ed229f4d87f1da6ff57c2ebd71ca8f35f9ed5d65848de4a4a7a21af97e876fe2c9218ffa67668460e6e89f903b43c2be8857c1a91ee66a265c1a560046e89ef4014fcf6409d4fddb500f6c507b8f376d66655729a4dddaca48a6bb9807f1ea8ffea037504057b6b33131733d37ff2fbff73a0aeaec923a7ad6de3e6a82ca9e87ad3dec65e425dcd4a4242d34ec2c34ade41f181b5a58a8980f31d53b37b7c66ff3ef32b31a2b532398eb4d3d6765676f20692dbf226a761978a7bb65ee667a6873497171aae013e561b9616d4283b7b1d2d82a3cb7e7b22adda9c8ff25f15b8977a151149fc815a6798e3bf576d98a5ebab36cc6ff6558a380b8218145989ace63963c923b4818c9966b5c5f6cf8c0308f49766d179e76fd5464a1831106d1b9042a3c4864ed47e44b5a90a95f09087bfc82394b584d4e09a24ca2c0ac0fd7bab360cc4ffdf555bd9cce59ef23d679b6babb68aa9c64d0e3b7357196b1769752d45394e793727c9bb26cef7e46ca49c1ec8705a59de32e7b397d3bcad64e5fef3c67fdd78fc2abff9aa7272497677730e331e636e4e634e731376330e13137353764e0e7653336e1e2e1373333e6e2e631ee37b9cc6f78c39f8f8b838b839394c4dd97978784df9cc7904b838f8cdaff5fe7066ae76821cecbfcef411447e96125334d6575cd9c8a00c2adae86b87b2dcc0875318c823a0a5fc64cac82b7979f441df87fd4ee42d32a6d5aeaf7aedb266b9d66364fa3e9749e8dbf1d87fc0f42162fe4fa10f220c5f2b8ece3517f59a8c7908a61a63a8e13f3206818ccf9de9d1ddbc598b758c667838ca4ba2b780bc549403fddee767ed7ee8099149830f28c61f90b58fff6ecfa2e1af98b4f067af90fba4ebe555e14ddd534fb77e648c3f921ba47a61a7547bfa0238d196300b1187c0698ed24be1b3d871c9552732350cba621ddaafa79c70961277cc4afe95eff89be7c603c7ebadd3756519e62a5dc1bc66ece07f4d5d85f855ebf3c71223d0d76d87b8967c5cbdffff6c7837ac3bf656ce6ef71c35b9aef8e24f92efabe40388ad961f497a226daeb32c6e6d9313cce17ac33ec435bd66e2e8aaf991fcc73267fa8f51ee4cff891392eb8defba0ff2351da0b95fec60fd90fdbbca3efc5031c9df9e878f830118078c3f6823bddf569adf0c3d17473dd167fbd98bf6d7c243b2d0b45f401db198dc1226da9e35fc25f3462c446bb863245d2a0023423e91ee6dff7ac7610c0d82df9a8d12d78a4a04ef87c6cef84aa094fa221a094d4f956e631c41f8812d90a0ff31ea88f4a2b982ef04699869859a672a7ad7d2315cdaef820c5e5e5e5e5e3346b9b2a0ebb480d93f88048fa7a680e6faa871b05adffe45bfaa7fbbae605c5ce26231322c799ddcfc260960f9a7b5755df008dad7296063a2a66355d29a07e681dc339690102406bb6e3e2167f7fce3a17baaa2db115e804a4244c68c721527b82c213ed147470fb42edd07ee0fbdd8dbf18b968228baf93c858a0d757925ef280250e9a47ac7affba225018fd5f4f7c3cc9e66a804f203a83077dedbe5eeab70897f814de7a6558f9a2aeb7701509e2d67ce775d655e3dc6e2cd1b3078560977bb4a1f40bd30e96ba8b4c9b997e898fe1c4b2d408cbdf100ffe1b7edc5aeed77aba120d3c5dae98b14d4c893fa6f8c0128d3ef9f1ce366b8ab9c409c2e1e4bbde522381c9b11fcb69db9348f56f90d56efb8854f2532042787c2db61006d14ff79795b6f88901964f111aad8597db5c0cb41c024b9046db4e41842bc363980c43ccfeb9782e6bd41f36d3b8e7257a69cf3e1325e128dfea7a7ee0db9dbf0006ad3192b8c4f750c12e6e1839d39d30c59ca95a1cd826fdb1984d5624e3e19b55658177d74f5882c0a978c02507e29033f0e5956a1b55e3ced7ef641e32461b87419408d5a5f9d0eb13dc6b1cb554354134f558d5ed5d602e65fbfac40738de052beaf6f61eea9502ecb0df12400552e72f9bded9400dc0cfbc840b8d2805512b57f12707faab63396b0ba4dbd35b5b431b4a3e089c4444322805af4ae74b38334ffbe0f090153d9cc02f193778841006a923757122491277ed5633ec86ce31a09ceb6591d20934e708ef19c804dcc84d1a673dc7b81ad0da7f025e5b7ed4a1c7102acf93b3ecbd00dc5b4ab4c510d210f01541803ed5cd5484e291135889d77cf5fcbd6c1e4c900957408b79638cbb3624a535ce06e627723f7cfe0390228db8450c5e82f1b32e68452e92206fac7c3e9a20b2dbe6dcf9dbe7c2dca4ec4431ae8310c7b0c4f6ae2360d9cbf63b604a2585a0d8b0929547d5298b245dc1e6006d697df4dd3c418693058ee2039ab0ec5b5b02f560c083d270e4f7384887b5cdf7ca4d5af6617f6906a350050e2fb19cdee36aa6d098631bc0a8deaf4622aabff0cb05028b20441122d6c924f8ad60d51a172529cbee90358009a8796be20c9963c12868e0c1e9ead40627aea0454f291c8cc11307fae695dc48ca7309de20e41717307a063b69f6fb6e725a617111dddae55f41718dc19d902c37fe57015e9e1e99d0ac976d6833861040dc37c00b5ef2bc10dee056cb2b426c7c017ba293a2c3086fd000b481de184a26a6c7110aad482d793f9f0e1db7c5e00ea1e87ea1b5353e427891cd0a4c22284121b08e4b100f44647db83dd03b58df9335f04a2206d2f73460d5d6ec0bed699fa9f22a2853e16b193798226af6dbaad07d89f62189f3bf03282896115f8b85129c6fea4b38d404e177d8efb380ceb58ad1fadb6753fa8db2c92c95be1dbf6ce51bbfefbcdc9dc6389af2cf804e79a8d116701d4260bce72bafc3ccefb8b3750da32f389f0b7799a807a80e52544586da7b579f3c8468faac10f4bca2be940d87f13875293cfdef7195de722d4d2b8d84ed58e0f50e9987baf3e4443715593437b5134ff9031bfea3e24e02e3c4dcf7a7f52d46537b9c969ad09773f8527de4bfddbf6c2050e4e77fde7271ac6c2912f793fb0d60c8903db6d0acbadfd1c467bc6c0336efc298a19069a1e1b00b5cae50b8c3aa3b4da04aa8e51ff9c090f27bcb93a805a7643d7886359cbaec5999c15e5143276516dd60b406d82ad7d167cfa492aeaaef0c7b75566551187dafd402583349374e522b96041f2ddbdb304b8855e1f9f9b02dfb65b85a81de94cbdc0fe924cfb82ec6433735cd61180c078ac9b47f9a81d4ad69bddc69b85f451a5b98d2280fae429b26ed9a51cad4a513e521c201c95ce9ed304d42f6b23a608ec7f39265e5a3ea5a67860afd31356021c54e76805edad32c40ab6f1c4c6e034d56352cd71032117cf4d0eb7f217fb84c9f53eec653b62ed92bef5c0fa6bbe717331be7732241fdd279ab8e717a8bb3305b8dcaf8a2a1df0e232a5026f8d45e80b5ae31c5b4f02a88838859b589759834e1c882c4f10a68946fb7306e981f9974e6723a478fb393e842d41aaacdd2978550febdb766c76f4dd63a5500ff9ecdaf098dd294563417a60fe7a5dac2babb35fdec92da51f232c9d0fdaec72074ac2b6ee8674a43dd26199b22cdad9103a2383eb840502aec8e8eee6da63831275556ca93d79b62ad41454c01dbc259b4be211bff6c995b9f3249095b1a50a4e8eeedbf63745793256465935b4b9487d4c988409f371cf00fd8fedec833fc24bb5776e8cf3f548f1a0856b22005898169a987c1e6fb4ab623944c0f70d888a293c5901f607f45808d27aaef6d1ec493681421b120dfc9af7803b79792a89e6c4c1715ae671a77ce5191326ae403e80eaf5dc67b63d9f24a4a3d85558978e14de8277110bf08725de7c827f78a12a641c0e7be43e97f072669f1b408d2c1639f0ba536ebb3842b76f732bb8e459da650b30fed47d1fd937c7375edb637bb2f4931d3f405d4700d0de61f27716fd86bef48dbc2a9192867645f264cc075065d9ccefceb1f9dc76ea758eddb0b0c588f9e2bf72efdbf613ed13df575b34a2349ea4c97e4fb3cce49b6a0026baa7f18e04383682f28bb1965f661a6b747cfd200e7ee3d2a607ab150a79e6f7b02f1073494300f2838069f526e9e8fea454a3802721a41a079b880db0bea163c4ca9769a2b20c828cb71153c6bfae8e5903fad34188eb6c5cc07c3353baa9111ead7fbb2d7f1b6039f0f3f9a8640ed52999dba2e6ff94992a943b3b1c484ba2be3b0c65e00fd3252888459b7dd124b3e94407d4af959fc35228b4ec0dcd0bee7aac70704d1238354f7edb7e37347a189178d36062bad7456cae97e032df18a807b37358b81f772e9797bcbd69785fbceb1124c52ec0182018eff3689a863b7fae4e48e9b84142b6fa021db07fe57d06c3c385724c6d1374cfc2b5da9db62cb281f9519d8ec0c853ee40cc1e6c93f4993ca343373602ecebe3313ccd52ff7a5c6aee400783e948037ea7caa66fdb93c7acfc03d0506e7764113c6a4e3ed1bead03fa57184c52a8ec188fc4decb31fb6dec4a2efb324601fb2f2bd799b05aa0334a6d6c2efab974280c97c30210819385e40d8f99056143c09dd93db7f0d669592dc9f9b67df6c1f6c86eefc76381c3aa40a27b640c23aa0240380e898881d04a22283cdb43f24430ec51f970ba35809acdce45b55b9ac5bc83acd2164c2fae20d7739b0cb857ec1540249ad74a25158ad5c9bff95965522fa00af03fb991207b57df2caf8b60d99740b1944572ead002fa7d776af42c25344da41aeb296f7797856c44251ea05fa9d4b78966374a0442768784db7c6a3569c2d00154c7b052e2ee1345d480c7ed14554deb13234e1fc681f8a92c92d1194ad8d545eceb04fdc93b4213059941a0c615e7e9587fba0e5d9266f45331d586c8526d536d00b57a3241ca6ef783a105f3bc65933c836a61001376fab7edba6b3d5446380fb661dfaaf5f063ceaa77205301f333b562cb77dbbaa725ae840569bf06be96d2c00bf87d017ba89aae33120af1e8aa24363714a602eff1da6fdb2f3637b3d63cd03e6f9d04129bc32c8e4f795600f2db7342ff75b27c0cf305a20d2ce5463755275b1fe0ff565ae914bddce7bb5dbcbef3c24f2cd9b15dfb1e605f0e69867023208fddc6ba82f59f3bc345142a9f03f2ad654e5aa00e4b73976bc389cd8580c4dcc6ed0db0bea23c6117f64a2942cddcaf11d2786f6ea5c5d800e9206c82c522f1cb6e2c8598e86ec43b018c69834b002af13c262983463cef521f0925957999f3f3d292a54fdfb607d8653a9a0ceee2b5a70b3e637f16c229e3690ed8770f1be73e898ceed0778318eea317cfa5711db980bb738b424443472f2636210e9f0e8c2c6838d1c14901fe658e6c52a065e0c91d96c7fbec5285f7cee56db28023b91da57e88b183832f2a5c2670565f51e7308af481e359d40cf7902ed2d7035ba4dbcd70544d6fce8d0801ffa6048b354afaa94e4590c7855a129c2909790b0ae01f28d2e3c3a7bdab7ab37b9fbb755e59e1b69d882290527478cbb257421e2cab68ce82545dd75d13235d77fbdbf6881d3bfa9759a4e9e8823c287827958f28d6c29dbe6d7746ab887c650fb9cbe2194ae0b5de8f48aea1011c0b1545b285f0b8a6fbba6be63d25e7a9097daedb08e44ffaa37363e3c3e35b95a9b50c18fc054b82c31b00369339ecd3412905784119c497212974198d6a79261fbe6daf864189832827503384b38f60648b17ea47ef016ab0fd6b9825bc464dab44224b5c1d0943b560cfeb01fb1f9c95ec3755ed3a774abf1882e5cd338fdd8b04a0fe86b5c6093561bd605bbeef9a40277bfea053970938fadb95fa8c238763151fd8b5385431e7e2efb46b07a042bfbe8013715579edfe8832f54e1d0e0d9b607a1db07fdaaaf76ec74b25e0bcdd56358e823746bcb9520aec2f254dadc289fe6cbac6504ee3189ca88a518507002745ae3eac9b69dc93376c9d01187b6b156dbaf99340fc779861a53665d2a9adb549f6ce53233d163bdb0cb05ff5aef1badde924210c4fdceaeb2488aa3dedec5e00fe793697155a1a91ef57763213f6c9e397bb012480fc534744c7aa43f4dc92c02dc8342d5f412a195d0638d1a9268bb52021553849f22db5075f9cd95eb455ba02fb036ab835e787138ea60cd6cedee2d4a22ccf43209d8a7d2cbb625af8b504661cd11dd14484f2253c15c0e2f7294259cf0c4e155b8d695d581c718e9576880958ff2ee1dbe62127469ed2cda25cb6f25846cc3c8940b9dc5471ef66f8f1d2457e439dddab14bfc2a5ce4f807ccc157bca36abcc2d6c7cae22f11cbea057e0d802eca7c059ab5d260c6c4ea8b8fe48029c2a93487309c0aa5015216ff65072d54352c1102f5528915d98170ff0cf8e7b39dc5623d244e3574fd916e71426d0b1f081fcc1bbc3e3a6264b7792c42defd8dbe48dc96eb58f81fcd9ebfd77067e0e9a0efa9270f17b0b46c6dbcc0c80ff93c5e71369bf2cf4e6d57c6c93f3327ea6e25d6755c0ffbf7faf2bd31be65eb03e59c38e2cc6bdfa3270ff0b56cf2e111be9d1319922fd7403275fe4b6922c7047c224dea53e4a6e38d5281beac8e94b9d45ce3d2e607e322583173c1e4be5b089963262eda167e81fce02fe457cbdc476936419a29e5ca043f002777a99333510dfe6d4047d2da3aaab48b6a0a4cb6084ef90cd2705306566eca8aa6a2e89610bb5d1e085be387443f45600f3eba6707b88e968b85287844b7ad43beffee33676207e614ed7092a8e8b796931c30bbb6f256008d1bfe40ccc5fd581a06484920c44aa67f073cee33bb0263967407c0c6b5208cbaeadfdf1b82c75b8d2829cd070146095a0f2119fd5721bbfadf966689dd1b9069d33c20ab06f37a858de093da47cc6fdc456801973aad1d4971e60e9647c23d534d12c9c6678e70cdd6c9df88d65451880ea8c766824b1da64f79a54635b991b093784ceab14886f390ae07d2673b30b7b59976f6188d70d4840e4032c8735ca73ad3e05f7f8746e9933bd8941bee5228e08d49c29a52d3e183e4ae3d452e7bbe883aa3a985659035814dd595eb8615a0dd343056b3ccf5a3f891815d3c5feb65da6565f607097848e4c852f310a7ddb0319a30658bfa364b722be24280a5c525ac452c66117cdd908307f5c2024f0386cdcce7b46a3b8b67a188dd3ee2140827389706971ab8f6ba23bb293328f9ddd38548105b869da5845b1902703bf1fedfcb46bbd5733387cde0c607520ac0c21b70dff4cfaa9ab159d16a3f02d797136c0f2fae14b39b60519173fc619a37339c2f0dd164607207f33821ee3f27c3fac60e4f5605cebd91766540f78e056200383a885d306c7476534a7f867d1475d9b926280ff4e656401f16462994fbd909f3fe952be6aa9e81590bf4795c69ec9c27f1f42c2d7e02887511fe3d9e61e068ccf4d003d42425752d47b675ffc850e948b7e10e07fad3edca015cf891cb55c6c2cc01a550ad9f2370458964363829a871852027c99c5b4769002cb2cbe0403e70b6517a4868f3e740df7bfd332e95615dd7c50d403ecef95b348b24516c13d50bca855e1fcec0c340627c0fa4f073bfad398d673e8d12e3df01fc137acd85004e6df4076353cd08a1a7e45926ec73ee5edec4cbf3ca09fc359624b0ff58e87e07de6d0d329b82a6242328123e9a40fe574087b3e7448b2ed6ae6da9836c6a48440fe34beaedca292377d4b935dc034bff980f6703ece1bd8ffd28f5cf0b8448493dd76e3242aeb5d75947681f38ff58b97acdd2e8b99ca77fd523fd1798f8aa063032cc2569c5ad14c3b5a3b5389484ad5ebb5528784bc00be59fc214b4bbf768fabd1ed74f964b7ed22fce7af81f85dcf700aabd7a85efd54356f7cd69e65536c5c0ad8bf4ae65464b63e8b2ac8317f087272b92554d5310cc45fc4c13938a594260a125ecae65d62b877da3bda01165af504d7a5b88c6ad9251682854fb1a121c133b780bb109e5e6e5f84c82f0f1676e44f5b235ed03a437700f62d8b534fe26dba6c415245cd40ed733ca3ea067480b541857550b6bf31db3129ffbef1e4ae8b8dc33b58c07ef6f79f5b0c3ef359f125f290647fb7ef3b6fc1075c49854fbe1c243482b0feec1898d5b87a82825b5106ac2f8a278d875aa23dea1ec698d549cee79832bb5060fe474b04accb4af46013c956c53f914044a3068502f7a130977a1770def9e09edbcd4dc4aa227e92ac88e604fc5f292b25d206c1fd793c95aa4d24cd3474b80fc071693b397bc04e71c1fdb5d1d0b68ab898ea67f32f00d694eaea377e2d1c3b19106e957dc4fcccb5b8162a008bee2bfc8f344fc69983a19b1dd06e22dca0f9421407f837de6782012e105164b704823367910de38902bf00ac4808750f22ecf1b337287299ca94a20d1488bbad80f85ac4536294bede3ffb8b70cab074ae6e9f002906f07e3850e9c58a2ec3a6b15ab9b3b73b48bd982b4680e364779f90af065f7c1e3e4aa1c4a5420da4f097a907ec33213e51c3de310a6d851e5406d6c47cd3488a29109f752d4ee3f30fe23417b4969deb97bc2070e1e80758ccf1667d289a78345607a0dd16aad769cd9c494f01fbb54d8db028976e7070c24218715e1c18f104e713f07ece4995687bbaf0ed86303d0c877d9690b3276880ff8c7dbe2aa12ee277f6745c5b0189177baadbf32dd0ff721f0799b8f059cb4374d9ecf0151c43f2bc7120feaf1178d5f4c823af8ccc71fc86f696ddc3205118e0fc57fd328d0885bc1011ff34d3ea427986d2122214404248defb4cdeb1cdbe7ef628ffd3bc86ef8dcf51bdc0f94400eb939bb6c6872afc0b274cd0228c4e469301807c6925553c6e6bb12fa958649cbb4737307f504804e49f27e2395196d523711291789df7878925252fcd00562ba95c6a8fbd1a21e6b7bc1333b65ecba124151c807f862cbebddc100cabf6ac0ce153df4d47e4268c125d20ffc346ee3ee549fe29083f9338c26c07df1ba116b80ed29ecc2b09998014768c8c214c53f09288a32f04c80f521f6ebeada5d5f62f30d73a8e192a81ae7877089cff99bd1d10ec628d871436485ca87dde6a8ad73b06944330fb8ab73396f4b58ad1b8f709843b9aa293f701f9c11bdc035aafe20313c3d16aa18d9838f3a38c8680f85ed5219af89ece032ebfc6b041af19b192b74126c0f949fa334d288ec5b9429f196a0c07dd943656f67280e53765c94c780ef529759d8774b76c9433d6738a23c03f9b208795f610c462ccc47d4d544abd8ec888c5450ec41ffb5692178aea5ac74e2136b2c66c2658f94780ffcbd7c697156cbbe76640b0a78eb8e919ed8a08565d120774ea20bec50ea843bba59dfbe9b53ea322035032c437e80d71a99ceea73f7de2d68db37a4af85c01f00fef850e410aa17fecdcfc98fed1c33c682983f5190085b8df7187156515d35a4ccc2494992404c6710d05381fefc37fc4b9edbfa15877cf3593b9625233e1eb3b8035a7cf45d2913dcd5b0a03e2a97b188eaff369001fa0ffd4ae93c803efeebd2a8cb4eb48e87d1993ecb90dac7fed734b1f0a2bfc793837e573a974a326acda7dc03e4837f79e077ccdfe5cc2f419f160c826fb60cc0e28075a47c7910f901c8811d0e1cc2e3be8aee97b126dfc6d7b5cd216cfd323b9518e1143af1de787d336e5f74c00ff93a941769e695e542c7936abdd5731380bba10f0dfbe78f2bb88f1520971984674b13e2237b8b08b04e2d31bcaee1e52011fb03f603f91c61a95dd2277ec04ce777849ed0c6586e55e4fe77ec96d4229a8ddc4ad02f413efe12962b0799966d3ae802e131fb7721e320fa0ff85905b2c6f6ca19cf6aa5a896367ec2f2bdb1580fa072c34ffcd82ee3773228f20f31031d9eece665103f2f138ddce4a405a50a9f70239cc7f7609f5913a04e0bfae74bceb4a84afecb399a51b6196eb2ebb404506fc2fcc410a198287117286b0e70fdef8781c9c1eaf03feff247f54dac7d04392f918d6e302e63d914755cce0fe2f5b6bb0acd6b5d1649489984f907fa741541ec86f9d578a9377daf35ff0d938ed9caac515b44c4402f52fc13709c95f190d39af43669bacdb2c7e95cebf0be42f3dd99cb532f10791d3cc8e5b876f9f336cde9007e28327b1b26e0781354f956f10f9a8c9a521e0df1b06d697ed2d65a2f4c5212fc9ebb6c7d5dce776fceeb97d807f8216a187ef5f91dae547342663eca1848a6605f8cf15c66e7055b2eecb3db9a9db56ec4f21784d6c80fd5f4855a3d033800c9317cd2a361ede41b593c41b98ffd4b90b6a8ec571c2f315e12f0ab7d3e866223a81fccd40230c53c2fbfb04aea2de8e24e3748cc317d9407eabb7cd17fae1f272ce935314a991d49edeb9fb7701ffdc52f1968e4a1c154754f6e9a5bb112bd5eb9ba7807f5812c462db2f49368d9073e383f6529b47a50e0d507fc0bd374a01717f54cddf8d86492673f286f8cc2d207ff02cb2f0d1d7c4cb209154d286b18d84f7c569b919c0fa9d6b0b6244d9c6e989caae969f6d6e28dfe200edcbaddbefd9acba389ed8b6c563b0fa4890e91a00f9ffe2d735052481059ca39326a7700ad3d808c7879140fc10b743275b633440b91478961ef2e9a3f118221af07e612915b3fc1f2729c242e8abc80d395e4db001f11ff6a55b0787fe8c4e47dfa493f669a68f2324015033bc202c9936f2aa8e87feada6f9468269c7caec328073275112c75a89781117914ff9a8f8312f7e762803103f31ca2b7b41ea653a3d108f2be66aa6b878e75201e407d1726cc35d6deaf9f8eb66b45c38ccad6fe599022cfee3237374f9d44d2be673ceb2dbed2bd037fd8880fc194145f76896795b1eb116b1a9e0667ad87e092f707ed33d54333a8ac5df185963d6554532175fa8b308cc8f5fb4cacee3ca001423e5d20568f56926bc3b5040fd506ab546d5472cd964a4646c3359720efcea8410e0fc7d3d0aba1e0181d600c9ff400ce9692c92bb0b47ffb7ed1f6d947b78e31dcee8bcd03f5d10cee8af194e03f559240a990aeafb5a9d64fd086264c48a8235fb0343807c444d59c5f5dc41c11af5b05de378575c288c05ec9f3e30822a3ce9a8d6a9150341a4f73b0fc872a680fc430661c3d4e15d969b3767add36d3624e2c5e86481fa9f70e382d45942ef3de4838732b7123b28448cde00ac68dbb93768096a6117f7bbe745b11f24eb977b9b02ac9e7889f44b97e1b5dbd6ced550455648cbb4cbaf80f35d24d4381384f2bc249899e299576f560ed983de0177b988ee2051300eab7ef5e970410bd7c1ba4b81580ac4cfe5225b3111864f3fad98910b7ab946f470170d01fb6fa3679c4b8bddd70e3efc2567ed997add56733ec0ff152e7df2162304ff22149d4bde5ad0aec2432e0cb01fda95558fb5932e5f978edc187ab5ddb76c4bff11c86f1bc35e50b7914c850acae042b689a53acb65ef03f6dbbc9bcc0d5bcc8137efb92aba16b5b1ab69cd0dc03f93beb1689fb4a328f7ee598619be624b77c2a42503601f0fe53a93da25098959d5f4dd44f7d964984c006aa8e05bae9baaef8bbf585629d552e7ca8f7dd45e05ea07e042589433e28f43293cc8a1c28845dfe6fd7f845d555cd44d17a6bbbb1be94e692425454040ba5bba1ba43b04e92ee9eeee0ee96e24a4bbfc6edfb9faaef7b7cbf2df9933cf79e2cc203180df8608b4738dd6da89da1190f97c219eb30f0e7900ff9664cfcca13b63f4dbed825f2234d34e68136e10e06f53b424fe9ade3337102eacf395ec3dfcd18bd927407f6b941847b25b4f2523da3ee590553491a3fbd00cac8f621b3f0a6768884ffa5746fe7b35084eea3637c0feb8e3508a7ea874e1d75b19109e8ebf6b48693703f6df6c29bdc2760b379ae0a17cfcdc07e804076611e0fd1330905b9c5a48b582017a06994d360d76944dc0ff2fbfed5d5d326bbe529d3b10ee15917e15af2c0cd4e738025506f5182305ecae18945aee2eaaf2dd6aa0bf8986415cf95e66587878527268938078841bd50ef01f9f4ab0ef5dad2786d09a26f2244fcd387d956e00fc7f9ff85827ec21bc84eab54a88c522aea561b80becbffca371ff17474e1f694d27a4704da3dc3f9558c0ad7ce89bf46c5afc6b370a47183868eff47abf932f01fbbb1a3dd20057934628ad7dbd6c5e29d7cf5e8638e6bfaf1f7b37e185d9bffd420c890ad13bd2fe5d59550ade1adb59d5d372117f467505fbadaa628a1c83dd0cb8f5b37adfc7236fe4dfb724b4631641d6f1bb1f3e10c0fe1b166949697652ebbd39d5b0ff2d263988962bd2f1dfd74fd55110cb56e8f161573d593ee8134bca134a02fe5ffb1bc785db2cda0b2279cf852a4ddd26d58921c05fcabdf9f37250dab4e61f69e0fe2c4b2bb3964f18d0bf329718bcd39c8216fe16bea7b173aeea02f74d07d89f6f1ec215261da404ee3548486db717867b2ab664807f0dcdfac498dd117f3027c0a5436877339a72b913e017a6e6b5319917dbbd0294bfbbb16594428b9900f329e14a7e2bf91414d8b41978133c595522bf837303f0e17e1c2b4aadd791b49211ad3a1f7d4e00c3c922b0be6e15e627bdb9828851238b621ef3d8fa2ca30d01fdc94ac970443dcfcc1831d3b7f28ae0b76fd1062db03fc4b7393a9abaf885930dda09efaa4369e0c9e601fca36f636a28a32c97b5809f26f71ce5c17161d90de83bbbe6219177adc44bb39a2719cb41f61f5cb4d880e717f32e8db1f6983a12ce4c38d0bae559b152f601d0bfb2997a235a2be536887d9fc245faa814c7276901fd34a70debbb54b630f1a01c36017ce609debc341ce0af53e8fcb55f91de51a1ef23a090662d606d28050bf0c3073061ee1c1a5902dadc673397eb37b95b9822407dfc7fb1bdff1717f8f3d43cfcf70367cdbf512594091f31e3ae0e2dc07f50533ee611bc531b15ce828106fd3d3d0bcef606b89510e20d82c4f2a50542e69dac4a87f1a74149065fc07fe0be7029e23512a1f201fb28855edeb04e808d16d83f4e610502c4179581edddc4c8e54168d57e423940b4123bcef2962d5a0293c74e25d54d7f8c77d15a0438df2a092249048e157a2bc8753ad56367bbdf5e0d80fadc5ed3cd72d0d98340191d2d679e1c40dfbb6b0cd4479aeb5688970ce52b2ea743f63e420d440ffa72f092fd57d7bff3a3247d051845b8b839c7efefdf3ffd00f8f71605a8312329943df63beb0d9a0b2ac3525be00a2dcf8af968b8b30c24ad6b011745d9c259dba755409f7bf9c5e2cc206361b5e1db45e921bb6be9316701f853e82a954b1e0a499a2cd98df83d3a54c909f60200ff5aceb7c6207426a14018b6969d383d652e56e129e0f92c22f5bd34e917640a50eb8618e1bd0f58fbc50df8bb7f556194cacfa2ba6e9bb642288ae25e7ab26d01fcae6ec92f3be6a1373962ea8de16e5f248c490573c07fced8e6187fb02cde7b3c5bfdc0f76d793dfb1f1df07cd8c6b4ac60659cb8d886e888119caf3248b9ea819176c71abdbf6b8dbfa9bd310d9fd44eb2c3a5bbff02f8894e7c45f8ddec7ed985fd0c86c43bb24c3e3505208e12252f24ad633c9563f5ef48fc7093bf232909310fe0b7ac6af8e38a3b5e542a14311dc2d468e0d0aa80fa9b533e6d273cddc41235bb3f6b3265154bd28807e82b2aadb6f01463064a5406aae2c3dcca04e7241f01fd3e56c5d9013db4a9fb52a5ca168a613cad176d00d0474fb4f74a04aacfbbc8a2fd5f7ec688d0ba6beb00dfafe333e61e1a340903db120bea569db661875a141099e26db741fd4a9c4b5deefedee3df1833de4cd62ee0bf9dcb90b9d82d44634116d01b2076d8ae3035f602fc252647131e27eb974a932c3e95c68fce228b8b7e407f8237debb6860e303fd608b37f7ee1d1adeabc323d07fb2f62b717f11c26aaec8c4b62543a94d143eaa04ce676ed75ccc887c413a977a0b4eb90908839a800160fd4ef9c585f0700f732bc6b94a9014fdd00ab463006ee5e67ce05edd1f6acef080d77326d8fa13d06fed08f8fb15cf617a5ef159ec5b8e1715a83894ac0f680681fe3f7539f42bcec4f0696501f684fd17a7c5bc9627a07f5d38ff3acc821f6d253965fb98c27396d6549f08e087a1e5dbd91e1d445c17b2ac9b526ecb0085b122e0368c74b5e0b69aedadfcab19648de92fce5d1c6ce88d003e2f7a83c3beef297e6cfc8b57d21129cf2d2607ac4f62478220552ea28967f2977d5156ead599084c206ec45ace8e6fa11f8862f7454e868510492a67f104e87fe7f350356f020e4d4df26c6dbfb0f60961b1a201fe794848b63fb47bc1d69150dee89d10136f334a2200bfdb6a685016d4805d9c9f4da75af84e3743db631078bda15dbf78d1e48375eb0c1bced3a7e5a48c334720922d35373bb532bc87245d742edd60c7d420697e08dc2a7d7ca20bdb41c34ed3053719d26437add79e1d0ff8834a6d37741c08cec34475188d0ee56d6b60f76280f38db5e27a68f7aaf27cf674bc7e6f688340b15e13a80f2609ae8edcd9a590dfd0ad3db739c721d4d1c901ffd9e5cf0744e2ad4054eec83258527dcb8b4cd541201fd53f26f634b8b5ce1462e73ff6b027a9f877ec17703eecd5a9d4bc6bfca718bf0ae3293edb1fa49a0e073c1fb99fcb07b7de533b694586f2db0c56cf21f0ef80fee1cd36ad892914b3559461e2ed78c02cf65d1839c07f159b474efa357efd229e78f5ae00b76dc13b960fa8dffc750dca83199cbee81f0f58591c3ea3e15082fdddc9c187e97d53a2ab136b13c7311ca3a33a4a61c01fbe3ebbf6038ee37dce3981a60342202f3c7a033d30c3f2e7ae4464ded3876bf53addfa323839a8711659801fb4b9c5f7db22e228d3a14fd41b69c2731e53db02faab76da375724b2aaf73d2790a31d1c5f1d573c3b80fa9078883a5d74a8bcb6eb509f8cc9cd68de2d7a0ae85787d456b80af2852c35323a5f3884fb9b92771801fdf25f8bd4997d6fbfaab37aa0c2ef2edbf59ba534204e59369ec7bf4df6fef86dfbd61016b9f83e8f840fd0ffa87c33c30af2a177834dcd13ded4cf19b55cfb80bf5f9515537b2f1405e14b67e9ecc7b7d13ebd6f038c72be72538f90e5e99827c147e57cd11487d9e21405f0a94b2c263a69de6aa77dc8888e4ce3d4382cd33e807ffa6fc80cdd6f2a73adecc8c6e69fa1734de1f5007eefa74085e203d2d29079a46f552fe5b50885d531e01f86535b10613fdbb883bf09b78ae0ef148f963307f617e4636d03fc2aed5d484c15d94852c08546c326a04fbd27589275d97f31b475f6ca6e41aab7235650f5fcefeb883383c9dfd8afb8e036f4cf23eb6fb2ae5bf101fc5efa422a1f5938b2afed1ed330f16e444718550de067fedf15a0edc39cc47c04c7a5d051ffde107aabb13456ffa600f8a6f2b3c64921deb37ff49a27047b174606ce47c0bff6213097069b18dfaaa0d5e0ca993ea539652218e83f7ef5852fea70b48f19ecf7042fe093df2067bf02fed048b21e8ca1d26c98c38f428d2219310e658dea007f4d46a3a9f392191ce27d96a9a74bf3bd64b8bf1ea8bf97b630b9a94b01550e1f108251fd0bc597ac0380feea5e29632d96f1c2b1aeb000d3e7f5e122caa11df87d3dacce2f886d2932ae60da6f7c89cf0c6b188300ff22379df87590fad3c04f15d12439da1d0daa5d7c60fd5cc0f53ff29946f0f5faac8c4f7aacb393f65101fc9c76045e5774cccb3d7d0b8f02de4adb0af43d16d0ffb05c9750f96c3dabe2a988d3c4bee5608737a101f57b53dfe4db49d6dde293d7892a055114aafdfa01708de06835e330dcab8730d1c7e0faa2848a019ee95f003fa3ef06a3d868db81db25b9abc3cbb1ba55586b079cafdbc443cbe888c8537e34d4569992671b26b40680ff34a219ce189feacfe8f7981ea7f180a10f473fd5007e69adf9ea21f677a3d7663894f05250ca536bd51f20df0275ab39f31aa03f9935ad2f5e32a72c28183508f01f06e4a64c36bfef74f7b6c28f1ee70fbe6f245e00fe01de951e182d337c034ecebcd1602631f15b1213c0ffe1feed0bb19f3cff26a6859bf376108cb425aeab30d05f1fc78b5b23b9303d16d4c3ff93a6b0351f9f03fc2b96e29ada06f6b3a27428b3ee225b9c8e49d198c0fd0c221e269d7f8e8c2bd6f87f1649bf84feb0790d00f8a17894eade5f26b32d2a73dd2fdb6f0e88d56e0800be96e091688498dff542fac97c4f31552003818695f9dfd74b5049374ed3be294214af8e1c363a0e63c18503f54f2fa3d22db82c96f229d2b32cc6fdf68530ad17e8af3596bc1c7bb38cfbe5b761501ff73189ba5c31417f91e205e9921eb6f18b84ab3adc5e18b2d2c3a9cb7f5faf6d6061b2d8e217eaadaae7f78280b622eaa502fa47f86891213a0969d719f70865fa66a7a6aa2a6ea03f5e8b45673c873c11c06cba1744f064b362dd8503ea23cb3665fa6bf2fa361c9b503fe96798c9118b7ba07f13fa33832ec5ca04b952ccace4cb294041827f04f01b2b16a3742cca81593fabc4f55532769b62607800fce6f926d17674dc2333927cc3adcde87a0b3bcb02e88786a2f03faf14c29160d22664384a6661dd9ef601fcbffec9274edcf5a5559f2ebd3dddce2f721bff0bf07ceaef23beaed1b0996feebe2739565f0b631aad00fc450e4bbd9592f219a4a59613afa33c48d3bd043700be804be8a81726375c4f51a8498a21a6987919b301be1fb646641f818d61e6ce21932b9bf1309deec203c0ef8eb2a967fb13f24333c4f1a4230c9cc49b0c6a03fafd090c9c86a60676f2c9bb470a0416859fb87f6a817c970977238a4183a9a61cc79065bfaf984d70183690bf0af4e8623647b697488991f3e6fdb7fce6452909e0d3355189c428396215f507ccf6a6eba9dcd0570dc07f54ef64b9d2e392c8d99ed98ac16bfb1bf313da1b23904f2a80f5e05d6a5789b0db52953a10bd3098bdcc02f079c60bfd7a04dfe9f4779596dfe7505c9fa47300ff2ac557a80f623adec2b00abdde292e0b2c8ded7c80be827a41984380143b9870faa36ab991e939896e1ee0e7e75f8fa6e5a8b58caa633bec22f44784fe456001f9c48aacdf76986b937c4d2a37cfcf69acdbd8d1d1407f7329d09944c6c0ae04ff8d9ab49ab0cd7e226b06c827417fafdc39b199ea2e9f76e566102fe570d16606fa67c47885ce46d52477cc1129636b3bb1aa11527ca0bed9ba8b4ca4cfa9544d084f5585a9cb63e47c64017e3fbbcfe1672581bca5910bf245d14a6c4ed3da1bc02c605dfff6c4f9273b5e4d5bb85bac33b3f457ed48205fb0f56f51b882555463ba67e89f53f10341b9cf1aa0bf2b34e16662bd3a7d8fb24f17e2455144208b1107fe7e8ab9aeaee82b9518b17bab0212f753c52c740280ff3f527fe62f39ef8135eba4c88b2a53b118684905f6f7217b4d777706f77428871f2e09c4074f3c3253009f7e40c6fa2098f4c46bf84c5ee84a2c216f46c009e8fb5e0b9f71fcc997ccc977aef62428ad340c63ba01fe85cdfabb707a8cf10aa76d95d77b5e8d46792e0260fdd6f52b2bfc646d538991c2ddd66d9e1db91aee06fcb327aab7bd753c468d263f44dde1582dcf1c30fa01fef11d0b8d8ba952c23a69b5ff76337297c1b8e58210985fa1b2cc232b93acbb64265ef9daf67578ca1ff007da45b5fb4eac0649c5cb423b91b53a44a597fd02ea1b9a38ed3b7f28a4f4f0725fe85d6fa22fafd6c9c0f5f88eb9f9a2b63e65d2ece34a058d3d0a6e0f708400be6bfa510ecd017d9f4f29962bff122a4a66da2c0fe093eb192455251a86938db559bcd6ca540c85ad3f80bfd6a3fe550c337780858aab56e6532e73e159ef14e04fe8e8f111fcde9ea53815f884bde9d37cce4e250be4cb7be0a0295ecc4d9faf32edaf2d77d72db773dc81fd5d6e0ed3e310b18244cd71dd5b1468c98536152e02e6b75e4df59c041e6a042ffdf4c2f3e60f63bd00fe33d5939e9f553c9da5d82677ce8569f8a67d731ac02fe42cd4bc065ce1953d8bdeb1cc15e4ebb2a79f017d1f91925f8723d52f98881e239a8f8bd29af83c0bd89fd7fb91be8a93c359d0761f9ea9d01acdaea1b100ff66f8a7be1e1649c9b4f7e9dad148aed8f7fdb926c0f9bc7dc05f373d793325f6ec9e508fc42a40f3cc04e497fec0ca106bedac47e1c5a1152a47cfa6f610b302f8ade06829fe2df19fa3189babdd038f31e1736c2019989f61e8bbe0cbbf2e6870f5be68349de1d38b0546469957189f8b5779513696348ba26478ec8bd6991f00fa0f4e7a3fa3f5a140d228978343754c63ac0507c0ef366a930f9285d73c84d1f0c178205ceb5bdee303f867c0d53b05d3c5bab2037fdc04e754c336a73e0fe0377ecc72affffec5fc08a7d2d56bcb85d74428bc0cf4e7bdd6e6d144506c9a34185d0ca26a71249d61b7fa80fe58b821f1e7f08728514557146427597300fa26e06f9420a3f9b32dd325b59532156b5022c424a9300cccbfb0807cfa79605957351c23764a22bbdad0ad9d0de05fc70fac932cf7f8e5f22d8881553f3b932e441a81fee1cae628292c98e9eb24cdbd3a4a421214750f386df9a0a146147d4e67e619bf8e3dbc94e09deae62a70fe0498f2279cdad5cb5ee0c5ec7fac311a15480a00ead78de4cba248f18b736cdb2f773f760bcd4fa662407fcfce208331c772d1f3d4e2e3c4b95dae9cc33a03ac3f56dcc1cb9ac51566e3a8bd7f09f1168d3049ba40ffe2c943f5c9bb8e49ec9b6c6a5ea760fd47ce2e47801f46cf818fffa34e5dd359153d715cd2f803a3a015387f38194e361323627ddef9cf94311e363be362c802fce7fa63ff2652d15e9a5eacd1745b0c82bf8e8e2888cfd216e825206ab487a1fa9eef563bee276bfd007e7174049e8da8129b2d3c887581b2c8ee517026b802e80f85be43e6be76c7ed5558727865476f3cad6e03fc23c68aa0c3550b7d03561fc9a266923c67541315d0df453eb43ea7d9374a68716f5107f6dd4dbb6b1703ebdbce8bd729576b0649fcac7bc990eb19899f7614f00f2fad6dbc8fddfb6d992f455e252d0df9eb373ea31250ff629e063ed65aecb71757ca884f7334e86e055d01fd0b5d4cbd56c146afaf907f273e9a7000b70829e08fa6eb336a479260135cbf9d1b3134427b339f58fa05e857cc9b2f9547748ef4e9a4d72b3d6d8652ebf9807f60af9a2d2efa6a96985314ef85d10ffa107d140de8af15853d7c5af7935d11286c4fe966226548479500ff9acbc6292cb4abbe27ad65d5c7f389dce2b1cf4a80ff87f25fb0fab49a441e16fc55c347b68c7a7bc66f003f976094fc9c2c5fde294bae3ebfee7df3e5f3b623e0af72e2a23be22ff9e6fa9bc4bc9d70b2d564d2e83b70be5e648b1f0e6d962edbdcf8628747541beaaa0702f5f7dda8efa4f64d895d484d3a7a7d18d3fb501b62c0bfe32ad543cc29c12c2b46a39ddcf8d18cc22b831cf0671d54fb2d5fe32f862128df7273344b6ced6e3b02fdcf262185eb3f957b3dd3ed8ccda46bccb0354d72409f8a1b50aaf8fb2e8e536a399efc35fd38cf2bb717c827d55ca2a7ce7fd9cac7b4a68009a791290a64e900f62ff73d36dd7b659b23e3cbbfd0ee9ed9896eed57c0ef7b199d1f4a42c6908de152e9f02a73e738c0550ef8638de1057f3314d264270c991fce8ac9a23392c45d00fb5b9cd6ed766e564c87c396dc44242310b15a1d986f23b83d94b500f759e92ae9e3321c3733796c7f2bf0f913eb0f1fa7d746395a17d584b31f7edbaa99f500f34da829ca78c7504823ca47bc3b822fc3223b6f2500fd88c9eec7cf4205cfed7896400cc68eae7c5dce61d05f5c87e191dfd8f28a3db97141320113104c6c06e48f0c98b61265f4e9646ad3d909f260a3f54278bb00ff912a54a572dbaae6e47438ed28c6577a7f94563320dff5c3fa27d58e09c2d2bde145e509d7cdf968f224a0af9893e5ec7672b0ce50071a2acf9e8fbb1f44b001f527b8ac924e3b465a5ccda7b329f8bdbc5bcfe4163011785634929ed7db7f63f9186358e357574ddfeb07603e4eccd704f55248b1300abbe7fc41a971dc5fee41c0fd87a7d212aabaecc1c353285735f0da6ed44f82b5407f94a6d47473296a0149311a8024f2820ddbf99e13a87ffb2ff43e06ac4b50fd18e2e22b73195444e9ab40fee1e7bf06b1b275620ca6abf11bc8f71d88c94cf151803fca15ee0249d7a0a09594292f41f53321cc3e3590bfb3274d90c2f91aea57816038cb6247e5fa75550fe05f58125caa3349cecca9d9ef99d1cff691f03f120213e4642e0e0b08133f359f73737f2fcf67b34d439202f097722d4e5c785641fcdd5276f83bf25aa6e5966560fe9558062581b2f8a5f0cdf5c09320ecdb9d6ff33e703e610ab574269d295dae9ddd599c0f3bf951efa203f379927d0fddb87726826bee0bf7c672dae7e31e7581f53bb014c7ac99173c7dd93d6fa9395fea29501201e447774737b26ca9b2df33c4729a9c899666a3362acd01fc184d87da0c0a64c08e83bb0861d6dacd87d01e00df47d823c5fd3177f027174f91c9fabecf3bdf6709bcbfa7c287b63aca49934436b774e1c548070e3709f0af8e1dd3225fffa0518e8a2befe2864f48d991d906ced72f761959755cdab750cc98f2a9cbd5e5f94482807e55f077c47585b0d26cebcc5ab31a8e5fe7d0e318f0b7f4a584224049f446fae64a57ab4f29c17145feb101eab72bf9fc74d1e9824835cda9c3c33a9dad9a14803fbb6464686e623d9b0f543cebf6e7e2166f7ce081e747d79d13d008b1c865474ade82ef1e9ae96f4909cc3781ade37113d2ec39a846dea8e21d8084596ac303f4938509cb2d67369fb96c2534f6b62a4cdcf7d244df81e7db397d123cc25694c6473e31e35b1ccac25f0ae03bf9b650a42f6c9fe26386de918cdf289d8dd3fd03f8d36414425d59b9aade87d41a07974a8e5641880a001f0f2443bc4c26e51cbeb092f935d33c8a66477a03f8d3599a4abe3cdfa0633e56a5db01e1c808ad3e179c0fc56227162f74638de0cdfd8d1a2973e285b307e88f79b8e632146c1dd8992c658bb72445f41dcee1007ce466e2050bc9a308bf81a1603f2265d574302a039cdfe8dee56bb8a32e39320f6607033b9f0ee57e9e03f86ef25ce8387697fb7b92cfd08725b23af8962c6b80bfe0f79636659c2de59bdf63838259e2bccd3cba05deff84666f9995897c2216e8d0f0ce1b2b4ce710c4ff1bbc4dd5b0282b6a92147f2ceebce2c9d40b3301ff64a86ffb12318fb88c39defafa57161ac4643722a0bea6265b75c1b420d0cd8b14269a2f8f1041dc4e00f90509be31a3a57035c9f5e7945e8441f59c63e224401f121399c9fc6e5a5e32bbb081e4051577ca5a6b00d4a7de35ac81758ac869c6a8bb0e0abc08e44cc903c03f5cb2ed5de6c9b636629dcc7cb1e2bec26d329d0ecc7fe0824b92c9aa64b8615ebd6642e7b5c4f16a1ddd06eabfcddc9afbd7fbb769c75fc96e940a79082581403eaa2e46fc66c870756939c0f89218a94cfc4d8b0cf8ff493b7afffa050e297292cfa49a8cde6ec97b0502f9949bf7f090f5c9701a4f482eb5c2b94c956d7559007f9abac385560f1155478f0ba9c263cebae7900c0df8f3e43e4d198aed8a2eb568e745468efe2e99597305e63fe41f6eac28503e9120157877df405926d02a5200fd35a742a1f8c87e7961a1712e962a3dc71c8d333a80ffe683570c3c57d5c3fb62b4a2d81e331d2eb5ce81fd9d0ba36fdb8d2b76534c5cdbb7a129c6c51ba105e41b3c66823f192458d6ad2f15e2a0f9b11484991d01f395a4ef07ec2644e7ccbf10482bc7d5526d8a607802fe6b873132b31531dbc4c05c76c98685699608e736a07f4be68c9627fb6cb4a9a4c664dd62d1d35120c80be0877511f9e20081eb03d204860b52c4a5b7a0981580bf58f0fd2b57566f40d1dfac40f1456803c9f46504e0b791306c78566f55e7af68838dd6b684d084f860017d74476d813f9c881d236287d464ba6abb1912131dd81f73f9b0ae548221fd66dfb6886fe1f78ba47cc1f995392434bfdf2ff2cfba6c5151b5a6883974f17b00fe341d7115be0174fe653a2edd55f1e55fbec217e8803e87140331cc7c01cf2cd46ded4a2563dfc9735d03cc7f53ce3f5184c66478c9a3aeefca38f096a1538202f8dd82d370a1aca76c247fbc979fcb963bd3120faf407f6e5e132220d220dc653f6d43368bf651456ed00cc8ffa859bb314f567dea906d90b36ba6d3225011a301fcb5118f4ae25bd3c7a20b49101fa328a7a57f247f02f4b30fe3c56a7a41462d512aaf685e770d0791a81c407e6ee08feb05f33f0e4ae8b55a48d997bf1f9e5a0580feab1d8df2137d307c7063f054fa8f72830f4b0ba0bfc51e8ef736be5c330b9eeee3bc08fae8c1b7897600bfaf8c31d7237a320c615cb51197221350ee95f900f8160d8aeb363c38b45fa18473f3dcda021aed510ad0efa607adf305dd38f9f3513c8ac645a2a90b7f81f3739e77382bf24ec959adde6d5a91a9c9c3a4ff9b01ce8fc8ac05383593226b14844cc2e9c2abfb7f5004007ed4f4e54e37a89208117024a2a14eaefe803c3e01cc772292d160ea77a8151b7efd5337e5fccf0d123510f0379c8f6f0f65b575940ed7c80870df56df98e14403f565185e53d65aabf966a53894e151ee289b93a11ee85f8e0329cdf79d6754b86e8f33972c1203fa7595007fe2f8b4838203aebf03f372c95fe6db299a3af20960ffd698e3663f10f7ab0a4a08f63f787838f6a7a301cfe7b7e262c901c968cb3237a44d1bb4248cc3e62bc0cf6d2ca27c2ae174fa060bd1f8318e2473c36bee0ca84fc4b0cb44bd2e0fde81d81434c2b928cb92f837807e8e33f7781db3d26201b5c4e9ef027dea67af2b04f0335b7866caf0f59ca2074d83c4ebbf2a3af33d9881fa9ecabf8749cb2477276f86e4c379aafa20b02106f85ffffdbb5e9537e5663dde5faa88e8214d0dbd9f03d6c746c920a9c6648c126153d622742cf37773c82e201f77ab38b6c4ada292c284dd8e171a75a0d6f29c02e0ff24ff838e6cdc0b49c8af2a78a4bd55b596c42f407fd34831f21ecb15013f5d4b4cb29bc5dcad53a514f08fd9f6fbf72136c3d79529dff09234aeaebe697601eb234e2d9cc2beebd8c8aab4a659f9621342d65614d8dfda25aa3eb753eafc50a8553c99e9b6dab4e602003f5f751192a420ca0e2512892ea6a7eb223379e706bcbfa47e31988f9278f9a8bd4432c2c2808beede1cd87f061f7e978f0ccdbe76ebc38712f72bdd3e195300fb97696fe6fdebbd21cfb70cd22cdd57b6b82f15a2407d8c2eff24959b9ba5c27fc9843a35d11de3f02d1798aff065927f68730feee3613d0a5bf0c567ac62b452407fe291dcfd7a4cb9ac4f169d68999c88404496e20af8bb1420542fcedf0c7618ccbfba4686b3127e5a3906d66f0e4f0125c4f7b64e93ee5bf96dbb8194cd135f40df1311b6a233dd25b5fb55ae38fdd78fe9a25aab1cf04720480b5c8a3f1b2df9a9059af2bdbb9c6211de05ce8f9b07e9119a1435c7bbba3f37cacf84358864b0c07c9f2ee37826e477625b94e6a35e968fd24b058b38407d7149f7cd662b55be469b417677fa7dd7c991096c4f88686c19dad59f8f767689e2e2a401e3491c830d40fdbd243a4f7f083218fa833732ec92dd26b9475e09f857e0f014f4eb9472b8d68cb8896c9529d90acd89007fcdd9348c0d798de21715e49ab6a225587c917e1840dfc42b6924ea54fac1f2a19accbc7938e97c7e6b0a58bf83a7340e3f5806acada8dcb4275ff23fe5c09503fce490061afb7793a1a0089163328db0e965c8712b80bfe57019ad3794d1e8500ba38a31085ea134847c00f2530f7ad46f2e4aa398f275598cbf0eaf5abfc1a682f58b7fb73af2b401196f921bf55cdf41a74a880cc06f522d75bfc256b8bc848ae6c6942ff6749eb6a8007d6624d730824632eed235ee38a8d273819e60400ce0efc65f568af4ba47dddba3c83c710eb771cd708281fe80ad31072aee2be295e0a2d8f35d44dee7fe7a3cc05fc6f7976075eae1c9682e53f04575232b575b050501e42728119216d3665e6961c4f512921b8230ff02f5ebd963f1e6ac6982cb4c69b3bc56a4534625d00be0a7d36f06ab91f5dc56a5afaf7728beae90661c5c03f98a8a035fede8d55cc43e9e88041adacd44ca77d600bfe4349a57bcc2297bf42ebba0df8ec7a7f70a2e0bf08ff8f7c152ddd46bb7f472dda7bc94500e75b3ea03cf074a86dcbf7f8346d9636655294c23b091afea1ed0afdcd0661fe39abadfb1d8766ef6e7fb2fcbd60800fe928da2e7f0afe149ad16d4088d8584ed304d8684c0fec2dff9b59adc9de2cd0c9f118ece479461f9e8980dd47f3c69b6cb1b4167d9d33cda4f78ecfc43d3f8407fea9fa6dacc632444cf38e35724e0eda462629f01f857197f9ffa41e76573734bdac13aea310fd6d2e600eb83233734c167d99fac1202c6f3a214ad05b9e911cc5fe20be0c0458788bf2df51fd9fb62bdd9b9b000578ee7b0d6ebfe94d54137433e0e9a80fcdd3679a60f9c6f6f854ee7997af3f5f61cb88123191842fcdd9cc0dfaf8090809ceba07a49f8ad496a94c309c71fad01e8bfea021e26971389441198c4ef366cafbf75b77103f35972e55dea846414d5b68b9118ca26ab5ad851c981fec71edf311396392222599d146e50f4a0400b020b981ff6bd94f4154afc6aad2677a4b9bec14f9820db0df0d7ff2e2588a0aad67fee7bfb4db6b6d99d4a67b00c9c8fb8dd6cef8f0468787e18cc55d0934490e353f402fc68e9d090ed60abaed6e354c56d13dfe11192ee77c0bf66409ca61b7e277d7ce2a492fdfce7737d81310c909fb8f6baa2cb428fb9e19af9878f7c219e06a33b0a7cbfea8494e03529683cd39c30df5da3459af66264805f404d3c239f5afc4308391d6a2aa4dc8560f306c07308010638df4a8a4bcc2382bfe11dea70f6d119ea807e76df116f6236e284f6876d62dd7fc3bef7c71c3aa0ef5f411ed0d2dc84773f65f75954d916067ea7ce00ea2be51e35621ecbd93f2b627eabf27835c46b9c3b809fa33c657163d84a8a87ff00c17c441f73da9df008ac0f539a9f3d26d75921df9aefd87c4baf9296ad72017de5f79ef1d317b7078f14df6f59bab9fbcc3eea8b00ff6b1c51609cc4d01880044d791c5c271dd392120ecc3fadc73c75316b984ee22e3292f1294297e9266703fc3bc47bdf8555eaa80ff29c223fb833dbd19222ab01f5198185aa32618f21b0b2801cb3c1df397dc9ab0ee0c75e1fbc266a67dfa8eb3d480bff56b8b75111bb01f98f24f61eecac21889f1487b19f46e830d1c4c2a881fa13748470ccc27be09e0093a2eba08a31fd936015c8d7b5bd0dd3d62c9cefece452f6075910237b8c1d02f397df57eae7933245f979eafdd26f7c3a4a6efbda0c9cafe1847059bbc5107f6d892c2b356aeb3d19555180fcb299038b65c28b315edb7b56ba3c49b313cb005ee0f3537db81b2c8b99ef0d086a5eb6949460fc55d6817c669ec1e540cef66111b7d7c04c1f877c2eef8b21e04fea62fb132d95f6a32d43793367e850e1bacf5314787e32bac25ace21dfdfb6cda8a755d319ad895aa101feb0ed1d4a9809b1fbea1e8aecd4293a6a0683572c70255086ddc59d4cfd6f29da3891c816c43145c2d02b60be1e84d3faa60efb4334a1e84dceca1977a0f36227f0fd395388c48ff3e65b9ea60afa8b5a7ef72ff82102ebf380e3755916afa47f8043a269b2d2cf459a6914982f11ccfe429553835ef9c6743427a2431cb9ff4915f04f0a54a2a9328563e4093108c762cfe3333656ee01f8bcbfdf9c1e8a221452f6d3ae7825299ac56629b03d2136a2b094f00576d63fa420b48f5af27f2a738006f27d21edb984d66a6a3489b9372cab53e546ccac72003f7d04f942d8310277829885799a12d1e898fc960ff85febdaea03526130e7de682a9562b4dcc7bc028701fea36a3ca7daae119d722f718be6c405a7d682970cc8ffb31b9e4ec0ef99761ddf94db890a582e05eb5101fadc98eb4108033644e7f9cf6a2474a31d77d5554e20bf4ffe19f9a7298aab0cd386f5b6f6bde3403c1f1550dfce6f6aa9fd924a55ebef90f04db3958d5cad1f01fcbfca559a6d52fcf571037678eba0c07be96d6118e0075ea4451f5433ef5e1eb6e2147d07d5ba1cff0502fe3acf461be78d5285bec6697ef68f4ff4d2629fe180fef53bcc1f53a17f10d4758bcc2b4bba3247d228fbc0f7d3b94f43d6e72412efc00cf26d21ba6a9b7f8103f0b7cdfbe67a37b5e4eb786a4f94fa6c91390b0444c05f500b2da70ff1e845e839ad53f26f96e43d623637907fb5846441f1dc7e42805d93c09e416d56147cc104f6e754c2df20b6da7a497e5ab9889fee858f479961c0f92e631f12f835c642d870d196e6c73b51aaeaa05e20bfa3fb5158ffc0c427b772d49ff6238ec9df608df573e0fc3edbe0ae22e209e98611d0f4ff173b717e6000f0bf6c98f5381e565f395bd6edd469959a4283fd7981f95fb59597bdd0aeeb255e79e833d49dc582d2ce21c0efef2a6ac0dcfd790e2e7db7efde423011a9ab381ed0af79e8d114323d8e44749fc325b528e02ea18aa481f385504deccd0d5a273fc99693af019119d73ce08f0ce0bf40d9c8715c8be5b5102e3d4de06b2d19ea8d04fcfff2cf7317c93f6a76b3ec04db84f5d86acba2c0f9422d1f4e4307946095e69e8279b34947b111624c017c18f4d95ad0ade9152107930083f8f9a128b5b00be81f8987cc7313a00b49747d842ed382cff08de3bf00fe351e194e2d5e752204fb9a701dc95cac8c449f5660ff4d94c7dc1aa7326c5dbfa6a48de8b5a84db6c303bf6fe39c82296c1d143666e265295af2f393a30a381f26a60bad659a031267170a991fab13039d328805c02fe98ae8ab709e3be656f4f06c1571fd21283015c0fcc7fbb393c227bedabb9072d469f2038c6fef19d501fe35ff4144c3c22839add709bd3ffe89cc6db27f1ea89ff8b22cd56655bc9d515a422d27e8a1b1fb13d6403ea7cb60d8fd9531959750452c5751a5246a866f10987f15926d66ab6fcde8c2220a01fff7c04efd243d1898dff5f796acf9df5ba73e2f4cbce88d9117d5055a1ae07fbaee0a5faa2ea98636935be542dbe851ac38cf01f0fb9e5b49a779b52c8b223cf43fc1cfd295ad78fa80bef57548a500caddd3a48ef66f88d29fb68d295265c05fd078fc4e0fb577cb00d5b708c2766535534a0a9c8f1ccb2407c7aaeec966fd2be33573c766fd37aa19e0dfcafa2cf64989bfd3624ef99f31a53504a524f57be0fb6169e1697cb218c9ac3e948691463de224ccc701f0e1805116068378935f3e15c7a8572063bfde03f8fda3b833e764d09d999b0baafc28dd468b5427f480f9af8dff3e2fa6121cad0eae5592270dfb64183e0b00fe01d92e37b24f1f772fdba3c80d9e7b141dbe6dac03dfbf4d8e6ab9795c557c9ff977e78a5fb13ab3351bc01f38212beeb1e5bc2b6e7970fc15fc65d214a3a01ee0af30037070320e2a0e0de6ee6fce766ce51251e9f8fefb7a36c49bc1e80acd61b24b5e519df38e18940b2f80afd0f74c689b333c36bf416f3c79f527ef67ff6600fc9991f09628e58b0efacbad64fd81186330f6198d007f9f19be7881129a0bb5785e56ace88dc94c01fb02ccdff8a527266179c48f19ef7fc3921a06754c4dab07e47f2ef4dc8cac3c559ff0331c9fb88a73ad4e6d2a01ffc727d19f5f601bdbb778ae25848ab7a683d9904e81fef426a533f7f0445de69deae660bc63dd8efcc40cc08f9254b752ec6a7d4f4c95b9ce79914ee9f130a302f25717fa968bf25912b602492edc41a1af34d834bf80db828be7f9475a75c550119f0c77632f9636be9e0f03f817222a8a508bf6125d38842997502c5c51bed014e04746d4a01620b4675b053915bc12ddf2fe2a854f02f9da2ea2fe1fba78c830516f66eed82850284761d3c0fe143c666033dafdb82decf5970c0a665ef047bc09803f1c16a877c363443ea41ef428dd8bde4860224400f8f8a7b3f8b4746ec25f0d8adf8b9f8b6d8eb8a63e02fc34ed130ce79ca76b18d29194f0e247b43cffbfd7007fb9bc36acdf9b2f1d5890814bfb4344b8ead4a317d0df741f14aede992bafa26a4d2b0ce33d9b53640a01f3d7be9d3c438d4f8e96d616b238b551c01370d102f206c4b567c2845ebd940eb65f4572b06d6e7d014c01e01f6bb70ed484b060f87b8fc27edbfb3c624ab9b601f087cec9e97b744f86539ddd2143d521c3bc659f620000f8c0e1e92ef02c8cc63511becddbc2572982aa0bd44768198d2b02ea405c03e66073a7f723d0269d58807f7c585d9b446e28b603bdbe6a9417dffd7a68001f981f290df76d96aaa213f9e0c71bc70ddb358937fa2e307fc2ebd1315fad3b17229dd611c5c4621fb94e161fd89f93aa3d142b8cd001363c66ddec04bd1910e5ae00be16c38356c428e09611366872aeb6dd11fdbea206ccbf13d8fd3b2564bb304092893f542933c0148f4b0de00b5a9932b4807efdae035a51c4c9d4dd73cd1a5b201f8a0b5dbec55d62f096b6711e9f4a347d74be4509f48737d9b1fd35d0e31d616d1bce7a6c7e9d2fe65240fd6674fe988492e2713a016f3428af366af36d290ac88fb80b08c3a4bb403f97dc4ec661405d7e37b22703f83345da8892cfd112a36ff70ddbd9f374af17c849007e8a31efa38a3a86110b26eddb369b31ef2c7dc301fce1a22b6401d9bfef91952ef203316b6e82fb91bf00fca319b71316fea35639070bab4b997125b1648e0cc03fea8b20a4fcb5e45578fad5705c946cb660717a0df813545c0448ee5eb52a9ec96b59826aacd353c9e2007ffe737269ee6b090d63e4e3bb11e7caa1db4b5d75c09f3d5885cc94cac04b768fed7e4c222f4e8ede140ef8fb50dbb75011e2849a3f2a7dda16f993943154a80be447fab4a4df7052c66898559613a7738d6ce8ae2c007f0696d84747cb7be4c6d3b3bc8f1cb54208cfebb5c0fe524539680fc48a88fa60e8925d327b1647b4c10be06fa442e18fe54fce7f9cc9b7b1ba2049ae9ab99080f98d1b98adbb904e180ab4509432d89f3e107f19f9073cff90849e6a8c3ac8ad6161b3c7bd79bafbac0e36e07cc26f86e581f8368631d050c0b53bf95eb945a807c85f2ccf3f2d63b1f9be7c29c19022ed08197b07e7049c4f5fe023d2f36c3eb893ec8fe93a9d10f9c7f9f403eb1b6eaf3661bc53376fe7628d4fd6fc63f06ebc2f507f9e22bd19da974f4af02a7cbd39a1b5c40daf8480f387766cb0f85bd6117fa7fd3fe74b4ce4666e3f63e0fe36b3e76274b15939ed8499accf736f284e2f98f980be8b277c53bc2c370e7b856b3dc181f3c823186a0be853da8cb1a56a9a9734718eb98e6352a36293d39c40ff904ae931b7336d25b3418cbd88125bd0fc0d1a07d83f541cafe7a9bc41133bb8a2a979fe93f5ecf2dd407f439022e7711cc5326300ab49119b6bbd2af08202cc47f983188ef3b62aba77600ab72db2b2ab7de5950d3c7f6be685ab7308b43eb1d77f34b43f870404fec900fce56281fb65ce173bd350df3ca5b4f7a43ca782f280ffa81d3ab19196ac806c7ff12f057f9c84d6585c07a03fe576bce1956a7360fb1157952dba4d17a14e8c01fc86fc8e869fda246d4bdb901defe7f5e352c8f16b80ff6db3b92f61312a1f0d1c69f22491ebb41e5aea03f4a956921fd48c1eee9f7d3e8e52d225cea3725ead02fe8941a4ea9c4cf2bd52f247cf790ef26ba2e25624009fc7ad79a26cf9e0a7378e6fc1c2c765d7352c4c00f8a0d09df81be4d091f3fb264352b866fd16bbb4f7807ed9b4a3a5f89d80acdb64fc939f002b126e24f11da03f8933e042b7e6c9598b3da775b2934f0b1709f601e74fda9a0cc6e61f4ae8a04b5b499124b55f01f63f00fe3ae4efc1b3add1cc9677de8f0995a65a36b6ea04e0f95f563a4c645a8fb3c6e0f01c2ddb5ddd90b46b01f84462b391b7980383e9d2e68e99541d2be82cff00b83f052d03819afb5a4f2c334148d06a6ae11baedc2fa0ff910d5e6780777f6cd35d2a7886386dafb7a7a104e69b7ccde02b46f956e5be7af7801fefbb7e5a670203acaf146d7cb832846cf7c0593519b712b2dc48660e003f63f2aadc0cd88ca62c2a63e1b7c698c0bc170b047e3fb3360bc6dd537d999245c1610e87828635eb71e0fcdb1e6a4f4e49d81856ff3ccfc3cb665a4bb9e50df00fd25312d26287fce73a81590eb3539fa89df5c17c58679d5cacc17afd18e3102e7c3e92be1165a210c07f6a209ea97ec2b21a92fc6e9d6ac1e2d7f346ab4a0cacef7a3bd78b6fa3cb52c862f61fbd96df1a90c201ffff285c026dd35aacf02744778cb0ce513c3a0f51409f428c610b56ee765b8cf81a793cf7674fa425c30fc87f0cf408ef41dc61e3954b69401105b14222718900e7b763c5fade456d6fb50a04ad4e23d9bdff5ee807e0f3173d5a357fce65a5531a8584b8dcc09d2e227c05fcd72db233c99d9069880ad6e813f1ccd9d7417db700ff103d10fdbe68752172b276c340c0691ed704da1c38ff66761afc6602232e827ea8a076dc3522a0e73302eb9bb99baa56f3d432de9587cb0a219440ea0a4203988ff29d259b65e94b52b987a14029ff2bbece8ac510e03fc3776d7b319cdf7b231fb646c9ddcd3fae51c605f8ff206255921ac5d36ab8b0d27072f9f3661f7a59003ff8a726053b5cfdc6417ff557d1d1a80fd7a71e02f8b529df083737bbc050fe24775fdb1c3af7f6c638409f55e0aa74a4fed2e273adf11d1f4ae3f3ee99283f301f7130fdd57f3d1201f397c3d1fb3faae7842eee1680be9ac73997a90251d0f4298ff83637f8c5579fd405f0bfed361684b43948d0d82135b0119bfc4ae3270901ea77debc81774674d90fc793b4ccd220f3ea6a15f07e4c2a8577eb736e34e9f641cfdabc3b0fa4ebff54817c97f827b9b640b94bf55cca9688e61a7501ae5633c01f566cb57f67dc73d5ec8f0dcb7f5d113ad8967903f89f939e6196347c918c05a2e0de7135dc8fda9dc902fed96abc05e35846d9dc9849cb655c4f18af575418c0df5cd7ac19429ff0bef5629bdbaa486656499c821258dfd504561f350db5e0b1a5f2cacb10171160903901ff7617191cfb5bdcc7d20caa6a36bb8e123c1ed26600ffc71d1a307510ce5ac240d1a31a45fb50fdf1f300f04923795bf6dcbe1a0f4faed892c0205fec83f306901ff6158c2b732818973caa3ca286f5aebb3dbb7200f845d28fdf7d02ee3ff38eb89f84ee49fe64c4432d00f851bd55b905852d1843d5dda22165fc35e5419813c03fcfec79c71916bf10827ae454abb4b37c66e0eb0be09b5a45aae45c4ce38d47b1bbab4c1e4a9456d94bc0bfdbdab743c3bc8868c4996d2c88ff23946e595b18e0bff4865b2885a20ee7e2619ad9d33c134fae235600fd98551c57211263368512eef1435044b5c2d92751a03f15f42d08f4e861eb414ae87648bebad5792d3206f8c5d81af51a347167782492ba5a93a3f7092293e1c0f7bf458d265ca5dd181825506fe4346f622934ee00ee1f3391be4c29e7d830956b970b0dbcfc82c8fab102d0bfcb60d383ce51bb1ca3646511a286af064557d080d7139b36beb7c657f9aeba35a3e77fae534eb15605fcede88f862cb02ed76d15dafb02379398055e6c0bc0fe8d9f7ca7e2cd3927f8ca6542a94ca77f85e3390ce44363cb4e787f6c4f53151668e5b9349d114d9d4802fa728683c4d7afafa297e518b891be4347eeaa3cc9407fae3e32e12b6c4636f1597357438e0f9719266b00c83f8fff6c4cef21d1c1b1604b60c0f0deb8639fb502f6371c9cef05130b7e529d680359c37eec78590711c0bfd654e36d3573b935e1f0ff88b44eb5623fc9836705fab3f146334fa51342acb9eb6e585e4fef1ff3ef80fee6e4b0ebf8742edaf7f5e806620ee50932f1e41538ffdeea78e98f7e8c99f761dd696620b16869d54900fad1f9c9390ba122d436a61dc345105e1bcea8150330bf01411c6f782453937c4f10d115dd0216b1c06115c04f4cf6507025f8918270c808080cd46bb629503e807f2133beba3fe72cf966619590bddae61386265d27b07e07914d59b777509e3d7aa04302837506b9057a00fe47c0219e3a89b9512d678cbf8a9e93a2c7d5ba02f037d832b0efe2e9f277b7237fde5bd2762d43448c03e6dfdca4a61b3971c5a4b9e1ca5e96951012f76a0d01fe198cb53537d204f6dd2ce3e2b3a8a6afdb98b33240fe8480af84c3ceb5704995fe0e4dd26ad257c0c008d89fbc43c3e5f4d249a80e7d7ebf24b91334518596807cece18fefe3fa42a75c2badc3e19d7354dbe1d643c07cf3ad1c815e575c3de50c9f088e397c7873cb8ffc803fb39d97222866b7f5fa1abfa0abf0333519dedb22f0f976e2115ba2c3023bf3e25c295338ee161f129701ff018c5966f07bbc1bb4dcf8282c7c7bbf28d94072f0fece5b5aa357e20ff37b013b333f4944bbd2dabe02fa42d47dd483b3a5e79c3c6f953a05e71277383e0e90cf5b9d176f1e43bae52cfa0523a95bbf6deeee1608e04f9b9b2dd7ef826d72eaa9210c6a29136c17d8b7c0f91d7bbd988b7c3852a926b5073fb22bfa6fe0580fd00f2454abf391c803823f750636fb5f0545b4a70503f3e58cfaee50bb5b88aa9cd46577c32e5a0d5b6c6001fd76ac434af27214b253cdd03cd9cd1fc73a9f7818e02f3fdbaf0e95361f86354275e0615cd73034c183f7bfb8badb580d32f0f8c5cf70414d5a0a324c7ea600f48f11aea4f16518c6127d9e221b239cbe00f75f7c40ff5683ad5c687ab3dcba30b2186d62f627aaca4b0da81fb8d464a63efdb8cae45be961c865546ab25277403ea7c40b3e451947dad17abbcecd421c1a4ab3ce1fe067f4e63b374b2bc309acdc65dfdec451fe16893f03fc1afe0adc77a9d826ca70cee28a9a22bcbbfba83460fecacc0f3e35535f0fdb70f48dda954be466cbb20460fef5af40624e8811714b0adae75f5c9d5db826ed13003f3691c1df33042dd87c3bb63f804d21fccd8dd50ad8ffb2e9ea0c71a1df38542ecb117a064fdbb96b3e8703feefa05cca29e27edead05ac03a24362d4edc76b00df53b587c4f3ed94ecdf6895692fc33975bbabdc03fe7bba0e43d13f631e5b580b2b10c55fcb043d33bb80fb1103ba22ea1fa8a323ffd11aeaccc0d9c28acfba00f8695eae8caf5627684671bfeb49567430ce0da51b980fb664056dea06752b829bb6e115c7e74e291a230df01fc2696e74cfc8998825e7ef04bf0e3bd5e6a56600f585dabf9005e71eba79f7d4cc26ed5d87e2fe4320901fc354aa620f2e70231a553c34cd6235859ac8b006ee2f133213b4dbebd9e43ad5c064549343cabfafdd00fc4d10c929a68a1446757d312a7f05b8c5d157589081fe2eab5cfa330e41a6fbc7acceef9d2887727b8e0c40ff23cb3c66bc3a14769d95bc5cd34e55c52420cd0ee0df80086fb854073a1d9c2bc4dcf3e4de9b5d7e09c01f1c6c5989f9ef9aaf9ab19b0b4f59f094147acc13cc8ff46033b421af4ebcc0e2976e7d45d2d0634607f26524cd3522cfa9ae68e924db5efcc6387f94bf9302fcd48b54dc7343aa5e74eef819ba0675cd66f2c73920bfe0e141f535c44a052765ee3374ab6b786bfd700770bed54ebc170eaff4ce8db9f960aa17927c6efe820cccf7494fd71a5b319bc4a64d686ab1bcdc52a9f55700fa5f17bd8e4eb6e1f27bfb4f0f1c5633a8a3284d4e40fdc21f2eaa1963bc82b4c10fa40b361cb26df9970cf8c7e34fe36b78ead5e9d12fdbf065c30e1ea8afad007eb49db653667b8276eb8d6171b4f01dce6abbc91240f0c9775399bbb05595cffd52d717593051b1915a020876e9530d983e4236f2a694efbff327d1563c7bbf01cf77a3abba9d65c8c9d1cb9e319a53f731eb47bb97cf7f5fe74e6de544c95f5effac3f77d1d7b486bf75fc08d407391b0e6207684fd99d4c59d26bcc5badf77918c0fa4b3239fdea1acebf26085b7bf4d8b0b64ecc7203eccff00709c47fea97987051e58e57f7353bd6e246407f7f446cdadcbc8be6f023c7633f20b369567bf614f0bfc4add74e90f414fb244651f142f2328bd4fc5a05de3f36305dad3f2f4c14dda5a52c368f3b033d5306e493a4c585635b953ec5fdc6262695c91038f91ccf0e7cfee6b604d392519f3cd79d3287be1c871b34db35c05fb418faeda8d3f1c48ba4a674533c2de7b90bff04f4237ced7e38bc733cca77e7354f7df70dab4a7eb4807e536470c6d45bcfc277879fd61e90edbfd28a8505e49b753748fffefa8b8b70b6e1c215c38e52e6c4f504e87312f023ab70a87da7a15abf3e7676894c2fef6d02f89bcffaeeeba119711153e4935f6083be75a22526905f9c59333bd55a379acf1f6a80989151ae535ad102d6d7476dc99fe635d2e7f6ea86c2de43612f7e786240beefaa1451a1d626bd604976c560ecef7922a44931806f0ff88b74d493e0704429533989b8ad64cef254017c1d8bedb3bc724fc784692b34ce24e7a2a67feb06f0dfc7193e4af6fddb3ff21e26720634a6e5a991fc81f3835e4ea1f717853932bc733cb629b5bb862e762ee06fa92f7f77328f737d7efd112942bb68ad39e0ef4720dfd9a37996bfc95c107510c59db3078363bf915e02e88f16c5a8de365c720151c3d79baef04683c983d0803ebc8087a4741a94a90e5d3affbe83fac111b2150ae84ffd4231b69f392c98d992d03ffa2ac13b43f73703e7b305b46ce887d7fbd35ea313b5fa59eac9f66223201f2e6a5b318cdb253a5f282cb820437efa6f446a1eb87f8c7a53eb46f19c747d52e7d9a47beb040b8d1113f04f7e96a2bf1f516f63a68c13fe3693541f780bed01f06bbf75f8933f458586f02aa93ed15287b0519f4801cf9f9895f79fcf6d6398d2d848e49b2e2394cc4216c06f2bd7adbf9aef5d58b3c4b5190cd51bf3d45ab203f37559039d148a881fa5ee654721e969dabb545b70007f203ec76f79f8512a9e038ba1ea2452f9a73afb3c607f6e624068f3b8337bb0fc39886a41fb3da36e3a0bf85779c8c6a6bebc8f3ee454a2f6d6a94212ceddff0cdcef9a21fb5cb3e01f3bff75d34715f74b58d99f854d60feac31fe037dc517eb301dcf03771d0e854af7d1af803f33e597048205f54285ddd9dc38be95577452fb77409ff31a175298cb824b4488ab1563ad4c7f5695e906eaf713c92aabcc0d5bbc9d4781e54ef0b7bc769d55c01fca453ed816b5c319511754af944ff272adb76002f85fa0ef29b07a953577be6433f1d5dba7f514e41803fd51f72811fd36e667eba91f588ac68ef0dc5f476081bf9f26c98ce164aac8f380e49aed2dfc2f17f9501be89f51bbfafcbdc2d2a927d80e9a0bc663ddb2cb0c80f9e869a11fbc875a7e3754e1f5d2a0c54f87d3659902febdba5b5fcf134645199f9f67dfd40a5ee11ca49d80fa460241b0a25a602117ba7dd6df73c11b9fd0f606f4e7fd4c412f2d76619f8f61b921aee18598d0affb81e7dbe0a04c0de31436fe4c4d8f13049d75e2bbe500e8bbe2335fa1105934c2f1258a7e58901238afbaeb03fe1af470111484ab13c2cf748793305c48d982285340bec41971fb13edaa58891abac6a5dfb1fc5b7cdf0490ff8b4f4f7dde2789fbf61933383b48d722795c3b01585f85532125d8d09535fa467ce8b8036ac1d52784c0fce467f15b536ed4c7518754afa8d0a3fc7e994244407f7bfeb1f4f36ab77e76dd6f79efc9d328da3b007cbe5f0867dd6446070b6a19f24586f78e32c430bb80fdbb08adedf6e819f084daf516d6eaa5143975c906e01bf850e30fa817cfb263bc5f02d5ba83b1eda10580fc32353f1f85bbd0b1d5390465eee47baa34139f7ec03f338c942b8112218bfdd2ef3760c8f7c737c34c10e07f9abea0f1659c529de0e10833417185b29261e102fd1745c0f1af8441a912353609298781f76bdcf3fb807e72249446dae9641945bf97959c1bfa8a76b00ede1fc722e9aff1bc9caa09cf134ac0fccd58fc8de201f0076d9128c2b2ce2286bd4ea4297b0ee685d21fdb03feae3478b50722ace49c7952770a8dee7e5c078512603ec931defbfb25d998c2d8b622a6943d72643ef950e0fc58f4573d83298d7d433aa80d1a72392244f5e901fcb15a2573505dccc3ec5f9c3198bb35e2bc72ba8d017ca45b8e12644cd82a2d1487ec9a9571a2d1a74002ccd7cadf324eb8e94da41fb52248f47aee93ec7cc102f22fb90742c20d764bc5830a8f4c1b830f44e37f7681f555c395ae02bb2b21f5f7afc666650c8b79801812f0fbb77b5379107ef3849c37c3c0ed1b48eaa2fc0e03f41f525db64b895c47993a84c4cc5126644435d3a8003f7a939ed0f24afc8ec629d3d4d9dd834868ffcf6600c0ef40444a13fbdd3a5a199bc808554575f611dd01f78b4c938b6220fc4ca0f59c0df93d18b22adf250807e023facd3346951529ac0f4c517de8f52e1ab745ef007c958b56ce0d33b39a28e7f6ba871586f1531bd10de02fbe9c773e05621bab3c2a0df8a40f8a78ef7fe0059e4f960af2da3b680487419cd3475a8639fbb7d875803fefb249fbc34d85e767274bcfc596b4345cd2dc04acbf01fe32988f5eceab3ebe886dc1a5908a2d3c28807ef93ff6de022aab6deb1f7ee846ba914640ba43404a52babbbb434240ba4550524240520401e94e69a5435201496969e11bf79e7bdfd77d3cfa1ccea3dfbdfff3bac67038d8f3597bafbdf66fce35d7aca52a451d2eab765e466a8a601639404b4494c55e07c0f703d848d1754ef73729f7e7b8d154cd7af9eb00fbfb0ba82c27fb00795c16633d4c93f7e6a6473e7000f96cedbc731ed1a0b49263d5288fc3e45ba4c7d80ab09fa3dc38fc1c9378b3661ef1e890cdcea50185ed3e20ff880519be584d7e4a3d7b2d64a56db76b7c904601f0fd90ed248f353969fa440d0962a4960fde939dc800ec4787d1a7d6de4d4415b54801f6b775efab39c36e03ecebf989fc9831424969f73a90c8cdd4c4784ac8f501f63fa7a4ade4e77c9dd86b573904cba76f75958b0800ec03bca2d3915056a0a62c57dc2bd6826eee3958e680f7e3cfe458d22c3f7167a9bed61b1ac15507f3a01560ffbb5e8c6a4cb929b5c0fc92e3bedcc828218b121540bfbbbfa7038d684e68be78aa163eadb2e9a08a5206b04f4139bf8c55e69297b3639237197d062dfd39ee1a20ff65335af235ab99ea3432b5744e86d9899abb912720bfc0a868c2c6b2ede4fd24f1a146e68466a0c004650b303e00fee1f2628b2381816025fd8d527c2e0766c0fa263cd556be55727bbd5b601e79c79a3a71a7db13903f4d0e0a6415c512f276997a758428d79761b0640dd8bfddaf8091c9f657a97111791c807b46222bf1c913503fc288899fe8896e075c7ecd28131a8af0d1c10c19e0fc2ee5a72b4f930434136bad1e2c68adbda34e673107e80f38ee9baa3d251f9eada6a675374ea9a63d103603f89f93873e0753417bd23fd06540d0db78a85b2c8303a8af057f8e93dea42f8775031bedb18ea98fb5242a2d203e634ea283135a1fdf78df6a39cc392c2aaf4f3e1c90df7eef7ec9f9d00a06bcdfde7268a19a7aa915d45540fc61ccadf9706beb0834f98878cefb9f0b274467d201fe83f5a2dacffeba0403e34fc5dedcb70ce15def9c00e0739e75c793562c237e2edaedcce54ae053e91c12c0f7fd6013a618460817e9f44189c36ce189e5f0356540fe40f1ea23dab82e5ba9adfe48a80ef4e378baa72980fd2dc65519119d9cf3a2d6d220c6f9bbb494cf50927980f1f96d6556fbdb874b562e992b98a2d52b296200fdab5292faa698d5c1697033341367091a89fe2809207e90665f05bd3581f1d6deb085615ac407f5f74d5b80fc2f3bc66782740e31e5a15b73d766e967a5b4a23b01f1cb94fcab64b42c11772f664e637470b273e45e7802e28ba917f7045416ea0be636b04c7530941be24d0201f14d5937a1dca80515194342681c35484e7113d81200f6e39c0ff5506a630cb319edb227c19817a94d793b00ff525690d2dee6b8130365303b4dff33bbd1e23a0c407df4ca678a0d6f0a7b9def164cc7f4a8227e7c6add05f05f2e1a25d72d94e8183069c8b49380eec9bc97c204c44f544d984a4ed608ef2eca1528303abf3e10cbf701acdfa3cd0c7c2706d1e4851ede8bf4ede3f1b5fa5a80fc9fe74b5e8f0b02c5dea39cd7dbd355a70573bc7203e43f50f7c840493f297a653c68c9d14e7487a4e04214503f3a90bc17f1c944af51f6b9f9b4ca2d67ef71f34d80fd7ba5e2112fac23df71e09b184b348274b5cf9a1480f84b712fd748936462f64edd6b0d7c66a46537375301febbcd70e84f2f7d0aac7042e824a0295966e1d39f02f0b7a5a59aa293995bf16c1824a7789d9ff734830d601f7bbbe5a6bee2c1bba654c33dbf03fd22c28cc115b0bff24fb7f46865c09e9ec14b96417c08c7f8cef72dc0ff53f8d45c4a91606640c7d9f9b45a5be70d55b302c07f8b8065daa9ecaf18a743647bd4d13262b0adef0c90ff070ee1b0fe7a1ce831edc766f5d8146e24cf5400f171cc4bdde803675d645e2f9f4c349d10c45e2ba704f8bf784decfd7bd056cad465c937de8aaf2488daa201fc235b56f7442ae8b02aac8b3eebcdae7aa04cf0a300eeff425f98266d0c9bc523cee47a5babeb59675b03200190dfdcc47635f4207e62d1398a9b85992637b900c05f2432c215413ecbfaf750b9d8251a235603765f00e4c7d884f6d55b73b148932cd525b8a7d885ce2400f31f2894f0f8f68af4e3c09bfd65b85ab4c87bcc07d980faf9a73473cbf58f65df5a6ad257b5d9aeac215c9b07c497bf0c7a3ded1a0a5bd50d3d4e25f1a2ad8a74ce03807fbd89c0536103c39b9cbc4e5d68ace7672c38fc80fc406e8a6996bab61c4bbc92b91aa96b9a2aebf68ef3c0f5ddceb741e79820596657c1aaea8ed6aad41e20fe0a5dc32d9106b1b489962a8d10c335f762478102c0ff7b4b2e2d8617ba0bd7ca19950cad7b3de2f63200f18bc481de3e84a7b15e124c87079fb50bf8fb9e01ebe3b4486f7272d8694bd29c287b6862aa0e4733f701d647e1aaa2dbd4a54e46a354f92ea489f973e2f5da00fb4a69273ea7165a96cc663a06edbb14e9a682f700f101524a26976749f74423bca551dc2bd6413730da0aa82f6fefe49d3ce39395e7ad92ce1a0be590d33f810cd08f3b0263b0b1bc2b8ba6eb05f644f7e505981f1803f20be2055b22e18d6a77df6675c66094f0041db85903fc3f49d7d2d86f6f8c68c9cce0bad5c4bb90897c02d6c77dc820cbb3f530e5c9ba988a73cf40c976141214c0bfd020b8811bbf8668c64996d2ef65d8625a8eb40ac85f6baf32c51d9816785335692d9251e9c46c7f6404d02f135567798ed59fab189d88bbdd70f74c938cf307c4ff9422bd3fd62ec8580bf13bca24d02f826fb61d05c89780f45af6b0dbd8ab7692cfab1958a14f28dede07e887e8b1aa71f75105ac4d3a5b9a718ba11febc503cf07b3125660fdc0e16710a55aaa6f6873043da52e07888f193c8fec607662186cce43ab9a606f290c47b502f82f1ae8073b641cd26e8f2aa4a21e7b7164cfbd9505e88fdc674e9a4442046ae2ee9f6c60f62beafcd6b800fa59eb80f173769ef7a475661d6f763bf7b1b6cd1e03f627762f612a4c33f453f14fcb189d10d5af22b61503e20734dcee36c8c3b27a0f8c52e428b7366ddf3dc903d8271745282ca913e92510ec486d0563e653304f0400f8da8b397cad7e354ed5e2ac2d021e978bb08a610b90ffc6cf43235e77c455d2e4c4534b39272f36c2110bc0bfd375f76d3cc5a2a752959fd8ee5d378f7f58510da88ff3742b7e54472e3c5852c868ecfeade69a05951c40fc63c4959abc63cd3274453b150a64e29e6b619a9e80fc01d5de4c44c737b9fd8ad16661d30608e1ef0a6e00f667cd4c78e1558d433493b76b9ee7dfeb1c187b5500907f0330aa6656947861b867834b240d53e95ce93700f1ad226239fa4855338bddca62ee12d6cbb99ff63001fa794c2ccab6a22af61e978c269c1a69db05ee2d47407e397b859014038a1f7c26d4fe60f35270aaff290f607ef237688b4877640686ce266aa345a67d321f7703f82bdf5423f2faf1e6a074e9567ac41462c5cb479f01fa19d281c59e80deced1ecceac4860c2e7e79846ba00fb1617fc3e4abcb3c47bf32172ce10bc9b9883c1f100ff2c21ec54b682b46ab228bc0b12a544ddd3ca964ec0f90ffe9b243563041a13c2248fbc5fed2a4f5b9a7e00ec4fbb79e7dee76776d02e2ce328a78fdae8bfdc6800e07f42ccad32cc5b90f7497b4ec7bd8f99d1ef321701fb4fcc2ae108caea8290fd2bdef31a952efe73a18d80fa271a06965dacc5050cbd4850192a9cb29eb34f0c00f3a32d8bf1098dbc10667269511babc3c3e909d51dc0fae9b1e6938ec785f8c2da002f0aff55c6bb830b0a40fc6b6da8d168d54c4a403a51fa42a46019ae92a82420ff982d8676dd21b8a8c73434db2cd022efd18b517440fe1343115d15a76182efbc7c2961e54aa754c57d2ec0f93a58adf814329b4b78a7ca4e4dfadbb18995d9b800f9c66d11973b403c364142e06a5b10bdae615ae80a38ffe3d369805122e2b00894c8ae89d316cf536db15a80fcce2df294c9e356c2cb0d9233c57c70ddcf8a2803a0ffae4377ad08f47761c3db337da62319221ffb9802c88f81bad7183cd90335acf4b29cd99dcc4687613a1310dfb32193be5014806929d3c1b619a592aae6edcf0c05d02fbb853669bcef3179750fa904dce733e526c703f8ef2b253a07d863192d0b13b7a41d0bdb1dce62b700f88b74ba86499a1f612a46a8e9634a784d2391e516203ef5445f084708254ae3ddb82b5b62c9138cd6a94140fc5b536cde886c6ad372faf55536b559ef948c1c13c0fb2b5c78114455bbb1930886cfbcc4485a09d23802f8a71aab0917d9ce6a03c51e580d40c72719150503f7ff9bc723a9618d45ccf7f2b2add724635c23753600e73fc433e73f7f6c76f6fcf8b5461f42a2b39571672520febf135990e93954f9be55e4f245efc02226a9d80b407c277281ddb94058047d4a64afab5d29da6670372a607cf8fe764c05896ed2f654d3cea37363d826e46980f5e9432d0787cbaa83e4d5814427794174f714f202807f77663679a3892923a37caa2ba5b93e392d16f71100df2162924268afc67ad4909f361a18f9f149799003ea276bb18c8e3aa6144d6525d99c8dd1c7efeba34f02eca7da7e18877d2aa1ae193cc5f14f4c2a6026048c01f6dbdeed9b4d2feba443cdad27faa4c6e7b95d260c00fcd18ef48ab7778e0515df53a20b2d3ed4a5dd9f10907f3e777bc4117d9fb100bb6af1508ef15ddc7df83140fc180be730d75bdd98bd9ba2a1710b4a8da80f490b01fe27cc94e6869b11a98c049650f6730f3a24cf29e100fb1f697576ac5068fa7daf7b860c7add269c1c941880fa6437296b3a6b5e588eb31ad17f94a1db38e4f1ed0514704ff6a2f12da195974c49aed93fa185352cf34303c84710688cd12ff6c996bcf847393a7ea795b5980780f32ba29c554d5ab41e9d6e3df8bc8389fe90fb7eca05e0fb3188c12ec2db5b6f625f1fe1c2c27fd189b2380ab01fba533a8ab1b23ebd5efdfaa4f6f8d9043bd9e61620be1764ca9f842bf2f829f1838ec0dda1f6467e176480fd5773e5099ed0ca1d56530297e22011d5c9e1eb4c00fe967b1e7b34359f14f036eae03c8cfda979c2610f20be9c4575425ddd0165cd7ef9ec105b9feea1947521e0f9689541b2d8f8e84db3f583830edc567b460f5200cfaf65bc784c9571e28ad762625117556fe6b4e00ed04f24e5654c482fe45787e39b8793643d8f71ef6403f20383364dbbaf0f0ddfa626cf6463fbc0bd4a2d660998ffcf1677c6589b10aff344f3715ea10a56ade2ca07acffcbded1d405f8ea4d72c48756138a68f758721e02e287371936a60d8b736b9e1195e729268d9521b5e103fccfbc2f19efb4c77ae7b36e2f6d585909b99842c100d637cdc9001af516d1b141c5ee557649836aa2933700ff727b0a7fdca7cead169032f4eba205715d95bc14407c82230831068f3c2f7c78c5e01aae36067aaa6711c0be085568c66d8d77919b21ff4ed40135c55bfdd40fe01f6621ea4b7ca488d688fb2c57666061a2ab4add13e07f67ab0a2fb6e6c57822c5b396dc7f337286a7981910df6bc678d72b996bdacbc7bbd6485b187fefe53b0540fe37f306acb5098dbb5585e748b389465be2161213803f3fc39024a1d610b5a6331351ea5fb75aed390c07d84f1c30d17c2bcc7a2c8cdf13b69ce666bd7e71e721207ef5b1269fd1ad1b30ec262c91282e3c290eaba89200fbe0ada6eba5497d2721e2879d7979864abac11f3901f689396551d1c084e74d5581e83856bb8fdd5a8e0f81e7a3231f53eec3bcc4d13bac5ef2956e081c918b03c4670ed82604647a9acfbd5a137f7278a86c29d73b0c887f20744d5bc93561a06373f8eccdead08e0062a400ac0fafa4f6493c25e94a74367a3f5ffd10234a79a503101fb896f606b5b2b9a5185d71ffda5098a33cf2b42a20befd3d6975a8db95e57753639f87906e39176935b301f2c3e6c67218838d3569b74dfa4688238e7965dd1f02f6270c1727f4320e71b2b6eab8ea51fa8923b8381600ffa307568a7e6a21179d2a272f89e69c50d82cd60180ff8504eb656a4ab5605ec5b122473553c1bd1c60009e5fed141d51b9f78410e59471cde2be200bfc2d65c0faeeb32a48ea993f8082bfc732ad6e7cb3c6061f98bf71efa3a62d4995a8c46240dd0c4c40c26c712b1c20be2cdbd8e9f891756b2116722e9d2701c36bac2160fefb4efe13313b55be953dd6c2cfcc8c374aea5dae02ed13eb4393eb2bea7af6fe85c2e90b7714cf4c7901fa011be7eadb6a3d0bae91235498f7f341ce77ac4a00f623dfe71fd14547a5def74bb79b3d8a83fba498bcf5f14b7ac1d6e1858f2701cdba3241f76278898328be0a207ed92598006bf239b51c5ebe003c811c29a58f1b0d30fec598157377cd837efc75557a5cb77d19d5a104203eaaee7407b9e480d435760b5ff0091bf267cd085f80fc231bd5b62783f5d5644111be478894bd43204105f00ffb16d6a6d9a4b3de6ca594b70c36c02d177f711b609f99bb52195a4600159d91ac37b3d78ede9adebf0bc88fa97d66acd8c38cbf5e7367750f0bf704e164731360dfebd42cd12e73d510f8109ff5362cdebc16ee4e1b203e0984d46613c8a8843b9a36b4171f6cc26c3f2ce7037d8cc61b8ace75f28808040a246e21f8c7bf1a34ea27eb166ca0efb79bdf2743830c6d0eec0f05120c8b75d3dbc4583f3304992c996a540f95320adbea9ef7a2cc7ebf3f1498fb43f9812ed1e24a109f813452b1114ab618a51f4f46442d45034cd799b036677319334b30af250289a4e784503e46777df78623cc68601ec90b860e0582acc15b7a3a5a9ad9fff5feca6677cccc6cff727f68c8f00105301d34d8de4563b16cadf2d30be30d247c53b951815e7019fa0f6dff6207306d1ec2ef6b3adfdbb1fb6df2953b570f30be37fdb53e5b00eb1388a86d0bfd8d3cfa90a0714fb9d41e5e8c29153cf59f7de5d99dc205461879b5a819158d58d31b9a99fc7d1897a143ca7fc0f9a7ea3ee06c8674fec134443a11353340f9b0cd3f683ff51d4698b2c10c32030c1d160c1d8c8c842207737b30730c370fe1f86e5e860eeefbfc050c6142363e184330fdc92f8348d13f90d17f197f7f6a0d8521878c0e3d0fe1fc83207b3e5819fbfbfe586af6562eee468eea1c60d7803fc3bff70421e33f10e0014b0f45bd90375699163e6e9211cce37ac385e09a82e96f08e1fc5cff43cc41ed5f1e6a3f47c701d79021942f1910f22f1464fca904f02f1641b5a2698660fbc259b8bd508fd55ba1ee3a5b02f37c1408df7f1ec2f787747ea12f23ff2e25ffff6d528d0f029440a67fdb7fd857b4bf69d14d55de4aa9965d260d3c02e17f5ca9255aa11149cb680fd83e7e53cf6d863e1af40bbc227a33482b46abbcba5d102b1f10cea0c5801bea5c406ddfbb04159978d84a27e04668f7ed99b87b70176125fb2ac2526250715683dd3b14d19c419b70bdd53ab11c94a2b815c5c0cfdbb9a653df7e53913a1fe2d6a86fdf9ffb9997fe6efa77a01cc31f9b33d50c7d8ca5e4178834111f48d9825483251e50a4de0b2148e02f2304ff601100032278304216098c90440503e22b80f7c3eb32f9d439587e1e62f75ad02b3ed12c27eb800d422108667cd0e2d677b000209c76d8bfadf788b1c1bd82591dab168e7eda98760732390a4e51f87fbd4181d344c40be05201b662ad3cbd0f6fd6b36bb2aec34d76edd7d7521eafc57f535770724e115a62856480222898a4e192c3670d7069f50e063a2994b9cbed6a7cb8f6886d5e1c75383ade04d7d68d612dccccfada5f1d5d0f5eaea0c04dd8a07b64aeadeed713d1422da8814bf283662aac129e8e2de018dde321deededf68f57b7523a787fe1fbef87ef87846c4991355a18b317c7237e3c4eef1ed1a8e37fab734427156cf80a440314896e7f67118bc6345aeddfab2d801850e844112b1b17193e20f08a825fe32847ef8e8a7b6e5ef1b9653901b6f7e9d9a46b06f3a05d61b067decb7bdb70ba715934c43f68a68ce3781d0248c2106f0d3a859e9daf9ee5102fb7ffc2f7df0fdf52702633c3f7fdeea1178c471736cdac1319150b7cab33ce2016da5d3cc8f06d954ca9fb9437019d6ef5bd984deca479bde336cfc09654d17650296d30f400b55eb2714a9c2bc71a1f7dcebd56af8dddfa15cd93758d3a5a256776dbf78621513f0adf045b5e8fcb12a6e056345f1c75e53194688e3d92fc85efbf1fbe4df4749a78532922d1100a0c5fb977e7b238e9bcfa5667fa2c6aa20346c8f02d9b898642fd1294f4c1a27c5a9571632958bf8c7976f17aaf29ee2cf2b5485faa7b8ecf6f0a1c900430e8a75a29b5c565f06244ab0ba16e6228f545a9df7fc975e347e19ba2d92671ee703a351eef2cac608a4a8bdb59e1fd2f7cfffdf07d660f9bc990f1a4cdd3a693ed500f29e2e403dc37f791142aa49fa56221c377a226d39ef72076d914a9430926684529fe6eea45b404e8a0d407e5fae306cc31594eb1b554f582c3db0a165c1fcd5074a5791ec65abfd83482ca56b6bd13527cf2a3f0fdb0dff741cb95215221c3b6fee360d73074b783d65ff8fefbe11b7d3ce27d861f9a49fc9b74363c2d7681dd0046f59f29bf95e71f2cf6cf59253af5a66f0f669020147655069cf75e19a51cd9f39224175ec03249b3ffa4bd32d7868df9ba826ab3f0cc6a6b86521aef9377e9d289e06e7c2124fbcb66e863d8c310716e399d125020e2716d20f3e49b1ad8c541aaf209c8ec8bb05f41db92d6b4017d52de2f05be9e34f793e58d2a8ca26de80c798b65abe6b1851ebcd65db876fbbb16d8da9f3c719bd6302dc8bef3c5fe39dc2fdb9b04b68dc01e6e99f632ac3b3411f3c3cae8a69397306ffe111d5c0c0518d3173418d8c1feb7f30dcc4fbe3fe26d3357a3db462e367fb13f9795a3b90b3f0b8b929dbab6b585b31d9b9dc96d2b3e093923f65ba662d64ab72d9514d94dbc34b925d9e43deced64954cb89438a44c7ea05c014387d43e0f4e6c89b27a70981a1b719a71729b721af37199f19af2f2b11a9bb0f19ab271189b9898f3f099f07199f3191999f1b1f1707098b31b7318737073b31a9b1b73711873739b41641503c19bddb1e36763fd69f810c143d95ed4adbced8efe5232b553ea7168f5c5cc69f00db51b9d92acfa30be3ed62e59f52b29413bec561419c6b87a3dddaf825ca7a9ae78136f2490baf4d707c31383177d30e3bf177d30ed908b3e98afe8260f4c845fe34b91d6de3d575a83a7609ff57c1bdb973ae5abf8f074620066a7477438d852a1b290cc6e09869798aaedf66544dfbd4643c9b6a10e2e7a19a4f34f911f3d077a34d0ffbb451ff4fc2fd1f783449fb61c9bf32dce3be616225262e2da4e726c5c6eee1e26da6e52ea464ab7c4d48c6c94642c655d9df8d839399cff7be65724f329dcc80d9d55ec838fe436162fd76283f89e5e931c79fa26c7b77314512ec88e73fb135414f4b1de73acdb0eaa29f82c198ad1326f3e38d867a390bfe83599202301cfdaf0f8ff666de8c3c3407ae35735f0b090b336dc570e2d4f533c7e7e0fa991f4f1c32c6fff378a9bd396f3c60f254f2b87234d57c3e650849d056ecef29cf1d67e64f980db1ab57719d6ce4661dccee485962e993fa837aecb99bea51424fddfcdda70e4ffd7595bdadedaccc4d5cc1472ad864fdc55caed1697b91ba7319bbc869b83aad16d4b19251551454f6733794533655713456dde3b2e4e2ece623f6efc90468edefcf95a8f9111b789a919371727171b9b310fab391b17bbb931bb091f2f2b0f3b2f2f2f1b372ba7311b271fb7192bab111f37178fb9313b2fab993987b9399711273737e77f54ebf901ef6f6c66cac6c669cac1c1cbcac56766ca66ccc9cb6162cec5c6666e6cc663cc6dc2cbc6cb6bc661c2c6cdcdc567ce656a64cec7ca6d6c64ccc9cac3c669ccc6cef1dfadf509770ad49fe5f3ece48a57d1c20c62bb8a441ea874b6beda425b0f0d54d561ce9775b9c9ceec82ba1d2ed7541aefead45eb47f95524ee60594839e620d6fdad34af05adf6828aee2e1741e0814e4ce15070a8656455904f9351fc9f905a11c991a568b63c22600627b7ab9c5fa02e2cad89caed149480a6e48bba4c93e05b737801e0985ca7cd5f2681a1484b2de0a0ac27cf714e4d7fc8fab4fa94191aebfbf0acdd22b3a75f4d555446e8faaafafa2cec6dcfbfa2ac2ee9e01f5574fdb9012c2fe7a0cc71a25affa80578f61bd4271e51e3f6e8ef6fbed3aca3159620dac699ba6aa2f9897fdf692f1af1b46231e095512a2787b30a9b99c3ef63f64f5eb7b769d02e5830f08c66a49748800cc4619c032e70efaad8aebc1d8b742408ee9e88da9828fa764bef31d62f042e49420b30f6597fe658989dbd81e5d082b0266fe90fff292fdaff905d3df0fb2251ddc8a090361202e0cb8404c9c0194daabb4c13b1e75b75127e6d430abe834b7b52f21527020144910f78774fcffe1fe3fbdfd537e31b7d3a7aa7e25d5e8f77d9059bfba7a3d72e73aea575719cdfbc3dabf9275993a798e8b5ffd165951c1b91c70f5181ecd2f081ebddbb5061ef60704d641a84701996ea42546a774c8a65bf079ac10764fdfb5a8e92e3408ad53e01c0fd4de7c83ac10ea82e022a03f8040208c7ffdffef7fff6effb83ef6e58f09a7798b8b8d61fcc450c991ed93d1b43f4941e1772d942beaf3d3527538dfa31fa34ecfe871dd2882b3efeeed6f09f59de70b6e06c1bdb336fcc09b183a844dd763bdffeef67502adbd465392de806a334e91aa369541d5d79408c3f822bc19ed4d81c18bd0e16448f1d7231f45223071a30577f66f24ce3091d453970b4672773509c91551b30cb6d0e26b796538ca8a452c12c9d64767e73dbb564aabb3ea685a52235478323bca85223ad8eb4636b55068e598bff292ccaeffaf7b9feec3fce30540200a402515d4122ef954a504640f376731b1b9b6bb8f8669917ebf8ab4602c9f14a8fb7526f3f43d7f3ad8531573fd3da0dad14c90a31fb5693d9b2eeda29bdf28be41c59a4ccfb717e01ed2dbb570fd1030f82ffc8240a13705820ff6f0966b636bc972db4e7d4690d424d76827c83bddd31ed843d91ea1061cb931d4f52815fb8e7fce1d990c3a68230c0381aa55cccc3c968db58ad841f358cdced037c81ce91662f5de79aaddea0a5561cc1a73527fe5c809e51706edf98c642aea2d53924fc280363fadbe55f68198769b4a992471b9affab10dceea001d5c90136297cae3d56a1928b9edb39e7dabf9ebf98e7b340c93324c7b73b4cbc1733765a7c982310e16fbbad83d1d91debf7b5db32f97884402097e41d52a20101cc0d7e1f35c0a8d7c8f72680b23422bdbacafc2f59153fbeff1cba0abfa8a8612b50ccf366ee6117d2359687022e03483768592bc3eb9574c049aa296260eb881f15842a2df85834985e425077f0ccdf4aff5e0972cfc250bc1cbc263e828bf400a11831ae8a05231585908d507489d4b80b3d36fbb14555d6f9be10e37ff38b05d86130495e9f1fe2fefe9402090b9adef6708e11ee42886072e81c69ebe57e52d1fac806b6ba20b8d7cab18ea700fa7df589d4bcdab711c47e280a621707a2f84fdbfd702a4222c45d8179588aa8f526d94a76f7218a096938437bbadb373055b0b3c8f99f688dc79f1b2bab51b734e1a1e47923a862ce930f5619a6b33d6e8e39ee1d8b20e128220df3da73cb7fe2dd7bbfa5d8b82579e4aaf323cb112357b82f39ce45a9235021d949eafd7ae10b15d0bbccbb5a9e221e1cf72bad82244083c89e62f7b5fe1df032396a18faa4261eefb66bff203055e11bd19484724559dada36d6a7f9975d58c5f0a7531cc2bc69fcfabd1bde780925850410e425d1faa632474e5112ea63076be64f92d22efeaedd1a128d7ece9244ad40ef9ea89118966e82345bf7fbbe3aac591f4f300759cb1d8662b3fb9a61fbcb612cb72cb1c5e9672902ffedef328ae089880b9e53f1be5880bcc154cbbd9d58372fecec86fda77c435a95413d2d6627ebbe5bf3c0b5fdfd288ade2e45d7af6f1a4e95af3cd6871c44a139e8aef4ecb6ce7cd66e8dd50648b74972b31ffe3b2c8c8cc48c8317976ab41da5d4f9dd52de22227508a9e46e575a689cbb58e6e5caaf06f19b37ed4e57f65db032f7fd3ac745135aa4efed5d58f2a1736525f1bb1d20c3810bfdec29d0ef87fbd857b7dd6ee41f7d56f41f8b282d95ffd767de2ee24c957bf65a5c5282cffea2a135257f7d7bf65d0dec235fc7abce57905db5f9bedbc09254bbe1ac32807880d68e2fba77171bd15f4355286cc1d9db39ea247519c5cb3915418902c34d83efc1e5b27bf1f7f710cb5e11778f5eec71aa845661e7f5cc83413e0820a8ebde4b07d8d12820378e94fedb86d9382f6d16e5b5dca94002e8f101c1dd0e824cfd596e8ae7e9b2eb1ea8fa24e7f99f7ffaa11793ff81ef9e2e2e202428108b6560af450281cef27942118c17f4a72f2e340bf1f28d1e0b775b486cbfee9324552f20b442c140ea4b873bf06e9476417c240b68903675984073c7fb05c96e1a1e55d358da12a71b1638ff6047a25e39fbcc9fc159d0971bb7c74a65bcb23ddd882c317f39e76da6417be64d01e6ddfb4d032c23d57f786822cfb8f1fd685ec29b4ff705414b79460e36a0c09f927f1d6067373661eb237d2518757ee060591a5e424a4f0e90f690df6787b08e4e43715cddacc17fbd363c46b1128fda8ecbfebe4efd66196b5aca908482230653c474147c783bff0fdf7c3b77891e715c733713fe98efabb07bd434f85b363bf594ae047441f87a79fe797e494fa806c6bd60750b1e1a2948f5f9fe812eb7ac877605b9bc70892c8877d5e25d7e67cbb8483da6fde7d45b3bd1e65145e50f728b885506beed6de8fc2f7a7c31650e1eeb0d31b09f248c53b45812ee52f057fe1fbef87effc77d88c1f5f1dd637aeb67dd056738716ea1198f95667ef0fb68aef8320c3b7e9754cf768dcd6e29d6367576b27cda5b6213bd63682b6eebd94fa249dc27d46fd17fd0f63e6f2d5e44894498596cb64def1c8df3e9ea546b856a3ca90f8a62008c2e87a84c1ff55aa10daff0b3c4b80e0cb7ac612456707d453ca503fe5103d136c25e1b5ab3f97e960607fcf7415a7e7b54952e60a83d969ad598a931583f40a0c3f99e9a0fedf663a702e7d285811d9bdf0cb08b67bba77e4ea718ecd53c202bd33a5517bf0dbf010beff8c6b1890e0c8dc9637ec9b6f27b42b727f2be6fb8f47dcf2fd89132c92ffa063dcfaaaec8c89ce7ee531126f02c1ed68554a27d919928c32030c2fae181d25c67c8cc9b176477baa0346cd53e7c7bd09cd6c694684c8ca2e28a08b4eae1fb528a68444592e21529de298517f70a53628a7d38c8cffc59f7f3ffe04c77f97574c80fc79591c99dbf2467cf7f6743edf747120bd8adf4296a7b9fb33f953aa3d4ab6041d5bfdc216697d642087e0e31df3840f6a31c8e6758329e37d0894b6ce78ccc1afd4e8f0797636c3c43094076e3e66f7ffd0dd8b274d28a7fc9675019294d0e61ebfc02ba9af823e5764081dc3d8fa0552f0efd5c098d2b0dc05075b80f1dd9128ded10b3ae67964cc4ba7f4e8a5320c6ec4c0eff68665e705e7d180d0e3012e6f037062f437bd4ac2af7740f65c4c016ecf16e65b6fde7e6279f07a3ea8a2f1e50cc8e1d2ef0c5e14a04148ffab4d5646a368a32f79e1db362668f7379eb39ddffda26fb7427fe6fe4057b813e9dbdf14da83352e5e1342fd1e9c360ac211f7185f2f0d093ae7bd1af68024be0e5d674ff6fd5f16acc750bf95750bbccaf4b206aa1def900e6031d6707f27c96fb7784b31c6c83934cd15673eb07ff84bba8ed7938ff5d0b0d8b961efd8de8a55d35b1711e5821183a990f00cf8fe609742110f84c8d998a5eed0c74f29ddd7db8d34a98cd89c3ed82358ac72a32d52bcc56ec1f1d9e8906533181b56b82a7065e37de7b1c67c4d98c4bdfe535a4525b8f65a48b3df45a6c2b5921f759920311f2e4d32dcbe52aaac6463fecaf0b8cadf07698b9e6ab6dc8280bb73423c42db75347a6b139ebde6352af7ac397502163922c38966ca83e83f91ab83f585e1f95759bbdf97b563a0a9b848e96fd65ae2e53bd669ceda51beb61afacb30f2f7338c4ce4871d464888df2341ddc7e4ebd4f0c983131cffa6fa75454b60e52a648c6dabfd66f61e6fdf4c95cef1ca76c51522493a0144b8622dcce95d5e12d44fdcaf187dcb4256eb2fd44314ca47a1abd9f836b2af766a3e744f2d175ab72361b09dfc517b9c59bd176ac6cf734ba8b0683e987e78c4c251bef3ec17beff7ef87ebe45835e24b2aedfc1576d2f1b437084805a2bfeadce3c016f5fa7c54086ef358d8a39b93cd04cd132231fc6dbf72e92d0466a8b83373625d2aa59ae9478c370deb9b335a0badb8061256f8b91f7caa97673155529709a2b8b69096b89248ee447958d69dde25aae7f2c9a5e6a9b67fdbeac527e191e93f217beff7ef8664ebf9fd9eb01b75b83b6ae3fe95018dc1cf3f89ba9470c7059fe54c48a10e19bd359849c2e0e271636cfe333a6a644e89c7e223c06de0d27d1c889e158f5055742a1dc635aee8a8e407ae79e6ede63c5665ecdb3d30b9bf325cb8f288b06a5cd3f4a7efb76f9c3dea1b837345b84e087cb41795d7b5287f017beff7ef81e9e6a176ccad56f3055bc7fed7a17c32b911af96fda01e9b32336bc90219a42114b67eba507f3179c7e011afbfebdba9fad23a827291311b4f2c7a0b1d69e6d1d28e83cd3618b91da1af8802e72f1e14da7e2d5688b38291ddc228e752b23fc27b13f4a7e8bb1ac53d81395dab8cfb270a462f295b24a94fd72bcff0df15dc555865526b1c3ca4bc0452cd05077969c52aff04d7cff00c7fb11566ea8f864f12ceb401159d3f12edad39e8016fa4826fabce4a9bae2b9c908613bbe84146113a3588284de3ba6b1b7d8604e9987ad23355012fbc33da111297e14bed7469836376ef013942c1d9c2497424b30a313befc85efbf1fbeb573754e8da7a0c4ce6cd29fc6d32f3f09654b7afbadce570576e8c49ef941846f86ba449857cf0a286672325e48236d6e159b4c26cac407af7cb8ff2224fca9ec8509b54ab34184e9bdf378b4eefcaa30778a7c66a359eecfa1bceeb41dd1cfb3c221b2d1431fddf6430844c7b2ab16dfd87a080eafe0fc35a7cdd047437e41709fde8182e0e1f6edaadb33e012bf6524c2ccf39f62754a55f8a30f44ef45eaafa9d2e9efb6227a90f625e10f025f452ff3c26764ec09120f5218ae65d4908e5b92dc249f4f83fbbe65f2baf63fc392611e8582aabfc2ca241e3607a71f25cb184affd963e6e3303bdf4030e761c1db7dfb6ece955c8c79732993e24a22c56913a8642fda46c0e411c0e9401fc3fccbd44b7e1c5803d34e1d6cd306660e6e42f695413088205448100f61b82b1484b577802786edb037d468f05af43bef6aea72443e144e14aac6ff6e7784ebdb108aa40c08570d001d9c47f42f300cd465c87f347f7f5d1e7ad829c59fcf43387fe0daf53f9e93ae4b05d5ff59fec299629258cd3aa699c064e009d919a7dd3d8fd403f7fecdd0c7c898ffcbd4c8b03caf70c0a50ffce470282453d571b4f0cb8c812f75a727e57e1f77fa421d83191eceed452145bbcb00f7eb46fa1a0c301320030ea2a947f2f1c7cb8c1185d1245585177bff599a074fcf01adca40e245f0771f811104f80e4a8d2179124443c3c34f38f93b1a7ba3d2f3cac0c5f374815bd1bf4f46b0fcfd91913ffb1dbfa64b4c43c8e0102e20b06078056c71b6ffaa23177ff8f7fb4f1eb9f867f0035ebff8cf1db9f8a7f02f77154205ec3f7ee4e21f63cef4f250fb6bcf87b4fd571db978f9498307282affd78f5c0427ff7ec4918b187f720de0be977ca70483f759ade104de8f066d33f4315cec6f4a211d5a740d5c50ab5f37dc55c4869046a7ab56216c744d9fd0d956c6969c1f9bd3beb6ce0c1fc7e40f0e1084ec91c09ccf2d8532998158637c1a32b51b7009471f5fb1ae9b7f1fe670f29001095c7c097484866c1a220fcae127a92a226c6b4ecbc324152879be37f724156ec9457e48dc1dc72bc47c9b8210a7c2969f77205f9f301cab110697876800a5513da7ada064064f12bbcfd143c05fe639169b1fa171d51ca460ae39f890fd513d1374a4482ec1334a65ee27d27521f1be9b3c4cb81225c5d4ec2d3ad4373fc538c13c4979ed78a73d94f86e00071b87a0abbe8d780b7cf3a267b56c99003fbe5bc07ac7a712aeabb745cfa963267d2a3cdec5d2e92b29e409d1af9fabc649229ea6d5f73d78e139cb14ce42cce11fee93dd2a476e547c7b5cbb44af2eebd437d53699863120290186ecace2a32cbbce4252d3f260b825e15d5d734e6484e45e8c84a0ed6c3949824d03d88d4292c4f124e795cfdacbfe3b062ce9a922cdce29165ed0f272e9657557587ac34298a46f10bd471fee6baf49eca06879067d678435d2adfbd0feb0ff55a962850ddd6d12ebeef84452f1742a3b64e4e2e854b404d139a2e5b58d9d2bc642d543ae5275a49a024f3aaf267a5fcb512bc27f168b15bd69166a89e089b578d6bdef9c5b6e4f4ff2e2309dbfce5679629db1b7960547784b114ecfa314cb638770213b5f7c3c8f41a2ca479197a2bc6529bde07db4bd821b721247ae5f87595f5ab0497c4034830d49bf6ad613be735ad95c41e9698f0873c3992e7f12fdb0b33ee7c4fad895502654cf9bb6d43726d3f43dab3867fc8c8c251adc100e325bd0a479ace7fcdf51542391642104f78d493ebcea2919dbd218b909db66e325e1afd6316de433dd899d001a62d7f7594832551e9e5c8f5ac05a8839bc6e93e12195afd7bbdaf96c939370d295f87553777b52d1dc4ab4a5744a0cea382cdcbc3b2a27c23c6bd8aa5a3a5ac0598696ec2b747638db593a317f9c2c7d474225bdfe25dab1a5e0731174792ed54f06c43fc53609699560c0f6f52f54a900c37fe0cb975837c8e75d6631f80b638422224e33bdcfc9e0956f9de47d2286e9fffc68a7bdb3d0b9ff5602e9fda77cdc2c10cc1fce3217d9b8e353e7801a525ca10f31955c9e6333f77e27946162fd0289f396fff1af0626e824e9a9f0973f60251e32db7ed962add93b15663de121ba90d90ce981f910469a82dd7d8020d79e611a3273ee7e3c3b547db8e9bd52789b5888889767eb5fe9c5d023f43c09aa747edbd1e102ad9fc2e3ed7bd44adea37a7d7a47d75d028d784d3e0d5078182977d9132bb7fe8cbf69ae5a245fc5af8e21321142ed59a40c1fb5073d688ab36e9f2245e604193588ac48f6feba9a9e5ec5f38e75eb5d2ffc16283479d644bb6044d223a27cfd722f7da6b387c7f64f52c7df5a2854ba2191fcf4f903f7fedf9f5f7003b8ccfcff057c8abc234394105f748ed4b6bf06f782c5509b28211a7524a434add2b0e494c3f6631fc963a245bee371e821da415bdc904e8e87303ba7f7a0cd6e9cf797f0f0a0b22a1143ac290d85c22353135cbd89f5efda0f7effb8c63a3f7c3e388ffac5b563d81dbf2f4ef3e17aa42e07e1d6094c41a8ebdde0b81319c2bd2f84b6f579c86e0f5c11c09d267ff107edbbf3f7a82afd32ccd58bad768158c85bf7da10938cb8ac2aa891667ce5fbdf872d17c2ad8f8865855cf560fcf6752fdc5c2129e8bdab8bbd0dba09670af3ac2cfc1c4c8283dde28f924969a81503b31863b3cd1b8611ab42fc988f29ca293f3ec243d74c572923fea9b2e5bb3f8042f5e1f996c6b1bb6d19ce56d4b01cf4937d1bd7ff1833a66f3140a90d7a0833a81baf8c8cce1e8d7783d032ffff9e1f10e928fcb7e687e81c7b5078b345d6eb3f323f50fb7f7a7ec094c4b98c3b0caf8c9b1a89cec3b519fa185ec92f1091763790fcbd610dbcf87f41f636e0feb8ea2f7cf9fa0d8e6d69ef76bd7f94f9189ec7700ec2eff02b72e3a7b7cb476e14f210a39dbaf5cdda7454bcbf01f5d67a78f29ad3b73e90f3f009c2893c64911b81accabe39fa2963faa2087cd9cf33f49fc9db53d40e8a635005ab2ec808bfdd809ed766a35dbc4ab09d75ca83b14105f56436634ca54bdfdb68e20695d219cff28f8a4c8a977aa05f2a2a30d4aebbc46cca3aea73ece12df00bdf7f3f7c5754978b4d78bf69b774628de8bab72edaba2140f44d25ff0744debde93267aa820b7cfe1a8a89e7621174e1a7c896b5a44b40d0b07934e0ca4a05cfbbf9e00afcf921cf83fe02439a31db4751a99170250e52b869d2aa2348e5b9b7212b09f28f750a96fa5320f68bfb5faf53962f6f69729c3ae7a1f7ef07cf2f763684989d495c8069ff4c48f614ff674232749f1f5c201d5af43fff685ef70b4239921ec8d6252cd0fdf2297751b63a4ac470a2c61b74cf370feb4d86a2679ba14743510ca774d66efe56b93018656e4ce8b72a9030823a5b219cc0cbc730cffd0219a62e6a60b24b73ee797e797339810beb851d4152ee49061c1aee7d522a8ca02480b6b1fdaa32d45839bc1a2dab2ad2055d78f3430ae99774ad3dbdd7ade7293725d84ed86a0fcc1893eb44c27bdcb0df7350f1adb90e3f4cd5d37b02ad631eb66ba24dd9af9b7211f3582b0b5c3883ff56201f5e6e7c21ebc047a3cf2b3b4a17b5d79b66b6ace9c78667c8603eb8bc6e80503b8654ea80bbbd11743df14b2f65e629926708fcad2f676bfbf5c3274821e453d88464efdc5ff7fdedbe90dd22e0c9e3eca2f14fc5f968347ed470a68c18cb088d11a22c3b132b2ca7d01c27d9111c522a0f1d876c974291bbaec926162c624fd558e8ed4f762abfd92b9d1b39c826a15b6373c479c5aa830817ca9d633375de83b748de7a913b5464d99f23962d674dbc62fd5184667f1aeaaecffdbbb5743634b1ca2698504a95cba9931fe1528b02c12cb9df61d721bbd1e5bd3b23953e433a989f1ef8dc7ffb82145032530d8d471ae6a04401b983128f80f9a1b2a1762960b64860da981fbfbf75954038d6d54bf619972a7ab0eb56e0242b83884c78e01963e5a7a7d833735deaef82e9d967344cf64f3a0ecb15c0b16bb1795b4b332e0dc9b98704b59d089610fbac0ce1006ee866233d552fd79ce7d42f76fdc5ae90b0ab9fad94e893e127bae638dac8d04fa53a475b1b8322a48c9061e24926374f2b0f9447e645cf48225f16452655c708c314753ec70f534329e5f3247a48e0a037b243aca46b82c3acd363b4379f24aadaa9d16a63d9da7db73b8f702627ca4f3c06ab4fc5662990eaa2d9fef33149e9e329c44759dbe27981acad1c4a242162638ab0067f9d5d892412d3aeb6690b1567977e86da28e8c9beca02b0bd8950c10594b4be3d4fcc84e32d8f78b89a9350effd255dda22c31f718d407802a7560a47aeb6cd9d35b595fcb49b06d7bf5eedca936403542418621b2bebe29c93a40cf36d174bbf732970ece2afe824514dbe85359b791cbe1f938fb5c5036a0b6dbb5a11f791097e70cb9299f617bbfe625748d85537dda344a1606fcae0029d8222f473fb8024d4954ac9342fc1b72a701eef53113d83057aef7681161674c27165e8ce0b26ef2284d7adc6592a0ebe40f18b465a22a99d9ca13a5ae09a4bafaa7dcb93f9fc4dfc18aa5c9054ead527a7448ccbede10a3ec12fafa4418d8b066932e7c6a317925511271b8e098e2ee86ba1dd189b94bbf9d7d9155bbb8020ab12461ee723e90ecf8abafbed6518c0e913c28a1f35ee5ed4abb9b178a6d8581b38f5b3a800a2383c48275354a2d4b63aaec9678a678ebd33ba32288a177852df43b660efed6cbfb27b9d4c60cc8590c906e9e8b31506e98aab770c1b383fd28d7e7c4c1b413546d19ee6279fc65cdf6c9d355ba6c5452c588c0d2ab42b7d4cf9c5aebfd81512764d767dedb4daf8a6f1634a7bb9b354de7bf7e44cadc4974264ef303d2893bb03f7913719839eede2464cd81ef56f87f04d8ddf3a7c3b3fb189f5d4b612f7c319512ec9fdaba65c343157a84c9a705e4fdda6c76732d72f5455ea234c92b1a6bad3a4717a779a4cdba251e381a58682e148d6cd3edec3021889a3868e6969cdd3cc37c56f49fe32bb6234e1e58a55b6a7c689b594c8b78954c6ef92000e2fb29a8fb2e96a596592a21dc19537a05b14a06906042e5b4aaec83b65b4536935765e3b87c7f4ca8e67a439eff3dc1d39ba4e6aa1b95b6c77bf98b22c76f71ae27d0e54ac943451d7521f70165bffde2386122b23f4b63b6d1e8fcd6bcb5ee344550bc5e9f2627fce89a1d2ae1f12fdc5aebfd8151276b53327a6732f16103f1c52491089686bbfdd9fcd0222f7f56a6197daa7a57c762a11d0d2a0e62ce96393fda0515af90592a6a602abc4038178bab6ea3b0c1e8692c42bb43d4c9b617343b4d4fef7cdc62310b9d1dafc347a5c0b999a64b4b8a168731528f84f2e285f33913e61a3bfde3a1986ff8807a35a0223cd9e113ef31cb7190f2cbbdafa0552e849fdb91a7bd55ffef1576aec452353603e860a9b7527ea0f512dbd6ee5f330e629e161cddc3b17cedb6dbb06e2e0e8e0ec7f60d80950638f77d6094bc14bdc0b6acc5322e7eaf4ea8534d66f252590420b41d52f541b75ec37a8c79a9eeff42dae1df8e5533d4afa5bd5d823d40fab909217a837f8e6c04d884d1ca219f4bf339fb5097eea173fcfd8cd31793dc4f5e3b7c7a774c81677200399c11c7c8dbdb5541f79c7f677f15947fd191d1a03b254c87e267ff58d7e0b1ef977382eeca246c5dbe75fd22f1dd77d0d0304d91203ab07ee06f02984b263b657c8a6f834eac2d9c53141bd6c78205090b1cfa835989ee0fcb5747ff4ba0c650c5d36340de211b7d39a451aa882cc9493dbbf7583a185018f1d8c444111a76be3ded372176d0870513f66687f98ad70147790fdacc0324f8905c4110cbbc3b4cda744fbadc4809f38b4a3e4859814f187c169e2b4e8a48b44e9e6428e9fb0795225edec61d1c22b1d6954a41b1aede1388afe0343335f31f2213d2c54e667b3b6851ab4ce699ce4f60f6fcc0c7676d93b0bbce05de0ef79e924f37012e63f30b4c559d9504e4ddc4ea27cb1352991fa694b5a9ebadafdb18863a869b337721e818aab2e6a13276bcfbe3534e6a1112cbf9b2266a6b78a5b653fba6d7eae3350f8416c30ca1237e7cae0202b1709c361758e9d8d1fd0247ee6be90b688ae7a44be800df310613a185d2eddf03fc0a17f94f55ac1d1c252be5c21a046bd5653eef3e8c2d64bc2e1bb4eb94f21021f9225aa7f3b2cef2a2171b6b5b107e0c681fb044f3dbb3f6b5d35b621f878a050ef7daf81b7f918b5cf2f90f8c9610d6abb38d97112846f02486f5a7d3c62a5e6dc84bdcb3f6b80ac2308e3103949f5251d61a3edae5e7872b9757251389c6ba2a0e3b2d98d2fe93a647aef6c9959a07a04a81088a8891499646d01aa89dc4bfaa96732be8de222cc95ebb8c2b51f34e40111c0b91ce69baabdbdd78f2d41b26eeab24f5cd475bf4ad5759f0d7fe9f5f41eb685fc4aaa591dbef397b4c9d75aaf9487236ccdee62da85bd431dad78d550f325bdc378a4151901797f2aaaac2f91ee459078cb0ce0d8cd2944ec4e6a543e5175cf94737194b78febb5e401eb8880a08177b546f7b967f6924a88c5189be2f61660a7f6a89b48cbbdfb597e1ced9dd1fdf962efa7146f007e7beeb2bb375e6f7076ecd9ab5d93aac5733aeae004f45fa39087bea2f3ee032ed208874c802c198a0602204830dcc7c5d06cd3a0e4698fd49e5e9deee695c8a7d7bea48f2ac42b0adb4c59f4bbbc3ab82389a0698db9870d58b31be4dd409fc90b083d3277a28a38b3f99b599abea4af249acbabefb5f1de7a26de681cc05f8c62850bc84a51ba8bce55caeb30b698b1f026355830ef28ba14505022819ff20546570b8f0195fe5bae07a3f18d78b880f9d36f70205798b8458225ec55ee2947d1815a850388ada0edbf0bd725445f65879f45048faf9c720b1301100396575e5af3e9932b11b347696b6d62235643c8c4c697f4eee1c87e874726cba38f6391b83a442aa55e5e01e06b91c37e48bea73daa681cda139d38ce4a884ebc05c06fb9ad290b2d74f19e0a940455eb5dc4214bf1fe00598764bfd444967337598187ba5848bab5f6463b601f514ac78ebc3df98ee2fe1af9198b88ac70ae2c2ea0fc76d74a058f3f6b27967f19770ef52c9e724f98d0de977487c2393acd3b5b461d58d82f0e2e5c7d385a4a012a55a9c596fa23913524f49aa475e4ce667bebde71d52fe9e7996b59b0fcaa8f88613f070e23b97b2a28bd07a45ef5b20cee912733caf6e1396e1e1ba6f0b14fb3034eda1b9a467924c2bebf1ac4f0fa229657f525898fc897670283244051863c0e32f63ac7560221fe3379a5d036007c7390af4b6265caac5b77f3500cdd1c7f388d0bdc99a458b45f4b1e9876d8d4a30f3b083c313849087af825ddace79062190d266ce77a4a17f93bf15b5e29ac80f149eadfc228dd64671b93548be400ad57a59a8f02626015e25efb49576432059406f074b7e5269a9eaa02fc1027b01e7e43648481d74d31ed9316a18cc2e700ec01120c525f12192ca4e45880d1bf1265922f6020b0fe25ddf34e4f0155c353dc9b5e01d227a3711f57555501319ca9b0adc30bc41746ef7c8583d4b333fc32609c01090c9f25c68d9f87df20846bc3745cf0ed66196ce690fa92be309d870e7da7ad6c7c23279c6f6fd463e4f63ae0fb1abe43d3509ef3094f2d9498ba295ae810b515490118bfbd032a654a82def95b8bb112b32c38bb52df832fe948ae1da15049348f555909f5ed45329c17e8da35bea4fb8cdbcff79f23e797e2a9ce524bd69946f21b527f493f0db323cd23def1bef622a024e599bd4957b305a0aae43e6db13626374766ccdb72430f869dbdf0174700b769c5ee484f2f912fadee7968ddc783c2fa07529f00cf6f55f6c8677ab8029b8de62c4d76554266c02902800fe9c69da3008e4fa3876e2186cffa58edee6f3002f059e5c041c46e83c5f12cba731bd43a2b27ccbe0358e9fb5e0d9d370951bdde220a9161b06ef129ac2a4f062843854ffaf784e87d250eac5507efeda7625b6502d28b55535cebbc2b8fde22f739eee3bc1dc2078d2100e2aa6e65fb42c3cbeb299e8cbaa44cccfa0ef9d6f600eac7e439b815b060d938d59787fbb367452c40557603aa21b5e1e1d72ce990542b7f960b7a834edf77435004e0b666f2e4d618fe18bae0039a3137178ecfec791904d8064617519b86c6f6d3663b6d23effadb564a46ef01f0559cec26a2806d23e5ddb49a9bafd229c2c9ad05c82abc50eeca32ca47344c9428e79b0de3a9bea84a79f725fd362d1c31a7169b3abe6e1efb1d26c3bc0cdcd1892fe9b1b61fc833df94c568a4a036889095e826852f01328638de3de0ccd8ce78a4b9f1d19beb8d9145771409e0fb09cf7ee4bcc61be397ad35abaa7226482195d40d28a5aebe908d99f6dabfc4a4e8f15101c771c4714e332fe0feef93a90f57cb2a3d569defc3afc639c232cb0362d4679c76a5dac7833ef80486298e88a3b6b681de347e496fc22ebf726f33697148a65663ee6a80fbe1a949c097f4f7a795f5613708b9b39af41a6f5db5bad1e9270138e29d12cfe543862b81a2cca9e008b671a36811d500a05ae106db103c546ee3057954c69209514bf145c2b35d007ea72730263b90ef308660b8df083d327cf1740a6086f8f0565f7d72fbd66d11e663a133a8f9dcd56360a9fd846b2577781f4727304aa2148cab6c1a10bef6a8fa929effe431033e96f9bb0fc35e0ab045551d0944498063b086e8b7aa1b451f7ad523f6d539bcec7fc8736a70fe25dd86ff41c033e8400e3d5dc4cff02c3615afdef000aa459fcc40dd9450ffecf42413676ad570b811eb2801103517fd62d381be6004fbca7c1a5734bd5114b3dcf61a809e7a7f98a446e07ed1473a7a861aaf94f26aa42380a57b65a6dc82ab6300e66d6938332f8952d4ab3880fc0ba956fb4411827c755af57dcb63b6402d722b3a807eb26abf5c25182b50495830bb153329c1fb42d45ce54bfa1ca15324cf5681a268a94e6bb090eb0323740440798d3c574b810c135b1fe9bb24a7c1eb3c7e6b52dd465fd24de03fe330cff610e42a8c4f9d8aea230a4eafba7c49bf23fa8a7cc2755b2541fedc6f19abf8c51bece7fb5fd283e9eedb3e7b19e7e2ceec536092857985a1560da0ff3a11a327fb09493485846ced8d93892746106302f4b31464ea97cc1f7975a1e78d4e5733ef72cca688167c49a7c17f7e9e9de4a9ab535ceba961a43dcff8207511a0bf53ca24755ef10c323126f3c7c95dcb1b79530d3851a5ccd23567ff29168c5ef8adbb5b4d93e857b7a301eb33ea7b03cd2bada824d10be2c104b7a8ea589494dd01fa2da9f549f4f8bc0466acb22fb2fe6477aff8ad08e863f8fbff9b3704efc72fecc20ea177005c4992e6d631f1dc4d460296cfb4254df6a0ae56ffd4bcdcf419a1bb273429e5ac3b3d608e2c01174b0b3c5d175ce91e708592fcc9038372ad3ceef65a19d84fb4d4f4a54eca3efaf6b32f6e59f7542783199f3884a659b09631231367071797bfdedfd6c8d3cc99e32ff78730df0198b7d5607b178dc5b2b5ca4f2f8c3790f04de546057ac165e8e0f2b6c0d1bf6a57446ffe64ef5bd0e68cfa3b88c608c21702635e05546b76a6797903ef346bfa6524726f9f3a83f39686b5cd9ffdd898989858aa143d76edca43e16ca27e1947410c6697a1837bb74b1732bb42bb0ae1f701d7c202fd90a321c3d0f7c6f89b0ca1fb72dfaaf994be094e60f9be315f7b36a7c083060ac9eb0c718338971e7914cde94896c8eef77e626e4b0fce75e2072e381ddc3006d0ce3fce16356e85b3af53d54e364ce91ee1d01e0678e6dcb015706a863e86d608b150f3bc1fd6f13fcb12b4782b9d3f40d5db1e395d5cd4bbc0a736b5bd49899926ea499ec8f3fd8766977ef9d7e52aa9fc361cc8bc7c5097cacfb9e4f824cc4a479dbeff591957c03c32e347fac82e377e73ec0d93108eef8fdf7800d2f13743bff91f4d272821d93bf777f983bb359fab21f27a418564fcb3be2a69bfd91f5444ad90327583cf998bf340ecb0abd508419d07e97e579382177eaff85bd88a46d61f84ad703eafbbee321e26492f2ea182487267b0f5ec39c01854264c95c9aaae8ec3111d162986f79928eb4c02e0b3657a5b876317d5d2733b81a63b35ef9ae7ad1bcf331dd534614986843fcca49b88054a0489d6bf2866d637f445149aa22a77c8810157ebcbbf29a5178650f5be8043a9c811bb0d2e7f5e2f19871eef9bd4e8a40d1e0fbda8c89fbc70fe0a5bf99b87ad18d0ac36382432ecd81606c2103b9e222d54b82a9044c0863db8e7c9d2a620d6a428d247c6f4a054caa33fc9423f60322b6bda6437620c9ffeb9a093e67c311314319e1822a246045904fb993032be2fb9a99cdea8f99a73e292616017aec991a0282f673c7569d603cb9606e2b587705a4b48f6e9e7e622b316581f023e15a1e4ca9380f3a3a396fa05924bbbfce35f0d6af62dd68c8941ff5b35bea2256c680fcbfc625dc50a0a1f7385f54bf3bf58cfd5566a9dad7f0ce1aa02f8eb786681719903d53d686bb7ea41fdb5f83691f4b36f76d523461fe985b903e6fee08a445cb176b37334736636f330b134b2b730bbec376560f560fd93ed2f61e652154a66770a171861e4d5a2665434624d6f6866f2f7615c867e504e60f0266237b679e1e1bdc642c75be507f93d90001eee89c3cb9f2b3391567e5f4315dc3b5cb606dd77df418f68fe62ae9e10c2951d169cc20a66bf0bc660010fae0e2db8e457086bacc2dd84d0c90cbaccf7bd7ce56870061938106474d80c087904c2e7436cd0f83d3e2e558315eeaecbb773e7b4df45a3e6234441867fa00cf90fd46015fd434c5ec3f8c3729850fb10d736bf5c0d5caab36f2f6ad07a143a1332c47fcbf907997ea33ecb8faa2dffa7f9eb673758c8f81b48ffa3d981d060f25f55e3f772eb3fd56b8d37cd8033effe03357e212d4d09e921235090ea88bfafe14bffb6ffb0af687fd3a29baabc95522dbb4c5afe0f8f8d6dbcadc54d746ba87c60eb5cf5db56916304a7ffddb42098fe63d3d28f16f86128d43df14935b6c9a1a2a564c04d5a9dfe7049f9a3ecd0177e7e19213f72d342cec669b4465df96c256be40d9f1be9ffc7de5b4065b92efdc30fddd2dd2948777777488774b774498b9202028274494b0b82024a87d29292d2080848487ceb9cb3ffefcb75ce56debd39fbfbdeffb7ceb3366b2f19eefbb9ef2be69af9cd6f66ee59e8e2febcd6072d12fc832af7caff382dff3e04fedf548eecefcf7d597cf04ec9a4afe1af755a608b10b6a90130ac5ea0c2819b8767dc714ff0f16b29da8f0bc218afffc83b5ea7d4aebb3f16fbe7fa43f7ccef1f6c25733d724656e59c552afef418fd8fd6f8b553715d6383df8b50c0084060282030c810e8564836c297d69f4462a1d321d02610281775ba6cb38ac8f47b9825d97467742e95e386fd51f4babf3bd6b0bdddb1c537b21198727bfec83177dd8cfd0bc07cff1de31f5995d7ada83fde1206bcff1eacc9dd12ed90da932262cd607ee51cfb2f97faea144a0ac7d31d3436846cb97eac4487cdddf02cceaf7ef79ba402dec184fc91e7f91346ddcf7729cda4697f84e5fe1f990f89c3db87e8783930f5fda572cdfdb9386aa81894e250bffb54b02c4401cad72e98ec6fa7f6567fc858a1e52b659b0efd6f76001c1a9ebc287b65abe9ef6a500a7bc5f9c65fe9d65f45e85aa14723f0c634a28e2b5142b15483c251f205dd2141ad1b41a1c45f6bf29f36127c861041ac1c98ceff61fff50b05ad271ebbfcdd1be41899881d75edfa070d7da0f92fa0a13f3a4c371f9e57d7195d7e0faba0fbac9877a3eee95539fe7ccb8719a4c46e92f01f9d2cef1dda1cce9d011a911152c31c3b8118bff217cdcab7e502b73f08b201711303178ff7c7163b8f480d9f274a3e41a5c6538d07f2d7f597dd5f55a631f98a10e8d771e7172d6e1ea90034acfc17d20a17f88b748b87a877b7b88d1634a55d001af1fb0397efc513d0ca2b7c9fbf1dc36494f6dad70334e7c1854364ef9ae6fc119451bad970258bc773a380c1c26d452e90be923276605c280df5e5c993aaf670e0fdee178f9113c91b93b43ef413f36598d055f065fa7c55ae3d0b675f58cac4cae8e52ee64e31537ae2a702d0f01a3e9212d7c97bd163385adf5713154c65326aff7855be6880346aeae8a5cdefdb9f877c19816d6062a67d557e7472e9b28be2f8d85d2c6e3a25bce451188f334093cad7a942b38c1441e6e3897d75918a4b96e35407d2a09bd87ff4e4df336ab2a1dc347845d363949201f4c0ebd6e09ba78679e2d0135e55eedc89c1d089980a8cbfa822975eba9eee87d06aa5574d8ff7d274ab9e0009d17af024eea23a2ba8dd2f90cc1d9972c649f06f0134b20654a830a2d6974b9d8ac9114dcd4ae1bb8c1f8faeca0547ea4f3217c573464b83079fd5c0889a6c970128f9aae577ca002f6e83344d8134b730190326e953e068882229844f4475370f53fe845938eebda936bd04cc1f8100fd176aa77d2513b22e248bef6543cef56019ecd4b95c44153b21abe28a2f95fcfe793e4c4e6680756dfbbd2db415f36271a35721f27ec239012f7609b0ffc4a87ebc691134803b1fcbb27fbd47491689c4bd7a55de2253c6a047b6fd68ef21797fcfb17c94fefe01a028a007dc3e918b6514a72c570a6157e597753e1d071086ee6c92ac0abd507f229128a67dc108f60da945daabf2e0a4d8f52e07b448472e047b68e6a8f2b65133a0dbd98824454e5d8d95a1ca3d54dbd3019e30aa7c34204a904754d020856aff5175f6b836ef643d89da20b8e4aabc5d8ac4964260fd79fce9fe1318ff362a2d3f5fc0c33b5d601528f8fed5b2ef0872a2bdb4f1a38f5c1c78fe871728efea5d605e5f787e9c111660711fcb7f5774556e96ea8e331d875dba9c17c3753fa83c6ed4dd5bfcaadcb957ce5cebc10986610cfd57f5a065cc86f17740b6e23d54a30e7be6586c2b95b61a470426ff883a3ae0f93711e209d159e62d34b5e6d6028aa85717d0ac80fde9bb3751b4be0f6bbf52b5f4741846825bf163b10c6025dd8b68c5a2bcfdc2f7f9c8592e2ac5a32a385ba07e86a9c8a0841602cdd6bb2ff758182ad85f1acda1036922aef96f6a66567244d22b857c70852b0c579cd10e81f97f3117a88cfd54fe58e3ddf2bc8764e669c044ff5579ca404a185af9d3f862afed26cecfa56ff30ddf585f951b67ee1311207dd210c77927e328a69899b1ec0dbc5f5f87aa8d46aeecb28d8f78fdbbc9bd7b2b1d94c0f8145544bf4a2d961c24cfb05f7c04af8aa2719712e05e582b051605bfd85a7980813f0063e19167881b04d0303fae052b7be9407d3d38ef1dc433e6cc2296abc405f4c75ed9e5189600b4de7c9d0625fdf21e57af5bdd55b9bb8ded7488b074ed7ded880b88a8c3e04a6512d049eef88999b5a81dee0444e42235f55377d53d0c5e403f0c199b65523cbfa52e9fab3b38db9cb4e284ca9c7455ae2a1ae6951d47b572faa85ab5af40f260732f1a483340b2834359be4cc52bf69735a9bfe45577bd5708e85fe65729bdd0db2777510c4d2e194fdf078d94ed02e72bc902fc32839df3a76c3c37a71065ca3c47df0040ff784fbac22bf62c98a4ba2921275d922e290cb82c5d95fb0f1eb043556e4a1d857c7b7ef49d2180b35804d0afe125d6d4872f36431d57aa6f4f328f05669ed9755e953f8b59ad877870e5e168e7acabda1357bfb5ef43ba2a47c3f708d7a017ec3924cd642625af412f0b8c0522f3f5b9dd132d6f3ae3529138fad9aa0308952c6e01a9aeeb53eb6f1a055f3fe8d2a65ac1ade29d67f30a05e697b25521ef3b497d19239eb350cb8ed7372cfb18e07cc7bf63391ae8a0c544522fe30f29a54a9966f302c6cf479942d1cc8bae9efc3e7579e57dd4f6f946dec9abf2a54d52b3ad313d94289e876cf98717506ebe49807e65651cd610faa8daf1caece4b56b819c5f7347e903c0d92b6f44e726331f271893c9932bb00a9c19ca04ec4cf72a6c8220eb3b2df9a51a7d82771faf2ce3f9bc03ec17c901c47bf8c52f7789e7f46b0ae88319acc8004749c29dc72caa877a5c2d750a26378a3b19968d28f7aa3c31b81ed77829cc2a95f15b63733db5780bb33380407225679acbe3e045269d4bc1642ab2a6b6f4ebab03fa615d315f7c6f39ae4e9af82014f6580fe1b125e0ba98c7e4d8de6f93e87cc45d7142f2d8840109bf0348934908b54067e7e9ff10faeded4b24e72c150cf83220cd4b13621f291e235af9f5bbf201e194427c998601c02c0998b6d9d2d6afdb7179ed1b319c614c96855f028450487d3c457bd11725ac5698f4c33fb516c84ed10269260f223cee697f1d74d9fe3ae0b6798958eb5f05577b55de4525af93db53f5485565a9e1f6b8f43b8b5134201b5cbf75e04d26fc68925941beea50ada99874f867a044779a49099cee9adba51431335250df6a2eb23a34d00a2164dafffc76e5c959004d9353b5f95bdb6dbd3cc055ba7f50de53a13e89adba8fa5e394c0fb38ca6c1770edf6cbcf083552140e34c35fb9d7a1be2dcfc749079a53d1c6497a7ffad61aed60bab7c3f59efb39f58f51c0154ae512f1ff2295274c75c855a245ef436a70a108d86fcd789fad95b5c97e6c2d576d2206fbcca17e3c020215eb39ae9b2bbea7a87dfc873f8a197bbac2cb44d101fb4975eb62aedd7ec412772315fbc58b710c7961c0be4b876e77fef4e53189bc8c97d62d8a6762db746d80fd35dace3d232bdc5ae204e352110f8948a463371c03ce5fcfb1e984878d275809419b6346318fc54fd959aeca87072bdab74ffa48fc864ee8dfa2ebbb32bcc001fc079d54aa379dfbab6d44e8cb187a044632de36bbfbbff59ee951ffdbcf7f7acffcdd675339b112097f52355b9f9f6d2add02898d75890c3ef971f7b7941dd44eeedd2fbaf314b1f1462f7d9c9eaaee9c55a17cf562c7b10cb95d322f505aef08a0d67f45ef19278508c2f5171dc5e647c11665453eead36ba1a7b2e38a6a9da80374b129ed0a50888144aaca54f15f9e6c7b74cc39094b24c5be8f1f0c0a83ac9adf8bbb73bf8ff82f1ebfebdeffbaf1bdee01fec8f8ff89f5292e8f61a0478e4efcae2db5fec3526ee73416ef65c5d48cf881a23211d9a653ef9c6d45a78084518afbb9f5c8c5adc1cb9eaa30758b019da629b522e47bfc0b970237ee3df30f8ca147fd2fc018a0912adc1cb8a278a94ab4655c0e5be48ecade0094f58b73a4e4ea3b3c6d66bbdf8a9b5b87dc93f5dedb023e945e4058d68457b4b4cde0d3686a7c0c0be96e6ec086223499f038f91106333241b084d75f365af116b0d1d7f7f1153b5aea5f50f36d6030f41fc5b4ee34002c0d99e93b524a2d8289744fa19ea818a20e55ec9803691b4664cc14293b4b9d552d8b82634dc1bbe934ae00c6701b6bf66e5e0f72ccfd07551b05e8af9cfb891fef008071bc7fd3de13fe6efa7cca319295425f0581717ec0465333f7be33237aaf76a1ad29526df791a2fd73c0066a2f0b5e9c7d133d8865c858e1fe193191ad0ccb01d0f1e8a50ab1fd5c6f69efaa6375e39144cf4fd702a9469fec3fc0d7e4ab2e482a2b0f3bbf9c90cc696202cec00b62573b0b9aba3c43e4e02698e64b622c49679eabf2ed6e17214c0d49ca27dc427ad6878a5a9a67930046e0fcb5ce2c77b7284272c250d3acf3558010b53990ca24b83f8e86b5f7247acaffed6b21a82c16ba578e40c5a2d191a233e1b711253a7e1e73ca410fbefb90f603fc6b59a737f03f9c657a0e4acc3fdecd0a571b33eeeb023002936a877c28d5a0de7bc9afc3365de6dee81c0018d48420553d969f68626ac487d5e266725533fa22c0868bf944960e65cc2d5bdec77ff661153976839f0d48b52d1eaa50e376763eecb5ba4799007b6ea01f1d06e80b6732ef4bb6d6558dee05db7403e320ec3efa7900b20cd325a83c54752d0e36873e4cf5ec22d8971006f459d670cbab073851e95cde2efcce141fd0ece13a80520150e9c4f5f577304269f62570d09589f3a6883580f3cc977c13a224f2d084b10df98e313151ca9bbc2560ff15d48a47faa7b8986616895128f34756ae54f903efdf148ab2ef9ef14de194efbcab1da1fa13397cd8d58c0408afdad74ef6fdb6c7e6f754a905eefe60acc16f0152f5c7e12d69fb3f4fab6a5be70f8de6c45370c6b201018d3ce6833cbd0ae99e2262059ab812aef0dab05ec0c64b9424e4789a8e4c14c61b39f15e734f82ed901b987fa4024829568c972fef59443c054a22794dd004d09caf1517a1ec61e9527cd652e6b7fabe473bea9e60290a62323adf45185785cec235fd28f2cba1b6e00920175dd6a5db40057716d7991249f3e5fbd1f0cb6a2780e99779375922b0e0ce5bb7f81c245c9fc0f6b50524c0063ab455f7d272fb4e1b76a9108c15f5bc0b41f11ee063209ebd3fee0c5e59e3c1a4a587fa84a1f8e2b92a6063199358d57e6991c2a489605e1babc25140b1f80ae8dfb4f18c69cf4a0753c8ab670745c67744e88546001f83bf8959fcfefa4b69c257f72eedf7d89c0e13b8815e681cb34107cba5a46e84fb2699bd317e86158a5fb0aeca3d69dc21be5a4c26726e63af9f937fa82b1e8c03300a3b0148daa33b8aac2b15bc26c8aa4fe1313ae080e38649a8172a30c4dbde9dd63d49d5434f63be35184885dd9e5018b5ce542fb7c29bcf7d267fe678b73870f7aa3c09138740226aa8552e16457f574a7c3d409c00b0b70cc2a46ff974a2f12e763f75768daacd889a1205d2f27a1e496d737ad306eab18f32553e8ae03046fb0160acaafdd9757ef1c6c171b409793b9982cf67bac580fd217ff0a89cfcfe78f7f3e73bc7823bf87ba59b715a0046de153a373293b43129185b8d499e3687d9ec5f7e55be62186b7be724534786cb26b021178ddfd79bc80660017cc42db1d31cefe02fc75eb0f8f44e761ccd0de87587424d72ba2c2ad9201216b60503ef0911ec3f05c235df3e308bf8493a43d6d678c276b7b831e744928054ee538c9ad8c4708f55aa54e3c32fcdee9d54592211000681e73f2a750893623c89cc6e52737e10196b3c74556ebfd2a19422ac87ed7bbb4896ab8d56ea117105303fa23af11c8adb8e9ba89f06681949c62567a44f001fbb71fa959a6b4d359705a15ea41da3e5b7b6efec00c6a981a7f146c442de786412c2f9e085b2f1da692290ca3e8bb79430a04a25fd354411ba3d2aa833b9fe25d01e84df9b0c8702f7f157a76799a4fc743288fb7443800f9c8a501f86097936376bf5f228886bdba5cdf10818bf4f43db2d9c310d4f8e899e464b10985fbe6f70074a2d6cd1ac07c8cb5170cfeab6c32fd37c688d34200230a4596959261421f94f9792320b2b476e2e962330c0fd1b7673b70d3109d18d5a371fd5ecbe9fe6bd47ad01ec2f267266a9f844536935fe20b9c7887e4fe98600164c3b55ecdbd947e302e2c598b13aac4e2a6121d5407e13c708836a7b9fb8f4f47ce6f46d4bb50e94f15000c3a1e5e8bb847bb1de08c93e8d8838b713510e7303529dc59fa7e6b411a9ee4f48b49695ae273e5717a300ec177fbb79ae17cdc93dafcb3b438e1337438846998084d2e1cf3db7a02648c5e6c8a6559eb51d0e31aa5200189cada6098a452aecbd3aaa187213d4c1ea1753340086b5a8152ed39bf17eb0f25392c63482f5b426ae1570be8defbf5c9e65d7921dee9513518aa634fa62781b589f15d5bd877155e4b9ec7623dbc2d0956f1fc17f0152cd99c758e9a573d735fae947ce9d1e362c85bd4007522a1b8843bda7f5f5096f07ec6f651220480dc33702f33fdc2454be7c7780fe47505eeff81e55677a6d38601f2d87ca2cf75c4ebc67a747ca6452b57db199c603b094a2cd72dc50189969b73308d3a8df96d93127f70018e7831f2ecdb3b41ea824acd0382df7515170521400fdbc1c1fed522cf45a81347ea658879f7c34f3420260f058a791e4a97e91329d48c0d559457f9eb0f1e31428d5912f5892ad5f6e8aa4f0dcca0be3104a3d5cb60cc04006110b96397d06f075a29fd63b1296f01c563901a57a10c55a55c8782ec402dee2ff20c8c2b1733c9105626c25d3fe871931a75f8de61f0510e6b39a604bda0118be4eff43e50c5f99be9891f432a1149a56546a27e07ce5cf7716170d19af223a4877fcc19924423e269a05380b78456e05abaf8f5fabfb7d7aa6b14787ffe60510a3e91cbf47db5fa715e75f8c336684ab34841541260fd8376505e12a6f1f9e44848eb12c57adcdabcdf513419f40bbfe56e5d054a911dae29fdb3e8df54a1f2c8f9b6e88185f160920132194c6c928ddd045fdff7bdba7eb883c16c7334680df22d577f248bdacda6e33c83f63acc1c2b9d7e101b0ee30c97e887ce6255e389a3a8db0471232c9c2caa2b8210ef04b1ffb8f3cdb9ff9f256e813d8a12b7d59dbff5fe8cbfaf19aeb619121f0a83718b41ba65b5c0bac5dc75cfa439dbc7faf52e0d57ffc89bead7f881df727fab616df903d272ee76b49568a4f6622b856ede38d2a94bd6ef015132182b2c9d1dad2f8c3c1eb06154ea1a026a7a586edc928ed408277397e5a912fe97c3048c5c351ec6c471c2448fe525ceee70b404a975a333963e3c94faf44c627ac545568bd21b0766d5fd2df5d33ff3edebbc91fe15d09876e71c0895724b7f79025405bf0b948c7553fb9e6fed7a55389fde9f1f96dfcff3591fa4a37ba0f11d094d566b34190bfa741071d2bfda31fddcddab3fee3b91e66ff221f9a92fc0b42cd9200d3ddc94bacf56f1783ae106d202f667eb28262eff56d427626a16787e872e11893ca9180c7c47190108afc8544230cf6e3238fd3c3ec470d3ea5fce868c9ce5ac6ccfecd7338e4c68fc46beec2efccbeb020dc97b0915a79a6709d22e4c7c6f772c8a3d112eeeeec7171c817fa4a09bf852ad8429f4b47df48e25d3e7cc38dfe9f7ce8ffdbef7bb35b8450366a7f3776159dc4d69f6e9b4537e5b7e3fece5ac892232e426dd47d1e1cff806f24a564951eb136a31aafeb8e1ee3b026abf95007366dc7c3f5487afba724e8c49e9f1325c85a38d6325f318eee2e5b101de84e1b0d3c7b475355b81ef8c59274e13e0f2e5695e2321942a49a3b736bf64a61099a49609a9761ddae4b9164110ee49a93e2b83a02119b37893bc72d94527baa21df62b9ecf6afaff048baa1f262042bd3bf7598149debe016f3f05891cedf459862e360f9cde663ff091df81f418f6ed3bf20e841832187628a239fa913a18cb750194796232e0538457da325ed8ff6e745c94261b3104c2476151e0e02f5477f881d53b99164ee6676350ca706c0f4266f6301c48b886d3f023379b35c3c215a0fc4d571e2d50403a04c7ed94eb1514c62f367f61dfd58cecf84f86eb9fea5007162ea3bb7c5d7e8849d368e979df369d693ea1840fd30ffa89768826a8fe698ab5c1322b282c552bb29800a178958f63483ddecc94a5e176cdc7341fc351d8e8b57e5b895ccb0741e1f5c8f124334b72e62edfc729903aeca3fdbc02e7749111dcded39f81b1012b04668a7014e73f80f6b2ccf81456a75e538399f86a744b6d56c800a4e8413d8be35e0cc87121d11c2a560ab44c4450f106fb644d857cc5733957cba37044fe83ca4ea246480a092b58b14f61cc6806a2ff4b9510e73a638fcad0ca053cbb8631b9c5ad48ff395acee0684d3c84aa7b8710034b92f510337e3613e326941cd311a1365b690670a04653cdc5468d52058f7926d7aa86f13c757fa542402c41c8b109da1697adf10475cf6626a6eefda2c5714a082b512d1d72c5d6e1de659f8e05308b357362d4419081ae90d0c9cc91f0527452e430b052522bd27f6b404887186ce9c85e6ea2c3ade2d4a71885603be0813a60031b45c3640f338fa1c9fc5f000c69192ce650b3e19a853c4c3bc805ec37f8a5de05e6c07bb3363eceae30b149d91fe821076f155f5b66f7f3c7e0b91d3b2594716608b7599c6b3ac53130e162d6d51054b3dad99f8209e7c558e4132f9d0f536d4f3f7727ea71b9c9f9ae68ca500e2c9670b321221fd458aa08dfce10e36341cfa867420358ec76fedebb0e89a8e441a69cfd9848d34b24503509fd850265b5296c4ece15835debe1ab22c5cbac136a00450c25ea38749b0b5d07b166474b4bb2b9bc85e00f52d43d9c6a23ba523717df6c86d96ad42b24809bb0013e3dd21451bebbc8973f29738492b8c37ac349353c011df967a32b457d2c6e6728bd0a8f7c35e6594061f401cd10eeaa8e8ba9011657899d167537ee6101e950a1cf1122749e747fbd94785bcf1f61f7998097ce82280fa996e23fada3d8547e528795cd5fbe5f3e939b78d81a05affebf3c8d7fe5fb511506857ec52821cb0a0e9006268bfa9753327cdfa4706c945d4cdc8865ae1847cc00176866e31ab92690c3f66731729b04e20f4815919bf2a9f5397831ca7514e5baafb5b8720bf7839205901cccf2a65f7e12d8c7772287053b0d6fd1514144d93c0fe716225c078bdb338cbe8041d842932d85fe69901046deee3be5fb8dba7c9d576e88da093f7218b94961d98dff19a2fd81e8f31be25b0c513d9285649c999806973fd21853ad60a9999d81b082c8b96fde817fe130009025139207b8a5f750bb939c7a13d35b8a2498217203e36ee74bc246a0987ebf6218afd44f820a30cce05a8efba8e9e56388faf98e5c2ec34d27dcfd67235a510203e77b77ea65072461cca9d301ce4558a9a81228701404f6c248efb7cd58dbd048df6bc3c52dc8921c4ba607dd2a77dda70d5815406fa0de78dc846da0df76281923cdf95dc8d5a3b16532663e342f0472993a1451f01f5cb75fc1a5fde5ddafe2efd5918d11347f1d99c081410142f2bba952953264d126928032bc429d74cdd9c0f64317df9f623bd88c40cf549a4d0da87e090c850813a00349d84412066c882be78d7298a32841d31e92df31500e52fca1f953b240db96fe733caf5c2175e0a46be04bedf232f62b516ae73b4e6e91d647e7ed588ef75dc80af6d4e9b57069fc4bd992bd425d169503dc65ea30ba44e6aea5fd6bec853cbacc1b3829b92adb009445701f453ed83274e426f3903c676de0f8fcc69d020b49f00fbfb763a49839186148b0ac7bc024d0dca52e307ab3cc01e972a54618e30befda15475e433615144d8311a201fbb38485e0ed4c05df5dc459a0a67c086d9770488d14bac9fe882ee1feb8d64072fb33b2961530bb40341012a91d5d4787529c1f7f17a6f28c43ab186a7fd00fd1d4dc14f56b3b2fc8e8c6ce6b51836c4d8b2f90980150c63b9d5737d5c408de611d9e415665c852d450412139ad88a9172a39527ef99e8e0bfe44141ebc0ad04ea338ff6db4ec8b0d4ef11e33c5da1a9eee524d26e034815ba14ec6f88a9aafda23d3b159bb3228f645eb302fa67e955ed4050c585e396a9af24546d0ef38191c62d8030f39e30cb622fb027d2c4d7dc1d5bf398c4f22b603f70d5d5425ca70d1bb8c24e4790de527fc77e6c0a90bc9cc8687da0a6040ec33cef288a43e10d17fa9e9c5e953f9fd3e78521b03db4bd9b436d725e118d2a1a029893c9f54bc41229444332b1119b6f5c18e18ce1e4002cc6db418b4d4587769eee50932bf62b7439bb340c60bfe9397b9e853b182492eab9a69b937765c31a3d05820a8271f859cebae4567c14f35dabfc6383a481872e003093e1599d9a2df2c335b829f6c331010566462940fc53225ff1b6b8b4191ff35b69b73edf167eb4bc0278cc3d7c911c29ef30c7fc30733ee6dd7f23f2cd3a18b03fa937996d0a1c990a4939df3f468a9aa2323026041203bed69e967877497fc9ef6a8e0b37dc2782231e0700d0cf12415eb7a44cbf9fc0d5b2509fcd40ab2fbe0040f999bb0c9de4dce68759e1a423f573798a0c5b88800f099f0bf3267be80e29d9508d4a4722cb00e2e20970be374036b19832b59fe9cddf53a33e62888a10b4048887648f4bbf7db43048666f6546e433161d9bf49e04820a9f8a7ca1363cc4c5166919c876031b65f283c781f593dcaa6db60719d15b445b43d07dd54fdcb7d40c10df5b62c4b712111f9b3e5dd735493e5d78706a940dd8f71f4574bfb138ed93e3a5b850f3a5da52a9d6d803f6b1223b4b849abc4ae87b69a92789cf1c3284e79c0062b8923883980266060bdb594367b6dd8bb374524f20287b0897ed6c5c71348863680af74c726756f892edc35539df474f3e0b74d4fd656a7a12b623a154a42906a03ef9c34ac62d18add7d5b49ac58534758d1edf67dc81a0ccd74b8b2f2bbe3d6c88b325635a1c2c580a6834e3d02728f1bf053df4f81a51c20686aa8d6e86df5e97590a7f8d8b86781dace18e8c23407d22498083b8d14e836977f4ca07f5695f4a01164e1b218a1cb3e1e840634476401cf5bdf381bd7416229cdf43d0a810e17f302af5233bbca0afea81664316a07056fe1620979ffa4d84e02edf1a2a6b42d4a4c191b61f1121c31d52911d417be63fe07623de0c9581ba213e0e7dcdf530d7e1eb413783f761af258646b063dcc256b11351d6e931cd9f659cbf2f151f86de8370472f6d2d6406a167d65d7771dab7db01bafaf994b46fdaa09b9eb3f844da50906b919dd41d7493c7be37242e8abf6a5eeba0c187abfa167c54bff30135a2e28b7cd3f8e80315d4e41e8538574449230471d597419599f9c7ee9e5e79ac97fc5dedf5c65215e90564692c3031410d2437fc7ea90adfc908f26826450aae8c8ffa94034e7b4e514c5a43ab50c56a45be64fd031e773a47b39559cb2490de1b11b8b46df40854965b4e1d558751edf64e0dd7eaddf4fd6da699e953fa1aa847cfdf102b6ee53477903dc7888ba6d33f0b82c3d9e4dd96e37ad06efe388ec37336019937c145cbf06264e4409f8622d642e3d12be761e6ad9bbebfcc12662094eaba9ee78b714477863b08835da5dd21d0b7a502d1bf079d3e9b79807ff42efe48b75bc1ba1986799eec32ee84e8a5a32cfe14b14d72adbd3274a7fe4ddf3fe7899fbb36492beb4926538aac5a54f42df87cce43bf578bd025c1ce5e2c584cf6a9dc468381efe1acc886ba63ab6a0972145e60d64c8af7e6aca64594bca7a9bde9f72fe559b1dcfa6227406d4650ad7b1ffab6fced7c8942aeb72913a18e336f5a22677539c3bc29dcf8158bca2de8aa683c751fafa339de4a6a20ae7c8a89052b472978d3f177d11b7be95cdef2c8535c2e12a7e98df6364593a9f8fa766c2ce3f65d477f79b25b8ff1e4c64ce69a47a65fd7ad056da8f1e223189d25dab4e389a3bae4258f9cdcf0fb01a20f0d11060ce7d7539633becc6e9ec24bd3acb64e80487b9b8d7b011dc32275a76c297c3adb957bb53f16203a681de4b4746a7dd4dae630a6e6cbf9bca6eaa10cf8443a3d7690934b3fda3ec3138bbd9cf8b789940d00d12402f578d6283db64a4cf8dcc94ef6eec1387e0160f3274139f194d7cee3e2737f1849bd1358a082f706d0ef490a33ee72785cb5c3deed7ae1822a0e6dbe6b804fff6c7df8913b1fffba5d105c611d89174a8bd723203e9566652116861149fc4e207d406d7c5ce42e524c3d208f3efda82e5a33b694a5a7d16d12fbcc40b813200aa775dc7e82bd7dd1a564c8e079164a34a8e10e0d9c9fe9631027e24b6fa3725ddb9d092226094dc30cc0a77a21c8992e5a71fbed9cff2be60c080ae2d9e36400736ac9fd1e75526e3223f4836407d6edd6ab535a1cc0a7eb52e86d68cd133b0c1f44ecd320b518d61277056cb28f58b35c9ff402e02f9f1d1b313e0d9d1c3b9a047c9a21d4505149b80538018b66b7682d98e0836058c0673bc025db3c254e8f984de8a61120a87abd134d0060a287e581bc216b857e97b929362ab73f66f57b130336f7f77591ea4796315f16d7434e321879a3a8923270ffc8fafc6ff053eac173b2c899a29ff566d622b6f6a7c3ee10f4f949f851e8b1b4389f538fd6cfba03dc457b45743c8572f1e62772f51596a929a4acc39f594f84a94d8aaa5c77d07e763d13e26e9709e9a2c8cf7aeb7d0e15f66557c7f37cf713ab04da7d9e85bb7fc71cbdf3f70b5dabdc75e13cf8fe36a4ff27efcfafe83ca2deccf6e427412d28986c362c86cde6ba9ff525d013754427f2e08dfb59f8b78301cdc407aa2db0a0f4f7e5f1babd2c655801d63fabf221f8b8ec95798dd746f54fe4fef214a115afe9c27f167d579aa9af63d99122fb59451ff656e30a9e5d19f5da9fc8d1243ab67f6cc49a304120add0238fa02c50cf85f08db4ff619aab9905b5429fc0b1ff373d098ee69fe94939536ff7050543ba82151d435e64aa1d94496130de4c514389dd3494ffbffb037b0d1300862626e812f966111ee8ff937a583e2def6380febca099217a3697e7861313b6571a01b4761c934baac2226259daf7536355f3643fc5667f3bfee7a3bf504ff01e5ae95d73fd72fec976cb0dc35fc004a06d6fa00aa9c8ec2a22a9bc2b9d3355fe226031f5ef94ffcb8766e366d3f087bff0fa0f8d298a1e80c90cc47fe2e1f7269c4521a478d03239be9f19c79cfefbbffc377fb15e83648f837baf3774e902239a33642d421225fcdfbd01017489959c5dfef4e4a3fd93779087aed81158a7999035c80d15502bf40954ca43835bdce1e2aaff45b2830a537fa20decc0579172960f25567d24e48df152059fb209f1e0995e7367a71b7133c4605edcf0fb21370361203837fc4eaa6bc61e9e28e8faa9a90a0a8565860bc51e9b6a84cad75f869af9d505560eec05370cfe43dd505d2dc32eebbfbcfa8bebaa7cfde11611ffc40810708da77eca54af99ddc7f7929e83a2cdd3befca7e751829c631fa789d4ecaf15aea6c64d9655eb0934735018fcad6d8946681a7ffe21b69b4dc8b5851857201008c66fffff3f3fff150d864020c0194738c3575161061324894a81ec948aa67f2807850f375fe5715244eea5f4f5a9b5d8488dcd185bca1c9ad0a543e0378751c8e2ac1d046ec13efc05cac3dc7d29b8f476fe514c7361e47a4f6d56184a0f7b5123faf21e554d5a030c1ec32e4325c9a433a4af245ecc869cab5d0c533f4c429b156c87f588247b88a947d6383276323ef5425209192f59a94b443eeeaf1434222039da98d456f25a63864e6e411349da3b1da605667ab32c049a4ce613eb31e6573a893dcfc275aef9f378420cccdf5e0002a1040a2ca05672aba4ab27237b7bdc97949c7bef97384287f4cf6562db30564f8bb583ba5279fbcbf2867a5f3d615c04b820cb49102b27ca40626ccde840f652be191efe11fe9fda38751f1d3eb711f5fd8ee817843c85a940f1cc85ae13c3e721b25c458b2b46f24140346ab9a7c7696dfef328742eaad05c4d343b7f3049dc55f9297dc9ed033d454f58765f7a72c11f4c8b54a6cff432991fae3066a3c6207d58c0eeb618ca7cb1acc9242bf926ca835f6d41eaaee635c3198c8520256f6e667879f9755438123227f5d49f94ec7bdbddce07368a270701c7b13752faff21e4fddf7fdf9bdd2244c46f36dbc0740a0d7efb70710d49ed3ee2787baec0c3d50011e471e8e05cab2f5c1f3dcbbb04105dad1b3aaacf3b9965b30458bea89c791123d3c42cf8a496e093ac4bb43762ca5ac7ca6b0b6cb82a922997e50c453e0de9173b78cedca96653b825877822bce8f87cecfd92cf1bcc2e5b429d4b448950e226297f7e942977813b7ed74077bfd8aef32f98b7d596fb49e51ecb68efbed14b2bdd970682b776ccf6978d7eb54cbe52fa8978d1512e4ed1d440c6a884d3cc3c71956e9068fedc2dafb78975cf2b83ecc35f1a797da5bf24d8a39abc680cc485d6e37f3e58e1d9dd49fd895f22ff19d275e679b06a458fbb623946c1f3f34159e3afe5c38736e25f503487dd6f4b6ffe18177040ffcf76fdcf76bdc9769d2e19c60aa3993b284075ebffe2820f67987cd125ca42dd8bea692578ba7d2cca74e1ceff0ef9b5e8d4b786fdf1d8415aec08a2be04c1dba8f2d16333c71ee182247a97b2a5a90c30040c8b2723b769fcec43e68eb40afb6e97a07b3c7f6566b6e4872df2bc44406a6dcf9c85a35532e1620f3d7a8577424eb67507b5727444d6bdf2cf6fd784842099303956d14c3846e4c7ad96d648101d001994c37036427565376744efe6862a9a51527fda2100d807091bb2682c520fd47124bbb6e4deb3c6b441bf2aebd5e6c068cabfeb956ee2e24e9c300963339fa878c6b74073b4c63e2e79ebc575db159a51ca246aaccd40ad48fe492e52df687764b8856fd8c3a186c54cf872d36aecff6cd7ff6cd79b6cd7b83bca1852022fa153493de15f46d9476f616cd545f3a6488ebe4567ebb0bcf073e42f0cef3d83dd7569126c2874f158630a84b970ee81cb0ce03a8ffd186fc24522aca44cfd69cd254de4f8099a6113635ee4439b61b7cd58e255ceb1857c29db702b3a98e645f63058fa1d2159cdc930ab480e08b76559025964be10aa61159bcb35863cf4091ce63f105c3259cc4638d80606bcbe6bdee9ba58fc350e16ec35b950b0f3ff9b10deaa435407ae2e6a992aaa4cc320dd0034cb24edf35ffdbd8df23dd6bff689ae43c0a129fe62c421e886d7df61f3b6e03535e3e2e2323535e5e6e2b0e43065b3b2e0e6b232e5e2f8dbafd9cdf82df8b94dcd38b9cdffd4f804dd1091b92e574f82cd9b9783df828787cd92dd9c8dc392cb928b9d9f978bdb92879dd782878bd38a8f9fd7949b938f8797d3dcdc8a9d8d9bdd928d9fd3ccccca949bcf8ccd949d83cdec460be097bd9f6c24e232affefb53a994428926cb1df431d5fc3bb5e28d93fa3586bfba1ec1600017f154c20c52a8d6e766fcc0d26684139dbcc69b0b916e9996204618111bb3d01ee6748d95af056ad22743e77ccae274b44c6af9ddbb3d0783e38508616411c2c4eb1056366fbebfcd350f071b9b290f379bb9192f27b715af250f370f9f052727279b15bb9519272f1f27171b1f078f251707373b1f9f05a7051b1f3b273bbb290f8fd95fbce7ffd163f5d60046281726c9bf26013ef0bd45413e12233055ac215cd88694ed4ad063f7cbfbe12d99b4421f5706854289d5862a298a35e43b969276fc11cb60a7b8d7be4fcd0d2af9c46878f81599de5d0383eb4221dd37d30222aaad5027b01e11b84a698189bb9030c1b45e4823acc57b5dcd077f247e027c7ebb5782e78e8c117796647323550cb2b29e5d4bc3196555e1d99b9722e17e973bbbfd7fe478b11490435d7ee4fb2498dff7ad57ef772a626155a55fc79092afa1cd415db3fdc19ecc7fc864c37dfb3ea4afd1ea9af1bb26b8072d76ddf85e73fd7554be6bdeff5aaade35eaf5ba1a94d751fd20388328afc9e8c2f7bc9b945127e6b4305fd1ebeeeaff81dd8d7343ed70e3eb6ffafcff1f5fff977f7ed18f9aa83645406ee631d3640ad98758a8992e9bb838ea5fdd0a7757d1f804eaee43e4f5a3e656c8df93ba1ba1a4b4d4180a6f68235cdb3e4b5c52535e55c5f8ae969a9a929eb18ab8a6bcb6b4b1a6aaa2b4caffcc88b9a18d056c3216a905f7a3315168ac10817ecc5622ccb16803c0c63f45cb4f7e98b75bdfdb4bc269bf9b3c4b33fdacfe17fae9ef63790233f2508a47c9a0f2b75185691fa2ae9db8d9a8feabea308f3317fd802f47fadaef427d039e9283ecb0d8ea3c9936da99ac23cfbaafecb3428d44aa8f4c407e7648595ae4f3fec69fdffb1fcf7af5b31f0bfbc94ad0d4ec24e72c41a0eef452ba8d88eb66de38f4759679f6cd542ff43ce47ff707e62fbe3faa86a999990f858ea98383a5fb9fb89edbd6c5ca4d809555ddd1dcc74bd3cc9d538287e7ae932c8fa6b9a3b386a9adb792b605bb24af9e178fac0f87b3b4f2fdfbf77d1d95d5fff78caf789a160736f29c28969dd0503e5df25bb4fa7c4b3e7cfe27316a6e5c4abee43151ec3d215fe768038d4639651bfc56ed38f7de3ab61c59c14d8a3abde251d33d6bb98e250b7d0253131486b2f90e128672f981a71126bf27c883ff66ba10dae4af5d3480c18610ad2795a7bb6c2e6468ad6dfe4a7f03028fdc0e8d08878400050b0df3273b7e9adcc460138b4d19fca53c215aed97f214b05df7bfca3f1eff5a3ef24bad21960eeff34b7956a8de8de4259ebf8c708ad5d9aa5d07c361181cef70abb6ad7cdbd5263abfddd00395162bd3a8c4f9214daa6089f5ebe5c89704fc497141039e131828918f9328c8d49b6b6fcb23f5fd5fc793e7bd26893841441409faf88071d25b3bceed75cea233429d78c1013cf90686c5d2ec9c405482daa22a91188ad681e07a0e53543c5ab30a8dbe92454d582366710ce9646fd9750c0f58a6c49e9a1c9c44af8ef815cd9c8332e823f8d08ea772d24fca5e51d40ace329e41179c54770d1d854a54be4df9386efb493411592a09f97c8eee0c56966404415c61e2c3a657b5b22f6dd7126a636e7d261e8ccefa8f111efc342ca38ec48df73bbaab6d8552ea8a950485aa9fb1e8c33c1507b85293de9688ad4a4b29ea3f6ac66c853eae89807e576e73fe0412063d4e641506bdf479b021d36a7e2a1b421604216b8590429b40c8a0fef61fca6ce297d9fe9d24d49437296810c8a942cb40d857e4bf0b2920a45034dc59d8d010141eef87acb00cb7b40d7b59f6cc5f6f263479faa0fe7603b26c08d9fcdfefa49a2ab0e7871e32754cc89e71ea8c1c4bcc8320feb7bba0307e54ebbba54c9fe54b6a5ddf8c157ff656c75a1b22000521f8ed212150ea5bfdadd027b07b41a16fe594433f0e6c35c22e97bbc6dcb93aae71ee099f43d8686b3f96548d28e9268bde39fc7c1d952d5bf8fda6cb7c0d2e556851992a3decfbb01624b1e31c0298532dd14a368d06dcfc172df52c46c7f3aa1a0b8b2c8d649db9230a8bfa87e9c8ebc7efe5c5c81c863dac84b450eeb86d8d0540a78b3a1f7941573a25691df755a898e709df330adf76bc6625c09a2fad0edfd074cbbe993a058d0cd5533d9b705f871089e5c2cfc3e2c15a232221ab37f1cf5c9c1d7ea92e9c5ddc6d7f797b4b77a0a6ac9a3025adfc638e07ddd8c696636527780d6eae397f44fe278e8beb4a6d8164a7df49eafbf5e5113f1f5fbfee0c79c9ebf8bd81404dd5ec06dbf84eca289eb6a10c0596ccca0f3cf09b72373d2f81f99a502cd6a26def1f26f8b6e76c6187585b59f0ec2f764dc4113a1d54b9a6f2a594318b9bcf5113e60ad55e7ad206e38b51ff1899e11976e135a19eaca9e99e77954f491177fad0d00827d45495f5f511c3f81d9dae32989a949ba61ad9df17b99c686da4095b5f8d3191ac5cead7e39131c742533543923f80b2a0ce452776f6981af482bfa3ff915f5887cc61bfe8927ec6ac98619dfbf1a4439ebe23c98da1452a2d8c508136e806a9fab84a0aa97fdabf24102b87e12b8c7fac484aae394c25b5c626433acb358a58c347540278cb81b61db8ac96e3edf667ffb8250946f8df7efe15ad14d58ae77f77e69752eebc5e3b74a42e7fa83e4df0ab2784175d54fb9b6af68e80c18c8ed176b5f8afe010ac45be52faeacd8c25a83fd4816433ea31bfd6f794f6e079ce78bf7db1c376a72f3afffc37679541f9e3efd31235a7321b9b31943efdea7e560ecc372c0409117fccadb53236216c91b48415c52c776760d76a948e29435922e2b272d4a1c7ed0d7e886c7acb4bcb9cdec7ac01afadeb1bc7bc744b3f967dffbaad3efc6062f10b3ac9bf71f7fe89dd0775c3e007d44dc4d78fff752e3484c231acca8c25d1e674b3630d5ef3188f98613107d32c60881485772a9b6b95e71af8f35a0ef235726a607ca0777cd03ea50a709f87f9c389cc563c4583d34abec9fafa1fac4f71ea51e84b9b2752c15c3a534e9c978f838616455a3423a41fcb93d8c73e683848b57a186a75c61fc9f0bd309945f9458aececbb68f4162c5bd49d59d6f5bd2e589f9b6a4f5981c3212dbd420eccb1dbe82724efb70525d39ba76c4a3fbc13cfe112c02f4297967e3798793b5ad0a4dfdb9e6d506f80ca9468ccc5238f5767cb76f060b06685e45a0d7902fd5b449a9c4fa6111a36854aede2868b1ef0d00c3c4e35f865d30776e033b2bb061f61ea946b00152ffead5d85c9ba6383f0d2cbaeb9fe1a5ce7da9ca79fcaa165b7129143a5b87ebde8182e6fa814ff07115f6e335e6e2b536e0e4e7e2e5e7e73730b4b2b361e7e0b5336735e0b363e3e76766e5e530b6e0ede9f90b77f6bebd329f51754b86b2dad88aa7d0c97f0266d486c6b298770fedb13201b350986c3d89318854039932afff90e43fb128a1170ff0433c1d15e07830b740f6c32967a3885b2ce7d0068c4c4e0bc55e434a5e8ff7253a60e4b3a8578f702f0789b5cb376ce0aca382070c5af5eed50f662297e032ad4a4b60479609f343dabc9fa91fd661345bf94ea36c0392693e88626d337d35d884a5df8264f5bf404951ba82073891d9a80d926446ac816deb0dcdf61448d3b0a54888ae440d511fa60e7e766ee58467c87f959b65033b05ec5355c9fcc76556ff0bc5e9bc8d11411cc9425072a6c2d24f973e6697c6d1e667281708ce0c7f1fbebe15d951f659edca7531a5b28bcf774bb1231a1492ac70e284b1f916dbdea0d5578cba96ba4d35babe299723b92ff553995ac500c12466162bbc4e2a86d5805e551150750c1c438b1021b514acc5359accff864e8b0b4c29c06a80088371f0dbdfc2a0ad940203ef0f0a262962b4d1f181f11d5e9d6fb19b3ba89c164e70a4c26f065f8fc40228ed3e3fbeacfb3f7e2cedfc67e9c53b883988387058c1f366274a29d8014c907f810a626959c843e6849a048eb34d284c2193cffd19b33daa5011ad3e19a2023c5ab72c653f6f7f22b2c0a58d9ad9f3ebdadf079903d04e0d876b8eeec4dce72492ad5e8314ac6f695111529405b81baa170cb209a8f44385e3cec253b75816fb2d501c4c5e25ead8e25ad0b8efe6454f468c62bd41d2e6280d536d87872f2ae8f3c268e23e0aec5637a72d25a6ba0029bb7bcb41abaeeb4b166c1f81d9c0284efb75186004410d34a739a431a260571242d9abadf4859c286f03be032563f1adc8beb707ce8659cb8b8b8731f734906c8f7982fd9a6ab80367f19c64b7b9f8711b2e3f2591ce0dc7b8563add2cb6f2eb6e719a7635992aba0c221006d9bfaa033f3eec18dd04b65311ae04bddf35d27c905724c75756116dea04736964f10e497989753eef6d603ef67d755e72b2b825aa842880aafbb69575140a105b4456af61916082aa687d08ad80a502b47960ef55301df2f1c198837965eb8d6bdc918be88aba2a478790954c8db08c8dda7bbed06b1147f2e205ae6eae248ab04b6b6fd6acb58a096c9e1d2e048666c8df0797f8d0cc8e7b5237c9c6e4fc94acadb62bfd7a30fd18eed2603c60fd69fe7539f70e9244119cfe3574e4dd6c6fe1e3157e5f7586eb3b377ffc87d9d33b376416e79e97e206e7f55dedff77402bbd9a39fcbf94922efcab4dcf8442450c172f75c946d479f675ff1c9aa10b7b61df5d6103550c12ec262208486151fc15c7d3a0cda68e6d1681125f07e0976cf619a02acdfa68b4c6d90bca74e78bfc50284e0bddfabaf28858ff47faee843a6d9178a3b3362042077e41f7df12701f99738b3814ca2de92e8281dd180cb84e28d5cf64c49924e62d8fae34996e4f6c4b36020ba97bc66fbeed9a31fe488691dd512523fb02ac6a7800a9fdfb3eaa4f704ccc2d0e943df481fe36dd847806d06efb7479fcab4271b7fd46447b422a88ca2d6d703dade780c0d4faf3529a8231ddeefa141d94209a356035a2fcbeb224e11f1466eb122477a1eaaf68f351be201897aa1a41d97d3366f9ed32d5b361e0e2520fcf02b0018341c64958f0a481c8be5cc5949895d63798e5769000a01f9e99ce9a7f0ed6e22728e386ae917d22cbbe70009de45293a5cc7b2b6f56e3f732ba3287bf4cb3baf38aecaa3198bc9fce9e4a0e2551c131ec8ed0886627f01f43beaa80b57ec6cda6ba7cb0c24c8ee96305617039094a91943e53c174999f6b92e99e95b63c7cba087af813698a51c532a19c9eee19ecec4b8862f65e01426a001566eba86842456e7ba95d5a940091f4cc986837e2e60c4f3d6a2e6a76ddb74964ca6118a127d8791d6b1bc7f55be64a9f754b85381ee4d5f508da37136ff00fd1850418f8924897bcac1348a510c56248b8921f069aa3ed0d6890d0ae2e48f0ff36e780d06a13593982588c0cce0aa7cf935ee5a74fe6d4e0223589a9d5613544f8d6780fe388253bf3ce95b3f7528c9736b2b4a7e8c1c1a00b48ff047bd57a8337c5673d111808a2a1dafd361d204b4befe704282c2d3de9081ca058f4b51b71afc3e621a8098a46105a6328908b590fc768a2ea3445f8e4dc101f3cb8985c691fcfcdb9347ebf4dd527eaf0bc3a09b018a0aaf84205bec420ea1b5e7db37117bd67229ab3240db3904dc2651214fd8079ef9a558af1e10d2c3353100f64fc587c5d290077609af9d75613d33bff030226c003991942c0b68ad3904388d56f9c2116e25672783704039e32f650884444e45905c5b8421483dea00cf820b1db0bf68092eaba3b956d36993cf139d678543512480f5f3216d7feac343883f853be527817c133cbf03358017b810c37abfad68ff88e6645b56830fb5231ae733d0ba76508fcbe6cb1796330c34f5b03c259d577446f240db1f539de9859d43069d49c1205ee90fa9cf3ff03102ad9f11b46a4fca1e985cd8d4da2fbefd9ac44ef8910f688be760ced7c32eefc2466a2bf452e3dd055c9c6b39102886d95181dfb28952d93e1af6b67ebf420cb7160d9c1f17867b2bd6dcc5e25cb2d6781b75534f3f61ed010cbd7be6526449979b6cf5813e6ce1291ef0f7d91e0015eec6b8adea927f3419b062f6d2202b5ff23f0b1238b92a87c33f0c11170bc675c46a2dc087e589507cf10ed02f6484cd186d71946f66ef91e712be1b51e7438b04d2f8115a71ca93eb1df96515fdfc5e7f9bdea4f2c700fca1ce30b4be8ed84fe9c8a6e68f9cdba8062a9cc0d6eb660c01823df553ac2a9a76c1ee6187a28ce4e6408ef53a865cff85d45368bf805cad0acec9d5442e7ca0b53b6f23afbb7579b502e75401cf088d51dce5401b60df0c8e2bba61ec2a927efc945cb4634d6bfa21133f15387f943267bb57420edb76550691b0f1cd9cfbdd80f17b7f7c07eb52abd3dc22a9a134cdfd53eaeae30c00a49213b23bc34765d6677736a96a5bd0dc0c3e5ea20420e2c6efd8395af2c577aa7752e843c28265b24a8014d0bda112216cff81b2401bbc0cc2cbcc6f1290a0b0bfd09db945903efff57b394b57f7b71a4f6ffab0fd8eb806c09d1881e44f60f6d132d769e1e4f6a828050a3000db01ee1d11fe59b7c067ce96aa88e69cfe286dfe00a02020838790a58aaf79f36d74cfcd619cdb2d5ab462c076177923e0c7a6b51dd01d537d5ba264d571a67313e8040de1cbf3258d94d6591f1f8e1281ad1b4f3b7703ccbd8ff194957d2e4dbe918901511a3f8432341a7cbe5e95b38873e41134f394d5234c6ae6a0779de9f80902ee0027c7c3815beb0d071b8db4437d1f4daa1e39a8035dceb0e668fd53badb949622aae9b227904ee6c29501f6660eb601d5f7c47bb10c3ba7e1c632f7968cc3f100c89ed8b6ec5638f7f7840df51ce703caaeef94640ec0714f98cc634a84a448e9d81f3355c7399213c83d0120f69daacc8ccc42da286c5a369d7a3c1cd36121b70146d919bccb036444777732b72cf451a8f01a1f066ba0d3394c8f5771f9b7a61ed3ca4fe9fe240f5dcd5f5f00904a74b5c186d296f6bca69e97f36bad2ce507702bba57e542a170d60a65fa89ce3fb409896e55e58ea6b402ee28bba45d6df463a1d446346f1515983055dd179fc1f242a3fe0f6ef9123e41feca2221b2870c457bc800e4c4753f19c257f801a1a8a6ffe69125fa7d8b4a7d19e80237a2306b6247157e1735ea95626f33fde67cba2da02e4f835204390f864f33f3b01ee2d6983cc4149c070a22178e31b80e9068759295f2e2d81cbe7471a59b068ee3b7c12530275eb3772e5ebaf6f4917617d0a8c502c72d96c4010a4d7e7c9800ccada1ddf25d146ae351607d6c5560a81bc42653d743075e62ee9390500a1e02c7c97ba83042adb5cb6953a5adb19e0aa1ad89a924c0dd3f9b9d9973c8943a6e3b83cfd66a16b2d67d3102147f3af81cb2bfe68fa0450e37f3c1e70bdfd2bbaa83f757e5f9f19fdd9060a6c78da7dce1ab93bcb0739089802e9df61d1138489be2cced9e17f41cfa91aed614df01792f199d6d7beb5d2f3197e2ca670ce4b8bca25580b9fcf6fe05c49e38aebaf1016ab9b440013e53961a30bf3062e451177568c51b1895b7494472b42539f08082eac2b9b624b46ec46d28ab7cb45f9f623023c74703e599f42b07371e318bdacacd717f368646f85ea12509303c64a6fa632b93125d55b47206c39f07a60c747101faa1e063e4b89be30acc14e6f8dda14857510ade5100141ae9d5ff30efb8a551085f8c9a155f478dcb1c0e1c27cb35ec4fa9e7e5e2789dab5a9847b97333ab9581f9f160f03b29358c5da7e6a98944df98fe0235aa0fb8fbadd30b65988907074551d9eedeae86615247c2c0fce096ce4827acb4aa4cdd61e928cbf72d2a81ab054a82ecdad706b4e2be40879d3dcff2fb868cb10031070afee3e097772634ace92042e08bb4a5700a8c9c9380e266aaf277a7555493b84ba71f8a2546c4c6cac650375f9517b504daf0121253e1adbeb456e69b78a11158021cc75b94a7d3c4ac23a5254a85470a2e48b77b24c38082d80a399ca450cfe28be1f61ab676996e13525255035d16cd2ae79e2eda2b3fe02898f1d65bfb444574d7cdfbaabcfc6c6f8f33875eecf5313e25ce82b015cd792cf955f9d42d49478d0d564ce66fdda736231322334aed40e32eccf31edd6df95ef1e34bd38728af96b9085fe001e521de75d1e9671809f00db30606753cb6669f904b01cca10826ba29a3d08d7c5c47f7dc9103b2a087d4b9807e9bcb3235bffbfdfd8052051d164206b71206bc2a70ff20422241ddc8a38066a44efff93d3c8fcf31c10081054533fb492fd172a571b7e7c3dde208fe7b035980fed262921795099c3646e2f15efafa9e903cf9dd2150f03951098755634e19d6529d2140da488e88b04b04e842fcf2c214298a877049afefb2bf106ea474834e0030d7d1448667633e33083d109671fe4e56d8b0a9c6ca7055fe144736ccad2e5150521b43ed4748db73d8990d008e69a7af4e60fef14a7e1acb667b1b8b944a9be018989fa78bb46fe5d975ee2870571b9ceedd0bbca43000e044eeda6fe20582a4c3f7ea32fb4b626cdf89e3800d1f244f85a8b6a2e1d0e57ba112b49b0fb40af2abdaafca870c284d334c2698490c4510c95ad6ed04a9ac0102d4b9b2c97c1f996b7b12d4047684d863ffbe87f4f180396e17f18071d6e1bea72fdd8bcde775234534749b80b99ae6f2c68260b8de8c1e1d21d42d20dbef5e2f509cef561946ad086ae3509e74dc488a93e03cbae636503cd806827e0bfe73124332ac743a5b65480a551717a05fbee86de17a7b0d320c901f58bf3957ebd99a1402cee7ddeaed8320de74abc7339f1f14f4b953842bb903e189e9d8d0cc1c53eafb8b051791ef68763e0bafa6010d354479cd32066810d4458887b6ce55ca58ef36ad02e77bd37c840ca567e8bde97be9b3a91965de2c777a80f94bb3e80ff4dd467e127309293380046fbf3183f971555e03addcf8b97a98c480d983cc170f53f1c72b6d607e61f7719a0fcdfc949f28ed121e4b9fdcba07cf049c1fab968cc80f4366869f720897e98d6cebb895c302e5e15a07a56fd1bbd106df7be05d209b8b1c93444b0fd8a7e54dee0a4fb94a06e1bd988e9c7c8665dad51c8114c439af989d81da357c824b34f52211a6fd1985e700eb8738555ee5e296be778c4c68e66b78c57a0b416680318289176e2f76db4503efb23882e34756d9e6b036d03cb0c0547303ba8647c2a3b819b7e8ae667955d52720d5e6ede6fad93b3bed52fecd68c4dd99fddd05111ba0cb706329a3d83397c6d76fdf4d746dbc3c269279be0ee87762a62dce379f7da42de753053e11b4f16c19ba02e5f91aa8283ca345901eae7f83ba73c7f8cc22eaed30e0ced8924ee5f764b709b771b1599f90d0b7877ff8042474390942466a965edfc1bf7c9ea014df37da1fdf0fe8e7d7dfec92655b03ee1867cff92f26ba47680f3d06f6a753137390bcf3bec3d984a8b15c135a5641b11410ef4fa6bdf5192dd1b257f0cc7a2e312ddb40ea821fd85f0621de4c14ddd0e4e53cec14644fe1b29e7c41f4b82aef39bc0b356bc71697c76bdec846ad9eaa9376ec0e7d027bf41b1bfcffa44d35c22edf9c100efb2f6c01893bfbc84ddbaabd8564993856ec6cb1258b3e5cd921938cb4d2d5dfbf19b275e5e345a487a4d0ad793244d4ad3f0c48f9f9bdff9510de20047955ace2be8c678a46573623696db9b6ab7b4db41a72b368f64d09e1b090ffe59fbf9a108e28ef646769ee6e69f127afff6f42b89e8f2dbfb686b38597b9b4d65d7e695d89fbba56761277ad397834eebabb4828ca3a2aba487bdd75b1d3e531fdf73dff4dd91cff86545676530b4b7e3e5e2b361e7e3e332e0e530b2e732b530e2e6e4e737e0e4b6e735e0b7e0b1e364e760e7e534b0e362e0b4b6e4b2b7e4e730e4b532e2b534b2b73cb9bcd9fa5a7a3001f1737e75fb43ec4d9898d0dce66ac5e94d060d48a69d30b4758c5300b7176eb4efb6c2b1e56e41ed90c538d8988f1d2b6bff46bd92457b27afa487b7ef745e07bcec499876f1b1c9dfe1ff6de02aaaa6dfb1f3fe7d00d920a0252d2d20dd2d2d2dd25dd0d12d2a220084a37a2528a202920291d12028aa40888208d08fcc7bbdef7bdeeabb22f1ebdeffddeffee31700ccfe7ec75d65e7baeb9d69c6bcecf24058f5640c2fba3562e12e2df502bb71fe47e447408323c9ae157d7ca05cb020211da5f5e2b37ef3883f503b5728be08b068188f1abe7d63e642c15d30a7ad36e75271c86109d1931b5e932c2c5c973eed0475a55c751327d6d215475886cc22954bbd89db0b8b3e3cc655dbe9e5c098e511896009cb572c194d277050079554fe77929c8dd38def8f0cc8fbfa054c58f532b17ba11ff00f53e442b151fe5c10ab36cf2e8d56b73d1da700d20407ec15aff05b5748ff5fbc71e5f1c6ffc06d82ed42b1c6a56ed10b6c3f607c5a97978c7cd1eb0052f6dc67494e5de3e3f2795f41e4c84ab24aa2d70be2f387248f8f52e5d8fca5cc8975f9ed852ae8c98e1151140b7650e7ab3d911b4fcc617ce4826b00513c5d3c4cbdcc5d1e9871724f7284233f5c5ebd822f8c3556aafb52b27842dfc6cebb642cb037d75b7b48cefb2dfe615daddade3ff2493c697c4b56742715ee264a3a8af5f7a96d7a8b2757be05f099fdb0e43235172c984fcdfd0c36669426d9be0dc4b800d1d0c0315ae690183b37f99c7b9ff78859c3f0f27c8ac008c0fceedd5087b65f530dfc48f0b12ef78ecc36e1428ff70fb4257b6305afa62e07cfe638d3f58106480be87422dc1ae654a44b05f962c6607711311ca71d406d8fd60cbfab7daff97e8ab0406a3f23a079f616baf8249fe998dfea5e386a2c14de6279ee5ac9a27aa91185e9ad27d8073ff2e0af99fbec0c830205f65e86339e4677375b38e85d64db7d7e0f1559e6dd2fa6e42144336cda92d66f836f8962aefec729a6a71c7d70f981c5fa055b7de1c0cca76ae653fa3ed2481661da5ead2babf4d7e4e3e85af0ac1df07955187b8629caefcc23973ede79f244dae8c85806ff077d43e67c48662bc1e16f9062781281bc30e3313c127d60ff90493c1c51093d3028347b5884cb21c0bdeead307f4775adafb91ca48a61fe84ae43c4528f1a83872d90d7c9afb1b71026273d9379a2b77786bb621f970286a64e59dccfbf0bbf779706b8f5406045d6777e2c21078ade7286d42a01c0e2495211c2c63f6e125c2dbe6d462ea1f274c139994a27cef115e9b39c1f11aadaaad43897372f1f1d99bcdd968f48ddca76fd1679e7d40b3e7a9da5da089f8a220b7d368666df02071b830abee36c353e14baedc44f867cc53bb8a6d7408d53d69899c70121de359ef1fb9afd14ee5fd3a314544ec3f909892955f543829757e6681c48f1a2d74a6a8b20fe5ab22e0c74c4c418673f9158b5bbb213f2fd2369164cd4c7d868d2cd6d8f3c063a487649db44c415d4564785ecf816320ae6346b7dc4220bfb245f63549043a9a5a67977cb865db326200d6e1ffcf135390e173e54128eb9895c41f946b462b377293529860df3fdf4b2ebf5a7a263e3e2db8ca6aa86d174eabea3f9d9802269f62ad7a4c78868792b8187ade853dca8bc3b18c8b658869e379913362c9ad98429556dc851ea1b2aacf120ca4e2310fdd3c885f5d4565cd99efe7ab44473461a809813731a577a30bf5553a57c8ede617dd88c82cafc78446c993445fb46f12abe0318cb0c49c9a458855246b8ab89baf77b8a81f5727ba4a6c94fe4c9c96dc9433129545c00b7c67bda319763ec76c49fce0df9aa75292816d2cf7887b88653cb6c0f66d91ffdab70c4520e5b224e67188fee140873550bc8062c2b72986411a7786d178169f161edee3b3627c3ae98c103ac0af4b314cd238a644eaaf7b7503befd0d28d7deb12ea9a5a927fdc3420907f962ae5853aec2bd6bb6dfb575ff3d5c475f67907ef10609d9c4ccc5d1d5f587efc7b371b777b27061b5f032b33271b864c16aff976fbd5e4d1f13e77bf4ac05cfec3904b9c07ad18775b03c5154b712c9b1445d3dfa645c7f87806efb8a779e909da073036c17b13ff08f13a16669b6cc17ad0327b238f3246ed421cb3e54f7cb5984b13c29d73c614966ecbc43a4788deb227ccb0050cd73b9fa433ef28eb886de397bee021ecbd6b4ceb5f1a3ee3edf9dfe14ce6d021c2ff4b78b91cd8bed2f5ebf7e99c6c3c33ba17ea6c3be597520925d3c30732784d1e23838989577dc5224a254ee10389da720cf0f434559a606301c3c1628b6e3e6e119b6ff2018592d49db3b258c5bfd339f11acfd13ec138f37ddd2b77aac25b2ddb39ecfcb382a15c33346e7bbd31b20bfd6015cf27d281602313bfad4f3bcefd5bbefcd9ece0542bad16bec18ccb75f9713a58dca654014b837b4bf35d8df1442cae127f55edf1a7a3091fd3c09e3fb098e70b0219084fb3d92b89dd6f087870d0151a33df5c50f2fa6887dfb3ddbbcdf6716f4146d2798da38f2e0c0d28982f267ae6c611d6c75bb018f552a5f580e6a2058c9dfe8e404d44e70c618e3486134a5992d93e19c605b8f0e081a3287f7373f6091229c2fcc98df96a12228e634baec3bd0fb5170a061c7edbeec3ee2c73ddeffa6cd0acdb3d22e295f024e89ba27bebc374b342d4a71bdbefbe5dfc5054edd0cef8560f5e38eebbfb4b6fd6b6e54fc3b10e4ffa646ee4f6006fc4ab4ffcc0c78438956d27d7cbf96794ce1391feea429dea242150a9bed33a2779bcf506ef7081de1c2fa2a1064b327a3952042ffbd75f87ec136937e3ca6887a0a7cbe6b304e6e505256380345fee317c27ff8f7ff08f45071b537e3d5d1b8c0c165a5ae24afa2a5c0c9a72c21a5c52ba1a1aaa9cba523e6e1ac2d67a57ac153824bf1d27fcff889052a5da75aaf97f13fd7237de0ca9d527f55dd62c016255a04cf0eeffd0189e24b8ecb6efa8331e5c895de555d6fde3ecb52142e7d59eecb97f1c9c37bf256e664e25f383742640f0c4ebc15189c775ab80a91e6e14b8b5c08347095a43e2da8e559672239898941d38aa748f6d58dba6623ee3d9117e347476dd03923acf934b3b94c11c7bf4aa616d79aeb3e17bd278846f31e63fd8ddd6d6f0c5ac64df8f672a0c482a2f02955d0bd3ebc44f9bffa1285cf25fae3b604b25b153193bcec51149d50b156e22d30e33ac46375dc58cde8e9b5d19804b1ca3e0e949caade898bd39d9762a8385d9935e29498049ea8cf289093afbc57ee4c9bbd1e758df98d228c4cf5306e59d2b118f27a221e59f0cdbbe233eaeffa53f8b90360a47533892d4198c3fe64ef78d5aef9bd0ef1686839b0bd101f72739ddbe0ce8a8d0ed303036c95fe152128516d59395ba102912879f8598297a71adfcc78680b094dcaf129a9866fda8b46c9e76fd45d4e2f4aeabb149a644797aacfa2a8d963e4fdb607657a39dc39fd2c6474d22cf6b516d8f3d10f2355bed4a7cfbb465f4080919d41f8fac32cccd10bd99b695225b3ed92698aca640cdd2321e94499d4f1b011278277c53539dccf8a62f8856c83a734061630c575a9bc7990e372f12e707d7474be82d8b217c6ae1635f2e88c5d1b15971e371a71f393e2b589543ae342ff67911ba20d777b339860891e8f68162e6acdf53a87fbbc52abeab8c1eb19b724bc24de6d29d6338adfedb01b5dd2a9fdc67239676e9faeaac11971c794d821c575947eaae5f1d3dbd286e79adfdf46ba798e73cd6650fc77ea29b487fffafb9a7a4a9763cbcf7044f169e924fbc35a66915bcaaee2df35efa16f50cf67eceb9834c07661ff2e297db1ba0a4633b9fbf8864b9a2536196c76f6a49ba1c965f3f3913e6a61ae5d1f90c8d104ebf3a2d04f607cd9d0710b1efcb960c2b72eb08208f0e9315046baffe70b3e5c9e5adc232584f2b6b5d2f0205e400d31baaf5d5e4815205e13d47e38a5a12de88d05f92486b6759a03397777a884e2741795f6f98cbed9178325e476c6db335d7fc135ab150ec56e3191e4c8fdec9b95d3ac94dc6cd26e85c73d887d1195a401b6f27f0eac5c96a07cb06a8b9006d840387b981e339399c1bfef0b04a927f1db2593163950caf99c571fd1601fc9cde86879b08b6ae8080cc6f1960cd92fcf14810d86436dbbc4162090cfccbd78533990c006d85004c2b44bbe81a81df0e3c170e8650ea5a6e6af3e0dc8d866bcf1f5a7897bc8b35f35ac5b103d4244f9f597c374f3bf6ed8bfca23d6fceb4f1b5cca71bffad4abbb563df1eb76af6b763a7df5a91ffe3216dfd7ed1a7feabef6f5a775e4f798819feea80686206d4e41425082a1855f9f98b6c9147156afa41d6619e0583fcbc48bccd89ef80e8d12acc4e06d3c63f30619a4611771eddfe68fdef5c05f14036f4567fe047b54293005b9f6f49d4d2ba11c2669251a63f6598ce724d92d87489e54859f9e9c941ff709e9e4902e22451a3a8ee973e04d1abe85526cfb5632ddfcf9cd76d2599b1352709a3effc4c0ffdaebe7c5c06b897b9a3a5b727a5bb8a97b99f03bf1f0ba697979f288498b39f38b2959f3b9c948396adb72715bc8b8a83bffbcfeff17c4c0f3f05bb0f370739b7299f29b9b9a9b9999985a9a70b1b3b1f3b0f17172985a7059f270b1739a70b073f1b271f35bf2f1999a7170b0999a99b2b373f0989b9b71c3778464e1612fc0cef6eb4cc734a834868a037d2a6c5647d3eb46e18d0ab66a5b61fc86973691a242175de489710d3543c850185ed2e53aa6e5946d6f51c87662e72a78ee3f2334240c91a27c0a1622b30b7d17187c9aa1ad0a3acbca1b4408dfeb820292959f627e6814a19c3970c4a0926f9cb24beea85f02706331972584ac2045ce74ad89eb67896cc8bbdb9d3f7f9cb1d15dd4724fc8a8a5d5ea0a8fac25dd45a74e4b0624231e8b0b906765e7aeef1164ec3ccb5a8ac2b465c779feaf8d6ab2eca3e0bf726207af93f4336df7bfbd7b88b37f43c6432fc8fdffed190f202a0c21f3388e083099fc818c8763858efe40c6c35df80c248898732ac38773e7f47ad124ded106d356debb7155e3e00575c5f8c5e11c348afd07c9f6c405374ede9f18ee0cb4729f4ba8726c6a92a13c872a1e5667994fb1ba351b42fa4b57a82304e0490a8f0e32e69187da182487dbf0cc8fbfd041a6efa4361cf7fcf52fcd6fb046033e09d70b32291f0a9cd6a0aea5a9519608f6052ba975f267fefe71c7ef5fefe7b7d81f6f21353a544830225371307eaf4fa5e49cb909482a0fb0569cb3233af5c9e903ec8ae4a0f47d469f9c02c348ad065857205230bd8cfa6f96dc6763e7d69d3dc8d7068cf2031fe53f7f0a13da53e236fefd53e291104860c340f850f9dd189f9da410849be19010a8cab2d26fd6670529e45f7f5fdb39073e03eeb73a2e657175510574bd9f3c3752dd257dd4509989a99cfd6cd012cd18079f5b70fbbac9dcabafd0de1a281f0a6b344ea02e22dde73a91357ae4f0a3ee307d693a916128fc22d389aee9d68767390adc9985949777f54ede135b1e51885ae02191272d2dbc7f96c545f6f16975d3ea6161b298db2e94012e0f8f633aad360593715e987e2912dae0cb329b6279e59a58fb3fa6d37fb5e9f413eb49a9babbababc8d8782b4babcab35f70541093e290e4d5f0d095d76597e4f476b715f7527257d3e435f1d4bcf03f653a7171719b9a735b9a719a99f19a9b9870f3b3b3b1b1995ab05958989bf0b1f1f0f1f0b173f25ab2995bf0f1735ab09b995af09a9b9b9ab1f3739b5ab0b19bc169f6fc72d34967e4aea95d4a97c0902d1da5ee7e8053c712d4e7c34517ee5b646783af50d89e3778744771acbe74c5569df2ba78f6c78dcdf52b2a21a23889b985a815ca0f950cc1eb6da18e8491dd14b37dacf19bdf927237b80ab5596846f0588cdcc75df19005701be17cf35876d696d662e6264e6e162e9a1cc71e5814c08e0f2cc4e9b8215010c83cea71c687d8142f2c35bbdc6a2d7ff07d3c398ac015f2a943f8c6ef787941ce3651697d9a48fa2115d9d5e850e324695d9570b093b1e0f593c4f0595d6031094881864cbac6ec962672304dc7c52856ca5adf133ee4275307353c0755ce714aef49c03706c70bc7010bb35b7f3dfb8284fa7caaeae2d079a5278b7c54a71fb21e39821c8411c791a11f310bc0b49789bd898ba3ed0fdf8f636fe166626fe26acb62ea626d7ec9e297ae1ef08682825a05c74c28fc0b3aec6f0f0585f7197f762828d81881cf0150118233141492b957d18d1c989645b185f14977f3ec66cc3091775361629fe8e897efe95b037bbc50d0bffced3f5f4785821e47fe8ea79f3e6f0940dae78047ffd9593bd8ffcaad75036c2d1cdd4a5b898b4dffdf279f99c88fa08c5d77d4ae5d4d69f44daba0f313bce37b73cd5e82ba0f0a1ddbbb9db1d600db2551f9c3b54822d9a88d8a08df2207b64692803d0788bb96360804bf02820783e0212038886b83360c0407d968d0822808da4810fc2a081e05825f03c1af83e02007f6b420d959b420641bb4b120781c080e123b411b0f822780e020697fb4b74170106e6ada24103c19040709aca54d05c1d3407090d81eda0c101cc4c5420b528d92361b04cf01c173417010b2225a9042f7b4f9203888eb9ef61e087e1f042f00c1414a67d182786069410c45da1210fc0108fe100407a13ba27d048283ad3fe520f86310bc0204af04c1ab40f06a10bc0604af05c19f80e07520783d080ee208a105c917a405315268415879689b41f016101c24028eb60d047f068283b8b8693b40f04e10bc0b04ef06c14108a768418e9669fb407010323e5a906ad0b4cf41f041107c08041f06c1474070902318da51101ca46237ed3808fe12047f05824f80e0af4170102702ed14083e0d82cf80e0b320f81c08fe060407a14da17d0b822f80e08b20f81208fe0e045f06c1df83e02b2038483d595a100229da35107c1d04072168a005c9a1a105c9b1a005093da0dd01c141684d683f82e07b20f827107c1f0407291d4b0b72d04207e21ea0037122d2819cf1d2811c71d281f83fe840082ee840685be84058dbe8409ce0746820383a088e01828310a0d06181e0d820380e088e0b82e381e027407010b2533a10e7231d48b8251d11080ee220a3037190d18184e6d09d02c14122b3e8404e50e94e83e0e4203805080ee2e0a63b03825381e0d420380d084e0b82832830bab320383d08ce00823382e04c2038083d201d0b08ce0a829f03c141820be8d8417090b3713a10e6733a2e9005a8b919f126608fdc2ddabc459c70936f36404ac0d22cae1eb7440fec301fecb05ad28a500270d85b72196d2bbccc30191962c7cf74f982b0ed871e10398782acf33f7c414179e33a028371627fcf94da85c50606c35efa06d35b8557c142fe0e565c544cb89e0f4e565cb04376e001de715871894a7968d0e8bd8e089b4a5f2a0e2200e3738237b415801fba0a32726be52f5bde86f894f7937aeff757008ed77fa0b43fe438e37fd9f37ea01074c17b3375fba3940bfaf89495040fd8f834ecc296037f0bd6fc9b689affc30239791c813d1e4df3bf6e0f3b42a04e66b39a2dcdfd6281048c0f18bbfa8fd01c1fe7fe1f1078e871de6f1d4d0e0d228bbedf1373c122ab692b91ed91ecbb60e30f7b11de08439ad7c6450881f9b43287c022ae27844055962f35bc090ca65643c96c82400a1ebf61b2633b0b855e30b8c089697c160631eb09c6b157cfb7b463aced0bd35dc7f511fd2d4e386439490d054037ff6fb6b99f4f377f90e7ba92663854e6c44e78a53d155788af810307cec9084a37ef6465e1ed6d6dfda393558c92ef511b64d13d2c84f574291bec21de92d2c9b11ef4f5516b96c7c3e78512af6190c63c6fd5a8c6bb033d5577dd3f466feb594874f2be1bb9c866edfd212c08df5fc8bc560e0c46a4cb0dc6afb8f6756ab3d523696dce3d977cecee8dd0c9d9b62761169fa4c018071ba0bb88eee1840ac901375721219edcf1902a44f3266d75ff1f9eb8bfb79558521e415ef208fd504522d9eaa38ea7656b129f4c60911eae1b7d82742322c8490f2240f02d04643067237c6e04f1fbd479766c51910a5f3c22090a7a83286cc00864de81c842eec31fd6cb84754d55d913209e540888a57e4430faefe30b723f581215c8f383c58283969501638b019bab047d18d5e474a11fbc6a14315fbcd6c0aba0d75ed53dc65c248073a701f7fdf0f6ff3f7cff2fbf1a6043e1c232cf928d7f2736f84ccd1ed8001b0e3f8f4978d5b4f465f05cbc44f0faf94fff5aae421bbeac7af7efe0ed5f93b6725b5667a7a7454e2c99d7e59c54b6628f8770a63b2a2e42eb4e8055d6477aa1a86a09afa253da1feac5b24c3f76619d3bc227fe75da8a4a5d58bed4a981e7cf33b8045aea3aafa5e7831598f9276d05029faa82f7fa7919ff26b6521c9ca617f878782e703848bbca9b2b28d9383a889bfb28b23bb8f0d98a3b2adbba4879f37ad8dbbbff4432b4ff82b4154e6e733e0b5e733e4e1313131e2e1e730e0b0b6e4e36734e4b0e366e6e0e0e4b760b0e5e0e3e6e0e367e0b5e7e333e133e2e6e763e0b4e4b330b537e3e3e73f8982e7f79da4ae18eadc1ce49093f4112bce095ba770c68eeb27269623798ef1cd6c4874616de656a475490b964b7cfda661122ada6ef5da3383f22db4cb916a45d83cba270c8f757c8e288bf2af889f813781ebf5a84ffccf358c819995b9c19f9d6b5e87059e6dd0e63e186e444de9030b7cd795e87b6b3e88dc7517d430d8cfc1f8da2290bbcfc66199a4712aa0d75b8febb551f6cf2ffefaa4fd1c2cd44d1c4d5166ed567a1cac52e63e5cdc1aba426cfc3a9a92ce3eee921a9c123e1a1c6636faacdabaea365a9e0eda265ebe362a1f2f78d1f4035622abd66ec6d2c4b21a1d77fe56d9a1d42552e02721c8e390ba61a78daf3b8ace799f2dd39a0a4f7f70398526fe23bf9e0c4b80a89ce14323bd63cb2df2925d7182bec2e919dd9aa16bcb43db14fa864185048d2843767831d5be8fb170a624255fee07c874a6a2833dcf9c52e2da89884baec4525232d5975194955312d23253175594d2923f58bf2524a7fc37a04d8afb14a4eb96d0f9d879db822d085d7700a6fe89a1e20c629178379358b0f26fb6072abd6b426efa5b44a88ec775bc66bf2869854b434c07affcf6d1d722bc9ef0e78fd20ede44d1aed0b5d3a4fb1ae9492f1bd9eced6bf7c64a81bc5e383c0df68d41045ade931c53e17d7a1e72dac9464f5183ffa101345048c0d08fadb03fc1ed7fffb037ce9b3f9b747feaff96c4cdfbd919ed097253b99d86d316539e817a6f6a0aef17558e1bdb5d9b2b74a5b8088a043e639cc38673d31677111c975fb38426437be895fedb3d16557d1d156e1fde1d5bb40edb4effb99385bec5a024199389a43519a3623a4517c668d9bf956aab52fae0a68ccdcf114630d4f1cbb672176c5a7ebc1a85453cf987c49be43e5544a27edfdbfb07a431ffe710c02cd55ac0b5e86730af083e0cebf485de25c388cd81d3f1ceafb4f2e7a96767ced703e8731b871ba0bd30e4739b52f7b1d02f9e3084bb2913e0890507eaa284a43eef64015bb314bdcc1b49bdc705265d6711c4befbf717d5fd43f7707be0973bc3cd063f66f99fba142e1d1ef8e050bce1301c8afebffc99a5ad37d87a3fb6fba096fff1b76a1d73faf2655d090995174404cec3193cb6149d32325c7191026516b156f2d57bd49ccf73845c511d96616aa31c7c17f404387383e575682f319001b5d7266b56ae9003bb97b1cba9e9772a10f935298b6e11843086c17913482c067f8e96d8675b910e468cfc0dffea94244ac62e385a3501af40651d830137c7e54b08aef70fb59bfef40f494155f337395fb51554ae0923fb0cf0512500eea6707b2f55e1ab17e3f3eaa8894ba128ea6f154842cf17d39480962b00350d052016026a6f1aff6231fff997feafdffed9718140a85408875e2acf2fe5c6da38b6b7f20df05de17d0f099703163eba7102e02e51d2cc55c2e881c6dd95c4cb0269b3de702caeb36d9bbf7968fd3f72b25b29297afa2cc3c497882a5b9ae2aa67abe1af0fbc73ab026c2d952c03e62ba1061c7d1a97b65c035df4fef1ce9d2f83b08177711ec027fe3c442309766cb7c91348465d4e8cf33a53355caf4bc85b07332747bf639cec2e8d345081e9ab7a0359cbb31c0ff9498a59105e58a3a698ab64388591eaf4ae45ffe7ec22936a91b72cdbd4e3877ba387f2a3273dcf1fc9f2aa0f6b308fafe7079be064918859735037237dca93a16ae6738aa8f9f65ac83fe8b49c32f97d159109a247139bd792e3773c9efe1fe39eb2f5f6d6813c5475ee4feae8521da4c1cb2cb720c78ee0a47b13afc16d1f01b89bc90ebd7110dc7e62262552568f82740e5bb8be5af0f50d92554090c46bbef134c75f54615a1e49522cd4e389d1d28f04d21049041450631d8d0405e1a26888ac501d9119c003cbf780b498378caf380bbfbc446932d432a39b7cf3d865345801153fd53b81f7ec7d8b10bf7638f5c9dce0cc4324be84d6727d2e1105cbbc2acf9bd9b7f46e17e76cc58cc4c06212defa47a9617eccebe6f88dd55c7b71dd6793f8d6d996811d52bf8bf0d35ec3ba3bc49f49a3cfc7ee2ea3b5913da09b488fdf01d46d9e9897d51d29f3452948c7b667d8d0d8f1ba65b6ff95eb82eb13dea6dfa77cbf771c9affe91efe3cb37b286eef2d9aed869dc6b0e67c5632f1c8c99f51f9d9b4d990f4f4572310dbebbc406ca57587b041e088ab2b1b55622da3e601d930e351751eb67b4b8842ba154459864a482b246f08c9d7ac07590d6e9ac1f0a31a3946fca5ecc07f1dcd19f25dff215771658438d22d383dfd371515d5ff07b12a4f48ffefedf93ef1d710c24b1d7ab3c27c79f973ae553255798707f973b8921fdce0927bd1b70e96f47737ca9a184122cbf0bede5fb58f2dd8a3b8dd52e324e4158d213760fb80b232125157e92d0eb2957deedf3e945d741737db8d25e042d6d3ce7c5b597b43f874af69346aa44cf42f5ded9395447d24539cc02c5573c88a789ff91efff3df90efff80607dd3759be5f7f8318cfa0e455d8157fa75fb93f91addc1a54725d6db1d0721e10229120f4db580fc2185bbcaaf349ffd5da82959837fbb494e8d2aa007a9f0b44ce837f0453f7234fc0bd558a6dc88a2c8ad9e6bd9fa5bf995c25ed1874fd3844dbae467450ce23ae4a2fdff947befff7e4bbe3ca098f790eeb8276fe092981079af40b58ad73bf52be5b60e6f42967765129153335200ba942f9fba9f4afbd1cacfd5d391e0724fb3fe568ebbdef9c7deed227952abb4ac199399348567465ec05c81d6dfca0baaa67223f4b7fdb72299e6a552d8055d67125725017adaa344f8efd23dfff7bf2ad5b975da0de482845d55f78ed408683333251e6bb0e587ad44c2f754eb86a5a8b45917d5055b966047524301279fc8156d0512c77b205fb440f22f1784108b2c6630d03743c63ae1aa99b9dafcfc6580b39901a2a8cdff051afd863f0ea6945535df959fa9bf80d0d0f753a9510f45e0df6c54f1b6ec8fac385ffc8f73ffe1378f5b76d949d5b61c183ac0bf62e46b21f22d4f05f463218f8fb3b56341c4249ee313b6acd353b2e6ba03c2a63ba58e51e4dc7fdbecd29de627d40b7bce6a22ba204c2043cfabb01b68bd01f188c483f1f8cdf285785d0ac3b0b3d32bdccd28e1d84081134081401be490176cc061a593d8b38ab0b080a013bd7387eee2d1484ece9e8f073a8e498fb774142b317059c2e13bf70c2d0546a9380d5de83b760fad1e1f751fcdf8fe44263327f6c9308764c21feed776a3e860b497d6280f20af35dab89c9a79b23ed10ac2c0874e317d0091ca1706445e5fdbe8b92c7089ba4630022011372b7922524db7dc66718692355c4e80be549508f9ed43b2a81c1b0d69321e8437d245f471467d254776bbc65c80fac66d976a5e10fce7f99ea73547bc80810e3dfdb6c410d41efcf53f8bacd2e1b1a655dff8f837d8f1c8992838414f456fb8e5e987057561b7611990383294687ab1069620798000445b57616fdf8740fca1c72f9f2b78b0d6658b4ceb41f077f16478a8d2a2f5b1ae6bf851d5ced58f31283046ca104abdd4709df62af0a7867dfe213607dddcd5662ea152ea5e9197a0a3f58c0d2de8ffbf7b086df527aab533d74fb397554b1b326193e169c61b1b4f713fc1d4307133a18312708012cd99104acb0920e3e80333262d5d69654bbd59d869fe3b9d125599793170104946797a6d8fa12865d84d92f12304f6cd9aa8d9321813dff97780ab7cf8d8513ef5334acf3a7a8accbcf2d6fb0a3c2a9d7c0d60d6338d79506383783c7922fb0f13bbefcab828daf18fa4cfbac26a4f365b5d05882098a2f55194d4f3793353dfa9a73f99dea138aef0d34ee384315f1cdb8b655dc1bb6bd79f2b0a00232c51b8ca86a881b9d5998cfe1dd88ef627605069396ca5761364b52ec26c2399c80ccbfb0c3106ed4752d68c33893f95a4b94aebd9619806f4c3a1de1c9ddd3636d5d0f8da547dfd89d78c6dd0be0dbbef8284af9959c264e476b3103fed093e0c64f4c807a01e6f578739967f549f6f50c432818cd03e844fa003c4c8f3037d6395db836ef4b7fb2ff602ade3fe5df0210a72d7f9a621f8588b12eaef1a62e43412d840c1740a0bce11b678e0972fd600f1daffe36e827e2e48a00404676c430f7540b73ba4728ce92664aabe6e50c765c009f251b798cb0996fe0052c89a8f7e949fea1c92b48001ea8ae756d59aba1697a8bc45de299e4a8d291339701db403b76ebda8961f3d1a47c0e42fca0d31a06b1f4003e587c3e59894a396a6acd4325eee8fd4e7c9bd715009a019eb153774e042a0d5bc9c56b182e12321668580212c3b683d03c77b319321b91a20344929985dafb9e02b6106bc2daa28bf2e12f7dabf2fc0bc93b72392a2e03b247b090596ad358e92e3221bf5a55972ef554ace702448ba5ed519d4942707e11e41f41ef34bce3b185780878ff7c66656bdc05a1fcb4a526c8d64f5fb47aac9d07d0fb88e4eda5e38ea610baf1100445a2d525d027500354f2e98f01fc56ad7b4e725166a8765b3586fa45b700f516821b772a0d045634255090f71b1fdbb3581e305b7e893f20dfd13b2030bbc4f9b48ac236ce84c1441c17908891e84ccfc27fdeae74e19a03457bb4afddb5973c00be3e0f0722431f7fc1648673b28e2fc2b38728467401b5ae651b258ccc09b55402154fc82a206ce60f750c03964382ec77c9cda273d9b43711cdd74d269f869d5f05f041b9a1995db4487ce72ff94c82127b0071ee230545c097f8d502d3c5a4751ba1f5f3fc2703d19b4ea811dd00e44ac53cc25be46945507e8bf21423274ea58e560609a08eb5de30ed3f1f9555280f9a79fce196ffc7f6da32403d0c1da2d37c0f5dade357021c8c24b6587569b488007c713145a41fa4e3584b5ae54be2e9b8dac852bc501cbfc4cf6ce22c1ae29fb294f7116b7b3bf5ec5494782d203662e70e69ed8d871630b24c3f8153035485531d75807a0ff61de91b8e043e2981ee9c550ae549b93b6e6b00fd42674475f0b4b4dab09e966eb88dde61d9b12109c0c717d45d25ffe4ca84749c2e3d3d7e52a0db84161a20742a65e4b9c88aa6a37250c1c700bf75bf76fe612f40357fccf9c5f7f1bcb7f651e9c3986590db2e326ed402f80243723c6aa3cffa52d8468b75b7fbd49dda34a2f1fa933e741d99643352b4ba3620c6783ab9198315f0fb3d04dada830196bd062897e7cc32d2ced0dfb205845f6387c74cde9f09267d83fae6a4a2a4a387df0b2d001fa87afa01ef9034a2f2bdc8ea5ab7281b0cbac7170166e2c20899b5206eccb8b6e9d456e2158ade320a3e009fbdb8d6981351dcadb7d3c518f3317931a8a2b72e02c69f8642b0517ab202392c1f1d1fdffd6d8bded90c80fcdd692a2710ba93fbf122ba66bc353762b0a6cd24e0fedb85ad42ec6c97d96e5273953e5ac9685ecd325403f87eea6b7706bc05c7ac0b6111c825e40fe8202800be403691950b08347e57a04e77fd1b67127bc3126e01f8bcf593d133ba036f63b1dcc0d29539d91c63315703885df237e0589f51b9e8d76352b718e5d5b92a979ba3f5251ed0d843868f7c594475c5aa21da4d9b3616bf1ac0077f7d41d5f34416aef6872e6cc51765cfbda4fbcb0071072f10ea9ae8b003903b3f604adae91d7a9d7d3b04f049bf654f7550ab3b1f3397d72fe4e48736a2b1f514902b4b96f6b1a5ec9adf70a989209128161a03424413e0fd61747e5adfaf36768a6d6df4194e391930ddaf03a897229475ff63de8200d2363f15fec1b86b8bf7d80680ef7183518a1433247db0a9cdfbc4698ca27af5712b40328ce0a44af60b4ea3d2d7d5a1bc3883822ef5983d80a24a0dabe812458259a333f3a5842895abea3ad95a8a80f9438ccf330f21d8f46758bd7d8b6b6c25035948f04bbc5b3566bff9c69dfc3a8e66776b9eb293c86882003e64c4bae51e45f72c5e55a5f218fd48c26a43515f40bd12c793bd6ac5383ed19ba18dc9091208cec2769d003ef9d9f3294a68b7eb2d85effb12ddea4fbedf7eaf07307f9f30a9472bad6c1461b84b10e7f66109becc4703d4dbe855752b1ec78eea53edba442b5b12d52578e91c603bff104f80d2d3d704ef7c9f236c6c493c2fcdde02c047b9fa4e7fed66ad07ee26a63b9618c3f3410f1b5640c061b2322e9663e408bbe61dd87c180b3dd28917e7017ccc9e5ee89c6f06a3eecc49dec0bb4c99f1ca63100ad84e4af9270f61dcbe90db49fe788fa892abdac5661fa03fce8f1b579a08f997e24d89d63b2ab1ac3cf48d06e83f2883e26991a5cde850bcac69b97b3b0898767280f79fd6cbb6fe01714979d36693cc22b38da541c00ec017bb2f2ae46241cb6f15b5c510d13a18a4afce940ba867427fb9c6f5b61f9d8af5fba819c733d928e5b3d58033c7c69dcd3a0c992e87733d4a552ae91f9c733ebe00f0a13e7d47b6fa4803f3a3f74adf1bbecd4a340b4c1d403d9a158e821339775ff7252626d83dec1fdcbb46f401c017c81131777af5118b6bab65c4c5c7d3ae9b469d8900fd584a5a44a8e439a1d3b446a0aa67fe92fe92031a20ed343766a971cd9fa598bf0e4d380a55dedf8dcf1fc0a2e520d25f47a03d4a0b9d13cd79f66ec030a1bf06a09f1fe6e3e1f8d96b69398893afed68872dbf3a5036fc12b7d1d8be696cbff0dad3ec15dad3c8d455e99b1d00bee4b35713ced97e88cc277f49b18b8bb2aeec867f16c0979cbe51b9d1bfaf40c490bf8fc5898af3082da60110ad2f4bcc46d62ff331cee2428cb9c779e7acbe5a5be0f821c987765a132b8e480c51b326378442f0ae00d67f7afe0b8e6baec86542344d383ed1b364e3bb1e006ba27c4673cde50e5f852471df29c1b7f66406a229807a49928ac6110fb668771aa171a6824212103cee49009ff9593d6e8ec456bed30b11191f29c4b8170f6b52007cba6b0f1cd7fd56f3a6a50749b907a36fa253ec3f04ec7f9db2870b20776a58d13fa15fa85ab848da7685dff54b3c6f8ce5dab81384a3f23653f192e6ab9818f1513dd84edfe7f0647a2acdca66fb7abb36381daba0d9f55c9cbcbc1c6c5c3f6c41f37b9f1414ad717029797d89cdd2ea55acc384a0a6835f2576fdb4711d72f7b2d3670eb9df535a2b25f1106f0116994e1e89ae2bf1a5ecce67e9a52e08bf93754d93cf01f5deff92266ffcd16408c5a55b60dcf0df6372540b596d200ca33704a81272fb10edfca7623331d8390681f2981fe3eff08a8076e937a63c846bcd3689777f0e53deef6ded64c5065609fbb95e9a44ce78a7a12df626aef022835aee55564d81fc8b3788ac8e6e1e5ea6bc425bf8ce66e060ca43d82793778d04a919040753deefe30b72fffff34c7935e5716f736cf8a0258c5508c2a7f4b49d7cbb958fa131fee34c79f0f6ff3f7cff2fbf1a60cfc33096f262f5437ecb1a0fc1247f621fd8f0dbca441e1218cc2bce5cd94c29dde70ea20dc57f74653ab377cad86c8bece8da870858a2c7718dde7838d416a59feecedf31af399841c44f4e7ffa53c38e7c60f0192dfaaf0f2e50b6ee8577a8a55c549c794e6267e32accda9f72a4239c3465a3e25f8d91f1d07edd98f992a1e575d225b9e79bf31f64d5575be9d87c8f3ca98a967376fbecd5bcf7ea1778354971720c9b7db14fa6969945a0a59519e855754903bc22ba0d5d678b84736d2469944894ee6b208bb503aa98b65ce30fbea22e5adf4faaa070ca35e58585ec9cc19778f69a81dd5dccc16b7521d5ed036557ba4d140a2e7f899fb85bac41c1b49672f3d320d712a2ce0bd5ea3080d7ecf62af749c43aaa83e6e89ed6fb31665b2b110300ab8143663fc07744cac7206b78304cb67d627dae17b0fa3f6f3d3dfb9e5987df352df0d9867ff528bb290b60578a23b941fd9c2fd1bb2c776fbdddd20b5fac17016075258c66b2216727181224aaa2abcedc5e245e7407301210ab8cd2be271ea430d69cc15da6c7ac56c44400f4df366c905cac69aace5ca1556998564aa0e40127a0ca8327b6e1e66b2c55f7048c2175d3dbeaf37814a6802a4af5657a5ba2126746bd9e4fb84761218f8dc70c0122a6ce1af23c9328737a26e145c516cc9ad99edc8302a842f5a19cd856aa73d269a9fde6f3d5d42714d23bc300abd8efeded673d1932ec618fce7d4a7b565cea489f09f07a85e5ccf10dbabaf986328fbf3a351f50dc48870ca8b293d3d337791f3d2472422563fae3cdb91b9cf5774dbec4e77b3efa9bb25d440bb85a9b7e4a49a0af50f11da08a288a979693c59b4b36cc19180938b332eeeeafae01565c98bb9f1e82e93d19f7ea7ef5ab6dccccd851b500afa409f5801313270ad634521b5605c2bbf25732b58015c1f28278648142bd46776bc7595dadf7a76463ad0155fc96afbb0b3e6f1f6aaa8aaba017d025628c7857012803cec12eecc4a63dc76355b4ee45451f3c50454b0a48da34568f270f7e723aac8ff72a1aaf147f07719c0d200d0faf659550c9b1d597f3a6523e05fdf38246b32480d727c3332abfef320b86b262a5c6631363c67bfe4280f62fde62339d8644751ac5d5dff2ea848eb7e77500aa1087ca0c949419b9ac755b4b5f72345694698e260064b46431f9a6ed6d428515d6364b6676121262c22f000e8e2fbff7b81031bf99a74127431adf7c5336b12e1690635eef9f1bdae737aaaf693c1b7345e231120ddd2d40e082b0b7f29b822734aa97f668ee9f5aec1121b53d04ac5ebe7507f98af88ebab7736077e37d167c37ef230174e7cb1b49c1af50deb2e36e5eefcbd7bbefc79ed80ed8112232a45ec8dc7885847eb9a11ab68fbd752fdb1bb01527b971e939997f854189ec9d0ddcb94b8d0a6e9800fdd8e34885bb6213dad3dbc8b92b7d0d552184861b509d22b2f0d49c71a06f7447a3456e76f373adedc63380f959affc82b6ffc136d26a2361fac5b3452ea1afca015e654a44736c06eee711d3a9ecb7b3c613c5dc489f02acdab7eeaf0baec7c40534c8a989e4e5590a331bd49901f49f5169c5e5ae3637b3a0a867fcf8e7ba144ab73c0056a1563d26a25c68eca5acd1a80559140b5dff5300abfcc2494d53fa9e139bb31163625e3915347beecd802a60d4e6b42e8d5151f7b76f1c7a993d314f5430d30044e24d68cc8f5d7c736eed313b6aca992b320b982d56805397abf8c40658dc2233c99a63ce726db95a496b9200b693a2c345e7fc08b7bb0b2d815b254be1171c6cce03aa4464362c31d1f3a0d73d9c61cbb99a152b104e610da8227a9daf58707e7f419413bfb72d79f010d6b4e10460a4515a2278b66fd10cf39337274a273e73e1d1d9606015b041c6a22da66c671469268c8194079a170a0401d4670e2c62ae1d0979af3f0589dfb8c7fb565f24370970eaf3d27741ce6ef8c9d20c642d33592e3b1b330c1f30ffed045ff59879dc9e7fa4a82e2f20951ca94eea0d38e4c5fb582521508a74d1f015edf30db5eee9c6b3d800e3994de1c2984ff4ab80616d190d370db599b3c938802a50f1fdb82bd8018c4a785a4382260bc41de3f93480f551f931d5e2a0ed43ce7464538788b15865c3eb42805345c2dd98fef7182e572464339ee862597a60669a00debf8e38c78e86697bb683683f3d835858ccb9465380d729f73c54940c72df9a60a3168542bbe0c5f5d7d1809d9ca3467d0532514c33daeb37aa136536cca4f9bb80437a0cbd9707b56c6df3114d446e77b2763994cada0169cebc3e2ec50dbd94d5960c35ef8b9b4f86a979d801aa74799169f5774b7d94ceb23ca7b27237322f804905b0feb6ac87311a89166584ecda76e321859de7b58804789de553ee97385e563f0549d2b440f12e21d37d751fa05f06f89cf8d4366dabec9e8b8ff4656ebe09b7d80578cd5591e51a55ebc2d8a335516b3f5e6e5dc72c2001786ddf5e57468f6e62f472b84f78b63ed5275716f123c0a255bd74a8df9af248a9175143de4aba2eed767b24607cb35f157b19de0bab51c50f19142b43531991e400e8773e24d367922337d911c2735ad22bfddb67733a0111ffdd5cc5ddae48220635b8ea15cbf5cf7d1f0e0d01f4c3cd76b90dbed6b0e1aea2ad295b151c03621119c0f8a51a84444c751ffa457ae36f4a4dc35003e91b00eba7e421bb414da7c22bf435feb21e016a0726c3658057592bc6428513c3d89c41aaadca6b4f65fae1db69e0fea4d353c673714ef3d1538c893dea2cd9dab2db80402112eff46de667017923faa6022f445322973c12005eaf43ed8777cd3a556ed6a46b175cbbbb9dc8728112b03e52b3a30d185def1a3dcf2b44492881f4f06d2014e0750b2bbafed68cbcc973ab6a5834bc77a1e08d5f2fa08aa0ca36d59a9d61449ea0fa13d628e27cfcd69b270155b49027ae0be03c1f94164be6bb108fb77465c8281350e5ac1f762ea2c46eff441101f138a2622d890ca21b60fcfb93abf374972f5f94e1c2e5ab47612252d0c0009caae66e2e6d13276ae16651e78d1dbc57a4592e13b2073cdf64ed4b8865cb0ace06bb2f2591a3074dae01a0ff32dcc885763e94632f3fb5dc41e22d7840a1bf0be8bf0882c5535537ae1ccf40c2d00d510988fdf34040ffd456c5e9797b503a8caad2d7db4e0d234ca82f00aa70e61fbce02afe784d43c39b863493a22850bb2319a01fe7eb7de3c66c712db81a03630b084abd1e8f3002f66fa4065a250fb27ad484eab2e8b8e8c4cd2774f0005556ad48fa7ce5e81f5644a4ce9c7f26a390941116bbf2d99cc922fa05e68cf7b4f9f4b6e8d4e3c71a9c4ce4ed3a0957120af400d3cd9b5002b15efdc46b1c433d7c7734ea870f0a004110264a88ac14aeabe57157a1c9f2a7377bd4df34039cb85702a6cc7ced386d292c1ff1525e8e9e410caa001ca2949e665cbcc565d990b6bf4fedb03315addd08012c8781843bd5a813f3312cec257e853e42dd83411a007166c5a7b8fd749b35d83b395452a5dc30ceb6d3075094b9cfecf190495e028207921f5305a6d80e35bf3de09050360082d517366275d366dcd1484ab1242b6d1650b4f1307722240c21b92f665b303474888a9a22e534e0904b33357542be63dbc5bd4ec68d2353aa24afd80660eecc4508ea134a3c285f57fff492297094c47eff02a0e8ff5cb425beda90ebc0cbf958352744fd92ecebe700cfd793309d237c47ead1c3b00cfb07294c95e254c600713ec99c172487add2701ac74e1372c297fe79b82bc0b03f784ae0116d51d5cabddfd47499fd1eb1d1780fa028f1c3f148f35647f9f843071d6493597382d5ed9380a2af06831907a59d5406d90f6fcaf8b5c9745e7ffd1260ce06637479de4c241ebbc4aec8adc91ef366eb992960390f9c3b1573fde9da24023b2d2e62c66899c9401a200ef852170a1971127ebbe5130a4b46b3b9cac9c53640acb41c417b8b012b71b697c67e9e870b5372b13fb2e497387735571e16c628dfdefaade867b3e235cab4c380e5d6f0a9d49d89806a42649ba9107df691fbb16db580e52c39c860fcc91c71fe0a6bfeb28dbfaad0662606a0a87cdf62282efd24e7ac45639635521841f6f5a74d515fe2b8443b8583cf782ef4f2f13b947bcb8551851a02b603c17197952510649cb3472cb1b622b2a2c488c56500cef67b3b8606adf37552a3d263d34a67e9f6d1d400f2a7e2a0c6f201d2c77a5df076fa499726f1d9a0748003b2113914d7718c11aa325d5c362554c59b57e90a08828a510875d9fc60eb31acddbde9cf07893780e600ccfd0bdb3771715b35c5ac1c966bd1b79c96dd273701cb516c25c3fc0da5ecc29a35142bed8c2d7e7c0a0640d142a68da9965ba6ab0cfd947de48ebcd092076172d15fe28aa2fda7e4ad6b7c2aac7ddf9faf4118fad4ec0090ffd94e89849a170ab91033a1deb452c3e26def1540908e16461b64d26d4581286ef396125b6570ea2934c07247d4bf7cf27d4d8ea09a183e825e1f647fa1c30a1084905c1b9089e1fc519d82127f241f76417dc9af11104466dfbb154a25ea8bd916d2d494392c721d65fa3460bb787f3fe715c78db2cc4c93d99e1e8eba8bfa24c0a2d185518e01af58fc2b37b758a4531fc6f6bcf209f704e8cf2c584176792cb50993fcfac6e3334c8b8e6f0145cb778408437670c21c66d63c64ed577dae7575d602e6a719f52b2b4b8dd63708840f05e6b7d6b125f1212e80ed5402515dc7a2af6645c67d62cbb8a6598b8318c076bf2de9956911f4a6928b1991b52d226724a77c8218008f36806451e6dca4907bd699cb6e4a79cfbdb108b05cbff27a7b7031230c6d74fb740ad2fd69f401098063f1629afa52ade61bf7fe1b22ecf8dea11466c4290077d380754bbcd57808bbb1eeb60fd2846640885a3540ff27ad669d789afc2e56cd4f983bd2524dfca5f618607dcbc1ef95cb10a6750a38c35bcabcb923beb97f011045ee56c472be9e96aee1c163b53499aa76d5bb0bf7018cae86d5e20d79cd6f3025e5e9544453c29c6c2bc601dbc9b45abbb66d71d58efb0be1acef126f0d88c7e200b65b1df14f78a48a1c0b6b4d5275164f9e4133c33601f03df3bd74fdb4b09a34884b85d85e6784cf9b6b250fa8c051bb32c76012a26bfebedfe6bd2ed1e9e7f7992300f31b4a8d19c8cbc6a3ddd2d1fede7852873dff13ab00c05cc4c1675d178874e45629954861e0235a615d05acdf1f1d6ecef35fa59260dccc30d1697a00b3c47c080ca27057682ce93de152cf8bb62e191f81a818c30558bfcd9893ebce5ee0668228f1e331becd5c2653385f0058ff9c3a6ddddc467563058c4a825ab4744f1f4802b64bd6d728210671b6ad41743dcd48e78b42a7ca6180201f429e8ec91ec60e2db1aa58ee5e7aeab1b5882c40108df7f5252422d96b566e1e5731d0bb34fb22166e01f60f4cf8d3d3d4661930d95761c58361195477aa1901c41489d9b3744cbd637c0ae996c9ac57b308d16a5f038ace33477b394a4562c49dc9bbcd5770952a697eff29403ed1f062631d939a062aadf24e176e9d823d992005e86f096bdd03ec9566435753d63b95adccc1b34f5a52bec4dd434f194d204e25deb96d2032a2cdb9c0d8fc4c181044b1e7d14261945e588a8d7ef8bedf640a21351a102472f9797c86f96c99f9c35cecce94cb051a1d5daf01faabecc4ac36aa7a8845baf85dcea21216e224b75d80bbeed35b1ed8e5d1c33774581896485d68b74b23e6014196bc75e727f47b0d99bb29d17d45763ccaddae15da0082842e9517531ebacfe572b95c4c1b88e7a4652900e8df6dc4cbbd8808220bb1fe7c6df76d7b0abd22c40141140e83ba458a75d3e5c982c3e71fdf8e3dd590630770970e9ebf3a6f29e9c035526bc3ecf47ebfd04b420fb0dda514526c8f08a39a4a4bc4a670bf71d8d878f500102438c622726e0a6f9892e0125e08269e2a5d4ad7274090a3b773b46e4acac158b56be66e69228d946b2390b1f8cec7247e65aa815b25f2034f0e1cae3dd97b12014830d457f47411251d72b5713be71fd3b24745842a097097751f2e07ce259de396e8d1542bf5429966ebb50604c934ede4f991bf0fa273298e517fbe30c9e14d219ffa253e7dcf724e6d27ca8e4b2e9dc9ab98d7ef6acb43803ba8f68a088e99e84c6d80c298b647e9b92a85069dab5fe2621ef47ccc736b14cd2134ac9d0739196f3975016ce25da60dfa55c142f33e5d8e6f6cf3293b63de5d04c4ac339f76ec4633da26920ad394e2787cc32b5b650ca01f2c6711526294852b9948d84e9cad7f872123590130d7ed8a6c4eb2f276a8f7499cbfeace6ac6b6936b0f38aef0efc859617889d4acf73e3ba82ecc58a5cd4e071064f431d2aed147203e19569cd7862fff8212612c6e14b60bd50e3f416e27a3db0009462deb0c669554fd7b6b21a869282b2be81cb712c2df5c0b216b27f403b9a93ac4f9349f5d028174c599cb42dfafb5f1fb7036c05602ff3da4b92c41f9d1e08795bb3095ffbbe51b94e948b795ed899fc2989793b5f8462a95a7589306e28e1361706c824324bb28b03e83bca46371281fbf7fb6471ae096762c783f1c61f2035749449fc5bcdfb546ea9b675b961fe572d0456b1f5d751b4927e6e8fe9beec2dbff06d8ee0995c060940ffdc1942597ab4efc0c5a4164f8840201a4d3c82042830612f68109a29a7000cf67876c9ad7ff582f0c932155e3b1af4437510285219caae79fb4f75f7e1d3fed1d89407f9e8a22d7996330acc57e89d0ca6f40fabb69ef57dba81123dfc2d541b1f95e914291679b1a8b4659123be7b21ee287cc411fa5d4a654c65f895f6f229020786af8f169de4be25599936fa560358f63622fed973e42ace0f3f244ceeb31c6fb59b4257d6ed77bc6ea27f2330fe4544e334b7aee5a35dbfe23dfff7bf2fd4081ff21fadd9b3509dbf127596c77ca3af68abe47cd08258c9646c387af128d188d4bae6d89ecc0dc30b51b410346059a69ab1bada73e5f923b69e1bb39aad72e2e9e4f70a7295097c8b5b6703d6f7c08a45c856d5c4ab85283bb9065c9757dfbfecf92efd3d90eaec9dbb1a709f55e3bcd2bc90aab5351e5fd23dfff7bf25de91ac3d79e6d8f6f8c1f19c09c7b9a20695cd0e47b37ff0cda92830fc248279a4bfc568df4d154efeaef8abd4fad63a11dbc39bf5a8a8d30a6bd8a3ed3e71a77867cee655fc91e6547aa0f3b0aa9e1352d1bb2f833ddd93bc421843f8b96271f79c2ef5cece0b3330a113dc2114161357dd59effc8f7ff9e7cbfca5ad4bdb271af54579107df5ce43e24a6d8f8c2f76e66431a1a3a2ded04977cb3cea7eca2daf4f61691adb14f543de7d843a7d10aa7f7cd2eee9eec8e688850c4aea2394d8287afbffd3c55186d4596ed8cb0c04033564e28a6bbf662a370f8e4cf92efa2a917d2c64b36f345210c6446ac5736b5235f58ffddf26df521f0538f158f55f48d90556eb674867a74ca847fe4fbe7ca77cd2d6c27facbc997447be55ee8613c7e85902b517f7413bb38f0c8f74b5c414ec7b2a91a8a5bd7e3821e608e5c5e1a2d6f482f54915b5c24c491c478c18bb4bc449fd031d216794aee35fede03c55ce5534172cd91837302dedac6ef7f1a6d20c3c3c7716d8d6d2ae3ea5261627aba640f742627fed1dfff7bf21d7ff9a0d2713a84b0d1edaee6c113aed755d7d0987ee5fee4308c8f2cde809aadcfae5775cc9294ddd48f778922238e3596c5c157856a768151ece239cbf4e4bd7cc35352d88e013dd29f424f98307924cbd1df8754b5345bc345abd611188c13671fb25f9e29f21b7112029668c823183973a5a45eeaea14c8dd47926309df63956d80edc212c3a93c4e8aa88a7e51423244684610ac86d10fd76c83fcfe73f0b9b6205876d696d662e6264e6e162e9a1cc7173b806bcc61e714c9d42c4763e44bac539730bcded6ecdee73e0efe03aae5c72f2afaf93a923838d486227b203fb0428db34d545a9f26927e484576353ad438495a57251c6c5a1291ce896beb628f47c3581823d7e7b059cf98b6f26bd286f5abbf70db46cbc1008c0162dfeeda9b6b7fc8d4693626b7f98ba120fed41fae077c9c1239ff2fd603164e9e854ef14f0921de3a5cd34bc36dfdb05211b33d945426501c1d214b2621872e61821d59a33743e78f40b5f724e89a10e62bad4b2f321873e77764991e620c8397ec5e090ca6d2c00d1648f890dbd41b28013a5b77d6c2f5e30c38c6e93c83491fb354cee211743382dc032862caefc7639c868373f6a363d98707ef59f3cd537cc0aac87ddfb9adcf8f9a28b542ff5d5caf711f95ee1558fe1c01589f60bb08ecd060cc70bd7ffd5521d06cdc7eed3e6d9be4b9b082a9d4c53fbd12e4271737109d9c54a0892617e91876be65811f9035e3f0ba343daa4dbf5e328659c3596dbcfb762c75e971c4008cbaeb9b1c85475d3822dadf55b79d140bb6db49bd5f9ed39e508740342096764c5f64c8cbe446f106605dd9217257d93bb1d3a5c581c4974b67ed4c51c1a40375130b936f21e8583244624d26b218c288098bdb9850cfb1949734e059beb46a295d8c3e03d6bf0808640602f9da6ec11a9e95374d4088388ed6ff066e2cff90b3b9bf405cdb8d84659eb949d22d74a4d808028134412010b93ffdfd5f48e21e3d5a1ebba4eb149191ad36349a723906a5f2dfdad7d28e71180229835c61fee12f59da31b67cd9179273bed626f72b960ffee456ec8f1a34c288df94bb75c449171e34210f615a9134ed4fda5df45b8fbd7c1cf9f88cf301144530759fd9c37ec6ae59b773e422a3b5d22314e2540db01dc5409460ec13f69592ef56e2a0f0695ae85ec38e4260c8e96e8b6fd4bb2fd8c23cd4f824ba179c050d45288dfd887b22f648663a28eb0c2e6c47f573d9e310bca99c6fb4b9f416079642060b634abfea943fc7ee87bcc57c64ad37c1e40e48036c30ec9de776ae28e48bf2fc810db0a170a80d03c3b5075f7d2ca992700165f24f1f0f87c3a86d50c48ab78235bc9f049bd19f0b81aa2c3ff9cc6a4034631c7cf656f2d7dd7daa39ebbcc41679761b6db84cda21d019494afabb2e2de6bed7454e4507b40db05de4cec0609cdce160eabaf7558834040ef38af0bd26b0f2668870ee881010ff51bd7f97ea05ec7b539e4960d07a96675bd508e9c3e054bdcc4582976aefded42fc77d065065ffebaaf75b8f8d4b1978b41afdd946f87164fb9b1e2458e24e4afcb77aba1392096a8ab10831b467a6e891238af474f36dad531c98899fc42a2b2a10a0d6fce441a8c20e1a7b01f605c49d94f86f4dc99d904cc3dbb169694cf875fb93b84fd9aec5487e2238e9cd5fe078022582eba66691625866efa803651662a32ee35c207bdf8dabee5816d4c28e36c9a8d8ed4963e34c2aa199e2fdaa0ca7041a306cb2095168b3f323d3066acec696ade83de97a21ba43edd49479405e652053fba8eb717f78ce3c8e01a7be7be4820b3b97b75a246c336842c07f3bfe5b323095c9831a9f2598957306f9d2337a5996aaeb1d22941e5dbcdcf728733a940df8986a87e30922b288cdc9870606921f2d53dfbe757ee2a0d1b0612725fe5baf702724b3604ccde332f662ec25e30fa14bb3c40d133aad05f81786e7258dc6ececf74e4d64d274e02b4d36f6ce7de4acd32dc3aa84ea5192a0594d6cf75f5f9cc2da207cb893b917ab45b35fac38f349daa0a5616e30ed19bb453c6dd3b5db94056fb8abee7ad165720bb4d94522a6645276e0c79fc14b25aa4de70ca86f3c9cdf9bc9bfe674e995ff8e4efcb72adcee8883bd42844cb6e4d3eff7a36665f90d2407a9acdbb4b64cd91ccb4f77cd2a6ae731e7ef66290fc77f6b0e37eca23f0f3f95c7fdbcaf01124cda2e5385deccdb4aa00b6641c2e359018fd943a7511fc18a3c4e1ff8533f74a45cefe2499fa961b42022509c1551066488bdffc6f5fda6cfd5a78c3e039b96b7e09b7468345e49bbcbc7e9e3b1a2d24eb97bf06eed00078c410a5f8fb7cdb664fd627973bd82200ba33708b10eea33f82c79547a310d8b077fe7337ef59622739fc2ab1ce1dc8383cc154410ba6ba44938fb277a1cfc58f3e42f8cffbf5676f8fa07c6ad04aa8b0017580182e3970f073b8040a0840f3f820efdaf8d3f04bedf3f767cf3090d076b574f13274d4eb09ffe2bf3f7aa10f5cff436cec589fba0bf5b6099597e4f413249e8871446680edf2117e8f8307d53e6a01b3fab523df4179f40a1c3a95f32e19cbf50f8e6e76d00934b11b4114b3b0cdf1fe9927b8966acc15b9a679fe6407e1f03cee79f84f3f9e11d5fd871f4dff1f6499fafb08490ee2fff8ffb17d700a9957ba891788d3753e87b4b7eb6d0c27611bdc211f0ae5dd77436ff8dfb93fc945b15a279ae42ea3c7cd31d6a7c9c6ed8db9eb93bc6697be669f58a9ddc1a6ee4b50f6c80b338e31531894999f7ea0e38f591b5ad7a311f3e3d0939aa3d4b3bbe1138d5a158d3d6016f89f91d6b418ac34695e847049472a9111e08bea5abbb38179d976e2c1aa9ab919dba1df53ea7670496d94995469859a673b02d39d4efa6d0e078fe4c3de94fb4a5e155073f70b8048507061f7f18d874a55ccd6dc28ff0d0ae4f700f1b7a194b6d951077ef4a06b93d4778ec726e21cc4007beed24d8f811af7ff9bf3b775d1d49d6833f74b1dc0cf5b8296298ff1a9a058f7cfd05f914b33db4ae5d8ae044b0e417b2cdc21d6799594d2dc543a08b5970efbbc43c2f6ca5d14cb349c461dced35b137e05311b085d0a781659dc04aab32142f23d5131607673c8c189794642de9ed148297843a6e8b5ece0310b94eab86f9b914122c32b79c70d34224fcc6fe0d1c459916499c32db3b4e0f3837c76d46d65287f4ab2e1ede151eaa25033d1cdc51fecc374cb91b5c29896698ffe84bf85b195e872057036ca7221ce1ba7f6e6b20e4b796cf2ef955e6eae99a3b1c47991d8f57f6af4de657d8632528ab7b8efea4362fbc2f1af08f3221369c5c1961dd172790bc8cad16d0f099a6e4d6c42fa029f1b25f63e75c43f66033d72aecd3e5be733f601b90e67ada1f9fe8311f21c282c1a2e9f662bc35656955ee973845c20e64e59217ef9ce622c3d85c0069610233c0c97bf9ff63efbbc3a2bab6bee945418a14a528a27490198661661090de7bef32c00cbd7744a483481541aa4a534429d20414a9224d8a208802820a5204a483a07c4f6ef2be977d93383124efcd776fcef3e48fb8661fced967edbd57f9addfea1fcc0d1a733ec962909221a55c64132fb50a80f1de9b8d1e26d0638fee385f62fd74c920e680d735a04c36871f662777898626b3e1b2f98d0b7768f34e930281515305b7ac69bf236c2d1e1267755fe15355065501be1cefa733d9ed1e523a4fd234dbc66019eacf33c581f773ae13602c4e4ba56cdc0c397e9d36c8eac39d20804681eabd6f7b6b032ae30651f31ddf03bb2e8833f24099b4b0db1b7e8793f444f2a912dc51253ed9bd8b6d40ad653dfa32e57b3da961a2ed85c19c0327089e04a2812ab06b164eac7ea75bc56f15d9a9f94f7505c526dd038046b6e4f662bc3bdee8c2d8953bc7389e16197c8a00b8c0dbc725211ccdf49cc1615faf3e9b13392dc1dd039491be492348505255d4b27f2cc0ce8366789db0680fb0ae2d1f7586b8f28a971a52dd6e0bbdfd7071eaadd2c7bd729daec42b17cfaa89a47d41bb131b565f5889b3040a118664c2ee559496f5bfb4cdd3c3d71d9b2e807a01ac655af76658e3189997b752da1e89c85c72a929230302cb09bcb5675df3544eb4535f94b079f1126133721178fe15f980da979dc969e4e783ab447b76728e596b0165dc2539b9066216d985b2b7b40fe562dccabc733975013b2adbaa479e9eeb9c6562e10e47ac3af501ae5e20e6b46a9c572a68924623ee77a561970afb2c5e102c935e4b465cf97424a08617be933a91c66b2ae87014807aad9a7367eb8b759736b42e691bdb17179388b36bef95e79e83bf1afab8f5982fbcdcb86d516a74fac01c4043d1679361d39f7127b5828a7c3858c8ec1ae1cb2d0009fc2622981206d1bf4f7b6b4a267673f6e47511b0d747fecad9d85db22103e657c53a3e45e6fedc4b9ac0e1d13330cb2994b1f4a430472f4286a4d210550f0358b5629b4c8b9df377b955855c9e4737b56b716e1101343835cd9d911393360142f3ec77a664536e1d94ca05f2af3a7a71a332c645ca4c02d4218acafa61ad53c7015bbdc2327567743aa3515e3588d8f4f0dda6785125c0173daca37a526b8d867c2ef3d38167359e13bd2de340acc9cb3177ec2de1337746d7d57e830166f32baf7a00e3e4c4cce6517a366dcbb082a8e57b47370473d69a001a0a36e2dc4275a253b7590d84baef4aaa289d5e1007f673a557c3a88f977af0126594be0e492cc65f24e0017c1d02d1d5236b5cb970c7260623246ce088428d23800c4af0146e4d838495b67ff5e69ca13b12b8e9c80df482a1f6a0cbd432afd3a7c7aadc1f50bb6a37886e0768441607343f185c4ac722f0d46b85b50e6ea0889700563eea99730be873ba2107d2bc9c6c3c6e13eb1fac0758c156daaef72c3fde8896b3a1e21b0da98c44954a02344f899f45e35f5f10bf7f1f517ecaa6f785c8d0f9298086a6ca146998cd9cbb5e1be2d818611d66315d930e0079751419cf6490b689163ab6061313dddaa1bac80cb42e1c8c3e6bd0d7609f9478c2fd945e9e05b9cf394580a7fd764cce94ac4cb7353fbddf0764b2f1a738af2c8026e595f995a02ff62a97d3bcefcabb36c0f9ae2d5c3abe572efe02dff9221dbfea1855219d8ca3276bf0682eb0bf78db7440c4b226d45f2fbc673c7ffe45e8b18c47003e83fad0b3cd85b8c20bac731cea02f0f4c31b5de14041a8f7a8e059fa038a7227611eb5e71eef0c2c35eb0371fbce81ca068a722976a163efa63f4627280d79dd05c63b3cbe95ba76e3784317b1a3b6c3e382459a3427e07c3cd7032be95b0b4b7ae4783f98a5e0b4288cfe2a00d6657ec271f66ed1b14a229992633a5ef98d52d784011a9ee456f9d8a742e64fd96a48f4f0c96f72130c1700fba7ec5b890df8bd2ae23b82133aab5bbdeb39e9eec0f3bdde7654117e4ba31f5b41ff4efd4c992a6196094043a39cf695a96dfb6be0cda05ddade21e7e4b1a04480a645b7f0cec4f635cfd0d48162d7fe4f47e0fde3ce80f375843f828afee154ff03f3814b65baa570cd0273607f34dbb91a26dc9a74d371366e97a6f662725b5e37b07fc90fdb8710b0a86c553b6fc7dc9de0d56f232a064a35c79cb2ce6896d46ece9528e2cbbbde97c3c4bf050a85ca0c27d78318ebab3d8f79bba233ea24469fac03acb051ac7847359332b55e0f779c43c95dacd96a4403ac769f13164e8fea2e690b18041a3ff7fee2ac1dfb0ca0a1f0a84bdaf08a78783c5090bcd89ecc9ffb505a20e05cb257d954d6a9eb9b208204c7a76be4d64eba450220802c0c67139fbef7646178c376a9d238fde7e0ee84bdf287016476f99f63db2c76e229b33b530f091c8c077a421c0f97162a376db8fbae4af1e8c71b0f5eadfb5401e3a5630ebfdf4ee04493e6a3d6390605de527a1901fbdf2552b7e034d1f3e99ea752055f2e45ec0a73c502bdb4a037a1b40185a3c5633a4798045424235d3fad02ac81ce6f5891bc93277617c67b542ccf74730e3d6803581daf10e762a1b48e9aba66056ed6c14ea95a4339004dc8da3db3171102f2d79bae11d8d4e6433c5882aa1fed952be4c35fcde3619d5fcd5f132d8a71fdc0937b05c8e70cac4c3224196de7dc36a7faa44e79e97ce97024c0fa2778f304b25cf50c24d96bdc9fba6f43c023d103a0a1322e292c18234ec8515eb8931d6b1357396d5e06342a387372a298d0d0b033e2faab7375ca547ec7338700e76d3764ded5d479779774bc97caff75f7a88fc6198abd72ce9103bede414b1c78cf0a92acdd6889042cc480fdeb305559b7f4f3f9fc464e3d29b6c76f6b49d14de7f7ca83177389468688aafa270cfc9ebeab8b284fab01daed2e9faed03eeb830f7d6439b533a45734d25ec00ad8af573e3d24749857b477ccba54fe3200414077abf8c45e39c799fb744d855f14d34f7e4a737a277e6a60e00e108b0a5ff6e17be7ecd52d4d5bcd321815cf7fe5c86d80b5f428fb7409374389f086bed6723289b8876992f1cc5ef98b70e486ee2ce331b86cfa4c5de6848c6eae07e00fd18af3247e8aebcf6add8d28e3be715a738ba44bf64f746718152d2134b6f7f81f20f343deca534f99a59200add538ee1ad24c04ce71af44d0853bd54ba7b92d4280e38e7959b45444d31aad792551c181f539ecd6f875c09c8a1e769caff2348f5b583cbf4af26cb68ec624178080fa9f21acd08a9c2153adcd13a45e3815ebe0de06b466129d1f22ba5d9d72a429a166fe7ce39dac9dda0d60ba3de16726a56362c284440d5f2eced05273185c065a0f151ad7f8aa7a2296d632527b3e655fb38eee1d0148924d0e1eee4bdb32e92bf315ede59e2b0f951cd801586f968ff7d02e3e91237d42c5f5a139177ff39e7cb8f25e39f71dd32be9b385b2773eae0d430f3185ae966802f2d72d2104c699e3bbee3caaef579eb4bb37399625ee953f317a592bf75ce98b751d4af38e404a235d321b102bb9bb70d46175f4e2930334932a014b3d462f8a3b00bc5c07e731ca721a82cf0b23e568df2b82fcac592dc071168b697ac71a5c85be02b508cd1bab6cd9297bdc041c47743a12b3a7274c32b5e1173a14e6044f5cca0648e8bdd78b43aea5dba5cdf0210bfc15fa733e7c7d0898a310a9f75d3ab5dbc7ab049edff6b84be6b49e771d6cbd148d3f2b62526b689260c63b417df29945ea0cc04a35722e21e145fefcbbd34c3619af58168f1f4ef460048e832bf404be7e07bf4c49f8bda3a7a5db89683e0a40985f3c6a956f36bfbd3d33aa0f3f3171b8de33d69301d0bfb1436e9760e786bba8d8e1064d6aa3da284fc09ddbbaefcf697c9ed3cd2d408bab3adb7258ef461cf0fe4a7c2a214153d78e269767f0c81524336acdbc3d07cc6f05cc347dce5088e7a6fa8756be328a270f81d3102f4d827ed22578e6c688f2d96df72e756868b10dd03a95dc284d1a4f3a3a89e4b604aaedf10ad305ad1740eb4fa3a4e6a89cea1ed30fdef6475407222a37b71701d6b7405f9bcaaac0d1ac042c7fdfd161a2b8e78c4b00c9ef7d7aeb2f2b774ba5571f7a66b36adfd7a1330f1e045253fc0241678c2c774c8f8de9179f2bbd7b24d014c0428518c7553e73dd7c76e019daca6655923f9ff3440770dcade8b56cdb1245a6cd4884de62a87e0bcdd90570490fb0e96b9f37a9562a426ecb30844ad9df1d2701de7f96c81f53a072ed2ac7517b6cb9332f42402d0c384e33348f5c969194f43aef46b3a955e596193302055841e93b91f18376cee5a817e71cd8c3cef1f6befa0a90c04baf09913f7d19798c4ea5a5663df0c2e777f834c078439bd7e3fc87470f39dcbc83cce888f76f188802f697ad4777859466925f28072d2fb609d24a55886f02ac9987ab891c529fbe7472bbd1ac6e2abe3639fdea12e02e147b0c77914acd254e6bf5170ba529080dd209019dd0657374db2753a5c4569e1e2955cde94596c64f03f367bccddb2d984fb462b03122e6fe48f881eb14135065e2c1b71da57bc6278a555bf37a76f0f90763bdeb405b29c2ce5277db6745b6be311f6e605bdb7bb59b9680f5216cd0c538a150806e5f8df108eaf73996c0750f30e7d5961eb58cb6b9a3428c02deef761ffab082bc09845b6ef61f7861daeee312f1fce15a4910d5195eb517400676d7ca9ee77933276553d89a0f7d9d876b8c7b3be0ee6e1b895c3da4bad92616da9ba8b1b594a225290bb48e7c2e177f415db6af93bc6b2aba3d8fcf8189250330f7e74f5d9cbc70b92ef8ab83d6950307490a23540200d6cc9b968757bb496919b9ac2402caba44084f7ca6b2d92b97d95992ba295f2c6aa20d7566c3fbf89edcf01010aebadb5fd750fb55a1ab6ab6a61c015f7856895205529393032751f777c30b1769f0c5ae6446c97f460902c7794d44c51324c646496274bc8682622544512f1c08970470178cf6bdbefc42f92d3a3c4b676ef7f84dd09d3a15bb9cc2bbeda340929e7ebbf600f5a9115b7b607fb23432ecf20ef2896f9eb89c77c27eedc3938b6b80bbfe7e7da79c983157ecc145d779311dae778e39198039b2e23e7cd2e4b3780f6b674dd9a90a7ae24732ec00eb638ab39eea83c88731f576e8cf6865a5b54aae48a0c9c4f90007a9f86027deaec8cff70ab7ed8b1297a980f3531633306dc2cbde59c02419dad4171d951d7001787f8a6599d9f6519705210147c1ebf215a68aa98b40139b0b1bac55d5929415a7a628b008bdcf63aabdf55de0f96a7b0ba16102e19ff6996f1e3d1b755d320920d197db4a38b5764576e1ade7cad72e2e8e52f5ab4b80bbbfce76ffa1624c2c79ea09ee8bbbcde1143a078a0177f8a502a9807e0cd9d30ce4dcdd817b266f6c022e0275e9ba8434d7e59c8cc74a436eb6c6530ca8a13b2e02acc394d6c147cbfdcc631eb67945bfbbf8fe4be5070f20758cd8700eb53c6b41e3dbf07077e7d4e793cf55dd80f9db4a8e2f6f7d99821279fcac77f46c4c7291c517607f42a6aae899c569abb42d5c1e471623850a291581f07c82fc06f319959687ca6e3deb18d1fc90a46b7440b249d829318790543b6df464de7d5625c22cdf9283c0f9db02a7e43b2b90e35d43b352eb77dfef2a95b734104e59666e3b9172ca5cad39f9b60847e5167ca8ed3a90aad0c4875d4aaef37f27d9782a7f3ad60d4f0ce57076af7c63f1c2a035d961d58b1f9bd9aebb481b05f31203e19caf32987b1376f304a7866113355aaa944bb19d00497f1d8d9c171e7cf025c757752afc5d3c87f78db7017741f0aa7fed71f26ef5a2071dd1a2fdd3d35f286380f3afa1d188bb4a4482b45285e9520c57e750e6fb3a80f599fa157337da8866d1cb3246976fd3671ce34003a4ee611ed49faeb4e105dc1d620896f70daa7338c409b096ef50dbb39d5cd07cfb6eb5de3b3b93459376b11a08b71362c622f28e369fb2414d4dc2b186de395e2a803bc2267bbde61e4ad7dca4b42b2de7c5047c2dff04e08edac4b889bd981a7350218f4b475196903c0f1d00ce8fed3bbae20fbf0ca97f210e5314b1befdb6439c0408373cd82a7e132860e616a9e1efb01de4dbc7fc421b7097b3bc4dba8ce6cefa5b1f51fe58fdb590614adf0760bd44b4e96f1fa8cc4c80e17542663e0e1d25cf6f03dcd5e3299f4496c96e4daa600f151e17e02e6de866dc24d822618c9015513529fd27e9220951dfc98a97fb032510ff2c4b786d6618dae2a5f12a6a95eff655ad1b01e8fcb584e12d412447606a03cf62e4edec0bf4125b7cd5783e17a50e2b9587fd7a711ade8f8f0b68d671a8d2e7ad1e878426bc08b7c391b49fafe34d7f0726e27764710970802e70351c2466c3fb6b5f847ff2fdc9d4309e6835b487c3ef1c0fb773c57a880a0a6a39618c0d74841474ddfc3c0de0b29ac6de9abe06b6f69af28a0eae6ecaaa2a7043151f4b77776b776979615bc33f105fb2bff5812fb94fd00f2e948334c4d71a82810a0961b0426861b8254c0406472244e030345c584844188546406096300c1682b142a0d030181405c5c0919670a8080a8eb046c12068ab7dcd0f09c6db49140af9dde3ff80f7c7c2d170281a85864010084be40fef2924226489b012466085b0c2c2586b61a830048240a3305814c4126e09b5168659624460482c162582b112f9b7be3f4e1483cd202583415d7a45bedf5131c2b3be022649926a7db5ce2fc248fb5e1d63eaa8b4d49c23335731c45e862ede61aceb653e6652e724f43c37729dc582ddbd82ee33ee12e72d62b7ffedde5b436c1d7f97c46a7f1f8d000f13c7af799ce3e179bcfe949270621d957e25caba48f69b232b5b8b429cfab2e4fb5b347f2cc7ebc84bfe72df0ba72567b53d63735b994b580f647e13d0caf62e0847196081fd3ea14d385fc90ae38871b7b273b772c4fc9ef134f65e4eae18f7d3185f2b5bb4b30de6b4d39f08edfaa53297ef915f34f5567d44b785cd880a0dc856a2e8606c6620fd9eeffbafd709aa33b8f47b7c7fef4f40463a7f12c8005689163bc24544069d96ce5caa95e5e89910a7aefd23df11d7fd69a163556b9ed7d7bbed6472bcb2fb3f28baa817ef678e70af019c2a84cb30fa065234110fcfeadb86195b0d5470c1aa713208efd981878e3cd61b6f2a19b286956fe0a9c2570d7f69b27f5109d906ebea7d7f69ea71a9ec8fa89eab7d74dfd8748936fec71eff9fad97e8fdfeed71a29fa1566d39adeb0e0dab0765903c62bdb5662b46d64358443f3f857e9f776ee4717cd5281bf1b3568a84dbc2b379cd1fea8f14687d8f3dae4f5fc3cca62e602ccce35e565cb80d2912a709fb6bdbe344787fdbe37f903daeee6a672923658c547110d24328c8cbd8cbc35451eaf23067693f5b3994889e951154de1de6a7a98f44a3feb8e7df2faaf80fb03785911811181a6265692d04118259c384e02894101689c6a221c2502cc64a0886b2c25ac2d09648341a86855a8b8820e05838042a0c434161706bd45fdbdebc60431aa20fd5aee7fd38320ef7905a5219eae7328d80613ef6f511f45eaae09662a616b66e2926fccae9b8bcc1bd3dc946399dd638bc7cebbde358edd4403ae16f40cd6a0585123429871d685da8fe395503a12245f359c85865d6d854a0a17f8c81d18b5c835fbb17f760a603b13bff0ff7d409da4313f1f3bb1e62223c583938f32026f18552d586b8bf7ba7e937cb1d485efaf6366c11d145113daba45ec35b270d3d61c05d4344540341ec03978e9fc010b1d4816303c0a1a1b4cb38c6e3a85b20fcdd8465441d5c573445bd49712cd05ffdfb446d15e5b76b6d71fc950f6438ee0f9c00819c59d3cbf0af6df90cb1e44774b85732c64c70b0dce1e330ee08d8f67942e1aa1ba1dbe70aa5fbfe77fe43c7ffd52f297c7c7c3cbce1cd8ef267ffcaf1625b7bbbbca26dbf36c08fd090fabe3f011a92b7ceed6f1626abe8cac446aba03d3e3967c8d8b7576e6e517621468986fd0be5f98b46931383571eb401a93bd6a4358b0c46ee91dda7777caa1e2208140a4c814fcbc45716582adef082f0f01905cd832ea4c455e300f9daab20b90dd2adc7bae7ad5973efcd1ce57c9ecc0e94a98c6c446f0b929cac101cadc3b0dbd25d8c961b072853d9a74e531a6e9f684b21fd7a24b8e95539d5b9edab7be5de443943ec3d705a0352c42cc57bbfcbb0430d40e89f9c720c5f102a0b5d18666083dfa30fbd61930354ae579234d79437bb87f14f3d50ef721fe9bee1dc0d402b8a254e5fced757a72089e83aaced6b6fdb785d0d40aaa91a615e9fdc759658b935c350d2a59a93e62a0ca4e60e12e6b585a76c39fadc317a25a167813e248405a037f0bbb51b49b18a210477959e4c14b5fa08ddfb0c54bc25de3963c5d04cc4f744d71e950079ee446cd2032049d14753ecb3eec2145477f856cedf18a4e95a57025263a2b7b36eb1df2f1e5a303912d7c849a7105d3e063cdf005d812be11799a4648a231633371382e9cd9480fedf6f940a45eb8da71c7d7b965dbbbc42fac84d5b81d466d8553f32728f569f89e5ba3c173b9ad31af45980fc4006b3c5196775be1aa7a6ddfa7ef39487796e00523824dce5d41aa3dfb2b000c5e4829ce568791519d0d2c2586f51da7b7bf38b1f6cab2fed682e7b8c122fd084c87a235b5e9cf313dbedc26e11fbbbef8dc7636781868a8df07b3b81131a7e4aeb65f68a0fbd853e1abd0690c83e3702ea92deb656855798076cad7f212b0b4e05fefee329bec4da3b527d1f97aebce5678d90a0a62a0252c75cc58be28fc5894f250e1f1ce69fd34e5b79cc0e2049c385b5a6485da7a9c6a94f55e70ed8d26b98390395068f1951dcd792ab88db3962f33c2490fc0669ef81f85ad8daf6a268f77509a1ab934e53ac9366c6341f34f6ca77cc94c752bbf8ec9ae4cff507aacb5e76e51400a0036637c8d72f518f25d29df595d7be3b6b215dab0be8478ee6d3830cc2b75ca2da20ad0fd53ff0445c3a0df4d72fbe51bf3ac7c4842fc577ddec9db055c3973bfec0f7a567efd3a80b67b941e8caab4e517e439675e8229040711971fbe86fc48ecca8eafdb2a0491764a4f10e28f2e488aef7590c852222b5bec8786f373257df57061a7ef10cbeb897b2d052808ebffa26e76970f8eb13c140a5c41946cb345e46b553ce4d36071af196976bd2e6807e451ffc4fdc23dcf2bea838422308b3d0b6346de6011aba3ebdc0f3eaa04c7ad9975607d7bacfc75723a24a80d60bd95d23788f3f35cfd11d9278c93bbb4a4a773511402a2eecdade0a43abdbb11e1c0b3c5f2376e8c10e1b1047212f7214697ba57cbda190a9566055df8b8c551c48fdbf0b2bc23018dc48dca45fb97071e5e66393b36a4043dce392478a6cd787336975bbe5027679224f23ef03d09127bc34d2de32a487bd5594d5b409ef04f6d61e00a031687e3eeab9e1c3eec1efb28e650633e4b188a780fdcb21d0cafb74d4349ce98fe186d537c2da30fc80b9c41da054861d9bdb5d965971866ab726b9347402d0982d9fa3903ab797bd1e493d6cfd1b97d493fa2d81f5476e3c6553ea7e0eae101cffa1fb8c1932d92a11b0e79b047449f219b0ab9ce17738aa4f42391c09a040ead289dd6ff190cc9887f484ae23de8b42e435f95920bc6e671d421d9f47f150b964a65b3cdc8edde4f96140bfdce96b0a9ea5172dfad0cdc1de6618c793528400e654fef583599196b927535be79a42fac59c9f3e907bba57ce1061957199defdd9725402997607a7bf2a1331d03058f8dd978f96ef0f6d2ea19c7344ef3e108d94b001f4a3b4a341a47e9b48b9c3c5242fa3c0e8e310ef02006d0caecc34d7ca681f308d7951f37c693451a7ce0e28b4b6da3954c09859982a26a078bcb5e9583f221b0b4067bcf3b807a7772a634542e6b59b960b1a72ecb40177806be879373989bbc4cc7509625daedaf36a5cab4043c13bdc67cbd567ef65dcb0d50a81dd399c1f66f400581ff5771fb67c8a7209821d939a4d882a317959c6fd6aafbcaefe9d5eb84759c78985000a0a6a7e3c36666d20cee66ae4f4ded963e3b20bc2770493f8cc95a0ec08805426f5bceaf87cb07234a47660a1903cd225b9af0980aed072f6902b0c26ab67d7bf5c36af154d6163ee049cb7514d520af62386f9c99f037cab19c876edac2681869b77c57a6ee2bfe3b0236aeeaf7bf9b8b5863efd0650e923932411794afa725ee35dc6a3ab6ae42c7e9ca4000f9b862ae2da45397387a2f265baeadd433d67238f024867c9636bf71162da030241b7f9f9e92a8b634b3800fb492a53b2beb7dfe7ec70b82dfdc3ded1657b7eb0d265d72340250ced8b1d7f71337580f493ba72762510277dfd8239e479c7b3c6b845c2b7148745eaceb9b101eed274969573b4d5b9335e537437d0d647d7af5dae00a05d26a32b7d552305c7751403ecfb3ddbaeb95ccd062afd7cd257d2fbe11ed5b746ced90cc9a4b734ae1e07f47fd74506654fc7d42b691a424c24aeafe29510029c2f0f6f3d841c9a965e7acb4320ab8dae97c7dfe103ecc39b495eb96c813182de8785fda2735d175e3da5051a6287b72ac9e999c454f45c65e8d3c99c7ca7741c7e6daf1c663c1f56fce6d6a1c0b94609b5b1f682c48a5da0d2a0d1d795506a99c484e91a89776b4912eb49916300b443c76a95fa09c1ae7ee835b9afaf855e40cfc1fa016eeff381ed2ca6f3c7c894c9e88fe777cb7910b3dc04faef3f7f9174b1cc57b0fbfee6414bc57ebfa727ef02c851bc167e585ac565c7c42c1d6f2b2f122996d6e7a6c0facbac82b38d7d15d6fc885195d3c9fae0307ee1215049e6659be9961dc897935312d9495907531450ab042ae1de07eb17099dcf58a2f13dc841232226aea6dc0e40234d304d2f239cc79f511231cf5138e78bb2f41a028c0a3cb14614f8a77298a51e129c0e98df08f23e6406eccf310f9e791e35915b6edda11cbbf496fef2f5470f00685dc9f29b566a573dcb97bee36df04ae2db87f35f02d02434f3f1a5b71fc8bd21d14b687523ee0f5f02a38808b6f0f7f480fc3fedc469a0a4ab28ab2d65f0bdbd38ff4f3b715ad00ca129b3028dc20d6cd11271bdf4cecb73b5bf1e5df9b176fbc7d2f09fe6f4e7a5e13ffa4a031e8454344e6333eb15a24f2fe77efb8933b381e69e33c6d61adf9c20e197530df85b445e91f4aae917933ee1852d3870e3d51059371bea06e2989c5f8fb0ff742fc91167de7a5e47b45d36f1f324e3fc6bea4b0ad6c1949fcedb681a95d7ef6ce370af8900e5f9be5275fc0486488a1b3874034704176c50fa5d190afac7cdec70765c342c077e77fce8a7f9c5313e687f19105c090642c93f39be48d77bb0f61867f892ef43358a976ff4681e701b7e32fe9ef8d57ee367fb1dbfdfe7ff378fffd3af06822dfcf2a030235b41bc30a3e0e35135f87909a26a502259e11bbe9f128e0d8e6ab4e5db2eb0d9eb1a20229fc425bc22c84850e33c6cc27991db6ef0f62be942e6c76f2f683ba60d705ddd10babed25f18e9badd7d8fd780bf5bdcd3a54cb96adb6a2eb682a62a6c04d35ec0ce79e7ca4b7d2d9fb6b39ffdc85d5f26c6074c21a26ca20b877a48be322764deef7e4474c6c7ca8897efb13484e933f2d4c56e4595c461d5e3feef9f70441e2d83454d88366ea51a6d473d617d4c6d3e3d3bc57185eb93e85afb4345d6730b9c5a493697268a5f574a690510dc9d662fc08b3ba12c8f20dc3a720dfad1e1fe5ddea466c6744dcc64c2c5b82bf0aca3a7bc94c35e9fbe41a447185b3ac27fd116b9b1fdc4240b5be5a86147ea7f77030b219a62290adb697c3af5853ffee9d14c8db22ffc2fe6484f9fb99555d324305c0d7ce3d8a20333c54f63672bd950f1618f7299a2e618093f4f0b22ebf187fdb20cbebcb2fe3c704ff67d53d392a3c9e644a4f80189a349dffa24a426cfe899244e997e1a91cb1bac789b4a79e72956c4a0449c97b46bad5b37f756423bd41ec849ffc4a4fd27e5a4abf3d916efec2a5d7c84869dbd95f7c02e6b7dd924807a73ecb2591883877ba9536e92c11b3da57cbaa7e8613e6bbdd76cdf9393ceb974e6195b794489695798930a599ffce76895d6ff9f72d2bf40dc25fd7b4f1cf2574e625f23ff6f57bf391f8f2bc300b55ea4cb126930de13c5a2e63c5c21f2031a2a866c066847478ce73e73d2964ed27ebe5803297567ac8c8c97b3978aad8e83be1dc2c80ee32a2bef8e7256c0e838f823756c35317e0eff5139691124140e4142add0d630144cd81a2b821486a251d66821244c04256c8d144120852ce170ac080a8a8463852c51d61088b5084208861286a05010cb7dbd3f19c6db4914290c87fd59199dee1efddbbb074d9496a378d36fa770261806387bbe38c1ddaf7d6e685629ef25ad158f0b3f2271f955b4f79b07026d4df5ecb38eb39a542ee665e425d7efdd598afb0d39699f487cb6cc367f8dea506ae2845096d013d5d6c115d315df634ba6fbf5d43a767c8eeb6e62649a39d81c1f1a76e69bdd890ecef577fd8cf0895ba8edaf41f874c57b51de0c7e43e6510d7bec013523fbfaea9d13f76fedd49548849fdf5dfcd4d540d0f3bffe48584a5ac0ad7fb4e83213feb145575710f10faff28fff6920188ce45e62fae2a969bfb767435303c1d6c1c4a050e6db1f7ef8afe660d82ab340d5fe960aaead18178722ae950c268f0bf0aecb4bce486bb287ee9aa0a6785ebd79781bf23d725c0f906c31cd5522d2aec6db2c8a7a34f1d5d78a3aeaf5af8ecc886c72cdf982ab131a60fdf72405dfc9b5da446bd8089d0d3c708cc35f26dee31b8f46e1a6878fc6712e917c8f8e7dd7fbfddf5c16fb1cbfcf720152403fd1d0cacf13d7f3b686ad671b24e364c9aaac1095fb7c3e1cce190e6c071e89ecdbe3bd33df73dcd18cbec8843d1593f5382375b2c039bfdb607d5a779fef806b0d077d8fb9f54ba701fe0fab90e7874ff9e873633a9e4851be750ad479b20719c3bb5cffcfda920c8f57c403540bf8077e498f7f696d4c9f94c96678ce78e3a81bcdb060860e9a59a6013899b08e82bebfc11c002fdac53d11687cb679398be2ef5963dfbf867111a3e26c70f25d001c5c73f67d7b20fe31f2eb8af7f6b74608be8b27fa77e8ffaf1f423f69dcf78c0fe74f9030693fab6d5fa65bbf40c1ba7bb227e4ee3ecd59e27d5a165be7d9755cbcdcad30eca2ec96180cd68f9d9f5dcac9c5cbd953c9594f47965d941d721a0211868940a01021041205872120c8fffd8d869727f023181209850841844544e04228767e766d0c16e3ee8e7664176567e767977744db78b08b42f9d9959c3d3136ee769e7e4ace581776d1f3ec2a183fa51fee0365e767d7b1b371467b7ab9fff048d6f2aa503f456b4b3b55036d77550308d6dbd2c90d21e7ac22037184fa68c3e150752d2d3b94ad3256445d53cacb4d59c909e922a48bb576c2387b38ea19a8bbd92af24939baf97a2b18e909fbc03d5c5484bde07e0a7edac69a8a6a824a7a0e4e062a0a68576b1f4b3f754f791f4514c40ae9a7278bf13246223435eced74047d647dadb5dcd0ce280d7b473e37277fac8a9fbe33c2de8ecf455e85cf082a6bec8f3684bbc8b940b14a6e8ac2aeceba68883f56dad21f26e52bec833046b9c391b6eada0618653e2f45236d0d452b38ca006ee76ea06bad2f84f6d4d585d9fa281bf209eaf379c1659c0de1281f17795d55636f0563154b292f79257b7505356d65576f1f1534c6d0106d692fc8877641ab186230fe8e86ee7a6a1e5a2242083d8c92bc813c12e521a58180a2d00e0898a3b5908a9dab979695364a5fcf1fa1a1ebee055114715591d346a37dc4c5d92f5cf8835dad5fdadb7031acfe7efbe78f385f71ba724463e72c6dffa8f9f9a5bdfd3730d07e2b73812b59f2b38b6365b581600bdf30920ecf33827e1cefdf9368d2d1d3d45435fade34d3ff71a289a9e8b29ef2b5e735500b812b5fdf7a2a0fa655677f238afd8fe9fca97a8ff0d46c28377bcbdfd57b7f57effdc14afd77f5dedfd57bbf3085ffe5d57bffec40af2bfb9b3ad07f3b294ccaa5d940b045a015144a427839f4d80bb31a02597b6f5a209830e2b2aa6696c45fe753795a9fb6969867c49273699f1f5112ef3ffac28504c0fb599590d333fa9baf177b5df1acf2647542fa35ad12f3987eede66149dbb7d43fed2faefd526f9de700593f2fca69ea109691244f2a6f8dab29ac3393acf3895af84ecab1f3622fc996e92e74dc5fe424e5ef2ebbf6cc4f070b6d08a786b2b1692316d57b71d7f6ffa3d6aaa533ec406fa5e7cfaba2260f92abf9b4a54cc0e8ebe79fd2c49e0878c2cffa2bb73a8738df999b30d5f0c33db5ff37b98bf7bc5a365b3618c0f8ff52eef22d4ae3e87d431d9d2d168df30125256a6c18d1dc2801bc747cdf058383adb3b90d5b843141a18896b81ac2b07f55fcb44854fdaba2b180a43357796d895130f2f8ebaffe64c5c7ffff5bf171413cf0c36283760feccfb4c517da77f8f7b7ac48bd072a86f3b51083a7865fc59642ee672a43e40b7ffde4d6e09b63baf9676c95fb7a307cef72d9e5db317fe007965272094cb823504dad9dd173ada3efdc389570f006e9fb618f709363e387b260a72d5719495fb814d126e889ed2029536d654f15bdbce6662b2312f7028b7acd701c57426c8b202628f404aca38620ecbe0c91ca3ebda0fd7a51008454cda3e8015ff3a8c825ec7cefa732ba30fc6cdfb7fb0918631d2f42f717f0c40b739561c0d55cccf9acb8dcd6a19da1b1a7c14b2c146cd24f8e1d978898fa4843912f76a14440ea34ae39a2dbe7f86f5d218dd164c552e5a637c3516fced4cd406cb79274c30e99871c5688ca090a49ff6029c2798d42245921fee9fa17a638b9465bc547c35a51f6cc6b199816b38eaed16096b496378c274d15c8861fbda98d290f31ae54fdf4faf411cc605a87f2bc847749a15b34552d4f976e77ad59fa65cb495db770571f210f0d22ccc599a14fb74fd73fc3a18f3f4b92e263ff1a49d2d492caa86325e50776b564d26d3f1bf9605bd3908a414526d49edcc9f24d44a538a229e38f5b568d17adbbdb476af0295516b70fcfd0617fcd697cf6bcc2bcbfe24a068e5b725d3696450564c5697b9e9dffc829a66031c2c4f4cd7710dc72fdf19644ec9ba1876f31fffc96defd84a4dc930273518a2fd0a2b56351b22c8f8c7086913a8242a9fc277f4c060f4412886dabc32d7eece41fc63814f68f46fe5184949e9e169d41ffdae03f0a3ffe52aba06bc3bffc7b7f8444a3e6054dfc505a8da0f08379673cf1821a16824259b8b00537dd046c711ae027b6cf126c1123fe95058ff8d41f8070fad90ef3afac1b39b44da550349ef9606fee0ecb203c6af315a987a6a3c291b7e48d852601b3dfc0bdfc1ce1b4e4d97bfefad5b0b466d64b8262b2766d15c97ceeb8423a38e47fb36efcb9179992b33dc6ca1363fd3bc7ff13e1a4ed6fe3ed0d1791b391d5b476b1b3d457369471d1b672f215f25447f91b7a20841118195f3f791b194361f41fe84aedf304ff03104e9656108c953006692d048762acac45b048241a03c15843ad6030ac9010d212690d43422010340c868662d0282c0c0315418a20603088882502f1ef65dd00e64f113a5aa5c99f67f6787eedb2020f1bfa5915318ed6a314ef7119847451f6f3d0a55be5a38eb5d7fdee63933d5829ea90c2e9f821fc1feb04b4e0224e2424db8c228f5b4aa81b65e37814eaefeb575eaeb67de79f3d111c3c8e39d8ff1b58e2f0698242093f6686b22449d7e0133152760bee532980456772e9ca46f628c967c8fc1d5904817db2809de2a35f1b18fdf424d1a57f21febcacacbd7bdf80a0d78f7a6857a93bcbf2f6985edef78c3717af5958126fa73ad663f06afd63b3e1e880d802aef13f9b941e2db5a17dae84ef6bf2faaa6f72934100bd20955511cfd4a6f248210338b9eb382a891f76ee26a6ace570c3fd93adbece86f4e3ede7a8a03db7868753535e4492327d518ac5c30bf3815fc50b27d03df8fe87f37b0b7f25288cc1f86043989167884e0dfefbd38860fa7dced681ef09939e79ce1c2288651f91452dd4ddf012e1370e47367fcf220e855fda199caf1a8f570d101ad860d6adbbc973781fa16c7c4cdaf2ce37e5319ab2df7ea213df4ec53305247e4bfcafe9d0dfb1ad05e1d6065ccc3c2d3a81f8b63c47b40eb348da446791ad0a1b197c13e0407bc5c711f75db39ebb2a9a78b054e83c4d347f37bd7398119970ee9b1b3cb55e70c316a16350e8092a680da13587e0795c671ee07cb83225bbfa1324dcbb9c50ee763d6eb28c5a842cf4db269110129747b74f8f0fc7994ae0f99bbceab3ddcb78ce708110afc277e34d926a376cd7bbc7c32a1f978fe2b97cf73bff067ce5bf2f3047f0d0d248bee99b9ac7c97ee5dbf738f9f8db61fedbfb8b17e3184f0b4f81ec2f228ecb4fc0a393afc5360eaa65510f2bb23314b9102d2868f9fbfcded7d9227afe3fce1873fbbd1aa2963fc01193c4e588e135f95dba9f8058a092eb96e6b0c294fbb79d969628e8a69faab8d996d29cd1fa3d8ed867cabc9488dc4f551d1d2c30874f29a3a75ea756edd311c3453e15b43f478d70fcbfdc11fbed8e165ad3ca4355d9480f0a35d09653f5d045c82abbc8cabbcb7a18a015a515659de59c512a1837116179597fa1ff2847cb1a658db4b2820b5bc1503024dad21a2a04c3c06018084cc80a8e8222b15642302b28126a8d4220211024022d8cb2825a5bc384d022c230619435fadfea68e18c9c478ab60607d95e0e67212dd5797a327681e8a6bb0b6f3a2fc34181f363dddcf521467766dca8ec2a2e3b974af1b9fbcc7d515c99de75ac7cfab5fbcc59491da2c05a5c9173822dc2a17f0d3111fe013bdbcfcb72ade2adce76332ab2d69effaa354b7242682220335cecd97a65c0057bfd5046c4addea9ce5594bc69e039a7c97b3c946c3cdfb3b3bd7fa013739aaf3534775afd96ffd8f337eb6ddd6b7fed1013c17ffbcef6c711bb6a6bfaca781be963e51d3c3ced1de0d66e30b434026908b5167132f4b246594a6969b9c958c355ec9da4fcfe3af32b15a53e0f273ea35cb70073992479ab6a3394e340c3f2b6b2bfb8a135504578d8c4f951c17b8df212b3d87159e64457d3e005614da509d68d82afb424f41ea20486388bc4361d8242295bdaaaad5bc692f271fc98f25725f7d1e48fde1502fd4367332fa8bbb64c24e76c3ebbf9c4a057e5e48120ab2d22cf48c29816fbd402bc305e4887e87eb91b7eba1785f872c9ba0df339d82c75e984c885399f35cf81535a39d5af4f9fe07098762cfa1ef34a474822e33d9dc707053c96b104efa99ca64c66896f9c6c090c115152fbb34f7142b7be71b0e4926fd63b567f4f3ce1bb36b19fe6779fe6db5f9ebb015efdf1d4d86c7b8edfd36e7b29d43d4c4b21d7fc772cd27f3b77c37e9fffdf3cfe4fbf70065674435be60dbb982a259534f163d72b79fd7b56bf69f1115f3f45ddb045b40c787a7f0aa9005b82cd91380a79a9ebcde396d2eb87fade120d19ea5fba75804a5651ba08e5c87bb1f7d91d22bd8df727bd2ee0e9a3426f7c8f3df4f854ee292201d3803aeb3345b66f6d253686720afedaf6d07f7dca8d421b6d69e9f7fb6905fe690f19220d3c3db0aad22a1ece4efa323ede5afe5228949ca2918397aa1346d7c1cdd05ed3c143df0f0d339613f98ff20451680c0a2264250c83a0e11004c2ca1283820aa3a022306188250a09131211c1c08420102b11214b04146e8db2424045205034c21269650d83c1207f6d4fb0f891eec167a8ba7c9e76b7f71068b5a99a2e63f696e2868b7773cdf5d6254d1e09f22b5ae8d39b060e16fde6d7bf2468b7f3204e64553266c89353be2d6a4b36c39532db5409fa61b7ab96a5214a01d0419d22325d2157cba06e5cdc720ae21f953cb25472712903c1167162d03fb02f5c43b435c4614d41edc4c7c8ea221ebb1db38b8072d7af1d824e0f4ebaa76339bbedb32f0dd188868788ef6f7ec0dcc2a24699726fa22523c7713d31e294cdf956c81cf6db4f4cac81e30fe070d808716d80d1c772d7b9fd752eb2718cc77dbd8be2cda3ad188c5e6655906522b8b3197ab860db3eb877a626d19cec2b4f1ea31503943435ab193e572a14fb6ec43d4d521dbbcbbcd1755b8eeba55b649ebb48b49f88b21d74f241d4a313c27c8eef8f4525576359ed1ff9159255ec46701cc01af5071f8b4c3b7c835bc672dc86532160467c0edbc23c74db59fe5d6be7897ea1d9830f5e51bd36ecf1a5bd2e1537cd7d053a2d4a319110288db1e111c20efaad735ab2ab251677fac684a02e9f8c8eb9535f352180c8668e61a2b841a536d02a92c6d6ea2417ab3e84687dcc69d97ed7873ef6a8c6c383291e123a0b240279ba34c5f4a4bda3c62ff36a88131b753a2690b76a8d3a1e6730b0c4ea5db50c3ff3a0840ce9f950ea53becd4bd7514648758079e788dae49d83b1d7749acea72f0933bf97d530b9eb15df64b77121fddaa80ddf72da8b39cbd822565fe65d92d1b38baa9e9623d41a0277ed893f3a5fbbc7bac25245ad4e726ae554bbc69db345cfae8c634882160c2e5f90b1538abe6f5c26714f9229cb34f6d66bac98eb4b169733b6f6526b2bf88f094f723112349b3ef93a4a79ca33233632d4badb3bfe48b6e50a97d194ffdde89a6bb306340b2c16a51696e69b1a2d26cae3344f291413349989c55a4ddf1d2e3c957b4e2f9d4386fbd316417452be148d989e2bcd35ea87e5189844f204edf46b9fad37ca64760a4f8eabd62b1de731186c2828d31fb8565fb6c3f2368ba503d21e40dc54d6272d6671e53299c4d4c183cfd126de07d3040769ab1fc33e0b5f1bd8449e535dbc4f1379a3134ac7062d33b3e84087b7f7858f750ab9b358a4e7326914f214a90a549556f5322d68dfcc74decdc974e02c343e948cbadbe5862f9a29d7c9a3172124306412d215b436ceb3b452cb73aeb888b6d82ca96390f977edef0dfbdce0716ea07bffe77780df709571e34445dad7a9dffe9e43ea773c23fe3d3ce3ee0cae62ea6e3d42eaac96a2debcbcb4cfefba6282030d09668cf9a583f6e32b7c801f1f72cd750fa961a597984aa882fb0d8e5e044e5082cd6b91f8eaf78b14f07e842522cb27aac34c0349f97efda675fcadacbf2a3c2c4d3edbf882099c78d7a4606fd7a1b45b9167ad2fb76bec0634457e75fcd663631da16f1a08b6086982428942ab43593e15d41012fd2ba6be72fb6b6daa2256a32f2fab294773b8b28f4783779fe6ca7f38a61e97eb8e4f24a5b27269efbfe0aa0afb7e943a17c0954de0e078aef19980995f117fc9e19626b763704bf96f2b06f29be9497cf5fa5f45cc683e57b8a82f9128f567c6ef8e65fb7e4885666852fb3c7dc03b9bec6660693d4d5c2759eacc33e1769169d2171e8229794af385989fc17e40834b259fe19ee63b7aa7ae9089220bf5af90e4d1df0061fa27bfddff14ccfe39aea8c009839e326599e2dcb1cb72a27904776b1eb1d9b54b7a3ce4ce3233c4e7357b5fc5d5f589ae9bf5dec99a2d894c8e4315dfe38a125fd374626c24e09f4f37400e556b4e9c4e7b7ee56f57f4bf2434afab8975903350b07481c1bd75edd4d14e464246feca7246beb2ae3eb25a22f2cad60e56da685f7fa4ab83fb7f942b8a846091686b212b6b61618815d64a042a82c058c2310828146d89824211082c020b8158c1d1565622186b4ba415d60a2284c620503084b0301206ff6bbba2496ecd6ae473afa35df13e7c3638406d535a2de61e7ba17d27b695e3cb27c28eeaa3972ea1b98fbaa44eb23f66e377271eb13839fe164d24ccc7d020dfa79eed1dfddbea00ff51b08a1f99f79b0a568f7d73c110d2dc68d8c2bf1714ca6b9053839f9758bc3082e37eccba3a14720724db8b75e21e9f33fcb2d010a893094149915fedd03fb0a42894f05ee89a23bcc3b6cea53086459b7fbc3185dbb7b185f302d6ef05f24ddc160d25ae4c13b0c95de13b75108665b312c3a346e93425b19c157c9dbe4f1dacc1375afdc44578fd30b9a9d9bb07a56208a21bc4fe97f6f9e1ada5c34e1fdafb0f0ea2ac4752524d02696513a1e55efc1f574cccdeec95a7ae61a4a3036c83733565e34befe7b306474ce00839d16eff6f64d7352894883e26f430d6a8da3a48931258b57eca6878acad9c91c6d511d6525e0e3e1345ba5fcf0519c8bebb465a0884851de7d4dccf3b519c7c3b89e8a319ca1f3f59be3ad440b0853f1c859fe839f5f07ad03f8f5cfc16de7b62f7be679a167ee1fa1ef9ecd69d04f463c7825b6f8653dfccb9b09c2b78b3f2ab7bdd4f8ffb7be53ffdeadb3f20e3d2fc9ee707ee9c70f3fc8878d6755c9a058cb9cca8756bf5a3cfd99238a653f31467b88ee9c4ecd75a6f20188c14241c5a1fa3c00bd575880845afdf0bc3d79adff891ec8570a9db6937f04a28a3db7c28b750630dbe6cc7c987aa318fc29f500bac0699eb4aa8699e0e5c9799e06cc84f5ee3d92c1aa9bfcaed7ef2d7f79b1f6bbff65e035f24279856c8201697753906e2f061f7edda4bf7b91a7141af2d715952a07dfe4bd7ef9e811fe77c8b382628945dc5a286382ca448bf739fc7322eb3c7627f662518c0d3bbe96ff1ec7a5f6a65ad8cc56e2ade5c15d6236a9f3a888b87ee07bb33e4d7a494631457eff67c7b8ab08ebcb84c170b461e3832f9f89707bc78ec165e059af358ff1881e13751e49abe737aeb988de0bd3f66e43afe06c99b20c8cea1499f050b7c5b7d41337c7fef8847aa129d7fefd5d4f6be6d106a2586fe28ee5797430ba219bd0a89e38e1f6450f8b77e9f71495f734b095cdf071735b6858429da9464d5fa724c66ed87b87071b52eb519b6d2f5feafe44bb3979aca86003388a640a03145fa21f3e29dd0944f024c8e0d4e2dfefbfd3e16f905ed437fc0f7e9b8b276e3b10386367be1449e732f79f76bf5b22fffdeef63d29032d380ebfbe0aa11b2688f09be7556a920be3e6a3bae93995823e6cc738f92f1d3171cfac47a3d92c4ecf6fe98ea125b17f22e5b406187a2e998ed66c43477cacc7ebf0f83855b72fbaba9dff54dfe5125cfb621f3df51258fda6f3cf83754c93b7dd91c7a6d63f4fafda7b4015b5269856335c1e6ec966142d098be92728b6e35655c47d2fec67ff30aa17b30a93d71d4e5541e3a8fd7c3bb96cc5a6e12494054de7f94f8a16f283a4900c2808544a5891b9c6b4e5d3f22796b867d9023d310bfa363ae6ae2a3853ce10566e721f679a5cc53752a87f48defc5f7230ef73310dfa5462bcb65a525753acd430e48dbfa1b96c4482486040bab3267478759856c76545d7518eed8f1c06aa0a038106a9b2a41a1c7105fab65754ce0a67b052d952d982b881585bee72d018da65f2e9052eafdeabac63bd853d1440cc123d822a5090a65f6adfee1bf1a52a2cfa9b967f7fe0cc2fc1cf3a9bcd1deb0f37594fd4b5fe977d90d79fbb4af70b960e37ff100f33eb99f71225871f1eaddc4c323accbce3f3fbfb3a17b652160faae1ab3041312b1f853e08260607f4110a6a07d7edff17d6e7038c32851be192c078a0878e81ef733a470392cd677087b487fbdb93379e002d48852ab97f6b6aef8f9d98a450c4d129dff9718297f8e376624d80fe52fb0d356745155c3b8f37884750df71e2ad0617636153d9d8e205ec2a3ab13757f4ac750ad7ee3db1c4ad87aa2b9cff9ddafff02e8cfc7c748dd321bcd727c61e622bac4f0493dd9bbd9df1a4c6c1886d8af7eaa6f6125c213ee8f56e5dd44cbd5e3c5c5b946076f6debfc74f852b4eeebed5353fef2fa798d5bb5e4029bf3466727ed1bbfa3b61a1cb68ef1ae06270e1005783be2ebe82952a07b91eb92d34bd541792dbe70c889c7332fef1a7d65697f1f88576ed4438fc58550fe617ed109e1e774c7aaa45ba87306a5ef240de892217e8a10697e83771a9f72e39859d43ee777bffa09c839f1d62b9e90f92813f66c5f11d63e2ed4ca45f94d230dffd9a3dcdffffeffb87017b3fed8f9ddb7fa4fe8fc9e26327573aa4770b932eef4cbe9a844f4d97c2c50ec57233f9b8249322d7573b0f37d96713293ad590be8acbaea4b77ce4d14134b7eb4879b3b22cb622933ae6daf3c8a3ae4e90d7373f988541e54ca61fa6d676a6120da2050a5ba2588d0dd3549a8ddccfe6847da68490e702157967e928c3ffb7124eb0d29fe528df691ae0e0bc6bd729b2f1b45fdea96a39ee74764c31b9bc9f58ff7019dc3339e4a42132a7609249692d38592d01f62f378017d7b257d20edb4d167e24b06ee3437b6eb3a738df0e6f6ca758d52d9ab34c6f03a2b4f1c99345435e65c380e743edf7eb17b734a63f28bce915bf7b2ee597dbe5a6b5db757be3271a7a79f60e0b3d6b573f427ef238fd4952e00b8d5a35dbec9cabcbbac56970d5d868ae829ba95f0938100e75308ca66e6fc93c1aa63588643a75e45b84603d9dce84ed801cf839fcf735a71267b7db875d00c2e06f0eec4a43a1ce7e11fb8bc3e35347ada4fe812958d254041fcc2e2a4f44b5799c5154b0f9fca8828c1d49b3580d398de789bbb61336edbc0d6c0d3adafe8655e5deb65c01f3b5e435f7ed34a4d4c702df156209a332dc408e89c7bed248f9b8319716df4f86813e99a904ac076e03af07cf92d0ce65284e1fe37881a4ad93bbf20e9c8ddf6ca938b15f16419397d586fe99f1b3be71a1c7bf8f120a07ff9e4dcd5876e0845d95ed093a23c1fa0a4fa1588518aaf5e0a743e47ccd55fc9d5f9f98466ec78d11b80a757f8a2610455a40a923d5ed2783330c62b72e1f632f0fd9f859879439bebe52e4a42bc4f7899bc1774049c89a8e9f99c087ccf2cd4b2f55ce3f8c6a7606fb4d25ef94546c51bf16ccf9f7171f4b2358abd9f63e38e04eedf57da69cceae6e4ac7a76ea26b251cbfbb9001c28b66031458604e1d5c9de6fbf7b8b36518a77b1c504e87caca7fdd6adacb4aae9ad35d5a87e61b9c5c5f943c0faa9e5da112d360ebb8a383a1034c89da841d63205747e953a381d15e9327623ceec2162e78884839bb3359052096e2d5244b77b9bbbb68af08596d59f6eb410053a6fa761456f495557af43da3ec5deae7bceffb032072035f53e3d4641d9f23cd9b72bb9f87ef028c4c43b0170d8fcc21d48b3830d8b4de60f377da557bd5aed520a90130c7974851ce19bf49a54b3711de1d236bbde45a8bd573e5c70415a0e33eaa8c5cdc48221378ff7b08d4202cf77af65856783cfc81143357547529046da3c09689e64a84d7d3cf6eadaf6648b620fdba99af58a1d29804a435d82d6fe229fb1d820268326223b45e4f536f4f45ef9bd95f62c8faf58e555c885e35101c11ef25b9cc0fe71025ffb4c556b9254fc28b1c1f0d5f6b11d592460af9f446706b01c3d9aed21128c8aa1ac20ae151a03008e9e73b24d67da2e9113de8cf998d2691df3c40d0f48809c586f564e4c1428dbe63a1ba579803670832d6d0b78bec75b35471e51ed5ea597b8588b0abc8be85aa901f4cfe176456e458e5e51b2ac46df870ff749923f00195c64f2d5529712d3d5b8e7e1f0f6e01bd40d995540ec8380f0624f924ae517cd789acb4e32e2c26aa7e381ecd4a7ae949084a0e5c3687e734d2b8d850a2a9f4e607d8a906c701bafb15c9b0f5f6e165c78b3f1c92410b0573187b4f5768e9b2d96bc7e2637f4400e3e122e0154041248d2ccbcd72c172b3c9462c4d7e29350670915db2b0f1c9e2c7c5ed141e96886f73e2344efd5d1b068b0d78dce92109b396548f2a51869b6271a1af43e9200ad75579ed4d4ebfe538691b34782f2f512fc43947ce2f7ca2fc8c8bc23bfbafe6978ac84acd9e9256d25693ca05f972a788c6dca2b1f6e06ce4247d6588292367ce3f6ca13bcab2f9bd697d91c9f7e7af145e704dce1202180f0ffd084e16a344c546841140e9b9e7b7465a57517f0b16f4211334ef48897641f25fb16360ecc24e8b201e4f69dafd7a8cc4b18f245ae9e8b3a715c6fccafd11120903804ef7e785f5be2d31bc5f2d7859f8c5e68394b03e69e43d89205ffc7ea54c69000aff042b4c3b04236e02f33a543999d42dac46e868dd9d2936fd5bb7c79059c7f1288f36e2e9dcfae64ec9267c8b032b0d0f91f07328ff708d6b974c5038ad26fec78be893c76972e6d9606d87fc6c614ae2f59ce329f329c7b4a4ed8af269e0e14532dd844db30d7a8c56d2c910e2a223ddf473c1704c814ce985da1972369a06368f2a5a94f4a28751b180adb2b2f2df40a634ba279f838dd3c4fb938d6f07a10d81b20022aa6c0a9bf3215bee87455644a5477c6d8e82bf8fdb92ada4f9e5525cdc687b3ae4473b7b2ea37ee95cf5578f43e7559c9f0d7f67cfbfc75fbd49107d3a05c7223efa8699baf8210990c511572951d71156099b5458a965930de1928bd19e910e6e257fef4280ad8df30379c193f74c6db4515130a07a0e20ae7b2b385f7ca1f1d52cd8f981e88be3b60faa07bb02f8940dd0ab0cffaa51e717b0b2317d7910623ef3f448b67ad9000b1e7f03e53ca0ea3971a7ac92f3fcad5d1669ed57702ecbf71296f7c8249a49dd606f28e0756e279b8840d37e0ef85595fdb16f1a26f60ee4fdd2e0acc3bdc4395b5573e359226fbd68fe5ca492f5966f73b75b75e889c073ac3b3bd7f20e3ebaecc58a789cc384488d2373b3f019c4feda774b6e203486223b9df3c5d98598cc81fcb0762eb262fcf6123113379642e45712c7a33caa9520701fbf7339ce52d93b3e087eddc2bf4705bcea9f9b008407f9cd2e56d261442b939187bdfcd7d5cce2a206b1ddb2b67cd90ae762d51531f4d8bcae1b82e2f22baf108b0e7b372974882fd79294e2fed723ca0600be4e9aed507ecb77e2773eaf6e73bdc4f37670494306b3305c980bfcb2aceae1837c4ccd1d150401c4cb33e240acfdd0414b47d2e29b8966d86d269dbc6d07966a467a30d28ecea92cbbd88188a22b7a816304391f12ed099c9bfdb2b973dccc5d314a77da155463d7df34473107d6e00108ee80f947d56145377623ca9c4b656e84bfcc6bc3d005b9b9d85c8df52c2e71618bb6371c15c22f5c392c48bbdf2f70f9c14fa694a3f7f26124fbf6cd915e2ade2f1e82794689e4628cbd79ebf51a27f4408f0bb51a2b850a0b850a4b8b2d027f424df9d41b3beb99cd65da4f6357c4041ad40e5db817ee4afc66178724e31adf3e39882fcbc863f7182a54e8430b1074eb4b86a5e13b411efb5dd0a25c3174a695db37e6ec532de92e0ca4cbddaed70b56a65bbfbc2aa4943e149fb4025a20503e611bb8156392319aada57bf01254aaef5cf45412efbef4fb4e3aaea2501febeecc9c7a5855c87525e45b839dac611d07591ae22ffdc44e1cf7b157cbfd2ee7be3f8fffcfafe5e0584af72f50ce732a43e881bad1f1620b85a7acc44f19b77b8cdbd1f48a854680444edf44b8e4aecc996170f69c96d55a88f9a92bb85c0ea471b77de68372294a21f2521d7882557971c735ef3eab61e7cd0f7d29afc70484dca0d3d2a8d4e09963f68a6eab83e3a94f97658a549e4493c5149229a10ccfcf27faddf7ff7e2f8f3f57be869b88cb2e555a8e78ecc84820277091ac2fe6b094682e616e80a1e9febbe0e96b6549f03f3ae072d99e0f52257556924fc29829d2b4abba4be6a7575100730c37d20ae6a9929596e378d9bf2ade6af3d30e7650e7cd57681bb5efa7a728d2fe63cf31f3453e694a4b7de9fa6b5da3cc7c93118e51e83c7e9c3f5f7fefd9fa7df8fcc14bc6f3533742e5b1ed066e32bfcbc2aa37de39b5f49a0723f0a2ee55cb1631bcdd3c59b277c45414cf17c5fe4ab0f9f19641fa8386f6de542842e7b1cdbd924e329eb657caf5f47146e6851bb78d6bf4fe8cd7166fe9eedd90795c466fbd1ef06822d2aad7f12e552fd114615f1feb24db8a84e48702c1a721c0a4d013cff1ae3c6250daf81d90c8968b7a8cb2ba9c3a6afdffe7d68fde72d6a12ad8a7eaa54e940a21e7a1636899d69fa4f83bfbaa80fb9418fbbfafe3ff6de022aab7cdd1fe7a5a55b1aa4a5bbbbbb5b41899706e914786950101010909456e90651909090100424240411906e5081ff3a67ce3d87ef3d2a33c3ccbdf77f7eb3d71ad72c9e777ff7dedf7ce2f37c9e0b8196a582fbdb449855a9c761444fc349427298c79f3bf8e298ae5f8feee3da7d711d4f4caed0fa3a428dcb3b32518d3bfdbcfd10afba073161288e0d74cee2574f586faefe518796d728faa65e2c64437070bd49484b9d45edb1e4f05ff3fb3f6f7e4f60a43377ea46b24dddd0532c154d53e7ed55d3bb90b7e09cf93d35a51736fff58e9233a963ea389bdf1187e6b28d76384d6759d7faf5f6859e6324c80184f11abf73dd42c5f16aaac9e1bc0e946fccee4b822d7985a62ab251fc1f65746cfaac780fa5bb2c0fbffe64bb54b5d4a046dd2ff2d7fcfecf9bdf50b3d728b83b366576c518b25f36200d956c793f2c6976f5d14a9d27f785404152a843b3b16333f13cd17dcee41344a3ba34e555c3c35c625b6e3c84f8d5e6e9b7f58c11b2174bf39b7922290b8cd6393ef6951e5d21a460ecaa10eef4222216dafaa3e6f7f4d124da975bab938f9fdeebe4f7efdf59957979f3aff9fd9f37bf33bb286f52f921f4d9084fe88932f1bc147781fdb086160b3b7702c5b30b81bea4764a7977746a1850de4f8b26d019a3bcda8ef06531d4af71a71cc6a9d4eb2673c1f9b4d12c0c5ff51c36d121db300693da9e79a4b856aa612925ef7d4b382df7ce1f35bfa772964854b9dac8d74968df6c951c3d27707a66f9d7fcfecf9bdfec1ecda6d9140f8fe2e5033e715ed6df2c8b37c0fdd1cd4c58be3656f417d3bf0f9328fbe21c7a70ccd22dbf320bac30be0c08cda78d47a048cfa057123f0e21b2e430dd3320f7184328d571ee19314092b4a115822b164765dfd9e7e7d1e4eeb9a051fdffef48c511ea68442077acdb9a3cfc1e792c6d8979ca9bbf94aaffc0455946f0dce59bd45b0d675d4f873033829e3a1ae31f72c379b3a3f5cc575fa84c86948e48c81db3940c24c7e1d67ccbe2e3e6914af56d7f5d8dfcbe3828cd25f382ec80270aeb4f8d203db11e7ad5989babc557e8057a74d3a24cc998b73f330d601bfc51878ec1259611a5d2446ecace85072165f60315f214557fcdefffbcf98df5eece5c360cd3226920938bc8885b643b9855ffcf348aadae67e8e184e5103d5f5b57d74ef08e2cfe943d409325db822d500e8dd4766ad064af38e8a2a76d3068821778b8f07a9856c639acf6d27e19ae94ccc6172b0fba3fcae913545ec628c3dd89caf874ec3072e69d574e43f4d8fff4fcfe2b52f1e7cf6fe2e2b728d46cce4f2467afa76e5bf4a8aff5f9e9fff409ea55e61799df0f049e33e361ed5d1a17497f9e73ad38f8cbad45772722f6e7e3de6c37c2969e3ce4453299213eaae8c23e50e5c174ec1ad1da0ee858488947161bcb782295b3277231a56a24bcd07034f75f7c25a1784a1d775be00f8760a1487b1fe04291910a71eadbb391927f1460c0bc1e4eaa932d45f33dcbe9aa6fd4d4d324069ab4dbcaa4001bd477f2d8a57fcb7b7fa3e27e20179bc6cc98dd40f1ce865c927a36e3a701122b0796fb2dbde19086a5ed328c1066831cd866f8c30cbef4ecbf3374e7fd8a82ba90bff3db162efe7bbd60a23a7e1eb9ce0605029ad6ea40f8d9270fc8539e9d572ff897cc9a5bb03f21b3a6630e86da6c5db7ff7e8a7265efb58dedbba3774026d073eb09a6ac1cba604c42dec249260797775702af9c95f7638ef735f65a70ce4a2520f963f637b2395603c846e2b6bc09599af2bd968f4f775c72206d0d02935fcecaf39799d717dcf9534b4e719e7c290de49cd4541d3c2b5f0ffc1265b6796309df7829b4c62a92d3837c1c402607b51d4775ac90d50c7dc3cc22cefad879056d08a832f589d3a3b6b03d09b39de098a7f3d552dd43392600199adf6d67894561d4be19de8af262da41e9c96cabf159b9837c4bba09a9c0c006941bc515fb3068ed9ec20360a0939a7449130ae5b5e5bce3120ca1aa0e90110068525102fbbc4f5024b27f5f975c58510ffb81e225adb372d22267c1a3d42ce56867c7d7d4a78bdc56f7ddc7cfcaf52c9f7dbd216a45c84b81274b78b856324a60df7f568e2bb424e93bff04ef7e5be93a43f5714019350980ec26825d3bb23433fda84ce83da1bc1af896b25625e7ac9c6aa727d6d9b5f0b3795b1b65c7e95deb1b6123004167ef352e737dd1398b1dffb8e715ea86a4ebf753e9cecac54f93adeec42cca1037562dbdc619df83e7860199682d8f7674208fa43d4ad02873af74f5d0a7df5d063207e271f9344ce453e01917f84cc3ed1038a61ee203d9d7bad9419e87a83ce27ab541fa01b043af75593ca07d73ddafcb8f4c8eebadba59f9349c188aa2f17da680a54f094128b885de9810509c741c86129bced60df4cf02153e35f7e4ab4dadd1870b2225703a0f6bd080e7ab2b9f20348a895cb6873cb697bcae8d64e96302f0448a25c16758d7cb613370aafa248465261ee781408cab717c64911b4f634a2d596bcb47a99b6c3f8d0099012fc2478cbe067e70a12c8879b06346aee982c40db03f857cba97e5351485a4c1a299bfb61045a66abe21031cf17b1b58cac6e557449be7bd365a3ebc9ede2203003f9f5257fd8f0a4bdf7be312e3c410b41d2bad2400991165d56a44519d4295a31e4bd5133a87ba12b73a81cc1b43e3924397db25095f93726583b67df0c214b953cfca9d98a8677d1ecf09c09b1a5223bd17d07bc7bc00646e14616a66669af7d11ab62396a21697a37e50e2a7392b9f7ca8c0ffea32975db63bd995f9fc75061489470035f7a40bd5296e7b9d70194da4af6b98e2248ca01dc86c39f422839e94bfb90f87703b42eca56275291e3690d9535f1da526d93b8d154f6f34affaf8d3de661e3100f4321cb0b208ce28938abe3e424660a358824b2e0664568c94a4332ea8ebdc3f79494b8436f41c37344c07d8dfb64595fc8d94eb3b7982580322cdad426a9ad600561b74069e1e7b6c5d531e08a746205b7caee436119039546fdb443ec4c0209679bbebfa64d5d8d0e64114b07ff02cd9f74f87f4a68e852d77c7d6508b38c74e0031a5ba1b3bca482ac362378d7864b3abeb1b55067700f562cd40feedbd3bc304148d4b13e94df07c28f049edc0fedc5b89cc66442fc24b127863fa986ff6db6c2660e129b2dfb552c2c2b5afd7c19f3bee9040969ad3f13b2bef9aea48fce433bef56a8b63dac9de4907ef9315a0c0432adc283f350b79d6181f1d2b3987bb14f42c009975b69f999f89ba2b878fbc53fffa02498c9f75a10f00646658d207ca760dbadb2e779edcaade4172b8aa04a8683778f7fd0bfabaee5e6af593d3923525d31dba0164e6300c3cf996577630571723c8c5e27b5013b0cdd37956fe70bb2f157593cf4243733f278adf93f292a33c909936a522b34325744bdbe1d9ed48cd2025849b632c00f2bd3005dbe6529ac8a97026ee565cea1dd646a24820332e4181a4a5de830e3e89e920c37975d5fae1420ed55939f780396d451dedebaba754654e5870e82919ee4059da2b9a92d8777c26ade60ad608bbe185c8cce82480f529e1a5ab3ed8ac2c61453d40fe0145a1046719af19f0f9052dabf04b1130381ae14fcd18e045641e130199dcb9f8f74697a775675fb4bcd2e38d2c7f9338e604643e71d6bd5842461fc7cf3288f8a24b5c2bd62b3a0c9c7f01f8132f5d4bccca698f3f1257bfd7b4b1451a05cee706feb7da83be38ede6451454f91f0ada849c7800daeaace1833a6327b2dc2d9f8fadc81b4f87dea2f30008ec8a2bb44f2f69b677af644e7d788dd317db2ae1097c3f3ac778e263e5686fae7c72baac6f9b06740b3200d50bf94dde6fdb36054d4f111eb8a971c8078adda805321f656686b1a293454929c3995788dc39431291a200a602c4e8e7366a48d03b1bacfeed625f57e31fe3bf05321b1b2fd55d9aef67c0d099d7db7abbf32d607ea40ac83cbd3d5c2e2973b7b32921274b4d112bd972dddd24edac3c9b9fbabfea44559ba9bb2b8b51c8f3262cef1a303e4fa61ed2265dcf89f517d24e8f7ff985bb7d62e0cd5979f968ed29c5c2679529d20083cca6b22743235d1667e51f347abedcc43ba8d6fbb882a29f7bfb2e711a15909905096a2370b829f9f19e11d43d62b325305b200b38ff7cb62a12c2b76c7bd5ef1193f61e2d05275a7702aab74cfaf88d5ef29b6674288d2e94e17ae576216bc0fa31c6d14ac91bec7d2a282c979acb74c2a1790d13c8fc601b8e7c616c9af82ae83d4afc47321376c74e3460ffe0ec27ea677dfbf0598e47b1c147b5e324e9684240f73e501bae889338697c46d3f14ebcd140d9f3840f280b2dee757cdb5de4531bd4601667cf81f324aed913c83ce4717c335554b35bce830e7d948b6278e3762614c88cfc6487f50a8d506df539a91145c0f0bd6133b67e60fc03f5ebc9ba27dd4b2e376a31dc5584b64b041702a95983389bb696d62889665948667ea1d7d0efd0de036aab63dc6f47a57c46a92925068f43581af1f48aae10e0e6955c609e0b0ddf9f4d9c31ef599c8d4a0a544de33a2b8feec0e179e62d1c72f93d62174fdbb0fbfa95a780e99508798c8bc73dc4e99ece71a32c95f01525b12b9059ad446a55e65c34f8410a377638234e598f521811389f28de53ed4cbfb99a322b7e33412ba4a99a2b621e005c62d82940ba912c46f03e58497da61fb355c82604326f3bdc602a24abd74f2112f4c9b585abab48d0b67ef823f8e1087f4ed1f189e17f9581846fa11a83609c63ba9c471203776321dc5261a21c99bc9edba0bd38414327bb6c6b8304e345d0fe97d0a129f1d28b793720b03fd2d720a4162e258b5ec123de8b2e17dacc9711df3dbef1c31fffa3bb7e7ee555fcc9ee1164330bd75b6e6ebffb7e6c3b0f4767a82b3bd4dbc2c6ccc91afa1b6e3d8973929bb6f8b9d57ef5bc8aedb0f32cedf3dee20de6c9da74c98b8d28ee15dac6f1e793d70f09180e827df2451d445c5a7a6021d8068f438f6bb2c5e10f3561ffe523f9f732612e668d7154aa15b38b89f9958c8c0cddf9d98c3f4f1dc39696fc1f2468ffefb5c22c343fc528a21858c920cc112515b9cfe60ac6193ae83e7ef65a36bea7b706fd27d3e1df09dafdbd9ec04421cb3e7be9075fe45cd1263fd8c8f09fb7a8cf91ff45d0fee75e7f1c41bba6b1878a9da7a3968b8cb69720af97afa6b78c93879cba17afacbdbda785aea599a1b293818b2e0f9f9bb2f91fe8d3bda0bfec8fa815c6cfc7cdc56b61c9cbc5cfc9c96fc623c0270815e2e7b1e4e3e332e711303733b3b4807209720a585a9873f20af0f2f009f1715941a142dc66bc96505eb38b75c69f4ed08ec3a647ef70dbf7a65ccb75011587c2464c37c68c5ead5d5bcc8daf297d4f93fd6f0fbaafdf5df7dac55a88445b490a659184d16dd18dc43d8cbe3f1f3f939ac5716e6d59f823642d580802b64108d325cd0664d939aa37cbbf655071a746d2793a4565dd44a4688b9cf2fb0df697742ff8d1d9178b239cbba96c43feb6ef89fecd82cf944879b6a68f6555ffb0f83efbd56123bb07d2e73ac4a73b25e149e0e0e048ffd60af4992257be0ff62115835a1286eb8bcbe35bfe261008daf7a243df23db36ed0deb335c9662976e9342a6488b5e74ec46ba029ebb1c0ebf629900d74144e1bfd458ac9790820591c91ffffabc38d66fa108ffc7f89c47ef7adef8cefe9643ebbc1efcb76bbaf3c7bd89554a4568df1e7ab1e90bff9bf8567fc7fa81fceeeffb9f89f37df1a3d1b9e5e16a01a511a691855ad9aa3a98399ad1b0d24839def270725772d2d391a511a6e1e265e7e7e2e513fae725f8cf9f6878b8ffeb379c3c42dc3cbcdc3c820282bc9cfc34ac34da502ba8abab99038d300d0d2b8dbc8399b51b8d30172b8d92933bd4dad5d6dd47c9c9ea168db01f8d0ad447e9efadd0b0d2e8d85a3b99b97bb8feed9df46564e5e59ccc3ddc647d145564ec5c6dd454f47d55ad78e56c04a1b7a45554ed0d747d38f47965a4b91414b8a45d6c9d655c78940d6ce4cd3c2dad3964dc6de5797d1c95ac3c3d78acbc54b40c34ac0c6da4157dcc0c5ca4a59c9d0d95e5a5f4ec65acbcf46e197b5972f05a4a599aab6ae8c9caf32be95819cb711969eb68b370f13a7939cb71d82bcaf1f01b6ab8737b0b6971280871ca9abb72a86af31a7a58f8dc62f1d1f6d412d052866a0968a95bf2e9b8c8d979eafaca2bca73f2b35898f1ea2b39f071aaf89a5919e93ac9f1fbca5bd979d8e81b39eaebfb42e5dc042cac75f9a04eb2024a3a7a360af22c864ade42b2369c329c76b6ca5c8a8e3c3296dcd63a3adef6c69c3cbc66b29e1e2a023ce672babeca56568ef20a96c6506f0b1743c75b9cea9e1cf2b6563cfc960656daf21e66ba325c161e5e767686ca664ad63a4e8e5ed6dcb2dade1c2e0aaa36fa2aae665e626234fefeff8c6546a015f9adebc0e042bdf812e1c2e075d13fc2c15ae047c2b13ce9218e4c218aa415217a89a7a110adf59bff20b7b374ff134270988aa26f3aeab4d306eb6bfbb8ebecc54ebdb356cfca8f9702df305bb1a164487d2272c3a3aa75c266065c14214a8fdedc8f0cd4df2bf7b415a0d66fe7a859045c3ccdfe1fd3076d13c6ef1e3492b860be60451f9e20015cf87cd3f62f0ba9e439fb5a9e16b5f31c2bfa7903b040a9083293fba248badb5e88eac6715dc175c2b4002c968d7bbf42fdee0ef5ee1e07dca799c634b4a72e801de57c7a29d4f5739169cdb7f6a37cb2ac6f73231f4ccecae70e9049af6ca2dd661f1f8b1e13d0fe7815070e6036cf29a2786ba3683441c2fbf4fad5ad207fbf1d32805c84a7a73427c4e9f0f321b43314b3200c3da0b9006032c6c428440a15d2b099db7727b9553af89a72860f08f15df27ce6d9eb6d4263bdcdd8c18d95d5a03b51059c4535958a52140574613ede5acc5806d9ad698b7440084d25783714a3545d7f5b31738efc4e084173a2e2d0597970deda759df005b4709dd7dc69b2a66ab08d24c005151d2c4a10f57ebbcc7f0dab77f290ea28e183ccb5b3f22f4f1f8e89472a653814373eb165e5ba6a15ec470cb84099cd543addb0e21a745ee471a570f43d0c310176df54b44f5ef147fe4f275f373d5cc2891f935a9a7c7b56de2ee1e134385ddeaea752c41bec56a5ebd5e90ed4b7a1bfdcfcacc7dabfa39fe743dcb4f56b0ae50156c005544d7e603b2a90a0df48ca457d40fe543ae77937703af635a066b55df91630f8ce6b43fc01e5bd65226f200436c0a3fcb849e42d6eb5c5169471337f51d2d707080194dff93a93c6e9cfc5e918c5cc8ab4b177a2ff0a2057628862bc4a3a608f40a49759fcf2ebb1f15d053a40bd524e3beee214be82925358c39d1dd5560e6b2001c87fd09c138aba74d074943e415fb921c994953fb0bc7a56fed8a9a77978d9a4d8aa6e0dbbd175e2553e260d408ef43e005f2e8785f57d7f0abcabffcb118b41494720845365b342b2103ef3a9c283b9b5d44375069d92152097bcd70dd7f21539bbe05bcdcaea400f6f68f9b708a0ff6a1f291cf7076b6e2626692cd9286d7cf8ba6b0eacbf2c96019920ee259729fbf5b0dbae433e6c8fb080022e47ba4c98316441fa11c5e812b78bfc95e978570172bfade2f85c63edc2ebd7ec260faeeb5a9a33de4b02427cf89c4e97cb8f27f1b95f946f4c4e0c7722950802da0792668265b3ca6955d2adfa50a4b28d775806084049693a999dd4cd46e4c759d7347932901be850b52a019f0086747574596b8c3f6791686a3822c6d7fb4fc78110c00cc6f1adaf75581da511d9c762d725a63e453103065130b2be845b4791508a6403b527ef6d82e32c3a40fbdecd3054f812e94ca5a85038807e2f624ead5c10583f6c0718991cdf8232b10aac936dcdee1779ae7df30642e4fb7e2b750461c5ce149dbee8df58ea9afdd10172b0399fdd6ef712b85413f83ad83c71b77930290b4016ec0ad73cbb19da629738385edde3ba3384f815ac1df1cc4f542f1b4e3cd5c4c54061262747e2e6de2780fcb3714d8665ccc55cd876258d6e1fda2bf4781c06a4dd68508f3dc5781439c3496e529a7dab408397c01358bf44dc3046a188542f52a15c789fdbf85e7bae92c967e551d31c792bbc9216c9622477b376a92e5fb64c28034294d70ed6847b736bdc10acdcbe72f685d63ccf075cf8a743dab050a84353fb78498b17d1d438d46d12387f5275c860af69c86acabd7676eebe5ea194cf4904c83f256fa251abb4bec15ae49b3c0ea6a12e4732f9507356be2fbe20ba24e83f04e9f073404a884cb2cb8901424c3979b65b97c2cb70ed9c59428df59c638d1e9d809c2e8c9cb4d6812a43a449c778699d18f5268f0a81fd57dee3aec000765b38bacd74e8fdce05d9b8c39918a07f374f31b685e982db94adc7797998321c693701f2327dcaf9f1431445fa0888f044db3d936a738464a07089797fff8251c746bcf523f996fa926ee1203c062044fe95b33b9666b6cac06dc2f494e5c627b56496a084b3f2cb935b33f8fbbd1dba28aee5c21a167e8364c200f9631e79e5b375e11d543be2e25d66ed7bda88b0712084622e8e28cad9b1b380fc81c84f27055dd5dff703a0bfc0a361c14e623258129fc991c2670f556c80dcaf7006d935bbc7e892d754cb9e8fd73b1ec61a5629017ef48dc2967b1faa8219541e368c92e235852b89d981ebf3bae0a6d115bd933b1835edad3cbb6e6f683f03f5ca9e2876fa2911a422708fbf221fcce0502a4cb102f697f97b2ea29dc703643eee644193d76ac79d721d8c8010f5b76a1bab1e721ca189d001fc1229f45c6bbf08e07cff94a7df53ba649d4785cb1df98abcc70af70b1062a56d45bc1d30c35d32b199a2ef5dfc5c8afbf9fec7b3f247d97c5153efd3d1658519af355b296d8837ce00101dc35e137ab496902bf1cd522ee37828f5da6232405a73dfe9ee6b47d763f8045ac191a06fcb0b4af6884088474d560af68c8a720be5b849c07ab9b74dfa9e2ef07e2afaf5a9eab29edac4b0fde984b7fe0fa5a88201f49ad04376e6c0549737d864ea1152f3664bc8f068c0fd79663e8ec37d638439f4777184ef95ba748ed102eb3b3a20061f278bd4b22d728faf07df78f8b678e9e2593957149fc3f697a65b8ab3692f233d5f85a2f2c601e4c6bbc20c265bb79cbd5849a5bf34064c8db42a91580310a6e52a089c4e1815474f58d7d4009d52f2126a3e00e10aa8db097b8e34992cf7225e828fc521f8f329fe59f9e687322af4c0bd9bb72803095bde1aa73c8f5500f4d72bfe54953b07afa6cdb9bce052b38edf3b2d1401fa731586667852c51bb13ab8c588f93bc45729eeb301213cd4bee3a29cdd09817cbabe6039e757e842f953409c413d6c662168b7d336e0268c848f5d421e19411b20770d5329dfe11c1320969ed226288f889f297ae09208ec7f379d52f5deed71d1587fbc146791eef82243ed121062658286661e1caf98fa589c12a999e5ce6d130388c10752a6d0435fec28c50f2338b5ba38dbe2157b409cecc4d9df3e7afe8e737bcea76ea23103ae6bb34d0004281f3df3d53003718f8d684862e2c4c9246bc22355f8d108c8ab666d0b49b8104cd9f910d207f27f3373fc5ae087fecb28fabbbb8d71fd3eac057284e81141a8fa303061f31753a901d1b2cd5037e0777bc8fed1d6bb4bdc7955a364911157b3a407e4b487343566be51c66c602ce2d3ef96060d9c535105f102d5b121714411767c17f3e09d176bfb49d882f045abc373f227e7f41fdaef0e7bfca37fcfb91f76b1b0c8795187f3b29d10cecb963a2fd64af006bd9192216ccbfb991ac6d88c1e6e1d93e1a6f16f7001115cd08574e1fb2ffafeffcbf7ffe9570bfce10d582844120e2e1485130eae5ecbca8105179e337e69fd7bee6f34fa9284adef089079e0e0fe26a2bcc9c85a3250e341e76be5803bf999c03c35a7bde508be1716c2ae82de00df8ef7441438156b854b1df8f8f9471db744a21a65e9073e88e134fed8dffeef7ffadeabfc96f62578b971371503aa85b76717dd1a21d4849fa0a43f0ab021da3c5cd3fd09241d658e2264d88de03c9feb394189ff5974c477dac7fba978172ebb7bf1fa3f2d6fea9ffcfa151c9cc58fc3c2f003b6d0700444b8758b970bb0efa50e7c6fe8717171f174d72b75bd6a63eb4dc7af1e083f400afef123104f562a356e7470fcfd11df9b99bff611e04f7e29b9363af2ff46c935898b85787e55c9b5ba93d01ad34bfd8a6e291ea4089ddc4f85470c6e47bc237ccba218bbf7f9358af1798e60b40bdeffb32bb8799fb0162b729d7046f3fefd0ab261a9263b62eb5ce11575ffadf224777e4d56e14797e3739f90f96ebc900cd5ee1592da6a7945db3a507cb7c3c1dfa048c782888c18d6a6386119f309b291e1d6a0fe6d3a3d0c19cd73b6d0187999f05569e9cb39ec81d563bc7ee69d640e5bc99829346c7c2503e9a12cd5aa2813dfd0c5fd47dde7e4121d216eff176085ac5bf14f02ab242b191df677284b3d1470e590cb51ebc739321dcd47c07b75ef86bb06e28addb1e8818c3de2e849cd825b1bf65838c24fe26eff0e56a1be2a877f4da0d3be6c47a3a6bd5955848dd9a7e8776b7de7696dbfc8ff02ab5cecc2d0363337f7a13630737080baff8efbff0556d1f1d6b757d0b25734e4b3d5d1f3f4723732b4d7d7e5355773d5b6b5f5b0814adbc8bade52745280f2cb7aeafe8187dc0537dd3f00ac6229c46bc9cdc9cd69c12928c82fc82d20c4c305855a70f3f2f3f0f170715a0a095a5a4205ccacf8f82ca0e6500b0b0101011e0b0b4e6e4b6e7e210ba8a539df85beff4f07ab7824a588494b0b10cf7f3c4e8e4acd1583322dc49b66d92a27731343aabaf9e3cc9bc5f5030a5678700c88ad9cfce4d69dd8971dbffa11050a3eeb157f2018ea7d5efee42f814293ca3f2150e8fb9881f561b6958acdc349a715896582606e7d86b3f229f780bbf0e5b8af44caee3d1069975b3bbe9601e48a4893a3325f95c040285ca8f72cdb32a15dce440672214e5be5d13a4c627435d65f3c8270af252a6e2702463e91f4cd26d3d6ba4c2f73b30291a025171a333300914a8ae449a56ac7a96e841f7a799d546c7d74880170e4d994add47dbb3f6474653747b64272b6982dd90cd8ceb3aee78ff0ebc4897dcd6ddf19a1bb9ba6ef3c0d0412f9af50aeb5c9c9a7e14a56a328b23ffd60fdf53df07dad3e2ec4d5ec3b53b97b376372d9645a592ae9002a85db57858a5161e40af3454241dc249863246b5f01477020798739f9cbfc4cc2c8969c448fe1451925692057c612169b2bf6ae1967efcbf51c836df5b0519716c091cef0f903536ce075af4af169e241c137c97b2b5b1d67e5baa15b713b536890b9e11059b9862be82ad0352017727fc7d5d5ff44c58974b1c3a73d3f6a8065441470b47d0b769e7c3bdd728078e7b0fe4db2485feef559607c765f35da52393615dbedf93eaa60b3636d783af519087464c69879a653b311166e5f2f30e116ce110e05aa4c41f5d3959bf60fdea5c8429eb8c94ab6e4cbc600b96e4d928b217673bbc5374630297123e12dc21c60809e9a5c21988a98ef42212bd9caead71f661dc4300c1cb7410acee17c478c661c5fa79a891b89e60a87740058acb57bb32866274fe9645ec93d2e41126be5caeb002c76efc9bbaa5135a5d461b1378f86fc6dab226f93b3018ee423ab1cde785ed4b7bccd33b9699a310c7b4b4020ac22c9b565ccb1a130fc5168e2879400aa8aa831c009f3a5a778962d5c42ae27b8e9168512d1cc269a381068bb359e1d682fdef17cb96c3a7d5644c64d54ea26a02ff4bb3e27a035f057820a636f2b0ac4320971dacb01819eb6fb52a4d3195b412706d482a20cdd9db78a815caa4aa4ed279b142161f856d275be69728b32cfd280f3bc5849fbdd5775e4c71e93256fd3034c51ebf227815c12c7e200fb0e898ab03a53d58277684d3716504d810ad38b4314d42e12c3a2b3b8ba1d5c57f60417303a815c42383a3fd8b5b9af6bb89aa11b32ed99ca1e90bef4b3e259fa26589cba8bf18002575c345b547b54622f307f1ce8dc37455bd3896e0fadf95f0e7bb079a5cf0ca81297b866fb26093669b8eb548bd0f3d9dbf97029c10e7014f32ad9b8f513b7da0e0ee3edc0159b98d0730055dab0aedf89edd2285dc7cd152660b67a269089157c07f07bdd81375c296c8321bdbe7e2796726f4339f308a8824240a4b676153af881d5e5dafd9d0696c169fc4ac0d1fbf0155e1f5acc4ce3f64124da3e973f76c7ea6b0f2050abbeec94fc54e5be47b6427061d1ac34276c02700259da9fc8520c85a03ccecbf986cbea83fd598804189f37c402bba2b06c4433c75a2ceda0a688eb6223c0fe309af72ca80c8d60a6d7a0b95078a6ff4b834e2f90abd1d2d94faa96b02c996ceb4178fa410dd75f2e12a84287f0d82c69ca2fb8ec5dc455abfc3df7fb6dfd13c0fe7a43ddd5527e54d51e9659619be171cf9ff4da0e10a838e5140fa3c0f1818938c7c223374ad267e01801087d9da6be3b01f8de5eaa342cc481f1971d21d12b4020e78a6f8ac46dcb28d2d2e15a56e1f87dd8651f56a00a559ad947f8dac08f8f67373fd13593362c7e541105d49198b0d87a239de3fc14a674fc3917c41503b756607eb13c9fdf17213f745d2c822d88b4a70bd10bd000e37f50e885cf92f34d34b85f17975bbf546268c304d0c78bd7f7b2469c48e55efbd6cfec1b1e29247c8307e6ff665083c8f3f9a21b616c637d6c81b5484675e48440a08fbdbbb5ca10c3a49095f5f0d1a358bf9de91400130dc551b2b84cb2c6e78b02a539f98cebd40cad01d479f667b9fd3b35bd53f909c43b879b5fc7f71e65f59c95533038f46e4abfc4c6282f503360f640b7798202a8b0e68b9a6921a945b3095244e33d9a8725afbb48a381f9f774b58f272e6e435c1b775cc6f05e14a11601902b7e8bf89271282ca6bfb0c921fc744381803016e5f159b95bdd2955614feed7fe271ec129a2d2d3954771401543af454d34b69cb44b1cc56b836f8550ace099b000975631d5a76079861534ec75c9ae72f8f90f145fac805c666afc139ab8510a41d36c0bcf99a414263d761b607c5083a9a3677b70de55a4debc666e6ce2c2a649db7d56ceecc0cc2f41547aa8b7c0cd911bde689ca2900c24ccf890e24c342649542e70cf515572ecf2d68cc901408dbe1bdafeb86e8d32af78553f7d66d755bd54370754d027ced334f46f52aa9d20c1f00a9e31a3e9b27600fa87b62f4e2941b6efa82f85d9afb5786c1baeefc01fd83feb8b197293f35af3321333784ed75ed8898a02e72f816f78e7ec714afba3c33ca8426a2c3d638c0a800468eecca8d4422b22e3191137259e6b2a497d7104cc8f503ad5a353c4d4d32bb189e66cef37435f199e02ebaf82b9ea49c3d3edd78f0d7937663d27af7ab27b00b962d40223a56d6832e6e523979a11b13f38320a170240a38fe95e7e8fc542dcd7bf30b6412339cde0758980f619a299264967359fd6f0a8f4a4ebe8910b676900817ec1fc434426a1937409668b9ef14ca41736f22b00d0c2c54c4551934efaedb5f21b11393b92246e2ecd009022aaba8fd9baad514ea47df5c504f371cc80d70100f4418e101b5ddbfce489514fc716e6ccf721981a09581f7591e1b3ea9f175f5fa31c838fd72bab881c6a04d61fedc44c8c20650f5576d2cc72cad1b74084e60e0088960aed1d92ed8299e319d3eeb2bc926e358d0e00f46b4f24efab89b20b871dc7ef13455b6bebd55a970fceca97ed29aa97df3c18c0b8f7c193acc378b566d309b00cc38d6ac488d902f1313c5e0fb2e32b7e5ce87404e627abc850f4c6e6e7ca3be377c327705756a38c920020015bebc06b9b6d4b5ab7cee95dc785e6781b77732057d70d319a3e138587fe2a1a439e3b4971974d7115707e8c71e9b014111d54f407f49a6dca3fc157e105d43bb86f62dfa64691fd6f25491273f0a15eae7de5edb6fa8b396370f34f30676268fa6d3b565e2856143dcf5e4da20c5b3274023cdcb8fd5f14982b2d970774070f42eb8e7254505600ea09fb302f661581249677f6a5d5811ea15a8f46db80eeac9e1cb33bd508a77eed412b3464056168dd68038afe5562cc15bc4d72375db926b53036b2e35738ea06c495117899d9b44f7539457815824d792fd9bc791607e04e0ec88634043aaf3536609b1d355e1e8c7e8448016c57337b399277d204d3cd4bb311d71d0a12cd8a08006a8d70011e41e421a704991b57792cef4bdb1411cee702e6c86b01c4b0217ada294e3fe7ad9aebb6c98b70d567e534f0911d97f65d19adf077a1f34891665a3272c0718974fce63e254b57fd87f558a4b0b4cfaa322228407cc7f2aa157b78553a72a4a6ba37e52e4ce500730248ddd48cc729d7b3e825987828f82ccdaa4740fbc35de0fdcccb456a5e7526bfe926955811bae6c429d0ed06e05af45a847994b437190b12540b4d34dd22dfe19300df1f6b56ae8aa9b76c7ef7e80be75b565314f45e61c05c7c648745b44ac43a40f1ecee5ca48a0256823c4a21a0ee8ae5495c525f6d2fc753588d6ee14a6fd591078ef3f49531f706cda3e53bafeba475b54bf32a5f3c02ccad313dcbe247d419f2aa91dd83332be316ba271bc0fcc31f295a3d1a8b7fe9fa423b6a6d4a55036dba16380ea617b0b688387027a95c4e6432d4bd2570df63eb00fed989fab974a3f147ef75e3f3dc7d38bcaef78801a97254335d5bfdd925b38da11555cab7bd180d9ce581f5f5dc46ea523497515b9a54fcd55e2bf42652066ce0b8ad7008bd938f517b02419fbd3ee0ee4351dd1d05e09e887611a956adb61b4995b63ef32697cc8e5e4506b266e2637da9bbfb0d6cf00c058d318acb7a4e269481f119b9bd7badf85982c7d53d96b9a49114f9cd9976e0f9280c70aff72cde76f2905e13272a4f33a293d003d4fd69a11aad838c6b1858656b77fc903b1edc6d8e01fa47fb5301bfbf8dd1e71c59eba8ddeb3d4aad5ad200ae6c2f4f2e72f1c194c6a78f631bd717ca9cf32670c2cfca9f460e2bc4ccb7ce9a3d7b7aa96c0525cbbee83580dbd2129f607f5432d892daf2d27725fcad3465643f807bb582d8d3729b967e40e976d7796eb84b6b12100dacef0226d6295dbda67c3a1c925be20964845bf488c0fc1b78f61eb9a492d64c6038ff15d471fed8cd28c4ebac7c0857db41cf647d1c1ed632f9cefcda76546509805b3edc2194dce60b9e224d76263675eff99c3a920f30879112924d1b36bf95bc6aba7f87be724de39ac30210523148d616e5c784ee9e708662e20d9d449e2ed501fb2f915df495dc2f0cae33a61daee96547a9941d4940d60c4516e4feb3f2c94afd13adbd5cb68fa4c28a6b00358f27ec9dc5ab840ee960bfbbd7668cdf8684ee510311d2dc6a232c27f80a24dc176b823634702557e4cc81a28ae1f79c9ba5e1a9f1774cfdf727cd275ad1c83e03b8a54fcba2195374d6eb07ea7747541a57df74a8c403ee0cead25621fb274af897da536088e3b77c24bcb0017358289377280edf524edc60144f40b19516514b0448adc7ea97923c21c930b431bb99ea0c5535d22647eb3d2b2fd3cb251896d7ef2ca7cc1b0a752358d0807600d109ac4b2b348dd4bece290f0225221586da5b2f090502e68eb4b5dcb4ef6a50c1fdc1b8ded55a1a8ba74e00ee575087817ae06395088f9cbf496630aea47a9f1270fe5c2119bbdb247c59cebf8adf0243b39628b497127027dce79108ba61f8f4c93d09f4e311ce7aba5ba1d2002e4b0f5b583c47cbf120793d432c0afb0e14abff3e68ce75be6f6d913343659350bf4d2c75d03e9e130bb8ab44b226bd3f291d39a1585f3f993727bbf3d14f0e50f7ade3b2b087df5948d76ba14ab15d4f5ebbc6d30eb4df463c9fbf8d31a848649e56b8e118a8bbb278701f30a7f787bbdc074e2551ac1fa1c52c67dcd38b4173382b37fcc08c494a16f35289d8235ef03604898db012787e855e43ee129300afea4bd3b001444fa1614546b068fa827334a1d8637fe31697f2410d04b5b0895a805a85daf7b1cbf18cb22a5ccb8b8f283a1dcd74b66aa0ba972749ab17fd044b310cce748bfdc5c7c223262086beaf416b5d5c4a3095c4457f332418dfaf60e723408d30347fb9276c3a3ace7c20fd6e546b40009e752c707e15bb0faaa0ebb068ec22b4545e6d68c44d1904a97f746e090584e2ec26b2db0fb5e776e5f530cd1d02ee4ccc2efca81c09375eb5c88c7c671f865791e44b00b54509859dfc099b4e051a6b6ea58111950fcdec47c01c1cd47a36d8aee7a5906fb5ad96bb40befa1aa710c88a935b4c5cd30edb7d8f8cbc97655f1f2d757297007087658b85ced6f619cc154d12aadd7019097214140f01d4f5871977455f1558e13e34d1ef69180d258fd00362eeabb78e31633a1fd00adc145dfa3a475dda5ff30d304773e79c59e1bd25d5becd57bd4a426dabd494e3008a166f7721cd730dbfba3baf5fdfa8cdc0af19c4a40a503fd1bd16d51b237b75b36114668cfc01d97b412d12587f54161582cde29795da75e1cbab10135f6e9b1502b87e5473cee64a2e077ae5c7b94c700ff6b91506c7016a36ec840f3911bedc71cece3057e614a4c541fb22b068f7d7bea76c576a0abaf9cc5693bf986a9942b4005c63dfabdd10e3de07ef7d07e4f8995fa6bd8fa24e05ceaf4086d412c22c45965313493763fa6acf2c844fc0feccd5a96429e9916826c01c738a23c5299f3efe08c0053f8e5052926ee299aa27d05819788a587723c80e30b729a3e32a296e93ee953e7ee0916f9ae225bac00ae006d11cee2587eb318d85c7bdc897de808f6fa19202cd698f4223de0a885c192f656585cb78637cc024707e11a03319d42955087cc6532cd7bb82fdf59025a0e9ac5c31ec738ff63b7acec01299f163bf044d4df68eaf67e5e3f9474145b643b3f6324d6ce5fb298cd58ce8c0fe31cbce805554db1e283bd551eede147133e4ca02703e9522922a9d861f5d9fe1532ac661f0c4f064a7058b72cfc4562d7e0dcc3092f5aabe77d42e0ab77803067f04c1fd2731430304f19abf7bf005236ad9e7ddcfc3c5c32fc06dc16569c1c90be5b61412b2e414e2b7b4b2e4e3e483427904b82c04790478052d05b8b92c84ccb9a0fc9cdcfc9c825ce642423ce67cfcbc4202e7b52f6826c4cd07e5e5b4e4b3e0b330b312e2e532e78742fff6571e7e6e7e6e0b4121213e211e73a810d44a50889bdbd28a971f6ac12768c1c5c5cfcb67c629d4027f04cf05fb2fbc67033c5df103cdb10b1a7af017eb54c879ede76128f5bef839f2429ceb87ad27b769f6903846fc408cc52a36b9923e60f6d3c2b03c95b30a3fc7a7b05eba409832f4db2d6fe86ff93eecb84e1397489aa17c5d69a9cf828c814a3084fcff7ecb796427bf850ca505fe5007762671f2df494ec4a521c9ec4b373307706ae6a473bc22a369e40a7e36e452d18a2dbfb48abed20a17863e332afe9d56536af7aefa527cf6350a70594a2e2b1297a8dafd3950d6157ba7e5175cd4cb93ff37705132ff13b828d2803b46a608aa4c243ae45a3e72453ada8403bd5a0e6e938c4e1553fbd935e73115a15df0fe9fe2a206a80ab04b71dcdd6627c61c5625cbc72f258d2724284c0b77486088894eb94c13a10864dc9854b9cbcd13d3ecb92e8767e31f7f579f1125b8752d5fb741d6bd89cc7142e852540c7d6f6db5dc82e07a397db82cb4869989ba9aec6d71b2bc9521334699c89b83db90e18991857782f8bba48e0513bad3986cae2e9401a1b77d7c1e9f878b4218fa172eaab801a1fde2b8a87f475b73055a987d99916ba8a99452cebfd6b8d3390a7dfffa74b5571349b9889eaf2b2cbe8f5fc1fb26b5d7460e86d33e0389cc6fc14549734bdf6c5ff070dddc42af71a76d24bb81ad98f27f1b17053ffb172eea0fc245590adab978b919189b1b42a5cca4e5750d04bddc14b9bcb47d151415b864f58cf8b5cc3c95dd050ded8d0d94ffeff4af1479f43a2fdcf881fdb7e72f2794a2ef7f3b85dac679c588310dedb21242bbb09dafa8b3599e92dabfd0546392fa5831f38222ccf1c96c4411cd2573884d83b1610bf9afc2fda8eafe098e729c94d80ee81edaf03a242963e2d9ec088c45057044e0bd8bbb828bad7f738d5244b5c05f528dd6c71670a4b5e6d4a46bf6293ca2b514c7edadef2e26330c01cec0741ffa46f9fae5f82f26345f6128f4cf2be55e0309861f5f2e4c5274eb5accbdf6e2ea1ab9efa9a9170e181249c38cdca4e65a71b3eb7bd5ac88a6ea9c6ff2b901434b1d91bd60f3c497f78a786e0e520a6ea6733ab01f7097303e5eaab65f471b207d814678a79b8e2d1370246a5e69e88a8ab23f16df8c8d8337b32c123dc66b030c091d62ca82e7b0b2d3fbfe5568cf0a4f064b0580e49fa10704228f23c5bbc6116374c789aae4281988809989375236945be73f691579704f3660e07dc26c2b60a8245cb6206e6576abb351a44ca3c68a2efa94960f1882a11c5adb5f29e74a6f5d3d650e455b875ec377001258354360f78e56e4c6c782eede50a7bb54cd219c3873568e78e5b954a9378aa374e04df8963bd69bc7be44c0f763bf6bff70fa9cc3336e356e92cb9c14b3e09918e0c8a8a70957eb20ef0f30ad984ee89bca21bcaa2c0ee06276122021eb61b0e2b782c97224adcbf94411e286c0066f1de99168ed1285f97a6ae939bc90c92d4c23c0902251ca94195ed1800e3ed1cab2204db9cb11fc0970d449ee3dd1d5978eef9621d93e89bd072749cc80277f562e03c133c3aecc63dba7bbeda86159574027b901041af879e003d6a88cf554eed3897117c9e25bdeb705080668ad863b5eb4f8a9b88f2b3fff6cbff1ed91e602c0012bdaefab80992f1b2d177d2ff3a5b2715386df34c55979635f884a6c501a29df33629831f58aa26b8001906a71e7b3968613af234ef63e7d39e445cad7c6ad19c0d1f074978bad2506ee5e4ec247afc71eb750399cdd81f5eb279adac71b59221e8dad5c5b552ecb98d9990c101cc892bdc63e568a8423a679d97afffd53b2cdc911202efcb1eacda28c0c233fce9231161c3d4ac651ff1d4cc0d173d76724a275a4196b374f77da224b79fde91e808bf9e88156c6ef5be55192b72284782892e599b60eec3facfe7a37949ff71918bfa27492afaa94e425930170476326922f0542c66b7887bd74eef47666325f83073884ad3e3f799339bf99f978f449dfa7e6e4af81a259c071fd86ba842435ab1c114b01ebc8ff680039530017e87fd7e8a8595bcb26f4b5fc5192c2c29d2a5bb5b5ff16374fe1b3efb82417b6d70f798ddefc1acf0e0624d7e5329d7cd1a77f8bb092795f63522053a894ce1fc02dba8e7bf7f43c7ce814114bd371f8dede85fd201058bfccb00771444fad0231646e992a4ddef8727c87102060b031c61b5e56b3763b0ea2b20c1baf6288c2c305d48deb30fa49bc838eeb6e6bf8f75711e211d417da801a02778447d2d0bb8cdee6b267636e3ea358170db603028118928b54fa4f3c5bb375a2bbafe59117e363db03813a8bdb8bcce8ef36a72be8756b18421b35c2bc0c40474734999ae98bd7ef877dc3f7df6158f2cd4c07028ea44dde01cd8e6eee4bf72489c495eb4889b5d2ee01b8153fa765923b52541e35621e9eca3366fc335c91807e1f16275ed0785bdf378155cbd0ee796d9bee2a0d90003d5a7ec22b819090d9a343dc4cb5351a7258fc0a4830e73819a4735559bb81d1e2af9cb15079efd3bb500077672815f73e56362dda0ee701c935dc775a08f418c0fbcd5cfa08d96b6a34ba2f43d3a329d56d347fcf1f08a4c0123d8290d112529750a271e8dad8f910beae0181aacaf745f25158965adda67cb4c798e8d6cab85f8140009e24749fe8a38b9d6a53af56a66954a47aec0c00525ef6a24f7ec6493fc43324c88272fbcaaeaff555c011e9baa4970c4fabccb5961c46352ac117a4ef36e30bc4e5e9ba46624d1ef4609a324bf5f5b63d5a23ed05d69fa733c7e8888a599308c76e01a24f1145497901b0beb3c49a2c7467925e285fc27c3171b56e0599c604581f29ea89da4f9a6d718a8b2f1d8e27941bf122801cf1ac1c8a86a7ea65b753de3f2a57eff6b8cd54af021004ad5838bcb9f2dc5bb6616c43d681279fe89a822218085e69648ca57e98e491d5ff151165d16f832c14c0cd696e6e8d3d59d8f027a936412ce97c2879dd11fc7e4faf23229741b811ebae23065b34379a9b63f3ef01dc47370f3b668a6123cdbe6fd593c7fd8ef71ebe006ce0185d497716a76a67dc3bb46653da0c6314312800ee2eab665a02e5e1db78b51a3ed2927b93cf6b25440047bf869ad1e31888805b8bcb37e4b7b0dc8f2f455381f1ffa4ca2f4cfc522fa4719bfc39d39d006a22351d8020e39364879eaa9a1743c7420301e3b29dbe9fcca0f3597924e57afbf4cb36e4daf1c9d5729129e1b608084040e07c6330a0d62b737da1358358ac76217d883c17709433d4f0b0adb2d70dbe9124f3769cc4a3dc476f0602659f5f2e75466bfa22904e0fa493989b848c24eb02eb6b717f606aa6d45be04d78e6160bf33255afda20a09fad07a770eebd0c76db22b74f8b386de28d24090254fcea702129ee1bafea57d3487c10b8a7e00f3ff301b8c1eee2fbc7bcf25fae1d1d0cf9a3a65e1b78549d07f838aeaa47f6ede06a5569e234ce0f8a8c1c6e154c02f679680932b7bdc52b4109f9d89dafa8ad5bce32c800ee474767d261ecb3324159fdc9f44cb779755aa6f6d3b3f264a912f6e7df866f9064a8a1e6e7bd1ff77cc38d0a047af0478de834a6aaa3d84226a2d3e762b007219ec0f967e1b51b7337f036d3acbee368a40122298d2fd0bf56b8e39fbb702da509516a253416633fb7242300815ef72b9e0a97d7164fa1349b47efa7543a18a6df0104061fb8cdf59ff86f88706f2ea03f4965d935372b06f4ef453821d1700e2c85cca3222fbc4b48b1454889c0fe591eeedfe97ddc09f3d426372f65aa0c73c60dc10602d585095bce46289cb8643c9ff513a3f11936fb0182818503e9c5fb19f4c9abd569c70b0192a963d51840206df863d44854fff003f172d36d9eee6d36aa3b730010c6a86582efe6edab75a9be276108b9cf59ee650e80b822da6c24946f45f5dcadcfa7f03afddd58072471e18f2006e19788d55db3ffc904d00091d5d3bc5a704177f97949a21029195d250df51b3a7a9a9aaa4637d4a57495f4e56ee86aa8c8a9ff2a43efa2de39c03bc62efbc1fd6044021e2f58b817b785147724fa1a50447403be1a5b1ce93041bb0557a6f0b0c5dea1ea88e6c74dffd29d2091f33fbaf64fca8d5b1ee76af7d09888dc65294cd4cabafd60fbfe425709cb8b177ef32554a8c89af3944c477b0a033166f36b9b02a4faf3bfc507146ed7a0cb8a6df26a2d5292e41dfa3381d6d1bbf0ffb77d407f1139ff6144ce3a5c6e722a8272ce3ac6da5cc65c960a9aaa02eacaeab6d27cae7cdcaa827a76e642fa3c16ae76fa5a9e5cee7fdcfbff1fc88d331312b2829a5b710a729959f09a9b5b7259f272720a5a7271715af1f359f25a5808580a9a9b99f1f10bf2f3085972ffedd742e6bc42e6dc961682568250a10b7dff9f9e1b77bbeab361bac40bc93decacbcd1a9b1b601fa6f73cac672a88c976d4db3f72b29d0970cd8e3d73b5ebb366c608ad006d4dc0daecb9cf118afc1f1ce8ad75165b23d9fc8f9bca0d1d06dcc43ceb75bcbd053547cd5058d98778de63f2c518dcb611c571a2e09d772a80a0ba5e8837ea7b99b25d24f9f578e7d467f6f2562a2054bc680974dfdd90b22edbc4c83df828512a4757cfafb3f797cddb83cffe06261a139d5477efa5f1038643b32d265c88d7f7cd6bfea1fba58fdb4fe21e91bf684aae63bd1df935dbdcd8f1160ac37557463f11300fcfe5fa87f98fe3df60d0278f63506a7efb26fd0c560427ec0bef137d1f7dee03f857d031dc36793e7276a0dca3c3aa6ba98fe7983f41fc5bec18cfae39ffe9c7d0322f141ea039784f49fc8be811c6b41cdcb3418f207b36fc01f2169c142a87737fef65f0392ac3c67f6d8ca00194356b4db9176d17beb51a78ed95b34b964effc869e54abdd3aa8990eeebbd889041e884a949743a2782d4c49836061241472b8358329dd3fbc55a243ab5f812efd82a1e20bd473f9659a707a73fecaebcfd718fe3ec2577a1cdbb587a2b8a461d987a1ccd0df224767b548d711c4df7d92e12dd0b3cfa0f326f934ec42ea6017dec3737e327bb1ef878f4459a3f5f92dfbe279df781e13ff79ede3714dd7eeb967eef7dbcae4783c7abba8784bbdf477f7d1af9ae3e74ea173b44a08edbffd05090e6e050e6e040eee2e1c9cc64fac160937458b9653cabf6d43106e29b720a54ad578cb09822fd4928459df1c8704bed7d952df3d65e110d949fdd5cefd96eced2ff656bfa95882e4e56b827dcaff3afa9030899424b8ca5bccbebb18a8ed55661bce5b26ffbe09240efe904bed2fe8ce6fda8f7f0d74c7098bf9b2a8d7f1a33be874b3fcd647210b5b118af3aa247315214e376cca38f343ceb99fe082f7ffec0a26500b7c1eaa797d52867b80a6a045f7f8d23ef363d3cbbd06ccdbfdbce602eb848e57cc4311c414584e5f8ce22324da656e691aac61092cf36af458cc094e2d5c27173799c21c75241b3aee88c30889b89795f08d6a7d643f278c8378ff24893b4749acf821dc9ed70c2af693b7fb06f1d54f57678dfcd69f4975cdbbee3930a7fb289c03dd39b4878550d70fd75bb230559de38ff248faf1be6be4635fdd1aff4339218c9ec58167f30882010ba1b24c6880206633088aff91faef77ae2ec025cec2f630dadf65b063dbac9b208d947a1575e3f2cfe72edb8fe8b8242aadee27a97cfb895bccfb16f62c3ef6f17993e308b20a0b21df2f6c807c64170822bc98b10f01a0f42af54ea3aaf0f5debecfb96f707ab2ca7d23d1bff95be4e75dc69f0d3c92b29ae80d7a23a29ac88ed068331ec6ffeee1622aa99d2ef9c94ec2f4b4e6704745fdb77cffbf5d0ce43fa537fd35a5f22e6aaefc32dcfdc57fc27063a634b2d772119b5d39de97cb5dcaf9a8d23ebcf55be4ffa3c34ddf878c61fb9387d1bfbe62c158f6fc82c35df5bf3ddcf087fae1ee018ec13b6d21d8d2922114ad70f5b27b6d86af7e7e57deea4f1d9e9b07082df047f05bb05fdaac7ed100ff311be92d509e6300935ed369d461e41e9d694abdcf69b2e0f0aee705d5880baae53f5739ad1cd8c4cee9caf3f626ea700c744296fc7a5b882137126640c3728645edf5a9b5dd45d4c1d5b2bb0718ce175389cf53a32e03e8808222b75bc43b215bbd6c09619e09e2a685339047e7b4ffd3c964a3666a789ea373226eb016ad2bfa3295c4c8f11727b83889a0f287f3b4668cc872aa87e934befa1af82a840a4d4d19370d6adbde45b1e04d88acad70954d3d9ad489a08033b9264e7e41476b57c3bed5a9a4cc24dc6efb67b96403782b7cabc2670bbcac3e89908f7b3cde527e726b2e6c6e525f4ae3f16dd29df15f8fad7d3c0a324537b089094e76ef6e1ffc1515f310bd231070a363f45d2cff39f7112df354d317cfb9f5e6055550e0f2bb0cf39089857f9d315a9f86d4436cfab037ebdfccb56fe5b0bcd1b68709ba13990d4d38aaefce99fbc217991b7feb7b024f5ca9e937039c1fecfd7ddba43951db834251c30e4e7c5d1e33eccd3d21bf1530aa9fb1835d4cd74612c8852a040747c042d794a7b492371b90362374d7a388ec7f378801b96010037211f1f9fd7f5e100f8e5a51efbe634ea6cf27c623821df84df15bf8c2e3e9890341989dfdd1ba108494c63f77efa105fa077ec307f35daa30df71e86d24f1a9d2444c24bd0717995fbf627e4ab14677bd852faea19beb9f274a1a6d25627acc83307f18c42e72176ef99e2acc0b53bb6ad9bbe36093815501a903b3362c2e5a461a3ada21c633d05f7b9ad69674c1f927e599e9313f91a67ebd321b4799f99dffbe9a58d4221f1bf63d44e6b6cb3954b415f615c1796ed0a19adb64c6d7c8cbb4b67b5a632503e7cb6ea1d21029470be871fd8a208fe62fe72da3be4dbd6ce7e604fe7947ff8f0d21f4f937e3fa9bdfd9ce9e26fe2f6c67b36f0c540e775bfd2e893c5587d0db4b77b433bb5f703b13bde876d6126ce2f13ad321cfc7ae3ab2a7a5f0d00f05d6cef99633d5fa76c7d3ab2c028fe4b1a96a1d12b2659643f80a356b6a933b563172964a3eefe7c2f3475458a1d7ff3fbe9d895e743bebe365dc54dabaabdefbad516ce05927b9d38c8043d7e5f7513a5da8f454096c08ff3fdfcece9b9f52f3eb6d77a3fc3adfc488d1892a93d0bc24b1fda4d857e6ac701266632c5a705c87d83e8ce9307c3799f4e4284797b178837f60e992cb49dad1073b57425f4cbacf1755a5e8ba3c1de9f5623b78571bdc9d457aea842ccc6fa4917822d76f118f2993363828bf362431b7c2be0e5ba52962268b4e408eb8f70e56a25da4c60e4d6e6ee5463f7f3b1b09c7331ea454590ac1d8e20821bda91a0ad15a0ffedb96441c8186cbed8915291e82bab41342c873bd0111b18153e002531a124714beb7744e58e082a5532f5c78e3c7177248d5a022aaf8efde729151178311ceab6cbb887a4efb806b440683a5a8ec9ae5ba48543ac66cbcedcac3fbe91ee7dc7f5e695fea8b6d19e75a67172e2cf2dbbff90fbdfffffa25058140e0e08e0c29473c7dfebbff1c21781115f9a20f68e98185603355861ed7648bc38fc04219b5ddddfffe4f241c5330fe838d6aa296c3bbb050e4fbf592f5213f39a468120fa594b0c61b7e386190e884f32363e9dcbfe70dfeae8f09b2fb8fffbdea7b26f47c180e0b891f2fad0fa4817daf54f8f75a97fc95917964ccab9e24f9f36cc2526f19a296563001880b8ae018760cab71008ae0047a087b37d1110437ec4a064f9f42087bc2700304317234a9f2626ebf73c91900d5535f5ff9738171b7809657eb93116985bacbddae409e80fd1c5277cf8450099fabe595e2e7271c5a054cc9bf5bb58583c35188c1fa99630f57afe0c5f461092c84bac4a6bee0c3d5ea7a04023808649711e7d7763e03040ec2ca88f36b0795021191118711a7118f86868626e66e9e5cd21d8f6c67357dfbedaeefb58ef470790b331c8593ba5bdbe1e01f9bf211fc63580879665503fc793e9c9f683682ef3eabdafcb16bfe77c443b22f30b4bfc6237dde2b0308addf8ae03adff201dfffbcfef91d08348096e17b53f04f7eff73e3614710adb0a5e17c9c5fb2f0ff02b7ff77703b29db5ae050ed60da6baf95872a31d912af4f9ad97fd2f6dfbbf2ff88df3168e84553c4fb0753ca58aac8effcd83316b8a298feb70d751d2186b6e678375ceaf602ff3bbd779f982aa83c55a14a94dd1588b78f63fad221b3b2977e5550f4d665162bb584bcdc1bfb11cc96d73e6dc89324ca7b14eca72290ff65a85fc850fff4ad8780ba4864768807d1f39bc1573d59ab58bfbed3cf21b82514b1927095a8ffe986fabeff0c3ccbe3aa264c3d6bebdb455889b4471c47b89288ad1a4c452a0625fcf8cc446c2b6fef193ba3f4e8bacc4128f45a577b74b2bb387b42152239cbdf54ab5dd4ef985487aa529e8a95d99aee6fe53016fbe2f6d2d58da1a8bae6f62bbe944b09f3d1b4962a16737d03b3995bafa2aa5f9998b177a3d30e56b05b4dea7ce20fe2d2db26fbd57e47b28c8c7ad94ba685404cf37cfce6bfae2b5f496f5aec9327fdd2e43f7274ea65774e760002dcdf0cf833469e6d811f89c0c24d35d280c19de13982b5c08f462258c90d79b3c3817fff7f05233d679f9147f1ffb1f71e5051254be3f8bd776618d2903328a86444c928a08282a82888a09840328a18401441d44172ce3923410912244b18099291019138484609029291f03feeee6f9fb36f61961dfdbef77fdff659cf1eade9beddd5d595baaa7a8e1013f9e785c25fd9c75b0f5cd5a2c31ea203983779a170dd2cd331509c91e8306cfc9f2c81a9687b8105a0008cf77df1b99b003ed9856c3a12795d68c55fb81c88e69cac23637876cf402ca97402ddcca6928c8a5044b8dd1fcecd29d6a979c016506e77df98fe3ea5c21b297aceba02c7dc675cd3db08b9c2beff8b9f103785b83197811c40b35fa3cc9fe3d0deae3022655e01984e312c73895e977c5499f1ce8e5d27e6cfcd609a882432c3c30e7bf0cae65f97d9c6121472e9119da2afe8cbbbbbc7672e69e395170999333aec667bd5eee96945ef8ccc846d764e7d529b7f82eeff95d6fb85ffb1278dd8ef3ee495aff8a560359f28fe0728ecc02caaa0ed779ccdfdf75c1dedd4b0994b464552d5120670daf196557d83079b8784d9758a2ca13c7e318f0b500e3c7b1f1052516e7cff97db6c81b76d209f54779f97e6515e43593452a49b472e4a485aaf2eb675995ce81a9c0a7d77157958797b819dce4e7d0731510f6cfa4bddc6532708c18923274abc57c5378c3e258b29076e44b2335dab92851d58537e7111344c09fac053a008dcdaf29a09ab7d30a2e0f4b1b1449c84ddf2758f08a86d4cd9ffcbce46025e02a65c02e8c710e7840018c65f47313fe43ecb963471fdcddab99bb51666d5cffffaf4c9491154ac5e524932dbc5a62fcbac9caf387de3c9bcd69f45e9efd6f1a941a73f33cea8566761ad9b51ba29edfd946def13e9be8369d31328d57922cd27bc16f7d2ae0567b79def73845cf115ca6c0765e510f88f347f7aa6530676c354cf7ae034b47c0de5cec7cad4d36c05de490344146b237194e36ff4f456fcdb6a00d4dfa77386f324f64dd313047e4564c81929a9c259a38cadac614b690404d7005acbc6373910e991831347637042f9f504708c20b407b4c4799760ba449af0c056f677eb1e5342da378c8b38f826351cffda190188fbfe965d2874676f5ebb63a577fb9c38d1671cb4aecf27ed209287e2c99221bfc336e4e39f8407263e73b2f432da229c180d89f4c812c28fd09fd21c38fb379cf37fcfa54e648313e91d962774bed609b4ad78dcffc6fc627ea48b704bf20100005ffa7d78af1fa48165a8f34ef48f102677d3cff96a7fe4ae5e2194d94441e4fa3144ae9fe8db01627508a74007bc8c6281ce8685fab4d9cf2635bbb2cb769e8dcf3aaefaa76eee92ab26ecf02bc309daa8d263c41c90259819da9eebd99d0298e15f30c76e12658ec1c5f6114a442332518d90ea6df997ccb1438d33c04d49e1277793077acbe44f455f9d6fec75c8297989fb29e6188a48f8df6d2a27b4d2c6eb4307f437fc452609eadd8b3d673759da65f96b074fff3ceecdda5ef5367a66e3f9ed38dc3d4219f2b3cd317f8530875b25f605fec2fcf3d04bc6ec9c338ee37fdbb66c71413c399eb3eb0696eed7785b091634065a82cdfdee062f807dfcd15e63424e1f42e3fb64b656b95f8ebabbbf76e4dcbb68a6fddbf9b7ad10f2c36f8a85e3a7597fb2d758974817c00637ddd02100b80a00aa682e5e13cf863fc3fbbebd2bfdfc16679644faaabbfdf93b2b4bf985369601e756b305e69e1f04b8de1797deffd35d8c410e619a375ee64b003cadce1b2bd6994bd775b96025ac8237a67ec745895d4b357f3a1a7c96d9b89d28c41cd738bc158d841065fdb1195b94e96c859d12a2fcc797ef9d2c6258320e77b1b78d3d4e59cb5cce8424b8f7318468f717073bd040feca4cc070e1430e5364c78968e0a4e4ecf93ffbdc060ef63003ed1c0cb444a98eb647d65db3e7cc5e2fa0547c9276ae8e48858648839a90c14a42e0e091e1cdcf53e3a4b97229ef3394b4e55ad4e17ad6a481bb7c444a4b02f383144defd1e1b1f4ee5bb3a7b4fd77175be5ec3947578810e8d6e79d268e3980f2c07f75232431817fbb0788a6a2316cd617a7403dcf6c9a2dc1ead7dbebeb6dd439fb4c84fbda35a250a8803d9183d6b4b2b90b5ad566d198224cd7f358ce8a3c8dd87f3499bd9039eb201d935bdc94d4b6be3e292c65b01d07fb646d76994c47f8b2aad5b34a39c5ae0bcaec3f085332d9d7a364dd3430ef0bef8846fb6444169e2834fb87befffbe8bbe0e1ad44ac75ac7262d3d7b5af2fefa4db1feddd709f507d1414b9393d0f88a1efdc86834acf750b2f391e799b54eaa6cce4c6bbceacf6d697739525f4fe89979e1799c0d9b16d364a2f041fe62a9fd8fb3478c9c729d7dcb60aa27a5f22e6ff21e13ec70fc21429956468855b46f93ee8faacf1cac0c27ad53deaff69fafe3b22fc1ffade1a7d53b59e7b79d4a12f045eb37dd0a2f05d0f4f42dad94d77a9eb632331f4cd25ac5f8cac7d75f58ca2055d67a3e3b6b15b3220a7746b7d3857ff0b3e669097a5d2a831b5de047c0913aabee537b623a8adc628c36574b8d47338b2ba96afe947f16f69946560eea3ab32ebf2ebe377f3b27663ef1bb1fcc3bffffbe83b9bc1f5c3f9280fc3a38faeb05d4fca540a4dd15cdea8b3904b67589955132331f45d1bec86ea08d6a5a93c7bf85060f7a3bd46039e39d609a5b7f4be8e57eee77ef79a27cb663ad784ab1d27a7dd822caca360e29911c03dd2813ff7f017ed8cca162286be31d012c9774605c98f302a888c9bc7bfa531bf99074fd2a52a78d3cd1678e7d42175217e04f73f87eebf50a8b4b9f5c7a05106816fa344992e88c97e79b27bc382aa6e55bbe0ae1f899aa0c26dfae2cfdc153cc7ecdd25ee70b71a6999193986678ca304b82e4567362bf37e3a5bef4236975d6bf0e2fe81b29ddedd2e61f125569967baf4e51b6c6a9897e6267f945011d570006cb9bf0cdd7c745edd4e26237cfbc186e67fe8fbbf8fbe5f4514f579287a625dd5baf6df1e9d5f55d6faba2109c105940e830bb789a2eff1474105de1f137030dda8d383ec4d3c1c819a12cd1a7b5bdafa8abcee0b529718bd28dee512bba27e829d4de0fa8cf89ec03334422ed8c29ba7ade6d8071542dd3988132a64df0915b21f215460c4113da19c6a12bcef0b7a3f0a3d257f8f2daa20daadb9f4bc773bdff1da7f0ee5ff6da1a3a1784c72fc3c7142a79a975c0bfde1de5bc6d9c6f70ad7d2e73f88df77a7886931e9fd28c73a80fbdcb0fdf9c045e551eb1294aaaf3b5b30f562b2ba78c11b531348f66ec9e4e4b3a0d61f25742c8c33c623ca4e312c3e35270f5f7f58830b5490ff87befffbe83bedeb4199f301cb49254d776b690bfbcd35b52737da27b0ef7341b1f97e2ea2e8fb7085bf896bb1e21dcb27891aeb417c394b4c3a323338cd636c85346b3745156a0ff095089739560696665cfcc0997222cd74d45f7ebfd72e43d3e53db581c58c613fcc1365d02f6a28ac9ececa8d296b69f351988caa20fb87befffbe83b5d98f255f5cd9dfd07e58ae82f2f25cfd5234d368c7fd03844c2ef474f547e9002e4f5ae5341a7c3b55feae6cedd7dc7d427af5590bf6d0b5753355ce77be215ac210d73e0b6ed7cc32c5c2b6b2fbcb4eb4e55a6dfa998968395cc48dfd8aaf6b95222952a30136d8fe84bb16749552a00e3696c75e289b3b4f1efbd3ff47e8ecfab65b4cd5c5e3894ba43924945fa4a0681f109619350bc19b16f76833fb27fa0ee47be74a99a5382e532fb8bfad6ee1bd0b87411db1f032d4efd2bbf70d0ac32112fc42ff3ca552d85dd6795cef81ec4d564d18a3f3eff90fcef2ef90075093b67d241898de00271dc6cf3bb3719591935f8e10fc95c84bce921ef85b1b73bf283c371056ec3af6edd937f26d8f25b4a2565853ddf49ee7fcfd2fca5ed7c7707464d7ba3e7d37cb64c95fb534251911868f1141a694f4577235f717cd20f248e2ac1af1870097ed719e651611af20c70900dab050ae086e5e7351ffd6d72fe6d2c39c185e9f01571f6b383d1de594aa496399711ad67b699430fd2c68c65df38beda7c78fcf07423996394832e363e76fb6d4aac6ae777b21f503bb9c9a27c989c7d0589e3aff1997fdbaf075b65db9b949544007f04687b930cefdff04ba03f818d27f47c1cc14c2722c3f709d6dd620846cb77a9f3f2bde09cad967c375c71b83f4b87740bec8ce8ba52c4f62776feffcbfd7f7ac3404b0831b47d4810dae125a0905180e03e70c65b9238cf08bcfd1b3f84a300162d93fe1cb98b36b94d7da98cc573db04cc726e270605aa8c58329cf06df16166b5bd59d9d613fff263e0b9576454a4e1fedbbcf7a6bdbd2eb17d7479a9e28a750508f68c8558bb4a6b79143ac9a4dd7ae33371ac903a457acc607059a45ea66031f393e5aa6b40500ca975d7dc10f9d8487fdd99994c6057eb74eab113fa244de53e7300ec128762b26e44ab19ab0486bad6ee94e9dab2cf7aec13a959c3febb3d67ac92dc8f8b1638995c11b546e65f54618c7cab0b77a2a4a9101d6fd53932d3928e842983f037d6fe16c915e728c95bedc391b748a2a86342d1ad6594e6025d6a5719a81b8f9325177820e8ced41f7aba7ae94139d5d851e46e5f9d1ef66ab26bccac4306ed0300c9271e5abd2fa3db8ff15d12cf14412f2dbfd805eb61aa9e3b162db9e39072ecc085bd2ca891bb6f5ec30370373e2dbd31ba38455ba997d85b6e79fe347702bd85513f889c62e06ebdf830f1c9f44a788df004678c892069d53e717ac584b7676f92afcd541d795987487ec94c5ae40bd5e8a047b7c3168f69dd5f51bb8d220d6c337ff4c91524dfb77f8ff1fbf28e6d1cc22981a2fc77c67053088af46caf13be327bea016bfaaea35670523b05ddc37e4257a3abfc8557461826e23f39f6bb74fa8892a19e16ef01284db34e97bff0bb311328fe35c472e5355d181387beb1edd70e8bf315ad0f47040e6b488728e45b01e9bb42a2d592ae985f114cef45c0c92a0331a6068f904cb22120954e656fa1b6feb8d987150ce3ab37aa8851131a55ef13373a3347c63c2f615905ac52ebafcb4a2aa88fdaed18aed3bcfd956550e21a77f55e56963b15d0e28be90b00b56b64217af07e49aba02749dde311b2d032c3a6a754ddede8ac638509e1edbdf3ea90155bc6887967a62a4a93c6464428b0e2325970d9a9cbd726831747442b02419ad0c473ef3b8fe9ea8c694459698bcf734e21101a8e3674cecb76425e1e4a8de97b16c54bea1a9f0fd5f205ead8cea1672f9d45ab7d36b7eb5623efa7c89a30940468e9221ea47234ec36a84c616bf7683815312a65b682107dcf19814cd0380731b5527ca62b9e835d9870d25195d065c5369daf1b64863d8fba3ef2e0b6a0d6c1134e00dd2517a7d797488f2b69c7e9dd3c72d2c8298e8d6e4df94edaf57599973c36bdcff76b1a2ad25395f8c8b57c2edab142fb440fe552b84a552322f7a2c6c125ecba8a32686cc64bce6469e900e03d9041c10d31a8bc344e14c3aad87f516ceeed676ff9f5212238580a1210257c78711118c1e4fd59bbd63cc9a92eabf18d7690cadd352bc317b66143240d38be9e3ebe2bd83563bea16e239edb7a00deddf3b1e5bbfca5ef994e7d20fb40d5ccadfef642b582080be06069c96408008684c51da10c1d77f33bac83cb5a870c12b65fa432bd1237b5fa7693524a1767104d739bc4045c7cb6686cf576134d857a6f63f68edff1b3e884b63fc069fb93cad7dd97e470ade85418ba59d1dd861278186dfdca1677b1acd9f2932c72973359be6cbe1f729773b5d11453fd62dd2fc90ff915a27a79f344fab30929b1f91bfc3be27a958eb0010d81fef17b0905c6d2fc4f2e66e387708c34f4616a323d9b75ce6ad221fb0f5a0cb8111c710d9d954fd6debea9160db39346ff27edccc6d1e6e4152e4c0e633b3735dfd7b733fd272dc688883303e6c699ad11722bfe4f2e268a183223f1b2224c66ea4ee2b7b167fd62edd96b52f31505443a377427a17a735f26114a1e05e2c71b9c7955f633a1745e7c1b12fd7bb9268efb3bffbd1ac8336fa7a1ab576a6db56ff40e76283a5f02cca573365dd2f1c7f27f52263e5cfc7fa1fa1c795b08eb4bec7a08fd1da73df7224d53992fe04afff89b2d569f3b42a4ab4de119798409c5117fb30c7ee9c8695f239bfd5fc932904c0b1317d07025586bf34b4d7f391c75f4a361de9ec68a3409e7ba783517e71bcb09e9c7a22760cb60c7b5ffe365e28ffc6ddfc46febd375bfd089b1e0b0b4b8ac601a2a0c0db1a996dd6222d1425e889a3e6b27774598c0fcfed3abcf11a24f85d31dec48067aa3f2801187bd2399f73b2baf3cada165df4e3b7b4ccc708937e88a28ed6dbfb19dea59fa5a539a3a0e16ed958ae3de38fda6d152b62fcff3e59f5f22b64cfc9a013662e6a0c0f69ee6f72c958d3a41324509356909ad4fd21a0f830fb69d37b8776980ec8bc531099bacf4fed79357eb48075befeeee3c75102a66ed32f13055ff6baf5efc5e2aee8f7ecdb0db8c5e928c995fd657ef5cf0913ec92e612a63b2e9782755beb13384faef63fef6a0b0b89a64389d00f355b3acaf61176451d3f7c44d294b6f87749f19e89f0e8e7e5546dc39c13f0712771e01cbd26d771c13f9f62ad30acff75ff0d8d4e52dab5adb4a241fa5fdc383c27b6e6c6d9fff4f3d28bcd56876d93042612aff790f0a135ae38f7e5098108e08d338411222f4a0f0c6953cc1ed0030a5ce75f2c4625725f7555691b8077bd9e68aaa49f6dccafb533c2a3e2c833df9991445882264a1c7e25ba1f8c3737c73d44cb1b0dcfa946345f5710ca7296976fc9ad0ba6136e49f8cb6e983c2d0e4efd74ff1aa12221bed36454373b64e4bb65f385e31ff255897b34b48a2557b3485037221d8d491225ed612288055bc3b12fb8500a2085d0fc410cb7a72d9c3ecd43d8e6b5f64c9ad477dede4a8b9d7d6c8fbba0f5b13099765e790b0fdb9ceeb5bc6f9fb4df4dfb1489f7091fd2c71a65c738d79fe107d5af6eaa160615dda6b6c9c7855c796a71d9e37cba8c1acaa32e35bdd325a0c34580954efa034fedbfd7fdb31e24413b1b5434133bc0dc4b108b58a0a1fd5f4b0a5306d6cbbe2557ff5e127eac3bfd008992b7ef1fbade38a602512426c9ec8a428829582f0e0b7fdedeedd6e0b4d743e64e85ea3b66e5be6bc66f6b7bffc97f67a03d6f06bad5d84f2ffcf6bedd26641730c019b6cee80b1b207973c217afea7d6eeaf2acddb11a45ac1d7433fb1d62e02a05c3f107819f3836bedd63b81051fbfa453da0b2c00e825d86f857761f199098ff154346b6af1076a1ce707d4acb80391c5eda673d5eba178ccc4ac5aa425af04b4dcd171894ed65029cf5513af1253f4b67dee13ace617a686b02c719750504bab4a2eb3881bab95fc2d41535641de6bcebeef5df45caf3f99295338a1e33c182a6d17496073ed5ef58e1d39722ece2e6c27bf5cf942233509b5ca3381fc0825b54fb10353db73577eae1a4b309c8fd0f07a5011fb4b9b337bba38929132652f7b0a1b745cdbb71149f1f0a050dbc47fc6fd755ce28678b2cfb5d0f272b56ec4bde44126ebdb67071623565312777942bc65e7b0a7a5ae4bd2edc8b2256bdc79f3a1586d9285bd3b92d657dc5c4c45ee66d1761dae200d9a19766ecb5c5d76ab188190532526392a2bc1e5e81b8d7bd68eb5bf9361c8e3b1eee4a270a4cf6b2ccc893c6d542571c6f20a56538551e1ab887b3b676471fedb5ad32e02feb425c80c6dcfd5852b80fe4a25ad3c3c71bad5aa52c8ec676c8fdc2ee8c04ef2b36a70a85b2b3dd338c3f8b65eddec4e17dfcd4cdc7c4cce3b427022bd0e7895b4f6f598d3a9d928da80efad9512b6777f5a3f4ea7feab866c8501f294a69f5f3c75689fd4994397ed4de4ac1f9f3ac52bfbb72a6911aa6df2b32a69f565d091272c5d50ddbee12fae8c5fd29d44136ba9333cb1ba58b8b85f534cd1c5cd07542857f75f52aedce8c78b994e4a9ec58a4c35df682e3fde703095406995bb41446aa042f84cb4bcda0747ab9dfe80543eceb7b4d537c4e842cd2fd5b2117e1e1bd44b8796e0a2687bf658a66f7f0ae0dca941a7db8974321359ee86d02905892c9703288abb6b8f7eff0f0119a4c98056043d326372f7f1b00e378f21af4d5350480ef51f25826b2ac2682ec1b7f2fdab2f8f9e17ff6a9144d530ebd83b5855ec64b4a24484c617df7bef443191e31359cb0b84e79f673944249de1d181bfd994d98bcc363b12499338831613d5c1ccd10062f690446a86103b16faf37d33fcf1d531b6485f3c2e526b5ba1afade20e032dd1d3a2ed4927fcedb92a450be8e13f20f390d0336c04dc3a848a4e2309f0790a024c851a6ffec1ee98ac9da93e4adefbbb6bae1c66b8c699dee2fa73556518fc8f992f395fd70a438e19ab61e323cbe24e77e46005d404893c94c466a7fd87374291cb205c4165066f1faf4ea3571aaf4a5df5f27198921489122825e70a24cecffbfbcba17f8b8e8ccdf61178f684402d389a3dd63f11c10a51777df6d4684367c22c611a6edda24223c61ccf6f2ea45cd2f077bc55fd08dca328200dbe411f7d5675b94c9fff691295ed87fd4c98a915c9bd0d982871ad64f88fca4c3b7b90f65ef067df1e46a3e8b133b4a54b7e2839a57fcee77fdff92474feb69e1dc847430c1d199beddbf4560ba698b7e14bb7b570def07ddb9e9efd99e733e37a97cf74d64d7e72cba1a327231eee1e61ba5c5638dbbe1690622b1a72884eefeef96b6ce001958e0173a9c551414471de2b6a26de17374b6d23a4f3cf459c0afa51e7b3859f143c247cf9c43d79b515aff64b313b8ea5c5fc733eff399f5b3d9f5ba52363b37d7d9b0e2f5d716b4347c5bd81ac84426e929f793e73f25f54a393b5949e29f879b922dde55c789a9badb31605a305bade7e92adbf62a2f8cc60c242d2e5a3a77edeb5dbc312a968e7a342eeb2f6a5a704cc5fcf5555ffa8f3998da81b71097f6d793a5f2de9bcb0f3c09a425fe33fe7f3bfef7c123a7f5bd76ff1cfe756e9c8d86cdfc046b0bf5219045439fe33afcb140ea166667a62de190cc34814d1cce72b7c2f1422295da93f0977589fc5d5b34f704af90dc8bdf3c1753d37572b58f379a3712a898bd48e5a747b71f1d392ca74fb1f5559646426ebd1c3db83a88e156ff29efb892eecd45e887fcee73fe773abe773ab74b4d9f9e4570e5f5dae2270451173f867668a2af0a55e4dcade57066fd9717a6f4355a57145cef91d5c165d01326f9b8d60b9e9f2f78753c8b52cfa0b464ead891d1558e35f4c385e6824eec7b660375c9b7ed48392b81a8f9b66e45b9fd093f4bcaa74412da07b5b86208fd0a5630c698433f2bf4b64edf6f8b189ace1d3550039b58cac0226dc72171d9752578d6601d97ef9a709fabb460e0904d66a9cb02c14d2c344768a7f3072bba55d5caed2829d57d4f9dac71ffedab0b3c60d0431d1fbd2c5f7b8b14e9a76cf67272fc6b5bb020d4176b7be565c39f6f4cb820c374d31ed7d5c53bada24e93bf11b474c2e4b7f49d25d998d8dce2845eea4a52b5404608e08f645feb381e606fecbe3c6081d83e1eaf8f39431753b9faed765b62627329a674bf13e8e92960b676248d61b3b7d7fa7b7c0a89fb00befc275a957ad2bae8d209cc74292ee93cd48b26ea6c5e7c78aedb45fa813ae501dd4e05353a55bdbdefd8254a619c23e6d67d0b3949fcc01d5c4eca9dfe6c67b32cfe4662b769a7356940e0308f4c1ecde584147d7220d72e6de42b7b2bd6e947b4b3245505a3bdbe51bf2cfae1ced28b05ae67c80493e8c1b0c60e8d790284a473b1b573db2e5192d647451bd0490a47eeeef8bfa045ed1e7fb6ab1eb2d25f3ed825a160f83dd3086de8a39f2b63bdaa7768ad8c8ee59e63432ae118a9c1c2ff50d193fa1aac737c491fd31e1f1f98b9600f24a2fe349cc7d5b4b6bc16bc5dd7b1a583f4ff6a97cb1f87af2a122d0b55b2349e38eeca5d360735d8ccc411fdf107359ea84073a56d12f30d3add3d4fec5de05140039176f6acade78c68f4fbe523bc41eb440aea40ced71fcf20e1df0ac71e735cb068ea3e7128e26aca479164837969d968712de3e4ef4c93ba7b05f675c1775cc78fe3e40f1f4da3993a5654e2e0dbec38a54f6b58e7ed7ab6c5ab8f8cfd1dcd97e328e9d32fb112897538c256bd69978d85bb5b8ebf57106a9f829756dadeb2514b3810d0bf740cad9d26c8ab37992b3ab06bcbcee1a31e117589e8995e94fea931c53a37a2098a80b303d6759459a76973ebcafc93570e5d8bd0a98c413e3c8bbb93dad90c73d0e5a801a4cd206a0dcaa06956bea4dc59fc203b62d9d4c1dbda8b61f65e27b8369e4fca93df556274a8e087e4999699663bef652a24be97e6132d3d4f4b6d8050f9a91a24480e6f2a1d30d27e33bb8b6ed3fba83af45f0b67f66866d53cfd17337e4ddbfea771f1263af7fd148da3f21577fce3d917287bb9673a2ac2d83fe1cef191f741c10576b0fd0c90d77501dbd4f6f2f7b441e48f476d12e7884524d2cbfe7d0ff3e6d2f67ba09efa194327aeb4627b23d476f7c4e7c612e94b153ade4a18840fdb470ac75f6f693170163333e3d26ebeac9bf9cc88a2198c88ac7b47f5e22eb64b5f50f4a6495ab0cbd07997ebd95bf628df171a1d50a6d0cd5da787d17e754ca79dc36813f5b34be21b30982d627e4d473bfbb9d808bfec235ede3737715c0b933bb8de201103dc5521a6957595d17b29d454fbb7cd2ea609cdb6c49c515c9af07dbbb367f5990d7023ce26a3fbbcb3f52d237c27d37d7b583cad36c02aa8fc856baed9d840c039ae308d5342170fb0011acab086e9c11f957342b629fbc44ff2fab4ef2c469af8411b4a13fddb2805948e5b8e5669ddf3bc0085d159278af154add55136a7317bff160623af6926a8ff1425ecfcc4ed1cb179ba2b86e17a399989f0dac3b4a26e8526ae68fea6821d6d42eeb79f395ddfbea95ded5e6189841afaefcc84dcd37909efe587eb88c9cb8aea0c6a0dd2a4f9fdf5019e5f1b63baed7d92c3cde044c0a98462544c5905cb858529842372babe856512570f48b2cad574467657b74769c0767fb013941de0c5cc76e7686b1709848cbd9d3fd7eb77b1627680d9ff662a543b31b3ed1d658672cd49571302c0703bc6f495c6763ddf6115adf8dac268d4732f64fa2fb1dca353c5c72a3064d2a76a53a3ebb79defadcdefc1d9dbafad4a59593c723aa5c8e6cbb1997219b1dfda19cfa7e079d2082bc777e8006a7e189de916945765e3f566bd82e2c9dff6e63f760e1037254abb9e9c7307beb3992edd0dcbdacdcfe458f1b29899f2e3bf1609e3e7df55672a425d72f8ef94876f20d1ed3178fee4639b06b6e63e6babfdb462ddc22735782372759c7d0a0b801c97145d7c040e79c60b2beb58abcb6558576551f6f2d0d0156ce536771862ef1326c982558b393a2d4c94b19803d7b4d6a01ac02bb2bbb9d38aafcf74a36fbd6970b0b4c0e9f0e0a4a3a7639c0ea2b49dc27f220a725f359e640466e8569b77a234445abf95332f2131f5d9e3eb9d9b2f1d8bf4ef5fb76abd5fa5eb0c879fa0e7b90ff786b8379d48e074486df11cac623f02813c14a4050ef7fb8c106fbc9e3539ed1d3d7b7e6d2d2333333b2fc1bfd25afdd36be23b377affa8d73578dae9d3ca67ad55c515dc146d2fca495b9b1de5d831b77ccc58e18a99b1eb1105352ba2d76eaec1dad3b276cfe73f0abd07768ce609738ade9090defc067e4f777e4d1183c39b6eb3855a075a276c7cd639edb19dc1cb7bf324abd7974d4e213599b94784d189ce6c8c78bd7592ac3f845b88e12f2772eaaa07f4dbbfea335b6d57426be77d401bf265adf60b1e7f05f203ed11aa89e11fb9344ebc6c8ff85446b6fee190e8cca586bc44c60b8e25d219d19f0fafd3ffe668b89d6ca442a140aa367344ebb08a72b3ac10e2f3fd56cdfa91016f539939973f8d4798704c301b35dd72854046e2d0fd714cdaace2dec0c3c7db03b21e86b4ad5c283155adcaef1958bffc713ad958963ed00d779d1ce10a69510e975a5d4c7af4e9ec8f3789f7904eb7ce2169fdc8304b9caf16202f3fb4f4fb426449f0ac8b93aab359645b62fcc56aa92871ac36dc05751596e674ec3b87d91ef3feb2f097f9e1aaa4da6437fbd3615f960963fade3fd83aff36f44f8d253aef4963937bb109b686d1c8708537f20ca538f41f71565a7683204ec955367921a7b27605990df8cc952a6be7d1bbd5612c0386fced0bfe4d9db15539d8b501a63e0126a11125a9b306027e8ae5a02b1687b4a49c0fe38ffd302b0e28f0cedcfbc77c459390433360931b52ddd115f68bd903dba6c48218fe25ce5399859c8aa0fb16da5ffc7e773a171b39e5ee7f5cc4e1f75c2d8d20272a55be90fe19ea1d7c94a426654e34c9ce15eac8becb42ec41c7ace607e34f0173695f67727640108bff4d0f20991a61521a4cb8b894bee179530969690dc2765a02f2a69b85f5c7abfb4949e81a89491a188be9eb498bebe9184b49eb181be88a491a1be98be91a894b1b4e83e29236371312942fe4c792929113d513d7d036351a97d06927ac6fa460662a2e27a06d2d222e2fb2544f524f40c44f444c4f6198a1a89894b4b8889eae91948184aee37d63714dd6f2ca5ff7f271f4daba3ea4125211af9afca4713dce43a61f37c34f0d0645e2a2aaa8ee864b18d3f41e29d0283d0393344a7bcfd41762fc132d1f65cb393f662220d05b0f81af4ddfdc4695b04dd4f445ebde195c4457a5c507c7a7ed040eeb2c93983bc8ba300097925448a009170320806fcbdf00a42daeea62571e54d9f496c0abfceabbb39dce4215170734a92cde1959b560897bf3f664814fc2169f5a6f0c755ab84784a47ff29a6f1a2bb326697458c232fefa0a63b71e7b66f49fd5ed238fb57c18cef9a1c8befd4ecae29251b511bcc3fb090c783bcaafd6e75fa8ee37a96efbdf8d79c3ea7e93a52578c664119b9e75ca3ba8b2c8e31662d4197c85b758c0ea7f3afdda00e5909d94fa67b2e9726ef6969436fd79c9e88926ed35b78c9fa89e8de0e566642320a6e7528e2812c0fa758bec7eb93fb84c777b75e7d327780ed74e0d93bc2ab197aa2a887511d7d8898eb29cfe8f63da30e3166f5bfe6ab326a70505c7dacd3d195e2f4fc05d4b951b8eab06d39efed261ed52299ca02c4c75a8a53404c738af9d5a08bb24211219ed7eef1e5bb1976bf5e28cb4c584366f235b915517ff2d033b00b1b30c2404b90961329bdebc15ec09e3d69c47ef721af0248b18cdf0e4f11552f714a52626b6e69899690a92ca9f3884acaf2d97c65f8d5a43fff49dbb8efafd321ce50d99a38d8e2fc68ad66df1132740e13a9be003f6ffe24d836d151e4e6f337102476fef88535c0f3ce48b6d5e39e00f02f2d50f1ec69814422b54042db0c2a1cd13caea67a45e3ece9d3272f5c5155d03c7e4ee98aa69a8a92eaff88bd8dc7a1f728f6592eb41e82e89ec8d4d362d8685b3d2ee1a5a714b1da7034e13231f91d51f38657337c9206ef6ccc017f4327065a42b63b83a5175583ea987fc72db262c864cfae1f39f77f6f3b1044fa122022710f7d370f5044d8f1c380e0a72ba9e93e461197e5a30adad40884744412c08f872681efbf21ce6c4410e96bf9c3a13e4861ae54e3a2679574d88f06631d3375816b6c2bf03f1911b395fd2514f2d3913cee94abed349f94939951b177598399269750b52aa3bf4f7f3c91aa0e649e04b447c392eda923c4f11f08a0be98f3f4fa476bb13612a30bec5430f9bddaf927b848c7526866ca318f624582fe27ad1a427bc0eb6be01debaf6525c772fde971eebe80ec14b3cecd478c8825920710db48ef58ea5998e8591afdcdfea83b776f1b5918dfb2b8b1e7beb5cd8ff7b712808767fefda5ff3b0d6ffd0c139c3e21f9cab0759af9618d90db8e6a7315665f0391d69982aa94604ac63da85ce38c3bdd75a3881992797ae4781b43890a72b2405ac5cec3783b15e4d11c982dfcf84b0f60bf74da74f62615c9172ac11be6fc26cab1fbe6880dab2510ba47489dc03b07bfd5f0fd814302f73e085a073ca2c440adcee46e93474cbb01072bc900c011d2a41804d0984577b40333656b5dbe7d57b3eb665132405684c2867b41813526779dd3fdd3d2767feee5facda854c0557dcfa8179bd10e88b93ec0810481e6caaf8841046faac1420af47f5eb61c36b05ba3b4944fcf33024f7c6d3555ff8fadfafc53815284ec88a7fefe8a780959efe21dca429b93b750f22f1886a9bc08228ce1d4e9531b4a3a0a0b13ea393591bf81e13ebfeffe7d896cc6f9188da4498ed4d02f954dc806456e97ff9b7818585e89fd98a9a450fa28245da3e940e58f146f78eda8974fe0819ff964de63cd138af81a92b4dd956a33bd8c9beb1ae3b6a7462fdc570c6d3bb1a9f54f0a1228da07db439c7a00c7fc48f366c8efb00df9f827e18189cf9c2cbd8cb6082746c3adc07fe8fe126a7f6dff098998c11f50768448131254c4a8785e20720e78bceeda51c4ed9a439143fd134522674a69e8ae50c66d529f1af46172952c2306cf70ba841d04e677f84f190d38fba789eee0ecff76258bbfbcb7c63fc6c4fdb55df550afe90852a20762ad9fb8090f0aa55b70846e8a779225954d4747c67211b97cf4161cec7f0041a75dc4a58688f310000c5bc5c91ffb8f2218513779aca4f2619707146e99ca7de8f924f5d7bb130cc4e22252fee1c5379e6d0911bb158a885eb87becccd1501fbe97acd65bcaca2684abbf814b42e12044664f13927f8422f97ef6f1c6df5f9a8923552ce6aab9072e263cb89fa30cef3f076de864041d83d9ebb771f810e3de3436cbf3dc0aff20443faf4e7645144b65b063eb5e940af77afade2922bb4c08014bd0ee5f42d90a206e574ed5c332aadc37002125b55837feedc1779b9f6f11df1b5a0110648768ba42509ca3ea960a4e0e75aeda287473a72588930809f13faec77dddeb286a77cbd4ec592b7504ef710f724572eb23711790dc07e8544ec7f8a0748b2732699ffba55618bef08a38343a029ed14f9d73d7d53da97fe133cfdcbcca34c93bd899da45b39372d12c0a313b64bcc4196c99bad2a55fe9855ab33c5572ccb73f23ba7fa70e47f59db1333702bce648ee6846020b9faeab172d21076f98e5187e386b76a9e8a66d4b4615453c798c4fdca36adf210fcb342ea34acb2455769217c1f5b734aa63d8ce6ac0f3db9c4382f24517c7abf5490232d35b5e24208f3877c664639bd722eaeed34bc4d0999da0ece000d5ee91291e50b7302c116b537f8d5ee6ae2b28150a02eadf6b4add7c87e0b7536fceabe2b85a64aa5e57c25aa47e41185b7feba84dc3cd4e3fcfbef0c20752d559c78a3075ed5567b7a12673772cca7ae5413a5d96d7ab787452a9d5f6a66b5a244868153bad97442852a13cb84c142ecdb8da5a5644f34db23098baf4193cd6bea4708867b94bf55edb1d568f3626e393b264afef2b6c2b67abd10ce1dfd3fdcae441f017cb775add91a427137d781864c6e84ab59e434fccf8eca9a0666758e6871e5f00b0a73e3e60cfe72a80c6fceba118bf9c1ff0500ce0ad8b819620b15fc674b8e01d7aae00e2d61a5fe527ced347307271f13b99f0ebb183e9e0792858c551341df5ceea3bbcd2d53fba68239c0ef7e2e59608c95f53b7d6ba73a997f23375da0b973803a333de5b6558e92e4d4623b61e65bbfcf92a275ec68bf17a9d17fd1efef1ddb4b3278358a41aabce116389b3a62f85c7367db6553855a91bcf250b2fadcd732dd7e0a93d22303dcb992a41b1f4e42f4eed89c88bdcc2a342a3c6e4a3cac1a177a3d39cefcaf9aff1de083d7affc4a9d2d300cf4de39e8bc59f8b05bae29dacc342f83f08095c1c3f682dcad3207b639b93e0e7460eef8fd61f8fd3dc98427b74bfa42a3e64646f706f79f8c6dc14c32e0db621b5bbf616a51f2d1993f41e1b8529adc52ce8dc3a187a845e45a4445dc9644547e3e84e3a7682de96dbff728d18c64707e23d6f151ba8ebb18b24ee96c543f5d0d6a74fe24fdb93fee608fbe5ae13943f1e7d964a7825e21d5bcfecead982334ed1a41b4b673215767213af210cd4eccc9f32246980fefd5a0c8d815a9ce8442286e40107d28c57f20ea4985d26e86f144d8bfed70d2d9cc965aae0fb01bfb2afd7b4edcf6fe5ea9c79386fdc79f2aab9ef2891ee2f42b2d130f7201a45a4fc2432731adf7d09ded68fedd93b5c103ed0d1725f610d1badb50f0efcef36bcf5b95798c8fa0e7bfbdd06d061c7ebbadbd3c8524f11e71e5c82fcd0f64c03ba0e17d0d2e802c801d58373d88a25f8372e2436b7982144ea8630edc3724617095e1be3edd8edaa35be47b451349416713498d8f48ebb721de3dfc3935af7d5146acba3c896f506bca7da7b2e4e56e76e05ee9bf57ec7f117872e5d0fc873e632d5387b5919fe692bf3fb8323adb95e6fb332683776a55dd94ca1bb27fbb5998b88febfceafe316cc50ffbe9c929719acf2c12753419e589119c417dd034117181f332c1e5bde47f5b2a743f28af2d064fea4c1b8de8de7b3b98ed3c5ebaf855a9486903b60be34e5071adafbd4cb63e9cf753c0878fcf87a26cfe7d00f8bd54eb44a4785d782a21f1dac781bf0291858d52d3d75aabefc7d0f8c93576870c224bc5a744ca2fa562c66091c47db6f073005e0e01e693b46e2c42e80577780507a84f8baa8fd03f58643dcd763ad45fadc2f150979ba6fe544d98eed5abd8e496b8dd24fe5d17a72fbed74c13c2b9e9be24fdda11b6cc436398583adc9c88de1f46f96a24cf8b7b2fe7fb75f93360d185f5f5f5fffb9163e002c811acedb3f8685ba63007bf69a63ff8475fcdba5494dbcf56e1f9894e97200d9d851feeb6149190d1bcff237542e51d6a3edd94f7117505628722e8510395d3c37ec81e23b0776f259c44e9f51e7884ebfd44d76fe7ad8f7f0bd05dd550be490c4b3f59b19eb4f21852fba7bf1e08f288ac140ed11adf7133bae081808ef0a4ec5af5dc565e7ea617504ce3f3c653f3f6f262a1fe5f5351bcf48b3663ca87a577c643558d5a43a9c87c46854eafdf7f00906533786a95a0cb7a4f6307fc5f385e027d7b2be8707334d6bac8235efcc5dfa79a0486acc0183dc07dfc36b698e5f519df43c9d7a95ecb072e5f0f1d0b90cbc38c50b27464e274a9233c81972f29960ada6f9b1698a7816078f9f48460263a8ceb2391bbf932a92ba3b7502cfcc0f98ed8ea3f04b0cad70b977def4a1450b8f15def848cde8619a6ce6b73d94821af25a0d2e9a0f16f0e2606ddcb12c50deb8c1c45b05d9682f655b13d5bb784289c309475d86c61e207f014bb6497531949fb0c5eb7fbe46e95481e287e38efbc56a2795c3e386a89e6a7f0f6fb45310394a5dc89c6f743e55547dda7f46a00fcffd7a50389a15b9d348bcfdd385d77cd12421f2d517f09486e3881e9ee8057bcf4472b29ac6cffee240d873bcdbf71773cd63b6a5b10dcbab5a6e7efa5748141acee34509ccdb3659beb2dfffd8dbc5e2a29051cc7e0ed6437815076ec4db06d728875f2d9ba096cd39fc653e51651b5e9cebf193b44d05248f304c275eb1f50c9569659a0ee1d1b79d1f5456235be3f754fee467f92fc8f046ae64bce2852bcad496b954f7b39dd71e9c7fc71a6022cc5c89972051e15a2e20ed79f2d29bbd63ecfd7686376e44c4e0a5c93f1ba00b8cca21dd477f2e75bee6e9edc1b173b278c549bbf4530feacb4a48e4c546395eaaeb5724eb2cb0fa1ede6c2bf7c9a4ad5fe7d1ae2c9c918ad989cbeed7f1d84d42a36c227747a0ed75c776971827334596a11c3cfabb79e4f3d965519a609e91fb5a50884dd8c5fc0778c56d6662d3e79f94bcc4ee9c7ff22825524f6cefd2033c3b337ccf07725baeeababcace50bd71c9daaeab42fe13d97155e1d9ed79742eba475d00e261ce9b760bbdb0f0f7ee5625811f4da36829d1a5a0dd8cfcdd82dea85f71aadd4e7d74d03f58b4d06dccec722c2ecb3954d4b78bf879b9c1c25796075d446fcd83a8f722bb2de6034d5f47b386edfcd7eb91d8a3d874722071b06dafb753479f0e290c14e59e0cb6e8d4b293d56333e37aa3dc38783f1dedeba918d5eb2e2302d3efba6a1d27f37dd115cd481c77842daac87ef369baedecad0e3d6d9c98b2a34c3854578df2f92a5111aa291cababf1e3d4495060f4a4ba2c5533a539f46df4c18d277d43af80a37ffb1584491066f7d8b89af66b982bdbb129fec3ffac5273d957282fadcf77005b162a53abe3024822909e46c7d98deb574112f89f6f1e30e3fb4ff4da773be1f8dc499e68f289ec357220a47a72f1ba7358d3b30e497ed12c84738f604e33de2b6dbe375fbad66afb0a0a747cb7a871ab2f8aab0e5dfc3958fd1e7e7a13eca9f3b10b9c75c5855fa02b336def9969a690df75df87a350a3061121e6f248fb3bd17fe3d3cb029e19260d4c5b0733cd5625dc6a710bdf1105e7127ea2ae73cf36bd72493ca3318d102b7faea522af094b02693c911ed771f1af79be1469011b225b6f1dbf04cd52b71ca8c5d5294ed4577d23da9bd38f5458e06e25db14de89c38f5e024cbb9e8d6db8dd76c1ecd7ec89ac6abd1d1f33557eaf94947de156631c458954e1b87814733defc57ef7a3a72c60193c3d1b970c6de0296724f3cfef9d5e14db3e3ccfeca61aa3415951d8a2f8ff8d1e2f5cff2823780eaa3f3d77a855bead4508d633e2378863c68b60c9ca6b9e8997b32fdf20959d982b48fa5785aff03110d1abb4b7bcf2c1efaa0d9bfc26fc459780b2f3b26ea389af6104029918ae551156cab72c43cd1c61b3fae7eef4edd46d1c47d824f9161a9dea4abeb0cd8efe19e9f965bf3fb8d8b6f6b8f59efcac285efc51dc6fbfe9e936d322f02e9e760a4860b5731de9d5fe6253f7c0f57b9edb5d38ed33902e190b394d6eb2986e13e3ff83dbc4084c72387e641b4e6d783d5c52c54ed2ff524f1d4951b71cee2235acc62804ad09bf581dd687f47002f22e8a37f612ff7d154bfa7ae80f693ab26b26de21c789196cb27b74fa27d2c13cde8c5a286174ebcbf2f1980777ed6c9e9eeab585eb1d0e796ec7958167ae10eb61b4f9dfc6073f89c81781d87503cabd5b3019ae3fedb63f1f49bc35cfd0566d311cdb19992628542ab172bdc76e029d9b7d84f65b44870f859f3e9d30b2b8251c3970496be87374804bf39e00b9da28f2999e95ace443ae5b61fff1e3efb4104251b7baee1a0c68b6c05e9a2068e2af13bdfc32b0594dcc275f35534a88f20e4ac68246e9a299ec4e3bf9347ccbc22b565581f71044b6fa3fa6cde988177bd7ade95d907ddf3c45ee3f0bd89e74b6a0292cda678fa5fb3bd938fe76c62407634156f3eef09a6d33a8278c1de5ee263b77c2fd3b75796453025901d583f70b8044ffed46b898c6967a15eb374d4f1e475deb93690e181271f6dac95ce0da70d1b93d6a87d88243b0e19ed65e0c1f363aa701adb24addd31360b3b25f0d043bcdfe23a1e7f535a41ed551bed61ce3ad1e16aa3951020474a8b778d3073796ab552fc11491c79acf5171353a6cb6e2478f4f752d6a8a6b8fe616a3dbdd1e3f9879287de6449e35d4f3bbf6f39e2da86ade89ec87bef6991de9540a76f87772d520d09f049cd4763c42c10887722587dfe68bc6ba20aadeca7a3991c2e698cb7ed1a9887b8fb381bf18cfa9627a556cf4b25571c463f73c6993ca72f70e1c25bdf749063ea3a0f567becbd43c3f8b2c1b3af2462fedfc3c594ef7e489be6fc5a762f4e145606bcbbf15696fa7b78c6f111123a9d3aa3f10ef7e47314537d364c761ff1f873817fad0a003da3cdd4b689e51842374e19e1f16fc5cfe8e685b61b97dfa75718bc73f33c21d7b28e271faf7c281f576a5b912f5113bd9cb12a71a433ea261e7fc93def63ffd04800abff9e41494ffb7005dfdefd788fd51c8db61cf97a9e29e24202f090b1fcb170f8e154bc483a2ad6628f6a497e798afbbcbe1c968ae3cb9e7c7846ae13a2f2e8fdce107392e798992f0f694e7f3181f6fccbc76f77f307f8f8512bbdbf3cf4f96d44760f9ff83d6c0f37f6a42169de4f9e53d0c6d4a2ed691ea7ff968f00c7fe2b1d155e21e97fee2491de5202f57d77138a0584930324944418ea443e6e43e8a61d46c0c424f4bc29fef589b15f80fd9ecee2ccbc1b17ebb4745d604669b8ce3ffa2dfed836c5af7fdee4569055477f769d3465dfab465d5a4ef6ac3c87129eb68f9bef9fe802812109252c290886b10a5b1e7e6a6ff5505c26ccead05de5b7a5dabda4f46c39130c2461fb2c81fb82adef96196a8d0e60472203d5c969f70939cbb45cf65c29927cbbe8805930e7f8b937d91b118094644d5a65cfe6bb1bd2ce835124ceef4c7082427f4a3314f95b71c9fde5f34d68d003f6e362088517411535dbfd20c37db795bc09e5c301ac447ac44042f8c740ef9da174ff853bf2bfdf2539d01daf74c7b885f2360cea340eead4c69036b9d3f8b992d7540b8e8dc0bd51eb884a309ccc05b54cb60a412624c1c3aa8844f14e70915cc01535ca24988e789aa6199fa609c7bad37c09e191ed4f035d5195901d4831540b8d9739803929b066771a371c520125e186d2df3951e600b6b8d35c9e8332a13598c4a123d073e81d0691e189dc9e96097ecc00dfb9d37c1dcb013bdeb2f05d1bae85c26228735360c92b30b232d1d7e22190ddb745b20d36c396a790adee34b1631088fc64c3fd023c1032ac1a3aac5a87cb03eb7179e0cbd7bcb5d5825407760394c950032e0fac8b2175b38315c18221803e3c8612e3898cf04416f7c33333418f0cb02d145c19cb0123c886c9f8c97605d9c3db43c1b1e454b8bf2b393fb2722f900736e2f2c0b7b83cd0658ee1eda04ed3a0ce2a95638d3f349e9c0af74495ae4147413586560ca20997076207753a42c17271d00705b8a20413d23433104c89699ac0e7320770b2cc019c2a7300a7cb1c4066ca436f40371408d17987900272d2d4b487cb409f10d217ea0215e2b0723b981bca1d6560f6aa8eaead1e2aaca746cc76c0faa949aeb6c3df4061c3aae1c3aa11c3aa45757423b550f3a00ea23e86742239152e6f1a0f0a9c6eaf8746fb9163fdc8da6af2c52ad2edd1a03da4f8a5cc010c1a02bd87c09441b03314ec0a053bdfb2ec02dc70e0783f7281fc23984145e206832305b1b83cb019970776d443a95fa00057f2ba6a7298f708fccd3a940967de76231f6cc1e581eea1bc25fdf0cfc9a9f09415d82ae46e07c3ac41400cc89b02d993323025a569163af117d7d1450eabbe72e277c781ee2e20d432a83353e6007a4f41f1894cdc81aee4ee75fc545ea827763020187c8f41bc1bd421591a8356c772c0e63dbea877b83cb0159707267822eb3c917929b0fa74703a038cc582f343609bee48002890ba029be847baa1949f800b79502f697728182ad1aefb750af9b91f194926ef8eb270031b62486710b85030690c4a1d04b3c621f83ae8e40301eea848fa3271a1573eb0241d68650ae954e30f7d847ba0903c0ed0fef7df3e3ea8335be600a67d81269353e1c8deec1d996e3ca867699acc2c00c9fb411d105c846023e0301900d8774155109cd4cc15abb7485e0efa4c416bead054722a5c50447bd4a60e0395d4d1450dabda831f6b2104491b2e0f2ced87878c415d6f5996619fc020577246a00d83309884b25ff322e91e034b7a75d58273b23e23705fa3311b79aac618526f941cf83c4d53ac1d83200194dbbfed112e0f041686c0cc48c80fd55705ce96832efa1a0c8c8ce4f929b067535010c4e38ea2dedd07232917ef810ec680113194259ec8103bdad86592ea747039035c9d42d663a07638a2040cb6877ba04a183b717960172e0f94f108e54df744ce953980f3650ee0d05bb02005d652068e64807158d0770aaa1d0267db20771cd203350976e3f2c0b529a497113a0c5a1862f9540ba5831f2269da0675da07753a067528e040a5782b869a23e7352f2fd93310f4449d8495d6d161eae83a300825800f7c8000b4c801d22ae928fa352a525bdf1052ff61302b125a1b021f17538c8fc06b8b54466b211c2e0ff4a47808880af1221fddc9fb0477000d3beb217792b5b11c707d2c07fc18002e9117300300f5225cf60d58b906231bab857a7079e0075c1e882029a1205f05935da16812be40307f77610a0cd60fab107f5d47173dac5a017a86f24ef623835dc93f0cb030faa0fc51db99608f17aaa1b23abae434cd9434cdd434cd4e0ca2ab1e6a6a87a09c5e08f62900641b55cc81c5cd831e286e5801b89b74a1cc015c2c7300e1b363d01b71e71a7fc8a5c61f9a4e4e85a3c773c02c371e4d3f234f141ae4c2f4c3653e527ef98597c034ab4005a8bb1e9a494e85db8de7805e280f3b188d1dd88541f4e2f2c03e5c1ee8dbc2cebc54e600a22111571118cd1af8042c04ab2192e53207f0753f7c3639157e6cae0326f791817ebc16b24cfd553280a8ba21909a6cae0d9a4b4e850b7d004f83c80f10a44ed38fcb03077079e0d76544e7a00e1932b813f29c80bc60601af458d1758e81bc6b50a77b500737a80303ebab050f8edb00e575741575744799610838bc175a8f0733de20c81183b83c70e8db40650ee0db18d2b27e38d54c084fccb0aa2f8a2f76587507348ccb0347707920389f9c0a0f8de007c26001a8091be0990e084220044790b961f560ee583d0fac1e29322d4d336e58d51305323781dd18842756cf0babe78dd5f3c1eaf962f5fcb07afe583dcafa21106125e68562f287d85f401fbfcda0191680d50bc4ea0561f582b17a3d833a2158bddd5ea89c8f700a7ebe4fb83cb0120cc5ea8561f5c2b17a1158bd48ac9e37ca11fc2517144e4ab352e6002201a03810f2b483ad83c81df424e08710ae11300b5ee4c4ff7458350aab178dd58bc1eac562f5e2b07a349575742fd23433c0895a681497073ec5eac563f512b07a8958bd24acde33acde73ac1e9c329a7e0d92547684c2c164acde87419dde419d14ac5e2a56cf679d1c485b817985f2d65793fb85907ae0fe9f2007411082befd0f0ec260000c0261000904a3df267c16cc03ed41a47f08e973683400cca27b49971d040343a053c6b0e73aaf52606e99e0fa14f2c9780ee88a03fd5039413012108741b48281a840783a780086ab873c71a0287a1a19e24a9e1b049b18817fb6f142917ad4f1e33e73d17aa3f282604e502c48461e0efb7d2e70e89b0e08c240380a84e0701808437ea2f49940c4f9214080393f08160a79a358a0d532079094fa4d1d5d551d5d03069ab4f14135540b522482fc9f6ba186d7a47410fcdabf341304008b1f5675adf187dc6afc219216d0b211327a03fea2ce904300393b0807e0700042918f824d31a4308a57bed08b1518c9642d840611c209c3aa0bc9a9f0eeb72c004842eb269cbf332191e9f51ae404925d4dd60908f97f19bea4540034d58fec8316bf89755412828c8c6fec1be3fcbe6d738333143bf107a13e93700c20806f7d7ff9f38be504e324034000260b827d833a3044ffa0cec0a00ecfe0a00e368614ee81438e0580e92b306f54851d4c48ad07834076b88211b0a1419df7186a1f5418949ea69991a69999a69938acbad30b0786c17d50db113012fed74b24cbe44bc9a9705f54e61b44371c82a66a21e4af5bcf02001012494e8162e29160804348361a7aa69dd7114c70da9d7c7b590438799922119f90563b9063a02800402700f6c33652cc5336f053240090a2f382d47e3c076c645c1c62591a62e9461cbc16ea4a7e88aa7780a5ba8e6e959c3f0eecee431d852018fc5bf3252321a3e086e0cd31a4e5fd7087f11c70da66094e4d4e85eb43f5f4a13ef4a17afb507d7d28381c8190a083c177b3eea2e4602d220b73252f06c56aeae8b2d234a76ba1e56f0c2e014c1a56ad028b5260c0e99d2c5f6cb2f7d21f0b03196aebe8383f6010453eb051e45a9903b85ee6005689578bd788d78ad7897f4d4e85270bc2035014d4a4ad648121a4f5e22ca8b008feb6ccedc1a8ee3d3e28761a642f061114424adf203e1e00faa2c6db613336bde02082ab2586b451fcad7893784335792db2a71e4ad5019423613d9fb9a6fb91e9d550b31c5ba51d4cdb17e587621007049e0dab0e0fea8c0ceaf84e20a4f33fc11152c7699d41bf1088aeb19a9ca4040e18f6611000044130188c9c0d4902e3507762088767acc010b4c53e300ab634acde1e4a603b0cc804bfd4428c0ca49e38241d0ece94a6832e7700bff4235943114f41d28f833a254a644038c8574c06cb5c814d42177ae0cf875599c77179e0042e0f7c2617880aa67dc49f06bdd0f170010ba60458484250be289652b8515d1d5d3f06e1cb09822d7250da5b5835181c420a0c88d8953b804fca1d408b7b4942e27609e0fd591b3f941faa0f5c86bf27fb8ccb033f0dead881fe28612fe878bacedb6a727a70ce866ca6162af181a1e84d05bec2fd38ebebe826717960492114e64fed389e03cedb60e0025cb3b5d00bac9e7db903e850ee008647f003779787584a9cf8bd4379bd71e0e8a0ced8a0ceca3701130b1e59aa221d1fd4617c062ed844215ea6691afb73823b90a4196022c2e91bddbd262d160a4265a76936d4d135d6d1bdada39b4116a7c0b0e2e7594251ec0318840413854f28af821f6aa61f6991a1539c0eda4d239dc77340097f943f4ab712f40de59d18d47119cf014503508f1ff885f28684902678224b52608be9e09369e41ed7f11c702200f436ca5a81bd5c8121bcec60656bd0fca53776301160ae168a8ca10c2f2379554f5de50862ea76559d7e0edadbc12041c77207d0a9dc01b49f4642d645d6abc9a9f0b5e454f8a20d2c7958b5a98e2e6558351c1274866ac04c9d30fea27aea2fee8233ee82b3ee8273ee82f3ee820bee822448a0e0137cdf89d7f0f95ae813abc334b259bc457c3d3915fe390064cf49d37c010d5e2ff285682f02be23f0376bb07698e33432b07017db422d140e7a432d307eff505eff00e85d19f8228716243d00ccd092207d8640e9360c75b82b7914221805008318840bc4e9538a98f2279bf2279bf5a31d6ea1885f668829db1e51c58c7138f0798c2ea1963aea051ce37060ee2bf3cb7846c72589b030a8ba028a1da26eaaa2748907c7d0b08060c4f00425c6e14076306d4137ddab7ab6d54f8c6b9f76c578920cb750c4946d7f324e87eeac20c3381cc81b87329ac85ce241dc27d9297f32cc5beea8168ade24ba805aeaaa298ae09293188703c9af286b47a1297fb2e1168af05e382690742d9c6aba1996b44aed120f663491a5c4437171704cc1d9f8e7541fbfc2d01872742fc5700bc59b2fb7a62ac0c1457072f8fa73bf9de81c16cc5beec00cf2a558944b3cd89b4497d8b6afe29749fffa5fbb97c0943fd97224fdb70f8dee98fa4cd75e44dae74bfe6e048e599aa49ccf2175b6637e96842a0e8425fa413165db63cab68f26239c9eae816bfddf06e87a8dc8ac227309814566d22ebc61fe9c431d51ea0f86bfa62a72a444e7b0f44c53f44e534466d2a26348d1311498b7dc6fbedc7ad7f26d3231983a547711595c0b2bdad506f3967b3e42ad7a99a1d30e56940d4e77528d39b1e2ec60dfbee00d3539c17edd1f4c502694ecc0361fa1867138f0ffd162ad316de5d95df7619b6b5f1b3006874cc26e6698ee64a6ab69bbab48bb89a7c956d5465b75b51f4aa56a2a55955a55aab4e263a57e32c975720938d8c610e310e238d780cd633c3c623b78cc8d717c03816048887186870960dec4360f83214c75b88c1b656657d148838e9065fd1fe77fceeffc7ee758c729e97508e43883b5aea1aca78466103ea2d690746819652915cd202ca57aba2fbb332f0f70e466026de34ef16765ccb080b5aca0eb0ebc7c51ca522aed7d2ce8fc63dd2a66d10a6906a9fdfa3f36cda87120bbc9230d71e4d715b3628b5668d10af56528cd20b70cb8ee10282ca50a702465c3bb46652f26383cfe3a3752865586b2bb6ee48ef8c04daf867c16426cb4e20e2b590e8bd73c321e014dbd1277a4c0d15c303b9cdfba98b3348d5c5f47ef4effe4f0940032b9265d787d8a66902dbfc0c711661fe6744138323964878bd9e1e2780089eb215eea03e1a68dd4efe10dedf866140bde2fbc3cfea1b64f90f212f7d4229a418c260c8ea61984492bca473e6aaf10e83865ac4ab2bf7e5eedc2aeeee1daca9fcc36caea56315dafc01d29185d3955e314c766f07a4ea9e394231ef080d1903d5f939d43055e0d79a94576a3b1303af1f1f04ece3c97e76ccfa79aa4bb21341362c05ef0f3da895f3c1b2cb4f88b161aa19c284e191d536cdec3584f09a4cd0cb00c264b1f5c8278d2268c3661bbb749d641b2c3c5809fa8e428a3c3c531ed6f92db788d531c0f4082bf183ccebfad2f24867098614b5bbf221e4002b58a759782a55423a94f99875277a4c0362267878b33e1eb78244cdbf5889a59c46e0cc9584ad5a9c726c2c57d067c7bb417b1a4ceb066f2a94b16bc543ada72927700a0fba08dc2bc1ad268c2324fbc97fce5ee1d295f244f7c026b4439b794f5822178bcc4ab89c53dec7b8d6610db8a24c491dd3e62f31e1c68533f41f6eecc8a6906d958c2866a44b515ffa99efe6438245ab321337a314ba95e9a24cf0672afafa3f7af7ec0da4577ee4251051b4f42110490839bb27039c9a415a1f963fe2ad432f35f940df76a488bbf881e155f0909aaaa4edfdc1297b59558fc45eab69738c529cbbc92cb0b05daf52c084cb4907febd6bad2b00247f3f54533c8ce5e36ffa1f56a0154df989ca554cb3b59cb3b59d307d8135ad0a52bded388e032f6b1940fb57155febc0a63295555d569f58cb02698cfda45e6a0b46f26cbdb858438e08acd5af80f1695a82d591b919386716955d5e9f5b464c48803491cd22bcd208d0694d190ba5ec1234ec2473fda24e7cb200312dee07b4a057e980d68c7f27b2ca5eaabf9154ba912bacf80e0f8bc6432b8664318ff07d690f4fed62fa30cf076f8f6eff8f439274eb0946aac3d8f6690ae51d9f69759aca7246d134e7809af861c7d9833654279fe8957132e6b16bfeb8eff9f3af5d8ca575f224d316cca84f29c323e26e72f4bcda0c01e7dd5a25efd59da84393524bd8cb066b2753e079c3e0ca05d3465427baf295341659327cb1cfa67a8de351b944a30591a4b5ce88b42c6f46568d043b4d8f03906ce79da0a0456b524b61bde379a30b301dddc15b0769176f15f022ff31bda7100fc93899f863872691a3118fe9a1d2eaedc8654e8d7446e4f5e3c40d4bb67c5167f513c80f08ad8a52be689ae8523f77c3934834c9950f5cd75743d9e6f1cfd8bf23689699c643d2547ecd2e347f9ffb1c485c65578b19996770e4318af9bd11606ad67b0264f166b17f141e3d7d30ce2182c740c169aeb2e0693a5167f9131fce18d551154791d76a94546470bd76c88c55f64aebbc8522a7f15faac1f7bda8a8538727c0385bbf99d2ca5aae7940fa65ce0250f5e5f0f5ad9d18c8e2dfd15e4282d6f5c9553f6d3968393356e114b93fd7388c5029acebb4447c107a7555a11ca1e497dca1e42af577f36c4915e0df996886420d4e347f987389a0b2eadca5b22c2fdeba727bc046b17d18bd29d991cab157fd02883977e4b1d3d7eb4ffdbb7dfea28f5fb88da7dc9d23400247405cb802a5e4df4fb5175f7318bbf68acec0b34162b50771fab784858efcb4c9eacb957b9fcddbc55faa46183a89e537af5c7bf748a79de8f5713ee48813b5230ee0731f73781e075cf62cb61f1c64b0228c484594c588f1f1d1dc88907909916648e96670aca923af3bdcf7ecb780742a993dc402e4ba9346d0aab1577f5903ccf7fd7c6bf054a1f9bdfedc80169d7158746b1e9c1536d35d94613f6669566ecd2aa9c6f02d4dd026af3e7502ab409fbeac9acf8ad852ca572b6e76760f6670c4aa29e5376d42ade04251fab5bbb92d6cb7f8c4e7cdcd091f5d6b61d17668ee1f38993c028934b671b0de8f71e7fc95798f07ddca9c7c24cfef39a5f3c2c979a7d58f30dfcad65650d4da8a3f367354eb1ae57f06a0d5eff7a4af22e81ff6146d9245e1716193cf6d0470ccefc1dcd20d75d529ea862dadfec9b14fb2685452b74dc954c6d624d1e69ec31e6b92b05613ca4dbc58dbc60b2d4bc0fddaf452b0cbe42efcfc8584fc9c0a36c88bc06547db7516e7064436d6ebc168ed2726b439e3a0d87ea3825af66ac5dc40be78bd43ff00781c00e4e0a59bb68de96d7befba304c0e22f622955481b41fafde8c881ac721b01aabf071a32d4288b8e293270be5a217fba255f0d4b5fc40ac0417fd1ed473f1fe480fc414bb42ea8e77bb3987a5a6a4c91ef78ffca4d82273bd62ebad422f371c020c6143939f71d14bf7be1417f1f2de43f03a26b9de2ad67aa4ed3af9d56a9bb1f9da3e5a9b03879ab138d07109316a47dc447b857e4eaf2ff85e5d1e0e74f83ca917268ce291b6e499df9def27bdb02c8c1729ea31962b3984059bb68d28654efe16fb2d79fb7176558b845dcef47a1582995c993e5d590eb0bf88f07fc23ce8e160ea561008b252e543bc53ca70477b0f9fd5cd653b2e225606e5b5f91c352fb4f61245c90061efda56e4fe2dd91d754c80f9a8eab0f84b7760133ae0aa8d98c5d1b42463871b2398f3f34e525525ec8b0ddf0fe9bcb78cb74893fb62d3b047d7a628e96d3eba04a3b9bc2ef3ac3aff4dd144df9f071069bd18b0756d1ab7b386b171d799b3a939a2582c9d2ae1bb97c53e0d590218ebcf5ea13fdaec268d0a2dcc6cf2cfea2dda482d7cb917240dab243f0a7dc1a77677df7cb9dd00977a420982c9dd18be718cc9d40c719acdd8e641a3cdedefdf1ea04deb5a5002d4ba033cfc86f46726bd267cd7517670da2f60dc5417a16e607be2b8f7b7eebea7e2f3c5550d1a4e0af31c5e55dabe8e58eac2b06185aa297ffbb367c44f5c696f3cc93f3e3ee73d6e7e7e2ee73e6ba8bdce059cb16884f476bdeab34444ebd0119ee1838dfb35f83dc6cd1429f1e0d7e0e606f820965b257d05d96cdda45aca704a070178f0f9d8f252e004585a0e776752a4623b2816d913b527065082e4c8e9d1b7e7d6a9c399ce8f852350e64cfd1728bbf68724d0acdd9fe1d988eeb39e563d3293e53131cb95f796cc447ac741554c6f08c4a7aad27ea3965ed62f69daad31bf7cfcf4dfdbab38c044ad85945e7431f35448fbdba076c9e19d9ded15833e95e911b9de2da15597817316fd6fdbf3eceed20a987a869011b70295682bf6ab7234722eabd264d347cb4668316be8fcd77fb88a61876d4800490f8f30f8d29b26e4ac20bb59581d636e525f6ea28642f8d5fd5666ff74a6a9d62f5c3cf9ef5636e1fd1db7aace285923e6c84fbd3ffca52aa2b5598d58a5badf8639d0222e72f62f7f36766048f1d181f26470f19b9268cb6108dfbefb19e9231131a1d538cf8887bd7d0ab072777e6731af473e2b74a76cd86445ee498358a88b1e801a7ec33e047f246a95acba5b723bf0f71e4d755f07ca7157aafba29898ec11a9aa0b57a319cdfc9fd4def9c1cb6d84509dd67cd1bbdd004b6687eeb74618e1ef276e4f7fd26f49b79bcac064b85f0278fb3f8ca585f85b270f9b084eeb33719b9d94ddcf4c035d1898f75bd82d6bd235fcd7517dbe7e55e0da9730b5f6dfc215e0dfcb6ec10344e15f4f589747b9200478e99d06498986f393190806163bd3b7bd206420f4a67fce681006639b3581393a5b7e4fe26e9fa02ce4f7a4b5fe4744fc178bf7cfd34eb29311bd010476e6affa0eb15e8cb509652c50347ada43a2a69f4c0917ccfc752aaa569a461308fa79f60b274f4b0f574f400af4073a015c62c87bf05b0e26bf7f3369cff164c965e9904c5a9718aab9d62aba5f04995241554da9b01effa32d4de775c17ff809f2a0782c4780205f703b539fa32998e535e7162ced051b8eceebc6498786980c1ed75455a60aebb4833c8ccf6896b0998856e2d4ae3b794d4c0bf7fb181b24d73624bea0c133b3ef280685c91bd6583e95c660cfa887a4ea9ab10a5d8bf376ce7d5a4cf8667440f9fc3f858e939671df89f58e2c25761b2bff193c53dcc357f1cea6c8e96d7a6707ea7452b5c9a46863d79167f91262eb31bdef77144fbd37f9c48a08b0954d72be07fdb53771fa34dbfab4848bc5d7065cd02f13a79344ab4a53e5df5bef728ad600fb5c9a21536ba6010a25ee5b85790bffde607fefd5f000000ffff512c4122 diff --git a/kona/crates/protocol/registry/Cargo.toml b/kona/crates/protocol/registry/Cargo.toml new file mode 100644 index 0000000000000..f865ad7e569f9 --- /dev/null +++ b/kona/crates/protocol/registry/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "kona-registry" +version = "0.4.5" +description = "A registry of superchain configs" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-genesis = { workspace = true, features = ["serde"] } + +# Alloy +alloy-primitives = { workspace = true, features = ["map"] } +alloy-genesis.workspace = true +alloy-eips.workspace = true +alloy-hardforks.workspace = true +alloy-chains = { workspace = true, features = ["serde"] } +alloy-op-hardforks = { workspace = true } + +# `serde` +serde = { workspace = true, features = ["derive", "alloc"] } +serde_json = { workspace = true, features = ["raw_value"] } + +# misc +lazy_static = { workspace = true, features = ["spin_no_std"] } + +# `tabled` feature +tabled = { workspace = true, features = ["derive"], optional = true } + +[build-dependencies] +toml = { workspace = true, features = ["parse", "serde"] } +serde = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } +kona-genesis = { workspace = true, features = ["serde"] } + +[dev-dependencies] +alloy-eips.workspace = true + +[features] +default = [] +tabled = [ "dep:tabled", "std" ] +std = [ + "alloy-chains/std", + "alloy-eips/std", + "alloy-genesis/std", + "alloy-primitives/std", + "kona-genesis/std", + "serde/std", + "serde_json/std", + "tabled?/std", +] diff --git a/kona/crates/protocol/registry/README.md b/kona/crates/protocol/registry/README.md new file mode 100644 index 0000000000000..223dbc19b3fae --- /dev/null +++ b/kona/crates/protocol/registry/README.md @@ -0,0 +1,128 @@ +## `kona-registry` + +CI +kona-registry +MIT License +Docs + +[`kona-registry`][sc] is a `no_std` crate that exports rust type definitions for chains +in the [`superchain-registry`][osr]. Since it reads static files to read configurations for +various chains into instantiated objects, the [`kona-registry`][sc] crate requires +[`serde`][serde] as a dependency. To use the [`kona-registry`][sc] crate, add the crate +as a dependency to a `Cargo.toml`. + +```toml +kona-registry = "0.1.0" +``` + +[`kona-registry`][sc] declares lazy evaluated statics that expose `ChainConfig`s, `RollupConfig`s, +and `Chain` objects for all chains with static definitions in the superchain registry. The way this works +is the golang side of the superchain registry contains an "internal code generation" script that has +been modified to output configuration files to the [`crates/registry`][s] directory in the +`etc` folder that are read by the [`kona-registry`][sc] rust crate. These static config files +contain an up-to-date list of all superchain configurations with their chain configs. It is expected +that if the commit hash of the [`superchain-registry`][osr] pulled in as a git submodule has breaking +changes, the tests in this crate (`kona-registry`) will break and updates will need to be made. + +There are three core statics exposed by the [`kona-registry`][sc]. +- `CHAINS`: A list of chain objects containing the superchain metadata for this chain. +- `OPCHAINS`: A map from chain id to `ChainConfig`. +- `ROLLUP_CONFIGS`: A map from chain id to `RollupConfig`. + +[`kona-registry`][sc] exports the _complete_ list of chains within the superchain, as well as each +chain's `RollupConfig`s and `ChainConfig`s. + +### Custom chain configurations + +`kona-registry` embeds a frozen snapshot of the upstream superchain registry, but downstream +users can extend that snapshot at build time. This is useful when you need bespoke test chains or +partner networks that are not yet part of the public registry but still want to rely on the crate's +lazy statics. + +1. Produce JSON files that follow the same schema as the generated artifacts in `etc/`: + - `chainList.json` containing additional [`Chain`][chains] entries. + - `configs.json` containing [`Superchain`][superchains] structures with matching `ChainConfig`s and + `RollupConfig`s for the new chain ids. +2. Point the build to those files by setting the following environment variables during `cargo build` + (or `cargo test`): + ```sh + export KONA_CUSTOM_CONFIGS=true + export KONA_CUSTOM_CONFIGS_DIR=/absolute/path/to/custom-configs + cargo build -p kona-registry + ``` +3. The build script merges the custom files into the generated `etc/chainList.json` and + `etc/configs.json` before compiling the crate. Attempting to override existing chain ids will + result in build failures. + +Both JSON files must stay in lockstep: every chain listed in `configs.json` must also appear in +`chainList.json`, and chain identifiers must map to a single chain id. The build script validates +those invariants and will fail fast if it detects duplicates or mismatches. When publishing another +crate that depends on `kona-registry`, you can check the custom artifacts into your workspace and set +`KONA_CUSTOM_CONFIGS_DIR` via a build script or `just` recipe so that consumers automatically embed +the additional definitions. + +### Usage + +Add the following to your `Cargo.toml`. + +```toml +[dependencies] +kona-registry = "0.1.0" +``` + +To make `kona-registry` `no_std`, toggle `default-features` off like so. + +```toml +[dependencies] +kona-registry = { version = "0.1.0", default-features = false } +``` + +Below demonstrates getting the `RollupConfig` for OP Mainnet (Chain ID `10`). + +```rust +use kona_registry::ROLLUP_CONFIGS; + +let op_chain_id = 10; +let op_rollup_config = ROLLUP_CONFIGS.get(&op_chain_id); +println!("OP Mainnet Rollup Config: {:?}", op_rollup_config); +``` + +A mapping from chain id to `ChainConfig` is also available. + +```rust +use kona_registry::OPCHAINS; + +let op_chain_id = 10; +let op_chain_config = OPCHAINS.get(&op_chain_id); +println!("OP Mainnet Chain Config: {:?}", op_chain_config); +``` + + +### Feature Flags + +- `std`: Uses the standard library to pull in environment variables. + + +### Credits + +[superchain-registry][osr] contributors for building and maintaining superchain types. + +[alloy] and [op-alloy] for creating and maintaining high quality Ethereum and Optimism types in rust. + + + + +[serde]: https://crates.io/crates/serde +[alloy]: https://github.com/alloy-rs/alloy +[op-alloy]: https://github.com/alloy-rs/op-alloy +[op-superchain]: https://docs.optimism.io/stack/explainer +[osr]: https://github.com/ethereum-optimism/superchain-registry + +[s]: ./crates/registry +[sc]: https://crates.io/crates/kona-registry +[g]: https://crates.io/crates/kona-genesis + +[chains]: https://docs.rs/kona-registry/latest/kona_registry/struct.CHAINS.html +[opchains]: https://docs.rs/kona-registry/latest/kona_registry/struct.OPCHAINS.html +[rollups]: https://docs.rs/kona-registry/latest/kona_registry/struct.ROLLUP_CONFIGS.html +[superchains]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.Superchain.html diff --git a/kona/crates/protocol/registry/build.rs b/kona/crates/protocol/registry/build.rs new file mode 100644 index 0000000000000..c362b82655b4a --- /dev/null +++ b/kona/crates/protocol/registry/build.rs @@ -0,0 +1,325 @@ +//! Build script that generates a `configs.json` file from the configs. + +use std::{ + collections::{BTreeMap, BTreeSet, btree_map::Entry}, + fs, + path::{Path, PathBuf}, +}; + +use kona_genesis::{Chain, ChainConfig, ChainList, Superchain, SuperchainConfig, Superchains}; +use serde::de::DeserializeOwned; + +fn main() { + // If the `KONA_BIND` environment variable is _not_ set, then return early. + let kona_bind: bool = + std::env::var("KONA_BIND").unwrap_or_else(|_| "false".to_string()) == "true"; + println!("cargo:rerun-if-env-changed=KONA_BIND"); + if !kona_bind { + merge_custom_configs(); + return; + } + + // Get the directory of this file from the environment + let src_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + // Check if the `superchain-registry` directory exists + let superchain_registry = format!("{src_dir}/superchain-registry"); + if !std::path::Path::new(&superchain_registry).exists() { + panic!("Git Submodule missing. Please run `just source` to initialize the submodule."); + } + + // Copy the `superchain-registry/chainList.json` file to `etc/chainList.json` + let chain_list = format!("{src_dir}/superchain-registry/chainList.json"); + let etc_dir = std::path::Path::new("etc"); + if !etc_dir.exists() { + std::fs::create_dir_all(etc_dir).unwrap(); + } + std::fs::copy(chain_list, "etc/chainList.json").unwrap(); + + // Get the `superchain-registry/superchain/configs` directory` + let configs_dir = format!("{src_dir}/superchain-registry/superchain/configs"); + let configs = std::fs::read_dir(configs_dir).unwrap(); + + // Get all the directories in the `configs` directory + let mut superchains = Superchains::default(); + for config in configs { + let config = config.unwrap(); + let config_path = config.path(); + let superchain_name = config.file_name().into_string().unwrap(); + let mut superchain = + Superchain { name: superchain_name, chains: Vec::new(), ..Default::default() }; + if config_path.is_dir() { + let config_files = std::fs::read_dir(&config_path).unwrap(); + for config_file in config_files { + let config_file = config_file.unwrap(); + let config_file_path = config_file.path(); + + // Read the `superchain.toml` as the `SuperchainConfig` + let config_file_name = config_file.file_name().into_string().unwrap(); + if config_file_name == "superchain.toml" { + let config = std::fs::read_to_string(config_file_path).unwrap(); + let config: SuperchainConfig = toml::from_str(&config).unwrap(); + superchain.config = config; + continue; + } + + // Read the config file as a `ChainConfig` + let config = std::fs::read_to_string(config_file_path).unwrap(); + let config: ChainConfig = toml::from_str(&config).unwrap(); + superchain.chains.push(config); + } + superchains.superchains.push(superchain); + } + } + + // Sort the superchains by name. + superchains.superchains.sort_by(|a, b| a.name.cmp(&b.name)); + + // For each superchain, sort the list of chains by chain id. + for superchain in superchains.superchains.iter_mut() { + superchain.chains.sort_by(|a, b| a.chain_id.cmp(&b.chain_id)); + } + + let output_path = std::path::Path::new("etc/configs.json"); + std::fs::write(output_path, serde_json::to_string_pretty(&superchains).unwrap()).unwrap(); + merge_custom_configs(); +} + +fn merge_custom_configs() { + let kona_custom_configs = + std::env::var("KONA_CUSTOM_CONFIGS").unwrap_or_else(|_| "false".to_string()) == "true"; + println!("cargo:rerun-if-env-changed=KONA_CUSTOM_CONFIGS"); + println!("cargo:rerun-if-env-changed=KONA_CUSTOM_CONFIGS_TEST"); + + // if we're running tests, bust the cache if the base etc configs are updated. This ensures that + // the test build can be repeated after modifying the base configs + if std::env::var("KONA_CUSTOM_CONFIGS_TEST") == Ok("true".to_string()) { + println!("cargo:rerun-if-changed=etc/chainList.json"); + println!("cargo:rerun-if-changed=etc/configs.json"); + } + + if !kona_custom_configs { + return; + } + + let custom_configs_dir = std::env::var("KONA_CUSTOM_CONFIGS_DIR") + .expect("KONA_CUSTOM_CONFIGS_DIR must be set when KONA_CUSTOM_CONFIGS is enabled"); + println!("cargo:rerun-if-env-changed=KONA_CUSTOM_CONFIGS_DIR"); + let custom_configs_dir = PathBuf::from(custom_configs_dir); + if !custom_configs_dir.exists() { + panic!("Custom configs directory {} does not exist", custom_configs_dir.display()); + } + + let custom_chain_list_path = custom_configs_dir.join("chainList.json"); + let custom_configs_path = custom_configs_dir.join("configs.json"); + + println!("cargo:rerun-if-changed={}", custom_chain_list_path.display()); + println!("cargo:rerun-if-changed={}", custom_configs_path.display()); + + let target_chain_list = Path::new("etc/chainList.json"); + let target_superchains = Path::new("etc/configs.json"); + + validate_chain_configs(&custom_chain_list_path, &custom_configs_path); + + merge_chain_list(&custom_chain_list_path, target_chain_list); + merge_superchain_configs(&custom_configs_path, target_superchains); + validate_chain_configs(target_chain_list, target_superchains); +} + +fn merge_chain_list(custom_path: &Path, target_path: &Path) { + if !custom_path.exists() { + panic!("Custom chain list {} does not exist", custom_path.display()); + } + if !target_path.exists() { + panic!("Target chain list {} does not exist", target_path.display()); + } + + let mut merged_chain_list: ChainList = read_json(target_path); + let custom_chain_list: ChainList = read_json(custom_path); + + let mut chains_by_id: BTreeMap = BTreeMap::new(); + let mut identifiers: BTreeMap = BTreeMap::new(); + + for chain in merged_chain_list.chains.iter() { + let ident_key = chain.identifier.to_ascii_lowercase(); + identifiers.insert(ident_key, chain.clone()); + chains_by_id.insert(chain.chain_id, chain.clone()); + } + // preserve ordering of chains in etc/chainList.json + for chain in custom_chain_list.chains.iter() { + let ident_key = chain.identifier.to_ascii_lowercase(); + if let Some(existing_chain) = identifiers.get(&ident_key) { + if existing_chain == chain { + continue; + } else { + panic!( + "Chain identifier `{}` in {} already exists in the registry with a different config", + chain.identifier, + custom_path.display() + ); + } + } + if let Some(existing_chain) = chains_by_id.get(&chain.chain_id) { + if existing_chain == chain { + continue; + } else { + panic!( + "Chain id {} in {} already exists in the registry with a different config for identifier `{}`", + chain.chain_id, + custom_path.display(), + existing_chain.identifier + ); + } + } + identifiers.insert(ident_key, chain.clone()); + chains_by_id.insert(chain.chain_id, chain.clone()); + merged_chain_list.chains.push(chain.clone()); + } + + write_pretty_json(target_path, &merged_chain_list); +} + +fn merge_superchain_configs(custom_path: &Path, target_path: &Path) { + if !custom_path.exists() { + panic!("Custom configs {} does not exist", custom_path.display()); + } + if !target_path.exists() { + panic!("Target configs {} does not exist", target_path.display()); + } + + let mut superchains: BTreeMap = read_json::(target_path) + .superchains + .into_iter() + .map(|sc| (sc.name.clone(), sc)) + .collect(); + + let custom_superchains: Superchains = read_json(custom_path); + + for custom in custom_superchains.superchains { + match superchains.entry(custom.name.clone()) { + Entry::Occupied(mut entry) => { + println!( + "cargo:warning=debug: merging custom chains {}: [{}]", + custom.name, + custom.chains.iter().map(|c| c.name.as_str()).collect::>().join(",") + ); + let existing = entry.get_mut(); + *existing = merge_superchain_entry(std::mem::take(existing), custom); + } + Entry::Vacant(entry) => { + println!( + "cargo:warning=debug: inserting new custom chain {}: [{}]", + custom.name, + custom.chains.iter().map(|c| c.name.as_str()).collect::>().join(",") + ); + entry.insert(custom); + } + } + } + + let mut merged: Vec = superchains.into_values().collect(); + merged.sort_by(|a, b| a.name.cmp(&b.name)); + for superchain in merged.iter_mut() { + superchain.chains.sort_by(|a, b| a.chain_id.cmp(&b.chain_id)); + } + + let merged = Superchains { superchains: merged }; + write_pretty_json(target_path, &merged); +} + +/// Merges the custom chains to the chains in the superchain-registry, panicking on conflicts +fn merge_superchain_entry(base: Superchain, custom: Superchain) -> Superchain { + let mut merged = base; + + // maintain the ordering of chains in base + let mut chain_map: BTreeMap = + merged.chains.clone().into_iter().map(|chain| (chain.chain_id, chain)).collect(); + for chain in custom.chains { + if let Some(existing_config) = chain_map.get(&chain.chain_id) { + if existing_config == &chain { + continue; + } else { + panic!( + "conflict merging superchain `{}`: chain id {} has differing configs", + merged.name, chain.chain_id + ); + } + } + chain_map.insert(chain.chain_id, chain.clone()); + merged.chains.push(chain.clone()); + } + merged +} + +fn validate_chain_configs(chain_list_path: &Path, superchains_path: &Path) { + if !chain_list_path.exists() || !superchains_path.exists() { + return; + } + + let chain_list: ChainList = read_json(chain_list_path); + let superchains: Superchains = read_json(superchains_path); + + let mut list_chain_ids = BTreeSet::new(); + for chain in &chain_list.chains { + if !list_chain_ids.insert(chain.chain_id) { + panic!( + "Duplicate chain id {} (identifier `{}`) detected in {}", + chain.chain_id, + chain.identifier, + chain_list_path.display() + ); + } + } + + let mut config_chain_ids = BTreeSet::new(); + for superchain in &superchains.superchains { + for chain in &superchain.chains { + if !config_chain_ids.insert(chain.chain_id) { + panic!( + "Duplicate chain id {} detected across superchain configs in {}", + chain.chain_id, + superchains_path.display() + ); + } + } + } + + for chain_id in &config_chain_ids { + if !list_chain_ids.contains(chain_id) { + panic!( + "Chain id {} present in {} but missing from {}", + chain_id, + superchains_path.display(), + chain_list_path.display() + ); + } + } + + for chain in chain_list.chains { + if !config_chain_ids.contains(&chain.chain_id) { + panic!( + "Chain `{}` (chain id {}) present in {} but missing from {}", + chain.identifier, + chain.chain_id, + chain_list_path.display(), + superchains_path.display() + ); + } + } +} + +fn read_json(path: &Path) -> T { + let contents = fs::read_to_string(path) + .unwrap_or_else(|e| panic!("Failed to read {}: {e}", path.display())); + serde_json::from_str(&contents) + .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", path.display())) +} + +fn write_pretty_json(path: &Path, value: &T) { + fs::write( + path, + serde_json::to_string_pretty(value) + .unwrap_or_else(|e| panic!("Failed to serialize {}: {e}", path.display())), + ) + .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display())); +} diff --git a/kona/crates/protocol/registry/etc/chainList.json b/kona/crates/protocol/registry/etc/chainList.json new file mode 100755 index 0000000000000..a3e69f36caaeb --- /dev/null +++ b/kona/crates/protocol/registry/etc/chainList.json @@ -0,0 +1,1373 @@ +[ + { + "name": "Automata Mainnet", + "identifier": "mainnet/automata", + "chainId": 65536, + "rpc": [ + "https://rpc.ata.network" + ], + "explorers": [ + "https://explorer.ata.network" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "gasPayingToken": "0xA2120b9e674d3fC3875f415A7DF52e382F141225", + "faultProofs": { + "status": "none" + } + }, + { + "name": "BOB", + "identifier": "mainnet/bob", + "chainId": 60808, + "rpc": [ + "https://rpc.gobob.xyz" + ], + "explorers": [ + "https://explorer.gobob.xyz" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Base", + "identifier": "mainnet/base", + "chainId": 8453, + "rpc": [ + "https://mainnet.base.org" + ], + "explorers": [ + "https://explorer.base.org" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Binary Mainnet", + "identifier": "mainnet/tbn", + "chainId": 624, + "rpc": [ + "https://rpc.zero.thebinaryholdings.com" + ], + "explorers": [ + "https://explorer.thebinaryholdings.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "gasPayingToken": "0x04E9D7e336f79Cdab911b06133D3Ca2Cd0721ce3", + "faultProofs": { + "status": "none" + } + }, + { + "name": "Cyber Mainnet", + "identifier": "mainnet/cyber", + "chainId": 7560, + "rpc": [ + "https://rpc.cyber.co" + ], + "explorers": [ + "https://cyberscan.co/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Ethernity", + "identifier": "mainnet/ethernity", + "chainId": 183, + "rpc": [ + "https://mainnet.ethernitychain.io" + ], + "explorers": [ + "https://ernscan.io" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Fraxtal", + "identifier": "mainnet/fraxtal", + "chainId": 252, + "rpc": [ + "https://rpc.frax.com" + ], + "explorers": [ + "https://fraxscan.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Funki", + "identifier": "mainnet/funki", + "chainId": 33979, + "rpc": [ + "https://rpc-mainnet.funkichain.com" + ], + "explorers": [ + "https://funki.superscan.network" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "HashKey Chain", + "identifier": "mainnet/hashkeychain", + "chainId": 177, + "rpc": [ + "https://mainnet.hsk.xyz" + ], + "explorers": [ + "https://explorer.hsk.xyz" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "gasPayingToken": "0xE7C6BF469e97eEB0bFB74C8dbFF5BD47D4C1C98a", + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Ink", + "identifier": "mainnet/ink", + "chainId": 57073, + "rpc": [ + "https://rpc-gel.inkonchain.com" + ], + "explorers": [ + "https://explorer.inkonchain.com" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Lisk", + "identifier": "mainnet/lisk", + "chainId": 1135, + "rpc": [ + "https://rpc.api.lisk.com" + ], + "explorers": [ + "https://blockscout.lisk.com" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Lyra Chain", + "identifier": "mainnet/lyra", + "chainId": 957, + "rpc": [ + "https://rpc.lyra.finance" + ], + "explorers": [ + "https://explorer.lyra.finance" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Metal L2", + "identifier": "mainnet/metal", + "chainId": 1750, + "rpc": [ + "https://rpc.metall2.com" + ], + "explorers": [ + "https://explorer.metall2.com" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Mint Mainnet", + "identifier": "mainnet/mint", + "chainId": 185, + "rpc": [ + "https://rpc.mintchain.io" + ], + "explorers": [ + "https://explorer.mintchain.io" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Mode", + "identifier": "mainnet/mode", + "chainId": 34443, + "rpc": [ + "https://mainnet.mode.network" + ], + "explorers": [ + "https://explorer.mode.network" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "OP Mainnet", + "identifier": "mainnet/op", + "chainId": 10, + "rpc": [ + "https://mainnet.optimism.io" + ], + "explorers": [ + "https://explorer.optimism.io" + ], + "superchainLevel": 2, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Orderly Mainnet", + "identifier": "mainnet/orderly", + "chainId": 291, + "rpc": [ + "https://rpc.orderly.network" + ], + "explorers": [ + "https://explorer.orderly.network" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Polynomial", + "identifier": "mainnet/polynomial", + "chainId": 8008, + "rpc": [ + "https://rpc.polynomial.fi" + ], + "explorers": [ + "https://polynomialscan.io" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "RACE Mainnet", + "identifier": "mainnet/race", + "chainId": 6805, + "rpc": [ + "https://racemainnet.io" + ], + "explorers": [ + "https://racescan.io/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Redstone", + "identifier": "mainnet/redstone", + "chainId": 690, + "rpc": [ + "https://rpc.redstonechain.com" + ], + "explorers": [ + "https://explorer.redstone.xyz" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Settlus Mainnet", + "identifier": "mainnet/settlus-mainnet", + "chainId": 5371, + "rpc": [ + "https://settlus-mainnet.g.alchemy.com/public" + ], + "explorers": [ + "mainnet.settlus.network" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Shape", + "identifier": "mainnet/shape", + "chainId": 360, + "rpc": [ + "https://mainnet.shape.network/" + ], + "explorers": [ + "https://shape-mainnet.explorer.alchemy.com/" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "SnaxChain", + "identifier": "mainnet/snax", + "chainId": 2192, + "rpc": [ + "https://mainnet.snaxchain.io" + ], + "explorers": [ + "https://explorer.snaxchain.io" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Soneium", + "identifier": "mainnet/soneium", + "chainId": 1868, + "rpc": [ + "https://rpc.soneium.org" + ], + "explorers": [ + "https://soneium.blockscout.com/" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Superseed", + "identifier": "mainnet/sseed", + "chainId": 5330, + "rpc": [ + "https://mainnet.superseed.xyz" + ], + "explorers": [ + "https://explorer.superseed.xyz" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Swan Chain Mainnet", + "identifier": "mainnet/swan", + "chainId": 254, + "rpc": [ + "https://mainnet-rpc.swanchain.org" + ], + "explorers": [ + "https://swanscan.io" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Swellchain", + "identifier": "mainnet/swell", + "chainId": 1923, + "rpc": [ + "https://swell-mainnet.alt.technology" + ], + "explorers": [ + "https://explorer.swellnetwork.io" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Unichain", + "identifier": "mainnet/unichain", + "chainId": 130, + "rpc": [ + "https://mainnet.unichain.org" + ], + "explorers": [ + "https://explorer.unichain.org" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "World Chain", + "identifier": "mainnet/worldchain", + "chainId": 480, + "rpc": [ + "https://worldchain-mainnet.g.alchemy.com/public" + ], + "explorers": [ + "https://worldchain-mainnet.explorer.alchemy.com/" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Xterio Chain (ETH)", + "identifier": "mainnet/xterio-eth", + "chainId": 2702128, + "rpc": [ + "https://xterio-eth.alt.technology/" + ], + "explorers": [ + "https://eth.xterscan.io/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Zora", + "identifier": "mainnet/zora", + "chainId": 7777777, + "rpc": [ + "https://rpc.zora.energy" + ], + "explorers": [ + "https://explorer.zora.energy" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "arena-z", + "identifier": "mainnet/arena-z", + "chainId": 7897, + "rpc": [ + "https://rpc.arena-z.gg" + ], + "explorers": [ + "https://explorer.arena-z.gg" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Base Sepolia Testnet", + "identifier": "sepolia/base", + "chainId": 84532, + "rpc": [ + "https://sepolia.base.org" + ], + "explorers": [ + "https://sepolia-explorer.base.org" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Binary Sepolia", + "identifier": "sepolia/tbn", + "chainId": 625, + "rpc": [ + "https://rpc.testnet.thebinaryholdings.com" + ], + "explorers": [ + "https://explorer.sepolia.thebinaryholdings.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "gasPayingToken": "0x46d878bf7BF62Ec542953CB89Ac0bF58d991181e", + "faultProofs": { + "status": "none" + } + }, + { + "name": "Boba Sepolia Testnet", + "identifier": "sepolia/boba", + "chainId": 28882, + "rpc": [ + "https://sepolia.boba.network" + ], + "explorers": [ + "https://testnet.bobascan.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Creator Chain Testnet", + "identifier": "sepolia/creator-chain-testnet", + "chainId": 66665, + "rpc": [ + "https://rpc.creatorchain.io" + ], + "explorers": [ + "https://explorer.creatorchain.io" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Cyber Testnet", + "identifier": "sepolia/cyber", + "chainId": 111557560, + "rpc": [ + "https://rpc.testnet.cyber.co" + ], + "explorers": [ + "https://testnet.cyberscan.co/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Ethernity Testnet", + "identifier": "sepolia/ethernity", + "chainId": 233, + "rpc": [ + "https://testnet.ethernitychain.io" + ], + "explorers": [ + "https://testnet.ernscan.io" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Funki Sepolia Testnet", + "identifier": "sepolia/funki", + "chainId": 3397901, + "rpc": [ + "https://funki-testnet.alt.technology" + ], + "explorers": [ + "https://sepolia-sandbox.funkichain.com/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Ink Sepolia", + "identifier": "sepolia/ink", + "chainId": 763373, + "rpc": [ + "https://rpc-gel-sepolia.inkonchain.com" + ], + "explorers": [ + "https://explorer-sepolia.inkonchain.com" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Lisk Sepolia Testnet", + "identifier": "sepolia/lisk", + "chainId": 4202, + "rpc": [ + "https://rpc.sepolia-api.lisk.com" + ], + "explorers": [ + "https://sepolia-blockscout.lisk.com" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Metal L2 Testnet", + "identifier": "sepolia/metal", + "chainId": 1740, + "rpc": [ + "https://testnet.rpc.metall2.com" + ], + "explorers": [ + "https://testnet.explorer.metall2.com" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Mode Testnet", + "identifier": "sepolia/mode", + "chainId": 919, + "rpc": [ + "https://sepolia.mode.network" + ], + "explorers": [ + "https://sepolia.explorer.mode.network" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "OP Sepolia Testnet", + "identifier": "sepolia/op", + "chainId": 11155420, + "rpc": [ + "https://sepolia.optimism.io" + ], + "explorers": [ + "https://testnet-explorer.optimism.io" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Ozean Poseidon Testnet", + "identifier": "sepolia/ozean", + "chainId": 7849306, + "rpc": [ + "https://ozean-testnet.rpc.caldera.xyz/http" + ], + "explorers": [ + "https://ozean-testnet.explorer.caldera.xyz" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "gasPayingToken": "0x43bd82D1e29a1bEC03AfD11D5a3252779b8c760c", + "faultProofs": { + "status": "none" + } + }, + { + "name": "Pivotal Sepolia", + "identifier": "sepolia/pivotal", + "chainId": 16481, + "rpc": [ + "https://sepolia.pivotalprotocol.com/" + ], + "explorers": [ + "https://sepolia.pivotalscan.org/" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "RACE Testnet", + "identifier": "sepolia/race", + "chainId": 6806, + "rpc": [ + "https://racetestnet.io" + ], + "explorers": [ + "https://testnet.racescan.io/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Settlus Sepolia", + "identifier": "sepolia/settlus-sepolia", + "chainId": 5373, + "rpc": [ + "https://settlus-septestnet.g.alchemy.com/public" + ], + "explorers": [ + "sepolia.settlus.network" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Shape Sepolia Testnet", + "identifier": "sepolia/shape", + "chainId": 11011, + "rpc": [ + "https://sepolia.shape.network/" + ], + "explorers": [ + "https://shape-sepolia.explorer.alchemy.com/" + ], + "superchainLevel": 1, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Soneium Testnet Minato", + "identifier": "sepolia/soneium-minato", + "chainId": 1946, + "rpc": [ + "https://rpc.minato.soneium.org" + ], + "explorers": [ + "https://soneium-minato.blockscout.com/" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Unichain Sepolia Testnet", + "identifier": "sepolia/unichain", + "chainId": 1301, + "rpc": [ + "https://sepolia.unichain.org" + ], + "explorers": [ + "https://sepolia.uniscan.xyz" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "World Chain Sepolia Testnet", + "identifier": "sepolia/worldchain", + "chainId": 4801, + "rpc": [ + "https://worldchain-sepolia.g.alchemy.com/public" + ], + "explorers": [ + "https://worldchain-sepolia.explorer.alchemy.com/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Zora Sepolia Testnet", + "identifier": "sepolia/zora", + "chainId": 999999999, + "rpc": [ + "https://sepolia.rpc.zora.energy" + ], + "explorers": [ + "https://sepolia.explorer.zora.energy" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Base devnet 0", + "identifier": "sepolia-dev-0/base-devnet-0", + "chainId": 11763072, + "rpc": [ + "" + ], + "explorers": [ + "" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia-dev-0" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "OP Labs Sepolia devnet 0", + "identifier": "sepolia-dev-0/oplabs-devnet-0", + "chainId": 11155421, + "rpc": [ + "" + ], + "explorers": [ + "" + ], + "superchainLevel": 0, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia-dev-0" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "Boba Mainnet", + "identifier": "mainnet/boba", + "chainId": 288, + "rpc": [ + "https://mainnet.boba.network" + ], + "explorers": [ + "https://bobascan.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Radius testnet", + "identifier": "sepolia/radius_testnet", + "chainId": 863, + "rpc": [ + "testnet-rpc.theradius.xyz" + ], + "explorers": [ + "https://testnet-rpc.theradius.xyz/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Camp Network Testnet V2", + "identifier": "sepolia/camp", + "chainId": 325000, + "rpc": [ + "https://rpc.camp-network-testnet.gelato.digital" + ], + "explorers": [ + "https://camp-network-testnet.blockscout.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "Celo", + "identifier": "mainnet/celo", + "chainId": 42220, + "rpc": [ + "https://forno.celo.org" + ], + "explorers": [ + "https://celoscan.io/" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "gasPayingToken": "0x057898f3C43F129a17517B9056D23851F124b19f", + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "rehearsal-0-bn-0", + "identifier": "rehearsal-0-bn/rehearsal-0-bn-0", + "chainId": 420120009, + "rpc": [ + "" + ], + "explorers": [ + "" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "rehearsal-0-bn" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "rehearsal-0-bn-1", + "identifier": "rehearsal-0-bn/rehearsal-0-bn-1", + "chainId": 420120010, + "rpc": [ + "" + ], + "explorers": [ + "" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "rehearsal-0-bn" + }, + "faultProofs": { + "status": "permissionless" + } + }, + { + "name": "arena-z-testnet", + "identifier": "sepolia/arena-z", + "chainId": 9899, + "rpc": [ + "https://testnet-rpc.arena-z.gg" + ], + "explorers": [ + "https://testnet-explorer.arena-z.gg" + ], + "superchainLevel": 1, + "governedByOptimism": true, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "sepolia" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "Silent Data Mainnet", + "identifier": "mainnet/silent-data-mainnet", + "chainId": 380929, + "rpc": [ + "https://mainnet.silentdata.com/${SILENTDATA_AUTH_TOKEN}" + ], + "explorers": [ + "https://explorer-mainnet.rollup.silentdata.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "alt-da", + "parent": { + "type": "L2", + "chain": "mainnet" + }, + "faultProofs": { + "status": "permissioned" + } + }, + { + "name": "test1", + "identifier": "test1/testnet", + "chainId": 123999119, + "rpc": [ + "https://rpc.test1.com" + ], + "explorers": [ + "https://explorer.test1.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "testnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "test2", + "identifier": "test2/testnet", + "chainId": 223999119, + "rpc": [ + "https://rpc.test2.com" + ], + "explorers": [ + "https://explorer.test2.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "testnet" + }, + "faultProofs": { + "status": "none" + } + } +] \ No newline at end of file diff --git a/kona/crates/protocol/registry/etc/configs.json b/kona/crates/protocol/registry/etc/configs.json new file mode 100644 index 0000000000000..1100b4484bdd7 --- /dev/null +++ b/kona/crates/protocol/registry/etc/configs.json @@ -0,0 +1,5652 @@ +{ + "superchains": [ + { + "name": "mainnet", + "config": { + "name": "Mainnet", + "l1": { + "chain_id": 1, + "public_rpc": "https://ethereum-rpc.publicnode.com", + "explorer": "https://eth.blockscout.com" + }, + "hardforks": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "protocol_versions_addr": "0x8062abc286f5e7d9428a0ccb9abd71e50d93b935", + "superchain_config_addr": "0x95703e0982140d16f8eba6d158fccede42f04a4c", + "op_contracts_manager_proxy_addr": null + }, + "chains": [ + { + "Name": "OP Mainnet", + "PublicRPC": "https://mainnet.optimism.io", + "SequencerRPC": "https://mainnet-sequencer.optimism.io", + "Explorer": "https://explorer.optimism.io", + "SuperchainLevel": 2, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 10, + "batch_inbox_address": "0xff00000000000000000000000000000000000010", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17422590, + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108" + }, + "l2": { + "number": 105235063, + "hash": "0xdbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3" + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddr": "0x6887246668a3b87f54deb3b94ba47a6f63f32985", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x99c9fc46f92e8a1c0dec1b1747d010903e884be1", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xbeb5fc579115071764c7423a4f12edde41f106ed", + "SystemConfigProxy": "0x229047fed2591dbec1ef1118d64f7af3db9eb290", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xe5965ab5962edc7477c8520243a95517cd252fa9", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Unichain", + "PublicRPC": "https://mainnet.unichain.org", + "SequencerRPC": "https://mainnet-sequencer.unichain.org", + "Explorer": "https://explorer.unichain.org", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 130, + "batch_inbox_address": "0xff00000000000000000000000000000000000130", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21116363, + "hash": "0x8c799ed21bd97471f76ba48ea54b30f8169c7e4e4a22acfba06951b571e0f4b6" + }, + "l2": { + "number": 0, + "hash": "0x3425162ddf41a0a1f0106d67b71828c9a9577e6ddeb94e4f33d2cde1fdc3befe" + }, + "l2_time": 1730748359, + "system_config": { + "batcherAddr": "0x2f60a5184c63ca94f82a27100643dbabe4f3f7fd", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000dbba0000007d0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x6d5b183f538abb8572f5cd17109c617b994d5833", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x81014f44b0a345033bb2b3b21c7a1a308b35feea", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x0bd48f6b86a26d3a217d0fa6ffe2b491b956a7a2", + "SystemConfigProxy": "0xc407398d063f942febbcc6f80a156b47f3f1bda6", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x2f12d621a16e2d3285929c9996f478508951dfe4", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "HashKey Chain", + "PublicRPC": "https://mainnet.hsk.xyz", + "SequencerRPC": "https://hashkeychain-mainnet.alt.technology", + "Explorer": "https://explorer.hsk.xyz", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 177, + "batch_inbox_address": "0x0004cb44c80b6fbf8ceb1d80af688c9f7c0b2ab5", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": "0xe7c6bf469e97eeb0bfb74c8dbff5bd47d4c1c98a", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21414667, + "hash": "0x0b4540b35529055a65950aef56df1a8fe22144f453f3bbc521a1248a8edbf761" + }, + "l2": { + "number": 0, + "hash": "0xa96aea946b763641b616ce0c69f37e61d9cd0abd709ef13a6b833e67b76de208" + }, + "l2_time": 1734347135, + "system_config": { + "batcherAddr": "0x9391791f7cb74f8bfda65edc0749efd964311b55", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000ffffffff01312d00", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x441f31c4cdf772558d4ea31f3114de59ae145e7c", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x2171e6d3b7964fa9654ce41da8a8ffaff2cc70be", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xe7aa79b59cac06f9706d896a047feb9d3bda8bd3", + "SystemConfigProxy": "0x43f8defe3e9286d152e91bb16a248808e7247198", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x04ec030f362ce5a0b5fe2d4b4219f287c2ebde50", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Ethernity", + "PublicRPC": "https://mainnet.ethernitychain.io", + "SequencerRPC": "https://mainnet.ethernitychain.io", + "Explorer": "https://ernscan.io", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 183, + "batch_inbox_address": "0xff00000000000000000000000000000000000183", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 20, + "eip1559Denominator": 1000, + "eip1559DenominatorCanyon": 1000 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20519340, + "hash": "0xbb7ad201b0ab296465865642878e9256e0fa4a3f8d06bca1f3e5c5b54be6be90" + }, + "l2": { + "number": 0, + "hash": "0xa5e974a6ace39285b42f92316a9e040e58f8ffc24e3aac124ff6243c28323607" + }, + "l2_time": 1723547735, + "system_config": { + "batcherAddr": "0x43ca061ea80fbb4a2b5515f4be4e953b191147af", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xb68361aaac2bc8a4b8bfe36b8c6d0b429b5930ea", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x908c324c35ff36f64236a7cda4d50f3003e9c5c3", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xda29f0b4da6c23f6c1af273945c290c0268c4ea9", + "SystemConfigProxy": "0x20c3035c92bdb4c461242571eeac59eed03df931", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xfcdb270b674911d321f1014c347eabb1c55134fb", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Mint Mainnet", + "PublicRPC": "https://rpc.mintchain.io", + "SequencerRPC": "https://rpc.mintchain.io", + "Explorer": "https://explorer.mintchain.io", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 185, + "batch_inbox_address": "0x4e31448a098393727b786e25b54e59dca1b77fe1", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19861561, + "hash": "0x12980bfeb6e787dc906b0a6ae000733f9909f6af2121bc78a77afb5ba77988a0" + }, + "l2": { + "number": 0, + "hash": "0x88441835ca2344ea384e6f73e3d6f921cdd304e5bd6dca15590217e3911c61a3" + }, + "l2_time": 1715608931, + "system_config": { + "batcherAddr": "0x68bdfece01535090c8f3c27ec3b1ae97e83fa4aa", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x2b3f201543adf73160ba42e1a5b7750024f30420", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x59625d1fe0eeb8114a4d13c863978f39b3471781", + "SystemConfigProxy": "0xc975862927797812371a9fb631f83f8f5e2240d5", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xd2922a726501f027a5a5ac122bec92bcfb437662", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Fraxtal", + "PublicRPC": "https://rpc.frax.com", + "SequencerRPC": "https://rpc.frax.com", + "Explorer": "https://fraxscan.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 252, + "batch_inbox_address": "0xff000000000000000000000000000000000420fc", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 1717002001, + "ecotone_time": 1717009201, + "fjord_time": 1733947201, + "granite_time": 1738958401 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19135251, + "hash": "0x728dfc57386f6c2a646e03b7346f567b3117b7f487c6f590392abd1509248053" + }, + "l2": { + "number": 0, + "hash": "0x521982bd54239dc71269eefb58601762cc15cfb2978e0becb46af7962ed6bfaa" + }, + "l2_time": 1706810711, + "system_config": { + "batcherAddr": "0x6017f75108f251a488b045a7ce2a7c15b179d1f2", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xe0d7755252873c4ef5788f7f45764e0e17610508", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x34c0bd5877a5ee7099d0f5688d65f4bb9158bde2", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x36cb65c1967a0fb0eee11569c51c2f2aa1ca6f6d", + "SystemConfigProxy": "0x34a9f273cbd847d49c3de015fc26c3e66825f8b2", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": null, + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Swan Chain Mainnet", + "PublicRPC": "https://mainnet-rpc.swanchain.org", + "SequencerRPC": "https://sequencer-mainnet.swanchain.org", + "Explorer": "https://swanscan.io", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 254, + "batch_inbox_address": "0xff00000000000000000000000000000000000254", + "block_time": 5, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20112576, + "hash": "0x90dafe09640e2858bce2675c66da6b0f136215a179d49f5c9274252bd5a1f8a2" + }, + "l2": { + "number": 0, + "hash": "0x834c39d79eb75c4c52d745e624750b817b66eb13e483b035c42ec3aa5cfa7429" + }, + "l2_time": 1718640215, + "system_config": { + "batcherAddr": "0xde794bec196832474f2f218135bfd0f7ca7fb038", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x6197f64902b9275e6815f9a5b641ed2291a5d39c", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xed7525946a09056c6aae29941b8323017382050e", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xba50434bc5fcc07406b1bad9ac72a4cdf776db15", + "SystemConfigProxy": "0x504d56cf68f791b45e3a2e895b0e1562f3431328", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x2069fc7097b7784fca21aa459e57e95c0046eecd", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Boba Mainnet", + "PublicRPC": "https://mainnet.boba.network", + "SequencerRPC": "https://mainnet.boba.network", + "Explorer": "https://bobascan.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 288, + "batch_inbox_address": "0xfff0000000000000000000000000000000000288", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1713302879, + "delta_time": 1713302879, + "ecotone_time": 1713302880, + "fjord_time": 1725951600, + "granite_time": 1729753200, + "holocene_time": 1738785600 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19670718, + "hash": "0x945d6244d259e63892abf93e5e6dd3388b79e25ae5ec0502e290a0d0163aa5cf" + }, + "l2": { + "number": 1149019, + "hash": "0x0a555516317be2719d9befcbcca5f5516b6b7ce0f05b759f5a166b697a8a0fbd" + }, + "l2_time": 1713302879, + "system_config": { + "batcherAddr": "0xe1b64045351b0b6e9821f19b39f81bc4711d2230", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x56121a8612474c3eb65d69a3b871f284705b9bc4", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xdc1664458d2f0b6090bea60a8793a4e66c2f1c00", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x7b02d13904d8e6e0f0efaf756ab14cb0ff21ee7e", + "SystemConfigProxy": "0x158fd5715f16ac1f2dc959a299b383aaaf9b59eb", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xf45a5f1e36fcea3cc830a98c6c3c5cea7d6af852", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Orderly Mainnet", + "PublicRPC": "https://rpc.orderly.network", + "SequencerRPC": "https://rpc.orderly.network", + "Explorer": "https://explorer.orderly.network", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 291, + "batch_inbox_address": "0x08aa34cc843ceebcc88a627f18430294aa9780be", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 18292529, + "hash": "0x787d5dd296d63bc6e7a4158d4f109e1260740ee115f5ed5124b35dece1fa3968" + }, + "l2": { + "number": 0, + "hash": "0xe53c94ddd42714239429bd132ba2fa080c7e5cc7dca816ec6e482ec0418e6d7f" + }, + "l2_time": 1696608227, + "system_config": { + "batcherAddr": "0xf8db8aba597ff36ccd16fecfbb1b816b3236e9b8", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xe07ea0436100918f157df35d01dce5c11b16d1f1", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x91493a61ab83b62943e6dcaa5475dd330704cc84", + "SystemConfigProxy": "0x886b187c3d293b1449a3a0f23ca9e2269e0f2664", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xc8bf04a73704051e5e274f1b43b1f2f153db2136", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Shape", + "PublicRPC": "https://mainnet.shape.network/", + "SequencerRPC": "https://shape-mainnet-sequencer.g.alchemy.com", + "Explorer": "https://shape-mainnet.explorer.alchemy.com/", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 360, + "batch_inbox_address": "0xff00000000000000000000000000000000000360", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 1727370000 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20369793, + "hash": "0x64772c7ca14e37ca9f0662b7d16b250f486218656012c0da994396608584b2b1" + }, + "l2": { + "number": 0, + "hash": "0xf17c665ffdebe214ec214bcd798c725141e7b5c29799500abd6a8738d15bdebe" + }, + "l2_time": 1721744471, + "system_config": { + "batcherAddr": "0xf7ca543d652e38692fd12f989eb55b5327ec9a20", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xa4fb12d15eb85dc9284a7df0adbc8b696edbbf1d", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x62edd5f4930ea92dca3fb81689bdd9b9d076b57b", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xeb06ffa16011b5628bab98e29776361c83741dd3", + "SystemConfigProxy": "0xff11e41d5c4f522e423ff6c064ff8d55af8f7355", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x2c03e8bf8b16af89079852be87f0e9ec674a5952", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "World Chain", + "PublicRPC": "https://worldchain-mainnet.g.alchemy.com/public", + "SequencerRPC": "https://worldchain-mainnet-sequencer.g.alchemy.com", + "Explorer": "https://worldchain-mainnet.explorer.alchemy.com/", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 480, + "batch_inbox_address": "0xff00000000000000000000000000000000000480", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1721826000, + "granite_time": 1727780400, + "holocene_time": 1738238400, + "isthmus_time": 1764072000 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20170118, + "hash": "0x793daed4743301e00143be5533cd1dce0a741519e9e9c96588a9ad7dbb4d8db4" + }, + "l2": { + "number": 0, + "hash": "0x70d316d2e0973b62332ba2e9768dd7854298d7ffe77f0409ffdb8d859f2d3fa3" + }, + "l2_time": 1719335639, + "system_config": { + "batcherAddr": "0xdbbe3d8c2d2b22a2611c5a94a9a12c2fcd49eb29", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 100000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xa4fb12d15eb85dc9284a7df0adbc8b696edbbf1d", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x470458c91978d2d929704489ad730dc3e3001113", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xd5ec14a83b7d95be1e2ac12523e2dee12cbeea6c", + "SystemConfigProxy": "0x6ab0777fd0e609ce58f939a7f70fe41f5aa6300a", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x069c4c579671f8c120b1327a73217d01ea2ec5ea", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Binary Mainnet", + "PublicRPC": "https://rpc.zero.thebinaryholdings.com", + "SequencerRPC": "https://sequencer.bnry.mainnet.zeeve.net", + "Explorer": "https://explorer.thebinaryholdings.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": 1726070401, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 624, + "batch_inbox_address": "0xff00000000000000000000000000000000000624", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": "0x04e9d7e336f79cdab911b06133d3ca2cd0721ce3", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1725536344, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20175246, + "hash": "0xdcc5838ee3dd0af995c87bec9614a09f08dd8979014876b42fd7e3ae044dd8c4" + }, + "l2": { + "number": 0, + "hash": "0xe222b4b07ee9c885d13ee341823c92aa449f9769ac68fb5f1e1d4e602a990a4a" + }, + "l2_time": 1719397463, + "system_config": { + "batcherAddr": "0x7f9d9c1bce1062e1077845ea39a0303429600a06", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x48ec051349ddc7e8babafcbfe27696ecf2a8a8b3", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xd1b30378cbf968e5525e8835219a5726a1e71d10", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x5ff88fcf8e9947f45f4caf8ffd5231b5ddf05e0a", + "SystemConfigProxy": "0x7ac7e5989eac278b7bbfef560871a2026bad472c", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x0d7e0590c58e4ac9b14b3ed6163cf55223931699", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Redstone", + "PublicRPC": "https://rpc.redstonechain.com", + "SequencerRPC": "https://rpc.redstonechain.com", + "Explorer": "https://explorer.redstone.xyz", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 690, + "batch_inbox_address": "0xff00000000000000000000000000000000000690", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0 + }, + "optimism": { + "eip1559Elasticity": 2, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 50 + }, + "alt_da": { + "da_challenge_address": "0x97a2da87d3439b172e6dd027220e01c9cb565b80", + "da_challenge_window": 3600, + "da_resolve_window": 3600, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 19578374, + "hash": "0xb9ec694afdde2e2ed661ed8ec56dace5cf8723801342fa1229e693f2a98af672" + }, + "l2": { + "number": 0, + "hash": "0xa4f55631013577464810893a05b18f07fe483885a6ef93e0060e7128bdf4ca3b" + }, + "l2_time": 1712185091, + "system_config": { + "batcherAddr": "0xa31cb9bc414601171d4537580f98f66c03aecd43", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000001", + "scalar": "0x0000000000000000000000000000000000000000000000000000000000001def", + "gasLimit": 100000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x70fdbcb066ed3621647ddf61a1f40aac6058bc89", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xc473ca7e02af24c129c2eef51f2adf0411c1df69", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xc7bcb0e8839a28a1cfadd1cf716de9016cda51ae", + "SystemConfigProxy": "0x8f2428f7189c0d92d1c4a5358903a8c80ec6a69d", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x8f68e849eaf8eb943536f9d1d49ea9c9b5868b98", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Lyra Chain", + "PublicRPC": "https://rpc.lyra.finance", + "SequencerRPC": "https://rpc.lyra.finance", + "Explorer": "https://explorer.lyra.finance", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 957, + "batch_inbox_address": "0x5f7f7f6db967f0ef10bda0678964dba185d16c50", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 18574841, + "hash": "0x00b06b23108483a0b6af8ff726b5ed3f508b7986f72c12679b10d72c05839716" + }, + "l2": { + "number": 0, + "hash": "0x047f535b3da7ad4f96d353b5a439740b7591413daee0e2f27dd3aabb24581af2" + }, + "l2_time": 1700021615, + "system_config": { + "batcherAddr": "0x14e4e97bdc195d399ad8e7fc14451c279fe04c8e", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x61e44dc0dae6888b5a301887732217d5725b0bff", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x85ea9c11cf3d4786027f7fd08f4406b15777e5f8", + "SystemConfigProxy": "0x0e4c4cdd01cecb01070e9fdfe7600871e4ae996e", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x87daff495b5f6c4f79ceeaaf85f1ef3df3b30d21", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Lisk", + "PublicRPC": "https://rpc.api.lisk.com", + "SequencerRPC": "https://rpc.api.lisk.com", + "Explorer": "https://blockscout.lisk.com", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1135, + "batch_inbox_address": "0xff00000000000000000000000000000000001135", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 20, + "eip1559Denominator": 1000, + "eip1559DenominatorCanyon": 1000 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19788720, + "hash": "0xd580bdbd001908860f225c16ddaa08ada64471a68435694760c111253d97ffce" + }, + "l2": { + "number": 0, + "hash": "0x5a693d1d8ee27b8e62868d0349af430a2d2e173c8c8988e7b0c9ef91893cbf00" + }, + "l2_time": 1714728791, + "system_config": { + "batcherAddr": "0xa6ea2f3299b63c53143c993d2d5e60a69cd6fe24", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xecd4150abbb1ebff13f74e42fb43c3d78b4e0b45", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x2658723bf70c7667de6b25f99fcce13a16d25d08", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x26db93f8b8b4f7016240af62f7730979d353f9a7", + "SystemConfigProxy": "0x05f23282ffdca8286e4738c1af79079f3d843750", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x0cf7d3706a27cce2017aeb11e8a9c8b5388c282c", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Metal L2", + "PublicRPC": "https://rpc.metall2.com", + "SequencerRPC": "https://rpc.metall2.com", + "Explorer": "https://explorer.metall2.com", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1750, + "batch_inbox_address": "0xc83f7d9f2d4a76e81145849381aba02602373723", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19527340, + "hash": "0x2493565ce8472656b7c22377c8d4d8ef5d17f78392c799ca5f2429b01e2c159c" + }, + "l2": { + "number": 0, + "hash": "0xd31c12ffff2d563897ad9a041c0d26790d635911bdbbfa589347fa955f75281e" + }, + "l2_time": 1711563515, + "system_config": { + "batcherAddr": "0xc94c243f8fb37223f3eb2f7961f7072602a51b8b", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x6d0f65d59b55b0fec5d2d15365154dcadc140bf3", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x3f37abde2c6b5b2ed6f8045787df1ed1e3753956", + "SystemConfigProxy": "0x7bd909970b0eedcf078de6aeff23ce571663b8aa", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x7bfff391a2dbbdc68a259792ac9748f50fcde93e", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Soneium", + "PublicRPC": "https://rpc.soneium.org", + "SequencerRPC": "https://rpc.soneium.org", + "Explorer": "https://soneium.blockscout.com/", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1738573200, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1868, + "batch_inbox_address": "0x008dc74cecc9deda8595b2fe210ce5979f0bfa8e", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1738573200, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21314185, + "hash": "0x68b4ad05c3dafc2613853d26cf39f0e1716e986f879207440f187b56c12cb4d6" + }, + "l2": { + "number": 0, + "hash": "0x295d22d269634c7d0055b33b887519362d0b31899e97109d1789a8a168de1b21" + }, + "l2_time": 1733134751, + "system_config": { + "batcherAddr": "0x6776be80dbada6a02b5f2095cf13734ac303b8d1", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xeb9bf100225c214efc3e7c651ebbadcf85177607", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x88e529a6ccd302c948689cd5156c83d4614fae92", + "SystemConfigProxy": "0x7a8ed66b319911a0f3e7288bddab30d9c0c875c3", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x512a3d2c7a43bd9261d2b8e8c9c70d4bd4d503c0", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Swellchain", + "PublicRPC": "https://swell-mainnet.alt.technology", + "SequencerRPC": "https://swell-mainnet.alt.technology", + "Explorer": "https://explorer.swellnetwork.io", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1764691201, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1923, + "batch_inbox_address": "0x005de5857e38dfd703a1725c0900e9c6f24cbde0", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1752732000, + "isthmus_time": 1764648001, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21277945, + "hash": "0xd904f8475cc7b74a9ef4749d0ee9aeca3b233aa3dc143667e8a20ffe43789a26" + }, + "l2": { + "number": 0, + "hash": "0x92379973a1576876b7337a9ce89e2a7a9cb99887f55e6045ed2069d5d98d9319" + }, + "l2_time": 1732696703, + "system_config": { + "batcherAddr": "0xf854cd5b26bfd73d51236c0122798907ed65b1f2", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x7aa4960908b13d104bf056b23e2c76b43c5aacc8", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x758e0ee66102816f5c3ec9ecc1188860fbb87812", + "SystemConfigProxy": "0xd3d4c6b703978a5d24fecf3a70a51127667ff1a4", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x87690676786cdc8cca75a472e483af7c8f2f0f57", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "SnaxChain", + "PublicRPC": "https://mainnet.snaxchain.io", + "SequencerRPC": "https://mainnet.snaxchain.io", + "Explorer": "https://explorer.snaxchain.io", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 2192, + "batch_inbox_address": "0xfec57bd3729a5f930d4ee8ac5992fdc8988426e4", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20520542, + "hash": "0x920dc48c1f037d444cb4dee5c69f41853f469dd9e7751398458126a6f76ecea6" + }, + "l2": { + "number": 0, + "hash": "0x518aadbc56e4ca8b03aa141c13b2fc246a9eae88edea09ee477f3d620b00d5ae" + }, + "l2_time": 1723562231, + "system_config": { + "batcherAddr": "0x060b915ca4904b56ada63565626b9c97f6cad212", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xa5fb68c24b02852e8b514e98a1014faf12547fa5", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x936d881b4760d5e9b6d55b774f65c509236b4743", + "SystemConfigProxy": "0x9c9b78f798f821c2f6398f603825fd175e2427f9", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x0fd13c7f11d95070ce5cf31baf1acf9355bf4578", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Superseed", + "PublicRPC": "https://mainnet.superseed.xyz", + "SequencerRPC": "https://mainnet.superseed.xyz", + "Explorer": "https://explorer.superseed.xyz", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 5330, + "batch_inbox_address": "0x8612014a343089f1ddbacfd42baf4afbf9133593", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 1726185601, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20737477, + "hash": "0x32566bf75b83734e532a687ad0c1d0b884e544815498401696ca5a49df238cf2" + }, + "l2": { + "number": 0, + "hash": "0x0e5eb156fd2ea921d57ee4921191a2a46ebe3a11854ee7a63a5c6c580aa99219" + }, + "l2_time": 1726179683, + "system_config": { + "batcherAddr": "0xa9b074b27de97f492f8f07fd7c213400e4ca5391", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x8b0576e39f1233679109f9b40cfcc2a7e0901ede", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x2c2150aa5c75a24fb93d4fd2f2a895d618054f07", + "SystemConfigProxy": "0x525a2744134805516a45b8abb6aa0aa1da3809f6", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x657c1b0e31ffc69a02b207be20699bdff938c7e7", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Settlus Mainnet", + "PublicRPC": "https://settlus-mainnet.g.alchemy.com/public", + "SequencerRPC": "https://settlus-mainnet-sequencer.g.alchemy.com/", + "Explorer": "mainnet.settlus.network", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 5371, + "batch_inbox_address": "0x003e40d3125591bd722ab1bb880c78e4d74d0977", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21897258, + "hash": "0x7737a04d48b78bd5ce39fdd16a5ecf652e3c35ee5226a56869aa0cbb230acbcb" + }, + "l2": { + "number": 0, + "hash": "0x47a86b86dc4ba2e8c877515717dab337673a24dc3fb06d69ac9f07e9949a48c6" + }, + "l2_time": 1740171575, + "system_config": { + "batcherAddr": "0xd0b4c3ac8a50b6f1b3949adaf55cc9805620eb57", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xa4fb12d15eb85dc9284a7df0adbc8b696edbbf1d", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xfd4918e51d1e5aa2195c42654cf769b152c9d9c0", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xfc1d560eb01443e31b0eb56620703e80e42a7e4e", + "SystemConfigProxy": "0x15c1daed5443a77b4dcf6fe35cafccebb0c6da0e", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xde9fda9c499ba1c0168ac083acf5bec5cc67fa76", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "RACE Mainnet", + "PublicRPC": "https://racemainnet.io", + "SequencerRPC": "https://racemainnet.io", + "Explorer": "https://racescan.io/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 6805, + "batch_inbox_address": "0xff00000000000000000000000000000000006805", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20260129, + "hash": "0xb6fd41e6c3515172c36d3912046264475eaad84c2c56e99d74f4abd1a75b63c9" + }, + "l2": { + "number": 0, + "hash": "0xa864791943836c37b40ea688f3853f2198afb683a3e168d48bfa76c9896e3e65" + }, + "l2_time": 1720421591, + "system_config": { + "batcherAddr": "0x8cda8351236199af7532bad53d683ddd9b275d89", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a669b2193718f189b0576c0cdcedfed6f40f9ea", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x680969a6c58183987c8126ca4de6b59c6540cd2a", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x0485ca8a73682b3d3f5ae98cdca1e5b512e728e9", + "SystemConfigProxy": "0xcf6a32db8b3313b3d439ce6909511c2c3415fa32", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": null, + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Cyber Mainnet", + "PublicRPC": "https://rpc.cyber.co", + "SequencerRPC": "https://cyber.alt.technology/", + "Explorer": "https://cyberscan.co/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 7560, + "batch_inbox_address": "0xff00000000000000000000000000000000001d88", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0x10e34efe14e4d270c0f77bf1af01b6c832161b49", + "da_challenge_window": 3600, + "da_resolve_window": 3600, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 19681125, + "hash": "0x8fa2a34b37420b863aa33da7661a8929af8bd7a436b9e612c727eb5e63da3879" + }, + "l2": { + "number": 0, + "hash": "0x0bbf36c47db3b42b40b2e6cadc183612337d1ac8602eb9e57071ae0043e70fc7" + }, + "l2_time": 1713428567, + "system_config": { + "batcherAddr": "0xf0748c52edc23135d9845cdfb91279cf61ee14b4", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000b4", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xc2259e7fb719411f97abdcdf449f6ba3b9d75398", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x12a580c05466eefb2c467c6b115844cdaf55b255", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x1d59bc9fce6b8e2b1bf86d4777289ffd83d24c99", + "SystemConfigProxy": "0x5d1f4bbaf6d484fa9d5d9705f92de6063bff6055", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xbf4676f21a7889e0fd61bcdc9b98e60b01c1b36f", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "arena-z", + "PublicRPC": "https://rpc.arena-z.gg", + "SequencerRPC": "https://rpc.arena-z.gg", + "Explorer": "https://explorer.arena-z.gg", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1736445601, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 7897, + "batch_inbox_address": "0x00f9bcee08dce4f0e7906c1f6cfb10c77802eed0", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 20, + "eip1559Denominator": 2000, + "eip1559DenominatorCanyon": 2000 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21167590, + "hash": "0x5e7db5c04973dd6e06ac7e2abf5b9373089f0f09e8ae8231642f0c6aff177e27" + }, + "l2": { + "number": 0, + "hash": "0xbe7112a730b1fae8d94115271adc600559ebe87c75df1d2df9414bd7298eb7fb" + }, + "l2_time": 1731366083, + "system_config": { + "batcherAddr": "0x2b8733e8c60a928b19bb7db1d79b918e8e09ac8c", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c3a30000060a4", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x564eb0cefcca86160649a8986c419693c82f3678", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xb20f99b598e8d888d1887715439851bc68806b22", + "SystemConfigProxy": "0x34a564bbd863c4bf73eca711cf38a77c4ccbdd6a", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x658656a14afdf9c507096ac406564497d13ec754", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Polynomial", + "PublicRPC": "https://rpc.polynomial.fi", + "SequencerRPC": "https://rpc.polynomial.fi", + "Explorer": "https://polynomialscan.io", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 8008, + "batch_inbox_address": "0x0bd57e83b5e0f9ecd84d559bb58e1ecfeedd2565", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 20062729, + "hash": "0xc6d18992ff6e4c9e4798d34a7b97423a7ec276e9f2bbecfd4bb69ccdd49f516a" + }, + "l2": { + "number": 0, + "hash": "0x1b5e76ad2f290fb6fa2c60755d8c52942dbefedab76d01091e24080d5c129fd0" + }, + "l2_time": 1718038175, + "system_config": { + "batcherAddr": "0x67a44ce38627f46f20b1293960559ed85dd194f1", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a4962275df8c60a80d3a25faec5aa7de116a746", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x3be64bf2b9c2de637067c7aab6bae5edf9feba55", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x034cbb620d1e0e4c2e29845229beac57083b04ec", + "SystemConfigProxy": "0x58b51fb9feed00dd846f91d265eba3cdd855a413", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xe9394679d0f0676e4a2de99f8ed6b4acb16c5f0f", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Base", + "PublicRPC": "https://mainnet.base.org", + "SequencerRPC": "https://mainnet-sequencer.base.org", + "Explorer": "https://explorer.base.org", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 8453, + "batch_inbox_address": "0xff00000000000000000000000000000000008453", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17481768, + "hash": "0x5c13d307623a926cd31415036c8b7fa14572f9dac64528e857a470511fc30771" + }, + "l2": { + "number": 0, + "hash": "0xf712aa9241cc24369b143cf6dce85f0902a9731e70d66818a3a5845b296c73dd" + }, + "l2_time": 1686789347, + "system_config": { + "batcherAddr": "0x5050f69a9786f081509234f1a7f4684b5e5b76c9", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x7bb41c3008b3f03fe483b28b8db90e19cf07595c", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x3154cf16ccdb4c6d922629664174b904d80f2c35", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x49048044d57e1c92a77f79988d21fa8faf74e97e", + "SystemConfigProxy": "0x73a79fab69143498ed3712e519a88a918e1f4072", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x43edb88c4b80fdd2adff2412a7bebf9df42cb40e", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Funki", + "PublicRPC": "https://rpc-mainnet.funkichain.com", + "SequencerRPC": "https://rpc-mainnet.funkichain.com", + "Explorer": "https://funki.superscan.network", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 33979, + "batch_inbox_address": "0xff00000000000000000000000000000084bb84bb", + "block_time": 2, + "seq_window_size": 8400, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0xf40b807c2407e1d7dabb85f3ceefd5eacc7bf3cd", + "da_challenge_window": 3600, + "da_resolve_window": 3600, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 20325568, + "hash": "0xa0768467271297b618c4306469577fd14ba5b0c0488d6e9710a24762cbfe2928" + }, + "l2": { + "number": 0, + "hash": "0x7d2831dd811c616d073342a3074f2ce737c6b200b8192f9528e8bf32b1fac83e" + }, + "l2_time": 1721211095, + "system_config": { + "batcherAddr": "0x73c98cf34af1f7d798e8e6f34b16037530bffc41", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x0100000000000000000000000000000000000000000000000000000000003138", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x89cb6669f87c165e7128f4a57476ee4daa7ffbcd", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xa2c1c1a473250094a6244f2bcf6cb51f670ad3ac", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x5c9c7f98ed153a2deaa981eb5c97b31744accf22", + "SystemConfigProxy": "0xd39a6cccfa23cb741bb530497e42ec337f1215a8", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x2dc9d2cb1ba0b8a46ae252ab4fbe1ad5c5c3b795", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Mode", + "PublicRPC": "https://mainnet.mode.network", + "SequencerRPC": "https://mainnet-sequencer.mode.network", + "Explorer": "https://explorer.mode.network", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 34443, + "batch_inbox_address": "0x24e59d9d3bd73ccc28dc54062af7ef7bff58bd67", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 18586927, + "hash": "0xf9b1b22a7ef9d13f063ea467bcb70fb6e9f29698ecb7366a2cdf5af2165cacee" + }, + "l2": { + "number": 0, + "hash": "0xb0f682e12fc555fd5ce8fce51a59a67d66a5b46be28611a168260a549dac8a9b" + }, + "l2_time": 1700167583, + "system_config": { + "batcherAddr": "0x99199a22125034c808ff20f377d91187e8050f2e", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x735adbbe72226bd52e818e7181953f42e3b0ff21", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x8b34b14c7c7123459cf3076b8cb929be097d0c07", + "SystemConfigProxy": "0x5e6432f18bc5d497b1ab2288a025fbf9d69e2221", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x6f13efadabd9269d6cead22b448d434a1f1b433e", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Celo", + "PublicRPC": "https://forno.celo.org", + "SequencerRPC": "https://cel2-sequencer.celo.org/", + "Explorer": "https://celoscan.io/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 42220, + "batch_inbox_address": "0xff00000000000000000000000000000000042220", + "block_time": 1, + "seq_window_size": 7200, + "max_sequencer_drift": 2892, + "GasPayingToken": "0x057898f3c43f129a17517b9056d23851f124b19f", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1752073200, + "isthmus_time": 1752073200 + }, + "optimism": { + "eip1559Elasticity": 5, + "eip1559Denominator": 400, + "eip1559DenominatorCanyon": 400 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 22128103, + "hash": "0xe499ec12e12fc2c94e6714a94f2640dbc748ff6c26fd3f420b25264a3d90066f" + }, + "l2": { + "number": 31056500, + "hash": "0x7586014e20c69b3fa7c9070baf1a7edd95833db57853126f32593b455da2e5c5" + }, + "l2_time": 1742957258, + "system_config": { + "batcherAddr": "0x0cd08c7f7a96aa9635f761b49216b9ea74c5ca60", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x0100000000000000000000000000000000000000000000000000000000000000", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4092a77baf58fef0309452ceacb09221e556e112", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x9c4955b92f34148dbcfdcd82e9c9ece5cf2badfe", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xc5c5d157928bdbd2acf6d0777626b6c75a9eaedc", + "SystemConfigProxy": "0x89e31965d844a309231b1f17759ccaf1b7c09861", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xfbac162162f4009bb007c6debc36b1dac10af683", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Ink", + "PublicRPC": "https://rpc-gel.inkonchain.com", + "SequencerRPC": "https://rpc-gel.inkonchain.com", + "Explorer": "https://explorer.inkonchain.com", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1742396400, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 57073, + "batch_inbox_address": "0x005969bf0ecbf6edb6c47e5e94693b1c3651be97", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1742396400, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 250, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 21344310, + "hash": "0x39e4e0c0c94f8076134b441a9dc18d10b595fd9777edeb763bb9f9bd8f922fd9" + }, + "l2": { + "number": 0, + "hash": "0x23a2658170ba70d014ba0d0d2709f8fbfe2fa660cd868c5f282f991eecbe38ee" + }, + "l2_time": 1733498411, + "system_config": { + "batcherAddr": "0x500d7ea63cf2e501dadaa5feec1fc19fe2aa72ac", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x88ff1e5b602916615391f55854588efcbb7663f0", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x5d66c1782664115999c47c9fa5cd031f495d3e4f", + "SystemConfigProxy": "0x62c0a111929fa32cec2f76adba54c16afb6e8364", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x10d7b35078d3baabb96dd45a9143b94be65b12cd", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "BOB", + "PublicRPC": "https://rpc.gobob.xyz", + "SequencerRPC": "https://rpc.gobob.xyz", + "Explorer": "https://explorer.gobob.xyz", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 60808, + "batch_inbox_address": "0x3a75346f81302aac0333fb5dcdd407e12a6cfa83", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 19634321, + "hash": "0x218132178d65c4bc490aadd93c31535326043fe1fe8fea2d87f26c1da83d45c2" + }, + "l2": { + "number": 0, + "hash": "0x8ed4903b7f9c3f7bb7a09374d63ae9c9852cd9aab1784b433c41dbeb47b4dba2" + }, + "l2_time": 1712861987, + "system_config": { + "batcherAddr": "0x08f9f14ff43e112b18c96f0986f28cb1878f1d11", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xc91482a96e9c2a104d9298d1980eccf8c4dc764e", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x3f6ce1b36e5120bbc59d0cfe8a5ac8b6464ac1f7", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x8adee124447435fe03e3cd24df3f4cae32e65a3e", + "SystemConfigProxy": "0xacb886b75d76d1c8d9248cfddfa09b70c71c5393", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x96123dbfc3253185b594c6a7472ee5a21e9b1079", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Automata Mainnet", + "PublicRPC": "https://rpc.ata.network", + "SequencerRPC": "https://automata-mainnet.alt.technology/", + "Explorer": "https://explorer.ata.network", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 65536, + "batch_inbox_address": "0xff00000000000000000000000000000001111111", + "block_time": 2, + "seq_window_size": 7200, + "max_sequencer_drift": 1800, + "GasPayingToken": "0xa2120b9e674d3fc3875f415a7df52e382f141225", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0x08c5dcdd5e46d31cc1591ee15b084663507597f3", + "da_challenge_window": 3600, + "da_resolve_window": 3600, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 20323239, + "hash": "0x2f2ea5358078fdc01d978ff85a3a8fb2efff470b624ba95c5eef0ecd75a6e81d" + }, + "l2": { + "number": 0, + "hash": "0x11bb44203634fd85571a9971e6f09d633cbf22826681f809283081ff4e1b5192" + }, + "l2_time": 1721183063, + "system_config": { + "batcherAddr": "0x5bef09f138921ef7985d83aab97da1db6e4dd190", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x0100000000000000000000000000000000000000000000000000000000013880", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x03ec1c43434e2f910a2fb984906cd2470fdb39c8", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xe639919b92ab6dd238aeacc6f2a8d6e355d17bd5", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xd52ba64cbe1e3b44167f810622fbef36be24d95c", + "SystemConfigProxy": "0x72934d7aedc1a2d889ca89aaf064cd9455e64d00", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xb52337f38747d6931f2976eea24a3f3f6b7cdea2", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Silent Data Mainnet", + "PublicRPC": "https://mainnet.silentdata.com/${SILENTDATA_AUTH_TOKEN}", + "SequencerRPC": "", + "Explorer": "https://explorer-mainnet.rollup.silentdata.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 380929, + "batch_inbox_address": "0x00a7a8fdb9a61f85dd8f926f4e1723927a18c9f3", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0xef8dfcf9a3915f0dd8194e398ec2ca0168690613", + "da_challenge_window": 16, + "da_resolve_window": 16, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 22995849, + "hash": "0x733567d023d25b9005fb1bc6c6027e6b3e97890db987c90219d9eaf01e9d3f49" + }, + "l2": { + "number": 0, + "hash": "0xc46d5d70c0b72228124c217822261df39071bd2059f710bda0ef17551b2a05c3" + }, + "l2_time": 1753442723, + "system_config": { + "batcherAddr": "0xca4c95a5a1660d14df8ec6c53537964f9c5367c7", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": "0xe534107c6071f29c4bfac3a0d1af6be53212740c", + "ProxyAdminOwner": "0xe512f69d8aeed75c737190f4db84687fba7c5e88", + "Guardian": "0x90f72cb63e608dd6c63793b7d90804963b478ccd", + "Challenger": "0xd04464568a8a8a5da8c9218714e6d9f4bd40df65", + "Proposer": "0x3e3f6b6010237678723a33ae227d167b55888c20", + "UnsafeBlockSigner": "0x018badc3c0026689d9f77f6a9cf6d31d954208bf", + "BatchSubmitter": "0xca4c95a5a1660d14df8ec6c53537964f9c5367c7" + }, + "Addresses": { + "AddressManager": "0xdd05146d14613bdc6a6cad371d15f1ae4269480e", + "L1CrossDomainMessengerProxy": "0x3131b01df2f9ef6f42113090edead5c97612c473", + "L1Erc721BridgeProxy": "0x74a3065e6a4ffaa07dac542e28452995f3c32eea", + "L1StandardBridgeProxy": "0xe97d73b0079e04f4ea4162b9173604a6213ef158", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": "0x00e3001f111ba89f20a8336bb986a78d8f734e7e", + "OptimismPortalProxy": "0xccd285b1ccf1cdab36da995b9fc68870e287694e", + "SystemConfigProxy": "0x5c3efe3ca554816e9960c02ae3b4eb3a9a8d2e16", + "ProxyAdmin": "0xd8eab3ed39df0afb9bfd853f49637f7e73963966", + "SuperchainConfig": "0x50f08e501f8a9d124eab4990b057fdefe3f6ae3e", + "AnchorStateRegistryProxy": "0x1ffff41f5e6384d6737d27b1f471e69212150e55", + "DelayedWethProxy": "0x2ddf646eaaac38aea031268a07de4e9ff1d967bd", + "DisputeGameFactoryProxy": "0x139cf05b34d0ec49d3bfb9704ec4ceba6ae95dd1", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": "0x1b99b322085da031e68c1202fdb756b3ffbac7a6", + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Xterio Chain (ETH)", + "PublicRPC": "https://xterio-eth.alt.technology/", + "SequencerRPC": "https://xterio-eth.alt.technology/", + "Explorer": "https://eth.xterscan.io/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 2702128, + "batch_inbox_address": "0xff00000000000000000000000000000000293b30", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0x16193e14197c10109f3e81b938153a04a2a00190", + "da_challenge_window": 3600, + "da_resolve_window": 3600, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 19938395, + "hash": "0x813c7b630e1d98a36dc26653406746f020b4f699381c2dbccaf51ef6dfd29ac2" + }, + "l2": { + "number": 0, + "hash": "0xb99fa3b9c265fe5364ed7ee2aca78535aaffd04e7b21f40383ed19a1c6d1d63b" + }, + "l2_time": 1716537431, + "system_config": { + "batcherAddr": "0x7d6251d49a102a330cfb46d132982781620700cb", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000b4", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xff75bd7672b79f2562faf98d488bbb3db1cd1574", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x2ad84abd52050956acc9c490d024b821a59e3fb6", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xbc2beda4ce7a1f40aa458322a33b44081b2f545a", + "SystemConfigProxy": "0x6e99cde188daafeecb6ed8ac28b98de4c8ee5d6c", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x443164f044d8840479234e00e7ad5bb06b85fc78", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Zora", + "PublicRPC": "https://rpc.zora.energy", + "SequencerRPC": "https://rpc.zora.energy", + "Explorer": "https://explorer.zora.energy", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 7777777, + "batch_inbox_address": "0x6f54ca6f6ede96662024ffd61bfd18f3f4e34dff", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17473923, + "hash": "0xbdbd2847f7aa5f7cd1bd4c9f904057f4ba0b498c7e380199c01d240e3a41a84f" + }, + "l2": { + "number": 0, + "hash": "0x47555a45a1af8d4728ca337a1e48375a83919b1ea16591e070a07388b7364e29" + }, + "l2_time": 1686693839, + "system_config": { + "batcherAddr": "0x625726c858dbf78c0125436c943bf4b4be9d9033", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a0aae59d09fccbddb6c6cceb07b7279367c3d2a", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x3e2ea9b92b7e48a52296fd261dc26fd995284631", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x1a0ad011913a150f69f6a19df447a0cfd9551054", + "SystemConfigProxy": "0xa3cab0126d5f504b071b81a3e8a2bbbf17930d86", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xb0f15106fa1e473ddb39790f197275bc979aa37e", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + } + ] + }, + { + "name": "rehearsal-0-bn", + "config": { + "name": "rehearsal-0-bn", + "l1": { + "chain_id": 11155111, + "public_rpc": "https://ci-sepolia-l1-archive.optimism.io", + "explorer": "" + }, + "hardforks": { + "interop_time": 1749150000 + }, + "protocol_versions_addr": "0x72b9c5a159b0dee8331a268791aa432619693c06", + "superchain_config_addr": "0x672f11f34b7be67c13a6fcba819c339bc3b0a585", + "op_contracts_manager_proxy_addr": null + }, + "chains": [ + { + "Name": "rehearsal-0-bn-0", + "PublicRPC": "", + "SequencerRPC": "", + "Explorer": "", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 420120009, + "batch_inbox_address": "0x00aa742d2eef0b65eb1dc7633cbaf8ef24a945b3", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 0, + "interop_time": 1749150000 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 8470494, + "hash": "0x9011befaa62e1a920aa3d14e9165107d522b709049c475843725f712bbf693f1" + }, + "l2": { + "number": 0, + "hash": "0x1055187f2eb4e8be192ee7e65756efa290267543bad721d3d8a6135addbe75a0" + }, + "l2_time": 1748984964, + "system_config": { + "batcherAddr": "0xd059234409a70327b48c03f7bbd5317a758e315b", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x26df14a0c889de2448d228ee23b2530550b5b774", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x1b0bf2aea5acdf3c7e0efede9389411333aaf9f9", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xf26c2e762c0447443b951a74dfa5bf380bcb8361", + "SystemConfigProxy": "0xc5d517463c38567a644f2fccefc0e534cc991a57", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x1a1e1649eb6b8f09896498b6cfd73eee6462447c", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "rehearsal-0-bn-1", + "PublicRPC": "", + "SequencerRPC": "", + "Explorer": "", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 420120010, + "batch_inbox_address": "0x00f4fb5369315803b9104640815bc29ff6381cf1", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 0, + "interop_time": 1749150000 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 8470494, + "hash": "0x9011befaa62e1a920aa3d14e9165107d522b709049c475843725f712bbf693f1" + }, + "l2": { + "number": 0, + "hash": "0xc9f60cb23f0e558123d0479675e47ce182c6fe806af66a002aeba5e54ec56cc4" + }, + "l2_time": 1748984964, + "system_config": { + "batcherAddr": "0xee9b6e81798f4873f80d6cce708ce1bb254fafd6", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x26df14a0c889de2448d228ee23b2530550b5b774", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xa2221fde8c41b4b0802f5ef991cbf4feb75e8081", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x1fd346d7e2394fbcc31501cbe88df89d6ddb0a0e", + "SystemConfigProxy": "0x08712404111e4b5b4504e67bef33205608791ef7", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x1a1e1649eb6b8f09896498b6cfd73eee6462447c", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + } + ] + }, + { + "name": "sepolia", + "config": { + "name": "Sepolia", + "l1": { + "chain_id": 11155111, + "public_rpc": "https://ethereum-sepolia-rpc.publicnode.com", + "explorer": "https://eth-sepolia.blockscout.com" + }, + "hardforks": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "protocol_versions_addr": "0x79add5713b383daa0a138d3c4780c7a1804a8090", + "superchain_config_addr": "0xc2be75506d5724086deb7245bd260cc9753911be", + "op_contracts_manager_proxy_addr": null + }, + "chains": [ + { + "Name": "Ethernity Testnet", + "PublicRPC": "https://testnet.ethernitychain.io", + "SequencerRPC": "https://testnet.ethernitychain.io", + "Explorer": "https://testnet.ernscan.io", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 233, + "batch_inbox_address": "0xff00000000000000000000000000000000000233", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6037466, + "hash": "0x2d9c0b883cfc901f509bd94953c283c477adc57c8a20e234029244e68f00142a" + }, + "l2": { + "number": 0, + "hash": "0x4715665848c513c54d71a3e60700743f2b37ec21df9b9d0b8eefee4c30cd557a" + }, + "l2_time": 1717499232, + "system_config": { + "batcherAddr": "0x973a9e30d6d11355a459a69e7cbfba61c7627736", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a19d3afd327bd01d390eb52c11c2a9a79bcfb32", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xfd1a12b7a04b13c031d8b075ba5b9080a2cf246f", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x1f24d471ef7291c7f97dbd2f76299b30d3e3b6e3", + "SystemConfigProxy": "0x7c957fec1f6b3d1024442e989cb08b8f2285686c", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x64d0bce6ed7c16cac7817f3597758e31afacd01b", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Binary Sepolia", + "PublicRPC": "https://rpc.testnet.thebinaryholdings.com", + "SequencerRPC": "https://sequencer.rpc.bnry.testnet.zeeve.net", + "Explorer": "https://explorer.sepolia.thebinaryholdings.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 625, + "batch_inbox_address": "0xff00000000000000000000000000000000042069", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": "0x46d878bf7bf62ec542953cb89ac0bf58d991181e", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1723204838, + "granite_time": 1723545055 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6092340, + "hash": "0x8d457519705ad17cf812d7cd4cfe5726eca8249f9e5f40a1d19992b07c2521ce" + }, + "l2": { + "number": 0, + "hash": "0xf1c38d92222758e1ea0c9c7ae1f7f2dfd0e3e747609f1c2d5a41ecfc5e84aba9" + }, + "l2_time": 1718195040, + "system_config": { + "batcherAddr": "0x9cf89f8cb7cc94c579426f967d9517cd2e9adf29", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x5a11a7a6ca68819c601a4136bfbdfba26d5f043e", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x3b78c3b41b3e3fc6bdf0bd3060c9e2471401c098", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xfbed910ca54f013bfea67bd4dc836263bdd0b46c", + "SystemConfigProxy": "0x1a6d0312faaaca2bf818660f164450176c6205c9", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x096b38bdc80b5bf5b5fb4e1a75ae38bda520474a", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Radius testnet", + "PublicRPC": "testnet-rpc.theradius.xyz", + "SequencerRPC": "dev-secure.rpc.theradius.xyz", + "Explorer": "https://testnet-rpc.theradius.xyz/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 863, + "batch_inbox_address": "0x005585b57b62c1eae08e995af66746cea56eb9e4", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 8090957, + "hash": "0xb4a93e1692ed6fcd3e196325471f653ff11ea4afb9618bf9ddc19d8d69d748be" + }, + "l2": { + "number": 0, + "hash": "0x5ef69a0280793403064cafbb45a27371eb72baccb997e79bb7041785ecb6a799" + }, + "l2_time": 1743250684, + "system_config": { + "batcherAddr": "0xe58318a4bf1a27ab2f3dd0433e27d81c8caaf04f", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4a0b7f6a778e3e91dbba791b7e43400a08735506", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xd5983849e1ebfdb97e66736962dbf9db4d2bdc08", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x86897210aad2861aa603937762ac7c1e6dfa9727", + "SystemConfigProxy": "0x59b71c0a919bc1e8dae5488a562215494f1257fe", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x6d6ef1d3a2ecc7511a0881f460422b461ca7128a", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Mode Testnet", + "PublicRPC": "https://sepolia.mode.network", + "SequencerRPC": "https://sepolia.mode.network", + "Explorer": "https://sepolia.explorer.mode.network", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1703203200, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 919, + "batch_inbox_address": "0xcddae6148da1e003c230e4527f9baedc8a204e7e", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1703203200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 3778382, + "hash": "0x4370cafe528a1b8f2aaffc578094731daf69ff82fd9edc30d2d842d3763f3410" + }, + "l2": { + "number": 0, + "hash": "0x13c352562289a88ed33087a51b6b6c859a27709c8555c9def7cb9757c043acad" + }, + "l2_time": 1687867932, + "system_config": { + "batcherAddr": "0x4e6bd53883107b063c502ddd49f9600dc51b3ddc", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xbc5c679879b2965296756cd959c3c739769995e2", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x320e1580efff37e008f1c92700d1eba47c1b23fd", + "SystemConfigProxy": "0x15cd4f6e0ce3b4832b33cb9c6f6fe6fc246754c2", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x7bb634b42373a87712da14064ded13db8b8b14f4", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Unichain Sepolia Testnet", + "PublicRPC": "https://sepolia.unichain.org", + "SequencerRPC": "https://sepolia-sequencer.unichain.org", + "Explorer": "https://sepolia.uniscan.xyz", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1734559200, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1301, + "batch_inbox_address": "0xff00000000000000000000000000000000001301", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 1734559200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6728364, + "hash": "0x81719966c43c08d616a832331500633db68006f5e8c0b575a6faf1704ad350c0" + }, + "l2": { + "number": 0, + "hash": "0xb7fe0bc9f98ca03294ca0094ff9374cc3e64130b6ec85850d6e260191f48bfe7" + }, + "l2_time": 1726852428, + "system_config": { + "batcherAddr": "0x4ab3387810ef500bfe05a49dc53a44c222cbab3e", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000dbba0000007d0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xd363339ee47775888df411a163c586a8bdea9dbf", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xea58fca6849d79ead1f26608855c2d6407d54ce2", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x0d83dab629f0e0f9d36c0cbc89b69a489f0751bd", + "SystemConfigProxy": "0xaee94b9ab7752d3f7704bde212c0c6a0b701571d", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xeff73e5aa3b9aec32c659aa3e00444d20a84394b", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Metal L2 Testnet", + "PublicRPC": "https://testnet.rpc.metall2.com", + "SequencerRPC": "https://testnet.rpc.metall2.com", + "Explorer": "https://testnet.explorer.metall2.com", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1708534800, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1740, + "batch_inbox_address": "0x24567b64a86a4c966655fba6502a93dfb701e316", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1708129622, + "delta_time": 1708385400, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 5304030, + "hash": "0x6a10927c70985f75898c48235b620acb2a48e9c777a40022f9dbad1b0c96a9c1" + }, + "l2": { + "number": 0, + "hash": "0xd24cf8e46b189b0c128dab4e46168520e3a4cdd390b239e8cc1e5abd22a629ae" + }, + "l2_time": 1708129620, + "system_config": { + "batcherAddr": "0xdb80eca386ac72a55510e33cf9cf7533e75916ee", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x21530aadf4dcfb9c477171400e40d4ef615868be", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x01d4dfc994878682811b2980653d03e589f093cb", + "SystemConfigProxy": "0x5d63a8dc2737ce771aa4a6510d063b6ba2c4f6f2", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xd9a68f90b2d2debe18a916859b672d70f79eebe3", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Soneium Testnet Minato", + "PublicRPC": "https://rpc.minato.soneium.org", + "SequencerRPC": "https://rpc.minato.soneium.org", + "Explorer": "https://soneium-minato.blockscout.com/", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1734685200, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 1946, + "batch_inbox_address": "0x00ca81e019a5312d404ae7fe8367d30de4c11365", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1730106000, + "granite_time": 1730106000, + "holocene_time": 1734685200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6465975, + "hash": "0x87e648e8d219737526cbcbc03dc914256d4f8e13ae13dc0a2ad377d831cd6473" + }, + "l2": { + "number": 0, + "hash": "0x7ec49d93fa8f47c00e98cc5965cd3903a8faa9238743d2eba0e0550ab4c45c43" + }, + "l2_time": 1723194336, + "system_config": { + "batcherAddr": "0xf0ab0441c8f4b89b561ae685b98c6ad5175e0cab", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x5f5a404a5edabcdd80db05e8e54a78c9ebf000c2", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x65ea1489741a5d72ffdd8e6485b216bbdcc15af3", + "SystemConfigProxy": "0x4ca9608fef202216bc21d543798ec854539baad3", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xb3ad2c38e6e0640d7ce6aa952ab3a60e81bf7a01", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Lisk Sepolia Testnet", + "PublicRPC": "https://rpc.sepolia-api.lisk.com", + "SequencerRPC": "https://rpc.sepolia-api.lisk.com", + "Explorer": "https://sepolia-blockscout.lisk.com", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 1708534800, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 4202, + "batch_inbox_address": "0xff00000000000000000000000000000000004202", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1705312994, + "delta_time": 1705312994, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 5089851, + "hash": "0x7d9d6dcec39efe182119f41b1bd2aa7b35b82e43927522afea86d210a4eace4b" + }, + "l2": { + "number": 0, + "hash": "0xead3e6ddd08ae7e27fd952b74ceb468ba889047ac96b351dd13bd55e5faf3372" + }, + "l2_time": 1705312992, + "system_config": { + "batcherAddr": "0x246e119a5bcc2875161b23e4e602e25cece96e37", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x465874903125f26316c730ae84862606a3326ca5", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x1fb30e446ea791cd1f011675e5f3f5311b70faf5", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xe3d90f21490686ec7ef37be788e02dfc12787264", + "SystemConfigProxy": "0xf54791059df4a12ba461b881b4080ae81a1d0ac0", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x9aa3890a87e6bd2cb85dad1a5d8b0a9d669e658a", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "World Chain Sepolia Testnet", + "PublicRPC": "https://worldchain-sepolia.g.alchemy.com/public", + "SequencerRPC": "https://worldchain-sepolia-sequencer.g.alchemy.com", + "Explorer": "https://worldchain-sepolia.explorer.alchemy.com/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 4801, + "batch_inbox_address": "0xff00000000000000000000000000000000484752", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1721739600, + "granite_time": 1726570800, + "holocene_time": 1737633600, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1761825600 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6278018, + "hash": "0xd220bbdf24df6d1611f4ece1d08c64feae914ce6299ab2806c864e30a5289201" + }, + "l2": { + "number": 0, + "hash": "0xf1deb67ee953f94d8545d2647918687fa8ba1f30fa6103771f11b7c483984070" + }, + "l2_time": 1720547424, + "system_config": { + "batcherAddr": "0x0f3ff4731d7a10b89ed79ad1fd97844d7f66b96d", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 100000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x945185c01fb641ba3e63a9bdf66575e35a407837", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xd7df54b3989855eb66497301a4aaec33dbb3f8de", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xff6eba109271fe6d4237eeed4bab1dd9a77dd1a4", + "SystemConfigProxy": "0x166f9406e79a656f12f05247fb8f5dfa6155bcbf", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x8ec1111f67dad6b6a93b3f42dfbc92d81c98449a", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Settlus Sepolia", + "PublicRPC": "https://settlus-septestnet.g.alchemy.com/public", + "SequencerRPC": "https://settlus-sep-testnet-sequencer.g.alchemy.com/", + "Explorer": "sepolia.settlus.network", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 5373, + "batch_inbox_address": "0x00b18c246d9b94634a54dcc2a2d258a048ab2227", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 7456555, + "hash": "0xd8c9f415fe06484e7713d1e08144a4d1b37e064813b6f3ca45c9d9a29a1f13db" + }, + "l2": { + "number": 0, + "hash": "0xa055eed3718c2ed1f04692c707b7045a804505b5e5184a37143514ba9d14281e" + }, + "l2_time": 1736459256, + "system_config": { + "batcherAddr": "0x26a1babee918f55bfddb002066c63678147b03a2", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x945185c01fb641ba3e63a9bdf66575e35a407837", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x1fe9c3b11ed334fc049ae9a92ec290aa69c39267", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x55585368857dcc7e0f89475d28963977db0b1ee1", + "SystemConfigProxy": "0x7f73514a7ec19f3f51e3c607d536560818df4205", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x017d15a6854ccaaa89c63d1a9fea8da45ced97cf", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "RACE Testnet", + "PublicRPC": "https://racetestnet.io", + "SequencerRPC": "https://racetestnet.io", + "Explorer": "https://testnet.racescan.io/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 6806, + "batch_inbox_address": "0xff00000000000000000000000000000000006806", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1749686400, + "granite_time": 1749686400, + "holocene_time": 1749772800, + "pectra_blob_schedule_time": 1742486400 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6210400, + "hash": "0x28dd1dd74080560ef0b02f8f1ae31d1be75b01a70a5be6ef22e673cec538770f" + }, + "l2": { + "number": 0, + "hash": "0x994d67464c3368b8eb6f9770087399486b25d721a1868b95bb37de327b49ab89" + }, + "l2_time": 1719646560, + "system_config": { + "batcherAddr": "0x584d61a30c7ef1e8d547ee02099dadc487f49889", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xac78e9b3aa9373ae4be2ba5bc9f716d7a746a65e", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x289179e9d43a35d47239456251f9c2fdbf9fbea2", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xf2891fc6819cdd6bd9221874619bb03a6277d72a", + "SystemConfigProxy": "0x07e7a3f25aa73da15bc19b71fef8f5511342a409", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xbdc16b0e8c18c5e13ed02221aa85598af486a88f", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "arena-z-testnet", + "PublicRPC": "https://testnet-rpc.arena-z.gg", + "SequencerRPC": "https://testnet-rpc.arena-z.gg", + "Explorer": "https://testnet-explorer.arena-z.gg", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 1763568001, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 9899, + "batch_inbox_address": "0x00894002c8ccc07469d2f98e575c375130761c35", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 0, + "holocene_time": 0, + "isthmus_time": 1747839600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 8374269, + "hash": "0xc169b1762b3430dc19a3c380536edb770eba127aeb6f518ef5a685ae17a588a2" + }, + "l2": { + "number": 0, + "hash": "0x29b5818dde83228647d69794cad3e28bfc2e99defc3696c46c068ba4a27b8d4a" + }, + "l2_time": 1747822512, + "system_config": { + "batcherAddr": "0xc7dbe00183fef815db161d61faf3cb6a172d4ea2", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 60000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x23592510f948c1465294041b44192f9656146544", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x90fdce6efff020605462150cde42257193d1e558", + "SystemConfigProxy": "0x5357be2d78aad17860570e14b74561840e959d4d", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xd02dd46b73ff5f3ec3970f9a12f08ad703c103df", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Shape Sepolia Testnet", + "PublicRPC": "https://sepolia.shape.network/", + "SequencerRPC": "https://shape-sepolia-sequencer.g.alchemy.com", + "Explorer": "https://shape-sepolia.explorer.alchemy.com/", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 11011, + "batch_inbox_address": "0xff00000000000000000000000000000000011011", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 1800, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 1721732400, + "granite_time": 1727197200 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6151675, + "hash": "0x0e1156bae935f43af44f8d3e011275b8aeab57acd83cbd71d92903d3c9b29cf3" + }, + "l2": { + "number": 0, + "hash": "0xcb3558db049390808cbde6b82a48d06ed98d3fe959e088ac20ae56174595cfce" + }, + "l2_time": 1718936160, + "system_config": { + "batcherAddr": "0x6ff556fa7cafec55ae77c5c1d58a010be75f9991", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x945185c01fb641ba3e63a9bdf66575e35a407837", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x341ab1dafdfb73b3d6d075ef10b29e3cacb2a653", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xff8ca2b4d8122e41441f7ccdcf61b8692198bd1e", + "SystemConfigProxy": "0xa1ac91ed91ebe40e00d61e233c8026318b4da5fb", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x575697f2c20bd63415e5b24656d935d4b81b8220", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Pivotal Sepolia", + "PublicRPC": "https://sepolia.pivotalprotocol.com/", + "SequencerRPC": "https://sepolia.pivotalprotocol.com/", + "Explorer": "https://sepolia.pivotalscan.org/", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 16481, + "batch_inbox_address": "0xff000000000000000000000b0000000000016481", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6548454, + "hash": "0x416fb0d9f67e3b0e9701c6bfcb03a13732ddb397158fa8c2ed6e53a33c10995c" + }, + "l2": { + "number": 0, + "hash": "0xac7379723e9ff584c00e1cbeb20e76c36dbc98dc8bf2146d1df317b7b6636b91" + }, + "l2_time": 1724315616, + "system_config": { + "batcherAddr": "0xc122b4d47644bbd7a98e41dd242d20054a11f720", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1d60661f40acc64e38fcade8979d22a9b9278e6e", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x788de2b0dd35808a05eaff7aaf5578b21e0dd9a7", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x923b28e0037a799a1e60368e60c92dffba982162", + "SystemConfigProxy": "0x5c72ce6ea707037bc476da8f4f969bc1f8abc78b", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xcb97c9224af16c95b8d8959a2752ef1832eb8ba9", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Boba Sepolia Testnet", + "PublicRPC": "https://sepolia.boba.network", + "SequencerRPC": "https://sepolia.boba.network", + "Explorer": "https://testnet.bobascan.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 28882, + "batch_inbox_address": "0xfff0000000000000000000000000000000028882", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1705600788, + "delta_time": 1709078400, + "ecotone_time": 1709078400, + "fjord_time": 1722297600, + "granite_time": 1726470000, + "holocene_time": 1736150400, + "pectra_blob_schedule_time": 1743534000 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 5109513, + "hash": "0x632d8caedbfd573e09c1b49134bd5147147e0904e0f04eba15c662be0258f517" + }, + "l2": { + "number": 511, + "hash": "0x097654c4c932c97808933b42179388f7bbcefaed3bd93fdf69157e19f1deea0e" + }, + "l2_time": 1705600788, + "system_config": { + "batcherAddr": "0xf598b6388ec06945021699f0bbb23dfcfc5edbe8", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x17070b4af21625106e7971983aa524f59ea40c57", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x244d7b81ee3949788da5f1178d911e83ba24e157", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xb079e6fa9b3eb072febf7f746044834eab308db6", + "SystemConfigProxy": "0xfdc9bce032cef55a71b4fde9b9a2198ad1551965", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x29bd67b23cac0e6bbde1373e3859dd25510f3331", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Creator Chain Testnet", + "PublicRPC": "https://rpc.creatorchain.io", + "SequencerRPC": "https://rpc.creatorchain.io", + "Explorer": "https://explorer.creatorchain.io", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 66665, + "batch_inbox_address": "0xfa5c2402828228a8dd700ba1e71f5e1128876fa8", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6576576, + "hash": "0xe84d982c811eafc34801b0fdab8249739495ada50d119a58128aae583a801529" + }, + "l2": { + "number": 0, + "hash": "0x218eeee63576977ed59d751a04ef53ac7ecf6c053331a6f10672c38d211957ed" + }, + "l2_time": 1724698536, + "system_config": { + "batcherAddr": "0xa488310ab2f8aa3294903930023bcab5880cb1ba", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x34478c2eb9018d5a6487bf0440838cd4238e8cf2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x78558fd5c8dc65d10753f004bfc4cfa8e199c668", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x1cb215554f36f518791b2e7359a73c96bfcadf69", + "SystemConfigProxy": "0x978e8311a5a710ef6413aba3a6b89092ce4a58f5", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x156670e48f72cb23eaadd8b51e398c678ab651af", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Base Sepolia Testnet", + "PublicRPC": "https://sepolia.base.org", + "SequencerRPC": "https://sepolia-sequencer.base.org", + "Explorer": "https://sepolia-explorer.base.org", + "SuperchainLevel": 1, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 84532, + "batch_inbox_address": "0xff00000000000000000000000000000000084532", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 4370868, + "hash": "0xcac9a83291d4dec146d6f7f69ab2304f23f5be87b1789119a0c5b1e4482444ed" + }, + "l2": { + "number": 0, + "hash": "0x0dcc9e089e30b90ddfc55be9a37dd15bc551aeee999d2e2b51414c54eaf934e4" + }, + "l2_time": 1695768288, + "system_config": { + "batcherAddr": "0x6cdebe940bc0f26850285caca097c11c33103e47", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 25000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x0fe884546476ddd290ec46318785046ef68a0ba9", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xfd0bf71f60660e2f608ed56e1659c450eb113120", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x49f53e41452c74589e85ca1677426ba426459e85", + "SystemConfigProxy": "0xf272670eb55e895584501d564afeb048bed26194", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xd6e6dbf4f7ea0ac412fd8b65ed297e64bb7a06e1", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Camp Network Testnet V2", + "PublicRPC": "https://rpc.camp-network-testnet.gelato.digital", + "SequencerRPC": "https://rpc.camp-network-testnet.gelato.digital", + "Explorer": "https://camp-network-testnet.blockscout.com", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 325000, + "batch_inbox_address": "0xff00000000000000000000000000000000325000", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0, + "granite_time": 1723478400, + "holocene_time": 1732633200 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6086446, + "hash": "0x124b0011271c5810826f74bf8767467009e136fecf0f8e80327b0000fb0456e5" + }, + "l2": { + "number": 0, + "hash": "0x7c7c4f48d2f33a04c5cbabc8b6ba6315f283d6ae0a874118ceb8881e21bfd05e" + }, + "l2_time": 1718120160, + "system_config": { + "batcherAddr": "0x624811105f39936d5ce277d93292061abfedd4d5", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x6dcd6421558f1478251231798fe7500fb23a1e84", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x5c3ec2182be9fbea0da50d517362a069e13fb50e", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x4238fab3746d41e18c87138b505b6857cef6cd1f", + "SystemConfigProxy": "0x7aa405b004401be20d4a587de34aeec99be0b268", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x83bae25fda209b31e903d3dd932906e2718a6e89", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Ink Sepolia", + "PublicRPC": "https://rpc-gel-sepolia.inkonchain.com", + "SequencerRPC": "https://rpc-gel-sepolia.inkonchain.com", + "Explorer": "https://explorer-sepolia.inkonchain.com", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 763373, + "batch_inbox_address": "0x004de1914f4f17ac234f5a5bc8a4072a231d44bf", + "block_time": 1, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6880819, + "hash": "0x5fb818b4d3ba100eeb3347c508bca24752ee88f235120e108bd9ee5d1b2ae50b" + }, + "l2": { + "number": 0, + "hash": "0xe5fd5cf0be56af58ad5751b401410d6b7a09d830fa459789746a3d0dd1c79834" + }, + "l2_time": 1729003296, + "system_config": { + "batcherAddr": "0x21e57c21530bc33f12ba96c9ddc135488365002f", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000000", + "scalar": "0x010000000000000000000000000000000000000000000000000c5fc500000558", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x33f60714bbd74d62b66d79213c348614de51901c", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x5c1d29c6c9c8b0800692acc95d700bcb4966a1d7", + "SystemConfigProxy": "0x05c993e60179f28bf649a2bb5b00b5f4283bd525", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x860e626c700af381133d9f4af31412a2d1db3d5d", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Funki Sepolia Testnet", + "PublicRPC": "https://funki-testnet.alt.technology", + "SequencerRPC": "https://funki-testnet.alt.technology", + "Explorer": "https://sepolia-sandbox.funkichain.com/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "alt-da", + "l2_chain_id": 3397901, + "batch_inbox_address": "0xff000000000000000000000000000000000084bb", + "block_time": 2, + "seq_window_size": 28800, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "pectra_blob_schedule_time": 1744696800 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": { + "da_challenge_address": "0x12c6a7db25b20347ca6f5d47e56d5e8219871c6d", + "da_challenge_window": 1, + "da_resolve_window": 1, + "da_commitment_type": "KeccakCommitment" + }, + "genesis": { + "l1": { + "number": 5853286, + "hash": "0x6bc7182e10df2bf35d76362989e10e2c6799c4ceff2eb32741cb804dfab9dd06" + }, + "l2": { + "number": 0, + "hash": "0xcebbf8fc7d0fab412866fe1e7df73c721104550ec35e55d173064acb2c0aeac9" + }, + "l2_time": 1715060436, + "system_config": { + "batcherAddr": "0xda19a4e4d1dbc69bacf13435f08f76ced9b3c245", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000b4", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x814973b1ec9eb9172996931de7bf1380bd64a824", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x1ba82f688ef3c5b4363ff667254ed4dc59e97477", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xcee7ef4ddf482447fe14c605ea94b37cbe87ca9d", + "SystemConfigProxy": "0xd6a01f1ef51d65f023433992a8f62feead35b172", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xec7c6e35f4e5361d279d5fe7222f3f45a8a83352", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Ozean Poseidon Testnet", + "PublicRPC": "https://ozean-testnet.rpc.caldera.xyz/http", + "SequencerRPC": "https://ozean-testnet.rpc.caldera.xyz/http", + "Explorer": "https://ozean-testnet.explorer.caldera.xyz", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 7849306, + "batch_inbox_address": "0xa1e1342d477f296820bd525575e873c9dd535737", + "block_time": 2, + "seq_window_size": 21600, + "max_sequencer_drift": 3600, + "GasPayingToken": "0x43bd82d1e29a1bec03afd11d5a3252779b8c760c", + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "fjord_time": 0 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 250, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 6584523, + "hash": "0x85467f187f321ff4e778a905e2e8b976469fbd2a9b31a41b0037d0ffb84757df" + }, + "l2": { + "number": 0, + "hash": "0xb32768b76d1a3e84947da1caf37443f46003bff8f24ceee43db387452cba5da1" + }, + "l2_time": 1724805984, + "system_config": { + "batcherAddr": "0x5c871e31efdf4875dcd68c9a7a5766c1e4c1e54f", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x01000000000000000000000000000000000000000000000000000000000a31c2", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x65def37656e89c9f4c2ca14130d6f3aa6078833e", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xb9558ce3c11ec69e18632a8e5b316581e852db91", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x6eeea09335d09870dd467fd34ecc10fdb5106527", + "SystemConfigProxy": "0xdec733b0643e7c3bd06576a4c70ca87e301eae87", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x80e233699f34b8fd91adea8150ea4c91b9324cb5", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "OP Sepolia Testnet", + "PublicRPC": "https://sepolia.optimism.io", + "SequencerRPC": "https://sepolia-sequencer.optimism.io", + "Explorer": "https://testnet-explorer.optimism.io", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 11155420, + "batch_inbox_address": "0xff00000000000000000000000000000011155420", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 4071408, + "hash": "0x48f520cf4ddaf34c8336e6e490632ea3cf1e5e93b0b2bc6e917557e31845371b" + }, + "l2": { + "number": 0, + "hash": "0x102de6ffb001480cc9b8b548fd05c34cd4f46ae4aa91759393db90ea0409887d" + }, + "l2_time": 1691802540, + "system_config": { + "batcherAddr": "0x8f23bb38f531600e5d8fddaaec41f13fab46e98c", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xfbb0621e0b23b5478b630bd55a5f21f67730b0f1", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x16fc5058f25648194471939df75cf27a2fdc48bc", + "SystemConfigProxy": "0x034edd2a225f7f429a63e0f1d2084b9e0a93b538", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x05f9613adb30026ffd634f38e5c4dfd30a197fa1", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Cyber Testnet", + "PublicRPC": "https://rpc.testnet.cyber.co", + "SequencerRPC": "https://cyber.alt.technology/", + "Explorer": "https://testnet.cyberscan.co/", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": null, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 111557560, + "batch_inbox_address": "0xff00000000000000000000000000000000042069", + "block_time": 10, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 0, + "pectra_blob_schedule_time": 1744696800 + }, + "optimism": { + "eip1559Elasticity": 10, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 5488109, + "hash": "0x0ad7012996ee943369fe592424821845ab115e43a9bc5d4e17bf9493869f98e3" + }, + "l2": { + "number": 0, + "hash": "0xd61aa9a43cba806375bec108aba488f00cf784a53a6e5164bc3ef9eb8b169eaf" + }, + "l2_time": 1710470112, + "system_config": { + "batcherAddr": "0x90bb84339856530192cd002533cd7f1290fc5142", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x642a102cd63f039930f99b4657f41fd4ad7699d6", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xaa1bd6d4d8cfd37330a917bc678cb38befaf44e6", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x06c9cadb0346c8e142fb8299cef3eb5120d4c9b6", + "SystemConfigProxy": "0x43b838aa237b27c4fc953e591594cebb1ca2817f", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x99f0f9b0e7b16b10042e0935ce34f2fcebbe13c1", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "test1", + "PublicRPC": "https://rpc.test1.com", + "SequencerRPC": "https://test1-sequencer.com", + "Explorer": "https://explorer.test1.com", + "SuperchainLevel": 2, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 123999119, + "batch_inbox_address": "0xff00000000000000000000000000000000000099", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17422590, + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108" + }, + "l2": { + "number": 105235063, + "hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddr": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "SystemConfigProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "test2", + "PublicRPC": "https://rpc.test2.com", + "SequencerRPC": "https://test2-sequencer.com", + "Explorer": "https://explorer.test2.com", + "SuperchainLevel": 2, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 223999119, + "batch_inbox_address": "0xff00000000000000000000000000000000000099", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17422590, + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108" + }, + "l2": { + "number": 105235063, + "hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddr": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "SystemConfigProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Zora Sepolia Testnet", + "PublicRPC": "https://sepolia.rpc.zora.energy", + "SequencerRPC": "https://sepolia.rpc.zora.energy", + "Explorer": "https://sepolia.explorer.zora.energy", + "SuperchainLevel": 1, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 999999999, + "batch_inbox_address": "0xcd734290e4bd0200dac631c7d4b9e8a33234e91f", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1699981200, + "delta_time": 1703203200, + "ecotone_time": 1708534800, + "fjord_time": 1716998400, + "granite_time": 1723478400, + "holocene_time": 1732633200, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744905600, + "jovian_time": 1763568001 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 4548041, + "hash": "0xf782446a2487d900addb5d466a8597c7c543b59fa9aaa154d413830238f8798a" + }, + "l2": { + "number": 0, + "hash": "0x8b17d2d52564a5a90079d9c860e1386272579e87b17ea27a3868513f53facd74" + }, + "l2_time": 1698080004, + "system_config": { + "batcherAddr": "0x3cd868e221a3be64b161d596a7482257a99d857f", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x1eb2ffc903729a0f03966b917003800b145f56e2", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x5376f1d543dcbb5bd416c56c189e4cb7399fcccb", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xeffe2c6ca9ab797d418f0d91ea60807713f3536f", + "SystemConfigProxy": "0xb54c7bfc223058773cf9b739cc5bd4095184fb08", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xa983a71253eb74e5e86a4e4ed9f37113fc25f2bf", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + } + ] + }, + { + "name": "sepolia-dev-0", + "config": { + "name": "Sepolia Dev 0", + "l1": { + "chain_id": 11155111, + "public_rpc": "https://ethereum-sepolia-rpc.publicnode.com", + "explorer": "https://eth-sepolia.blockscout.com" + }, + "hardforks": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 1706634000, + "fjord_time": 1715961600, + "granite_time": 1723046400, + "holocene_time": 1731682800, + "pectra_blob_schedule_time": 1741687200, + "isthmus_time": 1744300800, + "jovian_time": 1762185600 + }, + "protocol_versions_addr": "0x252cbe9517f731c618961d890d534183822dcc8d", + "superchain_config_addr": "0x02d91cf852423640d93920be0cadcec0e7a00fa7", + "op_contracts_manager_proxy_addr": null + }, + "chains": [ + { + "Name": "OP Labs Sepolia devnet 0", + "PublicRPC": "", + "SequencerRPC": "", + "Explorer": "", + "SuperchainLevel": 0, + "GovernedByOptimism": true, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 11155421, + "batch_inbox_address": "0xff00000000000000000000000000000011155421", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 0, + "delta_time": 0, + "ecotone_time": 1706634000, + "fjord_time": 1715961600, + "granite_time": 1723046400, + "holocene_time": 1731682800, + "pectra_blob_schedule_time": 1741687200, + "isthmus_time": 1744300800, + "jovian_time": 1762185600 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 5173577, + "hash": "0x5639be97000fec7131a880b19b664cae43f975c773f628a08a9bb658c2a68df0" + }, + "l2": { + "number": 0, + "hash": "0x027ae1f4f9a441f9c8a01828f3b6d05803a0f524c07e09263264a38b755f804b" + }, + "l2_time": 1706484048, + "system_config": { + "batcherAddr": "0x19cc7073150d9f5888f09e0e9016d2a39667df14", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x4377bb0f0103992b31ec12b4d796a8687b8dc8e9", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x6d8bc564ef04aaf355a10c3eb9b00e349dd077ea", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x76114bd29dfcc7a9892240d317e6c7c2a281ffc6", + "SystemConfigProxy": "0xa6b72407e2dc9ebf84b839b69a24c88929cf20f7", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0x2419423c72998eb1c6c15a235de2f112f8e38eff", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "Base devnet 0", + "PublicRPC": "", + "SequencerRPC": "", + "Explorer": "", + "SuperchainLevel": 0, + "GovernedByOptimism": false, + "SuperchainTime": 1706634000, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 11763072, + "batch_inbox_address": "0xff00000000000000000000000000000011763072", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1698436800, + "delta_time": 1706555000, + "ecotone_time": 1706634000, + "fjord_time": 1715961600, + "granite_time": 1723046400, + "holocene_time": 1731682800, + "pectra_blob_schedule_time": 1742486400, + "isthmus_time": 1744300800, + "jovian_time": 1762185600 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 4344216, + "hash": "0x86252c512dc5bd7201d0532b31d50696ba84344a7cda545e04a98073a8e13d87" + }, + "l2": { + "number": 0, + "hash": "0x1ab91449a7c65b8cd6c06f13e2e7ea2d10b6f9cbf5def79f362f2e7e501d2928" + }, + "l2_time": 1695433056, + "system_config": { + "batcherAddr": "0x212dd524932bc43478688f91045f2682913ad8ee", + "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 25000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0x0fe884546476ddd290ec46318785046ef68a0ba9", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0x5638e55db5fcf7a58df525f1098e8569c8dba80c", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0x579c82a835b884336b632eebecc78fa08d3291ec", + "SystemConfigProxy": "0x7f67dc4959cb3e532b10a99f41bdd906c46fdfde", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xe545ede9d1fadad12984c31467f56405884b9398", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/kona/crates/protocol/registry/justfile b/kona/crates/protocol/registry/justfile new file mode 100644 index 0000000000000..61dbed4eda870 --- /dev/null +++ b/kona/crates/protocol/registry/justfile @@ -0,0 +1,13 @@ +set positional-arguments + +# default recipe to display help information +default: + @just --list + +# Generate file bindings +bind: + KONA_BIND=true cargo build + +# Update the `superchain-registry` git submodule source +source: + git submodule update --remote --init --recursive diff --git a/kona/crates/protocol/registry/src/l1/mod.rs b/kona/crates/protocol/registry/src/l1/mod.rs new file mode 100644 index 0000000000000..44f6420837c3b --- /dev/null +++ b/kona/crates/protocol/registry/src/l1/mod.rs @@ -0,0 +1,378 @@ +//! L1 genesis configurations. +use crate::alloc::string::ToString; +use alloc::{collections::BTreeMap, string::String}; +use alloy_eips::eip7840::BlobParams; +use alloy_genesis::EthashConfig; +use core::{fmt::Display, ops::Deref}; +use kona_genesis::L1ChainConfig; + +use alloy_chains::NamedChain; +use alloy_primitives::{Address, U256, address, map::HashMap}; + +/// L1 chain configuration. +/// Simple wrapper around the [`L1ChainConfig`] type from the `alloy-genesis` crate. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct L1Config(L1ChainConfig); + +impl Deref for L1Config { + type Target = L1ChainConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for L1Config { + fn from(spec: L1ChainConfig) -> Self { + Self(spec) + } +} + +impl From for L1ChainConfig { + fn from(val: L1Config) -> Self { + val.0 + } +} + +impl L1Config { + const MAINNET_TTD: u128 = 58_750_000_000_000_000_000_000u128; + const MAINNET_DEPOSIT_CONTRACT_ADDRESS: Address = + address!("0x00000000219ab540356cbb839cbe05303d7705fa"); + + const SEPOLIA_TTD: u128 = 17_000_000_000_000_000u128; + const SEPOLIA_DEPOSIT_CONTRACT_ADDRESS: Address = + address!("0x7f02c3e3c98b133055b8b348b2ac625669ed295d"); + const SEPOLIA_MERGE_NETSPLIT_BLOCK: u64 = 1735371; + + const HOLESKY_TTD: u128 = 0; + const HOLESKY_DEPOSIT_CONTRACT_ADDRESS: Address = + address!("0x4242424242424242424242424242424242424242"); + + /// Get the genesis for a given chain ID. + pub fn get_l1_genesis(chain_id: u64) -> Result { + match NamedChain::try_from(chain_id) + .map_err(|_| L1GenesisGetterErrors::ChainIDDoesNotExist(chain_id))? + { + NamedChain::Mainnet => Ok(Self::mainnet()), + NamedChain::Sepolia => Ok(Self::sepolia()), + NamedChain::Holesky => Ok(Self::holesky()), + _ => Err(L1GenesisGetterErrors::UnknownChainID(chain_id)), + } + } + + fn default_blob_schedule() -> BTreeMap { + BTreeMap::from([ + ( + alloy_hardforks::EthereumHardfork::Cancun.name().to_string().to_lowercase(), + BlobParams::cancun(), + ), + ( + alloy_hardforks::EthereumHardfork::Prague.name().to_string().to_lowercase(), + BlobParams::prague(), + ), + ( + alloy_hardforks::EthereumHardfork::Osaka.name().to_string().to_lowercase(), + BlobParams::osaka(), + ), + ( + alloy_hardforks::EthereumHardfork::Bpo1.name().to_string().to_lowercase(), + BlobParams::bpo1(), + ), + ( + alloy_hardforks::EthereumHardfork::Bpo2.name().to_string().to_lowercase(), + BlobParams::bpo2(), + ), + ]) + } + + /// Parse the mainnet genesis. + pub fn mainnet() -> Self { + Self(L1ChainConfig { + chain_id: NamedChain::Mainnet.into(), + homestead_block: alloy_hardforks::EthereumHardfork::Homestead + .mainnet_activation_block(), + dao_fork_block: alloy_hardforks::EthereumHardfork::Dao.mainnet_activation_block(), + dao_fork_support: true, + eip150_block: alloy_hardforks::EthereumHardfork::Tangerine.mainnet_activation_block(), + eip155_block: alloy_hardforks::EthereumHardfork::SpuriousDragon + .mainnet_activation_block(), + eip158_block: alloy_hardforks::EthereumHardfork::SpuriousDragon + .mainnet_activation_block(), + byzantium_block: alloy_hardforks::EthereumHardfork::Byzantium + .mainnet_activation_block(), + constantinople_block: alloy_hardforks::EthereumHardfork::Constantinople + .mainnet_activation_block(), + petersburg_block: alloy_hardforks::EthereumHardfork::Petersburg + .mainnet_activation_block(), + istanbul_block: alloy_hardforks::EthereumHardfork::Istanbul.mainnet_activation_block(), + muir_glacier_block: alloy_hardforks::EthereumHardfork::MuirGlacier + .mainnet_activation_block(), + berlin_block: alloy_hardforks::EthereumHardfork::Berlin.mainnet_activation_block(), + london_block: alloy_hardforks::EthereumHardfork::London.mainnet_activation_block(), + arrow_glacier_block: alloy_hardforks::EthereumHardfork::ArrowGlacier + .mainnet_activation_block(), + gray_glacier_block: alloy_hardforks::EthereumHardfork::GrayGlacier + .mainnet_activation_block(), + shanghai_time: alloy_hardforks::EthereumHardfork::Shanghai + .mainnet_activation_timestamp(), + cancun_time: alloy_hardforks::EthereumHardfork::Cancun.mainnet_activation_timestamp(), + prague_time: alloy_hardforks::EthereumHardfork::Prague.mainnet_activation_timestamp(), + osaka_time: alloy_hardforks::EthereumHardfork::Osaka.mainnet_activation_timestamp(), + bpo1_time: alloy_hardforks::EthereumHardfork::Bpo1.mainnet_activation_timestamp(), + bpo2_time: alloy_hardforks::EthereumHardfork::Bpo2.mainnet_activation_timestamp(), + bpo3_time: alloy_hardforks::EthereumHardfork::Bpo3.mainnet_activation_timestamp(), + bpo4_time: alloy_hardforks::EthereumHardfork::Bpo4.mainnet_activation_timestamp(), + bpo5_time: alloy_hardforks::EthereumHardfork::Bpo5.mainnet_activation_timestamp(), + + ethash: Some(EthashConfig {}), + + blob_schedule: Self::default_blob_schedule(), + + merge_netsplit_block: None, + + terminal_total_difficulty: Some(U256::from(Self::MAINNET_TTD)), + deposit_contract_address: Some(Self::MAINNET_DEPOSIT_CONTRACT_ADDRESS), + + clique: None, + parlia: None, + extra_fields: Default::default(), + terminal_total_difficulty_passed: false, + }) + } + + /// Parse the sepolia genesis. + pub fn sepolia() -> Self { + Self(L1ChainConfig { + chain_id: NamedChain::Sepolia.into(), + homestead_block: alloy_hardforks::EthereumHardfork::Homestead + .sepolia_activation_block(), + dao_fork_block: alloy_hardforks::EthereumHardfork::Dao.sepolia_activation_block(), + dao_fork_support: true, + eip150_block: alloy_hardforks::EthereumHardfork::Tangerine.sepolia_activation_block(), + eip155_block: alloy_hardforks::EthereumHardfork::SpuriousDragon + .sepolia_activation_block(), + eip158_block: alloy_hardforks::EthereumHardfork::Byzantium.sepolia_activation_block(), + byzantium_block: alloy_hardforks::EthereumHardfork::Byzantium + .sepolia_activation_block(), + constantinople_block: alloy_hardforks::EthereumHardfork::Constantinople + .sepolia_activation_block(), + petersburg_block: alloy_hardforks::EthereumHardfork::Petersburg + .sepolia_activation_block(), + istanbul_block: alloy_hardforks::EthereumHardfork::Istanbul.sepolia_activation_block(), + muir_glacier_block: alloy_hardforks::EthereumHardfork::MuirGlacier + .sepolia_activation_block(), + berlin_block: alloy_hardforks::EthereumHardfork::Berlin.sepolia_activation_block(), + london_block: alloy_hardforks::EthereumHardfork::London.sepolia_activation_block(), + arrow_glacier_block: alloy_hardforks::EthereumHardfork::ArrowGlacier + .sepolia_activation_block(), + gray_glacier_block: alloy_hardforks::EthereumHardfork::GrayGlacier + .sepolia_activation_block(), + shanghai_time: alloy_hardforks::EthereumHardfork::Shanghai + .sepolia_activation_timestamp(), + cancun_time: alloy_hardforks::EthereumHardfork::Cancun.sepolia_activation_timestamp(), + prague_time: alloy_hardforks::EthereumHardfork::Prague.sepolia_activation_timestamp(), + osaka_time: alloy_hardforks::EthereumHardfork::Osaka.sepolia_activation_timestamp(), + bpo1_time: alloy_hardforks::EthereumHardfork::Bpo1.sepolia_activation_timestamp(), + bpo2_time: alloy_hardforks::EthereumHardfork::Bpo2.sepolia_activation_timestamp(), + bpo3_time: alloy_hardforks::EthereumHardfork::Bpo3.sepolia_activation_timestamp(), + bpo4_time: alloy_hardforks::EthereumHardfork::Bpo4.sepolia_activation_timestamp(), + bpo5_time: alloy_hardforks::EthereumHardfork::Bpo5.sepolia_activation_timestamp(), + + ethash: Some(EthashConfig {}), + + blob_schedule: Self::default_blob_schedule(), + + terminal_total_difficulty: Some(U256::from(Self::SEPOLIA_TTD)), + merge_netsplit_block: Some(Self::SEPOLIA_MERGE_NETSPLIT_BLOCK), + deposit_contract_address: Some(Self::SEPOLIA_DEPOSIT_CONTRACT_ADDRESS), + + clique: None, + parlia: None, + extra_fields: Default::default(), + terminal_total_difficulty_passed: false, + }) + } + + /// Parse the holesky genesis. + pub fn holesky() -> Self { + Self(L1ChainConfig { + chain_id: NamedChain::Holesky.into(), + homestead_block: Some(0), + dao_fork_block: Some(0), + dao_fork_support: true, + eip150_block: Some(0), + eip155_block: Some(0), + eip158_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + muir_glacier_block: Some(0), + berlin_block: Some(0), + london_block: Some(0), + arrow_glacier_block: Some(0), + gray_glacier_block: Some(0), + shanghai_time: Some(0), + cancun_time: alloy_hardforks::EthereumHardfork::Cancun.holesky_activation_timestamp(), + prague_time: alloy_hardforks::EthereumHardfork::Prague.holesky_activation_timestamp(), + osaka_time: alloy_hardforks::EthereumHardfork::Osaka.holesky_activation_timestamp(), + bpo1_time: alloy_hardforks::EthereumHardfork::Bpo1.holesky_activation_timestamp(), + bpo2_time: alloy_hardforks::EthereumHardfork::Bpo2.holesky_activation_timestamp(), + bpo3_time: alloy_hardforks::EthereumHardfork::Bpo3.holesky_activation_timestamp(), + bpo4_time: alloy_hardforks::EthereumHardfork::Bpo4.holesky_activation_timestamp(), + bpo5_time: alloy_hardforks::EthereumHardfork::Bpo5.holesky_activation_timestamp(), + + ethash: Some(EthashConfig {}), + + blob_schedule: Self::default_blob_schedule(), + + merge_netsplit_block: None, + + terminal_total_difficulty: Some(U256::from(Self::HOLESKY_TTD)), + deposit_contract_address: Some(Self::HOLESKY_DEPOSIT_CONTRACT_ADDRESS), + + clique: None, + parlia: None, + extra_fields: Default::default(), + terminal_total_difficulty_passed: false, + }) + } + + /// Build the l1 chain configurations from the genesis dump files. + pub fn build_l1_configs() -> HashMap { + let mut l1_configs = HashMap::default(); + l1_configs.insert(NamedChain::Mainnet.into(), Self::mainnet().0); + l1_configs.insert(NamedChain::Sepolia.into(), Self::sepolia().0); + l1_configs.insert(NamedChain::Holesky.into(), Self::holesky().0); + l1_configs + } +} + +/// Errors that can occur when trying to get the l1 genesis config for a given chain ID. +#[derive(Debug)] +pub enum L1GenesisGetterErrors { + /// The chain ID does not exist in the [`NamedChain`] registry. + ChainIDDoesNotExist(u64), + /// The chain ID is unknown. + UnknownChainID(u64), + /// Failed to parse the genesis. + ParseGenesisError(serde_json::Error), +} + +impl Display for L1GenesisGetterErrors { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{self:?}") + } +} + +impl From for L1GenesisGetterErrors { + fn from(error: serde_json::Error) -> Self { + Self::ParseGenesisError(error) + } +} + +#[cfg(test)] +mod tests { + use alloy_hardforks::EthereumHardfork; + + use super::*; + + #[test] + fn test_get_l1_genesis() { + let l1_config = L1Config::get_l1_genesis(NamedChain::Mainnet.into()).unwrap(); + assert_eq!(l1_config.chain_id, u64::from(NamedChain::Mainnet)); + + let l1_config = L1Config::get_l1_genesis(NamedChain::Sepolia.into()).unwrap(); + assert_eq!(l1_config.chain_id, u64::from(NamedChain::Sepolia)); + + let l1_config = L1Config::get_l1_genesis(NamedChain::Holesky.into()).unwrap(); + assert_eq!(l1_config.chain_id, u64::from(NamedChain::Holesky)); + + let l1_config = L1Config::get_l1_genesis(1000000).unwrap_err(); + assert!(matches!(l1_config, L1GenesisGetterErrors::ChainIDDoesNotExist(1000000))); + } + + #[test] + fn test_get_l1_bpo_mainnet() { + /// BPO1 hardfork activation timestamp + const MAINNET_BPO1_TIMESTAMP: u64 = 1_765_290_071; + + /// BPO2 hardfork activation timestamp + const MAINNET_BPO2_TIMESTAMP: u64 = 1_767_747_671; + + let mainnet = L1Config::mainnet(); + + assert_eq!(mainnet.blob_schedule.len(), 5); + assert_eq!( + mainnet.blob_schedule.get(&EthereumHardfork::Bpo1.name().to_lowercase()).unwrap(), + &BlobParams::bpo1() + ); + assert_eq!( + mainnet.blob_schedule.get(&EthereumHardfork::Bpo2.name().to_lowercase()).unwrap(), + &BlobParams::bpo2() + ); + + let blob_schedule = mainnet.blob_schedule_blob_params(); + assert_eq!(blob_schedule.scheduled.len(), 2); + assert_eq!(blob_schedule.scheduled[0].0, MAINNET_BPO1_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[1].0, MAINNET_BPO2_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[0].1, BlobParams::bpo1()); + assert_eq!(blob_schedule.scheduled[1].1, BlobParams::bpo2()); + } + + #[test] + fn test_get_l1_bpo_sepolia() { + /// BPO1 hardfork activation timestamp + const SEPOLIA_BPO1_TIMESTAMP: u64 = 1761017184; + + /// BPO2 hardfork activation timestamp + const SEPOLIA_BPO2_TIMESTAMP: u64 = 1761607008; + + let sepolia = L1Config::sepolia(); + + assert_eq!(sepolia.blob_schedule.len(), 5); + assert_eq!( + sepolia.blob_schedule.get(&EthereumHardfork::Bpo1.name().to_lowercase()).unwrap(), + &BlobParams::bpo1() + ); + assert_eq!( + sepolia.blob_schedule.get(&EthereumHardfork::Bpo2.name().to_lowercase()).unwrap(), + &BlobParams::bpo2() + ); + + let blob_schedule = sepolia.blob_schedule_blob_params(); + assert_eq!(blob_schedule.scheduled.len(), 2); + assert_eq!(blob_schedule.scheduled[0].0, SEPOLIA_BPO1_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[1].0, SEPOLIA_BPO2_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[0].1, BlobParams::bpo1()); + assert_eq!(blob_schedule.scheduled[1].1, BlobParams::bpo2()); + } + + #[test] + fn test_get_l1_bpo_holesky() { + /// BPO1 hardfork activation timestamp + const HOLESKY_BPO1_TIMESTAMP: u64 = 1759800000; + + /// BPO2 hardfork activation timestamp + const HOLESKY_BPO2_TIMESTAMP: u64 = 1760389824; + + let holesky = L1Config::holesky(); + + assert_eq!(holesky.blob_schedule.len(), 5); + assert_eq!( + holesky.blob_schedule.get(&EthereumHardfork::Bpo1.name().to_lowercase()).unwrap(), + &BlobParams::bpo1() + ); + assert_eq!( + holesky.blob_schedule.get(&EthereumHardfork::Bpo2.name().to_lowercase()).unwrap(), + &BlobParams::bpo2() + ); + + let blob_schedule = holesky.blob_schedule_blob_params(); + assert_eq!(blob_schedule.scheduled.len(), 2); + assert_eq!(blob_schedule.scheduled[0].0, HOLESKY_BPO1_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[1].0, HOLESKY_BPO2_TIMESTAMP); + assert_eq!(blob_schedule.scheduled[0].1, BlobParams::bpo1()); + assert_eq!(blob_schedule.scheduled[1].1, BlobParams::bpo2()); + } +} diff --git a/kona/crates/protocol/registry/src/lib.rs b/kona/crates/protocol/registry/src/lib.rs new file mode 100644 index 0000000000000..d3ab32b16f826 --- /dev/null +++ b/kona/crates/protocol/registry/src/lib.rs @@ -0,0 +1,193 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub use alloy_primitives::map::HashMap; +use kona_genesis::L1ChainConfig; +pub use kona_genesis::{Chain, ChainConfig, ChainList, RollupConfig}; + +pub mod superchain; +pub use superchain::Registry; + +/// L1 chain configurations. +pub mod l1; +pub use l1::L1Config; + +#[cfg(test)] +pub mod test_utils; + +lazy_static::lazy_static! { + /// Private initializer that loads the superchain configurations. + static ref _INIT: Registry = Registry::from_chain_list(); + + /// Chain configurations exported from the registry + pub static ref CHAINS: ChainList = _INIT.chain_list.clone(); + + /// OP Chain configurations exported from the registry + pub static ref OPCHAINS: HashMap = _INIT.op_chains.clone(); + + /// Rollup configurations exported from the registry + pub static ref ROLLUP_CONFIGS: HashMap = _INIT.rollup_configs.clone(); + + /// L1 chain configurations exported from the registry + /// Note: the l1 chain configurations are not exported from the superchain registry but rather from a genesis dump file. + pub static ref L1_CONFIGS: HashMap = _INIT.l1_configs.clone(); +} + +/// Returns a [RollupConfig] by its identifier. +pub fn scr_rollup_config_by_ident(ident: &str) -> Option<&RollupConfig> { + let chain_id = CHAINS.get_chain_by_ident(ident)?.chain_id; + ROLLUP_CONFIGS.get(&chain_id) +} + +/// Returns a [RollupConfig] by its identifier. +pub fn scr_rollup_config_by_alloy_ident(chain: &alloy_chains::Chain) -> Option<&RollupConfig> { + ROLLUP_CONFIGS.get(&chain.id()) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_chains::Chain as AlloyChain; + use alloy_hardforks::{ + holesky::{HOLESKY_BPO1_TIMESTAMP, HOLESKY_BPO2_TIMESTAMP}, + sepolia::{SEPOLIA_BPO1_TIMESTAMP, SEPOLIA_BPO2_TIMESTAMP}, + }; + use alloy_op_hardforks::{ + BASE_MAINNET_JOVIAN_TIMESTAMP, BASE_SEPOLIA_JOVIAN_TIMESTAMP, OP_MAINNET_JOVIAN_TIMESTAMP, + OP_SEPOLIA_JOVIAN_TIMESTAMP, + }; + + #[test] + fn test_hardcoded_rollup_configs() { + let test_cases = [ + (10, test_utils::OP_MAINNET_CONFIG), + (8453, test_utils::BASE_MAINNET_CONFIG), + (11155420, test_utils::OP_SEPOLIA_CONFIG), + (84532, test_utils::BASE_SEPOLIA_CONFIG), + ] + .to_vec(); + + for (chain_id, expected) in test_cases { + let derived = super::ROLLUP_CONFIGS.get(&chain_id).unwrap(); + assert_eq!(expected, *derived); + } + } + + #[test] + fn test_chain_by_ident() { + const ALLOY_BASE: AlloyChain = AlloyChain::base_mainnet(); + + let chain_by_ident = CHAINS.get_chain_by_ident("mainnet/base").unwrap(); + let chain_by_alloy_ident = CHAINS.get_chain_by_alloy_ident(&ALLOY_BASE).unwrap(); + let chain_by_id = CHAINS.get_chain_by_id(8453).unwrap(); + + assert_eq!(chain_by_ident, chain_by_id); + assert_eq!(chain_by_alloy_ident, chain_by_id); + } + + #[test] + fn test_rollup_config_by_ident() { + const ALLOY_BASE: AlloyChain = AlloyChain::base_mainnet(); + + let rollup_config_by_ident = scr_rollup_config_by_ident("mainnet/base").unwrap(); + let rollup_config_by_alloy_ident = scr_rollup_config_by_alloy_ident(&ALLOY_BASE).unwrap(); + let rollup_config_by_id = ROLLUP_CONFIGS.get(&8453).unwrap(); + + assert_eq!(rollup_config_by_ident, rollup_config_by_id); + assert_eq!(rollup_config_by_alloy_ident, rollup_config_by_id); + } + + #[test] + fn test_jovian_timestamps() { + let base_mainnet_config_by_ident = scr_rollup_config_by_ident("mainnet/base").unwrap(); + assert_eq!( + base_mainnet_config_by_ident.hardforks.jovian_time, + Some(BASE_MAINNET_JOVIAN_TIMESTAMP) + ); + + let base_sepolia_config_by_ident = scr_rollup_config_by_ident("sepolia/base").unwrap(); + assert_eq!( + base_sepolia_config_by_ident.hardforks.jovian_time, + Some(BASE_SEPOLIA_JOVIAN_TIMESTAMP) + ); + + let op_mainnet_config_by_ident = scr_rollup_config_by_ident("mainnet/op").unwrap(); + assert_eq!( + op_mainnet_config_by_ident.hardforks.jovian_time, + Some(OP_MAINNET_JOVIAN_TIMESTAMP) + ); + + let op_sepolia_config_by_ident = scr_rollup_config_by_ident("sepolia/op").unwrap(); + assert_eq!( + op_sepolia_config_by_ident.hardforks.jovian_time, + Some(OP_SEPOLIA_JOVIAN_TIMESTAMP) + ); + } + + #[test] + fn test_bpo_timestamps() { + let sepolia_config = L1_CONFIGS.get(&11155111).unwrap(); + assert_eq!(sepolia_config.bpo1_time, Some(SEPOLIA_BPO1_TIMESTAMP)); + assert_eq!(sepolia_config.bpo2_time, Some(SEPOLIA_BPO2_TIMESTAMP)); + + let holesky_config = L1_CONFIGS.get(&17000).unwrap(); + assert_eq!(holesky_config.bpo1_time, Some(HOLESKY_BPO1_TIMESTAMP)); + assert_eq!(holesky_config.bpo2_time, Some(HOLESKY_BPO2_TIMESTAMP)); + } + + const CUSTOM_CONFIGS_TEST_ENABLED: Option<&str> = option_env!("KONA_CUSTOM_CONFIGS_TEST"); + const CUSTOM_CONFIGS: Option<&str> = option_env!("KONA_CUSTOM_CONFIGS"); + const CUSTOM_CONFIGS_DIR: Option<&str> = option_env!("KONA_CUSTOM_CONFIGS_DIR"); + + #[test] + fn custom_chain_is_loaded_when_enabled() { + if CUSTOM_CONFIGS_TEST_ENABLED != Some("true") { + return; + }; + if CUSTOM_CONFIGS != Some("true") { + panic!("KONA_CUSTOM_CONFIGS is required when KONA_CUSTOM_CONFIGS_TEST is set"); + } + if CUSTOM_CONFIGS_DIR.is_none() { + panic!("KONA_CUSTOM_CONFIGS_DIR is required when KONA_CUSTOM_CONFIGS_TEST is set"); + } + + let test1_chain_id = 123999119; + let test2_chain_id = 223999119; + let test1_ident = "test1/testnet"; + let test2_ident = "test2/testnet"; + + let chain1 = CHAINS + .get_chain_by_ident(test1_ident) + .unwrap_or_else(|| panic!("custom chain `{test1_ident}` missing")); + assert_eq!(chain1.chain_id, test1_chain_id); + let chain2 = CHAINS + .get_chain_by_ident(test2_ident) + .unwrap_or_else(|| panic!("custom chain `{test2_ident}` missing")); + assert_eq!(chain2.chain_id, test2_chain_id); + + assert!( + OPCHAINS.contains_key(&test1_chain_id), + "chain config missing for {test1_chain_id}" + ); + assert!( + ROLLUP_CONFIGS.contains_key(&test1_chain_id), + "rollup config missing for {test1_chain_id}" + ); + assert!( + OPCHAINS.contains_key(&test2_chain_id), + "chain config missing for {test2_chain_id}" + ); + assert!( + ROLLUP_CONFIGS.contains_key(&test2_chain_id), + "rollup config missing for {test2_chain_id}" + ); + } +} diff --git a/kona/crates/protocol/registry/src/superchain.rs b/kona/crates/protocol/registry/src/superchain.rs new file mode 100644 index 0000000000000..f9c859dc0a35e --- /dev/null +++ b/kona/crates/protocol/registry/src/superchain.rs @@ -0,0 +1,167 @@ +//! Contains the full superchain data. + +use crate::L1Config; + +use super::ChainList; +use alloy_primitives::map::HashMap; +use kona_genesis::{ChainConfig, L1ChainConfig, RollupConfig, Superchains}; + +/// The registry containing all the superchain configurations. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct Registry { + /// The list of chains. + pub chain_list: ChainList, + /// Map of chain IDs to their chain configuration. + pub op_chains: HashMap, + /// Map of chain IDs to their rollup configurations. + pub rollup_configs: HashMap, + /// Map of l1 chain IDs to their l1 configurations. + pub l1_configs: HashMap, +} + +impl Registry { + /// Read the chain list. + pub fn read_chain_list() -> ChainList { + let chain_list = include_str!("../etc/chainList.json"); + serde_json::from_str(chain_list).expect("Failed to read chain list") + } + + /// Read superchain configs. + pub fn read_superchain_configs() -> Superchains { + let superchain_configs = include_str!("../etc/configs.json"); + serde_json::from_str(superchain_configs).expect("Failed to read superchain configs") + } + + /// Initialize the superchain configurations from the chain list. + pub fn from_chain_list() -> Self { + let chain_list = Self::read_chain_list(); + let superchains = Self::read_superchain_configs(); + let mut op_chains = HashMap::default(); + let mut rollup_configs = HashMap::default(); + + for superchain in superchains.superchains { + for mut chain_config in superchain.chains { + chain_config.l1_chain_id = superchain.config.l1.chain_id; + if let Some(a) = &mut chain_config.addresses { + a.zero_proof_addresses(); + } + let mut rollup = chain_config.as_rollup_config(); + rollup.protocol_versions_address = superchain + .config + .protocol_versions_addr + .expect("Missing protocol versions address"); + rollup.superchain_config_address = superchain.config.superchain_config_addr; + rollup_configs.insert(chain_config.chain_id, rollup); + op_chains.insert(chain_config.chain_id, chain_config); + } + } + + Self { chain_list, op_chains, rollup_configs, l1_configs: L1Config::build_l1_configs() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::{String, ToString}; + use alloy_op_hardforks::{ + BASE_MAINNET_ISTHMUS_TIMESTAMP, BASE_MAINNET_JOVIAN_TIMESTAMP, + BASE_SEPOLIA_ISTHMUS_TIMESTAMP, BASE_SEPOLIA_JOVIAN_TIMESTAMP, + OP_MAINNET_ISTHMUS_TIMESTAMP, OP_MAINNET_JOVIAN_TIMESTAMP, OP_SEPOLIA_ISTHMUS_TIMESTAMP, + OP_SEPOLIA_JOVIAN_TIMESTAMP, + }; + use alloy_primitives::address; + use kona_genesis::{AddressList, OP_MAINNET_BASE_FEE_CONFIG, Roles, SuperchainLevel}; + + #[test] + fn test_read_chain_configs() { + let superchains = Registry::from_chain_list(); + assert!(superchains.chain_list.len() > 1); + let base_config = ChainConfig { + name: String::from("Base"), + chain_id: 8453, + l1_chain_id: 1, + public_rpc: String::from("https://mainnet.base.org"), + sequencer_rpc: String::from("https://mainnet-sequencer.base.org"), + explorer: String::from("https://explorer.base.org"), + superchain_level: SuperchainLevel::StandardCandidate, + governed_by_optimism: false, + superchain_time: Some(0), + batch_inbox_addr: address!("ff00000000000000000000000000000000008453"), + hardfork_config: crate::test_utils::BASE_MAINNET_CONFIG.hardforks, + block_time: 2, + seq_window_size: 3600, + max_sequencer_drift: 600, + data_availability_type: "eth-da".to_string(), + optimism: Some(OP_MAINNET_BASE_FEE_CONFIG), + alt_da: None, + genesis: crate::test_utils::BASE_MAINNET_CONFIG.genesis, + roles: Some(Roles { + proxy_admin_owner: Some( + "7bB41C3008B3f03FE483B28b8DB90e19Cf07595c".parse().unwrap(), + ), + ..Default::default() + }), + addresses: Some(AddressList { + l1_standard_bridge_proxy: Some(address!( + "3154Cf16ccdb4C6d922629664174b904d80F2C35" + )), + optimism_portal_proxy: Some(address!("49048044D57e1C92A77f79988d21Fa8fAF74E97e")), + system_config_proxy: Some(address!("73a79Fab69143498Ed3712e519A88a918e1f4072")), + dispute_game_factory_proxy: Some(address!( + "43edb88c4b80fdd2adff2412a7bebf9df42cb40e" + )), + ..Default::default() + }), + gas_paying_token: None, + }; + assert_eq!(*superchains.op_chains.get(&8453).unwrap(), base_config); + } + + #[test] + fn test_read_rollup_configs() { + let superchains = Registry::from_chain_list(); + assert_eq!( + *superchains.rollup_configs.get(&10).unwrap(), + crate::test_utils::OP_MAINNET_CONFIG + ); + } + + #[test] + fn test_isthmus_timestamps() { + let superchains = Registry::from_chain_list(); + let op_mainnet_config = superchains.rollup_configs.get(&10).unwrap(); + assert_eq!(op_mainnet_config.hardforks.isthmus_time, Some(OP_MAINNET_ISTHMUS_TIMESTAMP)); + + let op_sepolia_config = superchains.rollup_configs.get(&11155420).unwrap(); + assert_eq!(op_sepolia_config.hardforks.isthmus_time, Some(OP_SEPOLIA_ISTHMUS_TIMESTAMP)); + + let base_mainnet_config = superchains.rollup_configs.get(&8453).unwrap(); + assert_eq!( + base_mainnet_config.hardforks.isthmus_time, + Some(BASE_MAINNET_ISTHMUS_TIMESTAMP) + ); + + let base_sepolia_config = superchains.rollup_configs.get(&84532).unwrap(); + assert_eq!( + base_sepolia_config.hardforks.isthmus_time, + Some(BASE_SEPOLIA_ISTHMUS_TIMESTAMP) + ); + } + + #[test] + fn test_jovian_timestamps() { + let superchains = Registry::from_chain_list(); + let op_mainnet_config = superchains.rollup_configs.get(&10).unwrap(); + assert_eq!(op_mainnet_config.hardforks.jovian_time, Some(OP_MAINNET_JOVIAN_TIMESTAMP)); + + let op_sepolia_config = superchains.rollup_configs.get(&11155420).unwrap(); + assert_eq!(op_sepolia_config.hardforks.jovian_time, Some(OP_SEPOLIA_JOVIAN_TIMESTAMP)); + + let base_mainnet_config = superchains.rollup_configs.get(&8453).unwrap(); + assert_eq!(base_mainnet_config.hardforks.jovian_time, Some(BASE_MAINNET_JOVIAN_TIMESTAMP)); + + let base_sepolia_config = superchains.rollup_configs.get(&84532).unwrap(); + assert_eq!(base_sepolia_config.hardforks.jovian_time, Some(BASE_SEPOLIA_JOVIAN_TIMESTAMP)); + } +} diff --git a/kona/crates/protocol/registry/src/test_utils/base_mainnet.rs b/kona/crates/protocol/registry/src/test_utils/base_mainnet.rs new file mode 100644 index 0000000000000..7d0c73e77bd5e --- /dev/null +++ b/kona/crates/protocol/registry/src/test_utils/base_mainnet.rs @@ -0,0 +1,73 @@ +//! Base Mainnet Rollup Config. + +use alloy_chains::Chain; +use alloy_eips::BlockNumHash; +use alloy_op_hardforks::{ + BASE_MAINNET_CANYON_TIMESTAMP, BASE_MAINNET_ECOTONE_TIMESTAMP, BASE_MAINNET_FJORD_TIMESTAMP, + BASE_MAINNET_GRANITE_TIMESTAMP, BASE_MAINNET_HOLOCENE_TIMESTAMP, + BASE_MAINNET_ISTHMUS_TIMESTAMP, BASE_MAINNET_JOVIAN_TIMESTAMP, +}; +use alloy_primitives::{address, b256, uint}; +use kona_genesis::{ + BASE_MAINNET_BASE_FEE_CONFIG, ChainGenesis, DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + HardForkConfig, RollupConfig, SystemConfig, +}; + +/// The [RollupConfig] for Base Mainnet. +pub const BASE_MAINNET_CONFIG: RollupConfig = RollupConfig { + genesis: ChainGenesis { + l1: BlockNumHash { + hash: b256!("5c13d307623a926cd31415036c8b7fa14572f9dac64528e857a470511fc30771"), + number: 17_481_768_u64, + }, + l2: BlockNumHash { + hash: b256!("f712aa9241cc24369b143cf6dce85f0902a9731e70d66818a3a5845b296c73dd"), + number: 0_u64, + }, + l2_time: 1686789347_u64, + system_config: Some(SystemConfig { + batcher_address: address!("5050f69a9786f081509234f1a7f4684b5e5b76c9"), + overhead: uint!(0xbc_U256), + scalar: uint!(0xa6fe0_U256), + gas_limit: 30_000_000_u64, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }), + }, + block_time: 2, + max_sequencer_drift: 600, + seq_window_size: 3600, + channel_timeout: 300, + granite_channel_timeout: 50, + l1_chain_id: 1, + l2_chain_id: Chain::base_mainnet(), + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(BASE_MAINNET_CANYON_TIMESTAMP), + delta_time: Some(1708560000), + ecotone_time: Some(BASE_MAINNET_ECOTONE_TIMESTAMP), + fjord_time: Some(BASE_MAINNET_FJORD_TIMESTAMP), + granite_time: Some(BASE_MAINNET_GRANITE_TIMESTAMP), + holocene_time: Some(BASE_MAINNET_HOLOCENE_TIMESTAMP), + pectra_blob_schedule_time: None, + isthmus_time: Some(BASE_MAINNET_ISTHMUS_TIMESTAMP), + jovian_time: Some(BASE_MAINNET_JOVIAN_TIMESTAMP), + interop_time: None, + }, + batch_inbox_address: address!("ff00000000000000000000000000000000008453"), + deposit_contract_address: address!("49048044d57e1c92a77f79988d21fa8faf74e97e"), + l1_system_config_address: address!("73a79fab69143498ed3712e519a88a918e1f4072"), + protocol_versions_address: address!("8062abc286f5e7d9428a0ccb9abd71e50d93b935"), + superchain_config_address: Some(address!("95703e0982140D16f8ebA6d158FccEde42f04a4C")), + da_challenge_address: None, + blobs_enabled_l1_timestamp: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + alt_da_config: None, + chain_op_config: BASE_MAINNET_BASE_FEE_CONFIG, +}; diff --git a/kona/crates/protocol/registry/src/test_utils/base_sepolia.rs b/kona/crates/protocol/registry/src/test_utils/base_sepolia.rs new file mode 100644 index 0000000000000..b52d182efb9db --- /dev/null +++ b/kona/crates/protocol/registry/src/test_utils/base_sepolia.rs @@ -0,0 +1,73 @@ +//! Base Sepolia Rollup Config. + +use alloy_chains::Chain; +use alloy_eips::BlockNumHash; +use alloy_op_hardforks::{ + BASE_SEPOLIA_CANYON_TIMESTAMP, BASE_SEPOLIA_ECOTONE_TIMESTAMP, BASE_SEPOLIA_FJORD_TIMESTAMP, + BASE_SEPOLIA_GRANITE_TIMESTAMP, BASE_SEPOLIA_HOLOCENE_TIMESTAMP, + BASE_SEPOLIA_ISTHMUS_TIMESTAMP, BASE_SEPOLIA_JOVIAN_TIMESTAMP, +}; +use alloy_primitives::{address, b256, uint}; +use kona_genesis::{ + BASE_SEPOLIA_BASE_FEE_CONFIG, ChainGenesis, DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, + HardForkConfig, RollupConfig, SystemConfig, +}; + +/// The [RollupConfig] for Base Sepolia. +pub const BASE_SEPOLIA_CONFIG: RollupConfig = RollupConfig { + genesis: ChainGenesis { + l1: BlockNumHash { + hash: b256!("cac9a83291d4dec146d6f7f69ab2304f23f5be87b1789119a0c5b1e4482444ed"), + number: 4370868, + }, + l2: BlockNumHash { + hash: b256!("0dcc9e089e30b90ddfc55be9a37dd15bc551aeee999d2e2b51414c54eaf934e4"), + number: 0, + }, + l2_time: 1695768288, + system_config: Some(SystemConfig { + batcher_address: address!("6cdebe940bc0f26850285caca097c11c33103e47"), + overhead: uint!(0x834_U256), + scalar: uint!(0xf4240_U256), + gas_limit: 25000000, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }), + }, + block_time: 2, + max_sequencer_drift: 600, + seq_window_size: 3600, + channel_timeout: 300, + granite_channel_timeout: 50, + l1_chain_id: 11155111, + l2_chain_id: Chain::base_sepolia(), + chain_op_config: BASE_SEPOLIA_BASE_FEE_CONFIG, + alt_da_config: None, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(BASE_SEPOLIA_CANYON_TIMESTAMP), + delta_time: Some(1703203200), + ecotone_time: Some(BASE_SEPOLIA_ECOTONE_TIMESTAMP), + fjord_time: Some(BASE_SEPOLIA_FJORD_TIMESTAMP), + granite_time: Some(BASE_SEPOLIA_GRANITE_TIMESTAMP), + holocene_time: Some(BASE_SEPOLIA_HOLOCENE_TIMESTAMP), + pectra_blob_schedule_time: Some(1742486400), + isthmus_time: Some(BASE_SEPOLIA_ISTHMUS_TIMESTAMP), + jovian_time: Some(BASE_SEPOLIA_JOVIAN_TIMESTAMP), + interop_time: None, + }, + batch_inbox_address: address!("ff00000000000000000000000000000000084532"), + deposit_contract_address: address!("49f53e41452c74589e85ca1677426ba426459e85"), + l1_system_config_address: address!("f272670eb55e895584501d564afeb048bed26194"), + protocol_versions_address: address!("79add5713b383daa0a138d3c4780c7a1804a8090"), + superchain_config_address: Some(address!("C2Be75506d5724086DEB7245bd260Cc9753911Be")), + da_challenge_address: None, + blobs_enabled_l1_timestamp: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, +}; diff --git a/kona/crates/protocol/registry/src/test_utils/mod.rs b/kona/crates/protocol/registry/src/test_utils/mod.rs new file mode 100644 index 0000000000000..3f18b58e3066b --- /dev/null +++ b/kona/crates/protocol/registry/src/test_utils/mod.rs @@ -0,0 +1,13 @@ +//! Test-only module containing hardcoded configs for testing. + +mod op_mainnet; +pub use op_mainnet::OP_MAINNET_CONFIG; + +mod base_mainnet; +pub use base_mainnet::BASE_MAINNET_CONFIG; + +mod op_sepolia; +pub use op_sepolia::OP_SEPOLIA_CONFIG; + +mod base_sepolia; +pub use base_sepolia::BASE_SEPOLIA_CONFIG; diff --git a/kona/crates/protocol/registry/src/test_utils/op_mainnet.rs b/kona/crates/protocol/registry/src/test_utils/op_mainnet.rs new file mode 100644 index 0000000000000..1bb0cbba4e7ea --- /dev/null +++ b/kona/crates/protocol/registry/src/test_utils/op_mainnet.rs @@ -0,0 +1,73 @@ +//! OP Mainnet Rollup Config. + +use alloy_chains::Chain; +use alloy_eips::BlockNumHash; +use alloy_op_hardforks::{ + OP_MAINNET_CANYON_TIMESTAMP, OP_MAINNET_ECOTONE_TIMESTAMP, OP_MAINNET_FJORD_TIMESTAMP, + OP_MAINNET_GRANITE_TIMESTAMP, OP_MAINNET_HOLOCENE_TIMESTAMP, OP_MAINNET_ISTHMUS_TIMESTAMP, + OP_MAINNET_JOVIAN_TIMESTAMP, +}; +use alloy_primitives::{address, b256, uint}; +use kona_genesis::{ + ChainGenesis, DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, HardForkConfig, + OP_MAINNET_BASE_FEE_CONFIG, RollupConfig, SystemConfig, +}; + +/// The [RollupConfig] for OP Mainnet. +pub const OP_MAINNET_CONFIG: RollupConfig = RollupConfig { + genesis: ChainGenesis { + l1: BlockNumHash { + hash: b256!("438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108"), + number: 17_422_590_u64, + }, + l2: BlockNumHash { + hash: b256!("dbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3"), + number: 105_235_063_u64, + }, + l2_time: 1_686_068_903_u64, + system_config: Some(SystemConfig { + batcher_address: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), + overhead: uint!(0xbc_U256), + scalar: uint!(0xa6fe0_U256), + gas_limit: 30_000_000_u64, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }), + }, + block_time: 2_u64, + max_sequencer_drift: 600_u64, + seq_window_size: 3600_u64, + channel_timeout: 300_u64, + granite_channel_timeout: 50, + l1_chain_id: 1_u64, + l2_chain_id: Chain::optimism_mainnet(), + chain_op_config: OP_MAINNET_BASE_FEE_CONFIG, + alt_da_config: None, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(OP_MAINNET_CANYON_TIMESTAMP), + delta_time: Some(1_708_560_000_u64), + ecotone_time: Some(OP_MAINNET_ECOTONE_TIMESTAMP), + fjord_time: Some(OP_MAINNET_FJORD_TIMESTAMP), + granite_time: Some(OP_MAINNET_GRANITE_TIMESTAMP), + holocene_time: Some(OP_MAINNET_HOLOCENE_TIMESTAMP), + pectra_blob_schedule_time: None, + isthmus_time: Some(OP_MAINNET_ISTHMUS_TIMESTAMP), + jovian_time: Some(OP_MAINNET_JOVIAN_TIMESTAMP), + interop_time: None, + }, + batch_inbox_address: address!("ff00000000000000000000000000000000000010"), + deposit_contract_address: address!("beb5fc579115071764c7423a4f12edde41f106ed"), + l1_system_config_address: address!("229047fed2591dbec1ef1118d64f7af3db9eb290"), + protocol_versions_address: address!("8062abc286f5e7d9428a0ccb9abd71e50d93b935"), + superchain_config_address: Some(address!("95703e0982140D16f8ebA6d158FccEde42f04a4C")), + da_challenge_address: None, + blobs_enabled_l1_timestamp: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, +}; diff --git a/kona/crates/protocol/registry/src/test_utils/op_sepolia.rs b/kona/crates/protocol/registry/src/test_utils/op_sepolia.rs new file mode 100644 index 0000000000000..e912d72ca9da1 --- /dev/null +++ b/kona/crates/protocol/registry/src/test_utils/op_sepolia.rs @@ -0,0 +1,73 @@ +//! OP Sepolia Rollup Config. + +use alloy_chains::Chain; +use alloy_eips::BlockNumHash; +use alloy_op_hardforks::{ + OP_SEPOLIA_CANYON_TIMESTAMP, OP_SEPOLIA_ECOTONE_TIMESTAMP, OP_SEPOLIA_FJORD_TIMESTAMP, + OP_SEPOLIA_GRANITE_TIMESTAMP, OP_SEPOLIA_HOLOCENE_TIMESTAMP, OP_SEPOLIA_ISTHMUS_TIMESTAMP, + OP_SEPOLIA_JOVIAN_TIMESTAMP, +}; +use alloy_primitives::{address, b256, uint}; +use kona_genesis::{ + ChainGenesis, DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, HardForkConfig, + OP_SEPOLIA_BASE_FEE_CONFIG, RollupConfig, SystemConfig, +}; + +/// The [RollupConfig] for OP Sepolia. +pub const OP_SEPOLIA_CONFIG: RollupConfig = RollupConfig { + genesis: ChainGenesis { + l1: BlockNumHash { + hash: b256!("48f520cf4ddaf34c8336e6e490632ea3cf1e5e93b0b2bc6e917557e31845371b"), + number: 4071408, + }, + l2: BlockNumHash { + hash: b256!("102de6ffb001480cc9b8b548fd05c34cd4f46ae4aa91759393db90ea0409887d"), + number: 0, + }, + l2_time: 1691802540, + system_config: Some(SystemConfig { + batcher_address: address!("8f23bb38f531600e5d8fddaaec41f13fab46e98c"), + overhead: uint!(0xbc_U256), + scalar: uint!(0xa6fe0_U256), + gas_limit: 30_000_000, + base_fee_scalar: None, + blob_base_fee_scalar: None, + eip1559_denominator: None, + eip1559_elasticity: None, + operator_fee_scalar: None, + operator_fee_constant: None, + min_base_fee: None, + da_footprint_gas_scalar: None, + }), + }, + block_time: 2, + max_sequencer_drift: 600, + seq_window_size: 3600, + channel_timeout: 300, + granite_channel_timeout: 50, + l1_chain_id: 11155111, + l2_chain_id: Chain::optimism_sepolia(), + chain_op_config: OP_SEPOLIA_BASE_FEE_CONFIG, + alt_da_config: None, + hardforks: HardForkConfig { + regolith_time: None, + canyon_time: Some(OP_SEPOLIA_CANYON_TIMESTAMP), + delta_time: Some(1703203200), + ecotone_time: Some(OP_SEPOLIA_ECOTONE_TIMESTAMP), + fjord_time: Some(OP_SEPOLIA_FJORD_TIMESTAMP), + granite_time: Some(OP_SEPOLIA_GRANITE_TIMESTAMP), + holocene_time: Some(OP_SEPOLIA_HOLOCENE_TIMESTAMP), + pectra_blob_schedule_time: Some(1742486400), + isthmus_time: Some(OP_SEPOLIA_ISTHMUS_TIMESTAMP), + jovian_time: Some(OP_SEPOLIA_JOVIAN_TIMESTAMP), + interop_time: None, + }, + batch_inbox_address: address!("ff00000000000000000000000000000011155420"), + deposit_contract_address: address!("16fc5058f25648194471939df75cf27a2fdc48bc"), + l1_system_config_address: address!("034edd2a225f7f429a63e0f1d2084b9e0a93b538"), + protocol_versions_address: address!("79add5713b383daa0a138d3c4780c7a1804a8090"), + superchain_config_address: Some(address!("C2Be75506d5724086DEB7245bd260Cc9753911Be")), + da_challenge_address: None, + blobs_enabled_l1_timestamp: None, + interop_message_expiry_window: DEFAULT_INTEROP_MESSAGE_EXPIRY_WINDOW, +}; diff --git a/kona/crates/protocol/registry/tests/fixtures/custom/chainList.json b/kona/crates/protocol/registry/tests/fixtures/custom/chainList.json new file mode 100644 index 0000000000000..a5003a5a05765 --- /dev/null +++ b/kona/crates/protocol/registry/tests/fixtures/custom/chainList.json @@ -0,0 +1,45 @@ +[ + { + "name": "test1", + "identifier": "test1/testnet", + "chainId": 123999119, + "rpc": [ + "https://rpc.test1.com" + ], + "explorers": [ + "https://explorer.test1.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "testnet" + }, + "faultProofs": { + "status": "none" + } + }, + { + "name": "test2", + "identifier": "test2/testnet", + "chainId": 223999119, + "rpc": [ + "https://rpc.test2.com" + ], + "explorers": [ + "https://explorer.test2.com" + ], + "superchainLevel": 0, + "governedByOptimism": false, + "dataAvailabilityType": "eth-da", + "parent": { + "type": "L2", + "chain": "testnet" + }, + "faultProofs": { + "status": "none" + } + } +] + diff --git a/kona/crates/protocol/registry/tests/fixtures/custom/configs.json b/kona/crates/protocol/registry/tests/fixtures/custom/configs.json new file mode 100644 index 0000000000000..ad3aa5dfedf89 --- /dev/null +++ b/kona/crates/protocol/registry/tests/fixtures/custom/configs.json @@ -0,0 +1,202 @@ +{ + "superchains": [ + { + "name": "sepolia", + "config": { + "name": "Sepolia", + "l1": { + "chain_id": 9999911112213, + "public_rpc": "https://rpc.faketestnet.com", + "explorer": "https://explorer.faketestnet.com" + }, + "hardforks": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "protocol_versions_addr": "0xdedaddeaddeaddeaddeaddeaddeaddeaddeadded", + "superchain_config_addr": "0xdedaddeaddeaddeaddeaddeaddeaddeaddeadded", + "op_contracts_manager_proxy_addr": null + }, + "chains": [ + { + "Name": "test1", + "PublicRPC": "https://rpc.test1.com", + "SequencerRPC": "https://test1-sequencer.com", + "Explorer": "https://explorer.test1.com", + "SuperchainLevel": 2, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 123999119, + "batch_inbox_address": "0xff00000000000000000000000000000000000099", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17422590, + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108" + }, + "l2": { + "number": 105235063, + "hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddr": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "SystemConfigProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + }, + { + "Name": "test2", + "PublicRPC": "https://rpc.test2.com", + "SequencerRPC": "https://test2-sequencer.com", + "Explorer": "https://explorer.test2.com", + "SuperchainLevel": 2, + "GovernedByOptimism": false, + "SuperchainTime": 0, + "DataAvailabilityType": "eth-da", + "l2_chain_id": 223999119, + "batch_inbox_address": "0xff00000000000000000000000000000000000099", + "block_time": 2, + "seq_window_size": 3600, + "max_sequencer_drift": 600, + "GasPayingToken": null, + "hardfork_configuration": { + "canyon_time": 1704992401, + "delta_time": 1708560000, + "ecotone_time": 1710374401, + "fjord_time": 1720627201, + "granite_time": 1726070401, + "holocene_time": 1736445601, + "isthmus_time": 1746806401, + "jovian_time": 1764691201 + }, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50, + "eip1559DenominatorCanyon": 250 + }, + "alt_da": null, + "genesis": { + "l1": { + "number": 17422590, + "hash": "0x438335a20d98863a4c0c97999eb2481921ccd28553eac6f913af7c12aec04108" + }, + "l2": { + "number": 105235063, + "hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + }, + "l2_time": 1686068903, + "system_config": { + "batcherAddr": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000a6fe0", + "gasLimit": 30000000, + "baseFeeScalar": null, + "blobBaseFeeScalar": null, + "eip1559Denominator": null, + "eip1559Elasticity": null, + "operatorFeeScalar": null, + "operatorFeeConstant": null, + "minBaseFee": null, + "daFootprintGasScalar": null + } + }, + "Roles": { + "SystemConfigOwner": null, + "ProxyAdminOwner": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "Guardian": null, + "Challenger": null, + "Proposer": null, + "UnsafeBlockSigner": null, + "BatchSubmitter": null + }, + "Addresses": { + "AddressManager": null, + "L1CrossDomainMessengerProxy": null, + "L1Erc721BridgeProxy": null, + "L1StandardBridgeProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "L2OutputOracleProxy": null, + "OptimismMintableErc20FactoryProxy": null, + "OptimismPortalProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "SystemConfigProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "ProxyAdmin": null, + "SuperchainConfig": null, + "AnchorStateRegistryProxy": null, + "DelayedWethProxy": null, + "DisputeGameFactoryProxy": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "FaultDisputeGame": null, + "Mips": null, + "PermissionedDisputeGame": null, + "PreimageOracle": null, + "DataAvailabilityChallenge": null + } + } + ] + } + ] +} diff --git a/kona/crates/providers/providers-alloy/Cargo.toml b/kona/crates/providers/providers-alloy/Cargo.toml new file mode 100644 index 0000000000000..48139b18bd217 --- /dev/null +++ b/kona/crates/providers/providers-alloy/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "kona-providers-alloy" +version = "0.3.3" +description = "Alloy Backed Providers" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# Kona +kona-macros.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true +kona-derive.workspace = true + +# Alloy +alloy-serde.workspace = true +alloy-eips = { workspace = true, features = ["kzg"] } +alloy-transport.workspace = true +alloy-transport-http = { workspace = true, features = ["reqwest", "reqwest-rustls-tls", "hyper", "hyper-tls", "jwt-auth"] } +alloy-consensus.workspace = true +alloy-rpc-types-beacon.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-rpc-client.workspace = true +alloy-provider = { workspace = true, features = ["ipc", "ws", "reqwest"] } +alloy-primitives = { workspace = true, features = ["map"] } + +# Op Alloy +op-alloy-consensus.workspace = true +op-alloy-network.workspace = true + +# Misc +lru.workspace = true +serde.workspace = true +thiserror.workspace = true +async-trait.workspace = true +reqwest = { workspace = true, features = ["json"] } +tower.workspace = true +http-body-util.workspace = true + +c-kzg.workspace = true + +# `metrics` feature +metrics = { workspace = true, optional = true } + +[features] +default = [] +metrics = [ "dep:metrics", "kona-derive/metrics" ] + +[dev-dependencies] +tokio.workspace = true diff --git a/kona/crates/providers/providers-alloy/README.md b/kona/crates/providers/providers-alloy/README.md new file mode 100644 index 0000000000000..68e76d096d826 --- /dev/null +++ b/kona/crates/providers/providers-alloy/README.md @@ -0,0 +1,8 @@ +# `kona-providers-alloy` + +CI +kona-provides-alloy +License +Codecov + +Alloy-backed providers for `kona`. diff --git a/kona/crates/providers/providers-alloy/src/beacon_client.rs b/kona/crates/providers/providers-alloy/src/beacon_client.rs new file mode 100644 index 0000000000000..0708a22275ef5 --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/beacon_client.rs @@ -0,0 +1,231 @@ +//! Contains an online implementation of the `BeaconClient` trait. + +#[cfg(feature = "metrics")] +use crate::Metrics; +use crate::blobs::BoxedBlobWithIndex; +use alloy_eips::eip4844::IndexedBlobHash; +use alloy_rpc_types_beacon::sidecar::{BeaconBlobBundle, GetBlobsResponse}; +use async_trait::async_trait; +use reqwest::Client; +use std::{boxed::Box, format, string::String, vec::Vec}; + +/// The config spec engine api method. +const SPEC_METHOD: &str = "eth/v1/config/spec"; + +/// The beacon genesis engine api method. +const GENESIS_METHOD: &str = "eth/v1/beacon/genesis"; + +/// The blob sidecars engine api method prefix. +const SIDECARS_METHOD_PREFIX_DEPRECATED: &str = "eth/v1/beacon/blob_sidecars"; + +/// THe blobs engine api method prefix. +const BLOBS_METHOD_PREFIX: &str = "eth/v1/beacon/blobs"; + +/// A reduced genesis data. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ReducedGenesisData { + /// The genesis time. + #[serde(rename = "genesis_time")] + #[serde(with = "alloy_serde::quantity")] + pub genesis_time: u64, +} + +/// An API genesis response. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct APIGenesisResponse { + /// The data. + pub data: ReducedGenesisData, +} + +/// A reduced config data. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ReducedConfigData { + /// The seconds per slot. + #[serde(rename = "SECONDS_PER_SLOT")] + #[serde(with = "alloy_serde::quantity")] + pub seconds_per_slot: u64, +} + +/// An API config response. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct APIConfigResponse { + /// The data. + pub data: ReducedConfigData, +} + +impl APIConfigResponse { + /// Creates a new API config response. + pub const fn new(seconds_per_slot: u64) -> Self { + Self { data: ReducedConfigData { seconds_per_slot } } + } +} + +impl APIGenesisResponse { + /// Creates a new API genesis response. + pub const fn new(genesis_time: u64) -> Self { + Self { data: ReducedGenesisData { genesis_time } } + } +} + +/// The [BeaconClient] is a thin wrapper around the Beacon API. +#[async_trait] +pub trait BeaconClient { + /// The error type for [BeaconClient] implementations. + type Error: core::fmt::Display; + + /// Returns the slot interval in seconds. + async fn slot_interval(&self) -> Result; + + /// Returns the beacon genesis time. + async fn genesis_time(&self) -> Result; + + /// Fetches blobs that were confirmed in the specified L1 block with the given slot. + /// Blob data is not checked for validity. + async fn filtered_beacon_blobs( + &self, + slot: u64, + blob_hashes: &[IndexedBlobHash], + ) -> Result, Self::Error>; +} + +/// An online implementation of the [BeaconClient] trait. +#[derive(Debug, Clone)] +pub struct OnlineBeaconClient { + /// The base URL of the beacon API. + pub base: String, + /// The inner reqwest client. + pub inner: Client, + /// The duration in seconds of an L1 slot. This can be used to override the CL slot + /// duration if the l1-beacon's slot configuration endpoint is not available. + pub l1_slot_duration: Option, +} + +impl OnlineBeaconClient { + /// Creates a new [OnlineBeaconClient] from the provided base URL string. + pub fn new_http(mut base: String) -> Self { + // If base ends with a slash, remove it + if base.ends_with("/") { + base.remove(base.len() - 1); + } + Self { + base, + inner: Client::builder().build().expect("Failed to create beacon client"), + l1_slot_duration: None, + } + } + + /// Sets the duration in seconds of an L1 slot. This can be used to override the CL slot + /// duration if the l1-beacon's slot configuration endpoint is not available. + pub const fn with_l1_slot_duration_override(mut self, l1_slot_duration: u64) -> Self { + self.l1_slot_duration = Some(l1_slot_duration); + self + } + + async fn filtered_beacon_blobs( + &self, + slot: u64, + blob_hashes: &[IndexedBlobHash], + ) -> Result, reqwest::Error> { + let blob_indexes = blob_hashes.iter().map(|blob| blob.index).collect::>(); + + Ok( + match self + .inner + .get(format!("{}/{}/{}", self.base, BLOBS_METHOD_PREFIX, slot)) + .send() + .await + { + Ok(response) if response.status().is_success() => { + let bundle = response.json::().await?; + + bundle + .data + .into_iter() + .enumerate() + .filter_map(|(index, blob)| { + let index = index as u64; + blob_indexes + .contains(&index) + .then_some(BoxedBlobWithIndex { index, blob: Box::new(blob) }) + }) + .collect::>() + } + // If the blobs endpoint fails, try the deprecated sidecars endpoint. CL Clients + // only support the blobs endpoint from Fusaka (Fulu) onwards. + _ => self + .inner + .get(format!("{}/{}/{}", self.base, SIDECARS_METHOD_PREFIX_DEPRECATED, slot)) + .send() + .await? + .json::() + .await? + .into_iter() + .filter_map(|blob| { + blob_indexes + .contains(&blob.index) + .then_some(BoxedBlobWithIndex { index: blob.index, blob: blob.blob }) + }) + .collect::>(), + }, + ) + } +} + +#[async_trait] +impl BeaconClient for OnlineBeaconClient { + type Error = reqwest::Error; + + async fn slot_interval(&self) -> Result { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_REQUESTS, "method" => "spec"); + + // Use the l1 slot duration if provided + if let Some(l1_slot_duration) = self.l1_slot_duration { + return Ok(APIConfigResponse::new(l1_slot_duration)); + } + + let result = async { + let first = self.inner.get(format!("{}/{}", self.base, SPEC_METHOD)).send().await?; + first.json::().await + } + .await; + + if result.is_err() { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_ERRORS, "method" => "spec"); + } + + result + } + + async fn genesis_time(&self) -> Result { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_REQUESTS, "method" => "genesis"); + + let result = async { + let first = self.inner.get(format!("{}/{}", self.base, GENESIS_METHOD)).send().await?; + first.json::().await + } + .await; + + if result.is_err() { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_ERRORS, "method" => "genesis"); + } + + result + } + + async fn filtered_beacon_blobs( + &self, + slot: u64, + blob_hashes: &[IndexedBlobHash], + ) -> Result, Self::Error> { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_REQUESTS, "method" => "blobs"); + + // Try to get the blobs from the blobs endpoint. + let result = self.filtered_beacon_blobs(slot, blob_hashes).await; + + if result.is_err() { + kona_macros::inc!(gauge, Metrics::BEACON_CLIENT_ERRORS, "method" => "blobs"); + } + + result + } +} diff --git a/kona/crates/providers/providers-alloy/src/blobs.rs b/kona/crates/providers/providers-alloy/src/blobs.rs new file mode 100644 index 0000000000000..0e1b455cb81d0 --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/blobs.rs @@ -0,0 +1,197 @@ +//! Contains an online implementation of the `BlobProvider` trait. + +use crate::BeaconClient; +#[cfg(feature = "metrics")] +use crate::Metrics; +use alloy_eips::eip4844::{ + Blob, BlobTransactionSidecarItem, IndexedBlobHash, env_settings::EnvKzgSettings, +}; +use alloy_primitives::FixedBytes; +use async_trait::async_trait; +use kona_derive::{BlobProvider, BlobProviderError}; +use kona_protocol::BlockInfo; +use std::{boxed::Box, string::ToString, vec::Vec}; + +/// A boxed blob with index. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BoxedBlobWithIndex { + /// The index of the blob. + pub index: u64, + /// The blob data. + pub blob: Box, +} + +/// An online implementation of the [BlobProvider] trait. +#[derive(Debug, Clone)] +pub struct OnlineBlobProvider { + /// The Beacon API client. + pub beacon_client: B, + /// Beacon Genesis time used for the time to slot conversion. + pub genesis_time: u64, + /// Slot interval used for the time to slot conversion. + pub slot_interval: u64, +} + +impl OnlineBlobProvider { + /// Creates a new instance of the [OnlineBlobProvider]. + /// + /// The `genesis_time` and `slot_interval` arguments are _optional_ and the + /// [OnlineBlobProvider] will attempt to load them dynamically at runtime if they are not + /// provided. + /// + /// ## Panics + /// Panics if the genesis time or slot interval cannot be loaded from the beacon client. + pub async fn init(beacon_client: B) -> Self { + let genesis_time = beacon_client + .genesis_time() + .await + .map(|r| r.data.genesis_time) + .map_err(|e| BlobProviderError::Backend(e.to_string())) + .expect("Failed to load genesis time from beacon client"); + let slot_interval = beacon_client + .slot_interval() + .await + .map(|r| r.data.seconds_per_slot) + .map_err(|e| BlobProviderError::Backend(e.to_string())) + .expect("Failed to load slot interval from beacon client"); + Self { beacon_client, genesis_time, slot_interval } + } + + /// Computes the slot for the given timestamp. + pub const fn slot( + genesis: u64, + slot_time: u64, + timestamp: u64, + ) -> Result { + if timestamp < genesis { + return Err(BlobProviderError::SlotDerivation); + } + Ok((timestamp - genesis) / slot_time) + } + + /// Fetches blobs for the given slot. + async fn fetch_filtered_blobs( + &self, + slot: u64, + blob_hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError> { + kona_macros::inc!(gauge, Metrics::BLOB_SIDECAR_FETCHES); + + let result = self + .beacon_client + .filtered_beacon_blobs(slot, blob_hashes) + .await + .map_err(|e| BlobProviderError::Backend(e.to_string())); + + #[cfg(feature = "metrics")] + if result.is_err() { + kona_macros::inc!(gauge, Metrics::BLOB_SIDECAR_FETCH_ERRORS); + } + + result + } + + /// Converts a vector of boxed blobs with index to a vector of blob transaction sidecar items. + /// + /// Note: for performance reasons, we need to transmute the blobs to the c_kzg::Blob type to + /// avoid the overhead of moving the blobs around or reallocating the memory. + fn sidecar_from_blobs( + blobs: Vec, + ) -> Result, c_kzg::Error> { + blobs + .into_iter() + .map(|blob| { + let kzg_settings = EnvKzgSettings::Default; + + // SAFETY: all types have the same size and alignment + let kzg_blob = + unsafe { Box::from_raw(Box::::into_raw(blob.blob) as *mut c_kzg::Blob) }; + + let commitment = kzg_settings + .get() + .blob_to_kzg_commitment(&kzg_blob) + .map(|blob| blob.to_bytes())?; + let proof = kzg_settings + .get() + .compute_blob_kzg_proof(&kzg_blob, &commitment) + .map(|proof| proof.to_bytes())?; + + // SAFETY: all types have the same size and alignment + let alloy_blob = + unsafe { Box::from_raw(Box::::into_raw(kzg_blob) as *mut Blob) }; + + Ok(BlobTransactionSidecarItem { + index: blob.index, + blob: alloy_blob, + kzg_commitment: FixedBytes::from(*commitment), + kzg_proof: FixedBytes::from(*proof), + }) + }) + .collect() + } + + /// Fetches blob sidecars for the given block reference and blob hashes. + /// Does not validate the blobs. Recomputes the kzg proofs associated with the blobs. + /// + /// Use [`Self::beacon_client`] to fetch the blobs without recomputing the kzg + /// proofs/commitments. + pub async fn fetch_filtered_blob_sidecars( + &self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError> { + if blob_hashes.is_empty() { + return Ok(Default::default()); + } + + // Calculate the slot for the given timestamp. + let slot = Self::slot(self.genesis_time, self.slot_interval, block_ref.timestamp)?; + + // Fetch blobs for the slot using. + let blobs = self.fetch_filtered_blobs(slot, blob_hashes).await?; + + Self::sidecar_from_blobs(blobs) + .map_err(|e| BlobProviderError::Backend(format!("KZG commitment error: {e}"))) + } +} + +#[async_trait] +impl BlobProvider for OnlineBlobProvider +where + B: BeaconClient + Send + Sync, +{ + type Error = BlobProviderError; + + /// Fetches blobs that were confirmed in the specified L1 block with the given indexed + /// hashes. The blobs are validated for their index and hashes using the specified + /// [IndexedBlobHash]. + async fn get_and_validate_blobs( + &mut self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result>, Self::Error> { + // Fetch the blob sidecars for the given block reference and blob hashes. + let blobs = self.fetch_filtered_blob_sidecars(block_ref, blob_hashes).await?; + + // Validate the blob sidecars straight away with the num hashes. + let blobs = blobs + .into_iter() + .enumerate() + .map(|(i, sidecar)| { + let hash = blob_hashes + .get(i) + .ok_or(BlobProviderError::Backend("Missing blob hash".to_string()))? + .hash + .as_slice(); + + if sidecar.to_kzg_versioned_hash() != hash { + return Err(BlobProviderError::Backend("KZG commitment mismatch".to_string())); + } + + Ok(sidecar.blob) + }) + .collect::>, BlobProviderError>>() + .map_err(|e| BlobProviderError::Backend(e.to_string()))?; + Ok(blobs) + } +} diff --git a/kona/crates/providers/providers-alloy/src/chain_provider.rs b/kona/crates/providers/providers-alloy/src/chain_provider.rs new file mode 100644 index 0000000000000..22f68b7b29acb --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/chain_provider.rs @@ -0,0 +1,271 @@ +//! Providers that use alloy provider types on the backend. + +#[cfg(feature = "metrics")] +use crate::Metrics; +use alloy_consensus::{Header, Receipt, TxEnvelope}; +use alloy_eips::BlockId; +use alloy_primitives::B256; +use alloy_provider::{Provider, RootProvider}; +use alloy_transport::{RpcError, TransportErrorKind}; +use async_trait::async_trait; +use kona_derive::{ChainProvider, PipelineError, PipelineErrorKind}; +use kona_protocol::BlockInfo; +use lru::LruCache; +use std::{boxed::Box, num::NonZeroUsize, vec::Vec}; + +/// The [AlloyChainProvider] is a concrete implementation of the [ChainProvider] trait, providing +/// data over Ethereum JSON-RPC using an alloy provider as the backend. +#[derive(Debug, Clone)] +pub struct AlloyChainProvider { + /// The inner Ethereum JSON-RPC provider. + pub inner: RootProvider, + /// Whether to trust the RPC without verification. + pub trust_rpc: bool, + /// `header_by_hash` LRU cache. + header_by_hash_cache: LruCache, + /// `receipts_by_hash_cache` LRU cache. + receipts_by_hash_cache: LruCache>, + /// `block_info_and_transactions_by_hash` LRU cache. + block_info_and_transactions_by_hash_cache: LruCache)>, +} + +impl AlloyChainProvider { + /// Creates a new [AlloyChainProvider] with the given alloy provider. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new(inner: RootProvider, cache_size: usize) -> Self { + Self::new_with_trust(inner, cache_size, true) + } + + /// Creates a new [AlloyChainProvider] with the given alloy provider and trust setting. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new_with_trust(inner: RootProvider, cache_size: usize, trust_rpc: bool) -> Self { + Self { + inner, + trust_rpc, + header_by_hash_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), + receipts_by_hash_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), + block_info_and_transactions_by_hash_cache: LruCache::new( + NonZeroUsize::new(cache_size).unwrap(), + ), + } + } + + /// Creates a new [AlloyChainProvider] from the provided [reqwest::Url]. + pub fn new_http(url: reqwest::Url, cache_size: usize) -> Self { + let inner = RootProvider::new_http(url); + Self::new(inner, cache_size) + } + + /// Returns the latest L2 block number. + pub async fn latest_block_number(&mut self) -> Result> { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_CALLS, "method" => "block_number"); + + let result = self.inner.get_block_number().await; + + #[cfg(feature = "metrics")] + if result.is_err() { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_ERRORS, "method" => "block_number"); + } + + result + } + + /// Returns the chain ID. + pub async fn chain_id(&mut self) -> Result> { + self.inner.get_chain_id().await + } + + /// Verifies that a header's hash matches the expected hash when trust_rpc is false. + fn verify_header_hash( + &self, + header: &Header, + expected_hash: B256, + ) -> Result<(), AlloyChainProviderError> { + if self.trust_rpc { + return Ok(()); + } + + let actual_hash = header.hash_slow(); + if actual_hash != expected_hash { + return Err(AlloyChainProviderError::Transport(RpcError::Transport( + TransportErrorKind::Custom( + format!( + "Header hash mismatch: expected {expected_hash:?}, got {actual_hash:?}" + ) + .into(), + ), + ))); + } + + Ok(()) + } +} + +/// An error for the [AlloyChainProvider]. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum AlloyChainProviderError { + /// Transport error + #[error(transparent)] + Transport(#[from] RpcError), + /// Block not found. + #[error("Block not found: {0}")] + BlockNotFound(BlockId), + /// Failed to convert RPC receipts into consensus receipts. + #[error("Failed to convert RPC receipts into consensus receipts: {0}")] + ReceiptsConversion(B256), +} + +impl From for PipelineErrorKind { + fn from(e: AlloyChainProviderError) -> Self { + match e { + AlloyChainProviderError::Transport(e) => { + Self::Temporary(PipelineError::Provider(format!("Transport error: {e}"))) + } + AlloyChainProviderError::BlockNotFound(id) => { + Self::Temporary(PipelineError::Provider(format!("L1 Block not found: {id}"))) + } + AlloyChainProviderError::ReceiptsConversion(_) => { + Self::Temporary(PipelineError::Provider( + "Failed to convert RPC receipts into consensus receipts".to_string(), + )) + } + } + } +} + +#[async_trait] +impl ChainProvider for AlloyChainProvider { + type Error = AlloyChainProviderError; + + async fn header_by_hash(&mut self, hash: B256) -> Result { + if let Some(header) = self.header_by_hash_cache.get(&hash) { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_HITS, "cache" => "header_by_hash"); + return Ok(header.clone()); + } + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_MISSES, "cache" => "header_by_hash"); + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_CALLS, "method" => "header_by_hash"); + + let block = self + .inner + .get_block_by_hash(hash) + .await + .inspect_err(|_e| { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_ERRORS, "method" => "header_by_hash"); + })? + .ok_or(AlloyChainProviderError::BlockNotFound(hash.into()))?; + let header = block.header.into_consensus(); + + // Verify the header hash matches what we requested + self.verify_header_hash(&header, hash)?; + + self.header_by_hash_cache.put(hash, header.clone()); + + kona_macros::inc!(gauge, Metrics::CACHE_ENTRIES, "cache" => "header_by_hash"); + + Ok(header) + } + + async fn block_info_by_number(&mut self, number: u64) -> Result { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_CALLS, "method" => "block_by_number"); + + let block = self + .inner + .get_block_by_number(number.into()) + .await + .inspect_err(|_e| { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_ERRORS, "method" => "block_by_number"); + })? + .ok_or(AlloyChainProviderError::BlockNotFound(number.into()))?; + let header = block.header.into_consensus(); + + let block_info = BlockInfo { + hash: header.hash_slow(), + number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }; + Ok(block_info) + } + + async fn receipts_by_hash(&mut self, hash: B256) -> Result, Self::Error> { + if let Some(receipts) = self.receipts_by_hash_cache.get(&hash) { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_HITS, "cache" => "receipts_by_hash"); + return Ok(receipts.clone()); + } + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_MISSES, "cache" => "receipts_by_hash"); + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_CALLS, "method" => "receipts_by_hash"); + + let receipts = self + .inner + .get_block_receipts(hash.into()) + .await + .inspect_err(|_e| { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_ERRORS, "method" => "receipts_by_hash"); + })? + .ok_or(AlloyChainProviderError::BlockNotFound(hash.into()))?; + let consensus_receipts = receipts + .into_iter() + .map(|r| r.inner.into_primitives_receipt().as_receipt().cloned()) + .collect::>>() + .ok_or(AlloyChainProviderError::ReceiptsConversion(hash))?; + + self.receipts_by_hash_cache.put(hash, consensus_receipts.clone()); + + kona_macros::inc!(gauge, Metrics::CACHE_ENTRIES, "cache" => "receipts_by_hash"); + + Ok(consensus_receipts) + } + + async fn block_info_and_transactions_by_hash( + &mut self, + hash: B256, + ) -> Result<(BlockInfo, Vec), Self::Error> { + if let Some(block_info_and_txs) = self.block_info_and_transactions_by_hash_cache.get(&hash) + { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_HITS, "cache" => "block_info_and_tx"); + return Ok(block_info_and_txs.clone()); + } + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_CACHE_MISSES, "cache" => "block_info_and_tx"); + + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_CALLS, "method" => "block_by_hash"); + + let block = self + .inner + .get_block_by_hash(hash) + .full() + .await + .inspect_err(|_e| { + kona_macros::inc!(gauge, Metrics::CHAIN_PROVIDER_RPC_ERRORS, "method" => "block_by_hash"); + })? + .ok_or(AlloyChainProviderError::BlockNotFound(hash.into()))? + .into_consensus() + .map_transactions(|t| t.inner.into_inner()); + + // Verify the block hash matches what we requested + self.verify_header_hash(&block.header, hash)?; + + let block_info = BlockInfo { + hash, // Use the already verified hash instead of recomputing + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + }; + + self.block_info_and_transactions_by_hash_cache + .put(hash, (block_info, block.body.transactions.clone())); + + kona_macros::inc!(gauge, Metrics::CACHE_ENTRIES, "cache" => "block_info_and_tx"); + + Ok((block_info, block.body.transactions)) + } +} diff --git a/kona/crates/providers/providers-alloy/src/l2_chain_provider.rs b/kona/crates/providers/providers-alloy/src/l2_chain_provider.rs new file mode 100644 index 0000000000000..d12ef30852aad --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/l2_chain_provider.rs @@ -0,0 +1,270 @@ +//! Providers that use alloy provider types on the backend. + +#[cfg(feature = "metrics")] +use crate::Metrics; +use alloy_eips::BlockId; +use alloy_primitives::{B256, Bytes}; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_engine::JwtSecret; +use alloy_transport::{RpcError, TransportErrorKind}; +use alloy_transport_http::{ + AuthLayer, Http, HyperClient, + hyper_util::{client::legacy::Client, rt::TokioExecutor}, +}; +use async_trait::async_trait; +use http_body_util::Full; +use kona_derive::{L2ChainProvider, PipelineError, PipelineErrorKind}; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, L2BlockInfo, to_system_config}; +use lru::LruCache; +use op_alloy_consensus::OpBlock; +use op_alloy_network::Optimism; +use std::{num::NonZeroUsize, sync::Arc}; +use tower::ServiceBuilder; + +/// The [AlloyL2ChainProvider] is a concrete implementation of the [L2ChainProvider] trait, +/// providing data over Ethereum JSON-RPC using an alloy provider as the backend. +#[derive(Debug, Clone)] +pub struct AlloyL2ChainProvider { + /// The inner Ethereum JSON-RPC provider. + inner: RootProvider, + /// Whether to trust the RPC without verification. + trust_rpc: bool, + /// The rollup configuration. + rollup_config: Arc, + /// The `block_by_number` LRU cache. + block_by_number_cache: LruCache, +} + +impl AlloyL2ChainProvider { + /// Creates a new [AlloyL2ChainProvider] with the given alloy provider and [RollupConfig]. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new( + inner: RootProvider, + rollup_config: Arc, + cache_size: usize, + ) -> Self { + Self::new_with_trust(inner, rollup_config, cache_size, true) + } + + /// Creates a new [AlloyL2ChainProvider] with the given alloy provider, [RollupConfig], and + /// trust setting. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new_with_trust( + inner: RootProvider, + rollup_config: Arc, + cache_size: usize, + trust_rpc: bool, + ) -> Self { + Self { + inner, + trust_rpc, + rollup_config, + block_by_number_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), + } + } + + /// Returns the chain ID. + pub async fn chain_id(&mut self) -> Result> { + self.inner.get_chain_id().await + } + + /// Returns the latest L2 block number. + pub async fn latest_block_number(&mut self) -> Result> { + self.inner.get_block_number().await + } + + /// Verifies that a block's hash matches the expected hash when trust_rpc is false. + fn verify_block_hash( + &self, + block_hash: B256, + expected_hash: B256, + ) -> Result<(), RpcError> { + if self.trust_rpc { + return Ok(()); + } + + if block_hash != expected_hash { + return Err(RpcError::local_usage_str(&format!( + "Block hash mismatch: expected {expected_hash:?}, got {block_hash:?}" + ))); + } + + Ok(()) + } + + /// Returns the [L2BlockInfo] for the given [BlockId]. [None] is returned if the block + /// does not exist. + pub async fn block_info_by_id( + &mut self, + id: BlockId, + ) -> Result, RpcError> { + #[cfg(feature = "metrics")] + let method_name = match id { + BlockId::Number(_) => "l2_block_ref_by_number", + BlockId::Hash(_) => "l2_block_ref_by_hash", + }; + + kona_macros::inc!(gauge, Metrics::L2_CHAIN_PROVIDER_REQUESTS, "method" => method_name); + + let result = async { + let block = match id { + BlockId::Number(num) => self.inner.get_block_by_number(num).full().await?, + BlockId::Hash(hash) => { + let block = self.inner.get_block_by_hash(hash.block_hash).full().await?; + + // Verify block hash matches if we fetched by hash + if let Some(ref b) = block { + self.verify_block_hash(b.header.hash, hash.block_hash)?; + } + + block + } + }; + + match block { + Some(block) => { + let consensus_block = + block.into_consensus().map_transactions(|t| t.inner.inner); + + let l2_block = L2BlockInfo::from_block_and_genesis( + &consensus_block, + &self.rollup_config.genesis, + ) + .map_err(|_| { + RpcError::local_usage_str( + "failed to construct L2BlockInfo from block and genesis", + ) + })?; + Ok(Some(l2_block)) + } + None => Ok(None), + } + } + .await; + + #[cfg(feature = "metrics")] + if result.is_err() { + kona_macros::inc!(gauge, Metrics::L2_CHAIN_PROVIDER_ERRORS, "method" => method_name); + } + + result + } + + /// Creates a new [AlloyL2ChainProvider] from the provided [reqwest::Url]. + pub fn new_http( + url: reqwest::Url, + rollup_config: Arc, + cache_size: usize, + jwt: JwtSecret, + ) -> Self { + let hyper_client = Client::builder(TokioExecutor::new()).build_http::>(); + + let auth_layer = AuthLayer::new(jwt); + let service = ServiceBuilder::new().layer(auth_layer).service(hyper_client); + + let layer_transport = HyperClient::with_service(service); + let http_hyper = Http::with_client(layer_transport, url); + let rpc_client = RpcClient::new(http_hyper, false); + + let rpc = RootProvider::::new(rpc_client); + Self::new(rpc, rollup_config, cache_size) + } +} + +/// An error for the [AlloyL2ChainProvider]. +#[derive(Debug, thiserror::Error)] +pub enum AlloyL2ChainProviderError { + /// Transport error + #[error(transparent)] + Transport(#[from] RpcError), + /// Failed to find a block. + #[error("Failed to fetch block {0}")] + BlockNotFound(u64), + /// Failed to construct [L2BlockInfo] from the block and genesis. + #[error("Failed to construct L2BlockInfo from block {0} and genesis")] + L2BlockInfoConstruction(u64), + /// Failed to convert the block into a [SystemConfig]. + #[error("Failed to convert block {0} into SystemConfig")] + SystemConfigConversion(u64), +} + +impl From for PipelineErrorKind { + fn from(e: AlloyL2ChainProviderError) -> Self { + match e { + AlloyL2ChainProviderError::Transport(e) => { + Self::Temporary(PipelineError::Provider(format!("Transport error: {e}"))) + } + AlloyL2ChainProviderError::BlockNotFound(_) => { + Self::Temporary(PipelineError::Provider("Block not found".to_string())) + } + AlloyL2ChainProviderError::L2BlockInfoConstruction(_) => Self::Temporary( + PipelineError::Provider("L2 block info construction failed".to_string()), + ), + AlloyL2ChainProviderError::SystemConfigConversion(_) => Self::Temporary( + PipelineError::Provider("system config conversion failed".to_string()), + ), + } + } +} + +#[async_trait] +impl BatchValidationProvider for AlloyL2ChainProvider { + type Error = AlloyL2ChainProviderError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + let block = self + .block_by_number(number) + .await + .map_err(|_| AlloyL2ChainProviderError::BlockNotFound(number))?; + L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) + .map_err(|_| AlloyL2ChainProviderError::L2BlockInfoConstruction(number)) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + if let Some(block) = self.block_by_number_cache.get(&number) { + return Ok(block.clone()); + } + + kona_macros::inc!(gauge, Metrics::L2_CHAIN_PROVIDER_REQUESTS, "method" => "l2_block_ref_by_number"); + + let block = self + .inner + .get_block_by_number(number.into()) + .full() + .await + .map_err(|e| { + kona_macros::inc!(gauge, Metrics::L2_CHAIN_PROVIDER_ERRORS, "method" => "l2_block_ref_by_number"); + AlloyL2ChainProviderError::Transport(e) + })? + .ok_or(AlloyL2ChainProviderError::BlockNotFound(number))? + .into_consensus() + .map_transactions(|t| t.inner.inner.into_inner()); + + self.block_by_number_cache.put(number, block.clone()); + Ok(block) + } +} + +#[async_trait] +impl L2ChainProvider for AlloyL2ChainProvider { + type Error = AlloyL2ChainProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error> { + let block = self + .block_by_number(number) + .await + .map_err(|_| AlloyL2ChainProviderError::BlockNotFound(number))?; + to_system_config(&block, &rollup_config) + .map_err(|_| AlloyL2ChainProviderError::SystemConfigConversion(number)) + } +} diff --git a/kona/crates/providers/providers-alloy/src/lib.rs b/kona/crates/providers/providers-alloy/src/lib.rs new file mode 100644 index 0000000000000..16bab6af91e5b --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/lib.rs @@ -0,0 +1,28 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod metrics; +pub use metrics::Metrics; + +mod beacon_client; +pub use beacon_client::{ + APIConfigResponse, APIGenesisResponse, BeaconClient, OnlineBeaconClient, ReducedConfigData, + ReducedGenesisData, +}; + +mod blobs; +pub use blobs::{BoxedBlobWithIndex, OnlineBlobProvider}; + +mod chain_provider; +pub use chain_provider::{AlloyChainProvider, AlloyChainProviderError}; + +mod l2_chain_provider; +pub use l2_chain_provider::{AlloyL2ChainProvider, AlloyL2ChainProviderError}; + +mod pipeline; +pub use pipeline::OnlinePipeline; diff --git a/kona/crates/providers/providers-alloy/src/metrics.rs b/kona/crates/providers/providers-alloy/src/metrics.rs new file mode 100644 index 0000000000000..17c8ac6752dbe --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/metrics.rs @@ -0,0 +1,210 @@ +//! Metrics for the Alloy providers. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the gauge that tracks chain provider cache hits. + pub const CHAIN_PROVIDER_CACHE_HITS: &str = "kona_providers_chain_cache_hits"; + + /// Identifier for the gauge that tracks chain provider cache misses. + pub const CHAIN_PROVIDER_CACHE_MISSES: &str = "kona_providers_chain_cache_misses"; + + /// Identifier for the gauge that tracks chain provider RPC calls. + pub const CHAIN_PROVIDER_RPC_CALLS: &str = "kona_providers_chain_rpc_calls"; + + /// Identifier for the gauge that tracks chain provider RPC errors. + pub const CHAIN_PROVIDER_RPC_ERRORS: &str = "kona_providers_chain_rpc_errors"; + + /// Identifier for the gauge that tracks beacon client requests. + pub const BEACON_CLIENT_REQUESTS: &str = "kona_providers_beacon_requests"; + + /// Identifier for the gauge that tracks beacon client errors. + pub const BEACON_CLIENT_ERRORS: &str = "kona_providers_beacon_errors"; + + /// Identifier for the gauge that tracks L2 chain provider requests. + pub const L2_CHAIN_PROVIDER_REQUESTS: &str = "kona_providers_l2_chain_requests"; + + /// Identifier for the gauge that tracks L2 chain provider errors. + pub const L2_CHAIN_PROVIDER_ERRORS: &str = "kona_providers_l2_chain_errors"; + + /// Identifier for the gauge that tracks blob sidecar fetches. + pub const BLOB_SIDECAR_FETCHES: &str = "kona_providers_blob_sidecar_fetches"; + + /// Identifier for the gauge that tracks blob sidecar fetch errors. + pub const BLOB_SIDECAR_FETCH_ERRORS: &str = "kona_providers_blob_sidecar_errors"; + + /// Identifier for the histogram that tracks provider request duration. + pub const PROVIDER_REQUEST_DURATION: &str = "kona_providers_request_duration"; + + /// Identifier for the gauge that tracks active cache entries. + pub const CACHE_ENTRIES: &str = "kona_providers_cache_entries"; + + /// Identifier for the gauge that tracks cache memory usage. + pub const CACHE_MEMORY_USAGE: &str = "kona_providers_cache_memory_bytes"; + + /// Initializes metrics for the Alloy providers. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in [`kona_providers_alloy`][crate]. + #[cfg(feature = "metrics")] + pub fn describe() { + metrics::describe_gauge!( + Self::CHAIN_PROVIDER_CACHE_HITS, + "Number of cache hits in chain provider" + ); + metrics::describe_gauge!( + Self::CHAIN_PROVIDER_CACHE_MISSES, + "Number of cache misses in chain provider" + ); + metrics::describe_gauge!( + Self::CHAIN_PROVIDER_RPC_CALLS, + "Number of RPC calls made by chain provider" + ); + metrics::describe_gauge!( + Self::CHAIN_PROVIDER_RPC_ERRORS, + "Number of RPC errors in chain provider" + ); + metrics::describe_gauge!( + Self::BEACON_CLIENT_REQUESTS, + "Number of requests made to beacon client" + ); + metrics::describe_gauge!( + Self::BEACON_CLIENT_ERRORS, + "Number of errors in beacon client requests" + ); + metrics::describe_gauge!( + Self::L2_CHAIN_PROVIDER_REQUESTS, + "Number of requests made to L2 chain provider" + ); + metrics::describe_gauge!( + Self::L2_CHAIN_PROVIDER_ERRORS, + "Number of errors in L2 chain provider requests" + ); + metrics::describe_gauge!(Self::BLOB_SIDECAR_FETCHES, "Number of blob sidecar fetches"); + metrics::describe_gauge!( + Self::BLOB_SIDECAR_FETCH_ERRORS, + "Number of blob sidecar fetch errors" + ); + metrics::describe_histogram!( + Self::PROVIDER_REQUEST_DURATION, + "Duration of provider requests in seconds" + ); + metrics::describe_gauge!( + Self::CACHE_ENTRIES, + "Number of active entries in provider caches" + ); + metrics::describe_gauge!( + Self::CACHE_MEMORY_USAGE, + "Memory usage of provider caches in bytes" + ); + } + + /// Initializes metrics to `0` so they can be queried immediately by consumers of prometheus + /// metrics. + #[cfg(feature = "metrics")] + pub fn zero() { + // Chain provider cache metrics + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_CACHE_HITS, "cache", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_CACHE_HITS, "cache", "receipts_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_CACHE_HITS, "cache", "block_info_and_tx", 0); + + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_CACHE_MISSES, "cache", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_CACHE_MISSES, "cache", "receipts_by_hash", 0); + kona_macros::set!( + gauge, + Self::CHAIN_PROVIDER_CACHE_MISSES, + "cache", + "block_info_and_tx", + 0 + ); + + // RPC call metrics + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_CALLS, "method", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_CALLS, "method", "receipts_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_CALLS, "method", "block_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_CALLS, "method", "block_number", 0); + + // RPC error metrics + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_ERRORS, "method", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_ERRORS, "method", "receipts_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_ERRORS, "method", "block_by_hash", 0); + kona_macros::set!(gauge, Self::CHAIN_PROVIDER_RPC_ERRORS, "method", "block_number", 0); + + // Beacon client metrics + kona_macros::set!(gauge, Self::BEACON_CLIENT_REQUESTS, "method", "spec", 0); + kona_macros::set!(gauge, Self::BEACON_CLIENT_REQUESTS, "method", "genesis", 0); + kona_macros::set!(gauge, Self::BEACON_CLIENT_REQUESTS, "method", "blob_sidecars", 0); + + kona_macros::set!(gauge, Self::BEACON_CLIENT_ERRORS, "method", "spec", 0); + kona_macros::set!(gauge, Self::BEACON_CLIENT_ERRORS, "method", "genesis", 0); + kona_macros::set!(gauge, Self::BEACON_CLIENT_ERRORS, "method", "blob_sidecars", 0); + + // L2 chain provider metrics + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_REQUESTS, + "method", + "l2_block_ref_by_label", + 0 + ); + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_REQUESTS, + "method", + "l2_block_ref_by_hash", + 0 + ); + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_REQUESTS, + "method", + "l2_block_ref_by_number", + 0 + ); + + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_ERRORS, + "method", + "l2_block_ref_by_label", + 0 + ); + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_ERRORS, + "method", + "l2_block_ref_by_hash", + 0 + ); + kona_macros::set!( + gauge, + Self::L2_CHAIN_PROVIDER_ERRORS, + "method", + "l2_block_ref_by_number", + 0 + ); + + // Blob sidecar metrics + kona_macros::set!(gauge, Self::BLOB_SIDECAR_FETCHES, 0); + kona_macros::set!(gauge, Self::BLOB_SIDECAR_FETCH_ERRORS, 0); + + // Cache metrics + kona_macros::set!(gauge, Self::CACHE_ENTRIES, "cache", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CACHE_ENTRIES, "cache", "receipts_by_hash", 0); + kona_macros::set!(gauge, Self::CACHE_ENTRIES, "cache", "block_info_and_tx", 0); + + kona_macros::set!(gauge, Self::CACHE_MEMORY_USAGE, "cache", "header_by_hash", 0); + kona_macros::set!(gauge, Self::CACHE_MEMORY_USAGE, "cache", "receipts_by_hash", 0); + kona_macros::set!(gauge, Self::CACHE_MEMORY_USAGE, "cache", "block_info_and_tx", 0); + } +} diff --git a/kona/crates/providers/providers-alloy/src/pipeline.rs b/kona/crates/providers/providers-alloy/src/pipeline.rs new file mode 100644 index 0000000000000..818e153d9d781 --- /dev/null +++ b/kona/crates/providers/providers-alloy/src/pipeline.rs @@ -0,0 +1,230 @@ +//! Contains an online derivation pipeline. + +use crate::{AlloyChainProvider, AlloyL2ChainProvider, OnlineBeaconClient, OnlineBlobProvider}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_derive::{ + DerivationPipeline, EthereumDataSource, IndexedAttributesQueueStage, L2ChainProvider, + OriginProvider, Pipeline, PipelineBuilder, PipelineErrorKind, PipelineResult, + PolledAttributesQueueStage, ResetSignal, Signal, SignalReceiver, StatefulAttributesBuilder, + StepResult, +}; +use kona_genesis::{L1ChainConfig, RollupConfig, SystemConfig}; +use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent}; +use std::sync::Arc; + +/// An online polled derivation pipeline. +type OnlinePolledDerivationPipeline = DerivationPipeline< + PolledAttributesQueueStage< + OnlineDataProvider, + AlloyChainProvider, + AlloyL2ChainProvider, + OnlineAttributesBuilder, + >, + AlloyL2ChainProvider, +>; + +/// An online managed derivation pipeline. +type OnlineManagedDerivationPipeline = DerivationPipeline< + IndexedAttributesQueueStage< + OnlineDataProvider, + AlloyChainProvider, + AlloyL2ChainProvider, + OnlineAttributesBuilder, + >, + AlloyL2ChainProvider, +>; + +/// An RPC-backed Ethereum data source. +type OnlineDataProvider = + EthereumDataSource>; + +/// An RPC-backed payload attributes builder for the `AttributesQueue` stage of the derivation +/// pipeline. +type OnlineAttributesBuilder = StatefulAttributesBuilder; + +/// An online derivation pipeline. +#[derive(Debug)] +pub enum OnlinePipeline { + /// An online derivation pipeline that uses a polled traversal stage. + Polled(OnlinePolledDerivationPipeline), + /// An online derivation pipeline that uses a managed traversal stage. + Managed(OnlineManagedDerivationPipeline), +} + +impl OnlinePipeline { + /// Constructs a new polled derivation pipeline that is initialized. + pub async fn new( + cfg: Arc, + l1_cfg: Arc, + l2_safe_head: L2BlockInfo, + l1_origin: BlockInfo, + blob_provider: OnlineBlobProvider, + chain_provider: AlloyChainProvider, + mut l2_chain_provider: AlloyL2ChainProvider, + ) -> PipelineResult { + let mut pipeline = Self::new_polled( + cfg.clone(), + l1_cfg.clone(), + blob_provider, + chain_provider, + l2_chain_provider.clone(), + ); + + // Reset the pipeline to populate the initial L1/L2 cursor and system configuration in L1 + // Traversal. + pipeline + .signal( + ResetSignal { + l2_safe_head, + l1_origin, + system_config: l2_chain_provider + .system_config_by_number(l2_safe_head.block_info.number, cfg.clone()) + .await + .ok(), + } + .signal(), + ) + .await?; + + Ok(pipeline) + } + + /// Constructs a new polled derivation pipeline that is uninitialized. + /// + /// Uses online providers as specified by the arguments. + /// + /// Before using the returned pipeline, a [`ResetSignal`] must be sent to + /// instantiate the pipeline state. [`Self::new`] is a convenience method that + /// constructs a new online pipeline and sends the reset signal. + pub fn new_polled( + cfg: Arc, + l1_cfg: Arc, + blob_provider: OnlineBlobProvider, + chain_provider: AlloyChainProvider, + l2_chain_provider: AlloyL2ChainProvider, + ) -> Self { + let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l1_cfg, + l2_chain_provider.clone(), + chain_provider.clone(), + ); + let dap = EthereumDataSource::new_from_parts(chain_provider.clone(), blob_provider, &cfg); + + let pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(dap) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(attributes) + .origin(BlockInfo::default()) + .build_polled(); + + Self::Polled(pipeline) + } + + /// Constructs a new indexed derivation pipeline that is uninitialized. + /// + /// Uses online providers as specified by the arguments. + /// + /// Before using the returned pipeline, a [`ResetSignal`] must be sent to + /// instantiate the pipeline state. [`Self::new`] is a convenience method that + /// constructs a new online pipeline and sends the reset signal. + pub fn new_indexed( + cfg: Arc, + l1_cfg: Arc, + blob_provider: OnlineBlobProvider, + chain_provider: AlloyChainProvider, + l2_chain_provider: AlloyL2ChainProvider, + ) -> Self { + let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l1_cfg, + l2_chain_provider.clone(), + chain_provider.clone(), + ); + let dap = EthereumDataSource::new_from_parts(chain_provider.clone(), blob_provider, &cfg); + + let pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(dap) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(attributes) + .origin(BlockInfo::default()) + .build_indexed(); + + Self::Managed(pipeline) + } +} + +#[async_trait] +impl SignalReceiver for OnlinePipeline { + /// Receives a signal from the driver. + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match self { + Self::Polled(pipeline) => pipeline.signal(signal).await, + Self::Managed(pipeline) => pipeline.signal(signal).await, + } + } +} + +impl OriginProvider for OnlinePipeline { + /// Returns the optional L1 [BlockInfo] origin. + fn origin(&self) -> Option { + match self { + Self::Polled(pipeline) => pipeline.origin(), + Self::Managed(pipeline) => pipeline.origin(), + } + } +} + +impl Iterator for OnlinePipeline { + type Item = OpAttributesWithParent; + + fn next(&mut self) -> Option { + match self { + Self::Polled(pipeline) => pipeline.next(), + Self::Managed(pipeline) => pipeline.next(), + } + } +} + +#[async_trait] +impl Pipeline for OnlinePipeline { + /// Peeks at the next [OpAttributesWithParent] from the pipeline. + fn peek(&self) -> Option<&OpAttributesWithParent> { + match self { + Self::Polled(pipeline) => pipeline.peek(), + Self::Managed(pipeline) => pipeline.peek(), + } + } + + /// Attempts to progress the pipeline. + async fn step(&mut self, cursor: L2BlockInfo) -> StepResult { + match self { + Self::Polled(pipeline) => pipeline.step(cursor).await, + Self::Managed(pipeline) => pipeline.step(cursor).await, + } + } + + /// Returns the rollup config. + fn rollup_config(&self) -> &RollupConfig { + match self { + Self::Polled(pipeline) => pipeline.rollup_config(), + Self::Managed(pipeline) => pipeline.rollup_config(), + } + } + + /// Returns the [SystemConfig] by L2 number. + async fn system_config_by_number( + &mut self, + number: u64, + ) -> Result { + match self { + Self::Polled(pipeline) => pipeline.system_config_by_number(number).await, + Self::Managed(pipeline) => pipeline.system_config_by_number(number).await, + } + } +} diff --git a/kona/crates/providers/providers-local/Cargo.toml b/kona/crates/providers/providers-local/Cargo.toml new file mode 100644 index 0000000000000..20cbfa5fc20c2 --- /dev/null +++ b/kona/crates/providers/providers-local/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "kona-providers-local" +version = "0.1.0" +description = "Local buffered provider for caching and reorg handling" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +# Kona +kona-derive.workspace = true +kona-genesis.workspace = true +kona-protocol.workspace = true +kona-macros.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-primitives.workspace = true + +# Op Alloy +op-alloy-consensus.workspace = true + +# Standard library +async-trait.workspace = true +lru.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["sync"] } + +# `metrics` feature +metrics = { workspace = true, optional = true } + +[dev-dependencies] +tokio.workspace = true +rstest.workspace = true + +[features] +default = [] +metrics = [ "dep:metrics", "kona-derive/metrics" ] diff --git a/kona/crates/providers/providers-local/README.md b/kona/crates/providers/providers-local/README.md new file mode 100644 index 0000000000000..a64dc94d7962b --- /dev/null +++ b/kona/crates/providers/providers-local/README.md @@ -0,0 +1,83 @@ +# `kona-providers-local` + +CI +kona-provides-local +License +Codecov + + +This crate provides a pure in-memory L2 provider implementation for the Kona OP Stack. It operates without any external RPC dependencies, serving all data from its internal cache. + +## Features + +- **BufferedL2Provider**: A pure in-memory L2 provider that serves data from cached blocks +- **ChainStateBuffer**: LRU cache for managing chain state with reorganization support +- **Chain Event Handling**: Support for processing execution extension notifications for chain events (commits, reorgs, reverts) +- **No External Dependencies**: Operates entirely from in-memory state without RPC calls + +## Architecture + +The buffered provider operates as a standalone in-memory data store: + +1. **In-Memory Storage**: Complete blocks with L2 block info are stored in memory +2. **Dual Indexing**: Blocks are indexed by both hash and number for efficient queries +3. **Reorg Handling**: Intelligent cache invalidation during chain reorganizations up to a configurable depth +4. **Event Processing**: Integration with execution extension notifications to maintain cache consistency +5. **Genesis Support**: Special handling for genesis blocks from the rollup configuration + +## Usage + +```rust,ignore +use kona_providers_local::{BufferedL2Provider, ChainStateEvent}; +use kona_genesis::RollupConfig; +use kona_protocol::{BatchValidationProvider, L2BlockInfo}; +use op_alloy_consensus::OpBlock; +use std::sync::Arc; + +async fn example() -> Result<(), Box> { + // Create a buffered provider with rollup configuration + let rollup_config = Arc::new(RollupConfig::default()); + let provider = BufferedL2Provider::new(rollup_config, 1000, 64); + + // Add blocks to the provider + // In practice, these would come from execution extension or other sources + let block: OpBlock = unimplemented!(); + let l2_info: L2BlockInfo = unimplemented!(); + provider.add_block(block, l2_info).await?; + + // Handle chain events from execution extension notifications + let event = ChainStateEvent::ChainCommitted { + new_head: alloy_primitives::B256::ZERO, + committed: vec![], + }; + provider.handle_chain_event(event).await?; + + // Query blocks from the cache + let mut provider_clone = provider.clone(); + let block = provider_clone.block_by_number(1).await?; + let l2_info = provider_clone.l2_block_info_by_number(1).await?; + + Ok(()) +} +``` + +## Configuration + +- `cache_size`: Number of blocks to cache (affects memory usage) +- `max_reorg_depth`: Maximum reorganization depth to handle before clearing cache + +## Provider Traits + +The `BufferedL2Provider` implements the following traits from `kona-derive`: + +- `ChainProvider`: Basic block and receipt access +- `L2ChainProvider`: L2-specific functionality including system config access +- `BatchValidationProvider`: Batch validation support + +## Error Handling + +The provider returns specific errors for different failure scenarios: +- `BlockNotFound`: When a requested block is not in the cache +- `L2BlockInfoConstruction`: When L2 block info cannot be constructed +- `SystemConfigConversion`: When a block cannot be converted to system config +- `Buffer` errors: For cache-related issues including deep reorgs diff --git a/kona/crates/providers/providers-local/src/buffer.rs b/kona/crates/providers/providers-local/src/buffer.rs new file mode 100644 index 0000000000000..73e49e247a9c0 --- /dev/null +++ b/kona/crates/providers/providers-local/src/buffer.rs @@ -0,0 +1,478 @@ +//! Chain state buffer implementation for handling L2 chain events and reorgs. +//! +//! This module provides the core caching infrastructure for the buffered provider. +//! It manages an LRU cache of blocks indexed by both hash and number, handles +//! chain reorganizations, and maintains the canonical chain state. + +use alloy_primitives::B256; +use kona_protocol::L2BlockInfo; +use lru::LruCache; +use op_alloy_consensus::OpBlock; +use std::num::NonZeroUsize; +use tokio::sync::RwLock; + +/// Events that can affect chain state +#[derive(Debug, Clone)] +pub enum ChainStateEvent { + /// New blocks have been committed to the canonical chain + ChainCommitted { + /// The new chain head + new_head: B256, + /// The blocks that were committed + committed: Vec, + }, + /// Chain reorganization occurred + ChainReorged { + /// The old chain head before reorg + old_head: B256, + /// The new chain head after reorg + new_head: B256, + /// The depth of the reorg (how many blocks were reverted) + depth: u64, + }, + /// Chain was reverted to a previous state + ChainReverted { + /// The old chain head before revert + old_head: B256, + /// The new chain head after revert + new_head: B256, + /// The blocks that were reverted + reverted: Vec, + }, +} + +/// Cached block data containing full block information. +/// +/// This structure stores a complete OP block along with its derived L2 block info. +/// This allows the buffered provider to serve all queries without needing to +/// recompute or fetch data from external sources. +#[derive(Debug, Clone)] +pub struct CachedBlock { + /// Full OP block data including header and body + pub block: OpBlock, + /// L2 block info derived from the block + pub l2_block_info: L2BlockInfo, + /// Whether this block is part of the canonical chain + pub canonical: bool, +} + +impl CachedBlock { + /// Create a new cached block + pub const fn new(block: OpBlock, l2_block_info: L2BlockInfo) -> Self { + Self { block, l2_block_info, canonical: true } + } + + /// Mark this block as non-canonical + pub const fn mark_non_canonical(mut self) -> Self { + self.canonical = false; + self + } + + /// Get the block hash + pub const fn hash(&self) -> B256 { + self.l2_block_info.block_info.hash + } + + /// Get the block number + pub const fn number(&self) -> u64 { + self.l2_block_info.block_info.number + } +} + +/// Buffer for managing chain state with LRU caching and reorg handling. +/// +/// This buffer maintains two indexes for efficient block lookups: +/// - By hash: Direct access to blocks by their hash +/// - By number: Maps block numbers to hashes for numbered queries +/// +/// The buffer also tracks the canonical chain head and handles reorganizations +/// up to a configurable depth. Deep reorgs beyond the configured limit will +/// trigger a cache clear to maintain consistency. +#[derive(Debug)] +pub struct ChainStateBuffer { + /// LRU cache for blocks by hash + blocks_by_hash: RwLock>, + /// LRU cache for blocks by number + blocks_by_number: RwLock>, + /// Current canonical chain head + canonical_head: RwLock>, + /// Maximum reorg depth to support + max_reorg_depth: u64, + /// Cache capacity + capacity: usize, +} + +impl ChainStateBuffer { + /// Create a new chain state buffer. + /// + /// # Arguments + /// * `capacity` - Maximum number of blocks to cache (affects memory usage) + /// * `max_reorg_depth` - Maximum reorg depth to handle before clearing cache + pub fn new(capacity: usize, max_reorg_depth: u64) -> Self { + Self { + blocks_by_hash: RwLock::new(LruCache::new(NonZeroUsize::new(capacity).unwrap())), + blocks_by_number: RwLock::new(LruCache::new(NonZeroUsize::new(capacity).unwrap())), + canonical_head: RwLock::new(None), + max_reorg_depth, + capacity, + } + } + + /// Get block by hash from cache + pub async fn get_block_by_hash(&self, hash: B256) -> Option { + let cache = self.blocks_by_hash.read().await; + cache.peek(&hash).cloned() + } + + /// Get block by number from cache + pub async fn get_block_by_number(&self, number: u64) -> Option { + let blocks_by_number = self.blocks_by_number.read().await; + if let Some(hash) = blocks_by_number.peek(&number) { + let hash = *hash; + drop(blocks_by_number); + self.get_block_by_hash(hash).await + } else { + None + } + } + + /// Insert a block into the cache + pub async fn insert_block(&self, block: CachedBlock) { + let hash = block.hash(); + let number = block.number(); + + let mut blocks_by_hash = self.blocks_by_hash.write().await; + let mut blocks_by_number = self.blocks_by_number.write().await; + + blocks_by_hash.put(hash, block); + blocks_by_number.put(number, hash); + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + kona_macros::set!( + gauge, + Metrics::CACHE_ENTRIES, + "cache", + "blocks_by_hash", + blocks_by_hash.len() as f64 + ); + kona_macros::set!( + gauge, + Metrics::CACHE_ENTRIES, + "cache", + "blocks_by_number", + blocks_by_number.len() as f64 + ); + } + } + + /// Handle a chain state event + pub async fn handle_event(&self, event: ChainStateEvent) -> Result<(), ChainBufferError> { + match event { + ChainStateEvent::ChainCommitted { new_head, committed } => { + self.handle_chain_committed(new_head, committed).await + } + ChainStateEvent::ChainReorged { old_head, new_head, depth } => { + self.handle_chain_reorged(old_head, new_head, depth).await + } + ChainStateEvent::ChainReverted { old_head, new_head, reverted } => { + self.handle_chain_reverted(old_head, new_head, reverted).await + } + } + } + + /// Handle chain committed event + async fn handle_chain_committed( + &self, + new_head: B256, + committed: Vec, + ) -> Result<(), ChainBufferError> { + // Update canonical head + let mut canonical_head = self.canonical_head.write().await; + *canonical_head = Some(new_head); + + // Mark all committed blocks as canonical + let mut blocks_by_hash = self.blocks_by_hash.write().await; + for hash in committed { + if let Some(block) = blocks_by_hash.get_mut(&hash) { + block.canonical = true; + } + } + + Ok(()) + } + + /// Handle chain reorged event + async fn handle_chain_reorged( + &self, + _old_head: B256, + new_head: B256, + depth: u64, + ) -> Result<(), ChainBufferError> { + if depth > self.max_reorg_depth { + return Err(ChainBufferError::ReorgTooDeep { depth, max_depth: self.max_reorg_depth }); + } + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + kona_macros::set!(gauge, Metrics::REORG_DEPTH, depth as f64); + } + + // Update canonical head + let mut canonical_head = self.canonical_head.write().await; + *canonical_head = Some(new_head); + + // We need to invalidate cached blocks that are no longer canonical + // For now, we'll clear the entire cache on deep reorgs + if depth > 10 { + let mut blocks_by_hash = self.blocks_by_hash.write().await; + let mut blocks_by_number = self.blocks_by_number.write().await; + blocks_by_hash.clear(); + blocks_by_number.clear(); + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + kona_macros::inc!(gauge, Metrics::CACHE_CLEARS); + } + } + + Ok(()) + } + + /// Handle chain reverted event + async fn handle_chain_reverted( + &self, + _old_head: B256, + new_head: B256, + reverted: Vec, + ) -> Result<(), ChainBufferError> { + // Update canonical head + let mut canonical_head = self.canonical_head.write().await; + *canonical_head = Some(new_head); + + // Mark reverted blocks as non-canonical and remove from cache + let mut blocks_by_hash = self.blocks_by_hash.write().await; + for hash in reverted { + blocks_by_hash.pop(&hash); + } + + Ok(()) + } + + /// Get the current canonical head + pub async fn canonical_head(&self) -> Option { + let canonical_head = self.canonical_head.read().await; + *canonical_head + } + + /// Get cache statistics + pub async fn cache_stats(&self) -> CacheStats { + let blocks_by_hash = self.blocks_by_hash.read().await; + let blocks_by_number = self.blocks_by_number.read().await; + + CacheStats { + blocks_by_hash_len: blocks_by_hash.len(), + blocks_by_number_len: blocks_by_number.len(), + capacity: self.capacity, + max_reorg_depth: self.max_reorg_depth, + } + } + + /// Clear the entire cache + pub async fn clear(&self) { + let mut blocks_by_hash = self.blocks_by_hash.write().await; + let mut blocks_by_number = self.blocks_by_number.write().await; + let mut canonical_head = self.canonical_head.write().await; + + blocks_by_hash.clear(); + blocks_by_number.clear(); + *canonical_head = None; + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + kona_macros::inc!(gauge, Metrics::CACHE_CLEARS); + kona_macros::set!(gauge, Metrics::CACHE_ENTRIES, "cache", "blocks_by_hash", 0); + kona_macros::set!(gauge, Metrics::CACHE_ENTRIES, "cache", "blocks_by_number", 0); + } + } +} + +/// Cache statistics +#[derive(Debug, Clone)] +pub struct CacheStats { + /// Number of blocks cached by hash + pub blocks_by_hash_len: usize, + /// Number of blocks cached by number + pub blocks_by_number_len: usize, + /// Total cache capacity + pub capacity: usize, + /// Maximum reorg depth supported + pub max_reorg_depth: u64, +} + +/// Errors that can occur in the chain buffer +#[derive(Debug, thiserror::Error)] +pub enum ChainBufferError { + /// Reorg is too deep to handle + #[error("Reorg depth {depth} exceeds maximum supported depth {max_depth}")] + ReorgTooDeep { + /// The depth of the reorg attempted + depth: u64, + /// The maximum supported reorg depth + max_depth: u64, + }, + /// Block not found in cache + #[error("Block not found in cache: {hash}")] + BlockNotFound { + /// The hash of the block that was not found + hash: B256, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Header; + use alloy_eips::BlockNumHash; + use alloy_primitives::{FixedBytes, U256}; + use kona_protocol::BlockInfo; + use op_alloy_consensus::OpTxEnvelope; + + fn create_test_block(number: u64, _hash: B256, parent_hash: B256) -> (OpBlock, L2BlockInfo) { + let header = Header { + number, + parent_hash, + timestamp: 1234567890, + gas_limit: 8000000, + gas_used: 5000000, + base_fee_per_gas: Some(20_000_000_000u64), + difficulty: U256::ZERO, + nonce: FixedBytes::ZERO, + ..Default::default() + }; + + let block = OpBlock { + header: header.clone(), + body: alloy_consensus::BlockBody { + transactions: Vec::::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + + let l2_block_info = L2BlockInfo { + block_info: BlockInfo { + hash: header.hash_slow(), + number: header.number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }, + l1_origin: BlockNumHash { number: 1, hash: B256::ZERO }, + seq_num: 0, + }; + + (block, l2_block_info) + } + + #[tokio::test] + async fn test_buffer_basic_operations() { + let buffer = ChainStateBuffer::new(100, 10); + + let (block, l2_info) = create_test_block(1, B256::ZERO, B256::ZERO); + let cached_block = CachedBlock::new(block, l2_info); + let computed_hash = cached_block.hash(); + + // Insert block + buffer.insert_block(cached_block.clone()).await; + + // Retrieve by hash + let retrieved = buffer.get_block_by_hash(computed_hash).await; + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().hash(), computed_hash); + + // Retrieve by number + let retrieved = buffer.get_block_by_number(1).await; + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().number(), 1); + } + + #[tokio::test] + async fn test_chain_committed_event() { + let buffer = ChainStateBuffer::new(100, 10); + + let hash1 = B256::with_last_byte(1); + let hash2 = B256::with_last_byte(2); + let (block1, l2_info1) = create_test_block(1, hash1, B256::ZERO); + let (block2, l2_info2) = create_test_block(2, hash2, hash1); + + let cached_block1 = CachedBlock::new(block1, l2_info1); + let cached_block2 = CachedBlock::new(block2, l2_info2); + + buffer.insert_block(cached_block1).await; + buffer.insert_block(cached_block2).await; + + // Handle committed event + let event = + ChainStateEvent::ChainCommitted { new_head: hash2, committed: vec![hash1, hash2] }; + + buffer.handle_event(event).await.unwrap(); + + // Check canonical head is updated + assert_eq!(buffer.canonical_head().await, Some(hash2)); + } + + #[tokio::test] + async fn test_reorg_too_deep() { + let buffer = ChainStateBuffer::new(100, 5); + + let hash1 = B256::with_last_byte(1); + let hash2 = B256::with_last_byte(2); + + let event = ChainStateEvent::ChainReorged { + old_head: hash1, + new_head: hash2, + depth: 10, // Exceeds max depth of 5 + }; + + let result = buffer.handle_event(event).await; + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ChainBufferError::ReorgTooDeep { .. })); + } + + #[tokio::test] + async fn test_cache_stats() { + let buffer = ChainStateBuffer::new(100, 10); + + let hash1 = B256::with_last_byte(1); + let (block, l2_info) = create_test_block(1, hash1, B256::ZERO); + let cached_block = CachedBlock::new(block, l2_info); + + buffer.insert_block(cached_block).await; + + let stats = buffer.cache_stats().await; + assert_eq!(stats.blocks_by_hash_len, 1); + assert_eq!(stats.blocks_by_number_len, 1); + assert_eq!(stats.capacity, 100); + assert_eq!(stats.max_reorg_depth, 10); + } + + #[tokio::test] + async fn test_clear_cache() { + let buffer = ChainStateBuffer::new(100, 10); + + let (block, l2_info) = create_test_block(1, B256::ZERO, B256::ZERO); + let cached_block = CachedBlock::new(block, l2_info); + let computed_hash = cached_block.hash(); + + buffer.insert_block(cached_block).await; + assert!(buffer.get_block_by_hash(computed_hash).await.is_some()); + + buffer.clear().await; + assert!(buffer.get_block_by_hash(computed_hash).await.is_none()); + assert_eq!(buffer.canonical_head().await, None); + } +} diff --git a/kona/crates/providers/providers-local/src/buffered.rs b/kona/crates/providers/providers-local/src/buffered.rs new file mode 100644 index 0000000000000..6c1bd84a97880 --- /dev/null +++ b/kona/crates/providers/providers-local/src/buffered.rs @@ -0,0 +1,420 @@ +//! Buffered L2 Provider implementation that serves data from in-memory chain state. +//! +//! This provider operates as a pure in-memory cache without any dependency on external RPC +//! providers. It stores complete blocks with their L2 block information and serves all queries +//! directly from this cached state. Chain updates are provided through the `add_block` and +//! `handle_chain_event` methods. + +use alloy_primitives::B256; +use async_trait::async_trait; +use kona_derive::{L2ChainProvider, PipelineError, PipelineErrorKind}; +use kona_genesis::{ChainGenesis, RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, L2BlockInfo, to_system_config}; +use op_alloy_consensus::OpBlock; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::{CachedBlock, ChainBufferError, ChainStateBuffer, ChainStateEvent}; + +/// A buffered L2 provider that serves data from in-memory chain state. +/// +/// This provider maintains an in-memory cache of L2 blocks and serves all queries +/// from this cache. It does not depend on any external RPC provider. Instead, blocks +/// must be explicitly added to the cache using the `add_block` method, typically +/// when processing chain events from execution extensions or other sources. +/// +/// # Features +/// - Pure in-memory operation without external dependencies +/// - LRU caching with configurable size +/// - Reorg handling up to a configurable depth +/// - Efficient block and L2 block info queries +/// - System config extraction from cached blocks +#[derive(Debug)] +pub struct BufferedL2Provider { + /// Chain state buffer for storing blocks + buffer: Arc, + /// Current chain head we're tracking + current_head: RwLock>, + /// Rollup configuration + rollup_config: Arc, + /// Genesis information + genesis: ChainGenesis, +} + +impl BufferedL2Provider { + /// Create a new buffered L2 provider. + /// + /// # Arguments + /// * `rollup_config` - The rollup configuration containing genesis and chain parameters + /// * `cache_size` - Maximum number of blocks to keep in the LRU cache + /// * `max_reorg_depth` - Maximum reorg depth to handle before clearing the cache + pub fn new(rollup_config: Arc, cache_size: usize, max_reorg_depth: u64) -> Self { + let genesis = rollup_config.genesis; + Self { + buffer: Arc::new(ChainStateBuffer::new(cache_size, max_reorg_depth)), + current_head: RwLock::new(None), + rollup_config, + genesis, + } + } + + /// Process a chain state event. + /// + /// This method should be called when receiving chain state notifications from + /// execution extensions or other chain event sources. It updates the internal + /// state and cache based on the event type (commit, reorg, or revert). + /// + /// # Arguments + /// * `event` - The chain state event to process + pub async fn handle_chain_event( + &self, + event: ChainStateEvent, + ) -> Result<(), BufferedProviderError> { + // Track metrics for chain events + #[cfg(feature = "metrics")] + let event_type = match &event { + ChainStateEvent::ChainCommitted { .. } => "committed", + ChainStateEvent::ChainReorged { .. } => "reorged", + ChainStateEvent::ChainReverted { .. } => "reverted", + }; + + // Update our tracked head based on the event + match &event { + ChainStateEvent::ChainCommitted { new_head, .. } | + ChainStateEvent::ChainReorged { new_head, .. } | + ChainStateEvent::ChainReverted { new_head, .. } => { + let mut current_head = self.current_head.write().await; + *current_head = Some(*new_head); + } + } + + // Handle the event in the buffer + let result = self.buffer.handle_event(event).await.map_err(BufferedProviderError::Buffer); + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + if result.is_ok() { + kona_macros::inc!(gauge, Metrics::CHAIN_EVENTS_PROCESSED, "event" => event_type); + } else { + kona_macros::inc!(gauge, Metrics::CHAIN_EVENT_ERRORS, "event" => event_type); + } + } + + result + } + + /// Add a block to the buffer. + /// + /// This is the primary method for populating the provider with block data. + /// The block and its associated L2 block info are stored in the cache and + /// can be queried using the various provider methods. + /// + /// # Arguments + /// * `block` - The OP block to add + /// * `l2_block_info` - The L2 block information associated with the block + pub async fn add_block( + &self, + block: OpBlock, + l2_block_info: L2BlockInfo, + ) -> Result<(), BufferedProviderError> { + let cached_block = CachedBlock::new(block, l2_block_info); + self.buffer.insert_block(cached_block).await; + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + kona_macros::inc!(gauge, Metrics::BLOCKS_ADDED); + } + + Ok(()) + } + + /// Get the current chain head + pub async fn current_head(&self) -> Option { + let current_head = self.current_head.read().await; + *current_head + } + + /// Get cache statistics + pub async fn cache_stats(&self) -> crate::buffer::CacheStats { + self.buffer.cache_stats().await + } + + /// Clear the cache + pub async fn clear_cache(&self) { + self.buffer.clear().await; + } +} + +/// Clone implementation for BufferedL2Provider +impl Clone for BufferedL2Provider { + fn clone(&self) -> Self { + Self { + buffer: self.buffer.clone(), + current_head: RwLock::new(None), + rollup_config: self.rollup_config.clone(), + genesis: self.genesis, + } + } +} + +#[async_trait] +impl L2ChainProvider for BufferedL2Provider { + type Error = BufferedProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error> { + // Check if this is the genesis block + if number == self.genesis.l2.number { + return self.genesis.system_config.ok_or(BufferedProviderError::SystemConfigMissing); + } + + // Get the block from cache + let cached_block = self.buffer.get_block_by_number(number).await; + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + if cached_block.is_some() { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_HITS, "method" => "system_config"); + } else { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_MISSES, "method" => "system_config"); + } + } + + let cached_block = cached_block.ok_or(BufferedProviderError::BlockNotFound(number))?; + + // Extract system config from the block + to_system_config(&cached_block.block, &rollup_config) + .map_err(|_| BufferedProviderError::SystemConfigConversion(number)) + } +} + +#[async_trait] +impl BatchValidationProvider for BufferedL2Provider { + type Error = BufferedProviderError; + + async fn block_by_number(&mut self, number: u64) -> Result { + // Get the block from cache + let cached_block = self.buffer.get_block_by_number(number).await; + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + if cached_block.is_some() { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_HITS, "method" => "block_by_number"); + } else { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_MISSES, "method" => "block_by_number"); + } + } + + let cached_block = cached_block.ok_or(BufferedProviderError::BlockNotFound(number))?; + + Ok(cached_block.block) + } + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + // Check if this is the genesis block + if number == self.genesis.l2.number { + return L2BlockInfo::from_block_and_genesis( + &OpBlock::default(), // Genesis doesn't need full block data + &self.genesis, + ) + .map_err(|_| BufferedProviderError::L2BlockInfoConstruction(number)); + } + + // Get the block from cache + let cached_block = self.buffer.get_block_by_number(number).await; + + #[cfg(feature = "metrics")] + { + use crate::Metrics; + if cached_block.is_some() { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_HITS, "method" => "l2_block_info"); + } else { + kona_macros::inc!(gauge, Metrics::BUFFERED_PROVIDER_CACHE_MISSES, "method" => "l2_block_info"); + } + } + + let cached_block = cached_block.ok_or(BufferedProviderError::BlockNotFound(number))?; + + Ok(cached_block.l2_block_info) + } +} + +/// Errors that can occur in the buffered provider +#[derive(Debug, thiserror::Error)] +pub enum BufferedProviderError { + /// Error from the chain buffer + #[error("Buffer error: {0}")] + Buffer(#[from] ChainBufferError), + /// Block not found in cache + #[error("Block {0} not found in cache")] + BlockNotFound(u64), + /// Failed to construct L2BlockInfo + #[error("Failed to construct L2BlockInfo for block {0}")] + L2BlockInfoConstruction(u64), + /// Failed to convert block to SystemConfig + #[error("Failed to convert block {0} to SystemConfig")] + SystemConfigConversion(u64), + /// System config missing from genesis + #[error("System config missing from genesis")] + SystemConfigMissing, +} + +impl From for PipelineErrorKind { + fn from(e: BufferedProviderError) -> Self { + match e { + BufferedProviderError::Buffer(ChainBufferError::ReorgTooDeep { depth, max_depth }) => { + Self::Critical(PipelineError::Provider(format!( + "Reorg too deep: {depth} > {max_depth}" + ))) + } + BufferedProviderError::Buffer(ChainBufferError::BlockNotFound { hash }) => { + Self::Temporary(PipelineError::Provider(format!( + "Block not found in cache: {hash}" + ))) + } + BufferedProviderError::BlockNotFound(number) => Self::Temporary( + PipelineError::Provider(format!("Block {number} not found in cache")), + ), + BufferedProviderError::L2BlockInfoConstruction(number) => { + Self::Temporary(PipelineError::Provider(format!( + "Failed to construct L2BlockInfo for block {number}" + ))) + } + BufferedProviderError::SystemConfigConversion(number) => { + Self::Temporary(PipelineError::Provider(format!( + "Failed to convert block {number} to SystemConfig" + ))) + } + BufferedProviderError::SystemConfigMissing => Self::Critical(PipelineError::Provider( + "System config missing from genesis".to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Header; + use alloy_eips::BlockNumHash; + use alloy_primitives::B256; + use kona_genesis::RollupConfig; + use kona_protocol::BlockInfo; + + async fn create_test_provider() -> BufferedL2Provider { + let mut rollup_config = RollupConfig::default(); + rollup_config.genesis.l2 = BlockNumHash { number: 0, hash: B256::ZERO }; + rollup_config.genesis.l1 = BlockNumHash { number: 0, hash: B256::ZERO }; + rollup_config.genesis.system_config = Some(SystemConfig::default()); + let rollup_config = Arc::new(rollup_config); + + BufferedL2Provider::new(rollup_config, 100, 10) + } + + #[tokio::test] + async fn test_provider_creation() { + let provider = create_test_provider().await; + assert!(provider.current_head().await.is_none()); + } + + #[tokio::test] + async fn test_chain_event_handling() { + let provider = create_test_provider().await; + + let hash1 = B256::with_last_byte(1); + let hash2 = B256::with_last_byte(2); + + // First add some blocks to the cache + let header1 = Header { + number: 1, + parent_hash: B256::ZERO, + timestamp: 1234567890, + ..Default::default() + }; + let block1 = OpBlock { header: header1, body: Default::default() }; + let l2_info1 = L2BlockInfo { + block_info: BlockInfo { + hash: hash1, + number: 1, + parent_hash: B256::ZERO, + timestamp: 1234567890, + }, + l1_origin: BlockNumHash { number: 1, hash: B256::ZERO }, + seq_num: 0, + }; + provider.add_block(block1, l2_info1).await.unwrap(); + + let event = + ChainStateEvent::ChainCommitted { new_head: hash2, committed: vec![hash1, hash2] }; + + let result = provider.handle_chain_event(event).await; + assert!(result.is_ok()); + + assert_eq!(provider.current_head().await, Some(hash2)); + } + + #[tokio::test] + async fn test_cache_stats() { + let provider = create_test_provider().await; + let stats = provider.cache_stats().await; + + assert_eq!(stats.capacity, 100); + assert_eq!(stats.max_reorg_depth, 10); + assert_eq!(stats.blocks_by_hash_len, 0); + assert_eq!(stats.blocks_by_number_len, 0); + } + + #[tokio::test] + async fn test_clear_cache() { + let provider = create_test_provider().await; + + // Clear should work even on empty cache + provider.clear_cache().await; + + let stats = provider.cache_stats().await; + assert_eq!(stats.blocks_by_hash_len, 0); + assert_eq!(stats.blocks_by_number_len, 0); + } + + #[tokio::test] + async fn test_add_and_retrieve_block() { + let mut provider = create_test_provider().await; + + let hash = B256::with_last_byte(1); + let header = Header { + number: 1, + parent_hash: B256::ZERO, + timestamp: 1234567890, + ..Default::default() + }; + let block = OpBlock { header, body: Default::default() }; + let l2_info = L2BlockInfo { + block_info: BlockInfo { + hash, + number: 1, + parent_hash: B256::ZERO, + timestamp: 1234567890, + }, + l1_origin: BlockNumHash { number: 1, hash: B256::ZERO }, + seq_num: 0, + }; + + // Add block to the provider + provider.add_block(block.clone(), l2_info).await.unwrap(); + + // Retrieve block by number + let retrieved_block = provider.block_by_number(1).await.unwrap(); + assert_eq!(retrieved_block.header.number, 1); + + // Retrieve L2 block info by number + let retrieved_info = provider.l2_block_info_by_number(1).await.unwrap(); + assert_eq!(retrieved_info.block_info.number, 1); + } +} diff --git a/kona/crates/providers/providers-local/src/lib.rs b/kona/crates/providers/providers-local/src/lib.rs new file mode 100644 index 0000000000000..5b7db5d730728 --- /dev/null +++ b/kona/crates/providers/providers-local/src/lib.rs @@ -0,0 +1,18 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod buffer; +pub use buffer::{CacheStats, CachedBlock, ChainBufferError, ChainStateBuffer, ChainStateEvent}; + +mod buffered; +pub use buffered::{BufferedL2Provider, BufferedProviderError}; + +#[cfg(feature = "metrics")] +mod metrics; +#[cfg(feature = "metrics")] +pub use metrics::Metrics; diff --git a/kona/crates/providers/providers-local/src/metrics.rs b/kona/crates/providers/providers-local/src/metrics.rs new file mode 100644 index 0000000000000..9b50979b28aa7 --- /dev/null +++ b/kona/crates/providers/providers-local/src/metrics.rs @@ -0,0 +1,130 @@ +//! Metrics for the local buffered provider. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub struct Metrics; + +impl Metrics { + /// Identifier for the gauge that tracks buffered provider cache hits. + pub const BUFFERED_PROVIDER_CACHE_HITS: &str = "kona_providers_local_cache_hits"; + + /// Identifier for the gauge that tracks buffered provider cache misses. + pub const BUFFERED_PROVIDER_CACHE_MISSES: &str = "kona_providers_local_cache_misses"; + + /// Identifier for the gauge that tracks chain events processed. + pub const CHAIN_EVENTS_PROCESSED: &str = "kona_providers_local_chain_events"; + + /// Identifier for the gauge that tracks chain event errors. + pub const CHAIN_EVENT_ERRORS: &str = "kona_providers_local_chain_event_errors"; + + /// Identifier for the gauge that tracks blocks added to cache. + pub const BLOCKS_ADDED: &str = "kona_providers_local_blocks_added"; + + /// Identifier for the gauge that tracks active cache entries. + pub const CACHE_ENTRIES: &str = "kona_providers_local_cache_entries"; + + /// Identifier for the gauge that tracks cache capacity. + pub const CACHE_CAPACITY: &str = "kona_providers_local_cache_capacity"; + + /// Identifier for the gauge that tracks reorg depth. + pub const REORG_DEPTH: &str = "kona_providers_local_reorg_depth"; + + /// Identifier for the gauge that tracks cache clears. + pub const CACHE_CLEARS: &str = "kona_providers_local_cache_clears"; + + /// Initializes metrics for the local buffered provider. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics to 0 so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in [`kona_providers_local`][crate]. + #[cfg(feature = "metrics")] + pub fn describe() { + metrics::describe_gauge!( + Self::BUFFERED_PROVIDER_CACHE_HITS, + "Number of cache hits in buffered provider" + ); + metrics::describe_gauge!( + Self::BUFFERED_PROVIDER_CACHE_MISSES, + "Number of cache misses in buffered provider" + ); + metrics::describe_gauge!(Self::CHAIN_EVENTS_PROCESSED, "Number of chain events processed"); + metrics::describe_gauge!( + Self::CHAIN_EVENT_ERRORS, + "Number of chain event processing errors" + ); + metrics::describe_gauge!(Self::BLOCKS_ADDED, "Number of blocks added to cache"); + metrics::describe_gauge!(Self::CACHE_ENTRIES, "Number of active entries in cache"); + metrics::describe_gauge!(Self::CACHE_CAPACITY, "Total capacity of cache"); + metrics::describe_gauge!(Self::REORG_DEPTH, "Maximum depth of reorganization observed"); + metrics::describe_gauge!(Self::CACHE_CLEARS, "Number of times cache was cleared"); + } + + /// Initializes metrics to `0` so they can be queried immediately. + #[cfg(feature = "metrics")] + pub fn zero() { + // Cache hit/miss metrics + kona_macros::set!( + gauge, + Self::BUFFERED_PROVIDER_CACHE_HITS, + "method", + "block_by_number", + 0 + ); + kona_macros::set!(gauge, Self::BUFFERED_PROVIDER_CACHE_HITS, "method", "block_by_hash", 0); + kona_macros::set!(gauge, Self::BUFFERED_PROVIDER_CACHE_HITS, "method", "l2_block_info", 0); + kona_macros::set!(gauge, Self::BUFFERED_PROVIDER_CACHE_HITS, "method", "system_config", 0); + + kona_macros::set!( + gauge, + Self::BUFFERED_PROVIDER_CACHE_MISSES, + "method", + "block_by_number", + 0 + ); + kona_macros::set!( + gauge, + Self::BUFFERED_PROVIDER_CACHE_MISSES, + "method", + "block_by_hash", + 0 + ); + kona_macros::set!( + gauge, + Self::BUFFERED_PROVIDER_CACHE_MISSES, + "method", + "l2_block_info", + 0 + ); + kona_macros::set!( + gauge, + Self::BUFFERED_PROVIDER_CACHE_MISSES, + "method", + "system_config", + 0 + ); + + // Chain event metrics + kona_macros::set!(gauge, Self::CHAIN_EVENTS_PROCESSED, "event", "committed", 0); + kona_macros::set!(gauge, Self::CHAIN_EVENTS_PROCESSED, "event", "reorged", 0); + kona_macros::set!(gauge, Self::CHAIN_EVENTS_PROCESSED, "event", "reverted", 0); + + kona_macros::set!(gauge, Self::CHAIN_EVENT_ERRORS, "event", "committed", 0); + kona_macros::set!(gauge, Self::CHAIN_EVENT_ERRORS, "event", "reorged", 0); + kona_macros::set!(gauge, Self::CHAIN_EVENT_ERRORS, "event", "reverted", 0); + + // General metrics + kona_macros::set!(gauge, Self::BLOCKS_ADDED, 0); + kona_macros::set!(gauge, Self::CACHE_ENTRIES, "cache", "blocks_by_hash", 0); + kona_macros::set!(gauge, Self::CACHE_ENTRIES, "cache", "blocks_by_number", 0); + kona_macros::set!(gauge, Self::CACHE_CAPACITY, 0); + kona_macros::set!(gauge, Self::REORG_DEPTH, 0); + kona_macros::set!(gauge, Self::CACHE_CLEARS, 0); + } +} diff --git a/kona/crates/providers/providers-local/tests/integration.rs b/kona/crates/providers/providers-local/tests/integration.rs new file mode 100644 index 0000000000000..8b30724b6d40a --- /dev/null +++ b/kona/crates/providers/providers-local/tests/integration.rs @@ -0,0 +1,337 @@ +//! Integration tests for the local buffered provider. + +use alloy_consensus::Header; +use alloy_eips::BlockNumHash; +use alloy_primitives::B256; +use kona_derive::L2ChainProvider; +use kona_genesis::{RollupConfig, SystemConfig}; +use kona_protocol::{BatchValidationProvider, BlockInfo, L2BlockInfo}; +use kona_providers_local::{BufferedL2Provider, ChainStateEvent}; +use op_alloy_consensus::OpBlock; +use rstest::*; +use std::sync::Arc; + +/// Create a test rollup configuration with genesis +fn create_test_config() -> Arc { + let mut rollup_config = RollupConfig::default(); + rollup_config.genesis.l2 = BlockNumHash { number: 0, hash: B256::ZERO }; + rollup_config.genesis.l1 = BlockNumHash { number: 0, hash: B256::ZERO }; + rollup_config.genesis.system_config = Some(SystemConfig::default()); + Arc::new(rollup_config) +} + +/// Create a test block with specific number and hash +fn create_test_block(number: u64, hash: B256, parent_hash: B256) -> (OpBlock, L2BlockInfo) { + let header = Header { + number, + parent_hash, + timestamp: 1234567890 + number, + gas_limit: 30_000_000, + gas_used: 15_000_000, + base_fee_per_gas: Some(7), + ..Default::default() + }; + + let block = OpBlock { header: header.clone(), body: Default::default() }; + + let l2_block_info = L2BlockInfo { + block_info: BlockInfo { hash, number, parent_hash, timestamp: header.timestamp }, + l1_origin: BlockNumHash { number: number / 2, hash: B256::from([1; 32]) }, + seq_num: number % 10, + }; + + (block, l2_block_info) +} + +#[fixture] +fn provider() -> BufferedL2Provider { + let config = create_test_config(); + BufferedL2Provider::new(config, 100, 10) +} + +#[rstest] +#[tokio::test] +async fn test_provider_initialization(provider: BufferedL2Provider) { + assert!(provider.current_head().await.is_none()); + let stats = provider.cache_stats().await; + assert_eq!(stats.capacity, 100); + assert_eq!(stats.max_reorg_depth, 10); + assert_eq!(stats.blocks_by_hash_len, 0); + assert_eq!(stats.blocks_by_number_len, 0); +} + +#[rstest] +#[tokio::test] +async fn test_add_and_retrieve_single_block(provider: BufferedL2Provider) { + let hash = B256::from([1; 32]); + let (block, l2_info) = create_test_block(1, hash, B256::ZERO); + + // Add block + provider.add_block(block.clone(), l2_info).await.unwrap(); + + // Retrieve as mutable reference for trait methods + let mut provider_mut = provider.clone(); + + // Retrieve block by number + let retrieved_block = provider_mut.block_by_number(1).await.unwrap(); + assert_eq!(retrieved_block.header.number, 1); + assert_eq!(retrieved_block.header.timestamp, block.header.timestamp); + + // Retrieve L2 block info by number + let retrieved_info = provider_mut.l2_block_info_by_number(1).await.unwrap(); + assert_eq!(retrieved_info.block_info.hash, hash); + assert_eq!(retrieved_info.seq_num, l2_info.seq_num); +} + +#[rstest] +#[tokio::test] +async fn test_multiple_blocks_sequential(provider: BufferedL2Provider) { + let mut parent_hash = B256::ZERO; + + // Add 10 sequential blocks + for i in 1..=10 { + let hash = B256::from([i as u8; 32]); + let (block, l2_info) = create_test_block(i, hash, parent_hash); + provider.add_block(block, l2_info).await.unwrap(); + parent_hash = hash; + } + + // Verify all blocks are retrievable + let mut provider_mut = provider.clone(); + for i in 1..=10 { + let block = provider_mut.block_by_number(i).await.unwrap(); + assert_eq!(block.header.number, i); + + let info = provider_mut.l2_block_info_by_number(i).await.unwrap(); + assert_eq!(info.block_info.number, i); + } +} + +#[rstest] +#[tokio::test] +async fn test_block_not_found_error(mut provider: BufferedL2Provider) { + // Try to retrieve non-existent block + let result = provider.block_by_number(999).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("999")); + + // Try to retrieve non-existent L2 block info + let result = provider.l2_block_info_by_number(999).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("999")); +} + +#[rstest] +#[tokio::test] +async fn test_genesis_block_handling(mut provider: BufferedL2Provider) { + // Genesis block should be handled specially + // Note: Genesis block handling with OpBlock::default() may fail + // because it doesn't have proper L1 info transaction + // This is a known limitation when not using actual genesis data + let result = provider.l2_block_info_by_number(0).await; + // For now, we just verify the call completes (may error due to missing L1 info) + let _ = result; +} + +#[rstest] +#[tokio::test] +async fn test_chain_committed_event(provider: BufferedL2Provider) { + // Add some blocks + let hash1 = B256::from([1; 32]); + let hash2 = B256::from([2; 32]); + let hash3 = B256::from([3; 32]); + + let (block1, l2_info1) = create_test_block(1, hash1, B256::ZERO); + let (block2, l2_info2) = create_test_block(2, hash2, hash1); + let (block3, l2_info3) = create_test_block(3, hash3, hash2); + + provider.add_block(block1, l2_info1).await.unwrap(); + provider.add_block(block2, l2_info2).await.unwrap(); + provider.add_block(block3, l2_info3).await.unwrap(); + + // Send chain committed event + let event = + ChainStateEvent::ChainCommitted { new_head: hash3, committed: vec![hash1, hash2, hash3] }; + + provider.handle_chain_event(event).await.unwrap(); + assert_eq!(provider.current_head().await, Some(hash3)); +} + +#[rstest] +#[tokio::test] +async fn test_chain_reorg_shallow(provider: BufferedL2Provider) { + let old_head = B256::from([1; 32]); + let new_head = B256::from([2; 32]); + + // Add initial block + let (block, l2_info) = create_test_block(1, old_head, B256::ZERO); + provider.add_block(block, l2_info).await.unwrap(); + + // Simulate shallow reorg (depth 2) + let event = ChainStateEvent::ChainReorged { old_head, new_head, depth: 2 }; + + provider.handle_chain_event(event).await.unwrap(); + assert_eq!(provider.current_head().await, Some(new_head)); +} + +#[rstest] +#[tokio::test] +async fn test_chain_reorg_deep_clears_cache(provider: BufferedL2Provider) { + // Add blocks + for i in 1..=15 { + let hash = B256::from([i as u8; 32]); + let parent_hash = if i == 1 { B256::ZERO } else { B256::from([(i - 1) as u8; 32]) }; + let (block, l2_info) = create_test_block(i, hash, parent_hash); + provider.add_block(block, l2_info).await.unwrap(); + } + + let old_head = B256::from([15; 32]); + let new_head = B256::from([20; 32]); + + // Simulate deep reorg (depth 11, which exceeds max_reorg_depth of 10) + let event = ChainStateEvent::ChainReorged { old_head, new_head, depth: 11 }; + + // This should fail because depth exceeds max_reorg_depth + let result = provider.handle_chain_event(event).await; + assert!(result.is_err()); + + // To test cache clearing, we need a reorg within the limit but > 10 + // Since our max_reorg_depth is 10, let's test with exactly 10 which should clear cache + let event2 = ChainStateEvent::ChainReorged { old_head, new_head, depth: 10 }; + + provider.handle_chain_event(event2).await.unwrap(); + // Note: Cache clearing happens when depth > 10 in the implementation, + // so depth 10 won't clear. This is a design decision in the implementation. +} + +#[rstest] +#[tokio::test] +async fn test_chain_reorg_too_deep_error(provider: BufferedL2Provider) { + let old_head = B256::from([1; 32]); + let new_head = B256::from([2; 32]); + + // Simulate reorg deeper than max_reorg_depth (10) + let event = ChainStateEvent::ChainReorged { old_head, new_head, depth: 15 }; + + let result = provider.handle_chain_event(event).await; + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Reorg depth") || err_msg.contains("reorg"), + "Error message should mention reorg: {err_msg}" + ); +} + +#[rstest] +#[tokio::test] +async fn test_chain_reverted_event(provider: BufferedL2Provider) { + // Add blocks + let hash1 = B256::from([1; 32]); + let hash2 = B256::from([2; 32]); + let hash3 = B256::from([3; 32]); + + let (block1, l2_info1) = create_test_block(1, hash1, B256::ZERO); + let (block2, l2_info2) = create_test_block(2, hash2, hash1); + let (block3, l2_info3) = create_test_block(3, hash3, hash2); + + provider.add_block(block1, l2_info1).await.unwrap(); + provider.add_block(block2, l2_info2).await.unwrap(); + provider.add_block(block3, l2_info3).await.unwrap(); + + // Revert back to block 1 + let event = ChainStateEvent::ChainReverted { + old_head: hash3, + new_head: hash1, + reverted: vec![hash2, hash3], + }; + + provider.handle_chain_event(event).await.unwrap(); + assert_eq!(provider.current_head().await, Some(hash1)); +} + +#[rstest] +#[tokio::test] +async fn test_cache_clear(provider: BufferedL2Provider) { + // Add some blocks + for i in 1..=5 { + let hash = B256::from([i as u8; 32]); + let parent_hash = if i == 1 { B256::ZERO } else { B256::from([(i - 1) as u8; 32]) }; + let (block, l2_info) = create_test_block(i, hash, parent_hash); + provider.add_block(block, l2_info).await.unwrap(); + } + + // Verify blocks are in cache + let stats = provider.cache_stats().await; + assert!(stats.blocks_by_hash_len > 0); + assert!(stats.blocks_by_number_len > 0); + + // Clear cache + provider.clear_cache().await; + + // Verify cache is empty + let stats = provider.cache_stats().await; + assert_eq!(stats.blocks_by_hash_len, 0); + assert_eq!(stats.blocks_by_number_len, 0); +} + +#[rstest] +#[tokio::test] +async fn test_system_config_retrieval(mut provider: BufferedL2Provider) { + let config = create_test_config(); + + // Add a block with system config data + let hash = B256::from([1; 32]); + let (block, l2_info) = create_test_block(1, hash, B256::ZERO); + provider.add_block(block, l2_info).await.unwrap(); + + // Retrieve system config for genesis + let genesis_config = provider.system_config_by_number(0, config.clone()).await.unwrap(); + // Just verify we got a config back + // The default SystemConfig might have zero values, so we just check it exists + let _ = genesis_config; + + // Note: For non-genesis blocks, the system config extraction depends on + // the block having proper L1 info deposit transaction, which our test blocks don't have. +} + +#[rstest] +#[tokio::test] +async fn test_provider_clone(provider: BufferedL2Provider) { + // Add a block + let hash = B256::from([1; 32]); + let (block, l2_info) = create_test_block(1, hash, B256::ZERO); + provider.add_block(block, l2_info).await.unwrap(); + + // Clone the provider + let cloned = provider.clone(); + + // Both should have access to the same cache + let mut provider_mut = provider.clone(); + let mut cloned_mut = cloned.clone(); + + let block1 = provider_mut.block_by_number(1).await.unwrap(); + let block2 = cloned_mut.block_by_number(1).await.unwrap(); + + assert_eq!(block1.header.number, block2.header.number); +} + +#[rstest] +#[tokio::test] +async fn test_lru_cache_eviction(_provider: BufferedL2Provider) { + // Create provider with small cache size + let config = create_test_config(); + let small_provider = BufferedL2Provider::new(config, 5, 10); + + // Add more blocks than cache size + for i in 1..=10 { + let hash = B256::from([i as u8; 32]); + let parent_hash = if i == 1 { B256::ZERO } else { B256::from([(i - 1) as u8; 32]) }; + let (block, l2_info) = create_test_block(i, hash, parent_hash); + small_provider.add_block(block, l2_info).await.unwrap(); + } + + // Cache stats should show at most 5 entries + let stats = small_provider.cache_stats().await; + assert!(stats.blocks_by_hash_len <= 5); + assert!(stats.blocks_by_number_len <= 5); +} diff --git a/kona/crates/supervisor/core/Cargo.toml b/kona/crates/supervisor/core/Cargo.toml new file mode 100644 index 0000000000000..7a673a4742f8d --- /dev/null +++ b/kona/crates/supervisor/core/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "kona-supervisor-core" +version = "0.1.0" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +# workspace +kona-interop.workspace = true +kona-protocol.workspace = true +kona-supervisor-types.workspace = true +kona-supervisor-rpc = { workspace = true, features = ["jsonrpsee", "client"] } +kona-supervisor-storage.workspace = true +kona-supervisor-metrics.workspace = true +kona-genesis.workspace = true + +# alloy +alloy-eips.workspace = true +alloy-network.workspace = true +alloy-provider = { workspace = true, features = ["reqwest"] } +alloy-primitives = { workspace = true, features = ["map", "rlp", "serde"] } +alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } +alloy-rpc-client.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-consensus.workspace = true + +# op-alloy +op-alloy-rpc-types = { workspace = true, features = ["jsonrpsee"] } +op-alloy-consensus.workspace = true + +# jsonrpsee +jsonrpsee = { workspace = true, features = [ "macros", "server", "client", "ws-client" ] } + +# general +async-trait.workspace = true +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["sync", "macros"] } +tokio-util.workspace = true +auto_impl.workspace = true +reqwest = { workspace = true } +futures = { workspace = true } +derive_more = { workspace = true, features = ["try_from"] } + +# `metrics` feature +metrics = { workspace = true } + +[dev-dependencies] +serde_json.workspace = true +tempfile.workspace = true +alloy-transport.workspace = true +kona-interop = {workspace = true, features = ["std", "test-utils"]} +mockall.workspace = true + +[lints] +workspace = true diff --git a/kona/crates/supervisor/core/src/chain_processor/chain.rs b/kona/crates/supervisor/core/src/chain_processor/chain.rs new file mode 100644 index 0000000000000..e1238ab83f1e9 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/chain.rs @@ -0,0 +1,147 @@ +use super::handlers::{ + CrossSafeHandler, CrossUnsafeHandler, EventHandler, FinalizedHandler, InvalidationHandler, + OriginHandler, ReplacementHandler, SafeBlockHandler, UnsafeBlockHandler, +}; +use crate::{ + LogIndexer, ProcessorState, + event::ChainEvent, + syncnode::{BlockProvider, ManagedNodeCommand}, +}; +use alloy_primitives::ChainId; +use kona_interop::InteropValidator; +use kona_supervisor_storage::{ + DerivationStorage, HeadRefStorageWriter, LogStorage, StorageRewinder, +}; +use std::{fmt::Debug, sync::Arc}; +use tokio::sync::mpsc; +use tracing::debug; + +/// Represents a task that processes chain events from a managed node. +/// It listens for events emitted by the managed node and handles them accordingly. +#[derive(Debug)] +pub struct ChainProcessor { + chain_id: ChainId, + metrics_enabled: Option, + + // state + state: ProcessorState, + + // Handlers for different types of chain events. + unsafe_handler: UnsafeBlockHandler, + safe_handler: SafeBlockHandler, + origin_handler: OriginHandler, + invalidation_handler: InvalidationHandler, + replacement_handler: ReplacementHandler, + finalized_handler: FinalizedHandler, + cross_unsafe_handler: CrossUnsafeHandler, + cross_safe_handler: CrossSafeHandler, +} + +impl ChainProcessor +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + DerivationStorage + HeadRefStorageWriter + StorageRewinder + 'static, +{ + /// Creates a new [`ChainProcessor`]. + pub fn new( + validator: Arc, + chain_id: ChainId, + log_indexer: Arc>, + db_provider: Arc, + managed_node_sender: mpsc::Sender, + ) -> Self { + let unsafe_handler = UnsafeBlockHandler::new( + chain_id, + validator.clone(), + db_provider.clone(), + log_indexer.clone(), + ); + + let safe_handler = SafeBlockHandler::new( + chain_id, + managed_node_sender.clone(), + db_provider.clone(), + validator, + log_indexer.clone(), + ); + + let origin_handler = + OriginHandler::new(chain_id, managed_node_sender.clone(), db_provider.clone()); + + let invalidation_handler = + InvalidationHandler::new(chain_id, managed_node_sender.clone(), db_provider.clone()); + + let replacement_handler = + ReplacementHandler::new(chain_id, log_indexer, db_provider.clone()); + + let finalized_handler = + FinalizedHandler::new(chain_id, managed_node_sender.clone(), db_provider); + let cross_unsafe_handler = CrossUnsafeHandler::new(chain_id, managed_node_sender.clone()); + let cross_safe_handler = CrossSafeHandler::new(chain_id, managed_node_sender); + + Self { + chain_id, + metrics_enabled: None, + + state: ProcessorState::new(), + + // Handlers for different types of chain events. + unsafe_handler, + safe_handler, + origin_handler, + invalidation_handler, + replacement_handler, + finalized_handler, + cross_unsafe_handler, + cross_safe_handler, + } + } + + /// Enables metrics on the database environment. + pub fn with_metrics(mut self) -> Self { + self.metrics_enabled = Some(true); + super::Metrics::init(self.chain_id); + self + } + + /// Handles a chain event by delegating it to the appropriate handler. + pub async fn handle_event(&mut self, event: ChainEvent) { + let result = match event { + ChainEvent::UnsafeBlock { block } => { + self.unsafe_handler.handle(block, &mut self.state).await + } + ChainEvent::DerivedBlock { derived_ref_pair } => { + self.safe_handler.handle(derived_ref_pair, &mut self.state).await + } + ChainEvent::DerivationOriginUpdate { origin } => { + self.origin_handler.handle(origin, &mut self.state).await + } + ChainEvent::InvalidateBlock { block } => { + self.invalidation_handler.handle(block, &mut self.state).await + } + ChainEvent::BlockReplaced { replacement } => { + self.replacement_handler.handle(replacement, &mut self.state).await + } + ChainEvent::FinalizedSourceUpdate { finalized_source_block } => { + self.finalized_handler.handle(finalized_source_block, &mut self.state).await + } + ChainEvent::CrossUnsafeUpdate { block } => { + self.cross_unsafe_handler.handle(block, &mut self.state).await + } + ChainEvent::CrossSafeUpdate { derived_ref_pair } => { + self.cross_safe_handler.handle(derived_ref_pair, &mut self.state).await + } + }; + + if let Err(err) = result { + debug!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %err, + ?event, + "Failed to process event" + ); + } + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/error.rs b/kona/crates/supervisor/core/src/chain_processor/error.rs new file mode 100644 index 0000000000000..31ef4feb931e5 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/error.rs @@ -0,0 +1,19 @@ +use crate::logindexer::LogIndexerError; +use kona_supervisor_storage::StorageError; +use thiserror::Error; + +/// Errors that may occur while processing chains in the supervisor core. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ChainProcessorError { + /// Represents an error that occurred while interacting with the storage layer. + #[error(transparent)] + StorageError(#[from] StorageError), + + /// Represents an error that occurred while indexing logs. + #[error(transparent)] + LogIndexerError(#[from] LogIndexerError), + + /// Represents an error that occurred while sending an event to the channel. + #[error("failed to send event to channel: {0}")] + ChannelSendFailed(String), +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/cross_chain.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/cross_chain.rs new file mode 100644 index 0000000000000..888b341a72707 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/cross_chain.rs @@ -0,0 +1,272 @@ +use super::EventHandler; +use crate::{ + ChainProcessorError, ProcessorState, chain_processor::Metrics, syncnode::ManagedNodeCommand, +}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use tokio::sync::mpsc; +use tracing::{trace, warn}; + +/// Handler for cross unsafe blocks. +/// This handler processes cross unsafe blocks by updating the managed node. +#[derive(Debug, Constructor)] +pub struct CrossUnsafeHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, +} + +#[async_trait] +impl EventHandler for CrossUnsafeHandler { + async fn handle( + &self, + block: BlockInfo, + _state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Processing cross unsafe block" + ); + + let result = self.inner_handle(block).await; + Metrics::record_block_processing(self.chain_id, Metrics::BLOCK_TYPE_CROSS_UNSAFE, &result); + + result + } +} + +impl CrossUnsafeHandler { + async fn inner_handle(&self, block: BlockInfo) -> Result { + self.managed_node_sender + .send(ManagedNodeCommand::UpdateCrossUnsafe { block_id: block.id() }) + .await + .map_err(|err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %block, + %err, + "Failed to send cross unsafe block update" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + })?; + Ok(block) + } +} + +/// Handler for cross safe blocks. +/// This handler processes cross safe blocks by updating the managed node. +#[derive(Debug, Constructor)] +pub struct CrossSafeHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, +} + +#[async_trait] +impl EventHandler for CrossSafeHandler { + async fn handle( + &self, + derived_ref_pair: DerivedRefPair, + _state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + "Processing cross safe block" + ); + + let result = self.inner_handle(derived_ref_pair).await; + Metrics::record_block_processing(self.chain_id, Metrics::BLOCK_TYPE_CROSS_SAFE, &result); + result + } +} + +impl CrossSafeHandler { + async fn inner_handle( + &self, + derived_ref_pair: DerivedRefPair, + ) -> Result { + self.managed_node_sender + .send(ManagedNodeCommand::UpdateCrossSafe { + source_block_id: derived_ref_pair.source.id(), + derived_block_id: derived_ref_pair.derived.id(), + }) + .await + .map_err(|err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %derived_ref_pair, + %err, + "Failed to send cross safe block update" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + })?; + Ok(derived_ref_pair.derived) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeError, + }; + use alloy_primitives::B256; + use alloy_rpc_types_eth::BlockNumHash; + use async_trait::async_trait; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use kona_supervisor_types::{BlockSeal, OutputV0, Receipts}; + use mockall::mock; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + #[tokio::test] + async fn test_handle_cross_unsafe_update_triggers() { + use crate::syncnode::ManagedNodeCommand; + + let (tx, mut rx) = mpsc::channel(8); + let chain_id = 1; + let handler = CrossUnsafeHandler::new(chain_id, tx); + + let block = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let mut state = ProcessorState::new(); + + // Call the handler + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + + // The handler should send the correct command + if let Some(ManagedNodeCommand::UpdateCrossUnsafe { block_id }) = rx.recv().await { + assert_eq!(block_id, block.id()); + } else { + panic!("Expected UpdateCrossUnsafe command"); + } + } + + #[tokio::test] + async fn test_handle_cross_unsafe_update_error() { + let (tx, rx) = mpsc::channel(8); + let chain_id = 1; + let handler = CrossUnsafeHandler::new(chain_id, tx); + + // Drop the receiver to simulate a send error + drop(rx); + + let block = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let mut state = ProcessorState::new(); + + // Call the handler, which should now error + let result = handler.handle(block, &mut state).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_handle_cross_safe_update_triggers() { + use crate::syncnode::ManagedNodeCommand; + + let (tx, mut rx) = mpsc::channel(8); + let chain_id = 1; + let handler = CrossSafeHandler::new(chain_id, tx); + + let derived = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let source = + BlockInfo { number: 1, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let derived_ref_pair = DerivedRefPair { source, derived }; + let mut state = ProcessorState::new(); + + // Call the handler + let result = handler.handle(derived_ref_pair, &mut state).await; + assert!(result.is_ok()); + + // The handler should send the correct command + if let Some(ManagedNodeCommand::UpdateCrossSafe { source_block_id, derived_block_id }) = + rx.recv().await + { + assert_eq!(source_block_id, source.id()); + assert_eq!(derived_block_id, derived.id()); + } else { + panic!("Expected UpdateCrossSafe command"); + } + } + + #[tokio::test] + async fn test_handle_cross_safe_update_error() { + let (tx, rx) = mpsc::channel(8); + let chain_id = 1; + let handler = CrossSafeHandler::new(chain_id, tx); + + // Drop the receiver to simulate a send error + drop(rx); + + let derived = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let source = + BlockInfo { number: 1, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + let derived_ref_pair = DerivedRefPair { source, derived }; + let mut state = ProcessorState::new(); + + // Call the handler, which should now error + let result = handler.handle(derived_ref_pair, &mut state).await; + assert!(result.is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/finalized.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/finalized.rs new file mode 100644 index 0000000000000..c081c5d026433 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/finalized.rs @@ -0,0 +1,283 @@ +use super::EventHandler; +use crate::{ + ChainProcessorError, ProcessorState, chain_processor::Metrics, syncnode::ManagedNodeCommand, +}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::HeadRefStorageWriter; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{trace, warn}; + +/// Handler for finalized block updates. +/// This handler processes finalized block updates by updating the managed node and state manager. +#[derive(Debug, Constructor)] +pub struct FinalizedHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, + db_provider: Arc, +} + +#[async_trait] +impl EventHandler for FinalizedHandler +where + W: HeadRefStorageWriter + Send + Sync + 'static, +{ + async fn handle( + &self, + finalized_source_block: BlockInfo, + _state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = finalized_source_block.number, + "Processing finalized L1 update" + ); + + let result = self.inner_handle(finalized_source_block).await; + Metrics::record_block_processing(self.chain_id, Metrics::BLOCK_TYPE_FINALIZED, &result); + + result + } +} + +impl FinalizedHandler +where + W: HeadRefStorageWriter + Send + Sync + 'static, +{ + async fn inner_handle( + &self, + finalized_source_block: BlockInfo, + ) -> Result { + let finalized_derived_block = self + .db_provider + .update_finalized_using_source(finalized_source_block) + .inspect_err(|err| { + warn!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %finalized_source_block, + %err, + "Failed to update finalized block using source" + ); + })?; + + self.managed_node_sender + .send(ManagedNodeCommand::UpdateFinalized { block_id: finalized_derived_block.id() }) + .await + .map_err(|err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %finalized_derived_block, + %err, + "Failed to send finalized block update" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + })?; + Ok(finalized_derived_block) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeError, + }; + use alloy_primitives::B256; + use alloy_rpc_types_eth::BlockNumHash; + use async_trait::async_trait; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{HeadRefStorageWriter, StorageError}; + use kona_supervisor_types::{BlockSeal, OutputV0, Receipts}; + use mockall::mock; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl HeadRefStorageWriter for Db { + fn update_finalized_using_source( + &self, + block_info: BlockInfo, + ) -> Result; + + fn update_current_cross_unsafe( + &self, + block: &BlockInfo, + ) -> Result<(), StorageError>; + + fn update_current_cross_safe( + &self, + block: &BlockInfo, + ) -> Result; + } + ); + + #[tokio::test] + async fn test_handle_finalized_source_update_triggers() { + use crate::syncnode::ManagedNodeCommand; + + let mut mocknode = MockNode::new(); + let mut mockdb = MockDb::new(); + let mut state = ProcessorState::new(); + + let finalized_source_block = + BlockInfo { number: 99, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 1234578 }; + + // The finalized_derived_block returned by update_finalized_using_source + let finalized_derived_block = + BlockInfo { number: 5, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 1234578 }; + + // Expect update_finalized_using_source to be called with finalized_source_block + mockdb.expect_update_finalized_using_source().returning(move |block_info: BlockInfo| { + assert_eq!(block_info, finalized_source_block); + Ok(finalized_derived_block) + }); + + // Expect update_finalized to be called with the derived block's id + let finalized_derived_block_id = finalized_derived_block.id(); + mocknode.expect_update_finalized().returning(move |block_id| { + assert_eq!(block_id, finalized_derived_block_id); + Ok(()) + }); + + let writer = Arc::new(mockdb); + + // Set up the channel and spawn a task to handle the command + let (tx, mut rx) = mpsc::channel(8); + + let handler = FinalizedHandler::new( + 1, // chain_id + tx, writer, + ); + let result = handler.handle(finalized_source_block, &mut state).await; + assert!(result.is_ok()); + + // The handler should send the correct command + if let Some(ManagedNodeCommand::UpdateFinalized { block_id }) = rx.recv().await { + assert_eq!(block_id, finalized_derived_block.id()); + } else { + panic!("Expected UpdateFinalized command"); + } + } + + #[tokio::test] + async fn test_handle_finalized_source_update_db_error() { + let mut mocknode = MockNode::new(); + let mut mockdb = MockDb::new(); + let mut state = ProcessorState::new(); + + let finalized_source_block = + BlockInfo { number: 99, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 1234578 }; + + // DB returns error + mockdb + .expect_update_finalized_using_source() + .returning(|_block_info: BlockInfo| Err(StorageError::DatabaseNotInitialised)); + + // Managed node's update_finalized should NOT be called + mocknode.expect_update_finalized().never(); + + let writer = Arc::new(mockdb); + let (tx, mut rx) = mpsc::channel(8); + + let handler = FinalizedHandler::new( + 1, // chain_id + tx, writer, + ); + let result = handler.handle(finalized_source_block, &mut state).await; + assert!(result.is_err()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_finalized_source_update_managed_node_error() { + let mut mockdb = MockDb::new(); + let mut state = ProcessorState::new(); + + let finalized_source_block = + BlockInfo { number: 99, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 1234578 }; + + let finalized_derived_block = + BlockInfo { number: 5, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 1234578 }; + + // DB returns the derived block as usual + mockdb.expect_update_finalized_using_source().returning(move |block_info: BlockInfo| { + assert_eq!(block_info, finalized_source_block); + Ok(finalized_derived_block) + }); + + let writer = Arc::new(mockdb); + + // Set up the channel and immediately drop the receiver to simulate a send error + let (tx, rx) = mpsc::channel(8); + drop(rx); + + let handler = FinalizedHandler::new( + 1, // chain_id + tx, writer, + ); + let result = handler.handle(finalized_source_block, &mut state).await; + assert!(result.is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/invalidation.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/invalidation.rs new file mode 100644 index 0000000000000..093f46e42529f --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/invalidation.rs @@ -0,0 +1,620 @@ +use super::EventHandler; +use crate::{ + ChainProcessorError, LogIndexer, ProcessorState, + chain_processor::metrics::Metrics, + syncnode::{BlockProvider, ManagedNodeCommand}, +}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_interop::{BlockReplacement, DerivedRefPair}; +use kona_protocol::BlockInfo; +use kona_supervisor_metrics::observe_metrics_for_result_async; +use kona_supervisor_storage::{DerivationStorage, LogStorage, StorageRewinder}; +use kona_supervisor_types::BlockSeal; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{debug, error, trace, warn}; + +/// Handler for block invalidation events. +/// This handler processes block invalidation by rewinding the state and updating the managed node. +#[derive(Debug, Constructor)] +pub struct InvalidationHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, + db_provider: Arc, +} + +#[async_trait] +impl EventHandler for InvalidationHandler +where + W: DerivationStorage + StorageRewinder + Send + Sync + 'static, +{ + async fn handle( + &self, + block: BlockInfo, + state: &mut ProcessorState, + ) -> Result { + observe_metrics_for_result_async!( + Metrics::BLOCK_INVALIDATION_SUCCESS_TOTAL, + Metrics::BLOCK_INVALIDATION_ERROR_TOTAL, + Metrics::BLOCK_INVALIDATION_LATENCY_SECONDS, + Metrics::BLOCK_INVALIDATION_METHOD_INVALIDATE_BLOCK, + async { + self.inner_handle(block, state).await + }, + "chain_id" => self.chain_id.to_string() + ) + } +} + +impl InvalidationHandler +where + W: DerivationStorage + StorageRewinder + Send + Sync + 'static, +{ + async fn inner_handle( + &self, + block: BlockInfo, + state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Invalidating block" + ); + + if state.is_invalidated() { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Invalidated block already set, skipping" + ); + return Ok(block); + } + + let source_block = self.db_provider.derived_to_source(block.id()).inspect_err(|err| { + warn!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %block, + %err, + "Failed to get source block for invalidation" + ); + })?; + + self.db_provider.rewind(&block.id()).inspect_err(|err| { + warn!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %block, + %err, + "Failed to rewind state for invalidation" + ); + })?; + + let block_seal = BlockSeal::new(block.hash, block.number, block.timestamp); + self.managed_node_sender + .send(ManagedNodeCommand::InvalidateBlock { seal: block_seal }) + .await + .map_err(|err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %block, + %err, + "Failed to send invalidate block command to managed node" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + })?; + + state.set_invalidated(DerivedRefPair { source: source_block, derived: block }); + Ok(block) + } +} + +/// Handler for block replacement events. +/// This handler processes block replacements by resyncing the log and derivation storage. +#[derive(Debug, Constructor)] +pub struct ReplacementHandler { + chain_id: ChainId, + log_indexer: Arc>, + db_provider: Arc, +} + +#[async_trait] +impl EventHandler for ReplacementHandler +where + P: BlockProvider + 'static, + W: LogStorage + DerivationStorage + 'static, +{ + async fn handle( + &self, + replacement: BlockReplacement, + state: &mut ProcessorState, + ) -> Result { + observe_metrics_for_result_async!( + Metrics::BLOCK_REPLACEMENT_SUCCESS_TOTAL, + Metrics::BLOCK_REPLACEMENT_ERROR_TOTAL, + Metrics::BLOCK_REPLACEMENT_LATENCY_SECONDS, + Metrics::BLOCK_REPLACEMENT_METHOD_REPLACE_BLOCK, + async { + self.inner_handle(replacement, state).await + }, + "chain_id" => self.chain_id.to_string() + ) + } +} + +impl ReplacementHandler +where + P: BlockProvider + 'static, + W: LogStorage + DerivationStorage + 'static, +{ + async fn inner_handle( + &self, + replacement: BlockReplacement, + state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %replacement, + "Handling block replacement" + ); + + let invalidated_ref_pair = match state.get_invalidated() { + Some(block) => block, + None => { + debug!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %replacement, + "No invalidated block set, skipping replacement" + ); + return Ok(replacement.replacement); + } + }; + + if invalidated_ref_pair.derived.hash != replacement.invalidated { + debug!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + invalidated_block = %invalidated_ref_pair.derived, + replacement_block = %replacement.replacement, + "Invalidated block hash does not match replacement, skipping" + ); + return Ok(replacement.replacement); + } + + let derived_ref_pair = DerivedRefPair { + source: invalidated_ref_pair.source, + derived: replacement.replacement, + }; + + self.retry_with_resync_derived_block(derived_ref_pair).await?; + state.clear_invalidated(); + Ok(replacement.replacement) + } + + async fn retry_with_resync_derived_block( + &self, + derived_ref_pair: DerivedRefPair, + ) -> Result<(), ChainProcessorError> { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + derived_block_number = derived_ref_pair.derived.number, + "Retrying with resync of derived block" + ); + + self.log_indexer.process_and_store_logs(&derived_ref_pair.derived).await.inspect_err( + |err| { + error!( + target: "supervisor::chain_processor::log_indexer", + chain_id = self.chain_id, + %derived_ref_pair, + %err, + "Failed to process and store logs for derived block" + ); + }, + )?; + + self.db_provider.save_derived_block(derived_ref_pair).inspect_err(|err| { + error!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %derived_ref_pair, + %err, + "Failed to save derived block" + ); + })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeError, + }; + use alloy_primitives::B256; + use alloy_rpc_types_eth::BlockNumHash; + use async_trait::async_trait; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{ + DerivationStorageReader, DerivationStorageWriter, LogStorageReader, LogStorageWriter, + StorageError, + }; + use kona_supervisor_types::{BlockSeal, Log, OutputV0, Receipts}; + use mockall::mock; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageWriter for Db { + fn initialise_log_storage( + &self, + block: BlockInfo, + ) -> Result<(), StorageError>; + + fn store_block_logs( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError>; + } + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self,block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + + impl DerivationStorageWriter for Db { + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_derived_block( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_source_block( + &self, + source: BlockInfo, + ) -> Result<(), StorageError>; + } + + impl StorageRewinder for Db { + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError>; + } + ); + + #[tokio::test] + async fn test_handle_invalidate_block_already_set_skips() { + let mockdb = MockDb::new(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = ProcessorState::new(); + + let block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + + // Set up state: invalidated_block is already set + state.set_invalidated(DerivedRefPair { source: block, derived: block }); + + let writer = Arc::new(mockdb); + + let handler = InvalidationHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_invalidate_block_derived_to_source_error() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = ProcessorState::new(); + + let block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + + mockdb.expect_derived_to_source().returning(move |_id| Err(StorageError::FutureData)); + + let writer = Arc::new(mockdb); + + let handler = InvalidationHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(matches!(result, Err(ChainProcessorError::StorageError(StorageError::FutureData)))); + + // make sure invalidated_block is not set + assert!(state.get_invalidated().is_none()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_invalidate_block_rewind_error() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = ProcessorState::new(); + + let block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + + mockdb.expect_derived_to_source().returning(move |_id| Ok(block)); + mockdb.expect_rewind().returning(move |_to| Err(StorageError::DatabaseNotInitialised)); + + let writer = Arc::new(mockdb); + + let handler = InvalidationHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(matches!( + result, + Err(ChainProcessorError::StorageError(StorageError::DatabaseNotInitialised)) + )); + + // make sure invalidated_block is not set + assert!(state.get_invalidated().is_none()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_invalidate_block_managed_node_error() { + let mut mockdb = MockDb::new(); + let (tx, rx) = tokio::sync::mpsc::channel(1); + let mut state = ProcessorState::new(); + + let block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + + mockdb.expect_derived_to_source().returning(move |_id| Ok(block)); + mockdb.expect_rewind().returning(move |_to| Ok(())); + + let writer = Arc::new(mockdb); + + // Drop the receiver to simulate a send error to the managed node + drop(rx); + + let handler = InvalidationHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_err()); + + // make sure invalidated_block is not set + assert!(state.get_invalidated().is_none()); + } + + #[tokio::test] + async fn test_handle_invalidate_block_success_sets_invalidated() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let mut state = ProcessorState::new(); + + let derived_block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + let source_block = BlockInfo::new(B256::from([2u8; 32]), 41, B256::ZERO, 12344); + + mockdb.expect_derived_to_source().returning(move |_id| Ok(source_block)); + mockdb.expect_rewind().returning(move |_to| Ok(())); + + let writer = Arc::new(mockdb); + + // Spawn a background task to receive and check the command + let derived_block_clone = derived_block; + + let handler = InvalidationHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(derived_block, &mut state).await; + assert!(result.is_ok()); + + // make sure invalidated_block is set + let pair = state.get_invalidated().unwrap(); + assert_eq!(pair.derived, derived_block); + assert_eq!(pair.source, source_block); + + if let Some(ManagedNodeCommand::InvalidateBlock { seal }) = rx.recv().await { + assert_eq!(seal.hash, derived_block_clone.hash); + assert_eq!(seal.number, derived_block_clone.number); + assert_eq!(seal.timestamp, derived_block_clone.timestamp); + } else { + panic!("Expected InvalidateBlock command"); + } + } + + #[tokio::test] + async fn test_handle_block_replacement_no_invalidated_block() { + let mockdb = MockDb::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + let replacement = BlockReplacement { + invalidated: B256::from([1u8; 32]), + replacement: BlockInfo::new(B256::from([2u8; 32]), 43, B256::ZERO, 12346), + }; + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = ReplacementHandler::new( + 1, // chain_id + log_indexer, + writer, + ); + + let result = handler.handle(replacement, &mut state).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_block_replacement_invalidated_hash_mismatch() { + let mockdb = MockDb::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + let invalidated_block = BlockInfo::new(B256::from([3u8; 32]), 42, B256::ZERO, 12345); + let replacement = BlockReplacement { + invalidated: B256::from([1u8; 32]), // does not match invalidated_block.hash + replacement: BlockInfo::new(B256::from([2u8; 32]), 43, B256::ZERO, 12346), + }; + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + state.set_invalidated(DerivedRefPair { + source: invalidated_block, + derived: invalidated_block, + }); + + let handler = ReplacementHandler::new( + 1, // chain_id + log_indexer, + writer, + ); + + let result = handler.handle(replacement, &mut state).await; + assert!(result.is_ok()); + + // invalidated_block should remain set + let invalidated = state.get_invalidated(); + assert!(invalidated.is_some()); + } + + #[tokio::test] + async fn test_handle_block_replacement_success() { + let mut mockdb = MockDb::new(); + let mut mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + let source_block = BlockInfo::new(B256::from([1u8; 32]), 45, B256::ZERO, 12345); + let invalidated_block = BlockInfo::new(B256::from([1u8; 32]), 42, B256::ZERO, 12345); + let replacement_block = BlockInfo::new(B256::from([2u8; 32]), 42, B256::ZERO, 12346); + + mockdb.expect_save_derived_block().returning(move |_pair| Ok(())); + mockdb.expect_store_block_logs().returning(move |_block, _logs| Ok(())); + + mocknode.expect_fetch_receipts().returning(move |_block_hash| { + assert_eq!(_block_hash, replacement_block.hash); + Ok(Receipts::default()) + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + state.set_invalidated(DerivedRefPair { source: source_block, derived: invalidated_block }); + + let handler = ReplacementHandler::new( + 1, // chain_id + log_indexer, + writer, + ); + + let result = handler + .handle( + BlockReplacement { + invalidated: invalidated_block.hash, + replacement: replacement_block, + }, + &mut state, + ) + .await; + assert!(result.is_ok()); + + // invalidated_block should be cleared + assert!(state.get_invalidated().is_none()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/mod.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/mod.rs new file mode 100644 index 0000000000000..41b41d0fa5316 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/mod.rs @@ -0,0 +1,31 @@ +//! This module contains various event handlers for processing different types of chain events. +mod cross_chain; +mod finalized; +mod invalidation; +mod origin; +mod safe_block; +mod unsafe_block; + +pub use cross_chain::{CrossSafeHandler, CrossUnsafeHandler}; +pub use finalized::FinalizedHandler; +pub use invalidation::{InvalidationHandler, ReplacementHandler}; +pub use origin::OriginHandler; +pub use safe_block::SafeBlockHandler; +pub use unsafe_block::UnsafeBlockHandler; + +use crate::{ChainProcessorError, ProcessorState}; +use async_trait::async_trait; +use kona_protocol::BlockInfo; + +/// [`EventHandler`] trait defines the interface for handling different types of events in the chain +/// processor. Each handler will implement this trait to process specific events like block updates, +/// invalidations, etc. +#[async_trait] +pub trait EventHandler { + /// Handle the event with the given state. + async fn handle( + &self, + event: E, + state: &mut ProcessorState, + ) -> Result; +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/origin.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/origin.rs new file mode 100644 index 0000000000000..94f488adea810 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/origin.rs @@ -0,0 +1,271 @@ +use super::EventHandler; +use crate::{ChainProcessorError, ProcessorState, syncnode::ManagedNodeCommand}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DerivationStorageWriter, StorageError}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{debug, error, trace, warn}; + +/// Handler for origin updates in the chain. +#[derive(Debug, Constructor)] +pub struct OriginHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, + db_provider: Arc, +} + +#[async_trait] +impl EventHandler for OriginHandler +where + W: DerivationStorageWriter + Send + Sync + 'static, +{ + async fn handle( + &self, + origin: BlockInfo, + state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %origin, + "Processing derivation origin update" + ); + + if state.is_invalidated() { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %origin, + "Invalidated block set, skipping derivation origin update" + ); + return Ok(origin); + } + + match self.db_provider.save_source_block(origin) { + Ok(_) => Ok(origin), + Err(StorageError::BlockOutOfOrder) => { + debug!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %origin, + "Block out of order detected, resetting managed node" + ); + + self.managed_node_sender.send(ManagedNodeCommand::Reset {}).await.map_err( + |err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %origin, + %err, + "Failed to send reset command to managed node" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + }, + )?; + Ok(origin) + } + Err(err) => { + error!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %origin, + %err, + "Failed to save source block during derivation origin update" + ); + Err(err.into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeError, + }; + use alloy_primitives::B256; + use alloy_rpc_types_eth::BlockNumHash; + use async_trait::async_trait; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{DerivationStorageWriter, StorageError}; + use kona_supervisor_types::{BlockSeal, OutputV0, Receipts}; + use mockall::mock; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl DerivationStorageWriter for Db { + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_derived_block( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_source_block( + &self, + source: BlockInfo, + ) -> Result<(), StorageError>; + } + ); + + #[tokio::test] + async fn test_handle_derivation_origin_update_triggers() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut state = ProcessorState::new(); + + let origin = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + + let origin_clone = origin; + mockdb.expect_save_source_block().returning(move |block_info: BlockInfo| { + assert_eq!(block_info, origin_clone); + Ok(()) + }); + + let writer = Arc::new(mockdb); + + let handler = OriginHandler::new( + 1, // chain_id + tx, writer, + ); + + let result = handler.handle(origin, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derivation_origin_update_block_out_of_order_triggers_reset() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut state = ProcessorState::new(); + + let origin = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + + mockdb.expect_save_source_block().returning(|_| Err(StorageError::BlockOutOfOrder)); + + let writer = Arc::new(mockdb); + + let handler = OriginHandler::new(1, tx, writer); + + let result = handler.handle(origin, &mut state).await; + assert!(result.is_ok()); + + // The handler should send the reset command + if let Some(ManagedNodeCommand::Reset {}) = rx.recv().await { + // Command received successfully + } else { + panic!("Expected Reset command"); + } + } + + #[tokio::test] + async fn test_handle_derivation_origin_update_reset_fails() { + let mut mockdb = MockDb::new(); + let (tx, rx) = mpsc::channel(1); + let mut state = ProcessorState::new(); + + let origin = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + + mockdb.expect_save_source_block().returning(|_| Err(StorageError::BlockOutOfOrder)); + + let writer = Arc::new(mockdb); + + drop(rx); // Simulate a send error by dropping the receiver + + let handler = OriginHandler::new(1, tx, writer); + + let result = handler.handle(origin, &mut state).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_handle_derivation_origin_update_other_storage_error() { + let mut mockdb = MockDb::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut state = ProcessorState::new(); + + let origin = + BlockInfo { number: 42, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 123456 }; + + mockdb.expect_save_source_block().returning(|_| Err(StorageError::DatabaseNotInitialised)); + + let writer = Arc::new(mockdb); + + let handler = OriginHandler::new(1, tx, writer); + + let result = handler.handle(origin, &mut state).await; + assert!(result.is_err()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/safe_block.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/safe_block.rs new file mode 100644 index 0000000000000..ed210d03c4078 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/safe_block.rs @@ -0,0 +1,1026 @@ +use super::EventHandler; +use crate::{ + ChainProcessorError, LogIndexer, ProcessorState, + chain_processor::Metrics, + syncnode::{BlockProvider, ManagedNodeCommand}, +}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_interop::{DerivedRefPair, InteropValidator}; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DerivationStorage, LogStorage, StorageError, StorageRewinder}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{debug, error, info, trace, warn}; + +/// Handler for safe blocks. +#[derive(Debug, Constructor)] +pub struct SafeBlockHandler { + chain_id: ChainId, + managed_node_sender: mpsc::Sender, + db_provider: Arc, + validator: Arc, + log_indexer: Arc>, +} + +#[async_trait] +impl EventHandler for SafeBlockHandler +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + DerivationStorage + StorageRewinder + 'static, +{ + async fn handle( + &self, + derived_ref_pair: DerivedRefPair, + state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + %derived_ref_pair, + "Processing local safe derived block pair" + ); + + if state.is_invalidated() { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + "Invalidated block already set, skipping safe event processing" + ); + return Ok(derived_ref_pair.derived); + } + + let result = self.inner_handle(derived_ref_pair).await; + Metrics::record_block_processing(self.chain_id, Metrics::BLOCK_TYPE_LOCAL_SAFE, &result); + + result + } +} + +impl SafeBlockHandler +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + DerivationStorage + StorageRewinder + 'static, +{ + async fn inner_handle( + &self, + derived_ref_pair: DerivedRefPair, + ) -> Result { + if self.validator.is_post_interop(self.chain_id, derived_ref_pair.derived.timestamp) { + self.process_safe_derived_block(derived_ref_pair).await?; + return Ok(derived_ref_pair.derived); + } + + if self.validator.is_interop_activation_block(self.chain_id, derived_ref_pair.derived) { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + "Initialising derivation storage for interop activation block" + ); + + self.db_provider.initialise_derivation_storage(derived_ref_pair).inspect_err( + |err| { + error!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %err, + "Failed to initialise derivation storage for interop activation block" + ); + }, + )?; + return Ok(derived_ref_pair.derived); + } + + Ok(derived_ref_pair.derived) + } + + async fn process_safe_derived_block( + &self, + derived_ref_pair: DerivedRefPair, + ) -> Result<(), ChainProcessorError> { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + "Processing safe derived block" + ); + + match self.db_provider.save_derived_block(derived_ref_pair) { + Ok(_) => Ok(()), + Err(StorageError::BlockOutOfOrder) => { + debug!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + "Block out of order detected, resetting managed node" + ); + + self.managed_node_sender.send(ManagedNodeCommand::Reset {}).await.map_err( + |err| { + warn!( + target: "supervisor::chain_processor::managed_node", + chain_id = self.chain_id, + %err, + "Failed to send reset command to managed node" + ); + ChainProcessorError::ChannelSendFailed(err.to_string()) + }, + )?; + Ok(()) + } + Err(StorageError::ReorgRequired) => { + info!( + target: "supervisor::chain_processor", + chain = self.chain_id, + derived_block = %derived_ref_pair.derived, + "Local derivation conflict detected — rewinding" + ); + + self.rewind_log_storage(&derived_ref_pair.derived).await?; + self.retry_with_resync_derived_block(derived_ref_pair).await?; + Ok(()) + } + Err(StorageError::FutureData) => { + debug!( + target: "supervisor::chain_processor", + chain = self.chain_id, + derived_block = %derived_ref_pair.derived, + "Future data detected — retrying with resync" + ); + + self.retry_with_resync_derived_block(derived_ref_pair).await + } + Err(err) => { + error!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + %err, + "Failed to save derived block pair" + ); + Err(err.into()) + } + } + } + + async fn rewind_log_storage( + &self, + derived_block: &BlockInfo, + ) -> Result<(), ChainProcessorError> { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = derived_block.number, + "Rewinding log storage for derived block" + ); + + let log_block = self.db_provider.get_block(derived_block.number).inspect_err(|err| { + warn!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + block_number = derived_block.number, + %err, + "Failed to get block for rewinding log storage" + ); + })?; + + self.db_provider.rewind_log_storage(&log_block.id()).inspect_err(|err| { + warn!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + block_number = derived_block.number, + %err, + "Failed to rewind log storage for derived block" + ); + })?; + Ok(()) + } + + async fn retry_with_resync_derived_block( + &self, + derived_ref_pair: DerivedRefPair, + ) -> Result<(), ChainProcessorError> { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + derived_block_number = derived_ref_pair.derived.number, + "Retrying with resync of derived block" + ); + + self.log_indexer.process_and_store_logs(&derived_ref_pair.derived).await.inspect_err( + |err| { + error!( + target: "supervisor::chain_processor::log_indexer", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + %err, + "Error resyncing logs for derived block" + ); + }, + )?; + + self.db_provider.save_derived_block(derived_ref_pair).inspect_err(|err| { + error!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + block_number = derived_ref_pair.derived.number, + %err, + "Error saving derived block after resync" + ); + })?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeError, + }; + use alloy_primitives::B256; + use alloy_rpc_types_eth::BlockNumHash; + use async_trait::async_trait; + use kona_interop::{DerivedRefPair, InteropValidationError}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{ + DerivationStorageReader, DerivationStorageWriter, HeadRefStorageWriter, LogStorageReader, + LogStorageWriter, StorageError, + }; + use kona_supervisor_types::{BlockSeal, Log, OutputV0, Receipts}; + use mockall::mock; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageWriter for Db { + fn initialise_log_storage( + &self, + block: BlockInfo, + ) -> Result<(), StorageError>; + + fn store_block_logs( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError>; + } + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self,block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + + impl DerivationStorageWriter for Db { + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_derived_block( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_source_block( + &self, + source: BlockInfo, + ) -> Result<(), StorageError>; + } + + impl HeadRefStorageWriter for Db { + fn update_finalized_using_source( + &self, + block_info: BlockInfo, + ) -> Result; + + fn update_current_cross_unsafe( + &self, + block: &BlockInfo, + ) -> Result<(), StorageError>; + + fn update_current_cross_safe( + &self, + block: &BlockInfo, + ) -> Result; + } + + impl StorageRewinder for Db { + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError>; + } + ); + + mock! ( + #[derive(Debug)] + pub Validator {} + + impl InteropValidator for Validator { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; + } + ); + + #[tokio::test] + async fn test_handle_derived_event_skips_if_invalidated() { + let mockdb = MockDb::new(); + let mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + // Simulate invalidated state + state.set_invalidated(DerivedRefPair { + source: BlockInfo { + number: 1, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 2, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + }); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, + }, + }; + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new(1, tx, writer, Arc::new(mockvalidator), log_indexer); + + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_pre_interop() { + let mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| false); + mockvalidator.expect_is_interop_activation_block().returning(|_, _| false); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 999, + }, + }; + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_post_interop() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, + }, + }; + + mockdb.expect_save_derived_block().returning(move |_pair: DerivedRefPair| { + assert_eq!(_pair, block_pair); + Ok(()) + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_interop_activation() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| false); + mockvalidator.expect_is_interop_activation_block().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1001, + }, + }; + + mockdb.expect_initialise_derivation_storage().returning(move |_pair: DerivedRefPair| { + assert_eq!(_pair, block_pair); + Ok(()) + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_block_out_of_order_triggers_reset() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + // Simulate BlockOutOfOrder error + mockdb + .expect_save_derived_block() + .returning(move |_pair: DerivedRefPair| Err(StorageError::BlockOutOfOrder)); + + // Expect reset to be called + mocknode.expect_reset().returning(|| Ok(())); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure reset command was sent + if let Some(cmd) = rx.recv().await { + assert!(matches!(cmd, ManagedNodeCommand::Reset {})); + } else { + panic!("Expected reset command to be sent"); + } + } + + #[tokio::test] + async fn test_handle_derived_event_block_out_of_order_triggers_reset_error() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + // Simulate BlockOutOfOrder error + mockdb + .expect_save_derived_block() + .returning(move |_pair: DerivedRefPair| Err(StorageError::BlockOutOfOrder)); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + drop(rx); // Simulate a send error by dropping the receiver + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_block_triggers_reorg() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + let mut seq = mockall::Sequence::new(); + // Simulate ReorgRequired error + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Err(StorageError::ReorgRequired)); + + mockdb.expect_get_block().returning(move |num| { + Ok(BlockInfo { + number: num, + hash: B256::random(), // different hash from safe derived block + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }) + }); + + // Expect reorg on log storage + mockdb.expect_rewind_log_storage().returning(|_block_id| Ok(())); + mockdb.expect_store_block_logs().returning(|_block_id, _logs| Ok(())); + mocknode.expect_fetch_receipts().returning(|_receipts| Ok(Receipts::default())); + + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Ok(())); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_block_triggers_reorg_block_error() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + let mut seq = mockall::Sequence::new(); + // Simulate ReorgRequired error + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Err(StorageError::ReorgRequired)); + + mockdb.expect_get_block().returning(move |_| Err(StorageError::DatabaseNotInitialised)); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await.unwrap_err(); + assert!(matches!( + result, + ChainProcessorError::StorageError(StorageError::DatabaseNotInitialised) + )); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_block_triggers_reorg_rewind_error() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + let mut seq = mockall::Sequence::new(); + // Simulate ReorgRequired error + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Err(StorageError::ReorgRequired)); + + mockdb.expect_get_block().returning(move |num| { + Ok(BlockInfo { + number: num, + hash: B256::random(), // different hash from safe derived block + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }) + }); + + // Expect reorg on log storage + mockdb + .expect_rewind_log_storage() + .returning(|_block_id| Err(StorageError::DatabaseNotInitialised)); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await; + assert!(matches!( + result, + Err(ChainProcessorError::StorageError(StorageError::DatabaseNotInitialised)) + )); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_block_triggers_resync() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mut mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + let mut seq = mockall::Sequence::new(); + // Simulate ReorgRequired error + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Err(StorageError::FutureData)); + + mockdb.expect_get_block().returning(move |num| { + Ok(BlockInfo { + number: num, + hash: B256::random(), // different hash from safe derived block + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }) + }); + + mockdb.expect_store_block_logs().returning(|_block_id, _logs| Ok(())); + + mocknode.expect_fetch_receipts().returning(|_receipts| Ok(Receipts::default())); + + mockdb + .expect_save_derived_block() + .times(1) + .in_sequence(&mut seq) + .returning(move |_pair: DerivedRefPair| Ok(())); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_ok()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_derived_event_other_error() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let (tx, mut rx) = mpsc::channel(1); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + let block_pair = DerivedRefPair { + source: BlockInfo { + number: 123, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 0, + }, + derived: BlockInfo { + number: 1234, + hash: B256::ZERO, + parent_hash: B256::ZERO, + timestamp: 1003, // post-interop + }, + }; + + // Simulate a different error + mockdb + .expect_save_derived_block() + .returning(move |_pair: DerivedRefPair| Err(StorageError::DatabaseNotInitialised)); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node.clone()), writer.clone())); + + let handler = SafeBlockHandler::new( + 1, // chain_id + tx, + writer, + Arc::new(mockvalidator), + log_indexer, + ); + let result = handler.handle(block_pair, &mut state).await; + assert!(result.is_err()); + + // Ensure no command was sent + assert!(rx.try_recv().is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/handlers/unsafe_block.rs b/kona/crates/supervisor/core/src/chain_processor/handlers/unsafe_block.rs new file mode 100644 index 0000000000000..a9a5501a14780 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/handlers/unsafe_block.rs @@ -0,0 +1,315 @@ +use super::EventHandler; +use crate::{ + ChainProcessorError, LogIndexer, ProcessorState, chain_processor::Metrics, + syncnode::BlockProvider, +}; +use alloy_primitives::ChainId; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_interop::InteropValidator; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::LogStorage; +use std::sync::Arc; +use tracing::{error, trace}; + +/// Handler for unsafe blocks. +/// This handler processes unsafe blocks by syncing logs and initializing log storage. +#[derive(Debug, Constructor)] +pub struct UnsafeBlockHandler { + chain_id: ChainId, + validator: Arc, + db_provider: Arc, + log_indexer: Arc>, +} + +#[async_trait] +impl EventHandler for UnsafeBlockHandler +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + 'static, +{ + async fn handle( + &self, + block: BlockInfo, + state: &mut ProcessorState, + ) -> Result { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Processing unsafe block" + ); + + if state.is_invalidated() { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Invalidated block already set, skipping unsafe event processing" + ); + return Ok(block); + } + + let result = self.inner_handle(block).await; + Metrics::record_block_processing(self.chain_id, Metrics::BLOCK_TYPE_LOCAL_UNSAFE, &result); + + result + } +} + +impl UnsafeBlockHandler +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + 'static, +{ + async fn inner_handle(&self, block: BlockInfo) -> Result { + if self.validator.is_post_interop(self.chain_id, block.timestamp) { + self.log_indexer.clone().sync_logs(block); + return Ok(block); + } + + if self.validator.is_interop_activation_block(self.chain_id, block) { + trace!( + target: "supervisor::chain_processor", + chain_id = self.chain_id, + block_number = block.number, + "Initialising log storage for interop activation block" + ); + + self.db_provider.initialise_log_storage(block).inspect_err(|err| { + error!( + target: "supervisor::chain_processor::db", + chain_id = self.chain_id, + %block, + %err, + "Failed to initialise log storage for interop activation block" + ); + })?; + return Ok(block); + } + + Ok(block) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ProcessorState, + syncnode::{BlockProvider, ManagedNodeError}, + }; + use alloy_primitives::B256; + use kona_interop::{DerivedRefPair, InteropValidationError}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{LogStorageReader, LogStorageWriter, StorageError}; + use kona_supervisor_types::{Log, Receipts}; + use mockall::mock; + use std::sync::Arc; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageWriter for Db { + fn initialise_log_storage( + &self, + block: BlockInfo, + ) -> Result<(), StorageError>; + + fn store_block_logs( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError>; + } + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self,block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + ); + + mock! ( + #[derive(Debug)] + pub Validator {} + + impl InteropValidator for Validator { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; + } + ); + + #[tokio::test] + async fn test_handle_unsafe_event_skips_if_invalidated() { + let mockdb = MockDb::new(); + let mockvalidator = MockValidator::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + // Simulate invalidated state + state.set_invalidated(DerivedRefPair { + source: BlockInfo::new(B256::ZERO, 1, B256::ZERO, 0), + derived: BlockInfo::new(B256::ZERO, 2, B256::ZERO, 0), + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + let handler = UnsafeBlockHandler::new(1, Arc::new(mockvalidator), writer, log_indexer); + + let block = BlockInfo::new(B256::ZERO, 123, B256::ZERO, 10); + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_unsafe_event_pre_interop() { + let mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| false); + mockvalidator.expect_is_interop_activation_block().returning(|_, _| false); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + let handler = UnsafeBlockHandler::new( + 1, // chain_id + Arc::new(mockvalidator), + writer, + log_indexer, + ); + + // Pre-interop block (timestamp < 1000) + let block = BlockInfo::new(B256::ZERO, 123, B256::ZERO, 10); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_unsafe_event_post_interop() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let mut mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| true); + + // Send unsafe block event + let block = BlockInfo::new(B256::ZERO, 123, B256::ZERO, 1003); + + mockdb.expect_store_block_logs().returning(move |_block, _log| Ok(())); + mocknode.expect_fetch_receipts().returning(move |block_hash| { + assert!(block_hash == block.hash); + Ok(Receipts::default()) + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + let handler = UnsafeBlockHandler::new( + 1, // chain_id + Arc::new(mockvalidator), + writer, + log_indexer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_unsafe_event_interop_activation() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| false); + mockvalidator.expect_is_interop_activation_block().returning(|_, _| true); + + // Block that triggers interop activation + let block = BlockInfo::new(B256::ZERO, 123, B256::ZERO, 1001); + + mockdb.expect_initialise_log_storage().times(1).returning(move |b| { + assert_eq!(b, block); + Ok(()) + }); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + // Create a mock log indexer + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + let handler = UnsafeBlockHandler::new( + 1, // chain_id + Arc::new(mockvalidator), + writer, + log_indexer, + ); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_unsafe_event_interop_activation_init_fails() { + let mut mockdb = MockDb::new(); + let mut mockvalidator = MockValidator::new(); + let mocknode = MockNode::new(); + let mut state = ProcessorState::new(); + + mockvalidator.expect_is_post_interop().returning(|_, _| false); + mockvalidator.expect_is_interop_activation_block().returning(|_, _| true); + + let block = BlockInfo::new(B256::ZERO, 123, B256::ZERO, 1001); + + mockdb + .expect_initialise_log_storage() + .times(1) + .returning(move |_b| Err(StorageError::ConflictError)); + + let writer = Arc::new(mockdb); + let managed_node = Arc::new(mocknode); + let log_indexer = Arc::new(LogIndexer::new(1, Some(managed_node), writer.clone())); + + let handler = UnsafeBlockHandler::new(1, Arc::new(mockvalidator), writer, log_indexer); + + let result = handler.handle(block, &mut state).await; + assert!(result.is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/metrics.rs b/kona/crates/supervisor/core/src/chain_processor/metrics.rs new file mode 100644 index 0000000000000..f4bbcd13b3383 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/metrics.rs @@ -0,0 +1,288 @@ +use crate::ChainProcessorError; +use alloy_primitives::ChainId; +use kona_protocol::BlockInfo; +use std::time::SystemTime; +use tracing::error; + +#[derive(Debug)] +pub(crate) struct Metrics; + +impl Metrics { + // --- Metric Names --- + /// Identifier for block processing success. + /// Labels: `chain_id`, `type` + pub(crate) const BLOCK_PROCESSING_SUCCESS_TOTAL: &'static str = + "supervisor_block_processing_success_total"; + + /// Identifier for block processing errors. + /// Labels: `chain_id`, `type` + pub(crate) const BLOCK_PROCESSING_ERROR_TOTAL: &'static str = + "supervisor_block_processing_error_total"; + + /// Identifier for block processing latency. + /// Labels: `chain_id`, `type` + pub(crate) const BLOCK_PROCESSING_LATENCY_SECONDS: &'static str = + "supervisor_block_processing_latency_seconds"; + + pub(crate) const BLOCK_TYPE_LOCAL_UNSAFE: &'static str = "local_unsafe"; + pub(crate) const BLOCK_TYPE_CROSS_UNSAFE: &'static str = "cross_unsafe"; + pub(crate) const BLOCK_TYPE_LOCAL_SAFE: &'static str = "local_safe"; + pub(crate) const BLOCK_TYPE_CROSS_SAFE: &'static str = "cross_safe"; + pub(crate) const BLOCK_TYPE_FINALIZED: &'static str = "finalized"; + + // --- Block Invalidation Metric Names --- + /// Identifier for block invalidation success. + /// Labels: `chain_id` + pub(crate) const BLOCK_INVALIDATION_SUCCESS_TOTAL: &'static str = + "supervisor_block_invalidation_success_total"; + + /// Identifier for block invalidation errors. + /// Labels: `chain_id` + pub(crate) const BLOCK_INVALIDATION_ERROR_TOTAL: &'static str = + "supervisor_block_invalidation_error_total"; + + /// Identifier for block invalidation latency. + /// Labels: `chain_id` + pub(crate) const BLOCK_INVALIDATION_LATENCY_SECONDS: &'static str = + "supervisor_block_invalidation_latency_seconds"; + + pub(crate) const BLOCK_INVALIDATION_METHOD_INVALIDATE_BLOCK: &'static str = "invalidate_block"; + + // --- Block Replacement Metric Names --- + /// Identifier for block replacement success. + /// Labels: `chain_id` + pub(crate) const BLOCK_REPLACEMENT_SUCCESS_TOTAL: &'static str = + "supervisor_block_replacement_success_total"; + + /// Identifier for block replacement errors. + /// Labels: `chain_id` + pub(crate) const BLOCK_REPLACEMENT_ERROR_TOTAL: &'static str = + "supervisor_block_replacement_error_total"; + + /// Identifier for block replacement latency. + /// Labels: `chain_id` + pub(crate) const BLOCK_REPLACEMENT_LATENCY_SECONDS: &'static str = + "supervisor_block_replacement_latency_seconds"; + + pub(crate) const BLOCK_REPLACEMENT_METHOD_REPLACE_BLOCK: &'static str = "replace_block"; + + // --- Safety Head Ref Metric Names --- + /// Identifier for safety head ref. + /// Labels: `chain_id`, `type` + pub(crate) const SAFETY_HEAD_REF_LABELS: &'static str = "supervisor_safety_head_ref_labels"; + + pub(crate) fn init(chain_id: ChainId) { + Self::describe(); + Self::zero(chain_id); + } + + fn describe() { + metrics::describe_counter!( + Self::BLOCK_PROCESSING_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successfully processed blocks in the supervisor", + ); + + metrics::describe_counter!( + Self::BLOCK_PROCESSING_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of errors encountered while processing blocks in the supervisor", + ); + + metrics::describe_histogram!( + Self::BLOCK_PROCESSING_LATENCY_SECONDS, + metrics::Unit::Seconds, + "Latency for processing in the supervisor", + ); + + metrics::describe_counter!( + Self::BLOCK_INVALIDATION_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successfully invalidated blocks in the supervisor", + ); + + metrics::describe_counter!( + Self::BLOCK_INVALIDATION_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of errors encountered while invalidating blocks in the supervisor", + ); + + metrics::describe_histogram!( + Self::BLOCK_INVALIDATION_LATENCY_SECONDS, + metrics::Unit::Seconds, + "Latency for invalidating blocks in the supervisor", + ); + + metrics::describe_counter!( + Self::BLOCK_REPLACEMENT_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successfully replaced blocks in the supervisor", + ); + + metrics::describe_counter!( + Self::BLOCK_REPLACEMENT_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of errors encountered while replacing blocks in the supervisor", + ); + + metrics::describe_histogram!( + Self::BLOCK_REPLACEMENT_LATENCY_SECONDS, + metrics::Unit::Seconds, + "Latency for replacing blocks in the supervisor", + ); + + metrics::describe_gauge!(Self::SAFETY_HEAD_REF_LABELS, "Supervisor safety head ref",); + } + + fn zero_block_processing(chain_id: ChainId, block_type: &'static str) { + metrics::counter!( + Self::BLOCK_PROCESSING_SUCCESS_TOTAL, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::counter!( + Self::BLOCK_PROCESSING_ERROR_TOTAL, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::histogram!( + Self::BLOCK_PROCESSING_LATENCY_SECONDS, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .record(0.0); + } + + fn zero_safety_head_ref(chain_id: ChainId, head_type: &'static str) { + metrics::gauge!( + Self::SAFETY_HEAD_REF_LABELS, + "type" => head_type, + "chain_id" => chain_id.to_string(), + ) + .set(0.0); + } + + fn zero_block_invalidation(chain_id: ChainId) { + metrics::counter!( + Self::BLOCK_INVALIDATION_SUCCESS_TOTAL, + "method" => Self::BLOCK_INVALIDATION_METHOD_INVALIDATE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::counter!( + Self::BLOCK_INVALIDATION_ERROR_TOTAL, + "method" => Self::BLOCK_INVALIDATION_METHOD_INVALIDATE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::histogram!( + Self::BLOCK_INVALIDATION_LATENCY_SECONDS, + "method" => Self::BLOCK_INVALIDATION_METHOD_INVALIDATE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .record(0.0); + } + + fn zero_block_replacement(chain_id: ChainId) { + metrics::counter!( + Self::BLOCK_REPLACEMENT_SUCCESS_TOTAL, + "method" => Self::BLOCK_REPLACEMENT_METHOD_REPLACE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::counter!( + Self::BLOCK_REPLACEMENT_ERROR_TOTAL, + "method" => Self::BLOCK_REPLACEMENT_METHOD_REPLACE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::histogram!( + Self::BLOCK_REPLACEMENT_LATENCY_SECONDS, + "method" => Self::BLOCK_REPLACEMENT_METHOD_REPLACE_BLOCK, + "chain_id" => chain_id.to_string() + ) + .record(0.0); + } + + fn zero(chain_id: ChainId) { + Self::zero_block_processing(chain_id, Self::BLOCK_TYPE_LOCAL_UNSAFE); + Self::zero_block_processing(chain_id, Self::BLOCK_TYPE_CROSS_UNSAFE); + Self::zero_block_processing(chain_id, Self::BLOCK_TYPE_LOCAL_SAFE); + Self::zero_block_processing(chain_id, Self::BLOCK_TYPE_CROSS_SAFE); + Self::zero_block_processing(chain_id, Self::BLOCK_TYPE_FINALIZED); + + Self::zero_block_invalidation(chain_id); + Self::zero_block_replacement(chain_id); + + Self::zero_safety_head_ref(chain_id, Self::BLOCK_TYPE_LOCAL_UNSAFE); + Self::zero_safety_head_ref(chain_id, Self::BLOCK_TYPE_CROSS_UNSAFE); + Self::zero_safety_head_ref(chain_id, Self::BLOCK_TYPE_LOCAL_SAFE); + Self::zero_safety_head_ref(chain_id, Self::BLOCK_TYPE_CROSS_SAFE); + Self::zero_safety_head_ref(chain_id, Self::BLOCK_TYPE_FINALIZED); + } + + /// Records metrics for a block processing operation. + /// Takes the result of the processing and extracts the block info if successful. + pub(crate) fn record_block_processing( + chain_id: ChainId, + block_type: &'static str, + result: &Result, + ) { + match result { + Ok(block) => { + metrics::counter!( + Self::BLOCK_PROCESSING_SUCCESS_TOTAL, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .increment(1); + + metrics::gauge!( + Self::SAFETY_HEAD_REF_LABELS, + "type" => block_type, + "chain_id" => chain_id.to_string(), + ) + .set(block.number as f64); + + // Calculate latency + match SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => { + let now = duration.as_secs_f64(); + let latency = now - block.timestamp as f64; + + metrics::histogram!( + Self::BLOCK_PROCESSING_LATENCY_SECONDS, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .record(latency); + } + Err(err) => { + error!( + target: "chain_processor", + chain_id = chain_id, + %err, + "SystemTime error when recording block processing latency" + ); + } + } + } + Err(_) => { + metrics::counter!( + Self::BLOCK_PROCESSING_ERROR_TOTAL, + "type" => block_type, + "chain_id" => chain_id.to_string() + ) + .increment(1); + } + } + } +} diff --git a/kona/crates/supervisor/core/src/chain_processor/mod.rs b/kona/crates/supervisor/core/src/chain_processor/mod.rs new file mode 100644 index 0000000000000..03d77150365ac --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/mod.rs @@ -0,0 +1,17 @@ +//! Chain Processor Module +//! This module implements the Chain Processor, which manages the nodes and process events per +//! chain. It provides a structured way to handle tasks, manage chains, and process blocks +//! in a supervisor environment. +mod error; +pub use error::ChainProcessorError; + +mod chain; +pub use chain::ChainProcessor; + +mod metrics; +pub(crate) use metrics::Metrics; + +mod state; +pub use state::ProcessorState; + +pub mod handlers; diff --git a/kona/crates/supervisor/core/src/chain_processor/state/mod.rs b/kona/crates/supervisor/core/src/chain_processor/state/mod.rs new file mode 100644 index 0000000000000..d82378134ab9a --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/state/mod.rs @@ -0,0 +1,2 @@ +mod processor; +pub use processor::ProcessorState; diff --git a/kona/crates/supervisor/core/src/chain_processor/state/processor.rs b/kona/crates/supervisor/core/src/chain_processor/state/processor.rs new file mode 100644 index 0000000000000..db22d93c25de8 --- /dev/null +++ b/kona/crates/supervisor/core/src/chain_processor/state/processor.rs @@ -0,0 +1,40 @@ +use kona_interop::DerivedRefPair; + +/// This module contains the state management for the chain processor. +/// It provides a way to track the invalidated blocks and manage the state of the chain processor +#[derive(Debug, Default)] +pub struct ProcessorState { + invalidated_block: Option, +} + +impl ProcessorState { + /// Creates a new instance of [`ProcessorState`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns `true` if the state is invalidated, otherwise `false`. + pub const fn is_invalidated(&self) -> bool { + self.invalidated_block.is_some() + } + + /// Returns the invalidated block if it exists. + pub const fn get_invalidated(&self) -> Option { + self.invalidated_block + } + + /// Sets the invalidated block to the given pair if it is not already set. + pub const fn set_invalidated(&mut self, pair: DerivedRefPair) -> bool { + if self.invalidated_block.is_some() { + return false; // Already set + } + // Set the invalidated block + self.invalidated_block = Some(pair); + true + } + + /// Clears the invalidated block. + pub const fn clear_invalidated(&mut self) { + self.invalidated_block = None; + } +} diff --git a/kona/crates/supervisor/core/src/config/core_config.rs b/kona/crates/supervisor/core/src/config/core_config.rs new file mode 100644 index 0000000000000..0c3e0f02b6d5e --- /dev/null +++ b/kona/crates/supervisor/core/src/config/core_config.rs @@ -0,0 +1,165 @@ +use super::RollupConfigSet; +use crate::syncnode::ClientConfig; +use alloy_primitives::ChainId; +use derive_more::Constructor; +use kona_interop::{DependencySet, InteropValidationError, InteropValidator}; +use kona_protocol::BlockInfo; +use std::{net::SocketAddr, path::PathBuf}; + +/// Configuration for the Supervisor service. +#[derive(Debug, Clone, Constructor)] +pub struct Config { + /// The URL of the L1 RPC endpoint. + pub l1_rpc: String, + + /// L2 consensus nodes configuration. + pub l2_consensus_nodes_config: Vec, + + /// Directory where the database files are stored. + pub datadir: PathBuf, + + /// The socket address for the RPC server to listen on. + pub rpc_addr: SocketAddr, + + /// Whether to enable the Supervisor Admin API. + pub enable_admin_api: bool, + + /// The loaded dependency set configuration. + pub dependency_set: DependencySet, + + /// The rollup configuration set. + pub rollup_config_set: RollupConfigSet, +} + +impl InteropValidator for Config { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError> { + // Interop must be active on both chains at the relevant times + if !self.rollup_config_set.is_post_interop(initiating_chain_id, initiating_timestamp) || + !self.rollup_config_set.is_post_interop(executing_chain_id, executing_timestamp) + { + return Err(InteropValidationError::InteropNotEnabled); + } + + // Executing timestamp must not be earlier than the initiating timestamp + if initiating_timestamp > executing_timestamp { + return Err(InteropValidationError::InvalidTimestampInvariant { + initiating: initiating_timestamp, + executing: executing_timestamp, + }); + } + + // Ensure the message has not expired by the time of execution + let expiry_window = self.dependency_set.get_message_expiry_window(); + let expires_at = initiating_timestamp.saturating_add(expiry_window); + let execution_deadline = executing_timestamp.saturating_add(timeout.unwrap_or(0)); + + if expires_at < execution_deadline { + return Err(InteropValidationError::InvalidInteropTimestamp(executing_timestamp)); + } + + Ok(()) + } + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool { + self.rollup_config_set.is_post_interop(chain_id, timestamp) + } + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool { + self.rollup_config_set.is_interop_activation_block(chain_id, block) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::RollupConfig; + use kona_interop::DependencySet; + use std::{collections::HashMap, net::SocketAddr, path::PathBuf}; + + fn mock_rollup_config_set() -> RollupConfigSet { + let chain1 = + RollupConfig { genesis: Default::default(), block_time: 2, interop_time: Some(100) }; + let chain2 = + RollupConfig { genesis: Default::default(), block_time: 2, interop_time: Some(105) }; + let mut config_set = HashMap::::new(); + config_set.insert(1, chain1); + config_set.insert(2, chain2); + + RollupConfigSet { rollups: config_set } + } + + fn mock_config() -> Config { + Config { + l1_rpc: Default::default(), + l2_consensus_nodes_config: vec![], + datadir: PathBuf::new(), + rpc_addr: SocketAddr::from(([127, 0, 0, 1], 8545)), + enable_admin_api: false, + dependency_set: DependencySet { + dependencies: Default::default(), + override_message_expiry_window: Some(10), + }, + rollup_config_set: mock_rollup_config_set(), + } + } + + #[test] + fn test_valid_case() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 2, 202, None); + assert_eq!(res, Ok(())); + } + #[test] + fn test_valid_with_timeout() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 2, 202, Some(5)); + assert_eq!(res, Ok(())); + } + + #[test] + fn test_chain_id_doesnt_exist() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 3, 215, Some(20)); + assert_eq!(res, Err(InteropValidationError::InteropNotEnabled)); + } + #[test] + fn test_interop_not_enabled_chain1() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 100, 2, 215, Some(20)); + assert_eq!(res, Err(InteropValidationError::InteropNotEnabled)); + } + + #[test] + fn test_invalid_timestamp_invariant() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 2, 195, Some(20)); + assert_eq!( + res, + Err(InteropValidationError::InvalidTimestampInvariant { + initiating: 200, + executing: 195 + }) + ); + } + + #[test] + fn test_expired_message_with_timeout() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 2, 250, Some(20)); + assert_eq!(res, Err(InteropValidationError::InvalidInteropTimestamp(250))); + } + + #[test] + fn test_expired_message_without_timeout() { + let cfg = mock_config(); + let res = cfg.validate_interop_timestamps(1, 200, 2, 215, None); + assert_eq!(res, Err(InteropValidationError::InvalidInteropTimestamp(215))); + } +} diff --git a/kona/crates/supervisor/core/src/config/mod.rs b/kona/crates/supervisor/core/src/config/mod.rs new file mode 100644 index 0000000000000..2841e240874f3 --- /dev/null +++ b/kona/crates/supervisor/core/src/config/mod.rs @@ -0,0 +1,7 @@ +//! Configuration management for the supervisor. + +mod rollup_config_set; +pub use rollup_config_set::{Genesis, RollupConfig, RollupConfigSet}; + +mod core_config; +pub use core_config::Config; diff --git a/kona/crates/supervisor/core/src/config/rollup_config_set.rs b/kona/crates/supervisor/core/src/config/rollup_config_set.rs new file mode 100644 index 0000000000000..fa5d03ee8e826 --- /dev/null +++ b/kona/crates/supervisor/core/src/config/rollup_config_set.rs @@ -0,0 +1,191 @@ +use alloy_primitives::{B256, ChainId}; +use kona_genesis::ChainGenesis; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use std::collections::HashMap; + +use crate::SupervisorError; + +/// Genesis provides the genesis information relevant for Interop. +#[derive(Debug, Default, Clone)] +pub struct Genesis { + /// The L1 [`BlockInfo`] that the rollup starts after. + pub l1: BlockInfo, + /// The L2 [`BlockInfo`] that the rollup starts from. + pub l2: BlockInfo, +} + +impl Genesis { + /// Creates a new Genesis with the given L1 and L2 block seals. + pub const fn new(l1: BlockInfo, l2: BlockInfo) -> Self { + Self { l1, l2 } + } + + /// Creates a new Genesis from a RollupConfig. + pub const fn new_from_rollup_genesis(genesis: ChainGenesis, l1_block: BlockInfo) -> Self { + Self { + l1: l1_block, + l2: BlockInfo::new(genesis.l2.hash, genesis.l2.number, B256::ZERO, genesis.l2_time), + } + } + + /// Returns the genesis as a [`DerivedRefPair`]. + pub const fn get_derived_pair(&self) -> DerivedRefPair { + DerivedRefPair { derived: self.l2, source: self.l1 } + } +} + +/// RollupConfig contains the configuration for the Optimism rollup. +#[derive(Debug, Default, Clone)] +pub struct RollupConfig { + /// Genesis anchor information for the rollup. + pub genesis: Genesis, + + /// The block time of the L2, in seconds. + pub block_time: u64, + + /// Activation time for the interop network upgrade. + pub interop_time: Option, +} + +impl RollupConfig { + /// Creates a new RollupConfig with the given genesis and block time. + pub const fn new(genesis: Genesis, block_time: u64, interop_time: Option) -> Self { + Self { genesis, block_time, interop_time } + } + + /// Creates a new [`RollupConfig`] with the given genesis and block time. + pub fn new_from_rollup_config( + config: kona_genesis::RollupConfig, + l1_block: BlockInfo, + ) -> Result { + if config.genesis.l1.number != l1_block.number { + return Err(SupervisorError::L1BlockMismatch { + expected: config.genesis.l1.number, + got: l1_block.number, + }); + } + + Ok(Self { + genesis: Genesis::new_from_rollup_genesis(config.genesis, l1_block), + block_time: config.block_time, + interop_time: config.hardforks.interop_time, + }) + } + + /// Returns `true` if the timestamp is at or after the interop activation time. + /// + /// Interop activates at [`interop_time`](Self::interop_time). This function checks whether the + /// provided timestamp is before or after interop timestamp. + /// + /// Returns `false` if `interop_time` is not configured. + pub fn is_interop(&self, timestamp: u64) -> bool { + self.interop_time.is_some_and(|t| timestamp >= t) + } + + /// Returns `true` if the timestamp is strictly after the interop activation block. + /// + /// Interop activates at [`interop_time`](Self::interop_time). This function checks whether the + /// provided timestamp is *after* that activation, skipping the activation block + /// itself. + /// + /// Returns `false` if `interop_time` is not configured. + pub fn is_post_interop(&self, timestamp: u64) -> bool { + self.is_interop(timestamp.saturating_sub(self.block_time)) + } + + /// Returns `true` if given block is the interop activation block. + /// + /// An interop activation block is defined as the block that is right after the + /// interop activation time. + /// + /// Returns `false` if `interop_time` is not configured. + pub fn is_interop_activation_block(&self, block: BlockInfo) -> bool { + self.is_interop(block.timestamp) && + !self.is_interop(block.timestamp.saturating_sub(self.block_time)) + } +} + +/// RollupConfigSet contains the configuration for multiple Optimism rollups. +#[derive(Debug, Clone, Default)] +pub struct RollupConfigSet { + /// The rollup configurations for the Optimism rollups. + pub rollups: HashMap, +} + +impl RollupConfigSet { + /// Creates a new RollupConfigSet with the given rollup configurations. + pub const fn new(rollups: HashMap) -> Self { + Self { rollups } + } + + /// Returns the rollup configuration for the given chain id. + pub fn get(&self, chain_id: u64) -> Option<&RollupConfig> { + self.rollups.get(&chain_id) + } + + /// adds a new rollup configuration to the set using the provided chain ID and RollupConfig. + pub fn add_from_rollup_config( + &mut self, + chain_id: u64, + config: kona_genesis::RollupConfig, + l1_block: BlockInfo, + ) -> Result<(), SupervisorError> { + let rollup_config = RollupConfig::new_from_rollup_config(config, l1_block)?; + self.rollups.insert(chain_id, rollup_config); + Ok(()) + } + + /// Returns `true` if interop is enabled for the chain at given timestamp. + pub fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool { + self.get(chain_id).map(|cfg| cfg.is_post_interop(timestamp)).unwrap_or(false) // if config not found, return false + } + + /// Returns `true` if given block is the interop activation block for the specified chain. + pub fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool { + self.get(chain_id).map(|cfg| cfg.is_interop_activation_block(block)).unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::ChainId; + use kona_protocol::BlockInfo; + + fn dummy_blockinfo(number: u64) -> BlockInfo { + BlockInfo::new(B256::ZERO, number, B256::ZERO, 0) + } + + #[test] + fn test_is_interop_enabled() { + let mut set = RollupConfigSet::default(); + let chain_id = ChainId::from(1u64); + + // Interop time is 100, block_time is 10 + let rollup_config = + RollupConfig::new(Genesis::new(dummy_blockinfo(0), dummy_blockinfo(0)), 10, Some(100)); + set.rollups.insert(chain_id, rollup_config); + + // Before interop time + assert!(!set.is_post_interop(chain_id, 100)); + assert!(!set.is_post_interop(chain_id, 109)); + // After interop time (should be true) + assert!(set.is_post_interop(chain_id, 110)); + assert!(set.is_post_interop(chain_id, 111)); + assert!(set.is_post_interop(chain_id, 200)); + + // Unknown chain_id returns false + assert!(!set.is_post_interop(ChainId::from(999u64), 200)); + } + + #[test] + fn test_rollup_config_is_interop_interop_time_zero() { + // Interop time is 100, block_time is 10 + let rollup_config = + RollupConfig::new(Genesis::new(dummy_blockinfo(0), dummy_blockinfo(0)), 2, Some(0)); + + assert!(rollup_config.is_interop(0)); + assert!(rollup_config.is_interop(1000)); + } +} diff --git a/kona/crates/supervisor/core/src/error.rs b/kona/crates/supervisor/core/src/error.rs new file mode 100644 index 0000000000000..08622fb595901 --- /dev/null +++ b/kona/crates/supervisor/core/src/error.rs @@ -0,0 +1,208 @@ +//! [`SupervisorService`](crate::SupervisorService) errors. + +use crate::syncnode::ManagedNodeError; +use derive_more; +use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; +use kona_supervisor_storage::StorageError; +use kona_supervisor_types::AccessListError; +use op_alloy_rpc_types::SuperchainDAError; +use thiserror::Error; + +/// Custom error type for the Supervisor core logic. +#[derive(Debug, Error)] +pub enum SupervisorError { + /// Indicates that a feature or method is not yet implemented. + #[error("functionality not implemented")] + Unimplemented, + + /// No chains are configured for supervision. + #[error("empty dependency set")] + EmptyDependencySet, + + /// Unsupported chain ID. + #[error("unsupported chain ID")] + UnsupportedChainId, + + /// Data availability errors. + /// + /// Spec . + #[error(transparent)] + SpecError(#[from] SpecError), + + /// Indicates that error occurred while interacting with the storage layer. + #[error(transparent)] + StorageError(#[from] StorageError), + + /// Indicates that managed node not found for the chain. + #[error("managed node not found for chain: {0}")] + ManagedNodeMissing(u64), + + /// Indicates the error occurred while interacting with the managed node. + #[error(transparent)] + ManagedNodeError(#[from] ManagedNodeError), + + /// Indicates the error occurred while parsing the access_list + #[error(transparent)] + AccessListError(#[from] AccessListError), + + /// Indicates the error occurred while serializing or deserializing JSON. + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + + /// Indicates the L1 block does not match the expected L1 block. + #[error("L1 block number mismatch. expected: {expected}, but got {got}")] + L1BlockMismatch { + /// Expected L1 block. + expected: u64, + /// Received L1 block. + got: u64, + }, + + /// Indicates that the chain ID could not be parsed from the access list. + #[error("failed to parse chain id from access list")] + ChainIdParseError(), +} + +impl PartialEq for SupervisorError { + fn eq(&self, other: &Self) -> bool { + use SupervisorError::*; + match (self, other) { + (Unimplemented, Unimplemented) => true, + (EmptyDependencySet, EmptyDependencySet) => true, + (SpecError(a), SpecError(b)) => a == b, + (StorageError(a), StorageError(b)) => a == b, + (ManagedNodeMissing(a), ManagedNodeMissing(b)) => a == b, + (ManagedNodeError(a), ManagedNodeError(b)) => a == b, + (AccessListError(a), AccessListError(b)) => a == b, + (SerdeJson(a), SerdeJson(b)) => a.to_string() == b.to_string(), + (L1BlockMismatch { expected: a, got: b }, L1BlockMismatch { expected: c, got: d }) => { + a == c && b == d + } + _ => false, + } + } +} + +impl Eq for SupervisorError {} + +/// Extending the [`SuperchainDAError`] to include errors not in the spec. +#[derive(Error, Debug, PartialEq, Eq, derive_more::TryFrom)] +#[repr(i32)] +#[try_from(repr)] +pub enum SpecError { + /// [`SuperchainDAError`] from the spec. + #[error(transparent)] + SuperchainDAError(#[from] SuperchainDAError), + + /// Error not in spec. + #[error("error not in spec")] + ErrorNotInSpec, +} + +impl SpecError { + /// Maps the proper error code from SuperchainDAError. + /// Introduced a new error code for errors not in the spec. + pub const fn code(&self) -> i32 { + match self { + Self::SuperchainDAError(e) => *e as i32, + Self::ErrorNotInSpec => -321300, + } + } +} + +impl From for ErrorObjectOwned { + fn from(err: SpecError) -> Self { + ErrorObjectOwned::owned(err.code(), err.to_string(), None::<()>) + } +} + +impl From for ErrorObjectOwned { + fn from(err: SupervisorError) -> Self { + match err { + // todo: handle these errors more gracefully + SupervisorError::Unimplemented | + SupervisorError::EmptyDependencySet | + SupervisorError::UnsupportedChainId | + SupervisorError::L1BlockMismatch { .. } | + SupervisorError::ManagedNodeMissing(_) | + SupervisorError::ManagedNodeError(_) | + SupervisorError::StorageError(_) | + SupervisorError::AccessListError(_) | + SupervisorError::ChainIdParseError() | + SupervisorError::SerdeJson(_) => ErrorObjectOwned::from(ErrorCode::InternalError), + SupervisorError::SpecError(err) => err.into(), + } + } +} + +impl From for SpecError { + fn from(err: StorageError) -> Self { + match err { + StorageError::Database(_) => Self::from(SuperchainDAError::DataCorruption), + StorageError::FutureData => Self::from(SuperchainDAError::FutureData), + StorageError::EntryNotFound(_) => Self::from(SuperchainDAError::MissedData), + StorageError::ConflictError => Self::from(SuperchainDAError::ConflictingData), + StorageError::BlockOutOfOrder => Self::from(SuperchainDAError::OutOfOrder), + StorageError::DatabaseNotInitialised => Self::ErrorNotInSpec, + _ => Self::ErrorNotInSpec, + } + } +} + +#[cfg(test)] +mod test { + use kona_supervisor_storage::EntryNotFoundError; + + use super::*; + + #[test] + fn test_storage_error_conversion() { + let test_err = SpecError::from(StorageError::DatabaseNotInitialised); + let expected_err = SpecError::ErrorNotInSpec; + + assert_eq!(test_err, expected_err); + } + + #[test] + fn test_unmapped_storage_error_conversion() { + let spec_err = ErrorObjectOwned::from(SpecError::ErrorNotInSpec); + let expected_err = SpecError::ErrorNotInSpec; + + assert_eq!(spec_err, expected_err.into()); + + let spec_err = ErrorObjectOwned::from(SpecError::from(StorageError::LockPoisoned)); + let expected_err = SpecError::ErrorNotInSpec; + + assert_eq!(spec_err, expected_err.into()); + + let spec_err = ErrorObjectOwned::from(SpecError::from(StorageError::FutureData)); + let expected_err = SpecError::SuperchainDAError(SuperchainDAError::FutureData); + + assert_eq!(spec_err, expected_err.into()); + + let spec_err = ErrorObjectOwned::from(SpecError::from(StorageError::EntryNotFound( + EntryNotFoundError::DerivedBlockNotFound(12), + ))); + let expected_err = SpecError::SuperchainDAError(SuperchainDAError::MissedData); + + assert_eq!(spec_err, expected_err.into()); + } + + #[test] + fn test_supervisor_error_conversion() { + // This will happen implicitly in server rpc response calls. + let supervisor_err = ErrorObjectOwned::from(SupervisorError::SpecError(SpecError::from( + StorageError::LockPoisoned, + ))); + let expected_err = SpecError::ErrorNotInSpec; + + assert_eq!(supervisor_err, expected_err.into()); + + let supervisor_err = ErrorObjectOwned::from(SupervisorError::SpecError(SpecError::from( + StorageError::FutureData, + ))); + let expected_err = SpecError::SuperchainDAError(SuperchainDAError::FutureData); + + assert_eq!(supervisor_err, expected_err.into()); + } +} diff --git a/kona/crates/supervisor/core/src/event/chain.rs b/kona/crates/supervisor/core/src/event/chain.rs new file mode 100644 index 0000000000000..69711ae20b421 --- /dev/null +++ b/kona/crates/supervisor/core/src/event/chain.rs @@ -0,0 +1,59 @@ +use kona_interop::{BlockReplacement, DerivedRefPair}; +use kona_protocol::BlockInfo; + +/// Represents chain events that are emitted from modules in the supervisor. +/// These events are used to notify the [`ChainProcessor`](crate::chain_processor::ChainProcessor) +/// about changes in block states, such as unsafe blocks, safe blocks, or block replacements. +/// Each event carries relevant information about the block involved, +/// allowing the supervisor to take appropriate actions based on the event type. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ChainEvent { + /// An unsafe block event, indicating that a new unsafe block has been detected. + UnsafeBlock { + /// The [`BlockInfo`] of the unsafe block. + block: BlockInfo, + }, + + /// A derived block event, indicating that a new derived block has been detected. + DerivedBlock { + /// The [`DerivedRefPair`] containing the derived block and its source block. + derived_ref_pair: DerivedRefPair, + }, + + /// A derivation origin update event, indicating that the origin for derived blocks has changed. + DerivationOriginUpdate { + /// The [`BlockInfo`] of the block that is the new derivation origin. + origin: BlockInfo, + }, + + /// An invalidate Block event, indicating that a block has been invalidated. + InvalidateBlock { + /// The [`BlockInfo`] of the block that has been invalidated. + block: BlockInfo, + }, + + /// A block replacement event, indicating that a block has been replaced with a new one. + BlockReplaced { + /// The [`BlockReplacement`] containing the replacement block and the invalidated block + /// hash. + replacement: BlockReplacement, + }, + + /// A finalized source update event, indicating that a new source block has been finalized. + FinalizedSourceUpdate { + /// The [`BlockInfo`] of the new finalized source(l1) block. + finalized_source_block: BlockInfo, + }, + + /// A cross unsafe update event, indicating that a cross unsafe block has been promoted. + CrossUnsafeUpdate { + /// The [`BlockInfo`] of the new cross unsafe block + block: BlockInfo, + }, + + /// A cross safe update event, indicating that a cross safe block has been promoted. + CrossSafeUpdate { + /// The [`DerivedRefPair`] containing the derived block and its source block. + derived_ref_pair: DerivedRefPair, + }, +} diff --git a/kona/crates/supervisor/core/src/event/mod.rs b/kona/crates/supervisor/core/src/event/mod.rs new file mode 100644 index 0000000000000..3db9adba7f1e1 --- /dev/null +++ b/kona/crates/supervisor/core/src/event/mod.rs @@ -0,0 +1,4 @@ +//! Event module for the chain processor and supervisor coordination. + +mod chain; +pub use chain::ChainEvent; diff --git a/kona/crates/supervisor/core/src/l1_watcher/mod.rs b/kona/crates/supervisor/core/src/l1_watcher/mod.rs new file mode 100644 index 0000000000000..e2751f3dc502f --- /dev/null +++ b/kona/crates/supervisor/core/src/l1_watcher/mod.rs @@ -0,0 +1,6 @@ +//! L1 Watcher Module +//! This module provides functionality for watching L1 blocks and managing subscriptions to L1 +//! events. +mod watcher; + +pub use watcher::L1Watcher; diff --git a/kona/crates/supervisor/core/src/l1_watcher/watcher.rs b/kona/crates/supervisor/core/src/l1_watcher/watcher.rs new file mode 100644 index 0000000000000..00476c970aead --- /dev/null +++ b/kona/crates/supervisor/core/src/l1_watcher/watcher.rs @@ -0,0 +1,509 @@ +use crate::event::ChainEvent; +use alloy_eips::{BlockNumHash, BlockNumberOrTag}; +use alloy_primitives::ChainId; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_eth::{Block, Header}; +use futures::StreamExt; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DbReader, FinalizedL1Storage, StorageRewinder}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, trace}; + +use crate::ReorgHandler; + +/// A watcher that polls the L1 chain for finalized blocks. +#[derive(Debug)] +pub struct L1Watcher { + /// The Alloy RPC client for L1. + rpc_client: RpcClient, + /// The cancellation token, shared between all tasks. + cancellation: CancellationToken, + /// The finalized L1 block storage. + finalized_l1_storage: Arc, + /// The event senders for each chain. + event_txs: HashMap>, + /// The reorg handler. + reorg_handler: ReorgHandler, +} + +impl L1Watcher +where + F: FinalizedL1Storage + 'static, + DB: DbReader + StorageRewinder + Send + Sync + 'static, +{ + /// Creates a new [`L1Watcher`] instance. + pub const fn new( + rpc_client: RpcClient, + finalized_l1_storage: Arc, + event_txs: HashMap>, + cancellation: CancellationToken, + reorg_handler: ReorgHandler, + ) -> Self { + Self { rpc_client, finalized_l1_storage, event_txs, cancellation, reorg_handler } + } + + /// Starts polling for finalized and latest blocks and processes them. + pub async fn run(&self) { + // TODO: Change the polling interval to 1535 seconds with mainnet config. + let finalized_head_poller = self + .rpc_client + .prepare_static_poller::<_, Block>( + "eth_getBlockByNumber", + (BlockNumberOrTag::Finalized, false), + ) + .with_poll_interval(Duration::from_secs(47)); + + let finalized_head_stream = finalized_head_poller.into_stream(); + + // TODO: Change the polling interval to 11 seconds with mainnet config. + let latest_head_poller = self + .rpc_client + .prepare_static_poller::<_, Block>( + "eth_getBlockByNumber", + (BlockNumberOrTag::Latest, false), + ) + .with_poll_interval(Duration::from_secs(2)); + + let latest_head_stream = latest_head_poller.into_stream(); + + self.poll_blocks(finalized_head_stream, latest_head_stream).await; + } + + /// Helper function to poll blocks using a provided stream and handler closure. + async fn poll_blocks(&self, mut finalized_head_stream: S, mut latest_head_stream: S) + where + S: futures::Stream + Unpin, + { + let mut finalized_number = 0; + let mut previous_latest_block: Option = None; + + loop { + tokio::select! { + _ = self.cancellation.cancelled() => { + info!(target: "supervisor::l1_watcher", "L1Watcher cancellation requested, stopping polling"); + break; + } + latest_block = latest_head_stream.next() => { + if let Some(latest_block) = latest_block { + previous_latest_block = self.handle_new_latest_block(latest_block, previous_latest_block).await; + } + } + finalized_block = finalized_head_stream.next() => { + if let Some(finalized_block) = finalized_block { + finalized_number = self.handle_new_finalized_block(finalized_block, finalized_number); + } + } + } + } + } + + /// Handles a new finalized [`Block`], updating the storage and broadcasting the event. + /// + /// Arguments: + /// - `block`: The finalized block to process. + /// - `last_finalized_number`: The last finalized block number. + /// + /// Returns: + /// - `u64`: The new finalized block number. + fn handle_new_finalized_block(&self, block: Block, last_finalized_number: u64) -> u64 { + let block_number = block.header.number; + if block_number == last_finalized_number { + return last_finalized_number; + } + + let Header { + hash, + inner: alloy_consensus::Header { number, parent_hash, timestamp, .. }, + .. + } = block.header; + let finalized_source_block = BlockInfo::new(hash, number, parent_hash, timestamp); + + trace!( + target: "supervisor::l1_watcher", + incoming_block_number = block_number, + previous_block_number = last_finalized_number, + "Finalized L1 block received" + ); + + if let Err(err) = self.finalized_l1_storage.update_finalized_l1(finalized_source_block) { + error!(target: "supervisor::l1_watcher", %err, "Failed to update finalized L1 block"); + return last_finalized_number; + } + + self.broadcast_finalized_source_update(finalized_source_block); + + block_number + } + + fn broadcast_finalized_source_update(&self, finalized_source_block: BlockInfo) { + for (chain_id, sender) in &self.event_txs { + if let Err(err) = + sender.try_send(ChainEvent::FinalizedSourceUpdate { finalized_source_block }) + { + error!( + target: "supervisor::l1_watcher", + chain_id = %chain_id, + %err, "Failed to send finalized L1 update event", + ); + } + } + } + + /// Handles a new latest [`Block`], checking if it requires a reorg or is sequential. + /// + /// Arguments: + /// - `incoming_block`: The incoming block to process. + /// - `previous_block`: The previously stored latest block, if any. + /// + /// Returns: + /// - `Option`: The ID of the new latest block if processed successfully, or the + /// previous block if no changes were made. + async fn handle_new_latest_block( + &self, + incoming_block: Block, + previous_block: Option, + ) -> Option { + let Header { + hash, + inner: alloy_consensus::Header { number, parent_hash, timestamp, .. }, + .. + } = incoming_block.header; + let latest_block = BlockInfo::new(hash, number, parent_hash, timestamp); + + let prev = match previous_block { + Some(prev) => prev, + None => { + return Some(latest_block.id()); + } + }; + + trace!( + target: "l1_watcher", + block_number = latest_block.number, + block_hash = ?incoming_block.header.hash, + "New latest L1 block received" + ); + + // Early exit if the incoming block is not newer than the previous block + if latest_block.number <= prev.number { + trace!( + target: "supervisor::l1_watcher", + incoming_block_number = latest_block.number, + previous_block_number = prev.number, + "Incoming latest L1 block is not greater than the stored latest block" + ); + return previous_block; + } + + // Early exit: check if no reorg is needed (sequential block) + if latest_block.parent_hash == prev.hash { + trace!( + target: "supervisor::l1_watcher", + block_number = latest_block.number, + "Sequential block received, no reorg needed" + ); + return Some(latest_block.id()); + } + + match self.reorg_handler.handle_l1_reorg(latest_block).await { + Ok(()) => { + trace!( + target: "supervisor::l1_watcher", + block_number = latest_block.number, + "Successfully processed potential L1 reorg" + ); + } + Err(err) => { + error!( + target: "supervisor::l1_watcher", + block_number = latest_block.number, + %err, + "Failed to handle L1 reorg" + ); + } + } + + Some(latest_block.id()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + SupervisorError, + syncnode::{ManagedNodeController, ManagedNodeError}, + }; + use alloy_primitives::B256; + use alloy_transport::mock::*; + use async_trait::async_trait; + use kona_supervisor_storage::{ChainDb, FinalizedL1Storage, StorageError}; + use kona_supervisor_types::BlockSeal; + use mockall::{mock, predicate}; + use std::sync::Arc; + use tokio::sync::mpsc; + // Mock the FinalizedL1Storage trait + mock! ( + pub finalized_l1_storage {} + impl FinalizedL1Storage for finalized_l1_storage { + fn update_finalized_l1(&self, block: BlockInfo) -> Result<(), StorageError>; + fn get_finalized_l1(&self) -> Result; + } + ); + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl ManagedNodeController for Node { + async fn update_finalized( + &self, + _finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + async fn reset(&self) -> Result<(), ManagedNodeError>; + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + ); + + mock! ( + pub ReorgHandler { + fn handle_l1_reorg(&self, latest_block: BlockInfo) -> Result<(), SupervisorError>; + } + ); + + fn mock_rpc_client() -> RpcClient { + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter); + RpcClient::new(transport, false) + } + + fn mock_reorg_handler() -> ReorgHandler { + let chain_dbs_map: HashMap> = HashMap::new(); + ReorgHandler::new(mock_rpc_client(), chain_dbs_map) + } + + #[tokio::test] + async fn test_broadcast_finalized_source_update_sends_to_all() { + let (tx1, mut rx1) = mpsc::channel(1); + let (tx2, mut rx2) = mpsc::channel(1); + + let mut event_txs = HashMap::new(); + event_txs.insert(1, tx1); + event_txs.insert(2, tx2); + + let watcher = L1Watcher { + rpc_client: mock_rpc_client(), + cancellation: CancellationToken::new(), + finalized_l1_storage: Arc::new(Mockfinalized_l1_storage::new()), + event_txs, + reorg_handler: mock_reorg_handler(), + }; + + let block = BlockInfo::new(B256::ZERO, 42, B256::ZERO, 12345); + watcher.broadcast_finalized_source_update(block); + + assert!( + matches!(rx1.recv().await, Some(ChainEvent::FinalizedSourceUpdate { finalized_source_block }) if finalized_source_block == block) + ); + assert!( + matches!(rx2.recv().await, Some(ChainEvent::FinalizedSourceUpdate { finalized_source_block }) if finalized_source_block == block) + ); + } + + #[tokio::test] + async fn test_handle_new_finalized_block_updates_and_broadcasts() { + let (tx, mut rx) = mpsc::channel(1); + let event_txs = [(1, tx)].into_iter().collect(); + + let mut mock_storage = Mockfinalized_l1_storage::new(); + mock_storage.expect_update_finalized_l1().returning(|_block| Ok(())); + + let watcher = L1Watcher { + rpc_client: mock_rpc_client(), + cancellation: CancellationToken::new(), + finalized_l1_storage: Arc::new(mock_storage), + event_txs, + reorg_handler: mock_reorg_handler(), + }; + + let block = Block { + header: Header { + hash: B256::ZERO, + inner: alloy_consensus::Header { + number: 42, + parent_hash: B256::ZERO, + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let mut last_finalized_number = 0; + last_finalized_number = + watcher.handle_new_finalized_block(block.clone(), last_finalized_number); + + let event = rx.recv().await.unwrap(); + let expected = BlockInfo::new( + block.header.hash, + block.header.number, + block.header.parent_hash, + block.header.timestamp, + ); + assert!( + matches!(event, ChainEvent::FinalizedSourceUpdate { ref finalized_source_block } if *finalized_source_block == expected), + "Expected FinalizedSourceUpdate with block {expected:?}, got {event:?}" + ); + assert_eq!(last_finalized_number, block.header.number); + } + + #[tokio::test] + async fn test_handle_new_finalized_block_storage_error() { + let (tx, mut rx) = mpsc::channel(1); + let event_txs = [(1, tx)].into_iter().collect(); + + let mut mock_storage = Mockfinalized_l1_storage::new(); + mock_storage + .expect_update_finalized_l1() + .returning(|_block| Err(StorageError::DatabaseNotInitialised)); + + let watcher = L1Watcher { + rpc_client: mock_rpc_client(), + cancellation: CancellationToken::new(), + finalized_l1_storage: Arc::new(mock_storage), + event_txs, + reorg_handler: mock_reorg_handler(), + }; + + let block = Block { + header: Header { + hash: B256::ZERO, + inner: alloy_consensus::Header { + number: 42, + parent_hash: B256::ZERO, + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let mut last_finalized_number = 0; + last_finalized_number = watcher.handle_new_finalized_block(block, last_finalized_number); + + assert_eq!(last_finalized_number, 0); + // Should NOT broadcast if storage update fails + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_handle_new_latest_block_updates() { + let (tx, mut rx) = mpsc::channel(1); + let event_txs = [(1, tx)].into_iter().collect(); + + let watcher = L1Watcher { + rpc_client: mock_rpc_client(), + cancellation: CancellationToken::new(), + finalized_l1_storage: Arc::new(Mockfinalized_l1_storage::new()), + event_txs, + reorg_handler: mock_reorg_handler(), + }; + + let block = Block { + header: Header { + hash: B256::ZERO, + inner: alloy_consensus::Header { + number: 1, + parent_hash: B256::ZERO, + timestamp: 123456, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let mut last_latest_number = None; + last_latest_number = watcher.handle_new_latest_block(block, last_latest_number).await; + assert_eq!(last_latest_number.unwrap().number, 1); + // Should NOT send any event for latest block + assert!(rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_trigger_reorg_handler() { + let (tx, mut rx) = mpsc::channel(1); + let event_txs = [(1, tx)].into_iter().collect(); + + let watcher = L1Watcher { + rpc_client: mock_rpc_client(), + cancellation: CancellationToken::new(), + finalized_l1_storage: Arc::new(Mockfinalized_l1_storage::new()), + event_txs, + reorg_handler: mock_reorg_handler(), + }; + + let block = Block { + header: Header { + hash: B256::ZERO, + inner: alloy_consensus::Header { + number: 101, + parent_hash: B256::ZERO, + timestamp: 123456, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let mut last_latest_number = Some(BlockNumHash { number: 100, hash: B256::ZERO }); + last_latest_number = watcher.handle_new_latest_block(block, last_latest_number).await; + assert_eq!(last_latest_number.unwrap().number, 101); + + // Send previous block as latest block + let reorg_block = Block { + header: Header { + hash: B256::ZERO, + inner: alloy_consensus::Header { + number: 105, + parent_hash: B256::from([1u8; 32]), + timestamp: 123456, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + let reorg_block_info = BlockInfo::new( + reorg_block.header.hash, + reorg_block.header.number, + reorg_block.header.parent_hash, + reorg_block.header.timestamp, + ); + let mut mock_reorg_handler = MockReorgHandler::new(); + mock_reorg_handler + .expect_handle_l1_reorg() + .with(predicate::eq(reorg_block_info)) + .returning(|_| Ok(())); + + last_latest_number = watcher.handle_new_latest_block(reorg_block, last_latest_number).await; + assert_eq!(last_latest_number.unwrap().number, 105); + // Should NOT send any event for latest block + assert!(rx.try_recv().is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/lib.rs b/kona/crates/supervisor/core/src/lib.rs new file mode 100644 index 0000000000000..bf211b7c4f347 --- /dev/null +++ b/kona/crates/supervisor/core/src/lib.rs @@ -0,0 +1,29 @@ +//! This crate contains the core logic for the Optimism Supervisor component. + +pub mod chain_processor; +pub use chain_processor::{ChainProcessor, ChainProcessorError, ProcessorState}; + +pub mod error; +pub use error::{SpecError, SupervisorError}; + +/// Contains the main Supervisor struct and its implementation. +mod supervisor; +pub use supervisor::{Supervisor, SupervisorService}; + +mod logindexer; +pub use logindexer::{ + LogIndexer, LogIndexerError, log_to_log_hash, log_to_message_payload, payload_hash_to_log_hash, +}; + +pub mod rpc; + +pub mod config; +pub mod event; +pub mod l1_watcher; +pub mod syncnode; + +pub mod safety_checker; +pub use safety_checker::{CrossSafetyCheckerJob, CrossSafetyError}; + +mod reorg; +pub use reorg::{ReorgHandler, ReorgHandlerError}; diff --git a/kona/crates/supervisor/core/src/logindexer/indexer.rs b/kona/crates/supervisor/core/src/logindexer/indexer.rs new file mode 100644 index 0000000000000..9299f1ae2444b --- /dev/null +++ b/kona/crates/supervisor/core/src/logindexer/indexer.rs @@ -0,0 +1,364 @@ +use crate::{ + logindexer::{log_to_log_hash, payload_hash_to_log_hash}, + syncnode::{BlockProvider, ManagedNodeError}, +}; +use alloy_primitives::ChainId; +use kona_interop::parse_log_to_executing_message; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{LogStorageReader, LogStorageWriter, StorageError}; +use kona_supervisor_types::{ExecutingMessage, Log}; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::Mutex; +use tracing::{debug, error}; + +/// The [`LogIndexer`] is responsible for processing L2 receipts, extracting [`ExecutingMessage`]s, +/// and persisting them to the state manager. +#[derive(Debug)] +pub struct LogIndexer { + /// The chain ID of the rollup. + chain_id: ChainId, + /// Component that provides receipts for a given block hash. + block_provider: Mutex>>, + /// Component that persists parsed log entries to storage. + log_storage: Arc, + /// Protects concurrent catch-up + is_catch_up_running: Mutex, +} + +impl LogIndexer +where + P: BlockProvider + 'static, + S: LogStorageWriter + LogStorageReader + 'static, +{ + /// Creates a new [`LogIndexer`] with the given receipt provider and state manager. + /// + /// # Arguments + /// - `block_provider`: Shared reference to a component capable of fetching block ref and + /// receipts. + /// - `log_storage`: Shared reference to the storage layer for persisting parsed logs. + pub fn new(chain_id: ChainId, block_provider: Option>, log_storage: Arc) -> Self { + Self { + chain_id, + block_provider: Mutex::new(block_provider), + log_storage, + is_catch_up_running: Mutex::new(false), + } + } + + /// Sets the block provider + pub async fn set_block_provider(&self, block_provider: Arc

) { + let mut guard = self.block_provider.lock().await; + *guard = Some(block_provider); + } + + /// Asynchronously initiates a background task to catch up and index logs + /// starting from the latest successfully indexed block up to the specified block. + /// + /// If a catch-up job is already running, this call is ignored. + /// + /// # Arguments + /// - `block`: The target block to sync logs up to (inclusive). + pub fn sync_logs(self: Arc, block: BlockInfo) { + tokio::spawn(async move { + let mut running = self.is_catch_up_running.lock().await; + + if *running { + debug!(target: "supervisor::log_indexer", chain_id = %self.chain_id, "Catch-up running log index"); + return; + } + + *running = true; + drop(running); // release the lock while the job runs + + if let Err(err) = self.index_log_upto(&block).await { + error!( + target: "supervisor::log_indexer", + chain_id = %self.chain_id, + %err, + "Log indexer catch-up failed" + ); + } + + let mut running = self.is_catch_up_running.lock().await; + *running = false; + }); + } + + /// Performs log indexing sequentially from the latest indexed block up to the given target + /// block. + async fn index_log_upto(&self, block: &BlockInfo) -> Result<(), LogIndexerError> { + let mut current_number = self.log_storage.get_latest_block()?.number + 1; + + while current_number < block.number { + let provider = { + let guard = self.block_provider.lock().await; + guard.as_ref().ok_or(LogIndexerError::NoBlockProvider)?.clone() + }; + + let current_block = provider.block_by_number(current_number).await?; + self.process_and_store_logs(¤t_block).await?; + current_number += 1; + } + self.process_and_store_logs(block).await?; + + Ok(()) + } + + /// Processes and stores the logs of a given block in into the state manager. + /// + /// This function: + /// - Fetches all receipts for the given block from the specified chain. + /// - Iterates through all logs in all receipts. + /// - For each log, computes a hash from the log and optionally parses an [`ExecutingMessage`]. + /// - Records each [`Log`] including the message if found. + /// - Saves all log entries atomically using the [`LogStorageWriter`]. + /// + /// # Arguments + /// - `block`: Metadata about the block being processed. + pub async fn process_and_store_logs(&self, block: &BlockInfo) -> Result<(), LogIndexerError> { + let provider = { + let guard = self.block_provider.lock().await; + guard.as_ref().ok_or(LogIndexerError::NoBlockProvider)?.clone() + }; + + let receipts = provider.fetch_receipts(block.hash).await?; + let mut log_entries = Vec::with_capacity(receipts.len()); + let mut log_index: u32 = 0; + + for receipt in receipts { + for log in receipt.logs() { + let log_hash = log_to_log_hash(log); + + let executing_message = parse_log_to_executing_message(log).map(|msg| { + let payload_hash = + payload_hash_to_log_hash(msg.payloadHash, msg.identifier.origin); + ExecutingMessage { + chain_id: msg.identifier.chainId.try_into().unwrap(), + block_number: msg.identifier.blockNumber.try_into().unwrap(), + log_index: msg.identifier.logIndex.try_into().unwrap(), + timestamp: msg.identifier.timestamp.try_into().unwrap(), + hash: payload_hash, + } + }); + + log_entries.push(Log { index: log_index, hash: log_hash, executing_message }); + + log_index += 1; + } + } + + log_entries.shrink_to_fit(); + + self.log_storage.store_block_logs(block, log_entries)?; + Ok(()) + } +} + +/// Error type for the [`LogIndexer`]. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum LogIndexerError { + /// No block provider set when attempting to index logs. + #[error("no block provider set")] + NoBlockProvider, + + /// Failed to write processed logs for a block to the state manager. + #[error(transparent)] + StateWrite(#[from] StorageError), + + /// Failed to fetch logs for a block from the state manager. + #[error(transparent)] + FetchReceipt(#[from] ManagedNodeError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{AuthenticationError, ClientError}; + use alloy_primitives::{Address, B256, Bytes}; + use async_trait::async_trait; + use kona_interop::{ExecutingMessageBuilder, InteropProvider, SuperchainBuilder}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::StorageError; + use kona_supervisor_types::{Log, Receipts}; + use mockall::mock; + use op_alloy_consensus::{OpReceiptEnvelope, OpTxType}; + use std::sync::Arc; + mock! ( + #[derive(Debug)] + pub BlockProvider {} + + #[async_trait] + impl BlockProvider for BlockProvider { + async fn fetch_receipts(&self, block_hash: B256) -> Result; + async fn block_by_number(&self, number: u64) -> Result; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageWriter for Db { + fn initialise_log_storage(&self, _block: BlockInfo) -> Result<(), StorageError>; + fn store_block_logs(&self, block: &BlockInfo, logs: Vec) -> Result<(), StorageError>; + } + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self,block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + ); + + fn hash_for_number(n: u64) -> B256 { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&n.to_be_bytes()); + B256::from(bytes) + } + + async fn build_receipts() -> Receipts { + let mut builder = SuperchainBuilder::new(); + builder + .chain(10) + .with_timestamp(123456) + .add_initiating_message(Bytes::from_static(b"init-msg")) + .add_executing_message( + ExecutingMessageBuilder::default() + .with_message_hash(B256::repeat_byte(0xaa)) + .with_origin_address(Address::ZERO) + .with_origin_log_index(0) + .with_origin_block_number(1) + .with_origin_chain_id(10) + .with_origin_timestamp(123456), + ); + let (headers, _, mock_provider) = builder.build(); + let block = headers.get(&10).unwrap(); + + mock_provider.receipts_by_hash(10, block.hash()).await.unwrap() + } + + #[tokio::test] + async fn test_process_and_store_logs_success() { + let receipts = build_receipts().await; + let block_hash = B256::random(); + let block_info = + BlockInfo { number: 1, hash: block_hash, timestamp: 123456789, ..Default::default() }; + + let mut mock_provider = MockBlockProvider::new(); + mock_provider + .expect_fetch_receipts() + .withf(move |hash| *hash == block_hash) + .returning(move |_| Ok(receipts.clone())); + + mock_provider.expect_block_by_number().returning(|_| Ok(BlockInfo::default())); // Not used here + + let mut mock_db = MockDb::new(); + mock_db + .expect_store_block_logs() + .withf(|block, logs| block.number == 1 && logs.len() == 2) + .returning(|_, _| Ok(())); + + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_provider)), Arc::new(mock_db)); + + let result = log_indexer.process_and_store_logs(&block_info).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_process_and_store_logs_with_empty_logs() { + let block_hash = B256::random(); + let block_info = + BlockInfo { number: 2, hash: block_hash, timestamp: 111111111, ..Default::default() }; + + let empty_log_receipt = + OpReceiptEnvelope::from_parts(true, 21000, vec![], OpTxType::Eip1559, None, None); + let receipts = vec![empty_log_receipt]; + + let mut mock_provider = MockBlockProvider::new(); + mock_provider + .expect_fetch_receipts() + .withf(move |hash| *hash == block_hash) + .returning(move |_| Ok(receipts.clone())); + + mock_provider.expect_block_by_number().returning(|_| Ok(BlockInfo::default())); // Not used + + let mut mock_db = MockDb::new(); + mock_db + .expect_store_block_logs() + .withf(|block, logs| block.number == 2 && logs.is_empty()) + .returning(|_, _| Ok(())); + + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_provider)), Arc::new(mock_db)); + + let result = log_indexer.process_and_store_logs(&block_info).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_process_and_store_logs_receipt_fetch_fails() { + let block_hash = B256::random(); + let block_info = + BlockInfo { number: 3, hash: block_hash, timestamp: 123456, ..Default::default() }; + + let mut mock_provider = MockBlockProvider::new(); + mock_provider.expect_fetch_receipts().withf(move |hash| *hash == block_hash).returning( + |_| { + Err(ManagedNodeError::ClientError(ClientError::Authentication( + AuthenticationError::InvalidHeader, + ))) + }, + ); + + mock_provider.expect_block_by_number().returning(|_| Ok(BlockInfo::default())); // Not used + + let mock_db = MockDb::new(); // No call expected + + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_provider)), Arc::new(mock_db)); + + let result = log_indexer.process_and_store_logs(&block_info).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_sync_logs_stores_all_blocks_in_range() { + let target_block = BlockInfo { + number: 5, + hash: B256::random(), + timestamp: 123456789, + ..Default::default() + }; + + // BlockProvider mock + let mut mock_provider = MockBlockProvider::new(); + mock_provider.expect_block_by_number().withf(|n| *n >= 1 && *n <= 5).returning(|n| { + Ok(BlockInfo { + number: n, + hash: hash_for_number(n), + timestamp: 0, + ..Default::default() + }) + }); + + mock_provider.expect_fetch_receipts().times(5).returning(move |_| { + Ok(vec![]) // Empty receipts + }); + + // Db mock + let mut mock_db = MockDb::new(); + mock_db + .expect_get_latest_block() + .returning(|| Ok(BlockInfo { number: 0, ..Default::default() })); + + mock_db.expect_store_block_logs().times(5).returning(move |_, _| Ok(())); + + let indexer = + Arc::new(LogIndexer::new(1, Some(Arc::new(mock_provider)), Arc::new(mock_db))); + + indexer.clone().sync_logs(target_block); + + // Let the background task complete + tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + } +} diff --git a/kona/crates/supervisor/core/src/logindexer/mod.rs b/kona/crates/supervisor/core/src/logindexer/mod.rs new file mode 100644 index 0000000000000..172de96b77aef --- /dev/null +++ b/kona/crates/supervisor/core/src/logindexer/mod.rs @@ -0,0 +1,17 @@ +//! Log indexing module for processing L2 receipts and extracting messages. +//! +//! This module provides functionality to extract and persist +//! [`ExecutingMessage`](kona_supervisor_types::ExecutingMessage)s and their corresponding +//! [`Log`](alloy_primitives::Log)s from L2 block receipts. It handles computing message payload +//! hashes and log hashes based on the interop messaging specification. +//! +//! # Modules +//! +//! - [`LogIndexer`] — main indexer that processes logs and persists them. +//! - [`LogIndexerError`] — error type for failures in fetching or storing logs. +//! - `util` — helper functions for computing payload and log hashes. +mod indexer; +pub use indexer::{LogIndexer, LogIndexerError}; + +mod util; +pub use util::{log_to_log_hash, log_to_message_payload, payload_hash_to_log_hash}; diff --git a/kona/crates/supervisor/core/src/logindexer/util.rs b/kona/crates/supervisor/core/src/logindexer/util.rs new file mode 100644 index 0000000000000..47a2a6848691b --- /dev/null +++ b/kona/crates/supervisor/core/src/logindexer/util.rs @@ -0,0 +1,95 @@ +use alloy_primitives::{Address, B256, Bytes, Log, keccak256}; + +/// Computes the log hash from a payload hash and log address. +/// +/// This is done by: +/// 1. Concatenating the raw 20-byte address with the 32-byte payload hash, +/// 2. Hashing the result with keccak256. +/// +/// This log hash is stored in the log storage and is used to map +/// an executing message back to the original initiating log. +pub fn payload_hash_to_log_hash(payload_hash: B256, addr: Address) -> B256 { + let mut buf = Vec::with_capacity(64); + buf.extend_from_slice(addr.as_slice()); // 20 bytes + buf.extend_from_slice(payload_hash.as_slice()); // 32 bytes + keccak256(&buf) +} + +/// Converts an L2 log into its raw message payload for hashing. +/// +/// This payload is defined as the concatenation of all log topics followed by the log data, +/// in accordance with the OP stack interop messaging spec. +/// +/// This data is what is hashed to produce the `payloadHash`. +pub fn log_to_message_payload(log: &Log) -> Bytes { + let mut payload = Vec::with_capacity(log.topics().len() * 32 + log.data.data.len()); + + // Append each topic in order + for topic in log.topics() { + payload.extend_from_slice(topic.as_slice()); + } + + // Append the raw data + payload.extend_from_slice(&log.data.data); + + payload.into() +} + +/// Computes the full log hash from a log using the OP Stack convention. +pub fn log_to_log_hash(log: &Log) -> B256 { + let payload = log_to_message_payload(log); + let payload_hash = keccak256(&payload); + payload_hash_to_log_hash(payload_hash, log.address) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Bytes, Log, address, b256}; + + /// Creates a dummy log with fixed topics and data for testing. + fn sample_log() -> Log { + Log::new_unchecked( + address!("0xe0e1e2e3e4e5e6e7e8e9f0f1f2f3f4f5f6f7f8f9"), + vec![ + b256!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + b256!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ], + Bytes::from_static(b"example payload"), + ) + } + + #[test] + fn test_log_to_message_payload_is_correct() { + let log = sample_log(); + let payload = log_to_message_payload(&log); + + // Expect: topics + data + let mut expected = Vec::new(); + expected.extend_from_slice(&log.topics()[0].0); + expected.extend_from_slice(&log.topics()[1].0); + expected.extend_from_slice(&log.data.data); + + assert_eq!(payload.as_ref(), expected.as_slice()); + } + + #[test] + fn test_payload_hash_to_log_hash_with_known_value() { + let address = address!("0xe0e1e2e3e4e5e6e7e8e9f0f1f2f3f4f5f6f7f8f9"); + let payload_hash = keccak256(Bytes::from_static(b"example payload")); + let log_hash = payload_hash_to_log_hash(payload_hash, address); + let expected_hash = + b256!("0xf9ed05990c887d3f86718aabd7e940faaa75d6a5cd44602e89642586ce85f2aa"); + + assert_eq!(log_hash, expected_hash); + } + + #[test] + fn test_log_to_log_hash_with_known_value() { + let log = sample_log(); + let actual_log_hash = log_to_log_hash(&log); + let expected_log_hash = + b256!("0x20b21f284fb0286571fbf1cbfc20cdb1d50ea5c74c914478aee4a47b0a82a170"); + assert_eq!(actual_log_hash, expected_log_hash); + } +} diff --git a/kona/crates/supervisor/core/src/reorg/error.rs b/kona/crates/supervisor/core/src/reorg/error.rs new file mode 100644 index 0000000000000..6b92461bddd89 --- /dev/null +++ b/kona/crates/supervisor/core/src/reorg/error.rs @@ -0,0 +1,29 @@ +use kona_supervisor_storage::StorageError; +use thiserror::Error; + +use crate::syncnode::ManagedNodeError; + +/// Error type for reorg handling +#[derive(Debug, Error)] +pub enum ReorgHandlerError { + /// Indicates managed node not found for the chain. + #[error("managed node not found for chain: {0}")] + ManagedNodeMissing(u64), + + /// Indicates an error occurred while interacting with the managed node. + #[error(transparent)] + ManagedNodeError(#[from] ManagedNodeError), + + /// Indicates an error occurred while interacting with the database. + #[error(transparent)] + StorageError(#[from] StorageError), + + /// Indicates an error occurred while interacting with the l1 RPC client. + #[error("failed to interact with l1 RPC client: {0}")] + RPCError(String), + + /// Indicates an error occurred while finding rewind target for reorg. + /// This can happen if the rewind target block is pre-interop. + #[error("rewind target is pre-interop")] + RewindTargetPreInterop, +} diff --git a/kona/crates/supervisor/core/src/reorg/handler.rs b/kona/crates/supervisor/core/src/reorg/handler.rs new file mode 100644 index 0000000000000..c7556ca1f97ca --- /dev/null +++ b/kona/crates/supervisor/core/src/reorg/handler.rs @@ -0,0 +1,91 @@ +use super::metrics::Metrics; +use crate::{ReorgHandlerError, reorg::task::ReorgTask}; +use alloy_primitives::ChainId; +use alloy_rpc_client::RpcClient; +use derive_more::Constructor; +use futures::future; +use kona_protocol::BlockInfo; +use kona_supervisor_metrics::observe_metrics_for_result_async; +use kona_supervisor_storage::{DbReader, StorageRewinder}; +use std::{collections::HashMap, sync::Arc}; +use tracing::{error, info, trace}; + +/// Handles L1 reorg operations for multiple chains +#[derive(Debug, Constructor)] +pub struct ReorgHandler { + /// The Alloy RPC client for L1. + rpc_client: RpcClient, + /// Per chain dbs. + chain_dbs: HashMap>, +} + +impl ReorgHandler +where + DB: DbReader + StorageRewinder + Send + Sync + 'static, +{ + /// Initializes the metrics for the reorg handler + pub fn with_metrics(self) -> Self { + // Initialize metrics for all chains + for chain_id in self.chain_dbs.keys() { + Metrics::init(*chain_id); + } + + self + } + + /// Wrapper method for segregating concerns between the startup and L1 watcher reorg handlers. + pub async fn verify_l1_consistency(&self) -> Result<(), ReorgHandlerError> { + info!( + target: "supervisor::reorg_handler", + "Verifying L1 consistency for each chain..." + ); + + self.verify_and_handle_chain_reorg().await + } + + /// Processes a reorg for all chains when a new latest L1 block is received + pub async fn handle_l1_reorg(&self, latest_block: BlockInfo) -> Result<(), ReorgHandlerError> { + trace!( + target: "supervisor::reorg_handler", + l1_block_number = latest_block.number, + "Potential reorg detected, processing..." + ); + + self.verify_and_handle_chain_reorg().await + } + + /// Verifies the consistency of each chain with the L1 chain and handles any reorgs, if any. + async fn verify_and_handle_chain_reorg(&self) -> Result<(), ReorgHandlerError> { + let mut handles = Vec::with_capacity(self.chain_dbs.len()); + + for (chain_id, chain_db) in &self.chain_dbs { + let reorg_task = + ReorgTask::new(*chain_id, Arc::clone(chain_db), self.rpc_client.clone()); + + let chain_id = *chain_id; + + let handle = tokio::spawn(async move { + observe_metrics_for_result_async!( + Metrics::SUPERVISOR_REORG_SUCCESS_TOTAL, + Metrics::SUPERVISOR_REORG_ERROR_TOTAL, + Metrics::SUPERVISOR_REORG_DURATION_SECONDS, + Metrics::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + async { + reorg_task.process_chain_reorg().await + }, + "chain_id" => chain_id.to_string() + ) + }); + handles.push(handle); + } + + let results = future::join_all(handles).await; + for result in results { + if let Err(err) = result { + error!(target: "supervisor::reorg_handler", %err, "Reorg task failed"); + } + } + + Ok(()) + } +} diff --git a/kona/crates/supervisor/core/src/reorg/metrics.rs b/kona/crates/supervisor/core/src/reorg/metrics.rs new file mode 100644 index 0000000000000..de9d194621578 --- /dev/null +++ b/kona/crates/supervisor/core/src/reorg/metrics.rs @@ -0,0 +1,106 @@ +use alloy_primitives::ChainId; + +/// Metrics for reorg operations +#[derive(Debug, Clone)] +pub(crate) struct Metrics; + +impl Metrics { + pub(crate) const SUPERVISOR_REORG_SUCCESS_TOTAL: &'static str = + "kona_supervisor_reorg_success_total"; + pub(crate) const SUPERVISOR_REORG_ERROR_TOTAL: &'static str = + "kona_supervisor_reorg_error_total"; + pub(crate) const SUPERVISOR_REORG_DURATION_SECONDS: &'static str = + "kona_supervisor_reorg_duration_seconds"; + pub(crate) const SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG: &'static str = + "process_chain_reorg"; + pub(crate) const SUPERVISOR_REORG_L1_DEPTH: &'static str = "kona_supervisor_reorg_l1_depth"; + pub(crate) const SUPERVISOR_REORG_L2_DEPTH: &'static str = "kona_supervisor_reorg_l2_depth"; + + pub(crate) fn init(chain_id: ChainId) { + Self::describe(); + Self::zero(chain_id); + } + + fn describe() { + metrics::describe_counter!( + Self::SUPERVISOR_REORG_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successfully processed L1 reorgs in the supervisor", + ); + + metrics::describe_counter!( + Self::SUPERVISOR_REORG_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of errors encountered while processing L1 reorgs in the supervisor", + ); + + metrics::describe_histogram!( + Self::SUPERVISOR_REORG_L1_DEPTH, + metrics::Unit::Count, + "Depth of the L1 reorg in the supervisor", + ); + + metrics::describe_histogram!( + Self::SUPERVISOR_REORG_L2_DEPTH, + metrics::Unit::Count, + "Depth of the L2 reorg in the supervisor", + ); + + metrics::describe_histogram!( + Self::SUPERVISOR_REORG_DURATION_SECONDS, + metrics::Unit::Seconds, + "Latency for processing L1 reorgs in the supervisor", + ); + } + + fn zero(chain_id: ChainId) { + metrics::counter!( + Self::SUPERVISOR_REORG_SUCCESS_TOTAL, + "chain_id" => chain_id.to_string(), + "method" => Self::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + ) + .increment(0); + + metrics::counter!( + Self::SUPERVISOR_REORG_ERROR_TOTAL, + "chain_id" => chain_id.to_string(), + "method" => Self::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + ) + .increment(0); + + metrics::histogram!( + Self::SUPERVISOR_REORG_L1_DEPTH, + "chain_id" => chain_id.to_string(), + "method" => Self::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + ) + .record(0); + + metrics::histogram!( + Self::SUPERVISOR_REORG_L2_DEPTH, + "chain_id" => chain_id.to_string(), + "method" => Self::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + ) + .record(0); + + metrics::histogram!( + Self::SUPERVISOR_REORG_DURATION_SECONDS, + "chain_id" => chain_id.to_string(), + "method" => Self::SUPERVISOR_REORG_METHOD_PROCESS_CHAIN_REORG, + ) + .record(0.0); + } + + pub(crate) fn record_block_depth(chain_id: ChainId, l1_depth: u64, l2_depth: u64) { + metrics::histogram!( + Self::SUPERVISOR_REORG_L1_DEPTH, + "chain_id" => chain_id.to_string(), + ) + .record(l1_depth as f64); + + metrics::histogram!( + Self::SUPERVISOR_REORG_L2_DEPTH, + "chain_id" => chain_id.to_string(), + ) + .record(l2_depth as f64); + } +} diff --git a/kona/crates/supervisor/core/src/reorg/mod.rs b/kona/crates/supervisor/core/src/reorg/mod.rs new file mode 100644 index 0000000000000..f3c49ac20d611 --- /dev/null +++ b/kona/crates/supervisor/core/src/reorg/mod.rs @@ -0,0 +1,9 @@ +mod task; + +mod handler; +pub use handler::ReorgHandler; + +mod error; +pub use error::ReorgHandlerError; + +mod metrics; diff --git a/kona/crates/supervisor/core/src/reorg/task.rs b/kona/crates/supervisor/core/src/reorg/task.rs new file mode 100644 index 0000000000000..4682cd5bbfc8e --- /dev/null +++ b/kona/crates/supervisor/core/src/reorg/task.rs @@ -0,0 +1,1253 @@ +use super::metrics::Metrics; +use crate::ReorgHandlerError; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::{B256, ChainId}; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_eth::Block; +use derive_more::Constructor; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DbReader, StorageError, StorageRewinder}; +use std::sync::Arc; +use tracing::{debug, info, trace, warn}; + +/// Handles reorg for a single chain +#[derive(Debug, Constructor)] +pub(crate) struct ReorgTask { + chain_id: ChainId, + db: Arc, + rpc_client: RpcClient, +} + +#[derive(Debug)] +struct RewoundState { + source: BlockInfo, + derived: Option, +} + +impl ReorgTask +where + DB: DbReader + StorageRewinder + Send + Sync + 'static, +{ + /// Processes reorg for a single chain. If the chain is consistent with the L1 chain, + /// does nothing. + pub(crate) async fn process_chain_reorg(&self) -> Result<(), ReorgHandlerError> { + trace!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Processing reorg for chain..." + ); + + let latest_state = self.db.latest_derivation_state()?; + + // Find last valid source block for this chain + let rewound_state = match self.find_rewind_target(latest_state).await { + Ok(Some(rewind_target_source)) => { + Some(self.rewind_to_target_source(rewind_target_source).await?) + } + Ok(None) => { + // No reorg needed, latest source block is still canonical + return Ok(()); + } + Err(ReorgHandlerError::RewindTargetPreInterop) => { + self.rewind_to_activation_block().await? + } + Err(err) => { + return Err(err); + } + }; + + // record metrics + if let Some(rewound_state) = rewound_state { + let l1_depth = latest_state.source.number - rewound_state.source.number; + let mut l2_depth = 0; + if let Some(derived) = rewound_state.derived { + l2_depth = latest_state.derived.number - derived.number; + } + Metrics::record_block_depth(self.chain_id, l1_depth, l2_depth); + } + info!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Processed reorged successfully" + ); + Ok(()) + } + + async fn rewind_to_target_source( + &self, + rewind_target_source: BlockInfo, + ) -> Result { + info!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + rewind_target_source = rewind_target_source.number, + "Reorg detected - rewinding to target source block..." + ); + + // Call the rewinder to handle the DB rewinding + let derived_block_rewound = + self.db.rewind_to_source(&rewind_target_source.id()).inspect_err(|err| { + warn!( + target: "supervisor::reorg_handler::db", + chain_id = %self.chain_id, + %err, + "Failed to rewind DB to derived block" + ); + })?; + + Ok(RewoundState { source: rewind_target_source, derived: derived_block_rewound }) + } + + async fn rewind_to_activation_block(&self) -> Result, ReorgHandlerError> { + info!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Reorg detected - rewinding to activation block..." + ); + + // If the rewind target is pre-interop, we need to rewind to the activation block + match self.db.get_activation_block() { + Ok(activation_block) => { + let activation_source_block = self.db.derived_to_source(activation_block.id())?; + self.db.rewind(&activation_block.id()).inspect_err(|err| { + warn!( + target: "supervisor::reorg_handler::db", + chain_id = %self.chain_id, + %err, + "Failed to rewind DB to activation block" + ); + })?; + Ok(Some(RewoundState { + source: activation_source_block, + derived: Some(activation_block), + })) + } + Err(StorageError::DatabaseNotInitialised) => { + debug!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "No activation block found, no rewind required" + ); + Ok(None) + } + Err(err) => Err(ReorgHandlerError::StorageError(err)), + } + } + + /// Finds the rewind target for a chain during a reorg + /// + /// Returns `None` if no rewind is needed, or the target block to rewind to. + /// Returns ReorgHandlerError::RewindTargetPreInterop if the rewind target is before the interop + /// activation block. + async fn find_rewind_target( + &self, + latest_state: DerivedRefPair, + ) -> Result, ReorgHandlerError> { + trace!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Finding rewind target..." + ); + + // Check if the latest source block is still canonical + if self.is_block_canonical(latest_state.source.number, latest_state.source.hash).await? { + debug!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + block_number = latest_state.source.number, + "Latest source block is still canonical, no reorg needed" + ); + return Ok(None); + } + + let common_ancestor = self.find_common_ancestor().await?; + let mut prev_source = latest_state.source; + let mut current_source = self.db.get_source_block(prev_source.number - 1)?; + + while current_source.number > common_ancestor.number { + if current_source.number % 5 == 0 { + trace!( + target: "supervisor::reorg_handler", + current_block=current_source.number, + common_ancestor=common_ancestor.number, + "Finding rewind target..." + ) + } + + // If the current source block is canonical, we found the rewind target + if self.is_block_canonical(current_source.number, current_source.hash).await? { + info!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + block_number = current_source.number, + "Found canonical block as rewind target" + ); + break; + } + + // Otherwise, walk back to the previous source block + prev_source = current_source; + current_source = self.db.get_source_block(current_source.number - 1)?; + } + + // return the previous source block as the rewind target since rewinding is inclusive + Ok(Some(prev_source)) + } + + async fn find_common_ancestor(&self) -> Result { + trace!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Finding common ancestor." + ); + + match self.db.get_safety_head_ref(kona_interop::SafetyLevel::Finalized) { + Ok(finalized_block) => { + let common_ancestor = self.db.derived_to_source(finalized_block.id())?; + return Ok(common_ancestor) + } + Err(StorageError::FutureData) => { /* fall through to activation block */ } + Err(err) => { + return Err(ReorgHandlerError::StorageError(err)); + } + } + + debug!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "No finalized block found, checking activation block." + ); + + match self.db.get_activation_block() { + Ok(activation_block) => { + let activation_source_block = self.db.derived_to_source(activation_block.id())?; + if self + .is_block_canonical( + activation_source_block.number, + activation_source_block.hash, + ) + .await? + { + Ok(activation_source_block) + } else { + debug!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Activation block is not canonical, no common ancestor found" + ); + Err(ReorgHandlerError::RewindTargetPreInterop) + } + } + Err(StorageError::DatabaseNotInitialised) => { + Err(ReorgHandlerError::RewindTargetPreInterop) + } + Err(err) => Err(ReorgHandlerError::StorageError(err)), + } + } + + /// Checks if a block is canonical on L1 + async fn is_block_canonical( + &self, + block_number: u64, + expected_hash: B256, + ) -> Result { + let canonical_l1 = self + .rpc_client + .request::<_, Block>( + "eth_getBlockByNumber", + (BlockNumberOrTag::Number(block_number), false), + ) + .await + .map_err(|err| { + warn!( + target: "supervisor::reorg_handler", + block_number, + %err, + "Failed to fetch L1 block from RPC" + ); + ReorgHandlerError::RPCError(err.to_string()) + })?; + Ok(canonical_l1.hash() == expected_hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::BlockNumHash; + use alloy_rpc_types_eth::Header; + use alloy_transport::mock::*; + use kona_interop::{DerivedRefPair, SafetyLevel}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{ + DerivationStorageReader, HeadRefStorageReader, LogStorageReader, StorageError, + }; + use kona_supervisor_types::{Log, SuperHead}; + use mockall::{mock, predicate}; + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self, block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + + impl HeadRefStorageReader for Db { + fn get_safety_head_ref(&self, safety_level: SafetyLevel) -> Result; + fn get_super_head(&self) -> Result; + } + + impl StorageRewinder for Db { + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError>; + } + ); + + mock! ( + pub chain_db {} + ); + + #[tokio::test] + async fn test_process_chain_reorg_no_reorg_needed() { + let mut mock_db = MockDb::new(); + + let latest_source = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let latest_state = DerivedRefPair { + source: latest_source, + derived: BlockInfo::new(B256::from([3u8; 32]), 50, B256::from([4u8; 32]), 12346), + }; + + // Mock the latest derivation state + mock_db.expect_latest_derivation_state().times(1).returning(move || Ok(latest_state)); + + // Mock the RPC to return the same block (no reorg) + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + let canonical_block: Block = Block { + header: Header { + hash: latest_source.hash, + inner: alloy_consensus::Header { + number: latest_source.number, + parent_hash: latest_source.parent_hash, + timestamp: latest_source.timestamp, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + asserter.push_success(&canonical_block); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + + let result = reorg_task.process_chain_reorg().await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_process_chain_reorg_with_rewind() { + let mut mock_db = MockDb::new(); + + let latest_source = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let latest_state = DerivedRefPair { + source: latest_source, + derived: BlockInfo::new(B256::from([3u8; 32]), 50, B256::from([4u8; 32]), 12346), + }; + + let canonical_source = + BlockInfo::new(B256::from([5u8; 32]), 95, B256::from([6u8; 32]), 12344); + + let rewind_target_source = + BlockInfo::new(B256::from([10u8; 32]), 96, B256::from([11u8; 32]), 12340); + + let rewind_target_derived = + BlockInfo::new(B256::from([12u8; 32]), 45, B256::from([13u8; 32]), 12341); + + let finalized_block = + BlockInfo::new(B256::from([20u8; 32]), 40, B256::from([21u8; 32]), 12330); + + // Mock the latest derivation state + mock_db.expect_latest_derivation_state().times(1).returning(move || Ok(latest_state)); + + // Mock finding common ancestor + mock_db.expect_get_safety_head_ref().times(1).returning(move |_| Ok(finalized_block)); + + mock_db.expect_derived_to_source().times(1).returning(move |_| Ok(canonical_source)); + + mock_db.expect_get_source_block().times(5).returning( + move |block_number| match block_number { + 99 => Ok(BlockInfo::new(B256::from([16u8; 32]), 99, B256::from([17u8; 32]), 12344)), + 98 => Ok(BlockInfo::new(B256::from([17u8; 32]), 98, B256::from([18u8; 32]), 12343)), + 97 => Ok(BlockInfo::new(B256::from([18u8; 32]), 97, B256::from([19u8; 32]), 12342)), + 96 => Ok(rewind_target_source), + 95 => Ok(canonical_source), + _ => Err(StorageError::ConflictError), + }, + ); + + // Mock the RPC to show reorg happened + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + // First call shows different hash (reorg detected) + let different_block: Block = Block { + header: Header { + hash: B256::from([99u8; 32]), // Different hash + inner: alloy_consensus::Header { + number: latest_source.number, + parent_hash: latest_source.parent_hash, + timestamp: latest_source.timestamp, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + asserter.push_success(&different_block); + asserter.push_success(&different_block); + asserter.push_success(&different_block); + asserter.push_success(&different_block); + asserter.push_success(&different_block); + + // Second call for checking if rewind target is canonical + let canonical_block: Block = Block { + header: Header { + hash: canonical_source.hash, + inner: alloy_consensus::Header { + number: canonical_source.number, + parent_hash: canonical_source.parent_hash, + timestamp: canonical_source.timestamp, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + asserter.push_success(&canonical_block); + + // Mock rewind operations + mock_db + .expect_rewind_to_source() + .times(1) + .returning(move |_| Ok(Some(rewind_target_derived))); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + + let result = reorg_task.process_chain_reorg().await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_process_chain_reorg_rewind_pre_interop() { + let mut mock_db = MockDb::new(); + + let latest_source = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let latest_state = DerivedRefPair { + source: latest_source, + derived: BlockInfo::new(B256::from([3u8; 32]), 50, B256::from([4u8; 32]), 12346), + }; + + let activation_block = + BlockInfo::new(B256::from([10u8; 32]), 1, B256::from([11u8; 32]), 12000); + + let activation_source = + BlockInfo::new(B256::from([12u8; 32]), 10, B256::from([13u8; 32]), 11999); + + // Mock the latest derivation state + mock_db.expect_latest_derivation_state().times(1).returning(move || Ok(latest_state)); + + // Mock finding common ancestor fails with pre-interop + mock_db.expect_get_safety_head_ref().times(1).returning(|_| Err(StorageError::FutureData)); + + mock_db + .expect_get_activation_block() + .times(2) // Once in find_common_ancestor, once in rewind_to_activation_block + .returning(move || Ok(activation_block)); + + mock_db + .expect_derived_to_source() + .times(2) // Once in find_common_ancestor, once in rewind_to_activation_block + .returning(move |_| Ok(activation_source)); + + // Mock the RPC calls + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + // First call shows different hash (reorg detected) + let different_block: Block = Block { + header: Header { + hash: B256::from([99u8; 32]), + inner: alloy_consensus::Header { + number: latest_source.number, + parent_hash: latest_source.parent_hash, + timestamp: latest_source.timestamp, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + asserter.push_success(&different_block); + + // Activation block is not canonical + let non_canonical_activation: Block = Block { + header: Header { + hash: B256::from([99u8; 32]), // Different from expected + inner: alloy_consensus::Header { + number: activation_source.number, + parent_hash: activation_source.parent_hash, + timestamp: activation_source.timestamp, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + asserter.push_success(&non_canonical_activation); + + // Mock rewind to activation block + mock_db.expect_rewind().times(1).returning(|_| Ok(())); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + + let result = reorg_task.process_chain_reorg().await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_process_chain_reorg_storage_error() { + let mut mock_db = MockDb::new(); + + // DB fails to get latest derivation state + mock_db + .expect_latest_derivation_state() + .times(1) + .returning(|| Err(StorageError::LockPoisoned)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.process_chain_reorg().await; + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ReorgHandlerError::StorageError(StorageError::LockPoisoned) + )); + } + + #[tokio::test] + async fn test_find_rewind_target_without_reorg() { + let mut mock_db = MockDb::new(); + let latest_source: Block = Block { + header: Header { + hash: B256::from([1u8; 32]), + inner: alloy_consensus::Header { + number: 42, + parent_hash: B256::ZERO, + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let latest_state = DerivedRefPair { + source: BlockInfo::new( + latest_source.header.hash, + latest_source.header.number, + latest_source.header.parent_hash, + latest_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([5u8; 32]), 200, B256::ZERO, 1100), + }; + + // Mock the latest derivation state and expect this to be called once + mock_db.expect_latest_derivation_state().times(1).returning(move || Ok(latest_state)); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + // Mock RPC response + asserter.push_success(&latest_source); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let rewind_target = reorg_task.process_chain_reorg().await; + + // Should succeed since the latest source block is still canonical + assert!(rewind_target.is_ok()); + } + + #[tokio::test] + async fn test_find_rewind_target_with_reorg() { + let mut mock_db = MockDb::new(); + let latest_source: Block = Block { + header: Header { + hash: B256::from([1u8; 32]), + inner: alloy_consensus::Header { + number: 41, + parent_hash: B256::from([2u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let latest_state = DerivedRefPair { + source: BlockInfo::new( + latest_source.header.hash, + latest_source.header.number, + latest_source.header.parent_hash, + latest_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([10u8; 32]), 200, B256::ZERO, 1100), + }; + + let finalized_source: Block = Block { + header: Header { + hash: B256::from([2u8; 32]), + inner: alloy_consensus::Header { + number: 38, + parent_hash: B256::from([1u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let finalized_state = DerivedRefPair { + source: BlockInfo::new( + finalized_source.header.hash, + finalized_source.header.number, + finalized_source.header.parent_hash, + finalized_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([20u8; 32]), 200, B256::ZERO, 1100), + }; + + let reorg_source: Block = Block { + header: Header { + hash: B256::from([14u8; 32]), + inner: alloy_consensus::Header { + number: 40, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let reorg_source_info = BlockInfo::new( + reorg_source.header.hash, + reorg_source.header.number, + reorg_source.header.parent_hash, + reorg_source.header.timestamp, + ); + + let mut source_39: Block = reorg_source.clone(); + source_39.header.inner.number = 39; + let source_39_info = BlockInfo::new( + source_39.header.hash, + source_39.header.number, + source_39.header.parent_hash, + source_39.header.timestamp, + ); + + let incorrect_source: Block = Block { + header: Header { + hash: B256::from([15u8; 32]), + inner: alloy_consensus::Header { + number: 5000, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + mock_db.expect_latest_derivation_state().returning(move || Ok(latest_state)); + mock_db + .expect_get_safety_head_ref() + .times(1) + .returning(move |_| Ok(finalized_state.derived)); + mock_db.expect_derived_to_source().times(1).returning(move |_| Ok(finalized_state.source)); + + mock_db.expect_get_source_block().times(3).returning( + move |block_number| match block_number { + 41 => Ok(latest_state.source), + 40 => Ok(reorg_source_info), + 39 => Ok(source_39_info), + 38 => Ok(finalized_state.source), + _ => Ok(finalized_state.source), + }, + ); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + // First return the reorged block + asserter.push_success(&reorg_source); + + // Then returning some random incorrect blocks 3 times till it reaches the finalized block + asserter.push_success(&incorrect_source); + asserter.push_success(&incorrect_source); + asserter.push_success(&incorrect_source); + + // Finally returning the correct block + asserter.push_success(&finalized_source); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let rewind_target = reorg_task.find_rewind_target(latest_state).await; + + // Should succeed since the latest source block is still canonical + assert!(rewind_target.is_ok()); + assert_eq!(rewind_target.unwrap(), Some(source_39_info)); + } + + #[tokio::test] + async fn test_find_rewind_target_with_finalized_future_activation_canonical() { + let mut mock_db = MockDb::new(); + let latest_source: Block = Block { + header: Header { + hash: B256::from([1u8; 32]), + inner: alloy_consensus::Header { + number: 41, + parent_hash: B256::from([2u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let latest_state = DerivedRefPair { + source: BlockInfo::new( + latest_source.header.hash, + latest_source.header.number, + latest_source.header.parent_hash, + latest_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([10u8; 32]), 200, B256::ZERO, 1100), + }; + + let activation_source: Block = Block { + header: Header { + hash: B256::from([2u8; 32]), + inner: alloy_consensus::Header { + number: 38, + parent_hash: B256::from([1u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let activation_state = DerivedRefPair { + source: BlockInfo::new( + activation_source.header.hash, + activation_source.header.number, + activation_source.header.parent_hash, + activation_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([20u8; 32]), 200, B256::ZERO, 1100), + }; + + let reorg_source: Block = Block { + header: Header { + hash: B256::from([14u8; 32]), + inner: alloy_consensus::Header { + number: 40, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let reorg_source_info = BlockInfo::new( + reorg_source.header.hash, + reorg_source.header.number, + reorg_source.header.parent_hash, + reorg_source.header.timestamp, + ); + + let mut source_39: Block = reorg_source.clone(); + source_39.header.inner.number = 39; + let source_39_info = BlockInfo::new( + source_39.header.hash, + source_39.header.number, + source_39.header.parent_hash, + source_39.header.timestamp, + ); + + let incorrect_source: Block = Block { + header: Header { + hash: B256::from([15u8; 32]), + inner: alloy_consensus::Header { + number: 5000, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + mock_db + .expect_get_safety_head_ref() + .times(1) + .returning(move |_| Err(StorageError::FutureData)); + mock_db + .expect_get_activation_block() + .times(1) + .returning(move || Ok(activation_state.derived)); + mock_db.expect_derived_to_source().times(1).returning(move |_| Ok(activation_state.source)); + + mock_db.expect_get_source_block().times(3).returning( + move |block_number| match block_number { + 41 => Ok(latest_state.source), + 40 => Ok(reorg_source_info), + 39 => Ok(source_39_info), + 38 => Ok(activation_state.source), + _ => Ok(activation_state.source), + }, + ); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + // First return the reorged block + asserter.push_success(&reorg_source); + + // Return the activation block source to make sure it is canonical + // Used in `find_common_ancestor` + asserter.push_success(&activation_source); + + // Then returning some random incorrect blocks 3 times till it reaches the finalized block + asserter.push_success(&incorrect_source); + asserter.push_success(&incorrect_source); + asserter.push_success(&incorrect_source); + + // Finally returning the correct block + asserter.push_success(&activation_source); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let rewind_target = reorg_task.find_rewind_target(latest_state).await; + + // Should succeed since the latest source block is still canonical + assert!(rewind_target.is_ok()); + assert_eq!(rewind_target.unwrap(), Some(source_39_info)); + } + + #[tokio::test] + async fn test_find_rewind_target_with_finalized_future_activation_not_canonical() { + let mut mock_db = MockDb::new(); + let latest_source: Block = Block { + header: Header { + hash: B256::from([1u8; 32]), + inner: alloy_consensus::Header { + number: 41, + parent_hash: B256::from([2u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let latest_state = DerivedRefPair { + source: BlockInfo::new( + latest_source.header.hash, + latest_source.header.number, + latest_source.header.parent_hash, + latest_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([10u8; 32]), 200, B256::ZERO, 1100), + }; + + let activation_source: Block = Block { + header: Header { + hash: B256::from([2u8; 32]), + inner: alloy_consensus::Header { + number: 38, + parent_hash: B256::from([1u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let activation_state = DerivedRefPair { + source: BlockInfo::new( + activation_source.header.hash, + activation_source.header.number, + activation_source.header.parent_hash, + activation_source.header.timestamp, + ), + derived: BlockInfo::new(B256::from([20u8; 32]), 200, B256::ZERO, 1100), + }; + + let reorg_source: Block = Block { + header: Header { + hash: B256::from([14u8; 32]), + inner: alloy_consensus::Header { + number: 40, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let incorrect_source: Block = Block { + header: Header { + hash: B256::from([15u8; 32]), + inner: alloy_consensus::Header { + number: 5000, + parent_hash: B256::from([13u8; 32]), + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + mock_db + .expect_get_safety_head_ref() + .times(1) + .returning(move |_| Err(StorageError::FutureData)); + mock_db + .expect_get_activation_block() + .times(1) + .returning(move || Ok(activation_state.derived)); + mock_db.expect_derived_to_source().times(1).returning(move |_| Ok(activation_state.source)); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + + // First return the reorged block + asserter.push_success(&reorg_source); + + // Return the incorrect source to make sure activation block is not canonical + // Used in `find_common_ancestor` + asserter.push_success(&incorrect_source); + + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let rewind_target = reorg_task.find_rewind_target(latest_state).await; + + assert!(matches!(rewind_target, Err(ReorgHandlerError::RewindTargetPreInterop))); + } + + #[tokio::test] + async fn test_is_block_canonical() { + let canonical_hash = B256::from([1u8; 32]); + let non_canonical_hash = B256::from([2u8; 32]); + + let canonical_block: Block = Block { + header: Header { + hash: canonical_hash, + inner: alloy_consensus::Header { + number: 100, + parent_hash: B256::ZERO, + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let non_canonical_block: Block = Block { + header: Header { + hash: non_canonical_hash, + inner: alloy_consensus::Header { + number: 100, + parent_hash: B256::ZERO, + timestamp: 12345, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let rpc_client = RpcClient::new(transport, false); + asserter.push_success(&canonical_block); + asserter.push_success(&non_canonical_block); + + let reorg_task = ReorgTask::new(1, Arc::new(MockDb::new()), rpc_client); + + let result = reorg_task.is_block_canonical(100, canonical_hash).await; + assert!(result.is_ok()); + + // Should return false + let result = reorg_task.is_block_canonical(100, canonical_hash).await; + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[tokio::test] + async fn test_rewind_to_activation_block_success() { + let mut mock_db = MockDb::new(); + + let activation_block = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let activation_source = + BlockInfo::new(B256::from([3u8; 32]), 200, B256::from([4u8; 32]), 12346); + + // Expect get_activation_block to be called + mock_db.expect_get_activation_block().times(1).returning(move || Ok(activation_block)); + + // Expect derived_to_source to be called + mock_db + .expect_derived_to_source() + .times(1) + .with(mockall::predicate::eq(activation_block.id())) + .returning(move |_| Ok(activation_source)); + + // Expect rewind to be called + mock_db + .expect_rewind() + .times(1) + .with(mockall::predicate::eq(activation_block.id())) + .returning(|_| Ok(())); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_activation_block().await; + + assert!(result.is_ok()); + let pair = result.unwrap().unwrap(); + assert_eq!(pair.source, activation_source); + assert_eq!(pair.derived.unwrap(), activation_block); + } + + #[tokio::test] + async fn test_rewind_to_activation_block_database_not_initialized() { + let mut mock_db = MockDb::new(); + + // Expect get_activation_block to return DatabaseNotInitialised + mock_db + .expect_get_activation_block() + .times(1) + .returning(|| Err(StorageError::DatabaseNotInitialised)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_activation_block().await; + + // Should succeed with None (no-op case) + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + + #[tokio::test] + async fn test_rewind_to_activation_block_storage_error() { + let mut mock_db = MockDb::new(); + + // Expect get_activation_block to return a different storage error + mock_db + .expect_get_activation_block() + .times(1) + .returning(|| Err(StorageError::LockPoisoned)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_activation_block().await; + + // Should return storage error + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ReorgHandlerError::StorageError(StorageError::LockPoisoned) + )); + } + + #[tokio::test] + async fn test_rewind_to_activation_block_derived_to_source_fails() { + let mut mock_db = MockDb::new(); + + let activation_block = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + // Expect get_activation_block to succeed + mock_db.expect_get_activation_block().times(1).returning(move || Ok(activation_block)); + + // Expect derived_to_source to fail + mock_db.expect_derived_to_source().times(1).returning(|_| Err(StorageError::LockPoisoned)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_activation_block().await; + + // Should return storage error + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ReorgHandlerError::StorageError(StorageError::LockPoisoned) + )); + } + + #[tokio::test] + async fn test_rewind_to_activation_block_rewind_fails() { + let mut mock_db = MockDb::new(); + + let activation_block = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let activation_source = + BlockInfo::new(B256::from([3u8; 32]), 200, B256::from([4u8; 32]), 12346); + + // Expect get_activation_block to succeed + mock_db.expect_get_activation_block().times(1).returning(move || Ok(activation_block)); + + // Expect derived_to_source to succeed + mock_db.expect_derived_to_source().times(1).returning(move |_| Ok(activation_source)); + + // Expect rewind to fail + mock_db.expect_rewind().times(1).returning(|_| Err(StorageError::LockPoisoned)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_activation_block().await; + + // Should return storage error + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ReorgHandlerError::StorageError(StorageError::LockPoisoned) + )); + } + + #[tokio::test] + async fn test_rewind_to_target_source_success() { + let mut mock_db = MockDb::new(); + + let rewind_target_source = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + let rewind_target_derived = + BlockInfo::new(B256::from([3u8; 32]), 50, B256::from([4u8; 32]), 12346); + + // Expect rewind to be called + mock_db + .expect_rewind_to_source() + .times(1) + .with(predicate::eq(rewind_target_source.id())) + .returning(move |_| Ok(Some(rewind_target_derived))); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_target_source(rewind_target_source).await; + + assert!(result.is_ok()); + let pair = result.unwrap(); + assert_eq!(pair.source, rewind_target_source); + assert_eq!(pair.derived.unwrap(), rewind_target_derived); + } + + #[tokio::test] + async fn test_rewind_to_target_source_rewind_fails() { + let mut mock_db = MockDb::new(); + + let rewind_target_source = + BlockInfo::new(B256::from([1u8; 32]), 100, B256::from([2u8; 32]), 12345); + + // Expect rewind to fail + mock_db.expect_rewind_to_source().times(1).returning(|_| Err(StorageError::LockPoisoned)); + + let reorg_task = ReorgTask::new( + 1, + Arc::new(mock_db), + RpcClient::new(MockTransport::new(Asserter::new()), false), + ); + + let result = reorg_task.rewind_to_target_source(rewind_target_source).await; + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ReorgHandlerError::StorageError(StorageError::LockPoisoned) + )); + } +} diff --git a/kona/crates/supervisor/core/src/rpc/admin.rs b/kona/crates/supervisor/core/src/rpc/admin.rs new file mode 100644 index 0000000000000..ab00f720af22c --- /dev/null +++ b/kona/crates/supervisor/core/src/rpc/admin.rs @@ -0,0 +1,216 @@ +use crate::syncnode::ClientConfig; +use alloy_rpc_types_engine::JwtSecret; +use async_trait::async_trait; +use derive_more::Constructor; +use jsonrpsee::{ + core::RpcResult, + types::{ErrorCode, ErrorObject, ErrorObjectOwned}, +}; +use kona_supervisor_rpc::SupervisorAdminApiServer; +use std::time::Duration; +use thiserror::Error; +use tokio::{ + sync::{mpsc::Sender, oneshot}, + time::timeout, +}; +use tracing::warn; + +/// Error types for Supervisor Admin RPC operations. +#[derive(Debug, Error)] +pub enum AdminError { + /// Indicates that the JWT secret is invalid. + #[error("invalid jwt secret: {0}")] + InvalidJwtSecret(String), + + /// Indicates that the request to the admin channel failed to send. + #[error("failed to send admin request")] + SendFailed, + + /// Indicates that the sender dropped before a response was received. + #[error("admin request sender dropped")] + SenderDropped, + + /// Indicates that the admin request timed out. + #[error("admin request timed out")] + Timeout, + + /// Indicates a service error occurred during processing the request. + #[error("service error: {0}")] + ServiceError(String), +} + +impl From for ErrorObjectOwned { + fn from(err: AdminError) -> Self { + match err { + // todo: handle these errors more gracefully + AdminError::InvalidJwtSecret(_) => ErrorObjectOwned::from(ErrorCode::InvalidParams), + AdminError::SendFailed | + AdminError::SenderDropped | + AdminError::Timeout | + AdminError::ServiceError(_) => ErrorObjectOwned::from(ErrorCode::InternalError), + } + } +} + +// timeout for admin requests (seconds) +const ADMIN_REQUEST_TIMEOUT_SECS: u64 = 3; + +/// Represents Admin Request types +#[derive(Debug)] +pub enum AdminRequest { + /// Adds a new L2 RPC to the Supervisor. + AddL2Rpc { + /// The configuration for the L2 RPC client. + cfg: ClientConfig, + /// The response channel to send the result back. + resp: oneshot::Sender>, + }, +} + +/// Supervisor Admin RPC interface +#[derive(Debug, Constructor)] +pub struct AdminRpc { + admin_tx: Sender, +} + +#[async_trait] +impl SupervisorAdminApiServer for AdminRpc { + /// Adds L2RPC to the supervisor. + async fn add_l2_rpc(&self, url: String, secret: String) -> RpcResult<()> { + let (resp_tx, resp_rx) = oneshot::channel(); + + let jwt_secret = JwtSecret::from_hex(secret).map_err(|err| { + warn!(target: "supervisor::admin_rpc", %url, %err, "Failed to decode JWT secret"); + ErrorObject::from(AdminError::InvalidJwtSecret(err.to_string())) + })?; + + let request = AdminRequest::AddL2Rpc { + cfg: ClientConfig { url: url.clone(), jwt_secret }, + resp: resp_tx, + }; + + self.admin_tx.send(request).await.map_err(|err| { + warn!(target: "supervisor::admin_rpc", %url, %err, "Failed to send AdminRequest"); + ErrorObject::from(AdminError::SendFailed) + })?; + + // wait for response with a timeout + timeout(Duration::from_secs(ADMIN_REQUEST_TIMEOUT_SECS), resp_rx) + .await + .map_or_else( + |_| { + warn!(target: "supervisor::admin_rpc", %url, "AdminRequest timed out"); + Err(ErrorObject::from(AdminError::Timeout)) + }, + |res| res + .unwrap_or(Err(AdminError::SenderDropped)) + .map_err(|err| { + warn!(target: "supervisor::admin_rpc", %url, %err, "Failed to process AdminRequest"); + ErrorObject::from(err) + }), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::{ + sync::mpsc, + time::{self, Duration}, + }; + + // valid 32-byte hex (64 hex chars) + const VALID_SECRET: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + #[tokio::test] + async fn test_add_l2_rpc_success() { + let (tx, mut rx) = mpsc::channel::(1); + let admin = AdminRpc::new(tx.clone()); + + // spawn a task that simulates the service handling the admin request + let handler = tokio::spawn(async move { + if let Some(AdminRequest::AddL2Rpc { cfg, resp }) = rx.recv().await { + assert_eq!(cfg.url, "http://node:8545"); + // reply success + let _ = resp.send(Ok(())); + } else { + panic!("expected AddL2Rpc request"); + } + }); + + let res = admin.add_l2_rpc("http://node:8545".to_string(), VALID_SECRET.to_string()).await; + assert!(res.is_ok(), "expected successful response"); + + handler.await.unwrap(); + } + + #[tokio::test] + async fn test_add_l2_rpc_invalid_jwt() { + // admin with working channel (not used because parsing fails early) + let (tx, _rx) = mpsc::channel::(1); + let admin = AdminRpc::new(tx); + + let res = admin.add_l2_rpc("http://node:8545".to_string(), "zzzz".to_string()).await; + assert!(res.is_err(), "expected error for invalid jwt secret"); + } + + #[tokio::test] + async fn test_add_l2_rpc_send_failed() { + // create channel and drop the receiver to force send() -> Err + let (tx, rx) = mpsc::channel::(1); + drop(rx); + let admin = AdminRpc::new(tx); + + let res = admin.add_l2_rpc("http://node:8545".to_string(), VALID_SECRET.to_string()).await; + assert!(res.is_err(), "expected error when admin channel receiver is closed"); + } + + #[tokio::test] + async fn test_add_l2_rpc_service_response_dropped() { + let (tx, mut rx) = mpsc::channel::(1); + let admin = AdminRpc::new(tx.clone()); + + // handler drops the response sender to simulate service failure before replying + let handler = tokio::spawn(async move { + if let Some(AdminRequest::AddL2Rpc { cfg: _, resp }) = rx.recv().await { + // drop the sender without sending -> receiver side will get Err + drop(resp); + } else { + panic!("expected AddL2Rpc request"); + } + }); + + let res = admin.add_l2_rpc("http://node:8545".to_string(), VALID_SECRET.to_string()).await; + assert!(res.is_err(), "expected error when service drops response channel"); + handler.await.unwrap(); + } + + #[tokio::test] + async fn test_add_l2_rpc_timeout() { + // use a handler that receives the request but does not reply (keeps sender alive) + let (tx, mut rx) = mpsc::channel::(1); + let admin = AdminRpc::new(tx.clone()); + + let handler = tokio::spawn(async move { + if let Some(AdminRequest::AddL2Rpc { cfg: _, resp: _ }) = rx.recv().await { + // hold the sender (do nothing) so the rpc call times out + // keep the future alive long enough (we use tokio::time::advance in the test) + time::sleep(Duration::from_secs(ADMIN_REQUEST_TIMEOUT_SECS + 5)).await; + } else { + panic!("expected AddL2Rpc request"); + } + }); + + // call the rpc concurrently + let call = tokio::spawn(async move { + admin.add_l2_rpc("http://node:8545".to_string(), VALID_SECRET.to_string()).await + }); + + let res = call.await.unwrap(); + assert!(res.is_err(), "expected timeout error for long-running admin handler"); + + // let handler finish cleanly + handler.await.unwrap(); + } +} diff --git a/kona/crates/supervisor/core/src/rpc/metrics.rs b/kona/crates/supervisor/core/src/rpc/metrics.rs new file mode 100644 index 0000000000000..c9f26ac2b8d98 --- /dev/null +++ b/kona/crates/supervisor/core/src/rpc/metrics.rs @@ -0,0 +1,125 @@ +//! Metrics for the Supervisor RPC service. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub(crate) struct Metrics; + +impl Metrics { + // --- Metric Names --- + /// Identifier for the counter of successful RPC requests. Labels: `method`. + pub(crate) const SUPERVISOR_RPC_REQUESTS_SUCCESS_TOTAL: &'static str = + "supervisor_rpc_requests_success_total"; + /// Identifier for the counter of failed RPC requests. Labels: `method`. + pub(crate) const SUPERVISOR_RPC_REQUESTS_ERROR_TOTAL: &'static str = + "supervisor_rpc_requests_error_total"; + /// Identifier for the histogram of RPC request durations. Labels: `method`. + pub(crate) const SUPERVISOR_RPC_REQUEST_DURATION_SECONDS: &'static str = + "supervisor_rpc_request_duration_seconds"; + + pub(crate) const SUPERVISOR_RPC_METHOD_CROSS_DERIVED_TO_SOURCE: &'static str = + "cross_derived_to_source"; + pub(crate) const SUPERVISOR_RPC_METHOD_DEPENDENCY_SET: &'static str = "dependency_set"; + pub(crate) const SUPERVISOR_RPC_METHOD_LOCAL_UNSAFE: &'static str = "local_unsafe"; + pub(crate) const SUPERVISOR_RPC_METHOD_LOCAL_SAFE: &'static str = "local_safe"; + pub(crate) const SUPERVISOR_RPC_METHOD_CROSS_SAFE: &'static str = "cross_safe"; + pub(crate) const SUPERVISOR_RPC_METHOD_FINALIZED: &'static str = "finalized"; + pub(crate) const SUPERVISOR_RPC_METHOD_FINALIZED_L1: &'static str = "finalized_l1"; + pub(crate) const SUPERVISOR_RPC_METHOD_SUPER_ROOT_AT_TIMESTAMP: &'static str = + "super_root_at_timestamp"; + pub(crate) const SUPERVISOR_RPC_METHOD_SYNC_STATUS: &'static str = "sync_status"; + pub(crate) const SUPERVISOR_RPC_METHOD_ALL_SAFE_DERIVED_AT: &'static str = + "all_safe_derived_at"; + pub(crate) const SUPERVISOR_RPC_METHOD_CHECK_ACCESS_LIST: &'static str = "check_access_list"; + + /// Initializes metrics for the Supervisor RPC service. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics with their labels to 0 so they can be queried immediately. + pub(crate) fn init() { + Self::describe(); + Self::zero(); + } + + /// Describes metrics used in the Supervisor RPC service. + fn describe() { + metrics::describe_counter!( + Self::SUPERVISOR_RPC_REQUESTS_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successful RPC requests processed by the supervisor" + ); + metrics::describe_counter!( + Self::SUPERVISOR_RPC_REQUESTS_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of failed RPC requests processed by the supervisor" + ); + metrics::describe_histogram!( + Self::SUPERVISOR_RPC_REQUEST_DURATION_SECONDS, + metrics::Unit::Seconds, + "Duration of RPC requests processed by the supervisor" + ); + } + + fn zero_rpc_method(method: &str) { + metrics::counter!( + Self::SUPERVISOR_RPC_REQUESTS_SUCCESS_TOTAL, + "method" => method.to_string() + ) + .increment(0); + + metrics::counter!( + Self::SUPERVISOR_RPC_REQUESTS_ERROR_TOTAL, + "method" => method.to_string() + ) + .increment(0); + + metrics::histogram!( + Self::SUPERVISOR_RPC_REQUEST_DURATION_SECONDS, + "method" => method.to_string() + ) + .record(0.0); // Record a zero value to ensure the label combination is present + } + + /// Initializes metrics with their labels to `0` so they appear in Prometheus from the start. + fn zero() { + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_CROSS_DERIVED_TO_SOURCE); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_LOCAL_UNSAFE); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_LOCAL_SAFE); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_CROSS_SAFE); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_FINALIZED); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_FINALIZED_L1); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_SUPER_ROOT_AT_TIMESTAMP); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_SYNC_STATUS); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_ALL_SAFE_DERIVED_AT); + Self::zero_rpc_method(Self::SUPERVISOR_RPC_METHOD_CHECK_ACCESS_LIST); + } +} + +/// Observes an RPC call, recording its duration and outcome. +/// +/// # Usage +/// ```ignore +/// async fn my_rpc_method(&self, arg: u32) -> RpcResult { +/// observe_rpc_call!("my_rpc_method_name", { +/// // todo: add actual RPC logic +/// if arg == 0 { Ok("success".to_string()) } else { Err(ErrorObject::owned(1, "failure", None::<()>)) } +/// }) +/// } +/// ``` +#[macro_export] +macro_rules! observe_rpc_call { + ($method_name:expr, $block:expr) => {{ + let start_time = std::time::Instant::now(); + let result = $block; // Execute the provided code block + let duration = start_time.elapsed().as_secs_f64(); + + if result.is_ok() { + metrics::counter!($crate::rpc::metrics::Metrics::SUPERVISOR_RPC_REQUESTS_SUCCESS_TOTAL, "method" => $method_name).increment(1); + } else { + metrics::counter!($crate::rpc::metrics::Metrics::SUPERVISOR_RPC_REQUESTS_ERROR_TOTAL, "method" => $method_name).increment(1); + } + + metrics::histogram!($crate::rpc::metrics::Metrics::SUPERVISOR_RPC_REQUEST_DURATION_SECONDS, "method" => $method_name).record(duration); + result + }}; +} diff --git a/kona/crates/supervisor/core/src/rpc/mod.rs b/kona/crates/supervisor/core/src/rpc/mod.rs new file mode 100644 index 0000000000000..8998551b1f84c --- /dev/null +++ b/kona/crates/supervisor/core/src/rpc/mod.rs @@ -0,0 +1,10 @@ +//! Supervisor RPC module + +mod server; +pub use server::SupervisorRpc; + +mod admin; +pub use admin::{AdminError, AdminRequest, AdminRpc}; + +mod metrics; +pub(crate) use metrics::Metrics; diff --git a/kona/crates/supervisor/core/src/rpc/server.rs b/kona/crates/supervisor/core/src/rpc/server.rs new file mode 100644 index 0000000000000..7154ea89e1fb0 --- /dev/null +++ b/kona/crates/supervisor/core/src/rpc/server.rs @@ -0,0 +1,520 @@ +//! Server-side implementation of the Supervisor RPC API. + +use super::Metrics; +use crate::{SpecError, SupervisorError, SupervisorService}; +use alloy_eips::eip1898::BlockNumHash; +use alloy_primitives::{B256, ChainId, map::HashMap}; +use async_trait::async_trait; +use jsonrpsee::{core::RpcResult, types::ErrorObject}; +use kona_interop::{DependencySet, DerivedIdPair, ExecutingDescriptor, SafetyLevel}; +use kona_protocol::BlockInfo; +use kona_supervisor_rpc::{ + SuperRootOutputRpc, SupervisorApiServer, SupervisorChainSyncStatus, SupervisorSyncStatus, +}; +use kona_supervisor_types::{HexStringU64, SuperHead}; +use std::sync::Arc; +use tracing::{trace, warn}; + +/// The server-side implementation struct for the [`SupervisorApiServer`]. +/// It holds a reference to the core Supervisor logic. +#[derive(Debug)] +pub struct SupervisorRpc { + /// Reference to the core Supervisor logic. + /// Using Arc allows sharing the Supervisor instance if needed, + supervisor: Arc, +} + +impl SupervisorRpc { + /// Creates a new [`SupervisorRpc`] instance. + pub fn new(supervisor: Arc) -> Self { + Metrics::init(); + trace!(target: "supervisor::rpc", "Creating new SupervisorRpc handler"); + Self { supervisor } + } +} + +#[async_trait] +impl SupervisorApiServer for SupervisorRpc +where + T: SupervisorService + 'static, +{ + async fn cross_derived_to_source( + &self, + chain_id_hex: HexStringU64, + derived: BlockNumHash, + ) -> RpcResult { + let chain_id = ChainId::from(chain_id_hex); + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_CROSS_DERIVED_TO_SOURCE, + async { + trace!( + target: "supervisor::rpc", + %chain_id, + ?derived, + "Received cross_derived_to_source request" + ); + + let source_block = + self.supervisor.derived_to_source_block(chain_id, derived).map_err(|err| { + warn!( + target: "supervisor::rpc", + %chain_id, + ?derived, + %err, + "Failed to get source block for derived block" + ); + ErrorObject::from(err) + })?; + + Ok(source_block) + } + .await + ) + } + + async fn local_unsafe(&self, chain_id_hex: HexStringU64) -> RpcResult { + let chain_id = ChainId::from(chain_id_hex); + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_LOCAL_UNSAFE, + async { + trace!(target: "supervisor::rpc", + %chain_id, + "Received local_unsafe request" + ); + + Ok(self.supervisor.local_unsafe(chain_id)?.id()) + } + .await + ) + } + + async fn local_safe(&self, chain_id_hex: HexStringU64) -> RpcResult { + let chain_id = ChainId::from(chain_id_hex); + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_LOCAL_SAFE, + async { + trace!(target: "supervisor::rpc", + %chain_id, + "Received local_safe request" + ); + + let derived = self.supervisor.local_safe(chain_id)?.id(); + let source = self.supervisor.derived_to_source_block(chain_id, derived)?.id(); + + Ok(DerivedIdPair { source, derived }) + } + .await + ) + } + + async fn dependency_set_v1(&self) -> RpcResult { + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_DEPENDENCY_SET, + async { + trace!(target: "supervisor::rpc", + "Received the dependency set" + ); + + Ok(self.supervisor.dependency_set().to_owned()) + } + .await + ) + } + + async fn cross_safe(&self, chain_id_hex: HexStringU64) -> RpcResult { + let chain_id = ChainId::from(chain_id_hex); + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_CROSS_SAFE, + async { + trace!(target: "supervisor::rpc", + %chain_id, + "Received cross_safe request" + ); + + let derived = self.supervisor.cross_safe(chain_id)?.id(); + let source = self.supervisor.derived_to_source_block(chain_id, derived)?.id(); + + Ok(DerivedIdPair { source, derived }) + } + .await + ) + } + + async fn finalized(&self, chain_id_hex: HexStringU64) -> RpcResult { + let chain_id = ChainId::from(chain_id_hex); + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_FINALIZED, + async { + trace!(target: "supervisor::rpc", + %chain_id, + "Received finalized request" + ); + + Ok(self.supervisor.finalized(chain_id)?.id()) + } + .await + ) + } + + async fn finalized_l1(&self) -> RpcResult { + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_FINALIZED_L1, + async { + trace!(target: "supervisor::rpc", "Received finalized_l1 request"); + Ok(self.supervisor.finalized_l1()?) + } + .await + ) + } + + async fn super_root_at_timestamp( + &self, + timestamp_hex: HexStringU64, + ) -> RpcResult { + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_SUPER_ROOT_AT_TIMESTAMP, + async { + let timestamp = u64::from(timestamp_hex); + trace!(target: "supervisor::rpc", + %timestamp, + "Received super_root_at_timestamp request" + ); + + self.supervisor.super_root_at_timestamp(timestamp) + .await + .map_err(|err| { + warn!(target: "supervisor::rpc", %err, "Error from core supervisor super_root_at_timestamp"); + ErrorObject::from(err) + }) + }.await + ) + } + + async fn check_access_list( + &self, + inbox_entries: Vec, + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> RpcResult<()> { + // TODO:: refactor, maybe build proc macro to record metrics + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_CHECK_ACCESS_LIST, + async { + trace!(target: "supervisor::rpc", + num_inbox_entries = inbox_entries.len(), + ?min_safety, + ?executing_descriptor, + "Received check_access_list request", + ); + self.supervisor + .check_access_list(inbox_entries, min_safety, executing_descriptor) + .map_err(|err| { + warn!(target: "supervisor::rpc", %err, "Error from core supervisor check_access_list"); + ErrorObject::from(err) + }) + }.await + ) + } + + async fn sync_status(&self) -> RpcResult { + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_SYNC_STATUS, + async { + trace!(target: "supervisor::rpc", "Received sync_status request"); + + let mut chains = self + .supervisor + .chain_ids() + .map(|id| (id, Default::default())) + .collect::>(); + + if chains.is_empty() { + // return error if no chains configured + // + // + // + // todo: add to spec + Err(SupervisorError::EmptyDependencySet)?; + } + + let mut min_synced_l1 = BlockInfo { number: u64::MAX, ..Default::default() }; + let mut cross_safe_timestamp = u64::MAX; + let mut finalized_timestamp = u64::MAX; + let mut uninitialized_chain_db_count = 0; + + for (id, status) in chains.iter_mut() { + let head = match self.supervisor.super_head(*id) { + Ok(head) => head, + Err(SupervisorError::SpecError(SpecError::ErrorNotInSpec)) => { + uninitialized_chain_db_count += 1; + continue; + } + Err(err) => return Err(ErrorObject::from(err)), + }; + + // uses lowest safe and finalized timestamps, as well as l1 block, of all l2s + // + // + // + // todo: add to spec + let SuperHead { l1_source, cross_safe, finalized, .. } = &head; + + let default_block = BlockInfo::default(); + let l1_source = l1_source.as_ref().unwrap_or(&default_block); + let cross_safe = cross_safe.as_ref().unwrap_or(&default_block); + let finalized = finalized.as_ref().unwrap_or(&default_block); + + if l1_source.number < min_synced_l1.number { + min_synced_l1 = *l1_source; + } + if cross_safe.timestamp < cross_safe_timestamp { + cross_safe_timestamp = cross_safe.timestamp; + } + if finalized.timestamp < finalized_timestamp { + finalized_timestamp = finalized.timestamp; + } + + *status = head.into(); + } + + if uninitialized_chain_db_count == chains.len() { + warn!(target: "supervisor::rpc", "No chain db initialized"); + return Err(ErrorObject::from(SupervisorError::SpecError( + SpecError::ErrorNotInSpec, + ))); + } + + Ok(SupervisorSyncStatus { + min_synced_l1, + cross_safe_timestamp, + finalized_timestamp, + chains, + }) + } + .await + ) + } + + async fn all_safe_derived_at( + &self, + derived_from: BlockNumHash, + ) -> RpcResult> { + crate::observe_rpc_call!( + Metrics::SUPERVISOR_RPC_METHOD_ALL_SAFE_DERIVED_AT, + async { + trace!(target: "supervisor::rpc", + ?derived_from, + "Received all_safe_derived_at request" + ); + + let mut chains = self + .supervisor + .chain_ids() + .map(|id| (id, Default::default())) + .collect::>(); + + for (id, block) in chains.iter_mut() { + *block = self.supervisor.latest_block_from(derived_from, *id)?.id(); + } + + Ok(chains) + } + .await + ) + } +} + +impl Clone for SupervisorRpc { + fn clone(&self) -> Self { + Self { supervisor: self.supervisor.clone() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::ChainId; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{EntryNotFoundError, StorageError}; + use mockall::*; + use std::sync::Arc; + + mock!( + #[derive(Debug)] + pub SupervisorService {} + + #[async_trait] + impl SupervisorService for SupervisorService { + fn chain_ids(&self) -> impl Iterator; + fn dependency_set(&self) -> &DependencySet; + fn super_head(&self, chain: ChainId) -> Result; + fn latest_block_from(&self, l1_block: BlockNumHash, chain: ChainId) -> Result; + fn derived_to_source_block(&self, chain: ChainId, derived: BlockNumHash) -> Result; + fn local_unsafe(&self, chain: ChainId) -> Result; + fn local_safe(&self, chain: ChainId) -> Result; + fn cross_safe(&self, chain: ChainId) -> Result; + fn finalized(&self, chain: ChainId) -> Result; + fn finalized_l1(&self) -> Result; + fn check_access_list(&self, inbox_entries: Vec, min_safety: SafetyLevel, executing_descriptor: ExecutingDescriptor) -> Result<(), SupervisorError>; + async fn super_root_at_timestamp(&self, timestamp: u64) -> Result; + } + ); + + #[tokio::test] + async fn test_sync_status_empty_chains() { + let mut mock_service = MockSupervisorService::new(); + mock_service.expect_chain_ids().returning(|| Box::new(vec![].into_iter())); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), ErrorObject::from(SupervisorError::EmptyDependencySet)); + } + + #[tokio::test] + async fn test_sync_status_single_chain() { + let chain_id = ChainId::from(1u64); + + let block_info = BlockInfo { number: 42, ..Default::default() }; + let super_head = SuperHead { + l1_source: Some(block_info), + cross_safe: Some(BlockInfo { timestamp: 100, ..Default::default() }), + finalized: Some(BlockInfo { timestamp: 50, ..Default::default() }), + ..Default::default() + }; + + let mut mock_service = MockSupervisorService::new(); + mock_service.expect_chain_ids().returning(move || Box::new(vec![chain_id].into_iter())); + mock_service.expect_super_head().returning(move |_| Ok(super_head)); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await.unwrap(); + + assert_eq!(result.min_synced_l1.number, 42); + assert_eq!(result.cross_safe_timestamp, 100); + assert_eq!(result.finalized_timestamp, 50); + assert_eq!(result.chains.len(), 1); + } + + #[tokio::test] + async fn test_sync_status_missing_super_head() { + let chain_id_1 = ChainId::from(1u64); + let chain_id_2 = ChainId::from(2u64); + + // Only chain_id_1 has a SuperHead, chain_id_2 is missing + let block_info = BlockInfo { number: 42, ..Default::default() }; + let super_head = SuperHead { + l1_source: Some(block_info), + cross_safe: Some(BlockInfo { timestamp: 100, ..Default::default() }), + finalized: Some(BlockInfo { timestamp: 50, ..Default::default() }), + ..Default::default() + }; + + let mut mock_service = MockSupervisorService::new(); + mock_service + .expect_chain_ids() + .returning(move || Box::new(vec![chain_id_1, chain_id_2].into_iter())); + mock_service.expect_super_head().returning(move |chain_id| { + if chain_id == chain_id_1 { + Ok(super_head) + } else { + Err(SupervisorError::StorageError(StorageError::EntryNotFound( + EntryNotFoundError::DerivedBlockNotFound(1), + ))) + } + }); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_sync_status_uninitialized_chain_db() { + let chain_id_1 = ChainId::from(1u64); + let chain_id_2 = ChainId::from(2u64); + + // Case 1: No chain db is initialized + let mut mock_service = MockSupervisorService::new(); + mock_service + .expect_chain_ids() + .returning(move || Box::new(vec![chain_id_1, chain_id_2].into_iter())); + mock_service + .expect_super_head() + .times(2) + .returning(move |_| Err(SupervisorError::SpecError(SpecError::ErrorNotInSpec))); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ErrorObject::from(SupervisorError::SpecError(SpecError::ErrorNotInSpec,)) + ); + + // Case 2: Only one chain db is initialized + let mut super_head_map = std::collections::HashMap::new(); + let block_info = BlockInfo { number: 42, ..Default::default() }; + let super_head = SuperHead { + l1_source: Some(block_info), + cross_safe: Some(BlockInfo { timestamp: 100, ..Default::default() }), + finalized: Some(BlockInfo { timestamp: 50, ..Default::default() }), + ..Default::default() + }; + super_head_map.insert(chain_id_1, super_head); + + let mut mock_service = MockSupervisorService::new(); + mock_service + .expect_chain_ids() + .returning(move || Box::new(vec![chain_id_1, chain_id_2].into_iter())); + mock_service.expect_super_head().times(2).returning(move |chain_id| { + if chain_id == chain_id_1 { + Ok(super_head) + } else { + Err(SupervisorError::SpecError(SpecError::ErrorNotInSpec)) + } + }); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await; + assert!(result.is_ok()); + + // Case 3: Both chain dbs are initialized + let mut super_head_map = std::collections::HashMap::new(); + let block_info_1 = BlockInfo { number: 42, ..Default::default() }; + let super_head_1 = SuperHead { + l1_source: Some(block_info_1), + cross_safe: Some(BlockInfo { timestamp: 100, ..Default::default() }), + finalized: Some(BlockInfo { timestamp: 50, ..Default::default() }), + ..Default::default() + }; + let block_info_2 = BlockInfo { number: 43, ..Default::default() }; + let super_head_2 = SuperHead { + l1_source: Some(block_info_2), + cross_safe: Some(BlockInfo { timestamp: 110, ..Default::default() }), + finalized: Some(BlockInfo { timestamp: 60, ..Default::default() }), + ..Default::default() + }; + super_head_map.insert(chain_id_1, super_head_1); + super_head_map.insert(chain_id_2, super_head_2); + + let mut mock_service = MockSupervisorService::new(); + mock_service + .expect_chain_ids() + .returning(move || Box::new(vec![chain_id_1, chain_id_2].into_iter())); + mock_service.expect_super_head().times(2).returning(move |chain_id| { + if chain_id == chain_id_1 { Ok(super_head_1) } else { Ok(super_head_2) } + }); + + let rpc = SupervisorRpc::new(Arc::new(mock_service)); + let result = rpc.sync_status().await; + assert!(result.is_ok()); + let status = result.unwrap(); + assert_eq!(status.min_synced_l1.number, 42); + assert_eq!(status.cross_safe_timestamp, 100); + assert_eq!(status.finalized_timestamp, 50); + assert_eq!(status.chains.len(), 2); + } +} diff --git a/kona/crates/supervisor/core/src/safety_checker/cross.rs b/kona/crates/supervisor/core/src/safety_checker/cross.rs new file mode 100644 index 0000000000000..7fcca42a2f1c4 --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/cross.rs @@ -0,0 +1,842 @@ +use crate::{ + CrossSafetyError, + safety_checker::{ValidationError, ValidationError::InitiatingMessageNotFound}, +}; +use alloy_primitives::{BlockHash, ChainId}; +use derive_more::Constructor; +use kona_interop::InteropValidator; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{CrossChainSafetyProvider, StorageError}; +use kona_supervisor_types::ExecutingMessage; +use op_alloy_consensus::interop::SafetyLevel; +use std::collections::HashSet; + +/// Uses a [`CrossChainSafetyProvider`] to verify the safety of cross-chain message dependencies. +#[derive(Debug, Constructor)] +pub struct CrossSafetyChecker<'a, P, V> { + chain_id: ChainId, + validator: &'a V, + provider: &'a P, + required_level: SafetyLevel, +} + +impl CrossSafetyChecker<'_, P, V> +where + P: CrossChainSafetyProvider, + V: InteropValidator, +{ + /// Verifies that all executing messages in the given block are valid based on the validity + /// checks + pub fn validate_block(&self, block: BlockInfo) -> Result<(), CrossSafetyError> { + self.map_dependent_block(&block, self.chain_id, |message, initiating_block_fetcher| { + // Step 1: Validate interop timestamps before any dependency checks + self.validator + .validate_interop_timestamps( + message.chain_id, // initiating chain id + message.timestamp, // initiating block timestamp + self.chain_id, // executing chain id + block.timestamp, // executing block timestamp + None, + ) + .map_err(ValidationError::InteropValidationError)?; + + // Step 2: Verify message dependency without fetching the initiating block. + // This avoids unnecessary I/O and ensures we skip validation when: + // - The current target head of the chain is behind the initiating block (must wait for + // that chain to process further) + // Only if the target head is ahead but the initiating block is missing, we return a + // validation error. + self.verify_message_dependency(&message)?; + + // Step 3: Lazily fetch the initiating block only after dependency checks pass. + let initiating_block = initiating_block_fetcher()?; + + // Step 4: Validate message existence and integrity. + self.validate_executing_message(initiating_block, &message)?; + + // Step 5: Perform cyclic dependency detection starting from the dependent block. + self.check_cyclic_dependency( + &block, + &initiating_block, + message.chain_id, + &mut HashSet::new(), + ) + })?; + + Ok(()) + } + + /// Ensures that the block a message depends on satisfies the given safety level. + fn verify_message_dependency( + &self, + message: &ExecutingMessage, + ) -> Result<(), CrossSafetyError> { + let head = self.provider.get_safety_head_ref(message.chain_id, self.required_level)?; + + if head.number < message.block_number { + return Err(CrossSafetyError::DependencyNotSafe { + chain_id: message.chain_id, + block_number: message.block_number, + }); + } + + Ok(()) + } + + /// Recursively checks for a cyclic dependency in cross-chain messages. + /// + /// # Purpose + /// This function walks backwards through message dependencies starting from a candidate block. + /// If any dependency chain leads back to the candidate itself (with the same timestamp), it is + /// considered a **cycle**, which would make the candidate block invalid for cross-safe + /// promotion. + /// + /// # How It Works + /// - It stops recursion if the block is already at required level (cannot be part of a new + /// cycle). + /// - It only follows dependencies that occur at the same timestamp as the candidate. + /// - It uses a `visited` set to avoid re-processing blocks or getting stuck in infinite loops. + /// It doesn't care about cycle that is created excluding the candidate block as that will be + /// detected by the specific chain's safety checker + /// + /// Example: + /// - A (candidate) → B → C → A → ❌ cycle detected (includes candidate) + /// - A → B → C → D (no return to A) → ✅ safe + /// - B → C → D → B → ❌ ignored, since candidate is not involved + fn check_cyclic_dependency( + &self, + candidate: &BlockInfo, + current: &BlockInfo, + chain_id: ChainId, + visited: &mut HashSet<(ChainId, BlockHash)>, + ) -> Result<(), CrossSafetyError> { + // Skipping different timestamps + if candidate.timestamp != current.timestamp { + return Ok(()); + } + + // Already visited, avoid infinite loop + let current_id = (chain_id, current.hash); + if !visited.insert(current_id) { + return Ok(()); + } + + // Reached back to candidate - cycle detected + if candidate.hash == current.hash && self.chain_id == chain_id { + return Err(ValidationError::CyclicDependency { block: *candidate }.into()); + } + + let head = self.provider.get_safety_head_ref(chain_id, self.required_level)?; + if head.number >= current.number { + return Ok(()); // Already at target safety level - cannot form a cycle + } + + self.map_dependent_block(current, chain_id, |message, origin_block_fetcher| { + let origin_block = origin_block_fetcher()?; + self.check_cyclic_dependency(candidate, &origin_block, message.chain_id, visited) + }) + } + + fn validate_executing_message( + &self, + init_block: BlockInfo, + message: &ExecutingMessage, + ) -> Result<(), CrossSafetyError> { + // Ensure timestamp invariant + if init_block.timestamp != message.timestamp { + return Err(ValidationError::TimestampInvariantViolation { + expected_timestamp: init_block.timestamp, + actual_timestamp: message.timestamp, + } + .into()); + } + + // Try to fetch the original log from storage + let init_msg = self + .provider + .get_log(message.chain_id, message.block_number, message.log_index) + .map_err(|err| match err { + StorageError::EntryNotFound(_) => { + CrossSafetyError::ValidationError(InitiatingMessageNotFound) + } + other => other.into(), + })?; + + // Verify the hash of the message against the original + // Don't need to verify the checksum as we're already verifying all the individual fields. + if init_msg.hash != message.hash { + return Err(ValidationError::InvalidMessageHash { + message_hash: message.hash, + original_hash: init_msg.hash, + } + .into()); + } + + Ok(()) + } + + /// For each executing log in the block, provide a lazy fetcher for the initiating block. + /// The callback decides if/when to fetch the initiating block. + fn map_dependent_block( + &self, + exec_block: &BlockInfo, + chain_id: ChainId, + mut f: F, + ) -> Result<(), CrossSafetyError> + where + F: for<'a> FnMut( + ExecutingMessage, + &'a dyn Fn() -> Result, + ) -> Result<(), CrossSafetyError>, + { + let logs = self.provider.get_block_logs(chain_id, exec_block.number)?; + for log in logs { + if let Some(msg) = log.executing_message { + // Capture what we need for a lazy fetch. + let provider = &self.provider; + let chain = msg.chain_id; + let number = msg.block_number; + + // Zero-arg closure that fetches the initiating block on demand. + let fetcher = + || provider.get_block(chain, number).map_err(CrossSafetyError::Storage); + + // Pass the message and the reference to the fetcher. + f(msg, &fetcher)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use kona_interop::{DerivedRefPair, InteropValidationError}; + use kona_supervisor_storage::{EntryNotFoundError, StorageError}; + use kona_supervisor_types::Log; + use mockall::mock; + use op_alloy_consensus::interop::SafetyLevel; + + mock! ( + #[derive(Debug)] + pub Provider {} + + impl CrossChainSafetyProvider for Provider { + fn get_block(&self, chain_id: ChainId, block_number: u64) -> Result; + fn get_log(&self, chain_id: ChainId, block_number: u64, log_index: u32) -> Result; + fn get_block_logs(&self, chain_id: ChainId, block_number: u64) -> Result, StorageError>; + fn get_safety_head_ref(&self, chain_id: ChainId, level: SafetyLevel) -> Result; + fn update_current_cross_unsafe(&self, chain_id: ChainId, block: &BlockInfo) -> Result<(), StorageError>; + fn update_current_cross_safe(&self, chain_id: ChainId, block: &BlockInfo) -> Result; + } + ); + + mock! ( + #[derive(Debug)] + pub Validator {} + + impl InteropValidator for Validator { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; + } + ); + + fn b256(n: u64) -> B256 { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&n.to_be_bytes()); + B256::from(bytes) + } + + #[test] + fn verify_message_dependency_success() { + let chain_id = 1; + let msg = ExecutingMessage { + chain_id, + block_number: 100, + log_index: 0, + timestamp: 0, + hash: b256(0), + }; + + let head_info = + BlockInfo { number: 101, hash: b256(101), parent_hash: b256(100), timestamp: 0 }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + provider + .expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::CrossSafe) + .returning(move |_, _| Ok(head_info)); + + let checker = CrossSafetyChecker::new(1, &validator, &provider, SafetyLevel::CrossSafe); + let result = checker.verify_message_dependency(&msg); + assert!(result.is_ok()); + } + + #[test] + fn verify_message_dependency_failed() { + let chain_id = 1; + let msg = ExecutingMessage { + chain_id, + block_number: 105, // dependency is ahead of safety head + log_index: 0, + timestamp: 0, + hash: b256(123), + }; + + let head_block = BlockInfo { + number: 100, // safety head is behind the message dependency + hash: b256(100), + parent_hash: b256(99), + timestamp: 0, + }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + provider + .expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::CrossSafe) + .returning(move |_, _| Ok(head_block)); + + let checker = CrossSafetyChecker::new(1, &validator, &provider, SafetyLevel::CrossSafe); + let result = checker.verify_message_dependency(&msg); + + assert!( + matches!(result, Err(CrossSafetyError::DependencyNotSafe { .. })), + "Expected DependencyNotSafe error" + ); + } + + #[test] + fn validate_block_success() { + let init_chain_id = 1; + let exec_chain_id = 2; + + let block = + BlockInfo { number: 101, hash: b256(101), parent_hash: b256(100), timestamp: 200 }; + + let dep_block = + BlockInfo { number: 100, hash: b256(100), parent_hash: b256(99), timestamp: 195 }; + + let exec_msg = ExecutingMessage { + chain_id: init_chain_id, + block_number: 100, + log_index: 0, + timestamp: 195, + hash: b256(999), + }; + + let init_log = Log { + index: 0, + hash: b256(999), // Matches msg.hash → passes checksum + executing_message: None, + }; + + let exec_log = Log { index: 0, hash: b256(999), executing_message: Some(exec_msg) }; + + let head = + BlockInfo { number: 101, hash: b256(101), parent_hash: b256(100), timestamp: 200 }; + + let mut provider = MockProvider::default(); + let mut validator = MockValidator::default(); + + provider + .expect_get_block_logs() + .withf(move |cid, num| *cid == exec_chain_id && *num == 101) + .returning(move |_, _| Ok(vec![exec_log.clone()])); + + provider + .expect_get_block() + .withf(move |cid, num| *cid == init_chain_id && *num == 100) + .returning(move |_, _| Ok(dep_block)); + + provider + .expect_get_log() + .withf(move |cid, blk, idx| *cid == init_chain_id && *blk == 100 && *idx == 0) + .returning(move |_, _, _| Ok(init_log.clone())); + + provider + .expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == init_chain_id && *lvl == SafetyLevel::CrossSafe) + .returning(move |_, _| Ok(head)); + + validator.expect_validate_interop_timestamps().returning(move |_, _, _, _, _| Ok(())); + + let checker = + CrossSafetyChecker::new(exec_chain_id, &validator, &provider, SafetyLevel::CrossSafe); + let result = checker.validate_block(block); + assert!(result.is_ok()); + } + + #[test] + fn validate_executing_message_timestamp_violation() { + let chain_id = 1; + let msg = ExecutingMessage { + chain_id, + block_number: 100, + log_index: 0, + timestamp: 1234, + hash: b256(999), + }; + + let init_block = BlockInfo { + number: 100, + hash: b256(100), + parent_hash: b256(99), + timestamp: 9999, // Different timestamp to trigger invariant violation + }; + + let provider = MockProvider::default(); + let validator = MockValidator::default(); + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + + let result = checker.validate_executing_message(init_block, &msg); + assert!(matches!( + result, + Err(CrossSafetyError::ValidationError( + ValidationError::TimestampInvariantViolation { .. } + )) + )); + } + + #[test] + fn validate_executing_message_initiating_message_not_found() { + let chain_id = 1; + let msg = ExecutingMessage { + chain_id, + block_number: 100, + log_index: 0, + timestamp: 1234, + hash: b256(999), + }; + + let init_block = + BlockInfo { number: 100, hash: b256(100), parent_hash: b256(99), timestamp: 1234 }; + + let mut provider = MockProvider::default(); + provider + .expect_get_log() + .withf(move |cid, blk, idx| *cid == chain_id && *blk == 100 && *idx == 0) + .returning(|_, _, _| { + Err(StorageError::EntryNotFound(EntryNotFoundError::LogNotFound { + block_number: 100, + log_index: 0, + })) + }); + + let validator = MockValidator::default(); + + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + let result = checker.validate_executing_message(init_block, &msg); + + assert!(matches!( + result, + Err(CrossSafetyError::ValidationError(InitiatingMessageNotFound)) + )); + } + + #[test] + fn validate_executing_message_hash_mismatch() { + let chain_id = 1; + let msg = ExecutingMessage { + chain_id, + block_number: 100, + log_index: 0, + timestamp: 1234, + hash: b256(123), + }; + + let init_block = + BlockInfo { number: 100, hash: b256(100), parent_hash: b256(99), timestamp: 1234 }; + + let init_log = Log { + index: 0, + hash: b256(990), // Checksum mismatch + executing_message: None, + }; + + let mut provider = MockProvider::default(); + provider + .expect_get_log() + .withf(move |cid, blk, idx| *cid == chain_id && *blk == 100 && *idx == 0) + .returning(move |_, _, _| Ok(init_log.clone())); + + let validator = MockValidator::default(); + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + let result = checker.validate_executing_message(init_block, &msg); + + assert!(matches!( + result, + Err(CrossSafetyError::ValidationError(ValidationError::InvalidMessageHash { + message_hash: _, + original_hash: _ + })) + )); + } + + #[test] + fn validate_executing_message_success() { + let chain_id = 1; + let timestamp = 1234; + + let init_block = BlockInfo { + number: 100, + hash: b256(100), + parent_hash: b256(99), + timestamp, // Matches msg.timestamp + }; + + let init_log = Log { + index: 0, + hash: b256(999), // Matches msg.hash → passes checksum + executing_message: None, + }; + + let msg = ExecutingMessage { + chain_id, + block_number: 100, + log_index: 0, + timestamp, + hash: b256(999), + }; + + let mut provider = MockProvider::default(); + provider + .expect_get_log() + .withf(move |cid, blk, idx| *cid == chain_id && *blk == 100 && *idx == 0) + .returning(move |_, _, _| Ok(init_log.clone())); + + let validator = MockValidator::default(); + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + + let result = checker.validate_executing_message(init_block, &msg); + assert!(result.is_ok(), "Expected successful validation"); + } + + #[test] + fn detect_cycle_when_it_loops_back_to_candidate() { + // Scenario: + // candidate: (chain 1, block 10) + // → depends on (chain 2, block 11) + // → depends on (chain 3, block 20) + // → depends on (chain 1, block 10) ← back to candidate! + // Expected result: cyclic dependency detected. + + let ts = 100; + let candidate = + BlockInfo { number: 10, hash: b256(10), parent_hash: b256(9), timestamp: ts }; + + let block11 = + BlockInfo { number: 11, hash: b256(11), parent_hash: b256(10), timestamp: ts }; + + let block20 = + BlockInfo { number: 20, hash: b256(20), parent_hash: b256(19), timestamp: ts }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + // All blocks are below safety head (to allow traversal) + provider.expect_get_safety_head_ref().returning(|_, _| { + Ok(BlockInfo { number: 0, hash: b256(0), parent_hash: b256(0), timestamp: 0 }) + }); + + // Define log dependencies + provider.expect_get_block_logs().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("1", 10) => Ok(vec![Log { + index: 0, + hash: b256(1010), + executing_message: Some(ExecutingMessage { + chain_id: 2, + block_number: 11, + log_index: 0, + timestamp: ts, + hash: b256(222), + }), + }]), + ("2", 11) => Ok(vec![Log { + index: 0, + hash: b256(1020), + executing_message: Some(ExecutingMessage { + chain_id: 3, + block_number: 20, + log_index: 0, + timestamp: ts, + hash: b256(333), + }), + }]), + ("3", 20) => Ok(vec![Log { + index: 0, + hash: b256(1030), + executing_message: Some(ExecutingMessage { + chain_id: 1, + block_number: 10, + log_index: 0, + timestamp: ts, + hash: b256(444), + }), + }]), + _ => Ok(vec![]), + } + }); + + // Define block fetch behavior + provider.expect_get_block().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("2", 11) => Ok(block11), + ("3", 20) => Ok(block20), + ("1", 10) => Ok(candidate), + _ => panic!("unexpected block lookup: chain={chain} num={number}"), + } + }); + + let checker = CrossSafetyChecker::new(1, &validator, &provider, SafetyLevel::CrossSafe); + + let result = checker.check_cyclic_dependency(&candidate, &block11, 2, &mut HashSet::new()); + + assert!( + matches!( + result, + Err(CrossSafetyError::ValidationError(ValidationError::CyclicDependency { .. })) + ), + "Expected cyclic dependency error" + ); + } + + #[test] + fn no_cycle_if_dependency_path_does_not_reach_candidate() { + // Scenario: + // candidate: (chain 1, block 10) + // → depends on (chain 2, block 11) + // → depends on (chain 3, block 20) + // But no further dependency → path ends safely without cycling back to candidate. + // Expected result: no cycle detected. + + let ts = 100; + let candidate = + BlockInfo { number: 10, hash: b256(10), parent_hash: b256(9), timestamp: ts }; + + let block11 = + BlockInfo { number: 11, hash: b256(11), parent_hash: b256(10), timestamp: ts }; + + let block20 = + BlockInfo { number: 20, hash: b256(20), parent_hash: b256(19), timestamp: ts }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + // All blocks are below safety head (to allow traversal) + provider.expect_get_safety_head_ref().returning(|_, _| { + Ok(BlockInfo { number: 0, hash: b256(0), parent_hash: b256(0), timestamp: 0 }) + }); + + // Define log dependencies + provider.expect_get_block_logs().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("1", 10) => Ok(vec![Log { + index: 0, + hash: b256(1010), + executing_message: Some(ExecutingMessage { + chain_id: 2, + block_number: 11, + log_index: 0, + timestamp: ts, + hash: b256(222), + }), + }]), + ("2", 11) => Ok(vec![Log { + index: 0, + hash: b256(1020), + executing_message: Some(ExecutingMessage { + chain_id: 3, + block_number: 20, + log_index: 0, + timestamp: ts, + hash: b256(333), + }), + }]), + ("3", 20) => Ok(vec![]), // No further dependency — traversal ends here + _ => Ok(vec![]), + } + }); + + // Define block fetch behavior + provider.expect_get_block().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("2", 11) => Ok(block11), + ("3", 20) => Ok(block20), + _ => panic!("unexpected block lookup: chain={chain} num={number}"), + } + }); + + let checker = CrossSafetyChecker::new(1, &validator, &provider, SafetyLevel::CrossSafe); + + let result = checker.check_cyclic_dependency(&candidate, &block11, 2, &mut HashSet::new()); + + assert!(result.is_ok(), "Expected no cycle when dependency path does not reach candidate"); + } + + #[test] + fn ignores_cycle_that_does_not_include_candidate() { + // Scenario: + // There is a cycle between blocks: + // Chain2 block 11 → Chain3 block 20 → Chain2 block 11 (forms a cycle) + // But candidate block (Chain1 block 10) is not in the cycle. + // Expected result: cycle is ignored since it doesn't involve the candidate. + + let ts = 100; + let candidate = + BlockInfo { number: 10, hash: b256(10), parent_hash: b256(9), timestamp: ts }; + + let block11 = + BlockInfo { number: 11, hash: b256(11), parent_hash: b256(10), timestamp: ts }; + + let block20 = + BlockInfo { number: 20, hash: b256(20), parent_hash: b256(19), timestamp: ts }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + // All blocks are below safety head (so we traverse them) + provider.expect_get_safety_head_ref().returning(|_, _| { + Ok(BlockInfo { number: 0, hash: b256(0), parent_hash: b256(0), timestamp: 0 }) + }); + + // Block logs setup: + // Chain1 block 10 → Chain2 block 11 + // Chain2 block 11 → Chain3 block 20 + // Chain3 block 20 → Chain2 block 11 (cycle here, but no candidate involvement) + provider.expect_get_block_logs().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("1", 10) => Ok(vec![Log { + index: 0, + hash: b256(1010), + executing_message: Some(ExecutingMessage { + chain_id: 2, + block_number: 11, + log_index: 0, + timestamp: ts, + hash: b256(222), + }), + }]), + ("2", 11) => Ok(vec![Log { + index: 0, + hash: b256(1020), + executing_message: Some(ExecutingMessage { + chain_id: 3, + block_number: 20, + log_index: 0, + timestamp: ts, + hash: b256(333), + }), + }]), + ("3", 20) => Ok(vec![Log { + index: 0, + hash: b256(1030), + executing_message: Some(ExecutingMessage { + chain_id: 2, + block_number: 11, + log_index: 0, + timestamp: ts, + hash: b256(444), + }), + }]), + _ => Ok(vec![]), + } + }); + + // Block fetches + provider.expect_get_block().returning(move |chain, number| { + match (chain.to_string().as_str(), number) { + ("2", 11) => Ok(block11), + ("3", 20) => Ok(block20), + _ => panic!("unexpected block lookup"), + } + }); + + let checker = CrossSafetyChecker::new(1, &validator, &provider, SafetyLevel::CrossSafe); + + // Start traversal from chain2: block11 is a dependency of candidate + let result = checker.check_cyclic_dependency(&candidate, &block11, 2, &mut HashSet::new()); + + assert!( + result.is_ok(), + "Expected no cycle error because candidate is not part of the cycle" + ); + } + + #[test] + fn stops_traversal_if_timestamp_differs() { + // Scenario: + // Candidate and dependency block have different timestamps. + // Should short-circuit the check and not recurse further. + // Expected result: no cycle detected. + + let chain_id = 1; + + let candidate = + BlockInfo { number: 10, hash: b256(10), parent_hash: b256(9), timestamp: 100 }; + let dep = BlockInfo { number: 9, hash: b256(9), parent_hash: b256(8), timestamp: 50 }; + + let provider = MockProvider::default(); + let validator = MockValidator::default(); + + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + + let result = + checker.check_cyclic_dependency(&candidate, &dep, chain_id, &mut HashSet::new()); + assert!(result.is_ok()); + } + + #[test] + fn stops_traversal_if_block_is_already_cross_safe() { + // Scenario: + // Dependency block is already cross-safe (head is ahead of it). + // Should skip further traversal. + // Expected result: no cycle detected. + + let chain_id = 1; + + let candidate = + BlockInfo { number: 10, hash: b256(10), parent_hash: b256(9), timestamp: 100 }; + let dep = BlockInfo { number: 9, hash: b256(9), parent_hash: b256(8), timestamp: 100 }; + + let mut provider = MockProvider::default(); + let validator = MockValidator::default(); + + provider.expect_get_safety_head_ref().returning(|_, _| { + Ok(BlockInfo { + number: 10, + hash: b256(10), + parent_hash: b256(9), + timestamp: 100, // head ahead + }) + }); + + let checker = + CrossSafetyChecker::new(chain_id, &validator, &provider, SafetyLevel::CrossSafe); + + let result = + checker.check_cyclic_dependency(&candidate, &dep, chain_id, &mut HashSet::new()); + assert!(result.is_ok()); + } +} diff --git a/kona/crates/supervisor/core/src/safety_checker/error.rs b/kona/crates/supervisor/core/src/safety_checker/error.rs new file mode 100644 index 0000000000000..4201b21e55bed --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/error.rs @@ -0,0 +1,81 @@ +use alloy_primitives::{B256, ChainId}; +use kona_interop::InteropValidationError; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::StorageError; +use op_alloy_consensus::interop::SafetyLevel; +use thiserror::Error; + +/// Errors returned when validating cross-chain message dependencies. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum CrossSafetyError { + /// Indicates a failure while accessing storage during dependency checking. + #[error("storage error: {0}")] + Storage(#[from] StorageError), + + /// The block that a message depends on does not meet the required safety level. + #[error( + "dependency on block {block_number} (chain {chain_id}) does not meet required safety level" + )] + DependencyNotSafe { + /// The ID of the chain containing the unsafe dependency. + chain_id: ChainId, + /// The block number of the dependency that failed the safety check + block_number: u64, + }, + + /// No candidate block is currently available for promotion. + #[error("no candidate block found to promote")] + NoBlockToPromote, + + /// The requested safety level is not supported for promotion. + #[error("promotion to level {0} is not supported")] + UnsupportedTargetLevel(SafetyLevel), + + /// Indicates that error occurred while validating block + #[error(transparent)] + ValidationError(#[from] ValidationError), +} + +/// Errors returned when block validation fails due to a fatal violation. +/// These errors indicate that the block must be invalidated. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ValidationError { + /// Indicates that error occurred while validating interop config for the block messages + #[error(transparent)] + InteropValidationError(#[from] InteropValidationError), + + /// Indicates a mismatch between the executing message hash and the expected original log hash. + #[error( + "executing message hash {message_hash} does not match original log hash {original_hash}" + )] + InvalidMessageHash { + /// The hash provided in the executing message. + message_hash: B256, + /// The expected hash from the original initiating log. + original_hash: B256, + }, + + /// Indicates that the timestamp in the executing message does not match the timestamp + /// of the initiating block, violating the timestamp invariant required for validation. + #[error( + "timestamp invariant violated while validating executing message: expected {expected_timestamp}, but found {actual_timestamp}" + )] + TimestampInvariantViolation { + /// The timestamp of the initiating block. + expected_timestamp: u64, + /// The timestamp found in the executing message. + actual_timestamp: u64, + }, + + /// The initiating message corresponding to the executing message could not be found in log + /// storage. + #[error("initiating message not found for the executing message")] + InitiatingMessageNotFound, + + /// Cyclic dependency detected involving the candidate block + #[error("cyclic dependency detected while promoting block {block}")] + CyclicDependency { + /// The candidate block which is creating cyclic dependency + block: BlockInfo, + }, +} diff --git a/kona/crates/supervisor/core/src/safety_checker/mod.rs b/kona/crates/supervisor/core/src/safety_checker/mod.rs new file mode 100644 index 0000000000000..5bf2a2e1c4524 --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/mod.rs @@ -0,0 +1,20 @@ +//! # Cross-Chain Block Safety Checker +//! +//! This module is responsible for verifying that all executing messages in a block +//! are based on dependencies that have reached the required safety level (e.g., +//! [`CrossSafe`](op_alloy_consensus::interop::SafetyLevel)). +//! +//! It ensures correctness in cross-chain execution by validating that initiating blocks +//! of messages are safely committed before the messages are executed in other chains. +mod cross; +pub use cross::CrossSafetyChecker; +mod error; +mod task; +mod traits; +pub use traits::SafetyPromoter; +mod promoter; +pub use promoter::{CrossSafePromoter, CrossUnsafePromoter}; + +pub use task::CrossSafetyCheckerJob; + +pub use error::{CrossSafetyError, ValidationError}; diff --git a/kona/crates/supervisor/core/src/safety_checker/promoter.rs b/kona/crates/supervisor/core/src/safety_checker/promoter.rs new file mode 100644 index 0000000000000..ca8ecdb03307d --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/promoter.rs @@ -0,0 +1,53 @@ +use crate::{CrossSafetyError, event::ChainEvent, safety_checker::traits::SafetyPromoter}; +use alloy_primitives::ChainId; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::CrossChainSafetyProvider; +use op_alloy_consensus::interop::SafetyLevel; + +/// CrossUnsafePromoter implements [`SafetyPromoter`] for [`SafetyLevel::CrossUnsafe`] +#[derive(Debug)] +pub struct CrossUnsafePromoter; + +impl SafetyPromoter for CrossUnsafePromoter { + fn target_level(&self) -> SafetyLevel { + SafetyLevel::CrossUnsafe + } + + fn lower_bound_level(&self) -> SafetyLevel { + SafetyLevel::LocalUnsafe + } + + fn update_and_emit_event( + &self, + provider: &dyn CrossChainSafetyProvider, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result { + provider.update_current_cross_unsafe(chain_id, block)?; + Ok(ChainEvent::CrossUnsafeUpdate { block: *block }) + } +} + +/// CrossSafePromoter implements [`SafetyPromoter`] for [`SafetyLevel::CrossSafe`] +#[derive(Debug)] +pub struct CrossSafePromoter; + +impl SafetyPromoter for CrossSafePromoter { + fn target_level(&self) -> SafetyLevel { + SafetyLevel::CrossSafe + } + + fn lower_bound_level(&self) -> SafetyLevel { + SafetyLevel::LocalSafe + } + + fn update_and_emit_event( + &self, + provider: &dyn CrossChainSafetyProvider, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result { + let derived_ref_pair = provider.update_current_cross_safe(chain_id, block)?; + Ok(ChainEvent::CrossSafeUpdate { derived_ref_pair }) + } +} diff --git a/kona/crates/supervisor/core/src/safety_checker/task.rs b/kona/crates/supervisor/core/src/safety_checker/task.rs new file mode 100644 index 0000000000000..0a7cae741c5bc --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/task.rs @@ -0,0 +1,469 @@ +use crate::{ + CrossSafetyError, + event::ChainEvent, + safety_checker::{CrossSafetyChecker, traits::SafetyPromoter}, +}; +use alloy_primitives::ChainId; +use derive_more::Constructor; +use kona_interop::InteropValidator; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{CrossChainSafetyProvider, StorageError}; +use op_alloy_consensus::interop::SafetyLevel; +use std::{sync::Arc, time::Duration}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info}; + +/// A background job that promotes blocks to a target safety level on a given chain. +/// +/// It uses [`CrossChainSafetyProvider`] to fetch candidate blocks and the [`CrossSafetyChecker`] +/// to validate cross-chain message dependencies. +#[derive(Debug, Constructor)] +pub struct CrossSafetyCheckerJob { + chain_id: ChainId, + provider: Arc

, + cancel_token: CancellationToken, + interval: Duration, + promoter: L, + event_tx: mpsc::Sender, + validator: Arc, +} + +impl CrossSafetyCheckerJob +where + P: CrossChainSafetyProvider + Send + Sync + 'static, + V: InteropValidator + Send + Sync + 'static, + L: SafetyPromoter, +{ + /// Runs the job loop until cancelled, promoting blocks by Promoter + /// + /// On each iteration: + /// - Tries to promote the next eligible block + /// - Waits for configured interval if promotion fails + /// - Exits when [`CancellationToken`] is triggered + pub async fn run(self) { + let target_level = self.promoter.target_level(); + let chain_id = self.chain_id; + + info!( + target: "supervisor::safety_checker", + chain_id, + %target_level, + "Started safety checker"); + + let checker = + CrossSafetyChecker::new(chain_id, &*self.validator, &*self.provider, target_level); + + loop { + tokio::select! { + _ = self.cancel_token.cancelled() => { + info!(target: "supervisor::safety_checker", chain_id, %target_level, "Canceled safety checker"); + break; + } + + _ = async { + match self.promote_next_block(&checker) { + Ok(block_info) => { + debug!( + target: "supervisor::safety_checker", + chain_id, + %target_level, + %block_info, + "Promoted next candidate block" + ); + } + Err(err) => { + match err { + // Expected / non-fatal errors: + // - no candidate is ready right now + // - validation failed (we already emitted invalidate event in promote_next_block for CrossSafe) + // - dependency not yet safe on another chain + CrossSafetyError::NoBlockToPromote | + CrossSafetyError::ValidationError(_) | + CrossSafetyError::DependencyNotSafe { .. } => { + debug!( + target: "supervisor::safety_checker", + chain_id, + %target_level, + %err, + "Error promoting next candidate block" + ); + }, + _ => { + error!( + target: "supervisor::safety_checker", + chain_id, + %target_level, + %err, + "Unexpected error promoting next candidate block" + ); + } + } + tokio::time::sleep(self.interval).await; + } + } + } => {} + } + } + + info!(target: "supervisor::safety_checker", chain_id = self.chain_id, %target_level, "Stopped safety checker"); + } + + // Attempts to promote the next block by the Promoter + // after validating cross-chain dependencies. + fn promote_next_block( + &self, + checker: &CrossSafetyChecker<'_, P, V>, + ) -> Result { + let candidate = self.find_next_promotable_block()?; + + match checker.validate_block(candidate) { + Ok(()) => { + // Success: promote + emit + let ev = self.promoter.update_and_emit_event( + &*self.provider, + self.chain_id, + &candidate, + )?; + self.broadcast_event(ev); + Ok(candidate) + } + + Err(err @ CrossSafetyError::ValidationError(_)) => { + // Only invalidate if we are targeting CrossSafe + if self.promoter.target_level() == SafetyLevel::CrossSafe { + info!( + target: "supervisor::safety_checker", + chain_id = self.chain_id, + target_level = %self.promoter.target_level(), + block_info = %candidate, + %err, + "Triggering block invalidation for the invalid block" + ); + self.broadcast_event(ChainEvent::InvalidateBlock { block: candidate }); + } + Err(err) // propagate the error for logging + } + Err(err) => Err(err), + } + } + + // Finds the next block that is eligible for promotion at the configured target level. + fn find_next_promotable_block(&self) -> Result { + let current_head = self + .provider + .get_safety_head_ref(self.chain_id, self.promoter.target_level()) + .map_err(|err| { + if matches!(err, StorageError::FutureData) { + CrossSafetyError::NoBlockToPromote + } else { + err.into() + } + })?; + + let upper_head = self + .provider + .get_safety_head_ref(self.chain_id, self.promoter.lower_bound_level()) + .map_err(|err| { + if matches!(err, StorageError::FutureData) { + CrossSafetyError::NoBlockToPromote + } else { + err.into() + } + })?; + + if current_head.number >= upper_head.number { + return Err(CrossSafetyError::NoBlockToPromote); + } + + let candidate = self.provider.get_block(self.chain_id, current_head.number + 1)?; + + Ok(candidate) + } + + fn broadcast_event(&self, event: ChainEvent) { + if let Err(err) = self.event_tx.try_send(event) { + error!( + target: "supervisor::safety_checker", + target_level = %self.promoter.target_level(), + %err, + "Failed to broadcast cross head update event", + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::safety_checker::promoter::{CrossSafePromoter, CrossUnsafePromoter}; + use alloy_primitives::{B256, ChainId}; + use kona_interop::{DerivedRefPair, InteropValidationError}; + use kona_supervisor_storage::{CrossChainSafetyProvider, StorageError}; + use kona_supervisor_types::{ExecutingMessage, Log}; + use mockall::mock; + use op_alloy_consensus::interop::SafetyLevel; + + mock! { + #[derive(Debug)] + pub Provider {} + + impl CrossChainSafetyProvider for Provider { + fn get_block(&self, chain_id: ChainId, block_number: u64) -> Result; + fn get_log(&self, chain_id: ChainId, block_number: u64, log_index: u32) -> Result; + fn get_block_logs(&self, chain_id: ChainId, block_number: u64) -> Result, StorageError>; + fn get_safety_head_ref(&self, chain_id: ChainId, level: SafetyLevel) -> Result; + fn update_current_cross_unsafe(&self, chain_id: ChainId, block: &BlockInfo) -> Result<(), StorageError>; + fn update_current_cross_safe(&self, chain_id: ChainId, block: &BlockInfo) -> Result; + } + } + + mock! ( + #[derive(Debug)] + pub Validator {} + + impl InteropValidator for Validator { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; + } + ); + + fn b256(n: u64) -> B256 { + let mut bytes = [0u8; 32]; + bytes[24..].copy_from_slice(&n.to_be_bytes()); + B256::from(bytes) + } + + fn block(n: u64) -> BlockInfo { + BlockInfo { number: n, hash: b256(n), parent_hash: b256(n - 1), timestamp: 0 } + } + + #[tokio::test] + async fn promotes_next_cross_unsafe_successfully() { + let chain_id = 1; + let mut mock = MockProvider::default(); + let mock_validator = MockValidator::default(); + let (event_tx, mut event_rx) = mpsc::channel::(10); + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::CrossUnsafe) + .returning(|_, _| Ok(block(99))); + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::LocalUnsafe) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block_logs() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(|_, _| Ok(vec![])); + + mock.expect_update_current_cross_unsafe() + .withf(move |cid, blk| *cid == chain_id && blk.number == 100) + .returning(|_, _| Ok(())); + + let job = CrossSafetyCheckerJob::new( + chain_id, + Arc::new(mock), + CancellationToken::new(), + Duration::from_secs(1), + CrossUnsafePromoter, + event_tx, + Arc::new(mock_validator), + ); + let checker = CrossSafetyChecker::new( + job.chain_id, + &*job.validator, + &*job.provider, + CrossUnsafePromoter.target_level(), + ); + let result = job.promote_next_block(&checker); + + assert!(result.is_ok()); + assert_eq!(result.unwrap().number, 100); + + // Receive and assert the correct event + let received_event = event_rx.recv().await.expect("expected event not received"); + + assert_eq!(received_event, ChainEvent::CrossUnsafeUpdate { block: block(100) }); + } + + #[tokio::test] + async fn promotes_next_cross_safe_successfully() { + let chain_id = 1; + let mut mock = MockProvider::default(); + let mock_validator = MockValidator::default(); + let (event_tx, mut event_rx) = mpsc::channel::(10); + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::CrossSafe) + .returning(|_, _| Ok(block(99))); + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::LocalSafe) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block_logs() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(|_, _| Ok(vec![])); + + mock.expect_update_current_cross_safe() + .withf(move |cid, blk| *cid == chain_id && blk.number == 100) + .returning(|_, _| Ok(DerivedRefPair { derived: block(100), source: block(1) })); + + let job = CrossSafetyCheckerJob::new( + chain_id, + Arc::new(mock), + CancellationToken::new(), + Duration::from_secs(1), + CrossSafePromoter, + event_tx, + Arc::new(mock_validator), + ); + + let checker = CrossSafetyChecker::new( + job.chain_id, + &*job.validator, + &*job.provider, + CrossSafePromoter.target_level(), + ); + let result = job.promote_next_block(&checker); + + assert!(result.is_ok()); + assert_eq!(result.unwrap().number, 100); + + // Receive and assert the correct event + let received_event = event_rx.recv().await.expect("expected event not received"); + + assert_eq!( + received_event, + ChainEvent::CrossSafeUpdate { + derived_ref_pair: DerivedRefPair { derived: block(100), source: block(1) } + } + ); + } + + #[tokio::test] + async fn promotes_next_cross_safe_triggers_block_invalidation() { + let chain_id = 1; + let mut mock = MockProvider::default(); + let mut mock_validator = MockValidator::default(); + let (event_tx, mut event_rx) = mpsc::channel::(10); + + let exec_msg = ExecutingMessage { + chain_id: 2, + block_number: 99, + log_index: 0, + timestamp: 195, + hash: b256(99), + }; + + let exec_log = Log { index: 0, hash: b256(100), executing_message: Some(exec_msg) }; + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::CrossSafe) + .returning(|_, _| Ok(block(99))); + + mock.expect_get_safety_head_ref() + .withf(move |cid, lvl| *cid == chain_id && *lvl == SafetyLevel::LocalSafe) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block() + .withf(move |cid, num| *cid == 2 && *num == 99) + .returning(|_, _| Ok(block(99))); + + mock.expect_get_block() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(|_, _| Ok(block(100))); + + mock.expect_get_block_logs() + .withf(move |cid, num| *cid == chain_id && *num == 100) + .returning(move |_, _| Ok(vec![exec_log.clone()])); + + mock_validator.expect_validate_interop_timestamps().returning(move |_, _, _, _, _| { + Err(InteropValidationError::InvalidTimestampInvariant { executing: 0, initiating: 0 }) + }); + + let job = CrossSafetyCheckerJob::new( + chain_id, + Arc::new(mock), + CancellationToken::new(), + Duration::from_secs(1), + CrossSafePromoter, + event_tx, + Arc::new(mock_validator), + ); + + let checker = CrossSafetyChecker::new( + job.chain_id, + &*job.validator, + &*job.provider, + CrossSafePromoter.target_level(), + ); + let result = job.promote_next_block(&checker); + + assert!(result.is_err()); + assert!( + matches!(result, Err(CrossSafetyError::ValidationError(_))), + "Expected validation error" + ); + + // Receive and assert the correct event + let received_event = event_rx.recv().await.expect("expected event not received"); + + assert_eq!(received_event, ChainEvent::InvalidateBlock { block: block(100) }); + } + + #[test] + fn promotes_next_cross_unsafe_failed_with_no_candidates() { + let chain_id = 1; + let mut mock = MockProvider::default(); + let mock_validator = MockValidator::default(); + let (event_tx, _) = mpsc::channel::(10); + + mock.expect_get_safety_head_ref() + .withf(|_, lvl| *lvl == SafetyLevel::CrossSafe) + .returning(|_, _| Ok(block(200))); + + mock.expect_get_safety_head_ref() + .withf(|_, lvl| *lvl == SafetyLevel::LocalSafe) + .returning(|_, _| Ok(block(200))); + + let job = CrossSafetyCheckerJob::new( + chain_id, + Arc::new(mock), + CancellationToken::new(), + Duration::from_secs(1), + CrossSafePromoter, + event_tx, + Arc::new(mock_validator), + ); + + let checker = CrossSafetyChecker::new( + job.chain_id, + &*job.validator, + &*job.provider, + CrossSafePromoter.target_level(), + ); + let result = job.promote_next_block(&checker); + + assert!(matches!(result, Err(CrossSafetyError::NoBlockToPromote))); + } +} diff --git a/kona/crates/supervisor/core/src/safety_checker/traits.rs b/kona/crates/supervisor/core/src/safety_checker/traits.rs new file mode 100644 index 0000000000000..7bc0504d5509e --- /dev/null +++ b/kona/crates/supervisor/core/src/safety_checker/traits.rs @@ -0,0 +1,27 @@ +use crate::{CrossSafetyError, event::ChainEvent}; +use alloy_primitives::ChainId; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::CrossChainSafetyProvider; +use op_alloy_consensus::interop::SafetyLevel; + +/// Defines the logic for promoting a block to a specific [`SafetyLevel`]. +/// +/// Each implementation handles: +/// - Which safety level it promotes to +/// - Its required lower bound +/// - Updating state and generating the corresponding [`ChainEvent`] +pub trait SafetyPromoter { + /// Target safety level this promoter upgrades to. + fn target_level(&self) -> SafetyLevel; + + /// Required lower bound level for promotion eligibility. + fn lower_bound_level(&self) -> SafetyLevel; + + /// Performs the promotion by updating state and returning the event to broadcast. + fn update_and_emit_event( + &self, + provider: &dyn CrossChainSafetyProvider, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result; +} diff --git a/kona/crates/supervisor/core/src/state/mod.rs b/kona/crates/supervisor/core/src/state/mod.rs new file mode 100644 index 0000000000000..695a56c9a5edd --- /dev/null +++ b/kona/crates/supervisor/core/src/state/mod.rs @@ -0,0 +1,2 @@ +//! This module is responsible for managing and persisting the state of the supervisor. +// TODO: Implement state management logic for the supervisor. diff --git a/kona/crates/supervisor/core/src/supervisor.rs b/kona/crates/supervisor/core/src/supervisor.rs new file mode 100644 index 0000000000000..ba8a8b1d1eebd --- /dev/null +++ b/kona/crates/supervisor/core/src/supervisor.rs @@ -0,0 +1,384 @@ +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, Bytes, ChainId, keccak256}; +use async_trait::async_trait; +use core::fmt::Debug; +use kona_interop::{ + DependencySet, ExecutingDescriptor, InteropValidator, OutputRootWithChain, SUPER_ROOT_VERSION, + SafetyLevel, SuperRoot, +}; +use kona_protocol::BlockInfo; +use kona_supervisor_rpc::{ChainRootInfoRpc, SuperRootOutputRpc}; +use kona_supervisor_storage::{ + ChainDb, ChainDbFactory, DerivationStorageReader, FinalizedL1Storage, HeadRefStorageReader, + LogStorageReader, +}; +use kona_supervisor_types::{SuperHead, parse_access_list}; +use op_alloy_rpc_types::SuperchainDAError; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::RwLock; +use tracing::{error, warn}; + +use crate::{ + SpecError, SupervisorError, + config::Config, + syncnode::{BlockProvider, ManagedNodeDataProvider}, +}; + +/// Defines the service for the Supervisor core logic. +#[async_trait] +#[auto_impl::auto_impl(&, &mut, Arc, Box)] +pub trait SupervisorService: Debug + Send + Sync { + /// Returns list of supervised [`ChainId`]s. + fn chain_ids(&self) -> impl Iterator; + + /// Returns mapping of supervised [`ChainId`]s to their [`ChainDependency`] config. + /// + /// [`ChainDependency`]: kona_interop::ChainDependency + fn dependency_set(&self) -> &DependencySet; + + /// Returns [`SuperHead`] of given supervised chain. + fn super_head(&self, chain: ChainId) -> Result; + + /// Returns latest block derived from given L1 block, for given chain. + fn latest_block_from( + &self, + l1_block: BlockNumHash, + chain: ChainId, + ) -> Result; + + /// Returns the L1 source block that the given L2 derived block was based on, for the specified + /// chain. + fn derived_to_source_block( + &self, + chain: ChainId, + derived: BlockNumHash, + ) -> Result; + + /// Returns [`LocalUnsafe`] block for the given chain. + /// + /// [`LocalUnsafe`]: SafetyLevel::LocalUnsafe + fn local_unsafe(&self, chain: ChainId) -> Result; + + /// Returns [`LocalSafe`] block for the given chain. + /// + /// [`LocalSafe`]: SafetyLevel::LocalSafe + fn local_safe(&self, chain: ChainId) -> Result; + + /// Returns [`CrossSafe`] block for the given chain. + /// + /// [`CrossSafe`]: SafetyLevel::CrossSafe + fn cross_safe(&self, chain: ChainId) -> Result; + + /// Returns [`Finalized`] block for the given chain. + /// + /// [`Finalized`]: SafetyLevel::Finalized + fn finalized(&self, chain: ChainId) -> Result; + + /// Returns the finalized L1 block that the supervisor is synced to. + fn finalized_l1(&self) -> Result; + + /// Returns the [`SuperRootOutput`] at a specified timestamp, which represents the global + /// state across all monitored chains. + /// + /// [`SuperRootOutput`]: kona_interop::SuperRootOutput + async fn super_root_at_timestamp( + &self, + timestamp: u64, + ) -> Result; + + /// Verifies if an access-list references only valid messages + fn check_access_list( + &self, + inbox_entries: Vec, + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> Result<(), SupervisorError>; +} + +/// The core Supervisor component responsible for monitoring and coordinating chain states. +#[derive(Debug)] +pub struct Supervisor { + config: Arc, + database_factory: Arc, + + // As of now supervisor only supports a single managed node per chain. + // This is a limitation of the current implementation, but it will be extended in the future. + managed_nodes: RwLock>>, +} + +impl Supervisor +where + M: ManagedNodeDataProvider + BlockProvider + Send + Sync + Debug, +{ + /// Creates a new [`Supervisor`] instance. + #[allow(clippy::new_without_default, clippy::missing_const_for_fn)] + pub fn new(config: Arc, database_factory: Arc) -> Self { + Self { config, database_factory, managed_nodes: RwLock::new(HashMap::new()) } + } + + /// Adds a new managed node to the [`Supervisor`]. + pub async fn add_managed_node( + &self, + chain_id: ChainId, + managed_node: Arc, + ) -> Result<(), SupervisorError> { + // todo: instead of passing the chain ID, we should get it from the managed node + if !self.config.dependency_set.dependencies.contains_key(&chain_id) { + warn!(target: "supervisor::service", %chain_id, "Unsupported chain ID"); + return Err(SupervisorError::UnsupportedChainId); + } + + let mut managed_nodes = self.managed_nodes.write().await; + if managed_nodes.contains_key(&chain_id) { + warn!(target: "supervisor::service", %chain_id, "Managed node already exists for chain"); + return Ok(()); + } + + managed_nodes.insert(chain_id, managed_node.clone()); + Ok(()) + } + + fn verify_safety_level( + &self, + chain_id: ChainId, + block: &BlockInfo, + safety: SafetyLevel, + ) -> Result<(), SupervisorError> { + let head_ref = self.database_factory.get_db(chain_id)?.get_safety_head_ref(safety)?; + + if head_ref.number < block.number { + return Err(SpecError::SuperchainDAError(SuperchainDAError::ConflictingData).into()); + } + + Ok(()) + } + + fn get_db(&self, chain: ChainId) -> Result, SupervisorError> { + self.database_factory.get_db(chain).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get database for chain"); + SpecError::from(err).into() + }) + } +} + +#[async_trait] +impl SupervisorService for Supervisor +where + M: ManagedNodeDataProvider + BlockProvider + Send + Sync + Debug, +{ + fn chain_ids(&self) -> impl Iterator { + self.config.dependency_set.dependencies.keys().copied() + } + + fn dependency_set(&self) -> &DependencySet { + &self.config.dependency_set + } + + fn super_head(&self, chain: ChainId) -> Result { + Ok(self.get_db(chain)?.get_super_head().map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get super head for chain"); + SpecError::from(err) + })?) + } + + fn latest_block_from( + &self, + l1_block: BlockNumHash, + chain: ChainId, + ) -> Result { + Ok(self + .get_db(chain)? + .latest_derived_block_at_source(l1_block) + .map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get latest derived block at source for chain"); + SpecError::from(err) + })? + ) + } + + fn derived_to_source_block( + &self, + chain: ChainId, + derived: BlockNumHash, + ) -> Result { + Ok(self.get_db(chain)?.derived_to_source(derived).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get derived to source block for chain"); + SpecError::from(err) + })?) + } + + fn local_unsafe(&self, chain: ChainId) -> Result { + Ok(self.get_db(chain)?.get_safety_head_ref(SafetyLevel::LocalUnsafe).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get local unsafe head ref for chain"); + SpecError::from(err) + })?) + } + + fn local_safe(&self, chain: ChainId) -> Result { + Ok(self.get_db(chain)?.get_safety_head_ref(SafetyLevel::LocalSafe).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get local safe head ref for chain"); + SpecError::from(err) + })?) + } + + fn cross_safe(&self, chain: ChainId) -> Result { + Ok(self.get_db(chain)?.get_safety_head_ref(SafetyLevel::CrossSafe).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get cross safe head ref for chain"); + SpecError::from(err) + })?) + } + + fn finalized(&self, chain: ChainId) -> Result { + Ok(self.get_db(chain)?.get_safety_head_ref(SafetyLevel::Finalized).map_err(|err| { + error!(target: "supervisor::service", %chain, %err, "Failed to get finalized head ref for chain"); + SpecError::from(err) + })?) + } + + fn finalized_l1(&self) -> Result { + Ok(self.database_factory.get_finalized_l1().map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to get finalized L1"); + SpecError::from(err) + })?) + } + + async fn super_root_at_timestamp( + &self, + timestamp: u64, + ) -> Result { + let mut chain_ids = self.config.dependency_set.dependencies.keys().collect::>(); + // Sorting chain ids for deterministic super root hash + chain_ids.sort(); + + let mut chain_infos = Vec::::with_capacity(chain_ids.len()); + let mut super_root_chains = Vec::::with_capacity(chain_ids.len()); + let mut cross_safe_source = BlockNumHash::default(); + + for id in chain_ids { + let managed_node = { + let guard = self.managed_nodes.read().await; + match guard.get(id) { + Some(m) => m.clone(), + None => { + error!(target: "supervisor::service", chain_id = %id, "Managed node not found for chain"); + return Err(SupervisorError::ManagedNodeMissing(*id)); + } + } + }; + let output_v0 = managed_node.output_v0_at_timestamp(timestamp).await?; + let output_v0_string = serde_json::to_string(&output_v0) + .inspect_err(|err| { + error!(target: "supervisor::service", chain_id = %id, %err, "Failed to serialize output_v0 for chain"); + })?; + let canonical_root = keccak256(output_v0_string.as_bytes()); + + let pending_output_v0 = managed_node.pending_output_v0_at_timestamp(timestamp).await?; + let pending_output_v0_string = serde_json::to_string(&pending_output_v0) + .inspect_err(|err| { + error!(target: "supervisor::service", chain_id = %id, %err, "Failed to serialize pending_output_v0 for chain"); + })?; + let pending_output_v0_bytes = + Bytes::copy_from_slice(pending_output_v0_string.as_bytes()); + + chain_infos.push(ChainRootInfoRpc { + chain_id: *id, + canonical: canonical_root, + pending: pending_output_v0_bytes, + }); + + super_root_chains + .push(OutputRootWithChain { chain_id: *id, output_root: canonical_root }); + + let l2_block = managed_node.l2_block_ref_by_timestamp(timestamp).await?; + let source = self + .derived_to_source_block(*id, l2_block.id()) + .inspect_err(|err| { + error!(target: "supervisor::service", %id, %err, "Failed to get derived to source block for chain"); + })?; + + if cross_safe_source.number == 0 || cross_safe_source.number < source.number { + cross_safe_source = source.id(); + } + } + + let super_root = SuperRoot { timestamp, output_roots: super_root_chains }; + let super_root_hash = super_root.hash(); + + Ok(SuperRootOutputRpc { + cross_safe_derived_from: cross_safe_source, + timestamp, + super_root: super_root_hash, + chains: chain_infos, + version: SUPER_ROOT_VERSION, + }) + } + + fn check_access_list( + &self, + inbox_entries: Vec, + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> Result<(), SupervisorError> { + let access_list = parse_access_list(inbox_entries)?; + + for access in &access_list { + // Check all the invariants for each message + // Ref: https://github.com/ethereum-optimism/specs/blob/main/specs/interop/derivation.md#invariants + + // TODO: support 32 bytes chain id and convert to u64 via dependency set to be usable + // across services + let initiating_chain_id = access.chain_id[24..32] + .try_into() + .map(u64::from_be_bytes) + .map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to parse initiating chain id from access list"); + SupervisorError::ChainIdParseError() + })?; + + let executing_chain_id = executing_descriptor.chain_id.unwrap_or(initiating_chain_id); + + // Message must be valid at the time of execution. + self.config.validate_interop_timestamps( + initiating_chain_id, + access.timestamp, + executing_chain_id, + executing_descriptor.timestamp, + executing_descriptor.timeout, + ).map_err(|err| { + warn!(target: "supervisor::service", %err, "Failed to validate interop timestamps"); + SpecError::SuperchainDAError(SuperchainDAError::ConflictingData) + })?; + + // Verify the initiating message exists and valid for corresponding executing message. + let db = self.get_db(initiating_chain_id)?; + + let block = db.get_block(access.block_number).map_err(|err| { + warn!(target: "supervisor::service", %initiating_chain_id, %err, "Failed to get block for chain"); + SpecError::from(err) + })?; + if block.timestamp != access.timestamp { + return Err(SupervisorError::from(SpecError::SuperchainDAError( + SuperchainDAError::ConflictingData, + ))) + } + + let log = db.get_log(access.block_number, access.log_index).map_err(|err| { + warn!(target: "supervisor::service", %initiating_chain_id, %err, "Failed to get log for chain"); + SpecError::from(err) + })?; + access.verify_checksum(&log.hash).map_err(|err| { + warn!(target: "supervisor::service", %initiating_chain_id, %err, "Failed to verify checksum for access list"); + SpecError::SuperchainDAError(SuperchainDAError::ConflictingData) + })?; + + // The message must be included in a block that is at least as safe as required + // by the `min_safety` level + if min_safety != SafetyLevel::LocalUnsafe { + // The block is already unsafe as it is found in log db + self.verify_safety_level(initiating_chain_id, &block, min_safety)?; + } + } + + Ok(()) + } +} diff --git a/kona/crates/supervisor/core/src/syncnode/client.rs b/kona/crates/supervisor/core/src/syncnode/client.rs new file mode 100644 index 0000000000000..9675636e60548 --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/client.rs @@ -0,0 +1,406 @@ +use super::{AuthenticationError, ClientError, metrics::Metrics}; +use alloy_primitives::{B256, ChainId}; +use alloy_rpc_types_engine::{Claims, JwtSecret}; +use alloy_rpc_types_eth::BlockNumHash; +use async_trait::async_trait; +use jsonrpsee::{ + core::client::Subscription, + ws_client::{HeaderMap, HeaderValue, WsClient, WsClientBuilder}, +}; +use kona_supervisor_metrics::observe_metrics_for_result_async; +use kona_supervisor_rpc::{BlockInfo, ManagedModeApiClient, jsonrpsee::SubscriptionTopic}; +use kona_supervisor_types::{BlockSeal, OutputV0, Receipts, SubscriptionEvent}; +use std::{ + fmt::Debug, + sync::{Arc, OnceLock}, +}; +use tokio::sync::Mutex; +use tracing::{error, info}; + +/// Trait for a managed node client that provides various methods to interact with the node. +#[async_trait] +pub trait ManagedNodeClient: Send + Sync + Debug { + /// Returns the [`ChainId`] of the managed node. + async fn chain_id(&self) -> Result; + + /// Subscribes to [`SubscriptionEvent`] from the managed node. + async fn subscribe_events(&self) -> Result, ClientError>; + + /// Fetches [`Receipts`] for a given block hash. + async fn fetch_receipts(&self, block_hash: B256) -> Result; + + /// Fetches the [`OutputV0`] at a specific timestamp. + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + + /// Fetches the pending [`OutputV0`] at a specific timestamp. + async fn pending_output_v0_at_timestamp(&self, timestamp: u64) + -> Result; + + /// Fetches the L2 [`BlockInfo`] by timestamp. + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; + + /// Fetches the [`BlockInfo`] by block number. + async fn block_ref_by_number(&self, block_number: u64) -> Result; + + /// Resets the managed node to the pre-interop state. + async fn reset_pre_interop(&self) -> Result<(), ClientError>; + + /// Resets the node state with the provided block IDs. + async fn reset( + &self, + unsafe_id: BlockNumHash, + cross_unsafe_id: BlockNumHash, + local_safe_id: BlockNumHash, + cross_safe_id: BlockNumHash, + finalised_id: BlockNumHash, + ) -> Result<(), ClientError>; + + /// Invalidates a block in the managed node. + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; + + /// Provides L1 [`BlockInfo`] to the managed node. + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; + + /// Updates the finalized block ID in the managed node. + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; + + /// Updates the cross-unsafe block ID in the managed node. + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ClientError>; + + /// Updates the cross-safe block ID in the managed node. + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ClientError>; + + /// Resets the ws-client to None when server disconnects + async fn reset_ws_client(&self); +} + +/// [`ClientConfig`] sets the configuration for the managed node client. +#[derive(Debug, Clone)] +pub struct ClientConfig { + /// The URL + port of the managed node + pub url: String, + /// jwt secret for the managed node interop rpc + pub jwt_secret: JwtSecret, +} + +/// Client for interacting with a managed node. +#[derive(Debug)] +pub struct Client { + config: ClientConfig, + /// Chain ID of the managed node + chain_id: OnceLock, + /// The attached web socket client + ws_client: Mutex>>, +} + +impl Client { + /// Creates a new [`Client`] with the given configuration. + pub fn new(config: ClientConfig) -> Self { + Metrics::init(config.url.as_ref()); + Self { config, chain_id: OnceLock::new(), ws_client: Mutex::new(None) } + } + + /// Creates authentication headers using JWT secret. + fn create_auth_headers(&self) -> Result { + // Create JWT claims with current time + let claims = Claims::with_current_timestamp(); + let token = self.config.jwt_secret.encode(&claims).map_err(|err| { + error!(target: "supervisor::managed_node", %err, "Failed to encode JWT claims"); + AuthenticationError::InvalidJwt + })?; + + let mut headers = HeaderMap::new(); + let auth_header = format!("Bearer {token}"); + + headers.insert( + "Authorization", + HeaderValue::from_str(&auth_header).map_err(|err| { + error!(target: "supervisor::managed_node", %err, "Invalid authorization header"); + AuthenticationError::InvalidHeader + })?, + ); + + Ok(headers) + } + + /// Returns a reference to the WebSocket client, creating it if it doesn't exist. + // todo: support http client as well + pub async fn get_ws_client(&self) -> Result, ClientError> { + let mut ws_client_guard = self.ws_client.lock().await; + if ws_client_guard.is_none() { + let headers = self.create_auth_headers().inspect_err(|err| { + error!(target: "supervisor::managed_node", %err, "Failed to create auth headers"); + })?; + + info!(target: "supervisor::managed_node", ws_url = self.config.url, "Creating a new web socket client"); + let client = + WsClientBuilder::default().set_headers(headers).build(&self.config.url).await?; + + *ws_client_guard = Some(Arc::new(client)); + } + Ok(ws_client_guard.clone().unwrap()) + } +} + +#[async_trait] +impl ManagedNodeClient for Client { + async fn reset_ws_client(&self) { + let mut ws_client_guard = self.ws_client.lock().await; + if ws_client_guard.is_some() { + *ws_client_guard = None; + }; + } + + async fn chain_id(&self) -> Result { + if let Some(chain_id) = self.chain_id.get() { + return Ok(*chain_id); + } + + let client = self.get_ws_client().await?; + let chain_id_str = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_CHAIN_ID, + async { + client.chain_id().await + }, + "node" => self.config.url.clone() + ) + .inspect_err(|err| { + error!(target: "supervisor::managed_node", %err, "Failed to get chain ID"); + })?; + + let chain_id = chain_id_str.parse::().inspect_err(|err| { + error!(target: "supervisor::managed_node", %err, "Failed to parse chain ID"); + })?; + + let _ = self.chain_id.set(chain_id); + Ok(chain_id) + } + + async fn subscribe_events(&self) -> Result, ClientError> { + let client = self.get_ws_client().await?; // This returns ManagedNodeError, handled by your function + let subscription = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_SUBSCRIBE_EVENTS, + async { + ManagedModeApiClient::subscribe_events(client.as_ref(), SubscriptionTopic::Events).await + }, + "node" => self.config.url.clone() + )?; + + Ok(subscription) + } + + async fn fetch_receipts(&self, block_hash: B256) -> Result { + let client = self.get_ws_client().await?; // This returns ManagedNodeError, handled by your function + let receipts = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_FETCH_RECEIPTS, + async { + ManagedModeApiClient::fetch_receipts(client.as_ref(), block_hash).await + }, + "node" => self.config.url.clone() + )?; + + Ok(receipts) + } + + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result { + let client = self.get_ws_client().await?; + let output_v0 = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_OUTPUT_V0_AT_TIMESTAMP, + async { + ManagedModeApiClient::output_v0_at_timestamp(client.as_ref(), timestamp).await + }, + "node" => self.config.url.clone() + )?; + + Ok(output_v0) + } + + async fn pending_output_v0_at_timestamp( + &self, + timestamp: u64, + ) -> Result { + let client = self.get_ws_client().await?; + let output_v0 = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_PENDING_OUTPUT_V0_AT_TIMESTAMP, + async { + ManagedModeApiClient::pending_output_v0_at_timestamp(client.as_ref(), timestamp).await + }, + "node" => self.config.url.clone() + )?; + + Ok(output_v0) + } + + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result { + let client = self.get_ws_client().await?; + let block_info = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_L2_BLOCK_REF_BY_TIMESTAMP, + async { + ManagedModeApiClient::l2_block_ref_by_timestamp(client.as_ref(), timestamp).await + }, + "node" => self.config.url.clone() + )?; + + Ok(block_info) + } + + async fn block_ref_by_number(&self, block_number: u64) -> Result { + let client = self.get_ws_client().await?; + let block_info = observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_BLOCK_REF_BY_NUMBER, + async { + ManagedModeApiClient::l2_block_ref_by_number(client.as_ref(), block_number).await + }, + "node" => self.config.url.clone() + )?; + + Ok(block_info) + } + + async fn reset_pre_interop(&self) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_RESET_PRE_INTEROP, + async { + ManagedModeApiClient::reset_pre_interop(client.as_ref()).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn reset( + &self, + unsafe_id: BlockNumHash, + cross_unsafe_id: BlockNumHash, + local_safe_id: BlockNumHash, + cross_safe_id: BlockNumHash, + finalised_id: BlockNumHash, + ) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_RESET, + async { + ManagedModeApiClient::reset(client.as_ref(), unsafe_id, cross_unsafe_id, local_safe_id, cross_safe_id, finalised_id).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_INVALIDATE_BLOCK, + async { + ManagedModeApiClient::invalidate_block(client.as_ref(), seal).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_PROVIDE_L1, + async { + ManagedModeApiClient::provide_l1(client.as_ref(), block_info).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_UPDATE_FINALIZED, + async { + ManagedModeApiClient::update_finalized(client.as_ref(), finalized_block_id).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_UPDATE_CROSS_UNSAFE, + async { + ManagedModeApiClient::update_cross_unsafe(client.as_ref(), cross_unsafe_block_id).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ClientError> { + let client = self.get_ws_client().await?; + observe_metrics_for_result_async!( + Metrics::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + Metrics::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + Metrics::RPC_METHOD_UPDATE_CROSS_SAFE, + async { + ManagedModeApiClient::update_cross_safe(client.as_ref(), derived_block_id, source_block_id).await + }, + "node" => self.config.url.clone() + )?; + Ok(()) + } +} diff --git a/kona/crates/supervisor/core/src/syncnode/command.rs b/kona/crates/supervisor/core/src/syncnode/command.rs new file mode 100644 index 0000000000000..92b96e6c70398 --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/command.rs @@ -0,0 +1,36 @@ +use alloy_eips::BlockNumHash; +use kona_supervisor_types::BlockSeal; + +/// Commands for managing a node in the supervisor. +/// These commands are sent to the managed node actor to perform various operations. +#[derive(Debug, PartialEq, Eq)] +pub enum ManagedNodeCommand { + /// Updates the finalized block in the managed node. + UpdateFinalized { + /// [`BlockNumHash`] of the finalized block. + block_id: BlockNumHash, + }, + + /// Updates the cross-unsafe block in the managed node. + UpdateCrossUnsafe { + /// [`BlockNumHash`] of the cross-unsafe block. + block_id: BlockNumHash, + }, + + /// Updates the cross-safe block in the managed node. + UpdateCrossSafe { + /// [`BlockNumHash`] of the source block. + source_block_id: BlockNumHash, + /// [`BlockNumHash`] of the derived block. + derived_block_id: BlockNumHash, + }, + + /// Resets the managed node. + Reset {}, + + /// Asks managed node to invalidate the block. + InvalidateBlock { + /// [`BlockSeal`] of the block to invalidate. + seal: BlockSeal, + }, +} diff --git a/kona/crates/supervisor/core/src/syncnode/error.rs b/kona/crates/supervisor/core/src/syncnode/error.rs new file mode 100644 index 0000000000000..cd429fe6e4cf9 --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/error.rs @@ -0,0 +1,67 @@ +use kona_supervisor_storage::StorageError; +use thiserror::Error; + +/// Represents various errors that can occur during node management. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ManagedNodeError { + /// Represents an error that occurred while starting the managed node. + #[error(transparent)] + ClientError(#[from] ClientError), + + /// Represents an error that occurred while fetching data from the storage. + #[error(transparent)] + StorageError(#[from] StorageError), + + /// Unable to successfully fetch block. + #[error("failed to get block by number, number: {0}")] + GetBlockByNumberFailed(u64), + + /// Represents an error that occurred while sending an event to the channel. + #[error("failed to send event to channel: {0}")] + ChannelSendFailed(String), + + /// Represents an error that occurred while resetting the managed node. + #[error("failed to reset the managed node")] + ResetFailed, +} + +/// Error establishing authenticated connection to managed node. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum AuthenticationError { + /// Missing valid JWT secret for authentication header. + #[error("jwt secret not found or invalid")] + InvalidJwt, + /// Invalid header format. + #[error("invalid authorization header")] + InvalidHeader, +} + +/// Represents errors that can occur while interacting with the managed node client. +#[derive(Debug, Error)] +pub enum ClientError { + /// Represents an error that occurred while starting the managed node. + #[error(transparent)] + Client(#[from] jsonrpsee::core::ClientError), + + /// Represents an error that occurred while authenticating to the managed node. + #[error("failed to authenticate: {0}")] + Authentication(#[from] AuthenticationError), + + /// Represents an error that occurred while parsing a chain ID from a string. + #[error(transparent)] + ChainIdParseError(#[from] std::num::ParseIntError), +} + +impl PartialEq for ClientError { + fn eq(&self, other: &Self) -> bool { + use ClientError::*; + match (self, other) { + (Client(a), Client(b)) => a.to_string() == b.to_string(), + (Authentication(a), Authentication(b)) => a == b, + (ChainIdParseError(a), ChainIdParseError(b)) => a == b, + _ => false, + } + } +} + +impl Eq for ClientError {} diff --git a/kona/crates/supervisor/core/src/syncnode/metrics.rs b/kona/crates/supervisor/core/src/syncnode/metrics.rs new file mode 100644 index 0000000000000..fa668173e89df --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/metrics.rs @@ -0,0 +1,103 @@ +//! Metrics for the Managed Mode RPC client. + +/// Container for metrics. +#[derive(Debug, Clone)] +pub(super) struct Metrics; + +impl Metrics { + // --- Metric Names --- + /// Identifier for the counter of successful RPC requests. Labels: `method`. + pub(crate) const MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL: &'static str = + "managed_node_rpc_requests_success_total"; + /// Identifier for the counter of failed RPC requests. Labels: `method`. + pub(crate) const MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL: &'static str = + "managed_node_rpc_requests_error_total"; + /// Identifier for the histogram of RPC request durations. Labels: `method`. + pub(crate) const MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS: &'static str = + "managed_node_rpc_request_duration_seconds"; + + pub(crate) const RPC_METHOD_CHAIN_ID: &'static str = "chain_id"; + pub(crate) const RPC_METHOD_SUBSCRIBE_EVENTS: &'static str = "subscribe_events"; + pub(crate) const RPC_METHOD_FETCH_RECEIPTS: &'static str = "fetch_receipts"; + pub(crate) const RPC_METHOD_OUTPUT_V0_AT_TIMESTAMP: &'static str = "output_v0_at_timestamp"; + pub(crate) const RPC_METHOD_PENDING_OUTPUT_V0_AT_TIMESTAMP: &'static str = + "pending_output_v0_at_timestamp"; + pub(crate) const RPC_METHOD_L2_BLOCK_REF_BY_TIMESTAMP: &'static str = + "l2_block_ref_by_timestamp"; + pub(crate) const RPC_METHOD_BLOCK_REF_BY_NUMBER: &'static str = "block_ref_by_number"; + pub(crate) const RPC_METHOD_RESET_PRE_INTEROP: &'static str = "reset_pre_interop"; + pub(crate) const RPC_METHOD_RESET: &'static str = "reset"; + pub(crate) const RPC_METHOD_INVALIDATE_BLOCK: &'static str = "invalidate_block"; + pub(crate) const RPC_METHOD_PROVIDE_L1: &'static str = "provide_l1"; + pub(crate) const RPC_METHOD_UPDATE_FINALIZED: &'static str = "update_finalized"; + pub(crate) const RPC_METHOD_UPDATE_CROSS_UNSAFE: &'static str = "update_cross_unsafe"; + pub(crate) const RPC_METHOD_UPDATE_CROSS_SAFE: &'static str = "update_cross_safe"; + + /// Initializes metrics for the Supervisor RPC service. + /// + /// This does two things: + /// * Describes various metrics. + /// * Initializes metrics with their labels to 0 so they can be queried immediately. + pub(crate) fn init(node: &str) { + Self::describe(); + Self::zero(node); + } + + /// Describes metrics used in the Supervisor RPC service. + fn describe() { + metrics::describe_counter!( + Self::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successful RPC requests processed by the managed mode client" + ); + metrics::describe_counter!( + Self::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of failed RPC requests processed by the managed mode client" + ); + metrics::describe_histogram!( + Self::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + metrics::Unit::Seconds, + "Duration of RPC requests processed by the managed mode client" + ); + } + + fn zero_rpc_method(method: &str, node: &str) { + metrics::counter!( + Self::MANAGED_NODE_RPC_REQUESTS_SUCCESS_TOTAL, + "method" => method.to_string(), + "node" => node.to_string() + ) + .increment(0); + metrics::counter!( + Self::MANAGED_NODE_RPC_REQUESTS_ERROR_TOTAL, + "method" => method.to_string(), + "node" => node.to_string() + ) + .increment(0); + metrics::histogram!( + Self::MANAGED_NODE_RPC_REQUEST_DURATION_SECONDS, + "method" => method.to_string(), + "node" => node.to_string() + ) + .record(0.0); + } + + /// Initializes metrics with their labels to `0` so they appear in Prometheus from the start. + fn zero(node: &str) { + Self::zero_rpc_method(Self::RPC_METHOD_CHAIN_ID, node); + Self::zero_rpc_method(Self::RPC_METHOD_SUBSCRIBE_EVENTS, node); + Self::zero_rpc_method(Self::RPC_METHOD_FETCH_RECEIPTS, node); + Self::zero_rpc_method(Self::RPC_METHOD_OUTPUT_V0_AT_TIMESTAMP, node); + Self::zero_rpc_method(Self::RPC_METHOD_PENDING_OUTPUT_V0_AT_TIMESTAMP, node); + Self::zero_rpc_method(Self::RPC_METHOD_L2_BLOCK_REF_BY_TIMESTAMP, node); + Self::zero_rpc_method(Self::RPC_METHOD_BLOCK_REF_BY_NUMBER, node); + Self::zero_rpc_method(Self::RPC_METHOD_RESET_PRE_INTEROP, node); + Self::zero_rpc_method(Self::RPC_METHOD_RESET, node); + Self::zero_rpc_method(Self::RPC_METHOD_INVALIDATE_BLOCK, node); + Self::zero_rpc_method(Self::RPC_METHOD_PROVIDE_L1, node); + Self::zero_rpc_method(Self::RPC_METHOD_UPDATE_FINALIZED, node); + Self::zero_rpc_method(Self::RPC_METHOD_UPDATE_CROSS_UNSAFE, node); + Self::zero_rpc_method(Self::RPC_METHOD_UPDATE_CROSS_SAFE, node); + } +} diff --git a/kona/crates/supervisor/core/src/syncnode/mod.rs b/kona/crates/supervisor/core/src/syncnode/mod.rs new file mode 100644 index 0000000000000..d248524e796af --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/mod.rs @@ -0,0 +1,23 @@ +//! Supervisor core syncnode module +//! This module provides the core functionality for managing nodes in the supervisor environment. + +mod command; +pub use command::ManagedNodeCommand; + +mod node; +pub use node::ManagedNode; + +mod error; +pub use error::{AuthenticationError, ClientError, ManagedNodeError}; + +mod traits; +pub use traits::{ + BlockProvider, ManagedNodeController, ManagedNodeDataProvider, ManagedNodeProvider, + SubscriptionHandler, +}; + +mod client; +pub use client::{Client, ClientConfig, ManagedNodeClient}; + +pub(super) mod metrics; +pub(super) mod resetter; diff --git a/kona/crates/supervisor/core/src/syncnode/node.rs b/kona/crates/supervisor/core/src/syncnode/node.rs new file mode 100644 index 0000000000000..b76c89b45851b --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/node.rs @@ -0,0 +1,943 @@ +//! [`ManagedNode`] implementation for handling events from the managed node. + +use super::{ + BlockProvider, ManagedNodeClient, ManagedNodeController, ManagedNodeDataProvider, + ManagedNodeError, SubscriptionHandler, resetter::Resetter, +}; +use crate::event::ChainEvent; +use alloy_eips::BlockNumberOrTag; +use alloy_network::Ethereum; +use alloy_primitives::{B256, ChainId}; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_types_eth::BlockNumHash; +use async_trait::async_trait; +use kona_interop::{BlockReplacement, DerivedRefPair}; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DerivationStorageReader, HeadRefStorageReader, LogStorageReader}; +use kona_supervisor_types::{BlockSeal, OutputV0, Receipts}; +use std::sync::Arc; +use tokio::sync::{Mutex, mpsc}; +use tracing::{debug, error, trace, warn}; + +/// [`ManagedNode`] processes events dispatched from the managed node. +/// +/// It implements `SubscriptionHandler`, forwards resulting `ChainEvent`s to the chain +/// processor, and delegates control operations to the underlying client/resetter. +/// The WebSocket subscription lifecycle (subscription creation, reconnection/restart) +/// is managed by the supervisor actor and the client, not by this type. +#[derive(Debug)] +pub struct ManagedNode { + /// The attached web socket client + client: Arc, + /// Shared L1 provider for fetching receipts + l1_provider: RootProvider, + /// Resetter for handling node resets + resetter: Arc>, + /// Channel for sending events to the chain processor + chain_event_sender: mpsc::Sender, + + /// Cached chain ID + chain_id: Mutex>, +} + +impl ManagedNode +where + DB: LogStorageReader + DerivationStorageReader + HeadRefStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + /// Creates a new [`ManagedNode`] with the specified client. + pub fn new( + client: Arc, + db_provider: Arc, + l1_provider: RootProvider, + chain_event_sender: mpsc::Sender, + ) -> Self { + let resetter = Arc::new(Resetter::new(client.clone(), l1_provider.clone(), db_provider)); + + Self { client, resetter, l1_provider, chain_event_sender, chain_id: Mutex::new(None) } + } + + /// Returns the [`ChainId`] of the [`ManagedNode`]. + /// If the chain ID is already cached, it returns that. + /// If not, it fetches the chain ID from the managed node. + pub async fn chain_id(&self) -> Result { + // we are caching the chain ID here to avoid multiple calls to the client + // there is a possibility that chain ID might be being cached in the client already + // but we are caching it here to make sure it caches in the `ManagedNode` context + let mut cache = self.chain_id.lock().await; + if let Some(chain_id) = *cache { + Ok(chain_id) + } else { + let chain_id = self.client.chain_id().await?; + *cache = Some(chain_id); + Ok(chain_id) + } + } +} + +#[async_trait] +impl SubscriptionHandler for ManagedNode +where + DB: LogStorageReader + DerivationStorageReader + HeadRefStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + async fn handle_exhaust_l1( + &self, + derived_ref_pair: &DerivedRefPair, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!( + target: "supervisor::managed_node", + %chain_id, + %derived_ref_pair, + "Handling L1 exhaust event" + ); + + let next_block_number = derived_ref_pair.source.number + 1; + let next_block = self + .l1_provider + .get_block_by_number(BlockNumberOrTag::Number(next_block_number)) + .await + .map_err(|err| { + error!(target: "supervisor::managed_node", %chain_id, %err, "Failed to fetch next L1 block"); + ManagedNodeError::GetBlockByNumberFailed(next_block_number) + })?; + + let block = match next_block { + Some(block) => block, + None => { + // If the block is None, it means the block is either empty or unavailable. + // ignore this case + return Ok(()); + } + }; + + let new_source = BlockInfo { + hash: block.header.hash, + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + }; + + if new_source.parent_hash != derived_ref_pair.source.hash { + // this could happen due to a reorg. + // this case should be handled by the reorg manager + debug!( + target: "supervisor::managed_node", + %chain_id, + %new_source, + current_source = %derived_ref_pair.source, + "Parent hash mismatch. Possible reorg detected" + ); + } + + self.client.provide_l1(new_source).await.inspect_err(|err| { + error!( + target: "supervisor::managed_node", + %chain_id, + %new_source, + %err, + "Failed to provide L1 block" + ); + })?; + Ok(()) + } + + async fn handle_reset(&self, reset_id: &str) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, reset_id, "Handling reset event"); + + self.resetter.reset().await?; + Ok(()) + } + + async fn handle_unsafe_block(&self, unsafe_block: &BlockInfo) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, %unsafe_block, "Unsafe block event received"); + + self.chain_event_sender.send(ChainEvent::UnsafeBlock { block: *unsafe_block }).await.map_err(|err| { + warn!(target: "supervisor::managed_node", %chain_id, %err, "Failed to send unsafe block event"); + ManagedNodeError::ChannelSendFailed(err.to_string()) + })?; + Ok(()) + } + + async fn handle_derivation_update( + &self, + derived_ref_pair: &DerivedRefPair, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, "Derivation update event received"); + + self.chain_event_sender.send(ChainEvent::DerivedBlock { derived_ref_pair: *derived_ref_pair }).await.map_err(|err| { + warn!(target: "supervisor::managed_node", %chain_id, %err, "Failed to send derivation update event"); + ManagedNodeError::ChannelSendFailed(err.to_string()) + })?; + Ok(()) + } + + async fn handle_replace_block( + &self, + replacement: &BlockReplacement, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, %replacement, "Block replacement received"); + + self.chain_event_sender.send(ChainEvent::BlockReplaced { replacement: *replacement }).await.map_err(|err| { + warn!(target: "supervisor::managed_node", %chain_id, %err, "Failed to send block replacement event"); + ManagedNodeError::ChannelSendFailed(err.to_string()) + })?; + Ok(()) + } + + async fn handle_derivation_origin_update( + &self, + origin: &BlockInfo, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, %origin, "Derivation origin update received"); + + self.chain_event_sender.send(ChainEvent::DerivationOriginUpdate { origin: *origin }).await.map_err(|err| { + warn!(target: "supervisor::managed_node", %chain_id, %err, "Failed to send derivation origin update event"); + ManagedNodeError::ChannelSendFailed(err.to_string()) + })?; + Ok(()) + } +} + +/// Implements [`BlockProvider`] for [`ManagedNode`] by delegating to the underlying WebSocket +/// client. +#[async_trait] +impl BlockProvider for ManagedNode +where + DB: LogStorageReader + DerivationStorageReader + HeadRefStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + async fn block_by_number(&self, block_number: u64) -> Result { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, block_number, "Fetching block by number"); + + let block = self.client.block_ref_by_number(block_number).await?; + Ok(block) + } + async fn fetch_receipts(&self, block_hash: B256) -> Result { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, %block_hash, "Fetching receipts for block"); + + let receipt = self.client.fetch_receipts(block_hash).await?; + Ok(receipt) + } +} + +#[async_trait] +impl ManagedNodeDataProvider for ManagedNode +where + DB: LogStorageReader + DerivationStorageReader + HeadRefStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, timestamp, "Fetching output v0 at timestamp"); + + let outputv0 = self.client.output_v0_at_timestamp(timestamp).await?; + Ok(outputv0) + } + + async fn pending_output_v0_at_timestamp( + &self, + timestamp: u64, + ) -> Result { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, timestamp, "Fetching pending output v0 at timestamp"); + + let outputv0 = self.client.pending_output_v0_at_timestamp(timestamp).await?; + Ok(outputv0) + } + + async fn l2_block_ref_by_timestamp( + &self, + timestamp: u64, + ) -> Result { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, timestamp, "Fetching L2 block ref by timestamp"); + + let block = self.client.l2_block_ref_by_timestamp(timestamp).await?; + Ok(block) + } +} + +#[async_trait] +impl ManagedNodeController for ManagedNode +where + DB: LogStorageReader + DerivationStorageReader + HeadRefStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + async fn update_finalized( + &self, + finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!( + target: "supervisor::managed_node", + %chain_id, + finalized_block_number = finalized_block_id.number, + "Updating finalized block" + ); + + self.client.update_finalized(finalized_block_id).await?; + Ok(()) + } + + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!( + target: "supervisor::managed_node", + %chain_id, + cross_unsafe_block_number = cross_unsafe_block_id.number, + "Updating cross unsafe block", + ); + + self.client.update_cross_unsafe(cross_unsafe_block_id).await?; + Ok(()) + } + + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!( + target: "supervisor::managed_node", + %chain_id, + source_block_number = source_block_id.number, + derived_block_number = derived_block_id.number, + "Updating cross safe block" + ); + self.client.update_cross_safe(source_block_id, derived_block_id).await?; + Ok(()) + } + + async fn reset(&self) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!(target: "supervisor::managed_node", %chain_id, "Resetting managed node state"); + + self.resetter.reset().await?; + Ok(()) + } + + async fn invalidate_block(&self, block_seal: BlockSeal) -> Result<(), ManagedNodeError> { + let chain_id = self.chain_id().await?; + trace!( + target: "supervisor::managed_node", + %chain_id, + block_number = block_seal.number, + "Invalidating block" + ); + + self.client.invalidate_block(block_seal).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::ClientError; + use alloy_primitives::{B256, ChainId, hex::FromHex}; + use alloy_provider::RootProvider; + use alloy_rpc_client::RpcClient; + use alloy_transport::mock::*; + use jsonrpsee::core::client::Subscription; + use kona_interop::{BlockReplacement, DerivedRefPair, SafetyLevel}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{ + DerivationStorageReader, HeadRefStorageReader, LogStorageReader, StorageError, + }; + use kona_supervisor_types::{BlockSeal, Log, OutputV0, Receipts, SubscriptionEvent, SuperHead}; + use mockall::{mock, predicate::*}; + use std::sync::Arc; + use tokio::sync::mpsc; + + mock! { + #[derive(Debug)] + pub Client {} + + #[async_trait] + impl ManagedNodeClient for Client { + async fn chain_id(&self) -> Result; + async fn subscribe_events(&self) -> Result, ClientError>; + async fn fetch_receipts(&self, block_hash: B256) -> Result; + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn pending_output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; + async fn block_ref_by_number(&self, block_number: u64) -> Result; + async fn reset_pre_interop(&self) -> Result<(), ClientError>; + async fn reset(&self, unsafe_id: BlockNumHash, cross_unsafe_id: BlockNumHash, local_safe_id: BlockNumHash, cross_safe_id: BlockNumHash, finalised_id: BlockNumHash) -> Result<(), ClientError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_safe(&self, source_block_id: BlockNumHash, derived_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn reset_ws_client(&self); + } + } + + mock! { + #[derive(Debug)] + pub Db {} + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self, block_number: u64, log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, _source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + + impl HeadRefStorageReader for Db { + fn get_safety_head_ref(&self, level: SafetyLevel) -> Result; + fn get_super_head(&self) -> Result; + } + } + + #[tokio::test] + async fn test_chain_id_caching() { + let mut client = MockClient::new(); + + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + // First call fetches from client + let id1 = node.chain_id().await.unwrap(); + assert_eq!(id1, ChainId::from(42u64)); + // Second call uses cache + let id2 = node.chain_id().await.unwrap(); + assert_eq!(id2, ChainId::from(42u64)); + } + + #[tokio::test] + async fn test_handle_unsafe_block_sends_event() { + let unsafe_block = + BlockInfo { hash: B256::ZERO, number: 1, parent_hash: B256::ZERO, timestamp: 123 }; + + let mut client = MockClient::new(); + + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, mut rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let result = node.handle_unsafe_block(&unsafe_block).await; + assert!(result.is_ok()); + + let event = rx.recv().await.unwrap(); + match event { + ChainEvent::UnsafeBlock { block } => assert_eq!(block.number, 1), + _ => panic!("Wrong event"), + } + } + + #[tokio::test] + async fn test_handle_derivation_update_sends_event() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, mut rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let derived_ref_pair = DerivedRefPair { + source: BlockInfo::new(B256::from([0u8; 32]), 0, B256::ZERO, 0), + derived: BlockInfo::new(B256::from([1u8; 32]), 1, B256::ZERO, 0), + }; + + let result = node.handle_derivation_update(&derived_ref_pair).await; + assert!(result.is_ok()); + + let event = rx.recv().await.unwrap(); + match event { + ChainEvent::DerivedBlock { derived_ref_pair: pair } => { + assert_eq!(pair, derived_ref_pair); + } + _ => panic!("Wrong event"), + } + } + + #[tokio::test] + async fn test_handle_replace_block_sends_event() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, mut rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let replacement = BlockReplacement { + replacement: BlockInfo::new(B256::from([1u8; 32]), 1, B256::ZERO, 0), + invalidated: B256::from([2u8; 32]), + }; + + let result = node.handle_replace_block(&replacement).await; + assert!(result.is_ok()); + + let event = rx.recv().await.unwrap(); + match event { + ChainEvent::BlockReplaced { replacement: rep } => assert_eq!(rep, replacement), + _ => panic!("Wrong event"), + } + } + + #[tokio::test] + async fn test_handle_derivation_origin_update_sends_event() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, mut rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let origin = + BlockInfo { hash: B256::ZERO, number: 10, parent_hash: B256::ZERO, timestamp: 12345 }; + + let result = node.handle_derivation_origin_update(&origin).await; + assert!(result.is_ok()); + + let event = rx.recv().await.unwrap(); + match event { + ChainEvent::DerivationOriginUpdate { origin: block } => assert_eq!(block.number, 10), + _ => panic!("Wrong event"), + } + } + + #[tokio::test] + async fn test_handle_exhaust_l1_calls_provide_l1_on_success() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client.expect_provide_l1().times(1).returning(|_| Ok(())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + + let derived_ref_pair = DerivedRefPair { + source: BlockInfo { + hash: B256::from_hex( + "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + ) + .unwrap(), + number: 5, + parent_hash: B256::from([14u8; 32]), + timestamp: 300, + }, + derived: BlockInfo { + hash: B256::from([11u8; 32]), + number: 40, + parent_hash: B256::from([12u8; 32]), + timestamp: 301, + }, + }; + + let next_block = r#"{ + "number": "6", + "hash": "0xd5f1812548be429cbdc6376b29611fc49e06f1359758c4ceaaa3b393e2239f9c", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + asserter.push(MockResponse::Success(serde_json::from_str(next_block).unwrap())); + + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let result = node.handle_exhaust_l1(&derived_ref_pair).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_exhaust_l1_calls_provide_l1_on_parent_hash_mismatch() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client.expect_provide_l1().times(1).returning(|_| Ok(())); // Should be called + + let client = Arc::new(client); + let db = MockDb::new(); + + let derived_ref_pair = DerivedRefPair { + source: BlockInfo { + hash: B256::from([1u8; 32]), // This will NOT match parent_hash below + number: 5, + parent_hash: B256::from([14u8; 32]), + timestamp: 300, + }, + derived: BlockInfo { + hash: B256::from([11u8; 32]), + number: 40, + parent_hash: B256::from([12u8; 32]), + timestamp: 301, + }, + }; + + // Block with mismatched parent_hash + let next_block = r#"{ + "number": "10", + "hash": "0xd5f1812548be429cbdc6376b29611fc49e06f1359758c4ceaaa3b393e2239f9c", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + asserter.push(MockResponse::Success(serde_json::from_str(next_block).unwrap())); + + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), Arc::new(db), l1_provider, tx); + + let result = node.handle_exhaust_l1(&derived_ref_pair).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_handle_reset_calls_resetter() { + let mut client = MockClient::new(); + client.expect_chain_id().times(2).returning(|| Ok(ChainId::from(42u64))); + client.expect_reset_pre_interop().times(1).returning(|| Ok(())); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state() + .times(1) + .returning(|| Err(StorageError::DatabaseNotInitialised)); + + let client = Arc::new(client); + let db = Arc::new(db); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + // Just check that it completes without error + let result = node.handle_reset("reset_id").await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_block_by_number_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client.expect_block_ref_by_number().with(eq(10)).times(1).returning(|_| { + Ok(BlockInfo { + hash: B256::from([1u8; 32]), + number: 10, + parent_hash: B256::from([2u8; 32]), + timestamp: 12345, + }) + }); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let block = node.block_by_number(10).await.unwrap(); + assert_eq!(block.number, 10); + assert_eq!(block.hash, B256::from([1u8; 32])); + } + + #[tokio::test] + async fn test_fetch_receipts_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_fetch_receipts() + .withf(|hash| *hash == B256::from([1u8; 32])) + .times(1) + .returning(|_| Ok(Receipts::default())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let receipts = node.fetch_receipts(B256::from([1u8; 32])).await.unwrap(); + assert!(receipts.is_empty()); + } + + #[tokio::test] + async fn test_output_v0_at_timestamp_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_output_v0_at_timestamp() + .with(eq(12345)) + .times(1) + .returning(|_| Ok(OutputV0::default())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let output = node.output_v0_at_timestamp(12345).await.unwrap(); + assert_eq!(output, OutputV0::default()); + } + + #[tokio::test] + async fn test_pending_output_v0_at_timestamp_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_pending_output_v0_at_timestamp() + .with(eq(54321)) + .times(1) + .returning(|_| Ok(OutputV0::default())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let output = node.pending_output_v0_at_timestamp(54321).await.unwrap(); + assert_eq!(output, OutputV0::default()); + } + + #[tokio::test] + async fn test_l2_block_ref_by_timestamp_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client.expect_l2_block_ref_by_timestamp().with(eq(11111)).times(1).returning(|_| { + Ok(BlockInfo { + hash: B256::from([9u8; 32]), + number: 99, + parent_hash: B256::from([8u8; 32]), + timestamp: 11111, + }) + }); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let block = node.l2_block_ref_by_timestamp(11111).await.unwrap(); + assert_eq!(block.number, 99); + assert_eq!(block.timestamp, 11111); + } + + #[tokio::test] + async fn test_update_finalized_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_update_finalized() + .withf(|block_id| block_id.number == 100) + .times(1) + .returning(|_| Ok(())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let block_id = BlockNumHash { number: 100, hash: B256::from([1u8; 32]) }; + let result = node.update_finalized(block_id).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_update_cross_unsafe_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_update_cross_unsafe() + .withf(|block_id| block_id.number == 200) + .times(1) + .returning(|_| Ok(())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let block_id = BlockNumHash { number: 200, hash: B256::from([2u8; 32]) }; + let result = node.update_cross_unsafe(block_id).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_update_cross_safe_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_update_cross_safe() + .withf(|source, derived| source.number == 300 && derived.number == 301) + .times(1) + .returning(|_, _| Ok(())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let source_block_id = BlockNumHash { number: 300, hash: B256::from([3u8; 32]) }; + let derived_block_id = BlockNumHash { number: 301, hash: B256::from([4u8; 32]) }; + let result = node.update_cross_safe(source_block_id, derived_block_id).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_invalidate_block_delegates_to_client() { + let mut client = MockClient::new(); + client.expect_chain_id().times(1).returning(|| Ok(ChainId::from(42u64))); + client + .expect_invalidate_block() + .withf(|seal| seal.number == 400) + .times(1) + .returning(|_| Ok(())); + + let client = Arc::new(client); + let db = Arc::new(MockDb::new()); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let block_seal = BlockSeal { number: 400, hash: B256::from([5u8; 32]), timestamp: 0 }; + let result = node.invalidate_block(block_seal).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_reset_calls_resetter() { + let mut client = MockClient::new(); + client.expect_chain_id().times(2).returning(|| Ok(ChainId::from(42u64))); + client.expect_reset_pre_interop().times(1).returning(|| Ok(())); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state() + .times(1) + .returning(|| Err(StorageError::DatabaseNotInitialised)); + + let client = Arc::new(client); + let db = Arc::new(db); + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let (tx, _rx) = mpsc::channel(10); + let node = ManagedNode::new(client.clone(), db, l1_provider, tx); + + let result = node.reset().await; + assert!(result.is_ok()); + } +} diff --git a/kona/crates/supervisor/core/src/syncnode/resetter.rs b/kona/crates/supervisor/core/src/syncnode/resetter.rs new file mode 100644 index 0000000000000..84a83f7ded638 --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/resetter.rs @@ -0,0 +1,586 @@ +use super::{ManagedNodeClient, ManagedNodeError}; +use alloy_eips::{BlockNumHash, BlockNumberOrTag}; +use alloy_network::Ethereum; +use alloy_primitives::ChainId; +use alloy_provider::{Provider, RootProvider}; +use kona_protocol::BlockInfo; +use kona_supervisor_storage::{DerivationStorageReader, HeadRefStorageReader, StorageError}; +use kona_supervisor_types::SuperHead; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{error, info, warn}; + +#[derive(Debug)] +pub(super) struct Resetter { + client: Arc, + l1_provider: RootProvider, + db_provider: Arc, + reset_guard: Mutex<()>, +} + +impl Resetter +where + DB: HeadRefStorageReader + DerivationStorageReader + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, +{ + /// Creates a new [`Resetter`] with the specified client. + pub(super) fn new( + client: Arc, + l1_provider: RootProvider, + db_provider: Arc, + ) -> Self { + Self { client, l1_provider, db_provider, reset_guard: Mutex::new(()) } + } + + /// Resets the node using the latest super head. + pub(crate) async fn reset(&self) -> Result<(), ManagedNodeError> { + // get the chain ID to log it, this is useful for debugging + // no performance impact as it is cached in the client + let chain_id = self.client.chain_id().await?; + let _guard = self.reset_guard.lock().await; + + let local_safe = match self.get_latest_valid_local_safe(chain_id).await { + Ok(block) => block, + // todo: require refactor and corner case handling + Err(ManagedNodeError::StorageError(StorageError::DatabaseNotInitialised)) => { + self.reset_pre_interop(chain_id).await?; + return Ok(()); + } + Err(err) => { + error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to get latest valid derived block"); + return Err(ManagedNodeError::ResetFailed); + } + }; + + // check if the source of valid local_safe is canonical + // If the source block is not canonical, it mean there is a reorg on L1 + // this makes sure that we always reset to a valid state + let source = self.db_provider.derived_to_source(local_safe.id())?; + if !self.is_canonical(chain_id, source.id()).await? { + warn!(target: "supervisor::syncnode_resetter", %chain_id, %source, "Source block for the valid local safe is not canonical"); + return Err(ManagedNodeError::ResetFailed); + } + + let SuperHead { cross_unsafe, cross_safe, finalized, .. } = + self.db_provider.get_super_head().inspect_err( + |err| error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to get super head"), + )?; + + // using the local safe block as the local unsafe as well + let local_unsafe = local_safe; + + let mut cross_unsafe = cross_unsafe.unwrap_or_else(BlockInfo::default); + if cross_unsafe.number > local_unsafe.number { + cross_unsafe = local_unsafe; + } + + let mut cross_safe = cross_safe.unwrap_or_else(BlockInfo::default); + if cross_safe.number > local_safe.number { + cross_safe = local_safe; + } + + let mut finalized = match finalized { + Some(block) => block, + // fall back to activation block if finalized is None + None => self.db_provider.get_activation_block()?, + }; + + if finalized.number > local_safe.number { + finalized = local_safe; + } + + info!(target: "supervisor::syncnode_resetter", + %chain_id, + %local_unsafe, + %cross_unsafe, + %local_safe, + %cross_safe, + %finalized, + "Resetting managed node with latest information", + ); + + self.client + .reset( + local_unsafe.id(), + cross_unsafe.id(), + local_safe.id(), + cross_safe.id(), + finalized.id(), + ) + .await + .inspect_err(|err| { + error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to reset managed node"); + })?; + Ok(()) + } + + async fn reset_pre_interop(&self, chain_id: ChainId) -> Result<(), ManagedNodeError> { + info!(target: "supervisor::syncnode_resetter", %chain_id, "Resetting the node to pre-interop state"); + + self.client.reset_pre_interop().await.inspect_err(|err| { + error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to reset managed node to pre-interop state"); + })?; + Ok(()) + } + + async fn get_latest_valid_local_safe( + &self, + chain_id: ChainId, + ) -> Result { + let latest_derivation_state = self.db_provider.latest_derivation_state()?; + let mut local_safe = latest_derivation_state.derived; + + loop { + let node_block = self.client.block_ref_by_number(local_safe.number).await.inspect_err( + |err| error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to get block by number"), + )?; + + // If the local safe block matches the node block, we can return the super + // head right away + if node_block == local_safe { + return Ok(local_safe); + } + + // Get the source block for the current local safe, this helps to skip empty source + // blocks + let source_block = self + .db_provider + .derived_to_source(local_safe.id()) + .inspect_err(|err| error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to get source block for the local safe head ref"))?; + + // Get the previous source block id + let prev_source_id = + BlockNumHash { number: source_block.number - 1, hash: source_block.parent_hash }; + + // If the previous source block id is 0, we cannot reset further. This should not happen + // in prod, added for safety during dev environment. + if prev_source_id.number == 0 { + error!(target: "supervisor::syncnode_resetter", %chain_id, "Source block number is 0, cannot reset further"); + return Err(ManagedNodeError::ResetFailed); + } + + // Get the latest derived block at the previous source block, this helps to skip derived + // blocks. If this loop is executed, it means there is something wrong with + // derivation. Faster to go back source blocks than to go back derived + // blocks. + local_safe = self + .db_provider + .latest_derived_block_at_source(prev_source_id) + .inspect_err(|err| { + error!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to get latest derived block for the previous source block") + })?; + } + } + + async fn is_canonical( + &self, + chain_id: ChainId, + source: BlockNumHash, + ) -> Result { + let canonical_block = self + .l1_provider + .get_block_by_number(BlockNumberOrTag::Number(source.number)) + .await + .map_err(|err| { + warn!(target: "supervisor::syncnode_resetter", %chain_id, %err, "Failed to fetch source block from L1"); + ManagedNodeError::GetBlockByNumberFailed(source.number) + })?; + + canonical_block.map_or_else(|| Ok(false), |block| Ok(block.hash() == source.hash)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syncnode::{AuthenticationError, ClientError}; + use alloy_eips::BlockNumHash; + use alloy_primitives::{B256, ChainId}; + use alloy_provider::mock::{Asserter, MockResponse, MockTransport}; + use alloy_rpc_client::RpcClient; + use async_trait::async_trait; + use jsonrpsee::core::client::Subscription; + use kona_interop::{DerivedRefPair, SafetyLevel}; + use kona_protocol::BlockInfo; + use kona_supervisor_storage::{DerivationStorageReader, HeadRefStorageReader, StorageError}; + use kona_supervisor_types::{BlockSeal, OutputV0, Receipts, SubscriptionEvent, SuperHead}; + use mockall::{mock, predicate}; + + // Mock for HeadRefStorageReader + mock! { + #[derive(Debug)] + pub Db {} + + impl HeadRefStorageReader for Db { + fn get_safety_head_ref(&self, level: SafetyLevel) -> Result; + fn get_super_head(&self) -> Result; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + } + + mock! { + #[derive(Debug)] + pub Client {} + + #[async_trait] + impl ManagedNodeClient for Client { + async fn chain_id(&self) -> Result; + async fn subscribe_events(&self) -> Result, ClientError>; + async fn fetch_receipts(&self, block_hash: B256) -> Result; + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn pending_output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; + async fn block_ref_by_number(&self, block_number: u64) -> Result; + async fn reset_pre_interop(&self) -> Result<(), ClientError>; + async fn reset(&self, unsafe_id: BlockNumHash, cross_unsafe_id: BlockNumHash, local_safe_id: BlockNumHash, cross_safe_id: BlockNumHash, finalised_id: BlockNumHash) -> Result<(), ClientError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_safe(&self, source_block_id: BlockNumHash, derived_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn reset_ws_client(&self); + } + } + + fn make_super_head() -> SuperHead { + SuperHead { + local_unsafe: BlockInfo::new(B256::from([0u8; 32]), 5, B256::ZERO, 0), + cross_unsafe: Some(BlockInfo::new(B256::from([1u8; 32]), 4, B256::ZERO, 0)), + local_safe: Some(BlockInfo::new(B256::from([2u8; 32]), 3, B256::ZERO, 0)), + cross_safe: Some(BlockInfo::new(B256::from([3u8; 32]), 2, B256::ZERO, 0)), + finalized: Some(BlockInfo::new(B256::from([4u8; 32]), 1, B256::ZERO, 0)), + l1_source: Some(BlockInfo::new(B256::from([54u8; 32]), 100, B256::ZERO, 0)), + } + } + + #[tokio::test] + async fn test_reset_success() { + let super_head = make_super_head(); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(move || { + Ok(DerivedRefPair { + derived: super_head.local_safe.unwrap(), + source: super_head.l1_source.unwrap(), + }) + }); + db.expect_get_super_head().returning(move || Ok(super_head)); + + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + client.expect_block_ref_by_number().returning(move |_| Ok(super_head.local_safe.unwrap())); + + db.expect_derived_to_source() + .with(predicate::eq(super_head.local_safe.unwrap().id())) + .returning(move |_| Ok(super_head.l1_source.unwrap())); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + let canonical_block = r#"{ + "number": "100", + "hash": "0x3636363636363636363636363636363636363636363636363636363636363636", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + asserter.push(MockResponse::Success(serde_json::from_str(canonical_block).unwrap())); + + client.expect_reset().returning(|_, _, _, _, _| Ok(())); + + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_ok()); + } + + #[tokio::test] + async fn test_reset_canonical_hash_mismatch() { + let super_head = make_super_head(); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(move || { + Ok(DerivedRefPair { + derived: super_head.local_safe.unwrap(), + source: super_head.l1_source.unwrap(), + }) + }); + db.expect_get_super_head().returning(move || Ok(super_head)); + + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + client.expect_block_ref_by_number().returning(move |_| Ok(super_head.local_safe.unwrap())); + + db.expect_derived_to_source() + .with(predicate::eq(super_head.local_safe.unwrap().id())) + .returning(move |_| Ok(super_head.l1_source.unwrap())); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + let canonical_block = r#"{ + "number": "100", + "hash": "0x3737373737373737373737373737373737373737373737373737373737367637", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + asserter.push(MockResponse::Success(serde_json::from_str(canonical_block).unwrap())); + + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_err()); + } + + #[tokio::test] + async fn test_reset_db_error() { + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(|| Err(StorageError::LockPoisoned)); + + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_err()); + } + + #[tokio::test] + async fn test_reset_block_error() { + let super_head = make_super_head(); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(move || { + Ok(DerivedRefPair { + derived: super_head.local_safe.unwrap(), + source: super_head.l1_source.unwrap(), + }) + }); + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + client + .expect_block_ref_by_number() + .returning(|_| Err(ClientError::Authentication(AuthenticationError::InvalidHeader))); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_err()); + } + + #[tokio::test] + async fn test_reset_inconsistency() { + let super_head = make_super_head(); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(move || { + Ok(DerivedRefPair { + derived: super_head.local_safe.unwrap(), + source: super_head.l1_source.unwrap(), + }) + }); + + let prev_source_block = BlockInfo::new(B256::from([8u8; 32]), 101, B256::ZERO, 0); + let current_source_block = + BlockInfo::new(B256::from([7u8; 32]), 102, prev_source_block.hash, 0); + let last_valid_derived_block = BlockInfo::new(B256::from([6u8; 32]), 9, B256::ZERO, 0); + + // return expected values when get_last_valid_derived_block() is called + db.expect_derived_to_source() + .with(predicate::eq(super_head.local_safe.unwrap().id())) + .returning(move |_| Ok(current_source_block)); + db.expect_latest_derived_block_at_source() + .with(predicate::eq(prev_source_block.id())) + .returning(move |_| Ok(last_valid_derived_block)); + + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + // Return a block that does not match local_safe + client + .expect_block_ref_by_number() + .with(predicate::eq(super_head.local_safe.unwrap().number)) + .returning(|_| Ok(BlockInfo::new(B256::from([4u8; 32]), 3, B256::ZERO, 0))); + // On second call, return the last valid derived block + client + .expect_block_ref_by_number() + .with(predicate::eq(last_valid_derived_block.number)) + .returning(move |_| Ok(last_valid_derived_block)); + + db.expect_derived_to_source() + .with(predicate::eq(last_valid_derived_block.id())) + .returning(move |_| Ok(prev_source_block)); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + let canonical_block = r#"{ + "number": "100", + "hash": "0x0808080808080808080808080808080808080808080808080808080808080808", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + asserter.push(MockResponse::Success(serde_json::from_str(canonical_block).unwrap())); + + db.expect_get_super_head().returning(move || Ok(super_head)); + + client.expect_reset().times(1).returning(|_, _, _, _, _| Ok(())); + + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_ok()); + } + + #[tokio::test] + async fn test_reset_rpc_error() { + let super_head = make_super_head(); + + let mut db = MockDb::new(); + db.expect_latest_derivation_state().returning(move || { + Ok(DerivedRefPair { + derived: super_head.local_safe.unwrap(), + source: super_head.l1_source.unwrap(), + }) + }); + + db.expect_derived_to_source() + .with(predicate::eq(super_head.local_safe.unwrap().id())) + .returning(move |_| Ok(super_head.l1_source.unwrap())); + + let asserter = Asserter::new(); + let transport = MockTransport::new(asserter.clone()); + let l1_provider = RootProvider::::new(RpcClient::new(transport, false)); + + let canonical_block = r#"{ + "number": "100", + "hash": "0x3636363636363636363636363636363636363636363636363636363636363636", + "mixHash": "0x24900fb3da77674a861c428429dce0762707ecb6052325bbd9b3c64e74b5af9d", + "parentHash": "0x1f68ac259155e2f38211ddad0f0a15394d55417b185a93923e2abe71bb7a4d6d", + "nonce": "0x378da40ff335b070", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x00000000000000100000004080000000000500000000000000020000100000000800001000000004000001000000000000000800040010000020100000000400000010000000000000000040000000000000040000000000000000000000000000000400002400000000000000000000000000000004000004000000000000840000000800000080010004000000001000000800000000000000000000000000000000000800000000000040000000020000000000000000000800000400000000000000000000000600000400000000002000000000000000000000004000000000000000100000000000000000000000000000000000040000900010000000", + "transactionsRoot":"0x4d0c8e91e16bdff538c03211c5c73632ed054d00a7e210c0eb25146c20048126", + "stateRoot": "0x91309efa7e42c1f137f31fe9edbe88ae087e6620d0d59031324da3e2f4f93233", + "receiptsRoot": "0x68461ab700003503a305083630a8fb8d14927238f0bc8b6b3d246c0c64f21f4a", + "miner":"0xb42b6c4a95406c78ff892d270ad20b22642e102d", + "difficulty": "0x66e619a", + "totalDifficulty": "0x1e875d746ae", + "extraData": "0xd583010502846765746885676f312e37856c696e7578", + "size": "0x334", + "gasLimit": "0x47e7c4", + "gasUsed": "0x37993", + "timestamp": "0x5835c54d", + "uncles": [], + "transactions": [ + "0xa0807e117a8dd124ab949f460f08c36c72b710188f01609595223b325e58e0fc", + "0xeae6d797af50cb62a596ec3939114d63967c374fa57de9bc0f4e2b576ed6639d" + ], + "baseFeePerGas": "0x7", + "withdrawalsRoot": "0x7a4ecf19774d15cf9c15adf0dd8e8a250c128b26c9e2ab2a08d6c9c8ffbd104f", + "withdrawals": [], + "blobGasUsed": "0x0", + "excessBlobGas": "0x0", + "parentBeaconBlockRoot": "0x95c4dbd5b19f6fe3cbc3183be85ff4e85ebe75c5b4fc911f1c91e5b7a554a685" + }"#; + asserter.push(MockResponse::Success(serde_json::from_str(canonical_block).unwrap())); + + db.expect_get_super_head().returning(move || Ok(super_head)); + + let mut client = MockClient::new(); + client.expect_chain_id().returning(move || Ok(1)); + client.expect_block_ref_by_number().returning(move |_| Ok(super_head.local_safe.unwrap())); + client.expect_reset().returning(|_, _, _, _, _| { + Err(ClientError::Authentication(AuthenticationError::InvalidJwt)) + }); + + let resetter = Resetter::new(Arc::new(client), l1_provider, Arc::new(db)); + + assert!(resetter.reset().await.is_err()); + } +} diff --git a/kona/crates/supervisor/core/src/syncnode/traits.rs b/kona/crates/supervisor/core/src/syncnode/traits.rs new file mode 100644 index 0000000000000..7d81aa21bfaf1 --- /dev/null +++ b/kona/crates/supervisor/core/src/syncnode/traits.rs @@ -0,0 +1,197 @@ +use super::ManagedNodeError; +use alloy_eips::BlockNumHash; +use alloy_primitives::B256; +use async_trait::async_trait; +use kona_interop::{BlockReplacement, DerivedRefPair}; +use kona_protocol::BlockInfo; +use kona_supervisor_types::{BlockSeal, OutputV0, Receipts}; +use std::fmt::Debug; + +/// Represents a handler for subscription events. +#[async_trait] +pub trait SubscriptionHandler: Send + Sync { + /// Handles the exhaustion L1 exhaust event from the node. + async fn handle_exhaust_l1( + &self, + derived_ref_pair: &DerivedRefPair, + ) -> Result<(), ManagedNodeError>; + + /// Handles the reset event from the node. + async fn handle_reset(&self, reset_id: &str) -> Result<(), ManagedNodeError>; + + /// Handles the unsafe block event from the node. + async fn handle_unsafe_block(&self, block: &BlockInfo) -> Result<(), ManagedNodeError>; + + /// Handles the derivation update event from the node. + async fn handle_derivation_update( + &self, + derived_ref_pair: &DerivedRefPair, + ) -> Result<(), ManagedNodeError>; + + /// Handles the block replacement event from the node. + async fn handle_replace_block( + &self, + replacement: &BlockReplacement, + ) -> Result<(), ManagedNodeError>; + + /// Handles the derivation origin update event from the node. + async fn handle_derivation_origin_update( + &self, + origin: &BlockInfo, + ) -> Result<(), ManagedNodeError>; +} + +/// [`BlockProvider`] abstracts fetching blocks and receipts for a given block. +#[async_trait] +pub trait BlockProvider: Send + Sync + Debug { + /// Fetch all transaction receipts for the block with the given hash. + /// + /// # Arguments + /// * `block_hash` - The hash of the block whose receipts should be fetched. + /// + /// # Returns + /// [Receipts] representing all transaction receipts in the block, + /// or an error if the fetch fails. + async fn fetch_receipts(&self, block_hash: B256) -> Result; + + /// Returns the block info for the given block number + async fn block_by_number(&self, number: u64) -> Result; +} + +/// [`ManagedNodeDataProvider`] abstracts the managed node data APIs that supervisor uses to fetch +/// info from the managed node. +#[async_trait] +pub trait ManagedNodeDataProvider: Send + Sync + Debug { + /// Fetch the output v0 at a given timestamp. + /// + /// # Arguments + /// * `timestamp` - The timestamp to fetch the output v0 at. + /// + /// # Returns + /// The output v0 at the given timestamp, + /// or an error if the fetch fails. + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + + /// Fetch the pending output v0 at a given timestamp. + /// + /// # Arguments + /// * `timestamp` - The timestamp to fetch the pending output v0 at. + /// + /// # Returns + /// The pending output v0 at the given timestamp, + /// or an error if the fetch fails. + async fn pending_output_v0_at_timestamp( + &self, + timestamp: u64, + ) -> Result; + + /// Fetch the l2 block ref by timestamp. + /// + /// # Arguments + /// * `timestamp` - The timestamp to fetch the l2 block ref at. + /// + /// # Returns + /// The l2 block ref at the given timestamp. + async fn l2_block_ref_by_timestamp( + &self, + timestamp: u64, + ) -> Result; +} + +/// [`ManagedNodeController`] abstracts the managed node control APIs that supervisor uses to +/// control the managed node state. +#[async_trait] +pub trait ManagedNodeController: Send + Sync + Debug { + /// Update the finalized block head using the given [`BlockNumHash`]. + /// + /// # Arguments + /// * `finalized_block_id` - The block number and hash of the finalized block + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(ManagedNodeError)` if the update fails + async fn update_finalized( + &self, + finalized_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + /// Update the cross unsafe block head using the given [`BlockNumHash`]. + /// + /// # Arguments + /// * `cross_unsafe_block_id` - The block number and hash of the cross unsafe block + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(ManagedNodeError)` if the update fails + async fn update_cross_unsafe( + &self, + cross_unsafe_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + /// Update the cross safe block head using the given [`BlockNumHash`]. + /// + /// # Arguments + /// * `source_block_id` - The block number and hash of the L1 block + /// * `derived_block_id` - The block number and hash of the new cross safe block + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(ManagedNodeError)` if the update fails + async fn update_cross_safe( + &self, + source_block_id: BlockNumHash, + derived_block_id: BlockNumHash, + ) -> Result<(), ManagedNodeError>; + + /// Reset the managed node based on the supervisor's state. + /// This is typically used to reset the node's state + /// when the supervisor detects a misalignment + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(ManagedNodeError)` if the reset fails + async fn reset(&self) -> Result<(), ManagedNodeError>; + + /// Instructs the managed node to invalidate a block. + /// This is used when the supervisor detects an invalid block + /// and needs to roll back the node's state. + /// + /// # Arguments + /// * `seal` - The [`BlockSeal`] of the block. + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(ManagedNodeError)` if the invalidation fails + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; +} + +/// Composite trait for any node that provides: +/// - Event subscriptions (`NodeSubscriber`) +/// - Receipt access (`ReceiptProvider`) +/// - Managed node API access (`ManagedNodeApiProvider`) +/// +/// This is the main abstraction used for a fully-managed node +/// within the supervisor context. +#[async_trait] +pub trait ManagedNodeProvider: + SubscriptionHandler + + BlockProvider + + ManagedNodeDataProvider + + ManagedNodeController + + Send + + Sync + + Debug +{ +} + +#[async_trait] +impl ManagedNodeProvider for T where + T: SubscriptionHandler + + BlockProvider + + ManagedNodeDataProvider + + ManagedNodeController + + Send + + Sync + + Debug +{ +} diff --git a/kona/crates/supervisor/metrics/Cargo.toml b/kona/crates/supervisor/metrics/Cargo.toml new file mode 100644 index 0000000000000..817b1e031b74e --- /dev/null +++ b/kona/crates/supervisor/metrics/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "kona-supervisor-metrics" +version = "0.1.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/kona/crates/supervisor/metrics/src/lib.rs b/kona/crates/supervisor/metrics/src/lib.rs new file mode 100644 index 0000000000000..6bd6a301fc8d9 --- /dev/null +++ b/kona/crates/supervisor/metrics/src/lib.rs @@ -0,0 +1,5 @@ +//! Metrics collection and reporting for the supervisor. +mod reporter; +pub use reporter::MetricsReporter; + +mod macros; diff --git a/kona/crates/supervisor/metrics/src/macros.rs b/kona/crates/supervisor/metrics/src/macros.rs new file mode 100644 index 0000000000000..9b24f96f54573 --- /dev/null +++ b/kona/crates/supervisor/metrics/src/macros.rs @@ -0,0 +1,75 @@ +/// Macro to observe a call to a storage method and record metrics. +#[macro_export] +macro_rules! observe_metrics_for_result { + ( + $success_metric:expr, + $error_metric:expr, + $duration_metric:expr, + $method_name:expr, + $block:expr $(, $tag_key:expr => $tag_val:expr )* + ) => {{ + let start_time = std::time::Instant::now(); + let result = $block; + let duration = start_time.elapsed().as_secs_f64(); + + if result.is_ok() { + metrics::counter!( + $success_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).increment(1); + } else { + metrics::counter!( + $error_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).increment(1); + } + + metrics::histogram!( + $duration_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).record(duration); + + result + }}; +} + +/// Macro to observe a call to an async function and record metrics. +#[macro_export] +macro_rules! observe_metrics_for_result_async { + ( + $success_metric:expr, + $error_metric:expr, + $duration_metric:expr, + $method_name:expr, + $block:expr $(, $tag_key:expr => $tag_val:expr )* + ) => {{ + let start_time = std::time::Instant::now(); + let result = $block.await; + let duration = start_time.elapsed().as_secs_f64(); + + if result.is_ok() { + metrics::counter!( + $success_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).increment(1); + } else { + metrics::counter!( + $error_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).increment(1); + } + + metrics::histogram!( + $duration_metric, + "method" => $method_name + $(, $tag_key => $tag_val )* + ).record(duration); + + result + }}; +} diff --git a/kona/crates/supervisor/metrics/src/reporter.rs b/kona/crates/supervisor/metrics/src/reporter.rs new file mode 100644 index 0000000000000..6b0f391a32c59 --- /dev/null +++ b/kona/crates/supervisor/metrics/src/reporter.rs @@ -0,0 +1,9 @@ +/// Defines a contract for types that can report metrics. +/// This trait is intended to be implemented by types that need to report metrics +pub trait MetricsReporter { + /// Reports metrics for the implementing type. + /// This function is intended to be called periodically to collect and report metrics. + /// The implementation should gather relevant metrics and report them to the configured metrics + /// backend. + fn report_metrics(&self); +} diff --git a/kona/crates/supervisor/rpc/Cargo.toml b/kona/crates/supervisor/rpc/Cargo.toml new file mode 100644 index 0000000000000..d0fdec143c2e3 --- /dev/null +++ b/kona/crates/supervisor/rpc/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "kona-supervisor-rpc" +description = "Kona Supervisor RPC" +version = "0.1.1" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-interop.workspace = true +kona-protocol.workspace = true +kona-supervisor-types.workspace = true + +# jsonrpsee +serde.workspace = true +serde_json.workspace = true +jsonrpsee = { workspace = true, optional = true, features = ["macros", "server"] } +async-trait.workspace = true + +# Alloy +alloy-eips.workspace = true +alloy-serde.workspace = true +alloy-primitives = { workspace = true, features = ["map", "rlp", "serde"] } +op-alloy-consensus.workspace = true +alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde", "std"], optional = true } +tokio = { workspace = true, features = ["time", "sync"], optional = true } +derive_more = { workspace = true, default-features = false, features = ["display", "from", "constructor", "std"], optional = true } + +# `reqwest` feature dependencies +alloy-rpc-client = { workspace = true, features = ["reqwest"], optional = true } +thiserror = { workspace = true, optional = true } + +[features] +serde = [ + "alloy-eips/serde", + "alloy-primitives/serde", + "alloy-rpc-types-engine?/serde", + "kona-interop/serde", + "kona-protocol/serde", + "op-alloy-consensus/serde", +] +# The `jsonrpsee` feature enables the core RPC functionality. +# When it's active, we also need the `serde` feature. +jsonrpsee = [ "dep:jsonrpsee", "serde" ] +# Client feature builds upon the base jsonrpsee feature +client = [ "jsonrpsee", "jsonrpsee/client" ] +# Server feature for supervisor RPC server functionality +server = [ + "dep:alloy-rpc-types-engine", + "dep:derive_more", + "dep:tokio", + "jsonrpsee", +] +# reqwest client feature +reqwest = [ + "client", + "dep:alloy-rpc-client", + "dep:derive_more", + "dep:thiserror", +] diff --git a/kona/crates/supervisor/rpc/README.md b/kona/crates/supervisor/rpc/README.md new file mode 100644 index 0000000000000..5ac480f498c2d --- /dev/null +++ b/kona/crates/supervisor/rpc/README.md @@ -0,0 +1 @@ +## `kona-supervisor-rpc` \ No newline at end of file diff --git a/kona/crates/supervisor/rpc/src/config.rs b/kona/crates/supervisor/rpc/src/config.rs new file mode 100644 index 0000000000000..3c454b01dc96d --- /dev/null +++ b/kona/crates/supervisor/rpc/src/config.rs @@ -0,0 +1,41 @@ +//! Contains the Configuration for the supervisor RPC server. + +#[cfg(feature = "server")] +use alloy_rpc_types_engine::JwtSecret; +#[cfg(feature = "server")] +use std::net::SocketAddr; + +/// The RPC Config. +#[cfg(feature = "server")] +#[derive(Debug, Clone)] +pub struct SupervisorRpcConfig { + /// If the RPC is disabled. + /// By default, the RPC server is disabled. + pub rpc_disabled: bool, + /// The socket address for the RPC server. + pub socket_address: SocketAddr, + /// The JWT secret for the RPC server. + pub jwt_secret: JwtSecret, +} + +#[cfg(feature = "server")] +impl SupervisorRpcConfig { + /// Returns if the rpc is disabled. + pub const fn is_disabled(&self) -> bool { + self.rpc_disabled + } +} + +// By default, the RPC server is disabled. +// As such, the socket address and JWT secret are unused +// and can be set to random values. +#[cfg(feature = "server")] +impl std::default::Default for SupervisorRpcConfig { + fn default() -> Self { + Self { + rpc_disabled: true, + socket_address: SocketAddr::new(std::net::Ipv4Addr::UNSPECIFIED.into(), 9333), + jwt_secret: JwtSecret::random(), + } + } +} diff --git a/kona/crates/supervisor/rpc/src/jsonrpsee.rs b/kona/crates/supervisor/rpc/src/jsonrpsee.rs new file mode 100644 index 0000000000000..494c6888d1a51 --- /dev/null +++ b/kona/crates/supervisor/rpc/src/jsonrpsee.rs @@ -0,0 +1,231 @@ +//! The Optimism Supervisor RPC API using `jsonrpsee` + +pub use jsonrpsee::{ + core::{RpcResult, SubscriptionResult}, + types::{ErrorCode, ErrorObjectOwned}, +}; + +use crate::{SuperRootOutputRpc, SupervisorSyncStatus}; +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, BlockHash, ChainId, map::HashMap}; +use jsonrpsee::proc_macros::rpc; +use kona_interop::{ + DependencySet, DerivedIdPair, DerivedRefPair, ExecutingDescriptor, ManagedEvent, SafetyLevel, +}; +use kona_protocol::BlockInfo; +use kona_supervisor_types::{BlockSeal, HexStringU64, OutputV0, Receipts, SubscriptionEvent}; +use serde::{Deserialize, Serialize}; + +/// Supervisor API for interop. +/// +/// See spec . +// TODO:: add all the methods +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "supervisor"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "supervisor"))] +pub trait SupervisorApi { + /// Gets the source block for a given derived block + #[method(name = "crossDerivedToSource")] + async fn cross_derived_to_source( + &self, + chain_id: HexStringU64, + block_id: BlockNumHash, + ) -> RpcResult; + + /// Returns the [`LocalUnsafe`] block for given chain. + /// + /// Spec: + /// + /// [`LocalUnsafe`]: SafetyLevel::LocalUnsafe + #[method(name = "localUnsafe")] + async fn local_unsafe(&self, chain_id: HexStringU64) -> RpcResult; + + /// Returns the [`LocalSafe`] block for given chain. + /// + /// Spec: + /// + /// [`LocalSafe`]: SafetyLevel::LocalSafe + #[method(name = "localSafe")] + async fn local_safe(&self, chain_id: HexStringU64) -> RpcResult; + + /// Returns the [`CrossSafe`] block for given chain. + /// + /// Spec: + /// + /// [`CrossSafe`]: SafetyLevel::CrossSafe + #[method(name = "crossSafe")] + async fn cross_safe(&self, chain_id: HexStringU64) -> RpcResult; + + /// Returns the [`Finalized`] block for the given chain. + /// + /// Spec: + /// + /// [`Finalized`]: SafetyLevel::Finalized + #[method(name = "finalized")] + async fn finalized(&self, chain_id: HexStringU64) -> RpcResult; + + /// Returns the finalized L1 block that the supervisor is synced to. + /// + /// Spec: + #[method(name = "finalizedL1")] + async fn finalized_l1(&self) -> RpcResult; + + /// Returns the [`SuperRootOutput`] at a specified timestamp, which represents the global + /// state across all monitored chains. Contains the + /// - Highest L1 [`BlockNumHash`] that is cross-safe among all chains + /// - Timestamp of the super root + /// - The [`SuperRoot`] hash + /// - All chains [`ChainRootInfo`]s + /// + /// Spec: + /// + /// [`SuperRootOutput`]: kona_interop::SuperRootOutput + /// [`SuperRoot`]: kona_interop::SuperRoot + /// [`ChainRootInfo`]: kona_interop::ChainRootInfo + #[method(name = "superRootAtTimestamp")] + async fn super_root_at_timestamp( + &self, + timestamp: HexStringU64, + ) -> RpcResult; + + /// Verifies if an access-list references only valid messages w.r.t. locally configured minimum + /// [`SafetyLevel`]. + #[method(name = "checkAccessList")] + async fn check_access_list( + &self, + inbox_entries: Vec, + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> RpcResult<()>; + + /// Describes superchain sync status. + /// + /// Spec: + #[method(name = "syncStatus")] + async fn sync_status(&self) -> RpcResult; + + /// Returns the last derived block, for each chain, from the given L1 block. This block is at + /// least [`LocalSafe`]. + /// + /// Spec: + /// + /// [`LocalSafe`]: SafetyLevel::LocalSafe + #[method(name = "allSafeDerivedAt")] + async fn all_safe_derived_at( + &self, + derived_from: BlockNumHash, + ) -> RpcResult>; + + /// Returns the [`DependencySet`] for the supervisor. + /// + /// Spec: + /// TODO: Replace the link above after the PR is merged. + #[method(name = "dependencySetV1")] + async fn dependency_set_v1(&self) -> RpcResult; +} + +/// Supervisor API for admin operations. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "admin"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "admin"))] +pub trait SupervisorAdminApi { + /// Adds L2RPC to the supervisor. + #[method(name = "addL2RPC")] + async fn add_l2_rpc(&self, url: String, jwt_secret: String) -> RpcResult<()>; +} + +/// Represents the topics for subscriptions in the Managed Mode API. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SubscriptionTopic { + /// The topic for events from the managed node. + Events, +} + +/// ManagedModeApi to send control signals to a managed node from supervisor +/// And get info for syncing the state with the given L2. +/// +/// See spec +/// Using the proc_macro to generate the client and server code. +/// Default namespace separator is `_`. +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "interop"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "interop"))] +pub trait ManagedModeApi { + /// Subscribe to the events from the managed node. + /// Op-node provides the `interop-subscribe` method for subscribing to the events topic. + /// Subscription notifications are then sent via the `interop-subscription` method as + /// [`SubscriptionEvent`]s. + // Currently, the `events` topic must be explicitly passed as a parameter to the subscription + // request, even though this function is specifically intended to subscribe to the `events` + // topic. todo: Find a way to eliminate the need to pass the topic explicitly. + #[subscription(name = "subscribe" => "subscription", item = SubscriptionEvent, unsubscribe = "unsubscribe")] + async fn subscribe_events(&self, topic: SubscriptionTopic) -> SubscriptionResult; + + /// Pull an event from the managed node. + #[method(name = "pullEvent")] + async fn pull_event(&self) -> RpcResult; + + /// Control signals sent to the managed node from supervisor + /// Update the cross unsafe block head + #[method(name = "updateCrossUnsafe")] + async fn update_cross_unsafe(&self, id: BlockNumHash) -> RpcResult<()>; + + /// Update the cross safe block head + #[method(name = "updateCrossSafe")] + async fn update_cross_safe(&self, derived: BlockNumHash, source: BlockNumHash) + -> RpcResult<()>; + + /// Update the finalized block head + #[method(name = "updateFinalized")] + async fn update_finalized(&self, id: BlockNumHash) -> RpcResult<()>; + + /// Invalidate a block + #[method(name = "invalidateBlock")] + async fn invalidate_block(&self, seal: BlockSeal) -> RpcResult<()>; + + /// Send the next L1 block + #[method(name = "provideL1")] + async fn provide_l1(&self, next_l1: BlockInfo) -> RpcResult<()>; + + /// Get the genesis block ref for l1 and l2; Soon to be deprecated! + #[method(name = "anchorPoint")] + async fn anchor_point(&self) -> RpcResult; + + /// Reset the managed node to the pre-interop state + #[method(name = "resetPreInterop")] + async fn reset_pre_interop(&self) -> RpcResult<()>; + + /// Reset the managed node to the specified block heads + #[method(name = "reset")] + async fn reset( + &self, + local_unsafe: BlockNumHash, + cross_unsafe: BlockNumHash, + local_safe: BlockNumHash, + cross_safe: BlockNumHash, + finalized: BlockNumHash, + ) -> RpcResult<()>; + + /// Sync methods that supervisor uses to sync with the managed node + /// Fetch all receipts for a give block + #[method(name = "fetchReceipts")] + async fn fetch_receipts(&self, block_hash: BlockHash) -> RpcResult; + + /// Get block info for a given block number + #[method(name = "l2BlockRefByNumber")] + async fn l2_block_ref_by_number(&self, number: u64) -> RpcResult; + + /// Get the chain id + #[method(name = "chainID")] + async fn chain_id(&self) -> RpcResult; + + /// Get the state_root, message_parser_storage_root, and block_hash at a given timestamp + #[method(name = "outputV0AtTimestamp")] + async fn output_v0_at_timestamp(&self, timestamp: u64) -> RpcResult; + + /// Get the pending state_root, message_parser_storage_root, and block_hash at a given timestamp + #[method(name = "pendingOutputV0AtTimestamp")] + async fn pending_output_v0_at_timestamp(&self, timestamp: u64) -> RpcResult; + + /// Get the l2 block ref for a given timestamp + #[method(name = "l2BlockRefByTimestamp")] + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> RpcResult; +} diff --git a/kona/crates/supervisor/rpc/src/lib.rs b/kona/crates/supervisor/rpc/src/lib.rs new file mode 100644 index 0000000000000..459bb3e31b1f6 --- /dev/null +++ b/kona/crates/supervisor/rpc/src/lib.rs @@ -0,0 +1,30 @@ +#![doc = include_str!("../README.md")] + +#[cfg(feature = "jsonrpsee")] +pub mod jsonrpsee; +#[cfg(all(feature = "jsonrpsee", feature = "client"))] +pub use jsonrpsee::{ManagedModeApiClient, SupervisorAdminApiClient, SupervisorApiClient}; +#[cfg(feature = "jsonrpsee")] +pub use jsonrpsee::{SupervisorAdminApiServer, SupervisorApiServer}; + +#[cfg(feature = "server")] +pub mod config; +#[cfg(feature = "server")] +pub use config::SupervisorRpcConfig; + +#[cfg(feature = "server")] +pub mod server; +#[cfg(feature = "server")] +pub use server::SupervisorRpcServer; + +#[cfg(feature = "reqwest")] +pub mod reqwest; +#[cfg(feature = "reqwest")] +pub use reqwest::{CheckAccessListClient, SupervisorClient, SupervisorClientError}; + +pub mod response; +pub use response::{ + ChainRootInfoRpc, SuperRootOutputRpc, SupervisorChainSyncStatus, SupervisorSyncStatus, +}; + +pub use kona_protocol::BlockInfo; diff --git a/kona/crates/supervisor/rpc/src/reqwest.rs b/kona/crates/supervisor/rpc/src/reqwest.rs new file mode 100644 index 0000000000000..eea78aa08b464 --- /dev/null +++ b/kona/crates/supervisor/rpc/src/reqwest.rs @@ -0,0 +1,65 @@ +//! RPC API implementation using `reqwest` + +#[cfg(feature = "reqwest")] +use alloy_primitives::B256; +#[cfg(feature = "reqwest")] +use alloy_rpc_client::ReqwestClient; +#[cfg(feature = "reqwest")] +use derive_more::Constructor; +#[cfg(feature = "reqwest")] +use kona_interop::{ExecutingDescriptor, SafetyLevel}; + +/// Error types for supervisor RPC interactions +#[cfg(feature = "reqwest")] +#[derive(Debug, thiserror::Error)] +pub enum SupervisorClientError { + /// RPC client error + #[error("RPC client error: {0}")] + Client(Box), +} + +#[cfg(feature = "reqwest")] +impl SupervisorClientError { + /// Creates a new client error + pub fn client(err: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Client(Box::new(err)) + } +} + +/// Subset of `op-supervisor` API, used for validating interop events. +#[cfg(feature = "reqwest")] +pub trait CheckAccessListClient { + /// Returns if the messages meet the minimum safety level. + fn check_access_list( + &self, + inbox_entries: &[B256], + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> impl std::future::Future> + Send; +} + +/// A supervisor client. +#[cfg(feature = "reqwest")] +#[derive(Debug, Clone, Constructor)] +pub struct SupervisorClient { + /// The inner RPC client. + client: ReqwestClient, +} + +#[cfg(feature = "reqwest")] +impl CheckAccessListClient for SupervisorClient { + async fn check_access_list( + &self, + inbox_entries: &[B256], + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> Result<(), SupervisorClientError> { + self.client + .request( + "supervisor_checkAccessList", + (inbox_entries, min_safety, executing_descriptor), + ) + .await + .map_err(SupervisorClientError::client) + } +} diff --git a/kona/crates/supervisor/rpc/src/response.rs b/kona/crates/supervisor/rpc/src/response.rs new file mode 100644 index 0000000000000..0cb500ad77190 --- /dev/null +++ b/kona/crates/supervisor/rpc/src/response.rs @@ -0,0 +1,326 @@ +//! Supervisor RPC response types. + +use alloy_eips::BlockNumHash; +use alloy_primitives::{B256, Bytes, ChainId, map::HashMap}; +use kona_protocol::BlockInfo; +use kona_supervisor_types::SuperHead; +use serde::{Deserialize, Serialize, Serializer}; + +/// Describes superchain sync status. +/// +/// Specs: . +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SupervisorSyncStatus { + /// [`BlockInfo`] of highest L1 block. + pub min_synced_l1: BlockInfo, + /// Timestamp of highest cross-safe block. + /// + /// NOTE: Some fault-proof releases may already depend on `safe`, so we keep JSON field name as + /// `safe`. + #[cfg_attr(feature = "serde", serde(rename = "safeTimestamp"))] + pub cross_safe_timestamp: u64, + /// Timestamp of highest finalized block. + pub finalized_timestamp: u64, + /// Map of all tracked chains and their individual [`SupervisorChainSyncStatus`]. + pub chains: HashMap, +} + +/// Describes the sync status for a specific chain. +/// +/// Specs: +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SupervisorChainSyncStatus { + /// Highest [`Unsafe`] head of chain. + /// + /// [`Unsafe`]: op_alloy_consensus::interop::SafetyLevel::LocalUnsafe + pub local_unsafe: BlockInfo, + /// Highest [`CrossUnsafe`] head of chain. + /// + /// [`CrossUnsafe`]: op_alloy_consensus::interop::SafetyLevel::CrossUnsafe + pub cross_unsafe: BlockNumHash, + /// Highest [`LocalSafe`] head of chain. + /// + /// [`LocalSafe`]: op_alloy_consensus::interop::SafetyLevel::LocalSafe + pub local_safe: BlockNumHash, + /// Highest [`Safe`] head of chain [`BlockNumHash`]. + /// + /// NOTE: Some fault-proof releases may already depend on `safe`, so we keep JSON field name as + /// `safe`. + /// + /// [`Safe`]: op_alloy_consensus::interop::SafetyLevel::CrossSafe + #[cfg_attr(feature = "serde", serde(rename = "safe"))] + pub cross_safe: BlockNumHash, + /// Highest [`Finalized`] head of chain [`BlockNumHash`]. + /// + /// [`Finalized`]: op_alloy_consensus::interop::SafetyLevel::Finalized + pub finalized: BlockNumHash, +} + +impl From for SupervisorChainSyncStatus { + fn from(super_head: SuperHead) -> Self { + let SuperHead { local_unsafe, cross_unsafe, local_safe, cross_safe, finalized, .. } = + super_head; + + let cross_unsafe = cross_unsafe.unwrap_or_else(BlockInfo::default); + let local_safe = local_safe.unwrap_or_else(BlockInfo::default); + let cross_safe = cross_safe.unwrap_or_else(BlockInfo::default); + let finalized = finalized.unwrap_or_else(BlockInfo::default); + + Self { + local_unsafe, + local_safe: local_safe.id(), + cross_unsafe: cross_unsafe.id(), + cross_safe: cross_safe.id(), + finalized: finalized.id(), + } + } +} + +/// This is same as [`kona_interop::ChainRootInfo`] but with [`u64`] serializing as a valid hex +/// string. +/// +/// Required by +/// [`super_root_at_timestamp`](crate::jsonrpsee::SupervisorApiServer::super_root_at_timestamp) RPC +/// for marshalling and unmarshalling in GO implementation. Required for e2e tests. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChainRootInfoRpc { + /// The chain ID. + #[serde(rename = "chainID", with = "alloy_serde::quantity")] + pub chain_id: ChainId, + /// The canonical output root of the latest canonical block at a particular timestamp. + pub canonical: B256, + /// The pending output root. + /// + /// This is the output root preimage for the latest block at a particular timestamp prior to + /// validation of executing messages. If the original block was valid, this will be the + /// preimage of the output root from the `canonical` array. If it was invalid, it will be + /// the output root preimage from the optimistic block deposited transaction added to the + /// deposit-only block. + pub pending: Bytes, +} + +/// This is same as [`kona_interop::SuperRootOutput`] but with timestamp serializing as a valid hex +/// string. version is also serialized as an even length hex string. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SuperRootOutputRpc { + /// The Highest L1 Block that is cross-safe among all chains. + pub cross_safe_derived_from: BlockNumHash, + /// The timestamp of the super root. + #[serde(with = "alloy_serde::quantity")] + pub timestamp: u64, + /// The super root hash. + pub super_root: B256, + /// The version of the super root. + #[serde(serialize_with = "serialize_u8_as_hex")] + pub version: u8, + /// The chain root info for each chain in the dependency set. + /// It represents the state of the chain at or before the timestamp. + pub chains: Vec, +} + +/// Serializes a [u8] as a hex string. Ensure that the hex string has an even length. +/// +/// This is used to serialize the [`SuperRootOutputRpc`]'s version field as a hex string. +fn serialize_u8_as_hex(value: &u8, serializer: S) -> Result +where + S: Serializer, +{ + let hex_string = format!("0x{value:02x}"); + serializer.serialize_str(&hex_string) +} + +#[cfg(test)] +mod test { + use super::*; + use alloy_primitives::b256; + use kona_interop::SUPER_ROOT_VERSION; + + const CHAIN_STATUS: &str = r#" + { + "localUnsafe": { + "number": 100, + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "timestamp": 40044440000, + "parentHash": "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + }, + "crossUnsafe": { + "number": 90, + "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + }, + "localSafe": { + "number": 80, + "hash": "0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef13" + }, + "safe": { + "number": 70, + "hash": "0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234" + }, + "finalized": { + "number": 60, + "hash": "0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12" + } + }"#; + + const STATUS: &str = r#" + { + "minSyncedL1": { + "number": 100, + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "timestamp": 40044440000, + "parentHash": "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + }, + "safeTimestamp": 40044450000, + "finalizedTimestamp": 40044460000, + "chains" : { + "1": { + "localUnsafe": { + "number": 100, + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "timestamp": 40044440000, + "parentHash": "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + }, + "crossUnsafe": { + "number": 90, + "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + }, + "localSafe": { + "number": 80, + "hash": "0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef13" + }, + "safe": { + "number": 70, + "hash": "0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234" + }, + "finalized": { + "number": 60, + "hash": "0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12" + } + } + } + }"#; + + #[cfg(feature = "serde")] + #[test] + fn test_serialize_supervisor_chain_sync_status() { + assert_eq!( + serde_json::from_str::(CHAIN_STATUS) + .expect("should deserialize"), + SupervisorChainSyncStatus { + local_unsafe: BlockInfo { + number: 100, + hash: b256!( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ), + timestamp: 40044440000, + parent_hash: b256!( + "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + ), + }, + cross_unsafe: BlockNumHash::new( + 90, + b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + ), + local_safe: BlockNumHash::new( + 80, + b256!("0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef13") + ), + cross_safe: BlockNumHash::new( + 70, + b256!("0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234") + ), + finalized: BlockNumHash::new( + 60, + b256!("0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12") + ), + } + ) + } + + #[cfg(feature = "serde")] + #[test] + fn test_serialize_supervisor_sync_status() { + let mut chains = HashMap::default(); + + chains.insert( + 1, + SupervisorChainSyncStatus { + local_unsafe: BlockInfo { + number: 100, + hash: b256!( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ), + timestamp: 40044440000, + parent_hash: b256!( + "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + ), + }, + cross_unsafe: BlockNumHash::new( + 90, + b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"), + ), + local_safe: BlockNumHash::new( + 80, + b256!("0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef13"), + ), + cross_safe: BlockNumHash::new( + 70, + b256!("0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234"), + ), + finalized: BlockNumHash::new( + 60, + b256!("0x34567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"), + ), + }, + ); + + assert_eq!( + serde_json::from_str::(STATUS).expect("should deserialize"), + SupervisorSyncStatus { + min_synced_l1: BlockInfo { + number: 100, + hash: b256!( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ), + timestamp: 40044440000, + parent_hash: b256!( + "0x111def1234567890abcdef1234567890abcdef1234500000abcdef123456aaaa" + ), + }, + cross_safe_timestamp: 40044450000, + finalized_timestamp: 40044460000, + chains, + } + ) + } + + #[test] + fn test_super_root_version_even_length_hex() { + let root = SuperRootOutputRpc { + cross_safe_derived_from: BlockNumHash::default(), + timestamp: 0, + super_root: B256::default(), + version: SUPER_ROOT_VERSION, + chains: vec![], + }; + let json = serde_json::to_string(&root).expect("should serialize"); + let v: serde_json::Value = serde_json::from_str(&json).expect("valid json"); + let version_field = + v.get("version").expect("version field present").as_str().expect("version is string"); + let hex_part = &version_field[2..]; // remove 0x + assert_eq!(hex_part.len() % 2, 0, "Hex string should have even length"); + // For SUPER_ROOT_VERSION = 1, should be 0x01 + assert_eq!(version_field, "0x01"); + } +} diff --git a/kona/crates/supervisor/rpc/src/server.rs b/kona/crates/supervisor/rpc/src/server.rs new file mode 100644 index 0000000000000..8684e9f7d9ebb --- /dev/null +++ b/kona/crates/supervisor/rpc/src/server.rs @@ -0,0 +1,55 @@ +//! Minimal supervisor RPC server implementation + +#[cfg(feature = "server")] +use alloy_rpc_types_engine::JwtSecret; +#[cfg(feature = "server")] +use jsonrpsee::server::ServerHandle; +#[cfg(feature = "server")] +use kona_interop::{ControlEvent, ManagedEvent}; +#[cfg(feature = "server")] +use std::net::SocketAddr; +#[cfg(feature = "server")] +use tokio::sync::broadcast; + +/// Minimal supervisor RPC server +#[cfg(feature = "server")] +#[derive(Debug)] +pub struct SupervisorRpcServer { + /// A channel to receive [`ManagedEvent`] from the node. + #[allow(dead_code)] + managed_events: broadcast::Receiver, + /// A channel to send [`ControlEvent`]. + #[allow(dead_code)] + control_events: broadcast::Sender, + /// A JWT token for authentication. + #[allow(dead_code)] + jwt_token: JwtSecret, + /// The socket address for the RPC server. + socket: SocketAddr, +} + +#[cfg(feature = "server")] +impl SupervisorRpcServer { + /// Creates a new instance of the `SupervisorRpcServer`. + pub const fn new( + managed_events: broadcast::Receiver, + control_events: broadcast::Sender, + jwt_token: JwtSecret, + socket: SocketAddr, + ) -> Self { + Self { managed_events, control_events, jwt_token, socket } + } + + /// Returns the socket address for the RPC server. + pub const fn socket(&self) -> SocketAddr { + self.socket + } + + /// Launches the RPC server with the given socket address. + pub async fn launch(self) -> std::io::Result { + let server = jsonrpsee::server::ServerBuilder::default().build(self.socket).await?; + // For now, start without any RPC methods - this is a minimal implementation + let module = jsonrpsee::RpcModule::new(()); + Ok(server.start(module)) + } +} diff --git a/kona/crates/supervisor/service/Cargo.toml b/kona/crates/supervisor/service/Cargo.toml new file mode 100644 index 0000000000000..f442af6ec5c49 --- /dev/null +++ b/kona/crates/supervisor/service/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "kona-supervisor-service" +version = "0.1.0" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +# Workspace dependencies +kona-supervisor-core = { workspace = true } +kona-supervisor-rpc = { workspace = true, features = ["jsonrpsee"] } +kona-supervisor-storage = { workspace = true } +kona-supervisor-metrics = { workspace = true } +kona-interop.workspace = true +kona-supervisor-types.workspace = true + +# External dependencies +jsonrpsee = { workspace = true, features = ["macros", "server"] } +mockall = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true} +alloy-eips = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-rpc-types-eth = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } +kona-genesis = { workspace = true } +kona-protocol = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync", "macros"] } +tokio-util = { workspace = true } +derive_more.workspace = true + +# Dev dependencies +alloy-rpc-client = { workspace = true } diff --git a/kona/crates/supervisor/service/src/actors/metric.rs b/kona/crates/supervisor/service/src/actors/metric.rs new file mode 100644 index 0000000000000..7b95b273b56a7 --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/metric.rs @@ -0,0 +1,107 @@ +use async_trait::async_trait; +use kona_supervisor_metrics::MetricsReporter; +use std::{io, sync::Arc, time::Duration}; +use tokio::time::sleep; +use tokio_util::sync::CancellationToken; +use tracing::info; + +use crate::SupervisorActor; + +#[derive(derive_more::Constructor)] +pub struct MetricWorker { + interval: Duration, + // list of reporters + reporters: Vec>, + cancel_token: CancellationToken, +} + +#[async_trait] +impl SupervisorActor for MetricWorker +where + R: MetricsReporter + Send + Sync + 'static, +{ + type InboundEvent = (); + type Error = io::Error; + + async fn start(mut self) -> Result<(), Self::Error> { + info!( + target: "supervisor::metric_worker", + "Starting MetricWorker with interval: {:?}", + self.interval + ); + + let reporters = self.reporters; + let interval = self.interval; + + loop { + if self.cancel_token.is_cancelled() { + info!("MetricReporter actor is stopping due to cancellation."); + break; + } + + for reporter in &reporters { + reporter.report_metrics(); + } + sleep(interval).await; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use mockall::{mock, predicate::*}; + use std::{sync::Arc, time::Duration}; + use tokio_util::sync::CancellationToken; + + mock! ( + #[derive(Debug)] + pub Reporter {} + + impl MetricsReporter for Reporter { + fn report_metrics(&self); + } + ); + + #[tokio::test] + async fn test_metric_worker_reports_metrics_and_stops_on_cancel() { + let mut mock_reporter = MockReporter::new(); + mock_reporter.expect_report_metrics().return_const(()); + + let reporter = Arc::new(mock_reporter); + let cancel_token = CancellationToken::new(); + + let worker = MetricWorker::new( + Duration::from_millis(50), + vec![reporter.clone()], + cancel_token.clone(), + ); + + let handle = tokio::spawn(worker.start()); + + tokio::time::sleep(Duration::from_millis(120)).await; + cancel_token.cancel(); + + let _ = handle.await; + } + + #[tokio::test] + async fn test_metric_worker_stops_immediately_on_cancel() { + let mut mock_reporter = MockReporter::new(); + mock_reporter.expect_report_metrics().times(0); + + let reporter = Arc::new(mock_reporter); + let cancel_token = CancellationToken::new(); + + let worker = MetricWorker::new( + Duration::from_millis(100), + vec![reporter.clone()], + cancel_token.clone(), + ); + + cancel_token.cancel(); + + let _ = worker.start().await; + } +} diff --git a/kona/crates/supervisor/service/src/actors/mod.rs b/kona/crates/supervisor/service/src/actors/mod.rs new file mode 100644 index 0000000000000..dae73ffddf028 --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/mod.rs @@ -0,0 +1,20 @@ +//! [SupervisorActor] services for the supervisor. +//! +//! [SupervisorActor]: super::SupervisorActor + +mod traits; +pub use traits::SupervisorActor; + +mod metric; +pub use metric::MetricWorker; + +mod processor; +pub use processor::ChainProcessorActor; + +mod node; +pub use node::ManagedNodeActor; + +mod rpc; +pub use rpc::SupervisorRpcActor; + +pub(super) mod utils; diff --git a/kona/crates/supervisor/service/src/actors/node.rs b/kona/crates/supervisor/service/src/actors/node.rs new file mode 100644 index 0000000000000..445bde8bb6e4f --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/node.rs @@ -0,0 +1,362 @@ +use anyhow::Error; +use async_trait::async_trait; +use derive_more::Constructor; +use kona_interop::ManagedEvent; +use kona_supervisor_core::syncnode::{ + ManagedNodeClient, ManagedNodeCommand, ManagedNodeController, SubscriptionHandler, +}; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +use crate::{SupervisorActor, actors::utils::spawn_task_with_retry}; + +/// Actor for managing a node in the supervisor environment. +#[derive(Debug, Constructor)] +pub struct ManagedNodeActor { + client: Arc, + node: Arc, + command_rx: mpsc::Receiver, + cancel_token: CancellationToken, +} + +#[async_trait] +impl SupervisorActor for ManagedNodeActor +where + C: ManagedNodeClient + 'static, + N: ManagedNodeController + SubscriptionHandler + 'static, +{ + type InboundEvent = ManagedNodeCommand; + type Error = SupervisorRpcActorError; + + async fn start(mut self) -> Result<(), Self::Error> { + // Task 1: Subscription handling + let node = self.node.clone(); + let client = self.client.clone(); + let cancel_token = self.cancel_token.clone(); + + spawn_task_with_retry( + move || { + let handler = node.clone(); + let client = client.clone(); + + async move { run_subscription_task(client, handler).await } + }, + cancel_token, + usize::MAX, + ); + + // Task 2: Command handling + let node = self.node.clone(); + let cancel_token = self.cancel_token.clone(); + run_command_task(node, self.command_rx, cancel_token).await?; + Ok(()) + } +} + +async fn run_command_task( + node: Arc, + mut command_rx: mpsc::Receiver, + cancel_token: CancellationToken, +) -> Result<(), SupervisorRpcActorError> +where + N: ManagedNodeController + SubscriptionHandler + 'static, +{ + info!(target: "supervisor::syncnode_actor", "Starting command task for managed node"); + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!(target: "supervisor::syncnode", "Cancellation requested, shutting down command task"); + return Ok(()); + } + maybe_cmd = command_rx.recv() => { + match maybe_cmd { + Some(cmd) => { + match cmd { + ManagedNodeCommand::UpdateFinalized { block_id } => { + let result = node.update_finalized(block_id).await; + if let Err(err) = result { + warn!( + target: "supervisor::syncnode", + %err, + "Failed to update finalized block" + ); + } + } + ManagedNodeCommand::UpdateCrossUnsafe { block_id } => { + let result = node.update_cross_unsafe(block_id).await; + if let Err(err) = result { + warn!( + target: "supervisor::syncnode", + %err, + "Failed to update cross unsafe block" + ); + } + } + ManagedNodeCommand::UpdateCrossSafe { source_block_id, derived_block_id } => { + let result = node.update_cross_safe(source_block_id, derived_block_id).await; + if let Err(err) = result { + warn!( + target: "supervisor::syncnode", + %err, + "Failed to update cross safe block" + ); + } + } + ManagedNodeCommand::Reset {} => { + let result = node.reset().await; + if let Err(err) = result { + warn!( + target: "supervisor::syncnode", + %err, + "Failed to reset managed node" + ); + } + } + ManagedNodeCommand::InvalidateBlock { seal } => { + let result = node.invalidate_block(seal).await; + if let Err(err) = result { + warn!( + target: "supervisor::syncnode", + %err, + "Failed to invalidate block" + ); + } + } + } + } + None => { + info!(target: "supervisor::syncnode", "Command channel closed, shutting down command task"); + return Err(SupervisorRpcActorError::CommandReceiverClosed); + } + } + } + } + } +} + +async fn run_subscription_task( + client: Arc, + handler: Arc, +) -> Result<(), Error> { + info!(target: "supervisor::syncnode", "Starting subscription task for managed node"); + + let mut subscription = client.subscribe_events().await.inspect_err(|err| { + error!( + target: "supervisor::syncnode", + %err, + "Failed to subscribe to node events" + ); + })?; + + loop { + tokio::select! { + incoming_event = subscription.next() => { + match incoming_event { + Some(Ok(subscription_event)) => { + if let Some(event) = subscription_event.data { + handle_subscription_event(&handler, event).await; + } + } + Some(Err(err)) => { + error!( + target: "supervisor::managed_event_task", + %err, + "Error in event deserialization" + ); + return Err(err.into()); + } + None => { + warn!(target: "supervisor::managed_event_task", "Subscription closed by server"); + client.reset_ws_client().await; + break; + } + } + } + } + } + Ok(()) +} + +async fn handle_subscription_event(handler: &Arc, event: ManagedEvent) { + if let Some(reset_id) = &event.reset { + if let Err(err) = handler.handle_reset(reset_id).await { + warn!( + target: "supervisor::syncnode", + %err, + %reset_id, + "Failed to handle reset event" + ); + } + } + + if let Some(unsafe_block) = &event.unsafe_block { + if let Err(err) = handler.handle_unsafe_block(unsafe_block).await { + warn!( + target: "supervisor::syncnode", + %err, + %unsafe_block, + "Failed to handle unsafe block event" + ); + } + } + + if let Some(derived_ref_pair) = &event.derivation_update { + if event.derivation_origin_update.is_none() { + if let Err(err) = handler.handle_derivation_update(derived_ref_pair).await { + warn!( + target: "supervisor::syncnode", + %err, + %derived_ref_pair, + "Failed to handle derivation update event" + ); + } + } + } + + if let Some(origin) = &event.derivation_origin_update { + if let Err(err) = handler.handle_derivation_origin_update(origin).await { + warn!( + target: "supervisor::syncnode", + %err, + %origin, + "Failed to handle derivation origin update event" + ); + } + } + + if let Some(derived_ref_pair) = &event.exhaust_l1 { + if let Err(err) = handler.handle_exhaust_l1(derived_ref_pair).await { + warn!( + target: "supervisor::syncnode", + %err, + %derived_ref_pair, + "Failed to handle L1 exhaust event" + ); + } + } + + if let Some(replacement) = &event.replace_block { + if let Err(err) = handler.handle_replace_block(replacement).await { + warn!( + target: "supervisor::syncnode", + %err, + %replacement, + "Failed to handle block replacement event" + ); + } + } +} + +#[derive(Debug, Error)] +pub enum SupervisorRpcActorError { + /// Error indicating that command receiver is closed. + #[error("managed node command receiver closed")] + CommandReceiverClosed, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::BlockNumHash; + use alloy_primitives::{B256, ChainId}; + use jsonrpsee::core::client::Subscription; + use kona_interop::{BlockReplacement, DerivedRefPair}; + use kona_protocol::BlockInfo; + use kona_supervisor_core::syncnode::{ + ClientError, ManagedNodeClient, ManagedNodeCommand, ManagedNodeController, + ManagedNodeError, SubscriptionHandler, + }; + use kona_supervisor_types::{BlockSeal, OutputV0, Receipts, SubscriptionEvent}; + use mockall::{mock, predicate::*}; + use std::sync::Arc; + use tokio::sync::mpsc; + use tokio_util::sync::CancellationToken; + + // Mock the ManagedNodeController trait + mock! { + #[derive(Debug)] + pub Node {} + + #[async_trait::async_trait] + impl ManagedNodeController for Node { + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn update_cross_safe(&self,source_block_id: BlockNumHash,derived_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn reset(&self) -> Result<(), ManagedNodeError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; + } + + #[async_trait::async_trait] + impl SubscriptionHandler for Node { + async fn handle_exhaust_l1(&self, derived_ref_pair: &DerivedRefPair) -> Result<(), ManagedNodeError>; + async fn handle_reset(&self, reset_id: &str) -> Result<(), ManagedNodeError>; + async fn handle_unsafe_block(&self, block: &BlockInfo) -> Result<(), ManagedNodeError>; + async fn handle_derivation_update(&self, derived_ref_pair: &DerivedRefPair) -> Result<(), ManagedNodeError>; + async fn handle_replace_block(&self, replacement: &BlockReplacement) -> Result<(), ManagedNodeError>; + async fn handle_derivation_origin_update(&self, origin: &BlockInfo) -> Result<(), ManagedNodeError>; + } + } + + mock! { + #[derive(Debug)] + pub NodeClient {} + + #[async_trait::async_trait] + impl ManagedNodeClient for NodeClient { + async fn chain_id(&self) -> Result; + async fn subscribe_events(&self) -> Result, ClientError>; + async fn fetch_receipts(&self, block_hash: B256) -> Result; + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn pending_output_v0_at_timestamp(&self, timestamp: u64)-> Result; + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; + async fn block_ref_by_number(&self, block_number: u64) -> Result; + async fn reset_pre_interop(&self) -> Result<(), ClientError>; + async fn reset( + &self, + unsafe_id: BlockNumHash, + cross_unsafe_id: BlockNumHash, + local_safe_id: BlockNumHash, + cross_safe_id: BlockNumHash, + finalised_id: BlockNumHash, + ) -> Result<(), ClientError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_unsafe(&self,cross_unsafe_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_safe(&self,source_block_id: BlockNumHash,derived_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn reset_ws_client(&self); + } + } + + #[tokio::test] + async fn test_run_command_task_update_finalized_and_reset() { + let mut mock_node = MockNode::new(); + mock_node.expect_update_finalized().times(1).returning(|_| Ok(())); + mock_node.expect_reset().times(1).returning(|| Ok(())); + + let node = Arc::new(mock_node); + let (tx, rx) = mpsc::channel(10); + let cancel_token = CancellationToken::new(); + + // Spawn the command task + let handle = tokio::spawn(super::run_command_task(node.clone(), rx, cancel_token.clone())); + + // Send commands + tx.send(ManagedNodeCommand::UpdateFinalized { + block_id: BlockNumHash::new(1, B256::random()), + }) + .await + .unwrap(); + tx.send(ManagedNodeCommand::Reset {}).await.unwrap(); + + // Drop the sender to close the channel and end the task + drop(tx); + + // Wait for the task to finish + let result = handle.await.unwrap(); + assert!(matches!(result, Err(SupervisorRpcActorError::CommandReceiverClosed))); + } +} diff --git a/kona/crates/supervisor/service/src/actors/processor.rs b/kona/crates/supervisor/service/src/actors/processor.rs new file mode 100644 index 0000000000000..e3a39dd94bf04 --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/processor.rs @@ -0,0 +1,324 @@ +use async_trait::async_trait; +use kona_interop::InteropValidator; +use kona_supervisor_core::{ChainProcessor, event::ChainEvent, syncnode::BlockProvider}; +use kona_supervisor_storage::{ + DerivationStorage, HeadRefStorageWriter, LogStorage, StorageRewinder, +}; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::info; + +use crate::SupervisorActor; + +/// Represents an actor that processes chain events using the [`ChainProcessor`]. +/// It listens for [`ChainEvent`]s and handles them accordingly. +#[derive(Debug)] +pub struct ChainProcessorActor { + chain_processor: ChainProcessor, + cancel_token: CancellationToken, + event_rx: mpsc::Receiver, +} + +impl ChainProcessorActor +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + DerivationStorage + HeadRefStorageWriter + StorageRewinder + 'static, +{ + /// Creates a new [`ChainProcessorActor`]. + pub const fn new( + chain_processor: ChainProcessor, + cancel_token: CancellationToken, + event_rx: mpsc::Receiver, + ) -> Self { + Self { chain_processor, cancel_token, event_rx } + } +} + +#[async_trait] +impl SupervisorActor for ChainProcessorActor +where + P: BlockProvider + 'static, + V: InteropValidator + 'static, + W: LogStorage + DerivationStorage + HeadRefStorageWriter + StorageRewinder + 'static, +{ + type InboundEvent = ChainEvent; + type Error = ChainProcessorActorError; + + async fn start(mut self) -> Result<(), Self::Error> { + info!( + target: "supervisor::chain_processor_actor", + "Starting ChainProcessorActor" + ); + + loop { + tokio::select! { + maybe_event = self.event_rx.recv() => { + if let Some(event) = maybe_event { + self.chain_processor.handle_event(event).await; + } else { + info!( + target: "supervisor::chain_processor_actor", + "Chain event receiver closed, stopping ChainProcessorActor" + ); + return Err(ChainProcessorActorError::ReceiverClosed); + } + } + _ = self.cancel_token.cancelled() => { + info!( + target: "supervisor::chain_processor_actor", + "ChainProcessorActor cancellation requested, stopping..." + ); + break; + } + } + } + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum ChainProcessorActorError { + /// Error when the chain event receiver is closed. + #[error("Chain event receiver closed")] + ReceiverClosed, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SupervisorActor; + use alloy_eips::BlockNumHash; + use alloy_primitives::{B256, ChainId}; + use kona_interop::{DerivedRefPair, InteropValidationError}; + use kona_protocol::BlockInfo; + use kona_supervisor_core::{ + LogIndexer, + syncnode::{BlockProvider, ManagedNodeCommand, ManagedNodeDataProvider, ManagedNodeError}, + }; + use kona_supervisor_storage::{ + DerivationStorageReader, DerivationStorageWriter, HeadRefStorageWriter, LogStorageReader, + LogStorageWriter, StorageError, StorageRewinder, + }; + use kona_supervisor_types::{Log, OutputV0, Receipts}; + use mockall::{mock, predicate::*}; + use std::sync::Arc; + use tokio::sync::mpsc; + use tokio_util::sync::CancellationToken; + + mock!( + #[derive(Debug)] + pub Node {} + + #[async_trait] + impl BlockProvider for Node { + async fn fetch_receipts(&self, _block_hash: B256) -> Result; + async fn block_by_number(&self, _number: u64) -> Result; + } + + #[async_trait] + impl ManagedNodeDataProvider for Node { + async fn output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn pending_output_v0_at_timestamp( + &self, + _timestamp: u64, + ) -> Result; + + async fn l2_block_ref_by_timestamp( + &self, + _timestamp: u64, + ) -> Result; + } + ); + + mock!( + #[derive(Debug)] + pub Db {} + + impl LogStorageWriter for Db { + fn initialise_log_storage( + &self, + block: BlockInfo, + ) -> Result<(), StorageError>; + + fn store_block_logs( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError>; + } + + impl LogStorageReader for Db { + fn get_block(&self, block_number: u64) -> Result; + fn get_latest_block(&self) -> Result; + fn get_log(&self,block_number: u64,log_index: u32) -> Result; + fn get_logs(&self, block_number: u64) -> Result, StorageError>; + } + + impl DerivationStorageReader for Db { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + fn latest_derived_block_at_source(&self, source_block_id: BlockNumHash) -> Result; + fn latest_derivation_state(&self) -> Result; + fn get_source_block(&self, source_block_number: u64) -> Result; + fn get_activation_block(&self) -> Result; + } + + impl DerivationStorageWriter for Db { + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_derived_block( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + fn save_source_block( + &self, + source: BlockInfo, + ) -> Result<(), StorageError>; + } + + impl HeadRefStorageWriter for Db { + fn update_finalized_using_source( + &self, + block_info: BlockInfo, + ) -> Result; + + fn update_current_cross_unsafe( + &self, + block: &BlockInfo, + ) -> Result<(), StorageError>; + + fn update_current_cross_safe( + &self, + block: &BlockInfo, + ) -> Result; + } + + impl StorageRewinder for Db { + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError>; + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError>; + } + ); + + mock!( + #[derive(Debug)] + pub Validator {} + + impl InteropValidator for Validator { + fn validate_interop_timestamps( + &self, + initiating_chain_id: ChainId, + initiating_timestamp: u64, + executing_chain_id: ChainId, + executing_timestamp: u64, + timeout: Option, + ) -> Result<(), InteropValidationError>; + + fn is_post_interop(&self, chain_id: ChainId, timestamp: u64) -> bool; + + fn is_interop_activation_block(&self, chain_id: ChainId, block: BlockInfo) -> bool; + } + ); + + #[tokio::test] + async fn test_actor_handles_event() { + let mock_node = MockNode::new(); + let mock_db = MockDb::new(); + let validator = MockValidator::new(); + let (mn_sender, mut mn_receiver) = mpsc::channel(1); + + let db = Arc::new(mock_db); + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_node)), db.clone()); + + let processor = + ChainProcessor::new(Arc::new(validator), 1, Arc::new(log_indexer), db, mn_sender); + + let cancel_token = CancellationToken::new(); + let (tx, rx) = mpsc::channel(1); + + let actor = ChainProcessorActor::new(processor, cancel_token.clone(), rx); + + // Send an event + let block = BlockInfo { + number: 1, + hash: B256::from([0; 32]), + timestamp: 1000, + ..Default::default() + }; + tx.send(ChainEvent::CrossUnsafeUpdate { block }).await.unwrap(); + + // Cancel after a short delay to exit the loop + let cancel = cancel_token.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + cancel.cancel(); + }); + + let result = actor.start().await; + assert!(result.is_ok()); + + if let Some(ManagedNodeCommand::UpdateCrossUnsafe { block_id }) = mn_receiver.recv().await { + assert_eq!(block_id, block.id()); + } else { + panic!("Expected UpdateCrossUnsafe command"); + } + } + + #[tokio::test] + async fn test_actor_receiver_closed() { + let mock_node = MockNode::new(); + let mock_db = MockDb::new(); + let validator = MockValidator::new(); + let (mn_sender, _mn_receiver) = mpsc::channel(1); + + let db = Arc::new(mock_db); + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_node)), db.clone()); + + let processor = + ChainProcessor::new(Arc::new(validator), 1, Arc::new(log_indexer), db, mn_sender); + + let cancel_token = CancellationToken::new(); + let (tx, rx) = mpsc::channel::(1); // No sender, so channel is closed + drop(tx); + + let actor = ChainProcessorActor::new(processor, cancel_token, rx); + + let result = actor.start().await; + assert!(matches!(result, Err(ChainProcessorActorError::ReceiverClosed))); + } + + #[tokio::test] + async fn test_actor_cancellation() { + let mock_node = MockNode::new(); + let mock_db = MockDb::new(); + let validator = MockValidator::new(); + let (mn_sender, _mn_receiver) = mpsc::channel(1); + + let db = Arc::new(mock_db); + let log_indexer = LogIndexer::new(1, Some(Arc::new(mock_node)), db.clone()); + + let processor = + ChainProcessor::new(Arc::new(validator), 1, Arc::new(log_indexer), db, mn_sender); + + let cancel_token = CancellationToken::new(); + let (_tx, rx) = mpsc::channel::(1); + + let actor = ChainProcessorActor::new(processor, cancel_token.clone(), rx); + + // Cancel immediately + cancel_token.cancel(); + + let result = actor.start().await; + assert!(result.is_ok()); + } +} diff --git a/kona/crates/supervisor/service/src/actors/rpc.rs b/kona/crates/supervisor/service/src/actors/rpc.rs new file mode 100644 index 0000000000000..59cce1022b17e --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/rpc.rs @@ -0,0 +1,140 @@ +use std::{io, net::SocketAddr}; + +use async_trait::async_trait; +use derive_more::Constructor; +use jsonrpsee::{RpcModule, server::ServerBuilder}; +use thiserror::Error; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; + +use crate::SupervisorActor; + +#[derive(Debug, Constructor)] +pub struct SupervisorRpcActor { + rpc_addr: SocketAddr, + rpc_module: RpcModule, + cancel_token: CancellationToken, +} + +#[async_trait] +impl SupervisorActor for SupervisorRpcActor +where + D: Send + Sync + 'static, +{ + type InboundEvent = (); + type Error = SupervisorRpcActorError; + + async fn start(mut self) -> Result<(), Self::Error> { + info!( + target: "supervisor::rpc_actor", + addr = %self.rpc_addr, + "RPC server bound to address", + ); + + // let supervisor_rpc = SupervisorRpc::new(self.supervisor.clone()); + let server = ServerBuilder::default().build(self.rpc_addr).await?; + // let mut root = supervisor_rpc.into_rpc(); + let handle = server.start(self.rpc_module); + + let stopped = handle.clone().stopped(); + let cancelled = self.cancel_token.cancelled(); + + tokio::select! { + _ = stopped => { + error!(target: "supervisor::rpc_actor", "RPC server stopped unexpectedly"); + return Err(SupervisorRpcActorError::ServerStopped); + } + _ = cancelled => { + match handle.stop() { + Ok(_) => info!(target: "supervisor::rpc_actor", "RPC server stopped gracefully"), + Err(e) => { + error!(target: "supervisor::rpc_actor", %e, "Failed to stop RPC server gracefully"); + return Err(SupervisorRpcActorError::StopFailed); + } + } + info!(target: "supervisor::rpc_actor", "Cancellation requested, stopping RPC server..."); + } + } + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum SupervisorRpcActorError { + /// Failed to build the RPC server. + #[error(transparent)] + BuildFailed(#[from] io::Error), + + /// Indicates that the RPC server failed to start. + #[error("rpc server stopped unexpectedly")] + ServerStopped, + + /// Indicates that the RPC server failed to stop gracefully. + #[error("failed to stop the RPC server")] + StopFailed, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::BlockNumHash; + use alloy_primitives::{B256, ChainId}; + use async_trait::async_trait; + use kona_interop::{DependencySet, ExecutingDescriptor, SafetyLevel}; + use kona_protocol::BlockInfo; + use kona_supervisor_core::{SupervisorError, SupervisorService}; + use kona_supervisor_rpc::{SuperRootOutputRpc, SupervisorApiServer}; + use kona_supervisor_types::SuperHead; + use mockall::mock; + use std::{ + net::{Ipv4Addr, SocketAddr}, + sync::Arc, + }; + use tokio_util::sync::CancellationToken; + + // Mock SupervisorService + mock!( + #[derive(Debug)] + pub SupervisorService {} + + #[async_trait] + impl SupervisorService for SupervisorService { + fn chain_ids(&self) -> impl Iterator; + fn dependency_set(&self) -> &DependencySet; + fn super_head(&self, chain: ChainId) -> Result; + fn latest_block_from(&self, l1_block: BlockNumHash, chain: ChainId) -> Result; + fn derived_to_source_block(&self, chain: ChainId, derived: BlockNumHash) -> Result; + fn local_unsafe(&self, chain: ChainId) -> Result; + fn local_safe(&self, chain: ChainId) -> Result; + fn cross_safe(&self, chain: ChainId) -> Result; + fn finalized(&self, chain: ChainId) -> Result; + fn finalized_l1(&self) -> Result; + fn check_access_list(&self, inbox_entries: Vec, min_safety: SafetyLevel, executing_descriptor: ExecutingDescriptor) -> Result<(), SupervisorError>; + async fn super_root_at_timestamp(&self, timestamp: u64) -> Result; + } + ); + + #[tokio::test] + async fn test_supervisor_rpc_actor_stops_on_cancel() { + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0)); + let supervisor = Arc::new(MockSupervisorService::new()); + let cancel_token = CancellationToken::new(); + + let supervisor_rpc = kona_supervisor_core::rpc::SupervisorRpc::new(supervisor.clone()); + let rpc_module = supervisor_rpc.into_rpc(); + let actor = SupervisorRpcActor::new(addr, rpc_module, cancel_token.clone()); + + let handle = tokio::spawn(actor.start()); + + // Give the server a moment to start + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // Trigger cancellation + cancel_token.cancel(); + + // Await the actor and ensure it stops gracefully + let result = handle.await.unwrap(); + assert!(result.is_ok() || matches!(result, Err(SupervisorRpcActorError::StopFailed))); + } +} diff --git a/kona/crates/supervisor/service/src/actors/traits.rs b/kona/crates/supervisor/service/src/actors/traits.rs new file mode 100644 index 0000000000000..58a0ed48fd7c6 --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/traits.rs @@ -0,0 +1,11 @@ +/// The [`SupervisorActor`] trait is an actor-like service for the supervisor. +use async_trait::async_trait; +#[async_trait] +pub trait SupervisorActor { + /// The event type received by the actor. + type InboundEvent; + /// The error type for the actor. + type Error: std::fmt::Debug; + /// Starts the actor. + async fn start(mut self) -> Result<(), Self::Error>; +} diff --git a/kona/crates/supervisor/service/src/actors/utils.rs b/kona/crates/supervisor/service/src/actors/utils.rs new file mode 100644 index 0000000000000..bba093cdb998d --- /dev/null +++ b/kona/crates/supervisor/service/src/actors/utils.rs @@ -0,0 +1,67 @@ +use std::{future::Future, time::Duration}; +use tokio::{select, task::JoinHandle, time::sleep}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +/// Spawns a background task that retries the given async operation with backoff on failure. +/// +/// - `operation`: The async task to retry (must return `Result<(), E>`) +/// - `cancel_token`: Cancels the retry loop +/// - `max_retries`: Max retries before exiting (use `usize::MAX` for infinite) +pub(super) fn spawn_task_with_retry( + operation: impl Fn() -> Fut + Send + Sync + 'static, + cancel_token: CancellationToken, + max_retries: usize, +) -> JoinHandle<()> +where + Fut: Future> + Send + 'static, + E: std::fmt::Display + Send + 'static, +{ + tokio::spawn(async move { + let mut attempt = 0; + + loop { + if cancel_token.is_cancelled() { + info!(target: "supervisor::retrier", "Retry loop cancelled before starting"); + break; + } + + match operation().await { + Ok(()) => { + info!(target: "supervisor::retrier", "Task exited successfully, restarting"); + attempt = 0; // Reset attempt count on success + } + Err(err) => { + attempt += 1; + + if attempt > max_retries { + error!(target: "supervisor::retrier", %err, "Retry limit ({max_retries}) exceeded"); + break; + } + + let delay = backoff_delay(attempt); + warn!( + target: "supervisor::retrier", + %err, + ?delay, + "Attempt {attempt}/{max_retries} failed, retrying after delay" + ); + + select! { + _ = sleep(delay) => {} + _ = cancel_token.cancelled() => { + warn!(target: "supervisor::retrier", "Retry loop cancelled during backoff"); + break; + } + } + } + } + } + }) +} + +/// Calculates exponential backoff delay with a max cap (30s). +fn backoff_delay(attempt: usize) -> Duration { + let secs = 2u64.saturating_pow(attempt.min(5) as u32); + Duration::from_secs(secs.min(30)) +} diff --git a/kona/crates/supervisor/service/src/lib.rs b/kona/crates/supervisor/service/src/lib.rs new file mode 100644 index 0000000000000..a89f69594e8d7 --- /dev/null +++ b/kona/crates/supervisor/service/src/lib.rs @@ -0,0 +1,9 @@ +//! This crate provides the runnable service layer for the Kona Supervisor. +//! It integrates the core logic with the RPC server. + +mod service; + +pub use service::Service; + +mod actors; +pub use actors::SupervisorActor; diff --git a/kona/crates/supervisor/service/src/service.rs b/kona/crates/supervisor/service/src/service.rs new file mode 100644 index 0000000000000..997b5d8b4fb8c --- /dev/null +++ b/kona/crates/supervisor/service/src/service.rs @@ -0,0 +1,508 @@ +//! Contains the main Supervisor service runner. + +use alloy_primitives::ChainId; +use alloy_provider::{RootProvider, network::Ethereum}; +use alloy_rpc_client::RpcClient; +use anyhow::Result; +use futures::future; +use jsonrpsee::client_transport::ws::Url; +use kona_supervisor_core::{ + ChainProcessor, CrossSafetyCheckerJob, LogIndexer, ReorgHandler, Supervisor, + config::Config, + event::ChainEvent, + l1_watcher::L1Watcher, + rpc::{AdminError, AdminRequest, AdminRpc, SupervisorRpc}, + safety_checker::{CrossSafePromoter, CrossUnsafePromoter}, + syncnode::{Client, ClientConfig, ManagedNode, ManagedNodeClient, ManagedNodeCommand}, +}; +use kona_supervisor_rpc::{SupervisorAdminApiServer, SupervisorApiServer}; +use kona_supervisor_storage::{ChainDb, ChainDbFactory, DerivationStorageWriter, LogStorageWriter}; +use std::{collections::HashMap, sync::Arc}; +use tokio::{sync::mpsc, task::JoinSet, time::Duration}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +use crate::actors::{ + ChainProcessorActor, ManagedNodeActor, MetricWorker, SupervisorActor, SupervisorRpcActor, +}; + +// simplify long type signature +type ManagedLogIndexer = LogIndexer, ChainDb>; + +/// The main service structure for the Kona +/// [`SupervisorService`](`kona_supervisor_core::SupervisorService`). Orchestrates the various +/// components of the supervisor. +#[derive(Debug)] +pub struct Service { + config: Arc, + + supervisor: Arc>>, + database_factory: Arc, + managed_nodes: HashMap>>, + log_indexers: HashMap>, + + // channels + chain_event_senders: HashMap>, + chain_event_receivers: HashMap>, + managed_node_senders: HashMap>, + managed_node_receivers: HashMap>, + admin_receiver: Option>, + + cancel_token: CancellationToken, + join_set: JoinSet>, +} + +impl Service { + /// Creates a new Supervisor service instance. + pub fn new(cfg: Config) -> Self { + let config = Arc::new(cfg); + let database_factory = Arc::new(ChainDbFactory::new(config.datadir.clone()).with_metrics()); + let supervisor = Arc::new(Supervisor::new(config.clone(), database_factory.clone())); + + Self { + config, + + supervisor, + database_factory, + managed_nodes: HashMap::new(), + log_indexers: HashMap::new(), + + chain_event_senders: HashMap::new(), + chain_event_receivers: HashMap::new(), + managed_node_senders: HashMap::new(), + managed_node_receivers: HashMap::new(), + admin_receiver: None, + + cancel_token: CancellationToken::new(), + join_set: JoinSet::new(), + } + } + + /// Initialises the Supervisor service. + pub async fn initialise(&mut self) -> Result<()> { + // create sender and receiver channels for each chain + for chain_id in self.config.rollup_config_set.rollups.keys() { + let (chain_tx, chain_rx) = mpsc::channel::(1000); + self.chain_event_senders.insert(*chain_id, chain_tx); + self.chain_event_receivers.insert(*chain_id, chain_rx); + + let (managed_node_tx, managed_node_rx) = mpsc::channel::(1000); + self.managed_node_senders.insert(*chain_id, managed_node_tx); + self.managed_node_receivers.insert(*chain_id, managed_node_rx); + } + + self.init_database().await?; + self.init_chain_processor().await?; + self.init_managed_nodes().await?; + self.init_l1_watcher()?; + self.init_cross_safety_checker().await?; + + // todo: run metric worker only if metrics are enabled + self.init_rpc_server().await?; + self.init_metric_reporter().await; + Ok(()) + } + + async fn init_database(&self) -> Result<()> { + info!(target: "supervisor::service", "Initialising databases for all chains..."); + + for (chain_id, config) in self.config.rollup_config_set.rollups.iter() { + // Initialise the database for each chain. + let db = self.database_factory.get_or_create_db(*chain_id)?; + let interop_time = config.interop_time; + let derived_pair = config.genesis.get_derived_pair(); + if config.is_interop(derived_pair.derived.timestamp) { + info!(target: "supervisor::service", chain_id, interop_time, %derived_pair, "Initialising database for interop activation block"); + db.initialise_log_storage(derived_pair.derived)?; + db.initialise_derivation_storage(derived_pair)?; + } + info!(target: "supervisor::service", chain_id, "Database initialized successfully"); + } + Ok(()) + } + + async fn init_managed_node(&mut self, config: &ClientConfig) -> Result<()> { + info!(target: "supervisor::service", node = %config.url, "Initialising managed node..."); + let url = Url::parse(&self.config.l1_rpc).map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to parse L1 RPC URL"); + anyhow::anyhow!("failed to parse L1 RPC URL: {err}") + })?; + let provider = RootProvider::::new_http(url); + let client = Arc::new(Client::new(config.clone())); + + let chain_id = client.chain_id().await.map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to get chain ID from client"); + anyhow::anyhow!("failed to get chain ID from client: {err}") + })?; + + let db = self.database_factory.get_db(chain_id)?; + + let chain_event_sender = self + .chain_event_senders + .get(&chain_id) + .ok_or(anyhow::anyhow!("no chain event sender found for chain {chain_id}"))? + .clone(); + + let managed_node = + ManagedNode::::new(client.clone(), db, provider, chain_event_sender); + + if self.managed_nodes.contains_key(&chain_id) { + warn!(target: "supervisor::service", %chain_id, "Managed node for chain already exists, skipping initialization"); + return Ok(()); + } + + let managed_node = Arc::new(managed_node); + // add the managed node to the supervisor service + // also checks if the chain ID is supported + self.supervisor.add_managed_node(chain_id, managed_node.clone()).await?; + + // set the managed node in the log indexer + let log_indexer = self + .log_indexers + .get(&chain_id) + .ok_or(anyhow::anyhow!("no log indexer found for chain {chain_id}"))? + .clone(); + log_indexer.set_block_provider(managed_node.clone()).await; + + self.managed_nodes.insert(chain_id, managed_node.clone()); + info!(target: "supervisor::service", + chain_id, + "Managed node for chain initialized successfully", + ); + + // start managed node actor + let managed_node_receiver = self + .managed_node_receivers + .remove(&chain_id) + .ok_or(anyhow::anyhow!("no managed node receiver found for chain {chain_id}"))?; + + let cancel_token = self.cancel_token.clone(); + self.join_set.spawn(async move { + if let Err(err) = + ManagedNodeActor::new(client, managed_node, managed_node_receiver, cancel_token) + .start() + .await + { + Err(anyhow::anyhow!(err)) + } else { + Ok(()) + } + }); + Ok(()) + } + + async fn init_managed_nodes(&mut self) -> Result<()> { + let configs = self.config.l2_consensus_nodes_config.clone(); + for config in configs.iter() { + self.init_managed_node(config).await?; + } + Ok(()) + } + + async fn init_chain_processor(&mut self) -> Result<()> { + info!(target: "supervisor::service", "Initialising chain processors for all chains..."); + + for (chain_id, _) in self.config.rollup_config_set.rollups.iter() { + let db = self.database_factory.get_db(*chain_id)?; + + let managed_node_sender = self + .managed_node_senders + .get(chain_id) + .ok_or(anyhow::anyhow!("no managed node sender found for chain {chain_id}"))? + .clone(); + + let log_indexer = Arc::new(LogIndexer::new(*chain_id, None, db.clone())); + self.log_indexers.insert(*chain_id, log_indexer.clone()); + + // initialise chain processor for the chain. + let mut processor = ChainProcessor::new( + self.config.clone(), + *chain_id, + log_indexer, + db, + managed_node_sender, + ); + + // todo: enable metrics only if configured + processor = processor.with_metrics(); + + // Start the chain processor actor. + let chain_event_receiver = self + .chain_event_receivers + .remove(chain_id) + .ok_or(anyhow::anyhow!("no chain event receiver found for chain {chain_id}"))?; + + let cancel_token = self.cancel_token.clone(); + self.join_set.spawn(async move { + if let Err(err) = + ChainProcessorActor::new(processor, cancel_token, chain_event_receiver) + .start() + .await + { + Err(anyhow::anyhow!(err)) + } else { + Ok(()) + } + }); + } + Ok(()) + } + + fn init_l1_watcher(&mut self) -> Result<()> { + info!(target: "supervisor::service", "Initialising L1 watcher..."); + + let l1_rpc_url = Url::parse(&self.config.l1_rpc).map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to parse L1 RPC URL"); + anyhow::anyhow!("failed to parse L1 RPC URL: {err}") + })?; + let l1_rpc = RpcClient::new_http(l1_rpc_url); + + let chain_dbs_map: HashMap> = self + .config + .rollup_config_set + .rollups + .keys() + .map(|chain_id| { + self.database_factory.get_db(*chain_id) + .map(|db| (*chain_id, db)) // <-- FIX: remove Arc::new(db) + .map_err(|err| { + error!(target: "supervisor::service", %err, "Failed to get database for chain {chain_id}"); + anyhow::anyhow!("failed to get database for chain {chain_id}: {err}") + }) + }) + .collect::>>>()?; + + let database_factory = self.database_factory.clone(); + let cancel_token = self.cancel_token.clone(); + let event_senders = self.chain_event_senders.clone(); + self.join_set.spawn(async move { + let reorg_handler = ReorgHandler::new(l1_rpc.clone(), chain_dbs_map).with_metrics(); + + // Start the L1 watcher streaming loop. + let l1_watcher = L1Watcher::new( + l1_rpc.clone(), + database_factory, + event_senders, + cancel_token, + reorg_handler, + ); + + l1_watcher.run().await; + Ok(()) + }); + Ok(()) + } + + async fn init_cross_safety_checker(&mut self) -> Result<()> { + info!(target: "supervisor::service", "Initialising cross safety checker..."); + + for (&chain_id, config) in &self.config.rollup_config_set.rollups { + let db = Arc::clone(&self.database_factory); + let cancel = self.cancel_token.clone(); + + let chain_event_sender = self + .chain_event_senders + .get(&chain_id) + .ok_or(anyhow::anyhow!("no chain event sender found for chain {chain_id}"))? + .clone(); + + let cross_safe_job = CrossSafetyCheckerJob::new( + chain_id, + db.clone(), + cancel.clone(), + Duration::from_secs(config.block_time), + CrossSafePromoter, + chain_event_sender.clone(), + self.config.clone(), + ); + + self.join_set.spawn(async move { + cross_safe_job.run().await; + Ok(()) + }); + + let cross_unsafe_job = CrossSafetyCheckerJob::new( + chain_id, + db, + cancel, + Duration::from_secs(config.block_time), + CrossUnsafePromoter, + chain_event_sender, + self.config.clone(), + ); + + self.join_set.spawn(async move { + cross_unsafe_job.run().await; + Ok(()) + }); + } + Ok(()) + } + + async fn init_metric_reporter(&mut self) { + // Initialize the metric reporter actor. + let database_factory = self.database_factory.clone(); + let cancel_token = self.cancel_token.clone(); + self.join_set.spawn(async move { + if let Err(err) = + MetricWorker::new(Duration::from_secs(30), vec![database_factory], cancel_token) + .start() + .await + { + Err(anyhow::anyhow!(err)) + } else { + Ok(()) + } + }); + } + + async fn init_rpc_server(&mut self) -> Result<()> { + let supervisor_rpc = SupervisorRpc::new(self.supervisor.clone()); + + let mut rpc_module = supervisor_rpc.into_rpc(); + + if self.config.enable_admin_api { + info!(target: "supervisor::service", "Enabling Supervisor Admin API"); + + let (admin_tx, admin_rx) = mpsc::channel::(100); + let admin_rpc = AdminRpc::new(admin_tx); + rpc_module + .merge(admin_rpc.into_rpc()) + .map_err(|err| anyhow::anyhow!("failed to merge Admin RPC module: {err}"))?; + self.admin_receiver = Some(admin_rx); + } + + let rpc_addr = self.config.rpc_addr; + let cancel_token = self.cancel_token.clone(); + self.join_set.spawn(async move { + if let Err(err) = + SupervisorRpcActor::new(rpc_addr, rpc_module, cancel_token).start().await + { + Err(anyhow::anyhow!(err)) + } else { + Ok(()) + } + }); + Ok(()) + } + + async fn handle_admin_request(&mut self, req: AdminRequest) { + match req { + AdminRequest::AddL2Rpc { cfg, resp } => { + let result = match self.init_managed_node(&cfg).await { + Ok(()) => Ok(()), + Err(e) => { + tracing::error!(target: "supervisor::service", %e, "admin add_l2_rpc failed"); + Err(AdminError::ServiceError(e.to_string())) + } + }; + + let _ = resp.send(result); + } + } + } + + /// Runs the Supervisor service. + /// This function will typically run indefinitely until interrupted. + pub async fn run(&mut self) -> Result<()> { + self.initialise().await?; + + // todo: refactor this to only run the tasks completion loop + // and handle admin requests elsewhere + loop { + tokio::select! { + // Admin requests (if admin_receiver was initialized) + maybe_req = async { + if let Some(rx) = self.admin_receiver.as_mut() { + rx.recv().await + } else { + // if no receiver present, never produce a value + future::pending::>().await + } + } => { + if let Some(req) = maybe_req { + self.handle_admin_request(req).await; + } + } + + // Supervisor task completions / failures + opt = self.join_set.join_next() => { + match opt { + Some(Ok(Ok(_))) => { + info!(target: "supervisor::service", "Task completed successfully."); + } + Some(Ok(Err(err))) => { + error!(target: "supervisor::service", %err, "A task encountered an error."); + self.cancel_token.cancel(); + return Err(anyhow::anyhow!("A service task failed: {err}")); + } + Some(Err(err)) => { + error!(target: "supervisor::service", %err, "A task encountered an error."); + self.cancel_token.cancel(); + return Err(anyhow::anyhow!("A service task failed: {err}")); + } + None => break, // all tasks finished + } + } + } + } + Ok(()) + } + + pub async fn shutdown(mut self) -> Result<()> { + self.cancel_token.cancel(); // Signal cancellation to all tasks + + // Wait for all tasks to finish. + while let Some(res) = self.join_set.join_next().await { + match res { + Ok(Ok(_)) => { + info!(target: "supervisor::service", "Task completed successfully during shutdown."); + } + Ok(Err(err)) => { + error!(target: "supervisor::service", %err, "A task encountered an error during shutdown."); + } + Err(err) => { + error!(target: "supervisor::service", %err, "A task encountered an error during shutdown."); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{net::SocketAddr, path::PathBuf}; + + use kona_interop::DependencySet; + use kona_supervisor_core::config::RollupConfigSet; + + use super::*; + + fn make_test_config(enable_admin: bool) -> Config { + let mut cfg = Config::new( + "http://localhost:8545".to_string(), + vec![], + PathBuf::from("/tmp/kona-supervisor"), + SocketAddr::from(([127, 0, 0, 1], 8545)), + false, + DependencySet { + dependencies: Default::default(), + override_message_expiry_window: None, + }, + RollupConfigSet { rollups: HashMap::new() }, + ); + cfg.enable_admin_api = enable_admin; + cfg + } + + #[tokio::test] + async fn test_init_rpc_server_enables_admin_receiver_when_flag_set() { + let cfg = Arc::new(make_test_config(true)); + let mut svc = Service::new((*cfg).clone()); + + svc.config = cfg.clone(); + svc.init_rpc_server().await.expect("init_rpc_server failed"); + assert!(svc.admin_receiver.is_some(), "admin_receiver must be set when admin enabled"); + } +} diff --git a/kona/crates/supervisor/storage/Cargo.toml b/kona/crates/supervisor/storage/Cargo.toml new file mode 100644 index 0000000000000..ea995d1042b80 --- /dev/null +++ b/kona/crates/supervisor/storage/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "kona-supervisor-storage" +version = "0.1.0" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +# Workspace +kona-protocol.workspace = true +kona-interop.workspace = true +kona-supervisor-types.workspace = true +kona-supervisor-metrics.workspace = true + +# Alloy +alloy-primitives = { workspace = true, features = ["map", "rlp", "serde", "rand"] } +alloy-eips = { workspace = true } + +# Op-Alloy +op-alloy-consensus.workspace = true + +# Misc +serde = { workspace = true, features = ["derive"] } +derive_more.workspace = true +bytes.workspace = true +modular-bitfield.workspace = true +thiserror.workspace = true +tracing.workspace = true +eyre.workspace = true +metrics.workspace = true + +#reth +reth-db-api = { workspace = true } +reth-db = { workspace = true } +reth-codecs = { workspace = true } + +# HTTP client and TLS for remote signer +tokio = { workspace = true, features = ["full"] } + +[dev-dependencies] +test-fuzz = { workspace = true } +tempfile = { workspace = true } +tokio.workspace = true +kona-cli.workspace = true + +[lints] +workspace = true diff --git a/kona/crates/supervisor/storage/src/chaindb.rs b/kona/crates/supervisor/storage/src/chaindb.rs new file mode 100644 index 0000000000000..376f0fd6ac1b4 --- /dev/null +++ b/kona/crates/supervisor/storage/src/chaindb.rs @@ -0,0 +1,1623 @@ +//! Main database access structure and transaction contexts. + +use crate::{ + Metrics, StorageRewinder, + error::StorageError, + providers::{DerivationProvider, LogProvider, SafetyHeadRefProvider}, + traits::{ + DerivationStorageReader, DerivationStorageWriter, HeadRefStorageReader, + HeadRefStorageWriter, LogStorageReader, LogStorageWriter, + }, +}; +use alloy_eips::eip1898::BlockNumHash; +use alloy_primitives::ChainId; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use kona_supervisor_metrics::{MetricsReporter, observe_metrics_for_result}; +use kona_supervisor_types::{Log, SuperHead}; +use metrics::{Label, gauge}; +use op_alloy_consensus::interop::SafetyLevel; +use reth_db::{ + DatabaseEnv, + mdbx::{DatabaseArguments, init_db_for}, +}; +use reth_db_api::database::Database; +use std::path::Path; +use tracing::warn; + +/// Manages the database environment for a single chain. +/// Provides transactional access to data via providers. +#[derive(Debug)] +pub struct ChainDb { + chain_id: ChainId, + metrics_enabled: Option, + + env: DatabaseEnv, +} + +impl ChainDb { + /// Creates or opens a database environment at the given path. + pub fn new(chain_id: ChainId, path: &Path) -> Result { + let env = init_db_for::<_, crate::models::Tables>(path, DatabaseArguments::default())?; + Ok(Self { chain_id, metrics_enabled: None, env }) + } + + /// Enables metrics on the database environment. + pub fn with_metrics(mut self) -> Self { + self.metrics_enabled = Some(true); + crate::Metrics::init(self.chain_id); + self + } + + fn observe_call Result>( + &self, + name: &'static str, + f: F, + ) -> Result { + if self.metrics_enabled.unwrap_or(false) { + observe_metrics_for_result!( + Metrics::STORAGE_REQUESTS_SUCCESS_TOTAL, + Metrics::STORAGE_REQUESTS_ERROR_TOTAL, + Metrics::STORAGE_REQUEST_DURATION_SECONDS, + name, + f(), + "chain_id" => self.chain_id.to_string() + ) + } else { + f() + } + } +} + +// todo: make sure all get method return DatabaseNotInitialised error if db is not initialised +impl DerivationStorageReader for ChainDb { + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_DERIVED_TO_SOURCE, || { + self.env.view(|tx| { + DerivationProvider::new(tx, self.chain_id).derived_to_source(derived_block_id) + }) + })? + } + + fn latest_derived_block_at_source( + &self, + source_block_id: BlockNumHash, + ) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_LATEST_DERIVED_BLOCK_AT_SOURCE, || { + self.env.view(|tx| { + DerivationProvider::new(tx, self.chain_id) + .latest_derived_block_at_source(source_block_id) + }) + })? + } + + fn latest_derivation_state(&self) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_LATEST_DERIVATION_STATE, || { + self.env.view(|tx| DerivationProvider::new(tx, self.chain_id).latest_derivation_state()) + })? + } + + fn get_source_block(&self, source_block_number: u64) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_SOURCE_BLOCK, || { + self.env.view(|tx| { + DerivationProvider::new(tx, self.chain_id).get_source_block(source_block_number) + }) + })? + } + + fn get_activation_block(&self) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_ACTIVATION_BLOCK, || { + self.env.view(|tx| DerivationProvider::new(tx, self.chain_id).get_activation_block()) + })? + } +} + +impl DerivationStorageWriter for ChainDb { + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_INITIALISE_DERIVATION_STORAGE, || { + self.env.update(|ctx| { + DerivationProvider::new(ctx, self.chain_id).initialise(incoming_pair)?; + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::LocalSafe, &incoming_pair.derived)?; + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::CrossSafe, &incoming_pair.derived) + }) + })? + } + + fn save_derived_block(&self, incoming_pair: DerivedRefPair) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_SAVE_DERIVED_BLOCK, || { + self.env.update(|ctx| { + DerivationProvider::new(ctx, self.chain_id).save_derived_block(incoming_pair)?; + + // Verify the consistency with log storage. + // The check is intentionally deferred until after saving the derived block, + // ensuring validation only triggers on the committed state to prevent false + // positives. + // Example: If the parent derived block doesn't exist, it should return error from + // derivation provider, not from log provider. + let derived_block = incoming_pair.derived; + let block = LogProvider::new(ctx, self.chain_id) + .get_block(derived_block.number) + .map_err(|err| match err { + StorageError::EntryNotFound(_) => { + warn!( + target: "supervisor::storage", + incoming_block = %derived_block, + "Derived block not found in log storage: {derived_block:?}" + ); + StorageError::FutureData + } + other => other, // propagate other errors as-is + })?; + if block != derived_block { + warn!( + target: "supervisor::storage", + incoming_block = %derived_block, + stored_log_block = %block, + "Derived block does not match the stored log block" + ); + return Err(StorageError::ReorgRequired); + } + + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::LocalSafe, &incoming_pair.derived) + }) + })? + } + + fn save_source_block(&self, incoming_source: BlockInfo) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_SAVE_SOURCE_BLOCK, || { + self.env.update(|ctx| { + DerivationProvider::new(ctx, self.chain_id).save_source_block(incoming_source) + }) + })? + } +} + +// todo: make sure all get method return DatabaseNotInitialised error if db is not initialised +impl LogStorageReader for ChainDb { + fn get_latest_block(&self) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_LATEST_BLOCK, || { + self.env.view(|tx| LogProvider::new(tx, self.chain_id).get_latest_block()) + })? + } + + fn get_block(&self, block_number: u64) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_BLOCK, || { + self.env.view(|tx| LogProvider::new(tx, self.chain_id).get_block(block_number)) + })? + } + + fn get_log(&self, block_number: u64, log_index: u32) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_LOG, || { + self.env.view(|tx| LogProvider::new(tx, self.chain_id).get_log(block_number, log_index)) + })? + } + + fn get_logs(&self, block_number: u64) -> Result, StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_GET_LOGS, || { + self.env.view(|tx| LogProvider::new(tx, self.chain_id).get_logs(block_number)) + })? + } +} + +impl LogStorageWriter for ChainDb { + fn initialise_log_storage(&self, block: BlockInfo) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_INITIALISE_LOG_STORAGE, || { + self.env.update(|ctx| { + LogProvider::new(ctx, self.chain_id).initialise(block)?; + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::LocalUnsafe, &block)?; + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::CrossUnsafe, &block) + }) + })? + } + + fn store_block_logs(&self, block: &BlockInfo, logs: Vec) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_STORE_BLOCK_LOGS, || { + self.env.update(|ctx| { + LogProvider::new(ctx, self.chain_id).store_block_logs(block, logs)?; + + SafetyHeadRefProvider::new(ctx, self.chain_id) + .update_safety_head_ref(SafetyLevel::LocalUnsafe, block) + }) + })? + } +} + +impl HeadRefStorageReader for ChainDb { + fn get_safety_head_ref(&self, safety_level: SafetyLevel) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_SAFETY_HEAD_REF, || { + self.env.view(|tx| { + SafetyHeadRefProvider::new(tx, self.chain_id).get_safety_head_ref(safety_level) + }) + })? + } + + /// Fetches all safety heads and current L1 state + fn get_super_head(&self) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_GET_SUPER_HEAD, || { + self.env.view(|tx| { + let sp = SafetyHeadRefProvider::new(tx, self.chain_id); + let local_unsafe = + sp.get_safety_head_ref(SafetyLevel::LocalUnsafe).map_err(|err| { + if matches!(err, StorageError::FutureData) { + StorageError::DatabaseNotInitialised + } else { + err + } + })?; + + let cross_unsafe = match sp.get_safety_head_ref(SafetyLevel::CrossUnsafe) { + Ok(block) => Some(block), + Err(StorageError::FutureData) => None, + Err(err) => return Err(err), + }; + + let local_safe = match sp.get_safety_head_ref(SafetyLevel::LocalSafe) { + Ok(block) => Some(block), + Err(StorageError::FutureData) => None, + Err(err) => return Err(err), + }; + + let cross_safe = match sp.get_safety_head_ref(SafetyLevel::CrossSafe) { + Ok(block) => Some(block), + Err(StorageError::FutureData) => None, + Err(err) => return Err(err), + }; + + let finalized = match sp.get_safety_head_ref(SafetyLevel::Finalized) { + Ok(block) => Some(block), + Err(StorageError::FutureData) => None, + Err(err) => return Err(err), + }; + + let l1_source = + match DerivationProvider::new(tx, self.chain_id).latest_derivation_state() { + Ok(pair) => Some(pair.source), + Err(StorageError::DatabaseNotInitialised) => None, + Err(err) => return Err(err), + }; + + Ok(SuperHead { + l1_source, + local_unsafe, + cross_unsafe, + local_safe, + cross_safe, + finalized, + }) + })? + }) + } +} + +impl HeadRefStorageWriter for ChainDb { + fn update_finalized_using_source( + &self, + finalized_source_block: BlockInfo, + ) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_UPDATE_FINALIZED_USING_SOURCE, || { + self.env.update(|tx| { + let sp = SafetyHeadRefProvider::new(tx, self.chain_id); + let safe = sp.get_safety_head_ref(SafetyLevel::CrossSafe)?; + + let dp = DerivationProvider::new(tx, self.chain_id); + let safe_block_pair = dp.get_derived_block_pair(safe.id())?; + + if finalized_source_block.number >= safe_block_pair.source.number { + // this could happen during initial sync + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + l1_finalized_block_number = finalized_source_block.number, + safe_source_block_number = safe_block_pair.source.number, + "L1 finalized block is greater than safe block", + ); + sp.update_safety_head_ref(SafetyLevel::Finalized, &safe)?; + return Ok(safe); + } + + let latest_derived = + dp.latest_derived_block_at_source(finalized_source_block.id())?; + sp.update_safety_head_ref(SafetyLevel::Finalized, &latest_derived)?; + Ok(latest_derived) + }) + })? + } + + fn update_current_cross_unsafe(&self, block: &BlockInfo) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_UPDATE_CURRENT_CROSS_UNSAFE, || { + self.env.update(|tx| { + let lp = LogProvider::new(tx, self.chain_id); + let sp = SafetyHeadRefProvider::new(tx, self.chain_id); + + // Check parent-child relationship with current CrossUnsafe head, if it exists. + let parent = sp.get_safety_head_ref(SafetyLevel::CrossUnsafe)?; + if !parent.is_parent_of(block) { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_block = %block, + latest_block = %parent, + "Incoming block is not the child of the current cross-unsafe head", + ); + return Err(StorageError::ConflictError); + } + + // Ensure the block exists in log storage and hasn't been pruned due to a re-org. + let stored_block = lp.get_block(block.number)?; + if stored_block.hash != block.hash { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_block_hash = %block.hash, + stored_block_hash = %stored_block.hash, + "Hash mismatch while updating CrossUnsafe head", + ); + return Err(StorageError::ConflictError); + } + + sp.update_safety_head_ref(SafetyLevel::CrossUnsafe, block)?; + Ok(()) + })? + }) + } + + fn update_current_cross_safe(&self, block: &BlockInfo) -> Result { + self.observe_call(Metrics::STORAGE_METHOD_UPDATE_CURRENT_CROSS_SAFE, || { + self.env.update(|tx| { + let dp = DerivationProvider::new(tx, self.chain_id); + let sp = SafetyHeadRefProvider::new(tx, self.chain_id); + + // Check parent-child relationship with current CrossUnsafe head, if it exists. + let parent = sp.get_safety_head_ref(SafetyLevel::CrossSafe)?; + if !parent.is_parent_of(block) { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_block = %block, + latest_block = %parent, + "Incoming block is not the child of the current cross-safe head", + ); + return Err(StorageError::ConflictError); + } + + // Ensure the block exists in derivation storage and hasn't been pruned due to a + // re-org. + let derived_pair = dp.get_derived_block_pair(block.id())?; + sp.update_safety_head_ref(SafetyLevel::CrossSafe, block)?; + + Ok(derived_pair.into()) + })? + }) + } +} + +impl StorageRewinder for ChainDb { + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_REWIND_LOG_STORAGE, || { + self.env.update(|tx| { + let lp = LogProvider::new(tx, self.chain_id); + let hp = SafetyHeadRefProvider::new(tx, self.chain_id); + + // Ensure we don't rewind to or before the LocalSafe head. + match hp.get_safety_head_ref(SafetyLevel::LocalSafe) { + Ok(local_safe) => { + // If the target block is less than or equal to the local safe head, + // we cannot rewind to it, as this would mean losing logs for the safe + // blocks. The check is inclusive since the rewind + // operation removes the target block as well. + if to.number <= local_safe.number { + return Err(StorageError::RewindBeyondLocalSafeHead { + to: to.number, + local_safe: local_safe.number, + }); + } + } + Err(StorageError::FutureData) => { + // If LocalSafe is not set, we can rewind to any point. + } + Err(err) => return Err(err), + } + + lp.rewind_to(to)?; + + // get the current latest block to update the safety head refs + match lp.get_latest_block() { + Ok(latest_block) => { + hp.reset_safety_head_ref_if_ahead(SafetyLevel::LocalUnsafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::CrossUnsafe, &latest_block)?; + } + Err(StorageError::DatabaseNotInitialised) => { + // If the database returns DatabaseNotInitialised, it means we have rewound + // past the activation block + hp.remove_safety_head_ref(SafetyLevel::LocalUnsafe)?; + hp.remove_safety_head_ref(SafetyLevel::CrossUnsafe)?; + } + Err(err) => return Err(err), + }; + Ok(()) + })? + }) + } + + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_REWIND, || { + self.env.update(|tx| { + let lp = LogProvider::new(tx, self.chain_id); + let dp = DerivationProvider::new(tx, self.chain_id); + let hp = SafetyHeadRefProvider::new(tx, self.chain_id); + + lp.rewind_to(to)?; + dp.rewind_to(to)?; + + // get the current latest block to update the safety head refs + match lp.get_latest_block() { + Ok(latest_block) => { + hp.reset_safety_head_ref_if_ahead(SafetyLevel::LocalUnsafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::CrossUnsafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::LocalSafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::CrossSafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::Finalized, &latest_block)?; + } + Err(StorageError::DatabaseNotInitialised) => { + // If the database returns DatabaseNotInitialised, it means we have rewound + // past the activation block + hp.remove_safety_head_ref(SafetyLevel::LocalUnsafe)?; + hp.remove_safety_head_ref(SafetyLevel::CrossUnsafe)?; + hp.remove_safety_head_ref(SafetyLevel::LocalSafe)?; + hp.remove_safety_head_ref(SafetyLevel::CrossSafe)?; + hp.remove_safety_head_ref(SafetyLevel::Finalized)?; + } + Err(err) => return Err(err), + } + Ok(()) + })? + }) + } + + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError> { + self.observe_call(Metrics::STORAGE_METHOD_REWIND_TO_SOURCE, || { + self.env.update(|tx| { + let lp = LogProvider::new(tx, self.chain_id); + let dp = DerivationProvider::new(tx, self.chain_id); + let hp = SafetyHeadRefProvider::new(tx, self.chain_id); + + let derived_target_block = dp.rewind_to_source(to)?; + if let Some(rewind_target) = derived_target_block { + lp.rewind_to(&rewind_target.id())?; + } + + // get the current latest block to update the safety head refs + match lp.get_latest_block() { + Ok(latest_block) => { + hp.reset_safety_head_ref_if_ahead(SafetyLevel::LocalUnsafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::CrossUnsafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::LocalSafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::CrossSafe, &latest_block)?; + hp.reset_safety_head_ref_if_ahead(SafetyLevel::Finalized, &latest_block)?; + } + Err(StorageError::DatabaseNotInitialised) => { + // If the database returns DatabaseNotInitialised, it means we have rewound + // past the activation block + hp.remove_safety_head_ref(SafetyLevel::LocalUnsafe)?; + hp.remove_safety_head_ref(SafetyLevel::CrossUnsafe)?; + hp.remove_safety_head_ref(SafetyLevel::LocalSafe)?; + hp.remove_safety_head_ref(SafetyLevel::CrossSafe)?; + hp.remove_safety_head_ref(SafetyLevel::Finalized)?; + } + Err(err) => return Err(err), + } + Ok(derived_target_block) + })? + }) + } +} + +impl MetricsReporter for ChainDb { + fn report_metrics(&self) { + let mut metrics = Vec::new(); + + let _ = self + .env + .view(|tx| { + for table in crate::models::Tables::ALL.iter().map(crate::models::Tables::name) { + let table_db = tx.inner.open_db(Some(table))?; + + let stats = tx.inner.db_stat(&table_db)?; + + let page_size = stats.page_size() as usize; + let leaf_pages = stats.leaf_pages(); + let branch_pages = stats.branch_pages(); + let overflow_pages = stats.overflow_pages(); + let num_pages = leaf_pages + branch_pages + overflow_pages; + let table_size = page_size * num_pages; + let entries = stats.entries(); + + metrics.push(( + "kona_supervisor_storage.table_size", + table_size as f64, + vec![ + Label::new("table", table), + Label::new("chain_id", self.chain_id.to_string()), + ], + )); + metrics.push(( + "kona_supervisor_storage.table_pages", + leaf_pages as f64, + vec![ + Label::new("table", table), + Label::new("type", "leaf"), + Label::new("chain_id", self.chain_id.to_string()), + ], + )); + metrics.push(( + "kona_supervisor_storage.table_pages", + branch_pages as f64, + vec![ + Label::new("table", table), + Label::new("type", "branch"), + Label::new("chain_id", self.chain_id.to_string()), + ], + )); + metrics.push(( + "kona_supervisor_storage.table_pages", + overflow_pages as f64, + vec![ + Label::new("table", table), + Label::new("type", "overflow"), + Label::new("chain_id", self.chain_id.to_string()), + ], + )); + metrics.push(( + "kona_supervisor_storage.table_entries", + entries as f64, + vec![ + Label::new("table", table), + Label::new("chain_id", self.chain_id.to_string()), + ], + )); + } + + Ok::<(), eyre::Report>(()) + }) + .inspect_err(|err| { + warn!(target: "supervisor::storage", %err, "Failed to collect database metrics"); + }); + + for (name, value, labels) in metrics { + gauge!(name, labels).set(value); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use kona_supervisor_types::Log; + use tempfile::TempDir; + + #[test] + fn test_create_and_open_db() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb"); + let db = ChainDb::new(1, &db_path); + assert!(db.is_ok(), "Should create or open database"); + } + + #[test] + fn test_log_storage() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_logs"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + let block = BlockInfo { + hash: B256::from([4u8; 32]), + number: 1, + parent_hash: anchor.derived.hash, + timestamp: 0, + }; + let log1 = Log { index: 0, hash: B256::from([0u8; 32]), executing_message: None }; + let log2 = Log { index: 1, hash: B256::from([1u8; 32]), executing_message: None }; + let logs = vec![log1, log2]; + + // Store logs + db.store_block_logs(&block, logs.clone()).expect("store logs"); + + // Retrieve logs + let retrieved_logs = db.get_logs(block.number).expect("get logs"); + assert_eq!(retrieved_logs.len(), 2); + assert_eq!(retrieved_logs, logs, "First log should match stored log"); + + let latest_block = db.get_latest_block().expect("latest block"); + assert_eq!(latest_block, block, "Latest block should match stored block"); + + let log = db.get_log(block.number, 1).expect("get block by log"); + assert_eq!(log, logs[1], "Block by log should match stored block"); + } + + #[test] + fn test_super_head_empty() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_super_head_empty"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + // Get super head when no blocks are stored + let err = db.get_super_head().unwrap_err(); + assert!(matches!(err, StorageError::DatabaseNotInitialised)); + } + + #[test] + fn test_get_super_head_populated() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb"); + let db = ChainDb::new(1, &db_path).unwrap(); + + // Prepare blocks + let block = BlockInfo { number: 1, ..Default::default() }; + let derived_pair = DerivedRefPair { source: block, derived: block }; + + // Initialise all heads + db.initialise_log_storage(block).unwrap(); + db.initialise_derivation_storage(derived_pair).unwrap(); + + let _ = db + .env + .update(|ctx| { + let sp = SafetyHeadRefProvider::new(ctx, 1); + sp.update_safety_head_ref(SafetyLevel::Finalized, &block) + }) + .unwrap(); + + // Should not error and all heads should be Some + let super_head = db.get_super_head().unwrap(); + assert_eq!(super_head.local_unsafe, block); + assert!(super_head.cross_unsafe.is_some()); + assert!(super_head.local_safe.is_some()); + assert!(super_head.cross_safe.is_some()); + assert!(super_head.finalized.is_some()); + assert!(super_head.l1_source.is_some()); + } + + #[test] + fn test_get_super_head_with_some_missing_heads() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb"); + let db = ChainDb::new(1, &db_path).unwrap(); + + // Only initialise log storage (not derivation storage) + let block = BlockInfo { number: 1, ..Default::default() }; + db.initialise_log_storage(block).unwrap(); + + let super_head = db.get_super_head().unwrap(); + assert_eq!(super_head.local_unsafe, block); + // These will be None because derivation storage was not initialised + assert!(super_head.local_safe.is_none()); + assert!(super_head.cross_safe.is_none()); + assert!(super_head.finalized.is_none()); + assert!(super_head.l1_source.is_none()); + } + + #[test] + fn test_latest_derivation_state_empty() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_latest_derivation_empty"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + // Get latest derivation state when no blocks are stored + let err = db.latest_derivation_state().unwrap_err(); + assert!(matches!(err, StorageError::DatabaseNotInitialised)); + } + + #[test] + fn test_get_latest_block_empty() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_latest_block_empty"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + // Get latest block when no blocks are stored + let err = db.get_latest_block().unwrap_err(); + assert!(matches!(err, StorageError::DatabaseNotInitialised)); + } + + #[test] + fn test_derivation_storage() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_derivation"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + // Create dummy derived block pair + let derived_pair = DerivedRefPair { + source: BlockInfo { + hash: B256::from([4u8; 32]), + number: 101, + parent_hash: anchor.source.hash, + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([6u8; 32]), + number: 1, + parent_hash: anchor.derived.hash, + timestamp: 0, + }, + }; + + // Initialise the database with the anchor derived block pair + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + // Save derived block pair - should error BlockOutOfOrder error + let err = db.save_derived_block(derived_pair).unwrap_err(); + assert!(matches!(err, StorageError::BlockOutOfOrder)); + + db.store_block_logs( + &BlockInfo { + hash: B256::from([6u8; 32]), + number: 1, + parent_hash: anchor.derived.hash, + timestamp: 0, + }, + vec![], + ) + .expect("storing logs failed"); + + // Save derived block pair + db.save_source_block(derived_pair.source).expect("save source block"); + db.save_derived_block(derived_pair).expect("save derived pair"); + + // Retrieve latest derived block pair + let latest_pair = db.latest_derivation_state().expect("get latest derived pair"); + assert_eq!(latest_pair, derived_pair, "Latest derived pair should match saved pair"); + + // Retrieve derived to source mapping + let derived_block_id = + BlockNumHash::new(derived_pair.derived.number, derived_pair.derived.hash); + let source_block = db.derived_to_source(derived_block_id).expect("get derived to source"); + assert_eq!( + source_block, derived_pair.source, + "Source block should match derived pair source" + ); + + // Retrieve latest derived block at source + let source_block_id = + BlockNumHash::new(derived_pair.source.number, derived_pair.source.hash); + let latest_derived = db + .latest_derived_block_at_source(source_block_id) + .expect("get latest derived at source"); + assert_eq!( + latest_derived, derived_pair.derived, + "Latest derived block at source should match derived pair derived" + ); + } + + #[test] + fn test_update_current_cross_unsafe() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb"); + let db = ChainDb::new(1, &db_path).unwrap(); + + let source = BlockInfo { number: 1, ..Default::default() }; + let block1 = BlockInfo { + number: 10, + hash: B256::random(), + parent_hash: B256::random(), + timestamp: 1, + }; + let mut block2 = BlockInfo { + number: 11, + hash: B256::random(), + parent_hash: B256::random(), + timestamp: 1, + }; + + db.initialise_log_storage(block1).expect("initialise log storage"); + db.initialise_derivation_storage(DerivedRefPair { source, derived: block1 }) + .expect("initialise derivation storage"); + + // should error as block2 must be child of block1 + let err = db.update_current_cross_unsafe(&block2).expect_err("should return an error"); + assert!(matches!(err, StorageError::ConflictError)); + + // make block2 as child of block1 + block2.parent_hash = block1.hash; + + // block2 doesn't exist in log storage - should return not found error + let err = db.update_current_cross_unsafe(&block2).expect_err("should return an error"); + assert!(matches!(err, StorageError::EntryNotFound(_))); + + db.store_block_logs(&block2, vec![]).unwrap(); + db.update_current_cross_unsafe(&block2).unwrap(); + + let cross_unsafe_block = db.get_safety_head_ref(SafetyLevel::CrossUnsafe).unwrap(); + assert_eq!(cross_unsafe_block, block2); + } + + #[test] + fn test_update_current_cross_safe() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb"); + let db = ChainDb::new(1, &db_path).unwrap(); + + let source = BlockInfo { number: 1, ..Default::default() }; + let block1 = BlockInfo { + number: 10, + hash: B256::random(), + parent_hash: B256::random(), + timestamp: 1, + }; + let mut block2 = BlockInfo { + number: 11, + hash: B256::random(), + parent_hash: B256::random(), + timestamp: 1, + }; + + db.initialise_log_storage(block1).expect("initialise log storage"); + db.initialise_derivation_storage(DerivedRefPair { source, derived: block1 }) + .expect("initialise derivation storage"); + + // should error as block2 must be child of block1 + let err = db.update_current_cross_safe(&block2).expect_err("should return an error"); + assert!(matches!(err, StorageError::ConflictError)); + + // make block2 as child of block1 + block2.parent_hash = block1.hash; + + // block2 doesn't exist in derivation storage - should return not found error + let err = db.update_current_cross_safe(&block2).expect_err("should return an error"); + assert!(matches!(err, StorageError::EntryNotFound(_))); + + db.store_block_logs(&block2, vec![]).unwrap(); + db.save_derived_block(DerivedRefPair { source, derived: block2 }).unwrap(); + + let ref_pair = db.update_current_cross_safe(&block2).unwrap(); + assert_eq!(ref_pair.source, source); + assert_eq!(ref_pair.derived, block2); + + let cross_safe_block = db.get_safety_head_ref(SafetyLevel::CrossSafe).unwrap(); + assert_eq!(cross_safe_block, block2); + } + + #[test] + fn test_source_block_storage() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_source_block"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let source1 = BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 1234, + }; + let source2 = BlockInfo { + hash: B256::from([2u8; 32]), + number: 101, + parent_hash: source1.hash, + timestamp: 5678, + }; + let derived1 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 1, + parent_hash: source1.hash, + timestamp: 9101, + }; + + db.initialise_log_storage(derived1).expect("initialise log storage"); + db.initialise_derivation_storage(DerivedRefPair { source: source1, derived: derived1 }) + .expect("initialise derivation storage"); + + assert!(db.save_source_block(source2).is_ok()); + + // Retrieve latest source block + let latest = db.latest_derivation_state().expect("get latest source block"); + assert_eq!(latest.source, source2); + } + + #[test] + fn test_all_safe_derived() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_source_block"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 1234, + }, + derived: BlockInfo { + hash: B256::from([1u8; 32]), + number: 1, + parent_hash: B256::from([2u8; 32]), + timestamp: 1234, + }, + }; + + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + let source1 = BlockInfo { + hash: B256::from([2u8; 32]), + number: 101, + parent_hash: anchor.source.hash, + timestamp: 1234, + }; + let source2 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 102, + parent_hash: source1.hash, + timestamp: 1234, + }; + let derived1 = BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: anchor.derived.hash, + timestamp: 1234, + }; + let derived2 = BlockInfo { + hash: B256::from([5u8; 32]), + number: 3, + parent_hash: derived1.hash, + timestamp: 1234, + }; + let derived3 = BlockInfo { + hash: B256::from([7u8; 32]), + number: 4, + parent_hash: derived2.hash, + timestamp: 1234, + }; + + assert!(db.save_source_block(source1).is_ok()); + db.store_block_logs(&derived1, vec![]).expect("storing logs failed"); + db.store_block_logs(&derived2, vec![]).expect("storing logs failed"); + db.store_block_logs(&derived3, vec![]).expect("storing logs failed"); + + assert!( + db.save_derived_block(DerivedRefPair { source: source1, derived: derived1 }).is_ok() + ); + + assert!(db.save_source_block(source2).is_ok()); + assert!( + db.save_derived_block(DerivedRefPair { source: source2, derived: derived2 }).is_ok() + ); + assert!( + db.save_derived_block(DerivedRefPair { source: source2, derived: derived3 }).is_ok() + ); + + let safe_derived = db.latest_derived_block_at_source(source1.id()).expect("should exist"); + assert_eq!(safe_derived, derived1); + + let safe_derived = db.latest_derived_block_at_source(source2.id()).expect("should exist"); + assert_eq!(safe_derived, derived3); + + let source = db.derived_to_source(derived2.id()).expect("should exist"); + assert_eq!(source, source2); + + let source = db.derived_to_source(derived3.id()).expect("should exist"); + assert_eq!(source, source2); + + let latest_derived_pair = db.latest_derivation_state().expect("should exist"); + assert_eq!(latest_derived_pair, DerivedRefPair { source: source2, derived: derived3 }); + } + + #[test] + fn test_rewind_log_storage() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_rewind_log"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let anchor = BlockInfo { + hash: B256::from([2u8; 32]), + number: 1, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }; + + let next_block = BlockInfo { + hash: B256::from([3u8; 32]), + number: 2, + parent_hash: anchor.hash, + timestamp: 0, + }; + + db.initialise_log_storage(anchor).unwrap(); + db.store_block_logs(&next_block, vec![]).unwrap(); + + // Add and promote next_block to CrossUnsafe and LocalUnsafe + db.update_current_cross_unsafe(&next_block).unwrap(); + + db.rewind_log_storage(&next_block.id()).expect("rewind log storage should succeed"); + + // Should be rewound to anchor + let local_unsafe = + db.get_safety_head_ref(SafetyLevel::LocalUnsafe).expect("get safety head ref"); + let cross_unsafe = + db.get_safety_head_ref(SafetyLevel::CrossUnsafe).expect("get safety head ref"); + + assert_eq!(local_unsafe, anchor); + assert_eq!(cross_unsafe, anchor); + } + + #[test] + fn test_rewind_log_storage_beyond_derivation_head_should_error() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb_rewind_beyond_derivation"); + let db = ChainDb::new(1, &db_path).unwrap(); + + // Initialise anchor derived block and derivation storage + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + db.initialise_log_storage(anchor.derived).unwrap(); + db.initialise_derivation_storage(anchor).unwrap(); + + let block1 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 1, + parent_hash: anchor.derived.hash, + timestamp: 0, + }; + let source1 = BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }; + + let result = db.store_block_logs(&block1, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + let result = db.save_source_block(source1); + assert!(result.is_ok(), "Should save source block successfully"); + let result = db.save_derived_block(DerivedRefPair { source: source1, derived: block1 }); + assert!(result.is_ok(), "Should save derived block successfully"); + + let block2 = BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: block1.hash, + timestamp: 0, + }; + + let result = db.store_block_logs(&block2, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + + // Attempt to rewind log storage beyond local safe head + let err = db.rewind_log_storage(&anchor.derived.id()).unwrap_err(); + assert!( + matches!(err, StorageError::RewindBeyondLocalSafeHead { to, local_safe } if to == anchor.derived.number && local_safe == block1.number), + "Should not allow rewinding log storage beyond derivation head" + ); + + // Attempt to rewind log storage to the local safe head + let result = db.rewind_log_storage(&block1.id()).unwrap_err(); + assert!( + matches!(result, StorageError::RewindBeyondLocalSafeHead { to, local_safe } if to == block1.number && local_safe == block1.number), + "Should not allow rewinding log storage to the local safe head" + ); + } + + #[test] + fn test_rewind_log_comprehensive() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb_rewind_beyond_derivation"); + let db = ChainDb::new(1, &db_path).unwrap(); + + // Initialise anchor derived block and derivation storage + let block0 = BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::ZERO, + timestamp: 0, + }; + + let result = db.initialise_log_storage(block0); + assert!(result.is_ok(), "Should initialise log storage successfully"); + + let block1 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 1, + parent_hash: block0.hash, + timestamp: 0, + }; + + let result = db.store_block_logs(&block1, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + + let block2 = BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: block1.hash, + timestamp: 0, + }; + + let result = db.store_block_logs(&block2, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + + db.update_current_cross_unsafe(&block1).expect("update cross unsafe"); + + let result = db.rewind_log_storage(&block2.id()); + assert!(result.is_ok(), "Should rewind log storage successfully"); + + let local_unsafe = + db.get_safety_head_ref(SafetyLevel::LocalUnsafe).expect("get safety head ref"); + let cross_unsafe = + db.get_safety_head_ref(SafetyLevel::CrossUnsafe).expect("get safety head ref"); + + assert_eq!(local_unsafe, block1); + assert_eq!(cross_unsafe, block1); + + let result = db.rewind_log_storage(&block1.id()); + assert!(result.is_ok(), "Should rewind log storage successfully"); + + let local_unsafe = + db.get_safety_head_ref(SafetyLevel::LocalUnsafe).expect("get safety head ref"); + let cross_unsafe = + db.get_safety_head_ref(SafetyLevel::CrossUnsafe).expect("get safety head ref"); + + assert_eq!(local_unsafe, block0); + assert_eq!(cross_unsafe, block0); + } + + #[test] + fn test_rewind_log_storage_to_activation_block() { + let tmp_dir = tempfile::TempDir::new().unwrap(); + let db_path = tmp_dir.path().join("chaindb_rewind_beyond_derivation"); + let db = ChainDb::new(1, &db_path).unwrap(); + + // Initialise anchor derived block and derivation storage + let block0 = BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::ZERO, + timestamp: 0, + }; + + let result = db.initialise_log_storage(block0); + assert!(result.is_ok(), "Should initialise log storage successfully"); + + let block1 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 1, + parent_hash: block0.hash, + timestamp: 0, + }; + + let result = db.store_block_logs(&block1, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + + let block2 = BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: block1.hash, + timestamp: 0, + }; + + let result = db.store_block_logs(&block2, Vec::new()); + assert!(result.is_ok(), "Should store block logs successfully"); + + let result = db.rewind_log_storage(&block0.id()); + assert!(result.is_ok(), "Should rewind log storage successfully"); + } + + #[test] + fn test_rewind_comprehensive() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_rewind_all"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 1, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + let pair1 = DerivedRefPair { + source: BlockInfo { + hash: B256::from([3u8; 32]), + number: 101, + parent_hash: anchor.source.hash, + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: anchor.derived.hash, + timestamp: 1, + }, + }; + + let pair2 = DerivedRefPair { + source: BlockInfo { + hash: B256::from([4u8; 32]), + number: 102, + parent_hash: pair1.source.hash, + timestamp: 1, + }, + derived: BlockInfo { + hash: B256::from([5u8; 32]), + number: 3, + parent_hash: pair1.derived.hash, + timestamp: 2, + }, + }; + + let unsafe_block = BlockInfo { + hash: B256::from([5u8; 32]), + number: 3, + parent_hash: pair1.derived.hash, + timestamp: 2, + }; + + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + db.store_block_logs(&pair1.derived, vec![]).expect("store logs"); + db.store_block_logs(&unsafe_block, vec![]).expect("store logs"); + + db.save_source_block(pair1.source).expect("save source block"); + db.save_derived_block(pair1).expect("save derived block"); + + db.save_source_block(pair2.source).expect("save source block"); + db.save_derived_block(pair2).expect("save derived block"); + + db.update_current_cross_unsafe(&pair1.derived).expect("update cross unsafe"); + db.update_current_cross_safe(&pair1.derived).expect("update cross safe"); + + db.update_current_cross_unsafe(&pair2.derived).expect("update cross unsafe"); + db.update_current_cross_safe(&pair2.derived).expect("update cross safe"); + + db.update_finalized_using_source(anchor.source).expect("update finalized using source"); + + db.rewind(&pair2.derived.id()).expect("rewind should succeed"); + + // Everything should be rewound to pair1.derived + let local_unsafe = db.get_safety_head_ref(SafetyLevel::LocalUnsafe).unwrap(); + let cross_unsafe = db.get_safety_head_ref(SafetyLevel::CrossUnsafe).unwrap(); + let local_safe = db.get_safety_head_ref(SafetyLevel::LocalSafe).unwrap(); + let cross_safe = db.get_safety_head_ref(SafetyLevel::CrossSafe).unwrap(); + let latest_pair = db.latest_derivation_state().unwrap(); + let log_block = db.get_latest_block().unwrap(); + let finalized = db.get_safety_head_ref(SafetyLevel::Finalized).unwrap(); + + assert_eq!(local_unsafe, pair1.derived); + assert_eq!(cross_unsafe, pair1.derived); + assert_eq!(local_safe, pair1.derived); + assert_eq!(cross_safe, pair1.derived); + assert_eq!(latest_pair, pair1); + assert_eq!(log_block, pair1.derived); + assert_eq!(finalized, anchor.derived); + + db.update_finalized_using_source(pair1.source).expect("update finalized using source"); + db.rewind(&pair1.derived.id()).expect("rewind should succeed"); + + // Everything should be rewound to anchor.derived + let local_unsafe = db.get_safety_head_ref(SafetyLevel::LocalUnsafe).unwrap(); + let cross_unsafe = db.get_safety_head_ref(SafetyLevel::CrossUnsafe).unwrap(); + let local_safe = db.get_safety_head_ref(SafetyLevel::LocalSafe).unwrap(); + let cross_safe = db.get_safety_head_ref(SafetyLevel::CrossSafe).unwrap(); + let latest_pair = db.latest_derivation_state().unwrap(); + let log_block = db.get_latest_block().unwrap(); + let finalized = db.get_safety_head_ref(SafetyLevel::Finalized).unwrap(); + + assert_eq!(local_unsafe, anchor.derived); + assert_eq!(cross_unsafe, anchor.derived); + assert_eq!(local_safe, anchor.derived); + assert_eq!(cross_safe, anchor.derived); + assert_eq!(latest_pair, anchor); + assert_eq!(log_block, anchor.derived); + assert_eq!(finalized, anchor.derived); + } + + #[test] + fn test_rewind_to_activation_block() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_rewind_all"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + let pair0 = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 1, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + let pair1 = DerivedRefPair { + source: BlockInfo { + hash: B256::from([3u8; 32]), + number: 101, + parent_hash: pair0.source.hash, + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([4u8; 32]), + number: 2, + parent_hash: pair0.derived.hash, + timestamp: 1, + }, + }; + + let unsafe_block = BlockInfo { + hash: B256::from([5u8; 32]), + number: 3, + parent_hash: pair1.derived.hash, + timestamp: 2, + }; + + db.initialise_log_storage(pair0.derived).expect("initialise log storage"); + db.initialise_derivation_storage(pair0).expect("initialise derivation storage"); + + db.store_block_logs(&pair1.derived, vec![]).expect("store logs"); + db.store_block_logs(&unsafe_block, vec![]).expect("store logs"); + + db.save_source_block(pair1.source).expect("save source block"); + db.save_derived_block(pair1).expect("save derived block"); + + db.update_current_cross_unsafe(&pair1.derived).expect("update cross unsafe"); + + db.rewind(&pair0.derived.id()).expect("rewind should succeed"); + + // Everything should return error + let local_unsafe = db.get_safety_head_ref(SafetyLevel::LocalUnsafe); + assert!(matches!(local_unsafe, Err(StorageError::FutureData))); + + let cross_unsafe = db.get_safety_head_ref(SafetyLevel::CrossUnsafe); + assert!(matches!(cross_unsafe, Err(StorageError::FutureData))); + + let local_safe = db.get_safety_head_ref(SafetyLevel::LocalSafe); + assert!(matches!(local_safe, Err(StorageError::FutureData))); + + let cross_safe = db.get_safety_head_ref(SafetyLevel::CrossSafe); + assert!(matches!(cross_safe, Err(StorageError::FutureData))); + + let latest_derivation_state = db.latest_derivation_state(); + assert!(matches!(latest_derivation_state, Err(StorageError::DatabaseNotInitialised))); + + let latest_log_block = db.get_latest_block(); + assert!(matches!(latest_log_block, Err(StorageError::DatabaseNotInitialised))); + } + + #[test] + fn test_rewind_to_source_updates_logs_and_heads() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_rewind_to_source"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + // Anchor (activation) + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + // Initialise DB with anchor + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + // Build two source entries and several derived blocks + let source1 = BlockInfo { + hash: B256::from([3u8; 32]), + number: 101, + parent_hash: anchor.source.hash, + timestamp: 0, + }; + let source2 = BlockInfo { + hash: B256::from([4u8; 32]), + number: 102, + parent_hash: source1.hash, + timestamp: 0, + }; + + // Derived blocks chained off the anchor/previous derived blocks + let derived1 = BlockInfo { + hash: B256::from([10u8; 32]), + number: 1, + parent_hash: anchor.derived.hash, + timestamp: 0, + }; + let derived2 = BlockInfo { + hash: B256::from([11u8; 32]), + number: 2, + parent_hash: derived1.hash, + timestamp: 0, + }; + let derived3 = BlockInfo { + hash: B256::from([12u8; 32]), + number: 3, + parent_hash: derived2.hash, + timestamp: 0, + }; + let derived4 = BlockInfo { + hash: B256::from([13u8; 32]), + number: 4, + parent_hash: derived3.hash, + timestamp: 0, + }; + let derived5 = BlockInfo { + hash: B256::from([14u8; 32]), + number: 5, + parent_hash: derived4.hash, + timestamp: 0, + }; + + // Insert sources and derived blocks into storage (logs + derivation) + assert!(db.save_source_block(source1).is_ok()); + db.store_block_logs(&derived1, vec![]).expect("store logs derived1"); + db.save_derived_block(DerivedRefPair { source: source1, derived: derived1 }) + .expect("save derived1"); + + db.store_block_logs(&derived2, vec![]).expect("store logs derived2"); + db.save_derived_block(DerivedRefPair { source: source1, derived: derived2 }) + .expect("save derived2"); + + db.store_block_logs(&derived3, vec![]).expect("store logs derived3"); + db.save_derived_block(DerivedRefPair { source: source1, derived: derived3 }) + .expect("save derived3"); + + assert!(db.save_source_block(source2).is_ok()); + db.store_block_logs(&derived4, vec![]).expect("store logs derived4"); + db.save_derived_block(DerivedRefPair { source: source2, derived: derived4 }) + .expect("save derived4"); + + db.store_block_logs(&derived5, vec![]).expect("store logs derived5"); + db.save_derived_block(DerivedRefPair { source: source2, derived: derived5 }) + .expect("save derived5"); + + // Advance safety heads to be ahead of anchor so that rewind will need to reset them. + db.update_current_cross_unsafe(&derived1).expect("update cross unsafe"); + db.update_current_cross_unsafe(&derived2).expect("update cross unsafe"); + db.update_current_cross_unsafe(&derived3).expect("update cross unsafe"); + db.update_current_cross_unsafe(&derived4).expect("update cross unsafe"); + + db.update_current_cross_safe(&derived1).expect("update cross safe"); + db.update_current_cross_safe(&derived2).expect("update cross safe"); + + // Now rewind to source1: expected derived rewind target is derived1 (first derived for + // source1) + let res = db.rewind_to_source(&source1.id()).expect("rewind_to_source should succeed"); + assert!(res.is_some(), "expected a derived rewind target"); + let rewind_target = res.unwrap(); + assert_eq!(rewind_target, derived1); + + // After rewind, logs should be rewound to before derived1 -> latest block == anchor.derived + let latest_log = db.get_latest_block().expect("latest block after rewind"); + assert_eq!(latest_log, anchor.derived); + + // All safety heads that were ahead should be reset to the new latest (anchor.derived) + let local_unsafe = db.get_safety_head_ref(SafetyLevel::LocalUnsafe).expect("local unsafe"); + let cross_unsafe = db.get_safety_head_ref(SafetyLevel::CrossUnsafe).expect("cross unsafe"); + let local_safe = db.get_safety_head_ref(SafetyLevel::LocalSafe).expect("local safe"); + let cross_safe = db.get_safety_head_ref(SafetyLevel::CrossSafe).expect("cross safe"); + + assert_eq!(local_unsafe, anchor.derived); + assert_eq!(cross_unsafe, anchor.derived); + assert_eq!(local_safe, anchor.derived); + assert_eq!(cross_safe, anchor.derived); + } + + #[test] + fn test_rewind_to_source_with_empty_source_returns_none() { + let tmp_dir = TempDir::new().expect("create temp dir"); + let db_path = tmp_dir.path().join("chaindb_rewind_to_source_empty"); + let db = ChainDb::new(1, &db_path).expect("create db"); + + // Anchor (activation) + let anchor = DerivedRefPair { + source: BlockInfo { + hash: B256::from([0u8; 32]), + number: 100, + parent_hash: B256::from([1u8; 32]), + timestamp: 0, + }, + derived: BlockInfo { + hash: B256::from([2u8; 32]), + number: 0, + parent_hash: B256::from([3u8; 32]), + timestamp: 0, + }, + }; + + // Initialise DB with anchor + db.initialise_log_storage(anchor.derived).expect("initialise log storage"); + db.initialise_derivation_storage(anchor).expect("initialise derivation storage"); + + // Insert a source block that has no derived entries + let source = BlockInfo { + hash: B256::from([3u8; 32]), + number: 101, + parent_hash: anchor.source.hash, + timestamp: 0, + }; + db.save_source_block(source).expect("save source block"); + + // Rewind to the source with empty derived list -> should return None + let res = db.rewind_to_source(&source.id()).expect("rewind_to_source should succeed"); + assert!(res.is_none(), "Expected None when source has no derived blocks"); + + // Ensure latest log and derivation state remain at the anchor + let latest_log = db.get_latest_block().expect("latest block after noop rewind"); + assert_eq!(latest_log, anchor.derived); + + let latest_pair = db.latest_derivation_state().expect("latest derivation state"); + assert_eq!(latest_pair, anchor); + } +} diff --git a/kona/crates/supervisor/storage/src/chaindb_factory.rs b/kona/crates/supervisor/storage/src/chaindb_factory.rs new file mode 100644 index 0000000000000..db46f02484806 --- /dev/null +++ b/kona/crates/supervisor/storage/src/chaindb_factory.rs @@ -0,0 +1,320 @@ +use std::{ + collections::HashMap, + path::PathBuf, + sync::{Arc, RwLock}, +}; + +use crate::{ + CrossChainSafetyProvider, FinalizedL1Storage, HeadRefStorageReader, HeadRefStorageWriter, + LogStorageReader, Metrics, chaindb::ChainDb, error::StorageError, +}; +use alloy_primitives::ChainId; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use kona_supervisor_metrics::{MetricsReporter, observe_metrics_for_result}; +use kona_supervisor_types::Log; +use op_alloy_consensus::interop::SafetyLevel; +use tracing::error; + +/// Factory for managing multiple chain databases. +/// This struct allows for the creation and retrieval of `ChainDb` instances +/// based on chain IDs, ensuring that each chain has its own database instance. +#[derive(Debug)] +pub struct ChainDbFactory { + db_path: PathBuf, + metrics_enabled: Option, + + dbs: RwLock>>, + /// Finalized L1 block reference, used for tracking the finalized L1 block. + /// In-memory only, not persisted. + finalized_l1: RwLock>, +} + +impl ChainDbFactory { + /// Create a new, empty factory. + pub fn new(db_path: PathBuf) -> Self { + Self { + db_path, + metrics_enabled: None, + dbs: RwLock::new(HashMap::new()), + finalized_l1: RwLock::new(None), + } + } + + /// Enables metrics on the database environment. + pub const fn with_metrics(mut self) -> Self { + self.metrics_enabled = Some(true); + self + } + + fn observe_call Result>( + &self, + name: &'static str, + f: F, + ) -> Result { + if self.metrics_enabled.unwrap_or(false) { + observe_metrics_for_result!( + Metrics::STORAGE_REQUESTS_SUCCESS_TOTAL, + Metrics::STORAGE_REQUESTS_ERROR_TOTAL, + Metrics::STORAGE_REQUEST_DURATION_SECONDS, + name, + f() + ) + } else { + f() + } + } + + /// Get or create a [`ChainDb`] for the given chain id. + /// + /// If the database does not exist, it will be created at the path `self.db_path/`. + pub fn get_or_create_db(&self, chain_id: ChainId) -> Result, StorageError> { + { + // Try to get it without locking for write + let dbs = self.dbs.read().map_err(|err| { + error!(target: "supervisor::storage", %err, "Failed to acquire read lock on databases"); + StorageError::LockPoisoned + })?; + if let Some(db) = dbs.get(&chain_id) { + return Ok(db.clone()); + } + } + + // Not found, create and insert + let mut dbs = self.dbs.write().map_err(|err| { + error!(target: "supervisor::storage", %err, "Failed to acquire write lock on databases"); + StorageError::LockPoisoned + })?; + // Double-check in case another thread inserted + if let Some(db) = dbs.get(&chain_id) { + return Ok(db.clone()); + } + + let chain_db_path = self.db_path.join(chain_id.to_string()); + let mut chain_db = ChainDb::new(chain_id, chain_db_path.as_path())?; + if self.metrics_enabled.unwrap_or(false) { + chain_db = chain_db.with_metrics(); + } + let db = Arc::new(chain_db); + dbs.insert(chain_id, db.clone()); + Ok(db) + } + + /// Get a [`ChainDb`] for the given chain id, returning an error if it doesn't exist. + /// + /// # Returns + /// * `Ok(Arc)` if the database exists. + /// * `Err(StorageError)` if the database does not exist. + pub fn get_db(&self, chain_id: ChainId) -> Result, StorageError> { + let dbs = self.dbs.read().map_err(|_| StorageError::LockPoisoned)?; + dbs.get(&chain_id).cloned().ok_or_else(|| StorageError::DatabaseNotInitialised) + } +} + +impl MetricsReporter for ChainDbFactory { + fn report_metrics(&self) { + let metrics_enabled = self.metrics_enabled.unwrap_or(false); + if metrics_enabled { + let dbs: Vec> = { + match self.dbs.read() { + Ok(dbs_guard) => dbs_guard.values().cloned().collect(), + Err(_) => { + error!(target: "supervisor::storage", "Failed to acquire read lock for metrics reporting"); + return; + } + } + }; + for db in dbs { + db.report_metrics(); + } + } + } +} + +impl FinalizedL1Storage for ChainDbFactory { + fn get_finalized_l1(&self) -> Result { + self.observe_call( + Metrics::STORAGE_METHOD_GET_FINALIZED_L1, + || { + let guard = self.finalized_l1.read().map_err(|err| { + error!(target: "supervisor::storage", %err, "Failed to acquire read lock on finalized_l1"); + StorageError::LockPoisoned + })?; + guard.as_ref().cloned().ok_or(StorageError::FutureData) + } + ) + } + + fn update_finalized_l1(&self, block: BlockInfo) -> Result<(), StorageError> { + self.observe_call( + Metrics::STORAGE_METHOD_UPDATE_FINALIZED_L1, + || { + let mut guard = self + .finalized_l1 + .write() + .map_err(|err| { + error!(target: "supervisor::storage", %err, "Failed to acquire write lock on finalized_l1"); + StorageError::LockPoisoned + })?; + + // Check if the new block number is greater than the current finalized block + if let Some(ref current) = *guard { + if block.number <= current.number { + error!(target: "supervisor::storage", + current_block_number = current.number, + new_block_number = block.number, + "New finalized block number is not greater than current finalized block number", + ); + return Err(StorageError::BlockOutOfOrder); + } + } + *guard = Some(block); + Ok(()) + } + ) + } +} + +impl CrossChainSafetyProvider for ChainDbFactory { + fn get_block(&self, chain_id: ChainId, block_number: u64) -> Result { + self.get_db(chain_id)?.get_block(block_number) + } + + fn get_log( + &self, + chain_id: ChainId, + block_number: u64, + log_index: u32, + ) -> Result { + self.get_db(chain_id)?.get_log(block_number, log_index) + } + + fn get_block_logs( + &self, + chain_id: ChainId, + block_number: u64, + ) -> Result, StorageError> { + self.get_db(chain_id)?.get_logs(block_number) + } + + fn get_safety_head_ref( + &self, + chain_id: ChainId, + level: SafetyLevel, + ) -> Result { + self.get_db(chain_id)?.get_safety_head_ref(level) + } + + fn update_current_cross_unsafe( + &self, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result<(), StorageError> { + self.get_db(chain_id)?.update_current_cross_unsafe(block) + } + + fn update_current_cross_safe( + &self, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result { + self.get_db(chain_id)?.update_current_cross_safe(block) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn temp_factory() -> (TempDir, ChainDbFactory) { + let tmp = TempDir::new().expect("create temp dir"); + let factory = ChainDbFactory::new(tmp.path().to_path_buf()); + (tmp, factory) + } + + #[test] + fn test_get_or_create_db_creates_and_returns_db() { + let (_tmp, factory) = temp_factory(); + let db = factory.get_or_create_db(1).expect("should create db"); + assert!(Arc::strong_count(&db) >= 1); + } + + #[test] + fn test_get_or_create_db_returns_same_instance() { + let (_tmp, factory) = temp_factory(); + let db1 = factory.get_or_create_db(42).unwrap(); + let db2 = factory.get_or_create_db(42).unwrap(); + assert!(Arc::ptr_eq(&db1, &db2)); + } + + #[test] + fn test_get_db_returns_error_if_not_exists() { + let (_tmp, factory) = temp_factory(); + let err = factory.get_db(999).unwrap_err(); + assert!(matches!(err, StorageError::DatabaseNotInitialised)); + } + + #[test] + fn test_get_db_returns_existing_db() { + let (_tmp, factory) = temp_factory(); + let db = factory.get_or_create_db(7).unwrap(); + let db2 = factory.get_db(7).unwrap(); + assert!(Arc::ptr_eq(&db, &db2)); + } + + #[test] + fn test_db_path_is_unique_per_chain() { + let (tmp, factory) = temp_factory(); + let db1 = factory.get_or_create_db(1).unwrap(); + let db2 = factory.get_or_create_db(2).unwrap(); + assert!(!Arc::ptr_eq(&db1, &db2)); + + assert!(tmp.path().join("1").exists()); + assert!(tmp.path().join("2").exists()); + } + + #[test] + fn test_get_finalized_l1_returns_error_when_none() { + let (_tmp, factory) = temp_factory(); + let err = factory.get_finalized_l1().unwrap_err(); + assert!(matches!(err, StorageError::FutureData)); + } + + #[test] + fn test_update_and_get_finalized_l1_success() { + let (_tmp, factory) = temp_factory(); + let block1 = BlockInfo { number: 100, ..Default::default() }; + let block2 = BlockInfo { number: 200, ..Default::default() }; + + // Set first finalized block + factory.update_finalized_l1(block1).unwrap(); + assert_eq!(factory.get_finalized_l1().unwrap(), block1); + + // Update with higher block number + factory.update_finalized_l1(block2).unwrap(); + assert_eq!(factory.get_finalized_l1().unwrap(), block2); + } + + #[test] + fn test_update_finalized_l1_with_lower_block_number_errors() { + let (_tmp, factory) = temp_factory(); + let block1 = BlockInfo { number: 100, ..Default::default() }; + let block2 = BlockInfo { number: 50, ..Default::default() }; + + factory.update_finalized_l1(block1).unwrap(); + let err = factory.update_finalized_l1(block2).unwrap_err(); + assert!(matches!(err, StorageError::BlockOutOfOrder)); + } + + #[test] + fn test_update_finalized_l1_with_same_block_number_errors() { + let (_tmp, factory) = temp_factory(); + let block1 = BlockInfo { number: 100, ..Default::default() }; + let block2 = BlockInfo { number: 100, ..Default::default() }; + + factory.update_finalized_l1(block1).unwrap(); + let err = factory.update_finalized_l1(block2).unwrap_err(); + assert!(matches!(err, StorageError::BlockOutOfOrder)); + } +} diff --git a/kona/crates/supervisor/storage/src/error.rs b/kona/crates/supervisor/storage/src/error.rs new file mode 100644 index 0000000000000..f7b0f2b75f0ed --- /dev/null +++ b/kona/crates/supervisor/storage/src/error.rs @@ -0,0 +1,97 @@ +use alloy_eips::BlockNumHash; +use reth_db::DatabaseError; +use thiserror::Error; + +/// Errors that may occur while interacting with supervisor log storage. +/// +/// This enum is used across all implementations of the Storage traits. +#[derive(Debug, Error)] +pub enum StorageError { + /// Represents a database error that occurred while interacting with storage. + #[error(transparent)] + Database(#[from] DatabaseError), + + /// Represents an error that occurred while initializing the database. + #[error(transparent)] + DatabaseInit(#[from] eyre::Report), + + /// Represents an error that occurred while writing to the database. + #[error("lock poisoned")] + LockPoisoned, + + /// The expected entry was not found in the database. + #[error(transparent)] + EntryNotFound(#[from] EntryNotFoundError), + + /// Represents an error that occurred while getting data that is not yet available. + #[error("data not yet available")] + FutureData, + + /// Represents an error that occurred when database is not initialized. + #[error("database not initialized")] + DatabaseNotInitialised, + + /// Represents a conflict occurred while attempting to write to the database. + #[error("conflicting data")] + ConflictError, + + /// Represents an error that occurred while writing to log database. + #[error("latest stored block is not parent of the incoming block")] + BlockOutOfOrder, + + /// Represents an error that occurred when there is inconsistency in log storage + #[error("reorg required due to inconsistent storage state")] + ReorgRequired, + + /// Represents an error that occurred when attempting to rewind log storage beyond the local + /// safe head. + #[error("rewinding log storage beyond local safe head. to: {to}, local_safe: {local_safe}")] + RewindBeyondLocalSafeHead { + /// The target block number to rewind to. + to: u64, + /// The local safe head block number. + local_safe: u64, + }, +} + +impl PartialEq for StorageError { + fn eq(&self, other: &Self) -> bool { + use StorageError::*; + match (self, other) { + (Database(a), Database(b)) => a == b, + (DatabaseInit(a), DatabaseInit(b)) => format!("{a}") == format!("{b}"), + (EntryNotFound(a), EntryNotFound(b)) => a == b, + (DatabaseNotInitialised, DatabaseNotInitialised) | (ConflictError, ConflictError) => { + true + } + _ => false, + } + } +} + +impl Eq for StorageError {} + +/// Entry not found error. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum EntryNotFoundError { + /// No derived blocks found for given source block. + #[error("no derived blocks for source block, number: {}, hash: {}", .0.number, .0.hash)] + MissingDerivedBlocks(BlockNumHash), + + /// Expected source block not found. + #[error("source block not found, number: {0}")] + SourceBlockNotFound(u64), + + /// Expected derived block not found. + #[error("derived block not found, number: {0}")] + DerivedBlockNotFound(u64), + + /// Expected log not found. + #[error("log not found at block {block_number} index {log_index}")] + LogNotFound { + /// Block number. + block_number: u64, + /// Log index within the block. + log_index: u32, + }, +} diff --git a/kona/crates/supervisor/storage/src/lib.rs b/kona/crates/supervisor/storage/src/lib.rs new file mode 100644 index 0000000000000..f7022421f65f4 --- /dev/null +++ b/kona/crates/supervisor/storage/src/lib.rs @@ -0,0 +1,44 @@ +//! Persistent storage for the Supervisor. +//! +//! This crate provides structured, append-only storage for the Supervisor, +//! exposing high-level APIs to write and query logs, block metadata, and +//! other execution states. +//! +//! The storage system is built on top of [`reth-db`], using MDBX, +//! and defines schemas for supervisor-specific data like: +//! - L2 log entries +//! - Block ancestry metadata +//! - Source and Derived Blocks +//! - Chain heads for safety levels: **SAFE**, **UNSAFE**, and **CROSS-SAFE** +//! +//! +//! ## Capabilities +//! +//! - Append logs emitted by L2 execution +//! - Look up logs by block number and index +//! - Rewind logs during reorgs +//! - Track sealed blocks and ancestry metadata + +pub mod models; +pub use models::SourceBlockTraversal; + +mod error; +pub use error::{EntryNotFoundError, StorageError}; + +mod providers; + +mod chaindb; +pub use chaindb::ChainDb; + +mod metrics; +pub(crate) use metrics::Metrics; + +mod chaindb_factory; +pub use chaindb_factory::ChainDbFactory; + +mod traits; +pub use traits::{ + CrossChainSafetyProvider, DbReader, DerivationStorage, DerivationStorageReader, + DerivationStorageWriter, FinalizedL1Storage, HeadRefStorage, HeadRefStorageReader, + HeadRefStorageWriter, LogStorage, LogStorageReader, LogStorageWriter, StorageRewinder, +}; diff --git a/kona/crates/supervisor/storage/src/metrics.rs b/kona/crates/supervisor/storage/src/metrics.rs new file mode 100644 index 0000000000000..5aa8b0b9b124f --- /dev/null +++ b/kona/crates/supervisor/storage/src/metrics.rs @@ -0,0 +1,118 @@ +use alloy_primitives::ChainId; + +/// Container for ChainDb metrics. +#[derive(Debug, Clone)] +pub(crate) struct Metrics; + +// todo: implement this using the reth metrics for tables +impl Metrics { + pub(crate) const STORAGE_REQUESTS_SUCCESS_TOTAL: &'static str = + "kona_supervisor_storage_success_total"; + pub(crate) const STORAGE_REQUESTS_ERROR_TOTAL: &'static str = + "kona_supervisor_storage_error_total"; + pub(crate) const STORAGE_REQUEST_DURATION_SECONDS: &'static str = + "kona_supervisor_storage_duration_seconds"; + + pub(crate) const STORAGE_METHOD_DERIVED_TO_SOURCE: &'static str = "derived_to_source"; + pub(crate) const STORAGE_METHOD_LATEST_DERIVED_BLOCK_AT_SOURCE: &'static str = + "latest_derived_block_at_source"; + pub(crate) const STORAGE_METHOD_LATEST_DERIVATION_STATE: &'static str = + "latest_derivation_state"; + pub(crate) const STORAGE_METHOD_GET_SOURCE_BLOCK: &'static str = "get_source_block"; + pub(crate) const STORAGE_METHOD_GET_ACTIVATION_BLOCK: &'static str = "get_activation_block"; + pub(crate) const STORAGE_METHOD_INITIALISE_DERIVATION_STORAGE: &'static str = + "initialise_derivation_storage"; + pub(crate) const STORAGE_METHOD_SAVE_DERIVED_BLOCK: &'static str = "save_derived_block"; + pub(crate) const STORAGE_METHOD_SAVE_SOURCE_BLOCK: &'static str = "save_source_block"; + pub(crate) const STORAGE_METHOD_GET_LATEST_BLOCK: &'static str = "get_latest_block"; + pub(crate) const STORAGE_METHOD_GET_BLOCK: &'static str = "get_block"; + pub(crate) const STORAGE_METHOD_GET_LOG: &'static str = "get_log"; + pub(crate) const STORAGE_METHOD_GET_LOGS: &'static str = "get_logs"; + pub(crate) const STORAGE_METHOD_INITIALISE_LOG_STORAGE: &'static str = "initialise_log_storage"; + pub(crate) const STORAGE_METHOD_STORE_BLOCK_LOGS: &'static str = "store_block_logs"; + pub(crate) const STORAGE_METHOD_GET_SAFETY_HEAD_REF: &'static str = "get_safety_head_ref"; + pub(crate) const STORAGE_METHOD_GET_SUPER_HEAD: &'static str = "get_super_head"; + pub(crate) const STORAGE_METHOD_UPDATE_FINALIZED_USING_SOURCE: &'static str = + "update_finalized_using_source"; + pub(crate) const STORAGE_METHOD_UPDATE_CURRENT_CROSS_UNSAFE: &'static str = + "update_current_cross_unsafe"; + pub(crate) const STORAGE_METHOD_UPDATE_CURRENT_CROSS_SAFE: &'static str = + "update_current_cross_safe"; + pub(crate) const STORAGE_METHOD_UPDATE_FINALIZED_L1: &'static str = "update_finalized_l1"; + pub(crate) const STORAGE_METHOD_GET_FINALIZED_L1: &'static str = "get_finalized_l1"; + pub(crate) const STORAGE_METHOD_REWIND_LOG_STORAGE: &'static str = "rewind_log_storage"; + pub(crate) const STORAGE_METHOD_REWIND: &'static str = "rewind"; + pub(crate) const STORAGE_METHOD_REWIND_TO_SOURCE: &'static str = "rewind_to_source"; + + pub(crate) fn init(chain_id: ChainId) { + Self::describe(); + Self::zero(chain_id); + } + + fn describe() { + metrics::describe_counter!( + Self::STORAGE_REQUESTS_SUCCESS_TOTAL, + metrics::Unit::Count, + "Total number of successful Kona Supervisor Storage requests" + ); + metrics::describe_counter!( + Self::STORAGE_REQUESTS_ERROR_TOTAL, + metrics::Unit::Count, + "Total number of failed Kona Supervisor Storage requests" + ); + metrics::describe_histogram!( + Self::STORAGE_REQUEST_DURATION_SECONDS, + metrics::Unit::Seconds, + "Duration of Kona Supervisor Storage requests" + ); + } + + fn zero_storage_methods(chain_id: ChainId, method_name: &'static str) { + metrics::counter!( + Self::STORAGE_REQUESTS_SUCCESS_TOTAL, + "method" => method_name, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::counter!( + Self::STORAGE_REQUESTS_ERROR_TOTAL, + "method" => method_name, + "chain_id" => chain_id.to_string() + ) + .increment(0); + + metrics::histogram!( + Self::STORAGE_REQUEST_DURATION_SECONDS, + "method" => method_name, + "chain_id" => chain_id.to_string() + ) + .record(0.0); + } + + fn zero(chain_id: ChainId) { + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_DERIVED_TO_SOURCE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_LATEST_DERIVED_BLOCK_AT_SOURCE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_LATEST_DERIVATION_STATE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_SOURCE_BLOCK); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_INITIALISE_DERIVATION_STORAGE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_SAVE_DERIVED_BLOCK); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_SAVE_SOURCE_BLOCK); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_LATEST_BLOCK); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_BLOCK); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_LOG); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_LOGS); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_INITIALISE_LOG_STORAGE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_STORE_BLOCK_LOGS); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_SAFETY_HEAD_REF); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_SUPER_HEAD); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_UPDATE_FINALIZED_USING_SOURCE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_UPDATE_CURRENT_CROSS_UNSAFE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_UPDATE_CURRENT_CROSS_SAFE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_UPDATE_FINALIZED_L1); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_GET_FINALIZED_L1); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_REWIND_LOG_STORAGE); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_REWIND); + Self::zero_storage_methods(chain_id, Self::STORAGE_METHOD_REWIND_TO_SOURCE); + } +} diff --git a/kona/crates/supervisor/storage/src/models/block.rs b/kona/crates/supervisor/storage/src/models/block.rs new file mode 100644 index 0000000000000..a477b95ffd182 --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/block.rs @@ -0,0 +1,127 @@ +//! Models for storing block metadata in the database. +//! +//! This module defines the data structure and schema used for tracking +//! individual blocks by block number. The stored metadata includes block hash, +//! parent hash, and block timestamp. +//! +//! Unlike logs, each block is uniquely identified by its number and does not +//! require dup-sorting. + +use alloy_primitives::B256; +use derive_more::Display; +use kona_protocol::BlockInfo; +use reth_codecs::Compact; +use serde::{Deserialize, Serialize}; + +/// Metadata reference for a single block. +/// +/// This struct captures essential block information required to track canonical +/// block lineage and verify ancestry. It is stored as the value +/// in the [`crate::models::BlockRefs`] table. +#[derive(Debug, Clone, Display, PartialEq, Eq, Default, Serialize, Deserialize, Compact)] +#[display("number: {number}, hash: {hash}, parent_hash: {parent_hash}, timestamp: {timestamp}")] +pub struct BlockRef { + /// The height of the block. + pub number: u64, + /// The hash of the block itself. + pub hash: B256, + /// The hash of the parent block (previous block in the chain). + pub parent_hash: B256, + /// The timestamp of the block (seconds since Unix epoch). + pub timestamp: u64, +} + +/// Converts from [`BlockInfo`] (external API format) to [`BlockRef`] (storage +/// format). +/// +/// Performs a direct field mapping. +impl From for BlockRef { + fn from(block: BlockInfo) -> Self { + Self { + number: block.number, + hash: block.hash, + parent_hash: block.parent_hash, + timestamp: block.timestamp, + } + } +} + +/// Converts from [`BlockRef`] (storage format) to [`BlockInfo`] (external API +/// format). +/// +/// This enables decoding values stored in a compact format for use in application logic. +impl From for BlockInfo { + fn from(block: BlockRef) -> Self { + Self { + number: block.number, + hash: block.hash, + parent_hash: block.parent_hash, + timestamp: block.timestamp, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + + fn test_b256(val: u8) -> B256 { + let mut val_bytes = [0u8; 32]; + val_bytes[0] = val; + let b256_from_val = B256::from(val_bytes); + B256::random() ^ b256_from_val + } + + #[test] + fn test_block_ref_compact_roundtrip() { + let original_ref = BlockRef { + number: 42, + hash: test_b256(10), + parent_hash: test_b256(11), + timestamp: 1678886400, + }; + + let mut buffer = Vec::new(); + let bytes_written = original_ref.to_compact(&mut buffer); + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + + let (deserialized_ref, remaining_buf) = BlockRef::from_compact(&buffer, bytes_written); + assert_eq!(original_ref, deserialized_ref, "Original and deserialized ref should be equal"); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after deserialization"); + } + + #[test] + fn test_from_block_info_to_block_ref() { + let block_info = BlockInfo { + number: 123, + hash: test_b256(1), + parent_hash: test_b256(2), + timestamp: 1600000000, + }; + + let block_ref: BlockRef = block_info.into(); + + assert_eq!(block_ref.number, block_info.number, "Number should match"); + assert_eq!(block_ref.hash, block_info.hash, "Hash should match"); + assert_eq!(block_ref.parent_hash, block_info.parent_hash, "Parent hash should match"); + assert_eq!(block_ref.timestamp, block_info.timestamp, "Time (timestamp) should match"); + } + + #[test] + fn test_from_block_ref_to_block_info() { + let block_ref = BlockRef { + number: 456, + hash: test_b256(3), + parent_hash: test_b256(4), + timestamp: 1700000000, + }; + + let block_info: BlockInfo = block_ref.clone().into(); + + assert_eq!(block_info.number, block_ref.number, "Number should match"); + assert_eq!(block_info.hash, block_ref.hash, "Hash should match"); + assert_eq!(block_info.parent_hash, block_ref.parent_hash, "Parent hash should match"); + assert_eq!(block_info.timestamp, block_ref.timestamp, "Timestamp (time) should match"); + } +} diff --git a/kona/crates/supervisor/storage/src/models/common.rs b/kona/crates/supervisor/storage/src/models/common.rs new file mode 100644 index 0000000000000..968dac51f6db5 --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/common.rs @@ -0,0 +1,87 @@ +//! Common model types used across various storage tables. + +use derive_more::{Deref, DerefMut}; +use reth_codecs::Compact; +use serde::{Deserialize, Serialize}; + +/// Wrapper for `Vec` to represent a list of numbers. +// todo: add support for Vec<64> in table +#[derive( + Deref, DerefMut, Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Compact, +)] +pub struct U64List(pub Vec); + +#[cfg(test)] +mod tests { + use super::*; + use reth_codecs::Compact; + + #[test] + fn test_u64list_compact_empty() { + let original_list = U64List(Vec::new()); + + let mut buffer = Vec::new(); + let bytes_written = original_list.to_compact(&mut buffer); + + assert_eq!( + bytes_written, + buffer.len(), + "Bytes written should match buffer length for empty list" + ); + let (deserialized_list, remaining_buf) = U64List::from_compact(&buffer, bytes_written); + + assert_eq!( + original_list, deserialized_list, + "Original and deserialized empty lists should be equal" + ); + assert!( + remaining_buf.is_empty(), + "Remaining buffer should be empty after deserialization of empty list" + ); + } + + #[test] + fn test_u64list_compact_with_data() { + let original_list = U64List(vec![10, 20, 30, 40, 50]); + + let mut buffer = Vec::new(); + let bytes_written = original_list.to_compact(&mut buffer); + + assert_eq!( + bytes_written, + buffer.len(), + "Bytes written should match buffer length for list with data" + ); + let (deserialized_list, remaining_buf) = U64List::from_compact(&buffer, bytes_written); + + assert_eq!( + original_list, deserialized_list, + "Original and deserialized lists with data should be equal" + ); + assert!( + remaining_buf.is_empty(), + "Remaining buffer should be empty after deserialization of list with data" + ); + } + + #[test] + fn test_u64list_deref() { + let list = U64List(vec![1, 2, 3]); + assert_eq!(list.len(), 3); + assert_eq!(list[0], 1); + assert!(!list.is_empty()); + } + + #[test] + fn test_u64list_deref_mut() { + let mut list = U64List(vec![1, 2, 3]); + list.push(4); + assert_eq!(list.0, vec![1, 2, 3, 4]); + + list.sort(); + assert_eq!(list.0, vec![1, 2, 3, 4]); + + list.clear(); + assert!(list.is_empty()); + } +} diff --git a/kona/crates/supervisor/storage/src/models/derivation.rs b/kona/crates/supervisor/storage/src/models/derivation.rs new file mode 100644 index 0000000000000..c8b022bc4947a --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/derivation.rs @@ -0,0 +1,220 @@ +//! Models for storing blockchain derivation in the database. +//! +//! This module defines the data structure and schema used for tracking +//! how blocks are derived from source. This is particularly relevant +//! in rollup contexts, such as linking an L2 block to its originating L1 block. + +use super::{BlockRef, U64List}; +use kona_interop::DerivedRefPair; +use reth_codecs::Compact; +use serde::{Deserialize, Serialize}; + +/// Represents a pair of blocks where one block [`derived`](`Self::derived`) is derived +/// from another [`source`](`Self::source`). +/// +/// This structure is used to track the lineage of blocks where L2 blocks are derived from L1 +/// blocks. It stores the [`BlockRef`] information for both the source and the derived blocks. +/// It is stored as value in the [`DerivedBlocks`](`crate::models::DerivedBlocks`) table. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct StoredDerivedBlockPair { + /// The block that was derived from the [`source`](`Self::source`) block. + pub derived: BlockRef, + /// The source block from which the [`derived`](`Self::derived`) block was created. + pub source: BlockRef, +} + +impl Compact for StoredDerivedBlockPair { + fn to_compact>(&self, buf: &mut B) -> usize { + let mut bytes_written = 0; + bytes_written += self.derived.to_compact(buf); + bytes_written += self.source.to_compact(buf); + bytes_written + } + + fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) { + let (derived, remaining_buf) = BlockRef::from_compact(buf, buf.len()); + let (source, final_remaining_buf) = + BlockRef::from_compact(remaining_buf, remaining_buf.len()); + (Self { derived, source }, final_remaining_buf) + } +} + +/// Converts from [`StoredDerivedBlockPair`] (storage format) to [`DerivedRefPair`] (external API +/// format). +/// +/// Performs a direct field mapping. +impl From for DerivedRefPair { + fn from(pair: StoredDerivedBlockPair) -> Self { + Self { derived: pair.derived.into(), source: pair.source.into() } + } +} + +/// Converts from [`DerivedRefPair`] (external API format) to [`StoredDerivedBlockPair`] (storage +/// format). +/// +/// Performs a direct field mapping. +impl From for StoredDerivedBlockPair { + fn from(pair: DerivedRefPair) -> Self { + Self { derived: pair.derived.into(), source: pair.source.into() } + } +} + +impl StoredDerivedBlockPair { + /// Creates a new [`StoredDerivedBlockPair`] from the given [`BlockRef`]s. + /// + /// # Arguments + /// + /// * `source` - The source block reference. + /// * `derived` - The derived block reference. + pub const fn new(source: BlockRef, derived: BlockRef) -> Self { + Self { source, derived } + } +} + +/// Represents a traversal of source blocks and their derived blocks. +/// +/// This structure is used to track the lineage of blocks where L2 blocks are derived from L1 +/// blocks. It stores the [`BlockRef`] information for the source block and the list of derived +/// block numbers. It is stored as value in the [`BlockTraversal`](`crate::models::BlockTraversal`) +/// table. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct SourceBlockTraversal { + /// The source block reference. + pub source: BlockRef, + /// The list of derived block numbers. + pub derived_block_numbers: U64List, +} + +impl Compact for SourceBlockTraversal { + fn to_compact>(&self, buf: &mut B) -> usize { + let mut bytes_written = 0; + bytes_written += self.source.to_compact(buf); + bytes_written += self.derived_block_numbers.to_compact(buf); + bytes_written + } + + fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) { + let (source, remaining_buf) = BlockRef::from_compact(buf, buf.len()); + let (derived_block_numbers, final_remaining_buf) = + U64List::from_compact(remaining_buf, remaining_buf.len()); + (Self { source, derived_block_numbers }, final_remaining_buf) + } +} + +impl SourceBlockTraversal { + /// Creates a new [`SourceBlockTraversal`] from the given [`BlockRef`] and [`U64List`]. + /// + /// # Arguments + /// + /// * `source` - The source block reference. + /// * `derived_block_numbers` - The list of derived block numbers. + pub const fn new(source: BlockRef, derived_block_numbers: U64List) -> Self { + Self { source, derived_block_numbers } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::BlockRef; + use alloy_primitives::B256; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use reth_codecs::Compact; + + fn test_b256(val: u8) -> B256 { + let mut val_bytes = [0u8; 32]; + val_bytes[0] = val; + let b256_from_val = B256::from(val_bytes); + B256::random() ^ b256_from_val + } + + #[test] + fn test_derived_block_pair_compact_roundtrip() { + let source_ref = BlockRef { + number: 100, + hash: test_b256(1), + parent_hash: test_b256(2), + timestamp: 1000, + }; + let derived_ref = BlockRef { + number: 200, + hash: test_b256(3), + parent_hash: test_b256(4), + timestamp: 1010, + }; + + let original_pair = StoredDerivedBlockPair { source: source_ref, derived: derived_ref }; + + let mut buffer = Vec::new(); + let bytes_written = original_pair.to_compact(&mut buffer); + + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + let (deserialized_pair, remaining_buf) = + StoredDerivedBlockPair::from_compact(&buffer, bytes_written); + + assert_eq!( + original_pair, deserialized_pair, + "Original and deserialized pairs should be equal" + ); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after deserialization"); + } + + #[test] + fn test_from_stored_to_derived_ref_pair() { + let source_ref = + BlockRef { number: 1, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 100 }; + let derived_ref = + BlockRef { number: 2, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 200 }; + + let stored = + StoredDerivedBlockPair { source: source_ref.clone(), derived: derived_ref.clone() }; + + // Convert to DerivedRefPair + let derived: DerivedRefPair = stored.into(); + + // The conversion should map fields directly (BlockRef -> BlockInfo) + assert_eq!(BlockInfo::from(source_ref), derived.source); + assert_eq!(BlockInfo::from(derived_ref), derived.derived); + } + + #[test] + fn test_from_derived_ref_pair_to_stored() { + let source_info = + BlockInfo { number: 10, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 100 }; + let derived_info = + BlockInfo { number: 20, hash: B256::ZERO, parent_hash: B256::ZERO, timestamp: 200 }; + + let pair = DerivedRefPair { source: source_info, derived: derived_info }; + + // Convert to StoredDerivedBlockPair + let stored: StoredDerivedBlockPair = pair.into(); + + // The conversion should map fields directly (BlockInfo -> BlockRef) + assert_eq!(BlockRef::from(source_info), stored.source); + assert_eq!(BlockRef::from(derived_info), stored.derived); + } + + #[test] + fn test_source_block_traversal_compact_roundtrip() { + let source_ref = BlockRef { + number: 123, + hash: test_b256(10), + parent_hash: test_b256(11), + timestamp: 1111, + }; + let derived_block_numbers = U64List(vec![1, 2, 3, 4, 5]); + let original = SourceBlockTraversal { source: source_ref, derived_block_numbers }; + + let mut buffer = Vec::new(); + let bytes_written = original.to_compact(&mut buffer); + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + let (deserialized, remaining_buf) = + SourceBlockTraversal::from_compact(&buffer, bytes_written); + assert_eq!( + original, deserialized, + "Original and deserialized SourceBlockTraversal should be equal" + ); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after deserialization"); + } +} diff --git a/kona/crates/supervisor/storage/src/models/head_ref.rs b/kona/crates/supervisor/storage/src/models/head_ref.rs new file mode 100644 index 0000000000000..7c5649baf4168 --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/head_ref.rs @@ -0,0 +1,138 @@ +use derive_more::TryFrom; +use op_alloy_consensus::interop::SafetyLevel; +use reth_db::DatabaseError; +use reth_db_api::table; +use serde::{Deserialize, Serialize}; + +/// Key representing a particular head reference type. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, TryFrom, +)] +#[try_from(repr)] +#[repr(u8)] +pub enum SafetyHeadRefKey { + /// Latest unverified or unsafe head. + Unsafe = 0, + + /// Head block considered safe via local verification. + LocalSafe = 1, + + /// Head block considered unsafe via cross-chain sync. + CrossUnsafe = 2, + + /// Head block considered safe. + Safe = 3, + + /// Finalized head block. + Finalized = 4, + + /// Invalid head reference. + Invalid = u8::MAX, +} + +/// Implementation of [`table::Encode`] for [`SafetyHeadRefKey`]. +impl table::Encode for SafetyHeadRefKey { + type Encoded = [u8; 1]; + + fn encode(self) -> Self::Encoded { + [self as u8] + } +} + +/// Implementation of [`table::Decode`] for [`SafetyHeadRefKey`]. +impl table::Decode for SafetyHeadRefKey { + fn decode(value: &[u8]) -> Result { + if value.is_empty() { + return Err(DatabaseError::Decode) + } + + value[0].try_into().map_err(|_| DatabaseError::Decode) + } +} + +/// Converts from [`SafetyHeadRefKey`] (internal storage reference) to [`SafetyLevel`] (public API +/// format). +/// +/// Performs a lossless and direct mapping from head reference level to safety level. +impl From for SafetyLevel { + fn from(key: SafetyHeadRefKey) -> Self { + match key { + SafetyHeadRefKey::Unsafe => Self::LocalUnsafe, + SafetyHeadRefKey::LocalSafe => Self::LocalSafe, + SafetyHeadRefKey::CrossUnsafe => Self::CrossUnsafe, + SafetyHeadRefKey::Safe => Self::CrossSafe, + SafetyHeadRefKey::Finalized => Self::Finalized, + SafetyHeadRefKey::Invalid => Self::Invalid, + } + } +} + +/// Converts from [`SafetyLevel`] (public API format) to [`SafetyHeadRefKey`] (internal storage +/// reference). +/// +/// Performs a direct mapping from safety level to head reference key. +impl From for SafetyHeadRefKey { + fn from(key: SafetyLevel) -> Self { + match key { + SafetyLevel::LocalUnsafe => Self::Unsafe, + SafetyLevel::LocalSafe => Self::LocalSafe, + SafetyLevel::CrossUnsafe => Self::CrossUnsafe, + SafetyLevel::CrossSafe => Self::Safe, + SafetyLevel::Finalized => Self::Finalized, + SafetyLevel::Invalid => Self::Invalid, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_db_api::table::{Decode, Encode}; + #[test] + fn test_head_ref_key_encode_decode() { + let cases = vec![ + (SafetyHeadRefKey::Unsafe, [0]), + (SafetyHeadRefKey::LocalSafe, [1]), + (SafetyHeadRefKey::CrossUnsafe, [2]), + (SafetyHeadRefKey::Safe, [3]), + (SafetyHeadRefKey::Finalized, [4]), + (SafetyHeadRefKey::Invalid, [255]), + ]; + + for (key, expected_encoding) in &cases { + // Test encoding + let encoded = key.encode(); + assert_eq!(encoded, *expected_encoding, "Encoding failed for {key:?}"); + + // Test decoding + let decoded = SafetyHeadRefKey::decode(&encoded).expect("Decoding should succeed"); + assert_eq!(decoded, *key, "Decoding mismatch for {key:?}"); + } + } + #[test] + fn test_round_trip_conversion() { + for level in [ + SafetyLevel::LocalUnsafe, + SafetyLevel::LocalSafe, + SafetyLevel::CrossUnsafe, + SafetyLevel::CrossSafe, + SafetyLevel::Finalized, + SafetyLevel::Invalid, + ] { + let round_trip = SafetyLevel::from(SafetyHeadRefKey::from(level)); + assert_eq!(round_trip, level, "Round-trip failed for {level:?}"); + } + + for key in [ + SafetyHeadRefKey::Unsafe, + SafetyHeadRefKey::LocalSafe, + SafetyHeadRefKey::CrossUnsafe, + SafetyHeadRefKey::Safe, + SafetyHeadRefKey::Finalized, + SafetyHeadRefKey::Invalid, + ] { + let round_trip = SafetyHeadRefKey::from(SafetyLevel::from(key)); + assert_eq!(round_trip, key, "Round-trip failed for {key:?}"); + } + } +} diff --git a/kona/crates/supervisor/storage/src/models/log.rs b/kona/crates/supervisor/storage/src/models/log.rs new file mode 100644 index 0000000000000..8e43a312f1d45 --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/log.rs @@ -0,0 +1,292 @@ +//! Models for storing blockchain logs in the database. +//! +//! This module defines the data structure and table mapping for logs emitted during +//! transaction execution. Each log is uniquely identified by its block number and +//! index within the block. +//! +//! The table is dup-sorted, allowing efficient grouping of multiple logs per block. +//! It supports fast appends, retrieval, and range queries ordered by log index. + +use alloy_primitives::B256; +use bytes::{Buf, BufMut}; +use kona_supervisor_types::{ExecutingMessage, Log}; +use reth_codecs::Compact; +use serde::{Deserialize, Serialize}; + +/// Metadata associated with a single emitted log. +/// +/// This is the value stored in the [`crate::models::LogEntries`] dup-sorted table. Each entry +/// includes: +/// - `index` - Index of the log in a block. +/// - `hash`: The keccak256 hash of the log event. +/// - `executing_message` - An optional field that may contain a cross-domain execution message. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct LogEntry { + /// Index of the log. + pub index: u32, + /// The keccak256 hash of the emitted log event. + pub hash: B256, + /// Optional cross-domain execution message. + pub executing_message: Option, +} +/// Compact encoding and decoding implementation for [`LogEntry`]. +/// +/// This encoding is used for storing log entries in dup-sorted tables, +/// where the `index` field is treated as the subkey. The layout is optimized +/// for lexicographic ordering by `index`. +/// +/// ## Encoding Layout (ordered): +/// - `index: u32` – Log index (subkey), used for ordering within dup table. +/// - `has_msg: u8` – 1 if `executing_message` is present, 0 otherwise. +/// - `hash: B256` – 32-byte Keccak256 hash of the log. +/// - `executing_message: Option` – Compact-encoded if present. +impl Compact for LogEntry { + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + let start_len = buf.remaining_mut(); + + buf.put_u32(self.index); // Subkey must be at first + buf.put_u8(self.executing_message.is_some() as u8); + buf.put_slice(self.hash.as_slice()); + + if let Some(msg) = &self.executing_message { + msg.to_compact(buf); + } + + start_len - buf.remaining_mut() + } + + fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) { + let index = buf.get_u32(); + let has_msg = buf.get_u8() != 0; + + assert!(buf.len() >= 32, "LogEntry::from_compact: buffer too small for hash"); + let hash = B256::from_slice(&buf[..32]); + buf.advance(32); + + let executing_message = if has_msg { + let (msg, rest) = ExecutingMessageEntry::from_compact(buf, buf.len()); + buf = rest; + Some(msg) + } else { + None + }; + + (Self { index, hash, executing_message }, buf) + } +} + +/// Conversion from [`Log`] to [`LogEntry`] used for internal storage. +/// +/// Maps fields 1:1, converting `executing_message` using `Into`. +impl From for LogEntry { + fn from(log: Log) -> Self { + Self { + index: log.index, + hash: log.hash, + executing_message: log.executing_message.map(Into::into), + } + } +} + +/// Conversion from [`LogEntry`] to [`Log`] for external API use. +/// +/// Mirrors the conversion from `Log`, enabling easy retrieval. +impl From for Log { + fn from(log: LogEntry) -> Self { + Self { + index: log.index, + hash: log.hash, + executing_message: log.executing_message.map(Into::into), + } + } +} + +/// Represents an entry of an executing message, containing metadata +/// about the message's origin and context within the blockchain. +/// - `chain_id` (`u64`): The unique identifier of the blockchain where the message originated. +/// - `block_number` (`u64`): The block number in the blockchain where the message originated. +/// - `log_index` (`u64`): The index of the log entry within the block where the message was logged. +/// - `timestamp` (`u64`): The timestamp associated with the block where the message was recorded. +/// - `hash` (`B256`): The unique hash identifier of the message. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ExecutingMessageEntry { + /// Log index within the block. + pub log_index: u32, + /// ID of the chain where the message was emitted. + pub chain_id: u64, + /// Block number in the source chain. + pub block_number: u64, + /// Timestamp of the block. + pub timestamp: u64, + /// Hash of the message. + pub hash: B256, +} + +/// Compact encoding for [`ExecutingMessageEntry`] used in log storage. +/// +/// This format ensures deterministic encoding and lexicographic ordering by +/// placing `log_index` first, which is used as the subkey in dup-sorted tables. +/// +/// ## Encoding Layout (ordered): +/// - `log_index: u32` – Subkey for dup sort ordering. +/// - `chain_id: u64` +/// - `block_number: u64` +/// - `timestamp: u64` +/// - `hash: B256` – 32-byte message hash. +impl Compact for ExecutingMessageEntry { + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + let start_len = buf.remaining_mut(); + + buf.put_u32(self.log_index); + buf.put_u64(self.chain_id); + buf.put_u64(self.block_number); + buf.put_u64(self.timestamp); + buf.put_slice(self.hash.as_slice()); + + start_len - buf.remaining_mut() + } + + fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) { + let log_index = buf.get_u32(); + let chain_id = buf.get_u64(); + let block_number = buf.get_u64(); + let timestamp = buf.get_u64(); + + assert!(buf.len() >= 32, "ExecutingMessageEntry::from_compact: buffer too small for hash"); + let hash = B256::from_slice(&buf[..32]); + buf.advance(32); + + (Self { chain_id, block_number, timestamp, hash, log_index }, buf) + } +} + +/// Converts from [`ExecutingMessage`] (external API format) to [`ExecutingMessageEntry`] (storage +/// format). +/// +/// Performs a direct field mapping. +impl From for ExecutingMessageEntry { + fn from(msg: ExecutingMessage) -> Self { + Self { + chain_id: msg.chain_id, + block_number: msg.block_number, + log_index: msg.log_index, + timestamp: msg.timestamp, + hash: msg.hash, + } + } +} + +/// Converts from [`ExecutingMessageEntry`] (storage format) to [`ExecutingMessage`] (external API +/// format). +/// +/// This enables decoding values stored in a compact format for use in application logic. +impl From for ExecutingMessage { + fn from(msg: ExecutingMessageEntry) -> Self { + Self { + chain_id: msg.chain_id, + block_number: msg.block_number, + log_index: msg.log_index, + timestamp: msg.timestamp, + hash: msg.hash, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; // Imports LogEntry, ExecutingMessageEntry + use alloy_primitives::B256; + use reth_codecs::Compact; // For the Compact trait methods + + // Helper to create somewhat unique B256 values for testing. + // Assumes the "rand" feature for alloy-primitives is enabled for tests. + fn test_b256(val: u8) -> B256 { + let mut val_bytes = [0u8; 32]; + val_bytes[0] = val; + let b256_from_val = B256::from(val_bytes); + B256::random() ^ b256_from_val + } + + #[test] + fn test_log_entry_compact_roundtrip_with_message() { + let original_log_entry = LogEntry { + index: 100, + hash: test_b256(1), + executing_message: Some(ExecutingMessageEntry { + chain_id: 10, + block_number: 1001, + log_index: 5, + timestamp: 1234567890, + hash: test_b256(2), + }), + }; + + let mut buffer = Vec::new(); + let bytes_written = original_log_entry.to_compact(&mut buffer); + + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + assert!(!buffer.is_empty(), "Buffer should not be empty after compression"); + + let (deserialized_log_entry, remaining_buf) = + LogEntry::from_compact(&buffer, bytes_written); + + assert_eq!( + original_log_entry, deserialized_log_entry, + "Original and deserialized log entries should be equal" + ); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after deserialization"); + } + + #[test] + fn test_log_entry_compact_roundtrip_without_message() { + let original_log_entry = + LogEntry { index: 100, hash: test_b256(3), executing_message: None }; + + let mut buffer = Vec::new(); + let bytes_written = original_log_entry.to_compact(&mut buffer); + + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + assert!(!buffer.is_empty(), "Buffer should not be empty after compression"); + + let (deserialized_log_entry, remaining_buf) = + LogEntry::from_compact(&buffer, bytes_written); + + assert_eq!( + original_log_entry, deserialized_log_entry, + "Original and deserialized log entries should be equal" + ); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after deserialization"); + } + + #[test] + fn test_executing_message_entry_compact_roundtrip() { + let original_entry = ExecutingMessageEntry { + log_index: 42, + chain_id: 1, + block_number: 123456, + timestamp: 1_654_321_000, + hash: test_b256(77), + }; + + let mut buffer = Vec::new(); + let bytes_written = original_entry.to_compact(&mut buffer); + + assert_eq!(bytes_written, buffer.len(), "Bytes written should match buffer length"); + assert!(!buffer.is_empty(), "Buffer should not be empty after serialization"); + + let (decoded_entry, remaining_buf) = + ExecutingMessageEntry::from_compact(&buffer, bytes_written); + + assert_eq!( + original_entry, decoded_entry, + "Original and decoded ExecutingMessageEntry should be equal" + ); + assert!(remaining_buf.is_empty(), "Remaining buffer should be empty after decoding"); + } +} diff --git a/kona/crates/supervisor/storage/src/models/mod.rs b/kona/crates/supervisor/storage/src/models/mod.rs new file mode 100644 index 0000000000000..357142fee4f42 --- /dev/null +++ b/kona/crates/supervisor/storage/src/models/mod.rs @@ -0,0 +1,247 @@ +//! Database table schemas used by the Supervisor. +//! +//! This module defines the value types, keys, and table layouts for all data +//! persisted by the `supervisor` component of the node. +//! +//! The tables are registered using [`reth_db_api::table::TableInfo`] and grouped into a +//! [`reth_db_api::TableSet`] for database initialization via Reth's storage-api. + +use reth_db_api::{ + TableSet, TableType, TableViewer, + table::{DupSort, TableInfo}, + tables, +}; +use std::fmt; + +mod log; +pub use log::{ExecutingMessageEntry, LogEntry}; + +mod block; +pub use block::BlockRef; + +mod derivation; +pub use derivation::{SourceBlockTraversal, StoredDerivedBlockPair}; + +mod common; +mod head_ref; +pub use head_ref::SafetyHeadRefKey; + +pub use common::U64List; + +/// Implements [`reth_db_api::table::Compress`] and [`reth_db_api::table::Decompress`] traits for +/// types that implement [`reth_codecs::Compact`]. +/// +/// This macro defines how to serialize and deserialize a type into a compressed +/// byte format using Reth's compact codec system. +/// +/// # Example +/// ```ignore +/// impl_compression_for_compact!(BlockRef, LogEntry); +/// ``` +macro_rules! impl_compression_for_compact { + ($($name:ident$(<$($generic:ident),*>)?),+) => { + $( + impl$(<$($generic: core::fmt::Debug + Send + Sync + Compact),*>)? reth_db_api::table::Compress for $name$(<$($generic),*>)? { + type Compressed = Vec; + + fn compress_to_buf>(&self, buf: &mut B) { + let _ = reth_codecs::Compact::to_compact(self, buf); + } + } + + impl$(<$($generic: core::fmt::Debug + Send + Sync + Compact),*>)? reth_db_api::table::Decompress for $name$(<$($generic),*>)? { + fn decompress(value: &[u8]) -> Result<$name$(<$($generic),*>)?, reth_db_api::DatabaseError> { + let (obj, _) = reth_codecs::Compact::from_compact(value, value.len()); + Ok(obj) + } + } + )+ + }; +} + +// Implement compression logic for all value types stored in tables +impl_compression_for_compact!( + BlockRef, + LogEntry, + StoredDerivedBlockPair, + U64List, + SourceBlockTraversal +); + +tables! { + /// A dup-sorted table that stores all logs emitted in a given block, sorted by their index. + /// Keyed by block number, with log index as the subkey for DupSort. + table LogEntries { + type Key = u64; // Primary key: u64 (block_number) + type Value = LogEntry; // Value: The log metadata + type SubKey = u32; // SubKey for DupSort: u32 (log_index) + } + + /// A table for storing block metadata by block number. + /// This is a standard table (not dup-sorted) where: + /// - Key: `u64` — block number + /// - Value: [`BlockRef`] — block metadata + table BlockRefs { + type Key = u64; + type Value = BlockRef; + } + + /// A table mapping a derived block number to its corresponding source and derived block reference. + /// - Key: `u64` — derived block number + /// - Value: [`StoredDerivedBlockPair`] — pair of source and derived block reference + table DerivedBlocks { + type Key = u64; + type Value = StoredDerivedBlockPair; + } + + /// A table mapping a source block number to a struct representing the traversal of its derived + /// block numbers. + /// - Key: `u64` — source block number + /// - Value: [`SourceBlockTraversal`] — contains the source block reference and the list of + /// derived block numbers. + table BlockTraversal { + type Key = u64; + type Value = SourceBlockTraversal; + } + + /// Stores the latest head block reference for each safety level. + /// # Key + /// - [`SafetyHeadRefKey`] — Enum variant indicating the type of head being tracked + /// (e.g., unsafe, locally safe, cross-chain safe, finalized). + /// + /// # Value + /// - [`BlockRef`] — Reference to a block including block number and hash. + table SafetyHeadRefs { + type Key = SafetyHeadRefKey; + type Value = BlockRef; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use reth_db_api::table::{Compress, Decompress}; + + // Helper to create somewhat unique B256 values for testing. + fn test_b256(val: u8) -> B256 { + let mut val_bytes = [0u8; 32]; + val_bytes[0] = val; // Place the u8 into the first byte of the array + let b256_from_val = B256::from(val_bytes); + B256::random() ^ b256_from_val + } + + #[test] + fn test_block_ref_compression_decompression() { + let original = BlockRef { + number: 1, + hash: test_b256(1), + parent_hash: test_b256(2), + timestamp: 1234567890, + }; + + let mut compressed_buf = Vec::new(); + original.compress_to_buf(&mut compressed_buf); + + // Ensure some data was written + assert!(!compressed_buf.is_empty()); + + let decompressed = BlockRef::decompress(&compressed_buf).unwrap(); + assert_eq!(original, decompressed); + } + + #[test] + fn test_log_entry_compression_decompression_with_message() { + let original = LogEntry { + index: 1, + hash: test_b256(3), + executing_message: Some(ExecutingMessageEntry { + chain_id: 1, + block_number: 100, + log_index: 2, + timestamp: 12345, + hash: test_b256(4), + }), + }; + + let mut compressed_buf = Vec::new(); + original.compress_to_buf(&mut compressed_buf); + assert!(!compressed_buf.is_empty()); + let decompressed = LogEntry::decompress(&compressed_buf).unwrap(); + assert_eq!(original, decompressed); + } + + #[test] + fn test_log_entry_compression_decompression_without_message() { + let original = LogEntry { index: 1, hash: test_b256(5), executing_message: None }; + let mut compressed_buf = Vec::new(); + original.compress_to_buf(&mut compressed_buf); + assert!(!compressed_buf.is_empty()); + let decompressed = LogEntry::decompress(&compressed_buf).unwrap(); + assert_eq!(original, decompressed); + } + + #[test] + fn test_derived_block_pair_compression_decompression() { + let source_ref = BlockRef { + number: 100, + hash: test_b256(6), + parent_hash: test_b256(7), + timestamp: 1000, + }; + let derived_ref = BlockRef { + number: 200, + hash: test_b256(8), + parent_hash: test_b256(8), // Link to source + timestamp: 1010, + }; + + let original_pair = StoredDerivedBlockPair { source: source_ref, derived: derived_ref }; + + let mut compressed_buf = Vec::new(); + original_pair.compress_to_buf(&mut compressed_buf); + + assert!(!compressed_buf.is_empty(), "Buffer should not be empty after compression"); + + let decompressed_pair = StoredDerivedBlockPair::decompress(&compressed_buf).unwrap(); + assert_eq!( + original_pair, decompressed_pair, + "Original and deserialized pairs should be equal" + ); + } + + #[test] + fn test_u64list_compression_decompression_empty() { + let original_list = U64List(Vec::new()); + + let mut compressed_buf = Vec::new(); + original_list.compress_to_buf(&mut compressed_buf); + + // For an empty list, the compact representation might also be empty or very small. + // The primary check is that deserialization works and results in an empty list. + let decompressed_list = U64List::decompress(&compressed_buf).unwrap(); + assert_eq!( + original_list, decompressed_list, + "Original and deserialized empty U64List should be equal" + ); + } + + #[test] + fn test_u64list_compression_decompression_with_data() { + let original_list = U64List(vec![10, 20, 30, 40, 50]); + + let mut compressed_buf = Vec::new(); + original_list.compress_to_buf(&mut compressed_buf); + + assert!( + !compressed_buf.is_empty(), + "Buffer should not be empty after compression of U64List with data" + ); + + let decompressed_list = U64List::decompress(&compressed_buf).unwrap(); + assert_eq!( + original_list, decompressed_list, + "Original and deserialized U64List with data should be equal" + ); + } +} diff --git a/kona/crates/supervisor/storage/src/providers/derivation_provider.rs b/kona/crates/supervisor/storage/src/providers/derivation_provider.rs new file mode 100644 index 0000000000000..331bd003b2aea --- /dev/null +++ b/kona/crates/supervisor/storage/src/providers/derivation_provider.rs @@ -0,0 +1,1377 @@ +//! Provider for derivation-related database operations. +use crate::{ + error::{EntryNotFoundError, StorageError}, + models::{ + BlockTraversal, DerivedBlocks, SourceBlockTraversal, StoredDerivedBlockPair, U64List, + }, +}; +use alloy_eips::eip1898::BlockNumHash; +use alloy_primitives::ChainId; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use reth_db_api::{ + cursor::DbCursorRO, + transaction::{DbTx, DbTxMut}, +}; +use tracing::{error, info, trace, warn}; + +const DEFAULT_LOG_INTERVAL: u64 = 100; + +/// Provides access to derivation storage operations within a transaction. +#[derive(Debug)] +pub(crate) struct DerivationProvider<'tx, TX> { + tx: &'tx TX, + chain_id: ChainId, + #[doc(hidden)] + observability_interval: u64, +} + +impl<'tx, TX> DerivationProvider<'tx, TX> { + pub(crate) const fn new(tx: &'tx TX, chain_id: ChainId) -> Self { + Self::new_with_observability_interval(tx, chain_id, DEFAULT_LOG_INTERVAL) + } + + pub(crate) const fn new_with_observability_interval( + tx: &'tx TX, + chain_id: ChainId, + observability_interval: u64, + ) -> Self { + Self { tx, chain_id, observability_interval } + } +} + +impl DerivationProvider<'_, TX> +where + TX: DbTx, +{ + /// Helper to get [`StoredDerivedBlockPair`] by block number. + fn get_derived_block_pair_by_number( + &self, + derived_block_number: u64, + ) -> Result { + let derived_block_pair_opt = + self.tx.get::(derived_block_number).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + derived_block_number, + %err, + "Failed to get derived block pair" + ); + })?; + + let derived_block_pair = derived_block_pair_opt.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + derived_block_number, + "Derived block not found" + ); + EntryNotFoundError::DerivedBlockNotFound(derived_block_number) + })?; + + Ok(derived_block_pair) + } + + /// Helper to get [`StoredDerivedBlockPair`] by derived [`BlockNumHash`]. + /// This function checks if the derived block hash matches the expected hash. + /// If there is a mismatch, it logs a warning and returns [`StorageError::EntryNotFound`] error. + pub(crate) fn get_derived_block_pair( + &self, + derived_block_id: BlockNumHash, + ) -> Result { + let derived_block_pair = self.get_derived_block_pair_by_number(derived_block_id.number)?; + + if derived_block_pair.derived.hash != derived_block_id.hash { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + derived_block_number = derived_block_id.number, + expected_hash = %derived_block_id.hash, + actual_hash = %derived_block_pair.derived.hash, + "Derived block hash mismatch" + ); + return Err(StorageError::ConflictError); + } + + Ok(derived_block_pair) + } + + /// Gets the source [`BlockInfo`] for the given derived [`BlockNumHash`]. + pub(crate) fn derived_to_source( + &self, + derived_block_id: BlockNumHash, + ) -> Result { + let derived_block_pair: StoredDerivedBlockPair = + self.get_derived_block_pair(derived_block_id)?; + Ok(derived_block_pair.source.into()) + } + + /// Gets the [`SourceBlockTraversal`] for the given source block number. + /// + /// # Arguments + /// + /// * `source_block_number` - The source block number. + /// + /// Returns the [`SourceBlockTraversal`] for the given source block number. + fn get_block_traversal( + &self, + source_block_number: u64, + ) -> Result { + let block_traversal = + self.tx.get::(source_block_number).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + source_block_number, + %err, + "Failed to get block traversal info for source block" + ); + })?; + + Ok(block_traversal.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + source_block_number, + "source block not found" + ); + EntryNotFoundError::SourceBlockNotFound(source_block_number) + })?) + } + + /// Gets the latest derived [`BlockInfo`] at the given source [`BlockNumHash`]. + /// This does NOT mean to get the derived block that is "derived from" the source block. + /// It could happen that a source block has no derived blocks, in which case the latest derived + /// block is from one of the previous source blocks. + /// + /// Returns the latest derived block pair. + pub(crate) fn latest_derived_block_at_source( + &self, + source_block_id: BlockNumHash, + ) -> Result { + let block_traversal = self.get_block_traversal(source_block_id.number)?; + + if block_traversal.source.hash != source_block_id.hash { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + source_block_hash = %source_block_id.hash, + "Source block hash mismatch" + ); + return Err(StorageError::ConflictError); + } + + let mut cursor = self.tx.cursor_read::()?; + let walker = cursor.walk_back(Some(source_block_id.number))?; + + for item in walker { + let (_, block_traversal) = item?; + if let Some(latest_derived_block_number) = block_traversal.derived_block_numbers.last() + { + let derived_block_pair = + self.get_derived_block_pair_by_number(*latest_derived_block_number)?; + return Ok(derived_block_pair.derived.into()); + } + } + + Err(EntryNotFoundError::MissingDerivedBlocks(source_block_id).into()) + } + + /// Gets the latest derivation state [`DerivedRefPair`], which includes the latest source block + /// and the latest derived block. + /// + /// # Returns + /// A [`DerivedRefPair`] containing the latest source block and latest derived block. + pub(crate) fn latest_derivation_state(&self) -> Result { + let mut cursor = self.tx.cursor_read::().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get cursor for DerivedBlocks" + ); + })?; + + let result = cursor.last().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to seek to last block" + ); + })?; + + let (_, block) = result.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + "No blocks found in storage" + ); + StorageError::DatabaseNotInitialised + })?; + + let latest_source_block = self.latest_source_block().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get latest source block" + ); + })?; + + Ok(DerivedRefPair { source: latest_source_block, derived: block.derived.into() }) + } + + /// Gets the latest [`SourceBlockTraversal`]. + /// + /// # Returns + /// The latest [`SourceBlockTraversal`] in the database. + fn latest_source_block_traversal(&self) -> Result { + let mut cursor = self.tx.cursor_read::()?; + let result = cursor.last()?; + + let (_, block_traversal) = result.ok_or_else(|| StorageError::DatabaseNotInitialised)?; + Ok(block_traversal) + } + + /// Gets the source block for the given source block number. + pub(crate) fn get_source_block( + &self, + source_block_number: u64, + ) -> Result { + let block_traversal = self.get_block_traversal(source_block_number)?; + Ok(block_traversal.source.into()) + } + + /// Gets the latest source block, even if it has no derived blocks. + pub(crate) fn latest_source_block(&self) -> Result { + let block = self.latest_source_block_traversal().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get latest source block traversal" + ); + })?; + + Ok(block.source.into()) + } + + /// Gets the activation block, which is the first block in the database. + pub(crate) fn get_activation_block(&self) -> Result { + let mut cursor = self.tx.cursor_read::()?; + let result = cursor.first()?; + + let (_, derived_block_pair) = result.ok_or_else(|| StorageError::DatabaseNotInitialised)?; + Ok(derived_block_pair.derived.into()) + } +} + +impl DerivationProvider<'_, TX> +where + TX: DbTxMut + DbTx, +{ + /// initialises the database with a derived activation block pair. + pub(crate) fn initialise(&self, activation_pair: DerivedRefPair) -> Result<(), StorageError> { + match self.get_derived_block_pair_by_number(0) { + Ok(pair) if activation_pair == pair.clone().into() => { + // Anchor matches, nothing to do + Ok(()) + } + Ok(_) => Err(StorageError::ConflictError), + Err(StorageError::EntryNotFound(_)) => { + self.save_source_block_internal(activation_pair.source)?; + self.save_derived_block_internal(activation_pair)?; + Ok(()) + } + Err(err) => Err(err), + } + } + + /// Saves a [`StoredDerivedBlockPair`] to [`DerivedBlocks`](`crate::models::DerivedBlocks`) + /// table and [`SourceBlockTraversal`] to [`BlockTraversal`](`crate::models::BlockTraversal`) + /// table in the database. + pub(crate) fn save_derived_block( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError> { + let latest_derivation_state = match self.latest_derivation_state() { + Ok(pair) => pair, + Err(StorageError::EntryNotFound(_)) => { + return Err(StorageError::DatabaseNotInitialised); + } + Err(e) => return Err(e), + }; + + // If the incoming derived block is not newer than the latest stored derived block, + // we do not save it, check if it is consistent with the saved state. + // If it is not consistent, we return an error. + if latest_derivation_state.derived.number >= incoming_pair.derived.number { + let stored_pair = self + .get_derived_block_pair_by_number(incoming_pair.derived.number) + .inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_derived_block_pair = %incoming_pair, + %err, + "Failed to get derived block pair" + ); + })?; + + if incoming_pair == stored_pair.into() { + return Ok(()); + } else { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %latest_derivation_state, + incoming_derived_block_pair = %incoming_pair, + "Incoming derived block is not consistent with the latest stored derived block" + ); + return Err(StorageError::ConflictError); + } + } + + // Latest source block must be same as the incoming source block + if latest_derivation_state.source != incoming_pair.source { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + latest_source_block = %latest_derivation_state.source, + incoming_source = %incoming_pair.source, + "Latest source block does not match the incoming derived block source" + ); + return Err(StorageError::BlockOutOfOrder); + } + + if !latest_derivation_state.derived.is_parent_of(&incoming_pair.derived) { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %latest_derivation_state, + incoming_derived_block_pair = %incoming_pair, + "Latest stored derived block is not parent of the incoming derived block" + ); + return Err(StorageError::BlockOutOfOrder); + } + + self.save_derived_block_internal(incoming_pair) + } + + /// Internal function to save a derived block pair. + /// This function does not perform checks on the incoming derived pair, + /// it assumes that the pair is valid and the latest derived block is its parent. + fn save_derived_block_internal( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError> { + // the derived block must be derived from the latest source block + let mut block_traversal = self.latest_source_block_traversal().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_derived_block_pair = %incoming_pair, + %err, + "Failed to get latest source block traversal" + ); + })?; + + let latest_source_block = block_traversal.clone().source.into(); + if incoming_pair.source != latest_source_block { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + latest_source_block = %latest_source_block, + incoming_source = %incoming_pair.source, + "Latest source block does not match the incoming derived block source" + ); + return Err(StorageError::BlockOutOfOrder); + } + + // Add the derived block number to the list + block_traversal.derived_block_numbers.push(incoming_pair.derived.number); + + // Save the derived block pair to the database + self.tx + .put::(incoming_pair.derived.number, incoming_pair.into()) + .inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_derived_block_pair = %incoming_pair, + %err, + "Failed to save derived block pair" + ); + })?; + + // Save the SourceBlockTraversal to the database + self.tx.put::(incoming_pair.source.number, block_traversal).inspect_err( + |err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_derived_block_pair = %incoming_pair, + %err, + "Failed to save derived block numbers for source block" + ); + }, + )?; + + Ok(()) + } + + /// Saves a source block to the database. + /// If the source block already exists, it does nothing. + /// If the source block does not exist, it creates a new [`SourceBlockTraversal`] and saves it + /// to the database. + pub(crate) fn save_source_block(&self, incoming_source: BlockInfo) -> Result<(), StorageError> { + let latest_source_block = match self.latest_source_block() { + Ok(latest_source_block) => latest_source_block, + Err(StorageError::EntryNotFound(_)) => { + return Err(StorageError::DatabaseNotInitialised); + } + Err(err) => return Err(err), + }; + + // idempotent check: if the source block already exists, do nothing + if latest_source_block == incoming_source { + return Ok(()); + } + + // If the incoming source block is not newer than the latest source block, + // we do not save it, check if it is consistent with the saved state. + // If it is not consistent, we return an error. + if latest_source_block.number > incoming_source.number { + let source_block = + self.get_source_block(incoming_source.number).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + incoming_source = %incoming_source, + %err, + "Failed to get source block" + ); + })?; + + if source_block == incoming_source { + return Ok(()); + } else { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + latest_source_block = %latest_source_block, + incoming_source = %incoming_source, + "Incoming source block is not consistent with the latest source block" + ); + return Err(StorageError::ConflictError); + } + } + + if !latest_source_block.is_parent_of(&incoming_source) { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + latest_source_block = %latest_source_block, + incoming_source = %incoming_source, + "Stored latest source block is not parent of the incoming source block" + ); + return Err(StorageError::BlockOutOfOrder); + } + + self.save_source_block_internal(incoming_source)?; + Ok(()) + } + + fn save_source_block_internal(&self, incoming_source: BlockInfo) -> Result<(), StorageError> { + let block_traversal = SourceBlockTraversal { + source: incoming_source.into(), + derived_block_numbers: U64List::default(), + }; + + self.tx.put::(incoming_source.number, block_traversal).inspect_err( + |err| { + error!(target: "supervisor::storage", chain_id = %self.chain_id, %err, "Failed to save block traversal"); + }, + )?; + + Ok(()) + } + + /// Rewinds the derivation database from the given derived block onward. + /// This removes all derived blocks with number >= the given block number + /// and updates the traversal state accordingly. + pub(crate) fn rewind_to(&self, block: &BlockNumHash) -> Result<(), StorageError> { + info!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + target_block_hash = %block.hash, + "Starting rewind of derivation storage" + ); + + // Validate the block exists and get the block pair - this provides hash validation + let block_pair = self.get_derived_block_pair(*block)?; + + // Get the latest block number from DerivedBlocks + let latest_block = { + let mut cursor = self.tx.cursor_read::()?; + cursor.last()?.map(|(num, _)| num).unwrap_or(block.number) + }; + + // Check for future block + if block.number > latest_block { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + latest_block, + "Cannot rewind to future block" + ); + return Err(StorageError::FutureData) + } + + // total blocks to rewind down to and including tgt block + let total_blocks = latest_block - block.number + 1; + let mut processed_blocks = 0; + + // Delete all derived blocks with number ≥ `block.number` + { + let mut cursor = self.tx.cursor_write::()?; + let mut walker = cursor.walk(Some(block.number))?; + + trace!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + target_block_hash = %block.hash, + latest_block, + total_blocks, + observability_interval = %self.observability_interval, + "Rewinding derived block storage..." + ); + + while let Some(Ok((key, _stored_block))) = walker.next() { + // Remove the block first + walker.delete_current()?; + + // Only count as processed after successful deletion + processed_blocks += 1; + + // Log progress periodically or on last block + if processed_blocks % self.observability_interval == 0 || + processed_blocks == total_blocks + { + let percentage = if total_blocks > 0 { + (processed_blocks as f64 / total_blocks as f64 * 100.0).min(100.0) + } else { + 100.0 + }; + + info!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number = %key, + percentage = %format!("{:.2}%", percentage), + processed_blocks, + total_blocks, + "Rewind progress" + ); + } + } + + info!( + target: "supervisor::storage", + target_block_number = %block.number, + target_block_hash = %block.hash, + chain_id = %self.chain_id, + total_blocks, + "Rewind completed successfully" + ); + } + + self.rewind_block_traversal_to(&block_pair) + } + + /// Rewinds the block traversal for a given derived block pair. + /// - If only part of the derived list needs to be removed, it updates the list in-place. + /// - If later source blocks exist, they are removed entirely. + // TODO: validate the logic in block invalidation and re-org + fn rewind_block_traversal_to( + &self, + block_pair: &StoredDerivedBlockPair, + ) -> Result<(), StorageError> { + // Retain only valid derived blocks < the invalidated one + let mut traversal = self.get_block_traversal(block_pair.source.number)?; + traversal.derived_block_numbers.retain(|&num| num < block_pair.derived.number); + + let mut walk_from = block_pair.source.number; + + // If there's still something left, update the entry. Otherwise, skip — let the walker + // delete it. + if !traversal.derived_block_numbers.is_empty() { + self.tx.put::(block_pair.source.number, traversal).inspect_err( + |err| { + error!(target: "supervisor::storage", chain_id = %self.chain_id, %err, "Failed to update block traversal"); + }, + )?; + walk_from += 1; + } + + // Walk from (source.number) forward, deleting entries with key ≥ source.number + let mut cursor = self.tx.cursor_write::()?; + let mut walker = cursor.walk(Some(walk_from))?; + while let Some(Ok((_, _))) = walker.next() { + walker.delete_current()?; + } + + Ok(()) + } + + /// Rewinds the derivation storage to a specific source block. + /// This will remove all derived blocks and their traversals from the given source block onward. + /// + /// # Arguments + /// * `source` - The source block number and hash to rewind to. + /// + /// # Returns + /// [`BlockInfo`] of the derived block that was rewound to, or `None` if no derived blocks + /// were found. + pub(crate) fn rewind_to_source( + &self, + source: &BlockNumHash, + ) -> Result, StorageError> { + let mut derived_rewind_target: Option = None; + { + let mut cursor = self.tx.cursor_write::()?; + let mut walker = cursor.walk(Some(source.number))?; + while let Some(Ok((block_number, block_traversal))) = walker.next() { + if block_number == source.number && block_traversal.source.hash != source.hash { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + source_block_number = source.number, + expected_hash = %source.hash, + actual_hash = %block_traversal.source.hash, + "Source block hash mismatch during rewind" + ); + return Err(StorageError::ConflictError); + } + + if derived_rewind_target.is_none() && + !block_traversal.derived_block_numbers.is_empty() + { + let first_num = block_traversal.derived_block_numbers[0]; + let derived_block_pair = self.get_derived_block_pair_by_number(first_num)?; + derived_rewind_target = Some(derived_block_pair.derived.into()); + } + + walker.delete_current()?; + } + } + + // Delete all derived blocks with number ≥ `block_info.number` + if let Some(rewind_target) = derived_rewind_target { + let mut cursor = self.tx.cursor_write::()?; + let mut walker = cursor.walk(Some(rewind_target.number))?; + while let Some(Ok((_, _))) = walker.next() { + walker.delete_current()?; // we’re already walking from the rewind point + } + } + + Ok(derived_rewind_target) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Tables; + use alloy_primitives::B256; + use kona_cli::init_test_tracing; + use kona_interop::DerivedRefPair; + use kona_protocol::BlockInfo; + use reth_db::{ + Database, DatabaseEnv, + mdbx::{DatabaseArguments, init_db_for}, + }; + use tempfile::TempDir; + + static CHAIN_ID: ChainId = 1; + + fn block_info(number: u64, parent_hash: B256, timestamp: u64) -> BlockInfo { + BlockInfo { hash: B256::from([number as u8; 32]), number, parent_hash, timestamp } + } + + const fn derived_pair(source: BlockInfo, derived: BlockInfo) -> DerivedRefPair { + DerivedRefPair { source, derived } + } + + fn genesis_block() -> BlockInfo { + BlockInfo { + hash: B256::from([0u8; 32]), + number: 0, + parent_hash: B256::ZERO, + timestamp: 100, + } + } + + /// Sets up a new temp DB + fn setup_db() -> DatabaseEnv { + let temp_dir = TempDir::new().expect("Could not create temp dir"); + init_db_for::<_, Tables>(temp_dir.path(), DatabaseArguments::default()) + .expect("Failed to init database") + } + + /// Helper to initialize database in a new transaction, committing if successful. + fn initialize_db(db: &DatabaseEnv, pair: &DerivedRefPair) -> Result<(), StorageError> { + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let res = provider.initialise(*pair); + if res.is_ok() { + tx.commit().expect("Failed to commit transaction"); + } + res + } + + /// Helper to insert a pair in a new transaction, committing if successful. + fn insert_pair(db: &DatabaseEnv, pair: &DerivedRefPair) -> Result<(), StorageError> { + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let res = provider.save_derived_block(*pair); + if res.is_ok() { + tx.commit().expect("Failed to commit transaction"); + } + res + } + + /// Helper to insert a source block in a new transaction, committing if successful. + fn insert_source_block(db: &DatabaseEnv, source: &BlockInfo) -> Result<(), StorageError> { + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let res = provider.save_source_block(*source); + if res.is_ok() { + tx.commit().expect("Failed to commit transaction"); + } + res + } + + #[test] + fn initialise_inserts_anchor_if_not_exists() { + let db = setup_db(); + + let source = block_info(100, B256::from([100u8; 32]), 200); + let derived = block_info(0, genesis_block().hash, 200); + let anchor = derived_pair(source, derived); + + // Should succeed and insert the anchor + assert!(initialize_db(&db, &anchor).is_ok()); + + // Check that the anchor is present + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let stored = provider.get_derived_block_pair_by_number(0).expect("should exist"); + assert_eq!(stored.source.hash, anchor.source.hash); + assert_eq!(stored.derived.hash, anchor.derived.hash); + } + + #[test] + fn initialise_is_idempotent_if_anchor_matches() { + let db = setup_db(); + + let source = block_info(100, B256::from([100u8; 32]), 200); + let anchor = derived_pair(source, genesis_block()); + + // First initialise + assert!(initialize_db(&db, &anchor).is_ok()); + // Second initialise with the same anchor should succeed (idempotent) + assert!(insert_pair(&db, &anchor).is_ok()); + } + + #[test] + fn initialise_fails_if_anchor_mismatch() { + let db = setup_db(); + + let source = block_info(100, B256::from([100u8; 32]), 200); + let anchor = derived_pair(source, genesis_block()); + + // Insert the genesis + assert!(initialize_db(&db, &anchor).is_ok()); + + // Try to initialise with a different anchor (different hash) + let wrong_derived = block_info(0, B256::from([42u8; 32]), 200); + let wrong_anchor = derived_pair(source, wrong_derived); + + let result = initialize_db(&db, &wrong_anchor); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn save_derived_block_positive() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + let source3 = block_info(101, source1.hash, 400); + let derived3 = block_info(3, derived2.hash, 400); + let pair3 = derived_pair(source3, derived3); + assert!(insert_source_block(&db, &source3).is_ok()); + assert!(insert_pair(&db, &pair3).is_ok()); + } + + #[test] + fn save_derived_block_wrong_parent_should_fail() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let wrong_parent_hash = B256::from([99u8; 32]); + let derived2 = block_info(2, wrong_parent_hash, 300); + let pair2 = derived_pair(source1, derived2); + let result = insert_pair(&db, &pair2); + assert!(matches!(result, Err(StorageError::BlockOutOfOrder))); + } + + #[test] + fn save_derived_block_gap_in_number_should_fail() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(4, derived1.hash, 400); // should be 2, not 4 + let pair2 = derived_pair(source1, derived2); + let result = insert_pair(&db, &pair2); + assert!(matches!(result, Err(StorageError::BlockOutOfOrder))); + } + + #[test] + fn duplicate_derived_block_number_should_pass() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + // Try to insert the same derived block again + let result = insert_pair(&db, &pair1); + assert!(result.is_ok(), "Should allow inserting the same derived block again"); + } + + #[test] + fn save_old_block_should_pass() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + // Try to insert a block with a lower number than the latest + let result = insert_pair(&db, &pair1); + assert!(result.is_ok(), "Should allow inserting an old derived block"); + } + + #[test] + fn non_monotonic_l2_number_should_fail() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + // Try to insert a block with a lower number than the latest + let derived_non_monotonic = block_info(1, derived2.hash, 400); + let pair_non_monotonic = derived_pair(source1, derived_non_monotonic); + let result = insert_pair(&db, &pair_non_monotonic); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn test_latest_derived_block_at_source_returns_latest() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + let source2 = block_info(101, B256::from([100u8; 32]), 300); + let derived3 = block_info(3, derived2.hash, 400); + let pair3 = derived_pair(source2, derived3); + assert!(insert_source_block(&db, &source2).is_ok()); + assert!(insert_pair(&db, &pair3).is_ok()); + + // Now check latest_derived_block_at_source returns derived2 for source1 + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let source_id1 = BlockNumHash { number: source1.number, hash: source1.hash }; + let latest = provider.latest_derived_block_at_source(source_id1).expect("should exist"); + assert_eq!(latest.number, derived2.number); + assert_eq!(latest.hash, derived2.hash); + + // Now check latest_derived_block_at_source returns derived3 for source2 + let source_id2 = BlockNumHash { number: source2.number, hash: source2.hash }; + let latest = provider.latest_derived_block_at_source(source_id2).expect("should exist"); + assert_eq!(latest, derived3); + } + + #[test] + fn test_latest_derived_block_at_source_empty_list_returns_error() { + let db = setup_db(); + + // Use a source block that does not exist + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let source_id = BlockNumHash { number: 9999, hash: B256::from([99u8; 32]) }; + let result = provider.latest_derived_block_at_source(source_id); + assert!(matches!(result, Err(StorageError::EntryNotFound(_)))); + } + + #[test] + fn test_latest_derived_block_at_source_hash_mismatch_returns_error() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + // Use correct number but wrong hash + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let wrong_hash = B256::from([123u8; 32]); + let source_id = BlockNumHash { number: source1.number, hash: wrong_hash }; + let result = provider.latest_derived_block_at_source(source_id); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn test_latest_derivation_state() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let latest = provider.latest_derivation_state().expect("should exist"); + assert_eq!(latest, pair2); + } + + #[test] + fn test_latest_derivation_state_empty_storage() { + let db = setup_db(); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let result = provider.latest_derivation_state(); + print!("{result:?}"); + assert!( + matches!(result, Err(StorageError::DatabaseNotInitialised)), + "Should return DatabaseNotInitialised error when no derivation state exists" + ); + } + + #[test] + fn test_latest_derivation_state_empty_source() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source2 = block_info(101, source1.hash, 300); + let derived2 = block_info(2, derived1.hash, 300); + let pair2 = derived_pair(source2, derived2); + assert!(insert_source_block(&db, &source2).is_ok()); + assert!(insert_pair(&db, &pair2).is_ok()); + + let source3 = block_info(102, source2.hash, 400); + assert!(insert_source_block(&db, &source3).is_ok()); + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let latest = provider.latest_derivation_state().expect("should exist"); + let expected_derivation_state = DerivedRefPair { source: source3, derived: derived2 }; + assert_eq!(latest, expected_derivation_state); + } + + #[test] + fn test_latest_derivation_state_empty_returns_error() { + let temp_dir = TempDir::new().expect("Could not create temp dir"); + let db = init_db_for::<_, Tables>(temp_dir.path(), DatabaseArguments::default()) + .expect("Failed to init database"); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + assert!(matches!( + provider.latest_derivation_state(), + Err(StorageError::DatabaseNotInitialised) + )); + } + + #[test] + fn test_derived_to_source_returns_correct_source() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(1, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let derived_block_id = BlockNumHash { number: derived1.number, hash: derived1.hash }; + let source = provider.derived_to_source(derived_block_id).expect("should exist"); + assert_eq!(source, source1); + } + + #[test] + fn test_derived_to_source_not_found_returns_error() { + let db = setup_db(); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let derived_block_id = BlockNumHash { number: 9999, hash: B256::from([9u8; 32]) }; + let result = provider.derived_to_source(derived_block_id); + assert!(matches!(result, Err(StorageError::EntryNotFound(_)))); + } + + #[test] + fn save_source_block_positive() { + let db = setup_db(); + + let derived0 = block_info(10, B256::from([10u8; 32]), 200); + let pair1 = derived_pair(genesis_block(), derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(1, genesis_block().hash, 200); + assert!(insert_source_block(&db, &source1).is_ok()); + } + + #[test] + fn save_source_block_idempotent_should_pass() { + let db = setup_db(); + + let derived0 = block_info(10, B256::from([10u8; 32]), 200); + let pair1 = derived_pair(genesis_block(), derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(1, genesis_block().hash, 200); + assert!(insert_source_block(&db, &source1).is_ok()); + // Try saving the same block again + assert!(insert_source_block(&db, &source1).is_ok()); + } + + #[test] + fn save_source_invalid_parent_should_fail() { + let db = setup_db(); + + let source0 = block_info(10, B256::from([10u8; 32]), 200); + let derived0 = genesis_block(); + let pair1 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(11, B256::from([1u8; 32]), 200); + let result = insert_source_block(&db, &source1); + assert!( + matches!(result, Err(StorageError::BlockOutOfOrder)), + "Should fail with BlockOutOfOrder error" + ); + } + + #[test] + fn save_source_block_lower_number_should_pass() { + let db = setup_db(); + + let source0 = block_info(10, B256::from([10u8; 32]), 200); + let derived0 = genesis_block(); + let pair1 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(11, source0.hash, 400); + assert!(insert_source_block(&db, &source1).is_ok()); + + // Try to save a block with a lower number + let result = insert_source_block(&db, &source0); + assert!(result.is_ok(), "Should allow saving an old source block"); + } + + #[test] + fn save_inconsistent_source_block_lower_number_should_fail() { + let db = setup_db(); + + let source0 = block_info(10, B256::from([10u8; 32]), 200); + let derived0 = genesis_block(); + let pair1 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(11, source0.hash, 400); + assert!(insert_source_block(&db, &source1).is_ok()); + + let old_source = block_info(source0.number, B256::from([1u8; 32]), 400); + // Try to save a block with a lower number + let result = insert_source_block(&db, &old_source); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn save_source_block_gap_number_should_fail() { + let db = setup_db(); + + let derived0 = block_info(10, B256::from([10u8; 32]), 200); + let pair1 = derived_pair(genesis_block(), derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(2, genesis_block().hash, 400); + // Try to skip a block + let result = insert_source_block(&db, &source1); + assert!(matches!(result, Err(StorageError::BlockOutOfOrder))); + } + + #[test] + fn save_source_block_higher_number_should_succeed() { + let db = setup_db(); + + let derived0 = block_info(10, B256::from([10u8; 32]), 200); + let pair1 = derived_pair(genesis_block(), derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(1, genesis_block().hash, 200); + let source2 = block_info(2, source1.hash, 400); + assert!(insert_source_block(&db, &source1).is_ok()); + assert!(insert_source_block(&db, &source2).is_ok()); + } + + #[test] + fn save_source_block_traversal_updates_existing_traversal_positive() { + let db = setup_db(); + + let derived0 = block_info(10, B256::from([10u8; 32]), 200); + let pair1 = derived_pair(genesis_block(), derived0); + assert!(initialize_db(&db, &pair1).is_ok()); + + let source1 = block_info(1, genesis_block().hash, 200); + assert!(insert_source_block(&db, &source1).is_ok()); + + let derived1 = block_info(100, derived0.hash, 200); + + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let mut block_traversal = + provider.get_block_traversal(source1.number).expect("should exist"); + block_traversal.derived_block_numbers.push(derived1.number); + assert!(tx.put::(source1.number, block_traversal).is_ok()); + } + + #[test] + fn test_get_activation_block_returns_error_if_empty() { + let db = setup_db(); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let result = provider.get_activation_block(); + assert!(matches!(result, Err(StorageError::DatabaseNotInitialised))); + } + + #[test] + fn test_get_activation_block_with_multiple_blocks_returns_first() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(0, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + let derived2 = block_info(1, derived1.hash, 300); + let pair2 = derived_pair(source1, derived2); + assert!(insert_pair(&db, &pair2).is_ok()); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let activation = provider.get_activation_block().expect("should exist"); + assert_eq!(activation, derived1); + } + + #[test] + fn rewind_to_source_returns_none_when_no_source_present() { + let db = setup_db(); + let tx = db.tx_mut().expect("Failed to get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let source = BlockNumHash { number: 9999, hash: B256::from([9u8; 32]) }; + let res = provider.rewind_to_source(&source).expect("should succeed"); + assert!(res.is_none(), "Expected None when no source traversal exists"); + } + + #[test] + fn rewind_to_source_fails_on_source_hash_mismatch() { + let db = setup_db(); + + let source1 = block_info(100, B256::from([100u8; 32]), 200); + let derived1 = block_info(0, genesis_block().hash, 200); + let pair1 = derived_pair(source1, derived1); + assert!(initialize_db(&db, &pair1).is_ok()); + + // insert a source block at number 1 with a certain hash + let source_saved = block_info(101, source1.hash, 200); + assert!(insert_source_block(&db, &source_saved).is_ok()); + + // create provider and call rewind_to_source with same number but different hash + let tx = db.tx_mut().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let mismatched_source = BlockNumHash { number: 101, hash: B256::from([42u8; 32]) }; + let result = provider.rewind_to_source(&mismatched_source); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn rewind_to_source_deletes_derived_blocks_and_returns_target() { + let db = setup_db(); + + let source0 = block_info(100, B256::from([100u8; 32]), 200); + let derived0 = block_info(0, genesis_block().hash, 200); + let pair0 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair0).is_ok()); + + // Setup source1 with derived 10,11,12 and source2 with 13,14 + let source1 = block_info(101, source0.hash, 200); + let source2 = block_info(102, source1.hash, 300); + let derived1 = block_info(1, derived0.hash, 195); + let derived2 = block_info(2, derived1.hash, 197); + let derived3 = block_info(3, derived2.hash, 290); + let derived4 = block_info(4, derived3.hash, 292); + let derived5 = block_info(5, derived4.hash, 295); + + assert!(insert_source_block(&db, &source1).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived1)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived2)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived3)).is_ok()); + + assert!(insert_source_block(&db, &source2).is_ok()); + assert!(insert_pair(&db, &derived_pair(source2, derived4)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source2, derived5)).is_ok()); + + // Perform rewind_to_source starting at source1 + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let source_id = BlockNumHash { number: source1.number, hash: source1.hash }; + let res = provider.rewind_to_source(&source_id).expect("rewind should succeed"); + + // derived_rewind_target should be the first derived block encountered (10) + assert!(res.is_some(), "expected a derived rewind target"); + let target = res.unwrap(); + assert_eq!(target, derived1); + + let res = provider.get_derived_block_pair_by_number(10); + assert!(matches!(res, Err(StorageError::EntryNotFound(_)))); + } + + #[test] + fn rewind_to_deletes_derived_blocks_and_returns_target() { + init_test_tracing(); + + let db = setup_db(); + + let source0 = block_info(100, B256::from([100u8; 32]), 200); + let derived0 = block_info(0, genesis_block().hash, 200); + let pair0 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair0).is_ok()); + + // Setup source1 with derived 10,11,12 and source2 with 13,14 + let source1 = block_info(101, source0.hash, 200); + let source2 = block_info(102, source1.hash, 300); + let derived1 = block_info(1, derived0.hash, 195); + let derived2 = block_info(2, derived1.hash, 197); + let derived3 = block_info(3, derived2.hash, 290); + let derived4 = block_info(4, derived3.hash, 292); + let derived5 = block_info(5, derived4.hash, 295); + + assert!(insert_source_block(&db, &source1).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived1)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived2)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source1, derived3)).is_ok()); + + assert!(insert_source_block(&db, &source2).is_ok()); + assert!(insert_pair(&db, &derived_pair(source2, derived4)).is_ok()); + assert!(insert_pair(&db, &derived_pair(source2, derived5)).is_ok()); + + // Perform rewind_to_source starting at source1 + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new_with_observability_interval(&tx, CHAIN_ID, 1); + let derived_id = BlockNumHash { number: derived1.number, hash: derived1.hash }; + provider.rewind_to(&derived_id).expect("rewind should succeed"); + + let res = provider.get_derived_block_pair_by_number(1); + assert!(matches!(res, Err(StorageError::EntryNotFound(_)))); + } + + #[test] + fn rewind_to_source_with_empty_derived_list_returns_none() { + let db = setup_db(); + + let source0 = block_info(100, B256::from([100u8; 32]), 200); + let derived0 = block_info(0, genesis_block().hash, 200); + let pair0 = derived_pair(source0, derived0); + assert!(initialize_db(&db, &pair0).is_ok()); + + // Insert a source block that has no derived_block_numbers + let source1 = block_info(101, source0.hash, 200); + assert!(insert_source_block(&db, &source1).is_ok()); + + // Call rewind_to_source on that source + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + let res = provider.rewind_to_source(&source1.id()).expect("rewind should succeed"); + + assert!(res.is_none(), "Expected None when source has empty derived list"); + + let tx = db.tx().expect("Could not get tx"); + let provider = DerivationProvider::new(&tx, CHAIN_ID); + + let activation = provider.get_activation_block().expect("activation should exist"); + assert_eq!(activation, derived0); + } +} diff --git a/kona/crates/supervisor/storage/src/providers/head_ref_provider.rs b/kona/crates/supervisor/storage/src/providers/head_ref_provider.rs new file mode 100644 index 0000000000000..fe71f13b61f90 --- /dev/null +++ b/kona/crates/supervisor/storage/src/providers/head_ref_provider.rs @@ -0,0 +1,331 @@ +//! Provider for tracking block safety head reference +use crate::{StorageError, models::SafetyHeadRefs}; +use alloy_primitives::ChainId; +use derive_more::Constructor; +use kona_protocol::BlockInfo; +use op_alloy_consensus::interop::SafetyLevel; +use reth_db_api::transaction::{DbTx, DbTxMut}; +use tracing::{error, warn}; + +/// A Safety Head Reference storage that wraps transactional reference. +#[derive(Debug, Constructor)] +pub(crate) struct SafetyHeadRefProvider<'tx, TX> { + tx: &'tx TX, + chain_id: ChainId, +} + +impl SafetyHeadRefProvider<'_, TX> +where + TX: DbTx, +{ + pub(crate) fn get_safety_head_ref( + &self, + safety_level: SafetyLevel, + ) -> Result { + let head_ref_key = safety_level.into(); + let result = self.tx.get::(head_ref_key).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %safety_level, + %err, + "Failed to seek head reference" + ); + })?; + let block_ref = result.ok_or_else(|| StorageError::FutureData)?; + Ok(block_ref.into()) + } +} + +impl SafetyHeadRefProvider<'_, Tx> +where + Tx: DbTxMut + DbTx, +{ + /// Updates the safety head reference with the provided block info. + /// If the block info's number is less than the current head reference's number, + /// it will not update the head reference and will log a warning. + pub(crate) fn update_safety_head_ref( + &self, + safety_level: SafetyLevel, + incoming_head_ref: &BlockInfo, + ) -> Result<(), StorageError> { + // Ensure the block_info.number is greater than the stored head reference + // If the head reference is not set, this check will be skipped. + if let Ok(current_head_ref) = self.get_safety_head_ref(safety_level) { + if current_head_ref.number > incoming_head_ref.number { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %current_head_ref, + %incoming_head_ref, + %safety_level, + "Attempting to update head reference with a block that has a lower number than the current head reference", + ); + return Ok(()); + } + } + + self.tx + .put::(safety_level.into(), (*incoming_head_ref).into()) + .inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %incoming_head_ref, + %safety_level, + %err, + "Failed to store head reference" + ) + })?; + Ok(()) + } + + /// Forcefully resets the head reference only if the current stored head is ahead of the + /// incoming one. + /// + /// This is intended for internal use during rewinds, where the safety head needs to be directly + /// set to a previous block regardless of the current head state. + pub(crate) fn reset_safety_head_ref_if_ahead( + &self, + safety_level: SafetyLevel, + incoming_head_ref: &BlockInfo, + ) -> Result<(), StorageError> { + // Skip if the current head is behind or missing. + match self.get_safety_head_ref(safety_level) { + Ok(current_head_ref) => { + if current_head_ref.number < incoming_head_ref.number { + return Ok(()); + } + } + Err(StorageError::FutureData) => { + return Ok(()); + } + Err(err) => return Err(err), + } + + self.tx + .put::(safety_level.into(), (*incoming_head_ref).into()) + .inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %incoming_head_ref, + %safety_level, + %err, + "Failed to reset head reference" + ) + })?; + Ok(()) + } + + /// Removes the safety head reference for the specified safety level. + pub(crate) fn remove_safety_head_ref( + &self, + safety_level: SafetyLevel, + ) -> Result<(), StorageError> { + self.tx.delete::(safety_level.into(), None).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %safety_level, + %err, + "Failed to remove head reference" + ) + })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Tables; + use alloy_primitives::B256; + use reth_db::{ + DatabaseEnv, + mdbx::{DatabaseArguments, init_db_for}, + }; + use reth_db_api::Database; + use tempfile::TempDir; + + static CHAIN_ID: ChainId = 1; + + fn setup_db() -> DatabaseEnv { + let temp_dir = TempDir::new().expect("Could not create temp dir"); + init_db_for::<_, Tables>(temp_dir.path(), DatabaseArguments::default()) + .expect("Failed to init database") + } + + #[test] + fn test_safety_head_ref_retrieval() { + let db = setup_db(); + + // Create write transaction first + let write_tx = db.tx_mut().expect("Failed to create write transaction"); + let write_provider = SafetyHeadRefProvider::new(&write_tx, CHAIN_ID); + + // Initially, there should be no head ref + let result = write_provider.get_safety_head_ref(SafetyLevel::CrossSafe); + assert!(result.is_err()); + + // Update head ref + let block_info = BlockInfo::default(); + write_provider + .update_safety_head_ref(SafetyLevel::CrossSafe, &block_info) + .expect("Failed to update head ref"); + + // Commit the write transaction + write_tx.commit().expect("Failed to commit the write transaction"); + + // Create a new read transaction to verify + let tx = db.tx().expect("Failed to create transaction"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + let result = + provider.get_safety_head_ref(SafetyLevel::CrossSafe).expect("Failed to get head ref"); + assert_eq!(result, block_info); + } + + #[test] + fn test_safety_head_ref_update() { + let db = setup_db(); + let write_tx = db.tx_mut().expect("Failed to create write transaction"); + let write_provider = SafetyHeadRefProvider::new(&write_tx, CHAIN_ID); + + // Create initial block info + let initial_block_info = BlockInfo { + hash: Default::default(), + number: 1, + parent_hash: Default::default(), + timestamp: 100, + }; + write_provider + .update_safety_head_ref(SafetyLevel::CrossSafe, &initial_block_info) + .expect("Failed to update head ref"); + + // Create updated block info + let mut updated_block_info = BlockInfo { + hash: Default::default(), + number: 1, + parent_hash: Default::default(), + timestamp: 200, + }; + updated_block_info.number = 100; + write_provider + .update_safety_head_ref(SafetyLevel::CrossSafe, &updated_block_info) + .expect("Failed to update head ref"); + + // Commit the write transaction + write_tx.commit().expect("Failed to commit the write transaction"); + + // Verify the updated value + let tx = db.tx().expect("Failed to create transaction"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + let result = + provider.get_safety_head_ref(SafetyLevel::CrossSafe).expect("Failed to get head ref"); + assert_eq!(result, updated_block_info); + } + + #[test] + fn test_reset_safety_head_ref_if_ahead() { + let db = setup_db(); + let tx = db.tx_mut().expect("Failed to start write tx"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + + // Set initial head at 100 + let head_100 = BlockInfo { + number: 100, + hash: B256::from([1u8; 32]), + parent_hash: B256::ZERO, + timestamp: 1234, + }; + provider.update_safety_head_ref(SafetyLevel::CrossSafe, &head_100).expect("update failed"); + + // Try to reset to 101 (should NOT update — current is behind) + let head_101 = BlockInfo { number: 101, ..head_100 }; + provider + .reset_safety_head_ref_if_ahead(SafetyLevel::CrossSafe, &head_101) + .expect("reset failed"); + + // Should still be 100 + let current = provider.get_safety_head_ref(SafetyLevel::CrossSafe).expect("get failed"); + assert_eq!(current.number, 100); + + // Now try to reset to 90 (should update — current is ahead) + let head_90 = BlockInfo { number: 90, ..head_100 }; + provider + .reset_safety_head_ref_if_ahead(SafetyLevel::CrossSafe, &head_90) + .expect("reset failed"); + + // Should now be 90 + let current = provider.get_safety_head_ref(SafetyLevel::CrossSafe).expect("get failed"); + assert_eq!(current.number, 90); + + tx.commit().expect("commit failed"); + } + + #[test] + fn test_reset_safety_head_ref_should_ignore_future_data() { + let db = setup_db(); + let tx = db.tx_mut().expect("Failed to start write tx"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + + // Set initial head at 100 + let head_100 = BlockInfo { + number: 100, + hash: B256::from([1u8; 32]), + parent_hash: B256::ZERO, + timestamp: 1234, + }; + + provider + .reset_safety_head_ref_if_ahead(SafetyLevel::CrossSafe, &head_100) + .expect("reset should succeed"); + + // check head is not updated and still returns FutureData Err + let result = provider.get_safety_head_ref(SafetyLevel::CrossSafe); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), StorageError::FutureData)); + + tx.commit().expect("commit failed"); + } + + #[test] + fn test_remove_safety_head_ref_removes_existing() { + let db = setup_db(); + let tx = db.tx_mut().expect("Failed to start write tx"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + + // Set a head ref + let block_info = BlockInfo { + hash: Default::default(), + number: 42, + parent_hash: Default::default(), + timestamp: 1234, + }; + provider + .update_safety_head_ref(SafetyLevel::CrossSafe, &block_info) + .expect("update failed"); + + // Remove it + provider.remove_safety_head_ref(SafetyLevel::CrossSafe).expect("remove failed"); + + // Should now return FutureData error + let result = provider.get_safety_head_ref(SafetyLevel::CrossSafe); + assert!(matches!(result, Err(StorageError::FutureData))); + } + + #[test] + fn test_remove_safety_head_ref_no_existing() { + let db = setup_db(); + let tx = db.tx_mut().expect("Failed to start write tx"); + let provider = SafetyHeadRefProvider::new(&tx, CHAIN_ID); + + // Remove when nothing exists + let result = provider.remove_safety_head_ref(SafetyLevel::CrossSafe); + assert!(result.is_ok()); + + // Still returns FutureData error + let result = provider.get_safety_head_ref(SafetyLevel::CrossSafe); + assert!(matches!(result, Err(StorageError::FutureData))); + } +} diff --git a/kona/crates/supervisor/storage/src/providers/log_provider.rs b/kona/crates/supervisor/storage/src/providers/log_provider.rs new file mode 100644 index 0000000000000..1f94c5ce40643 --- /dev/null +++ b/kona/crates/supervisor/storage/src/providers/log_provider.rs @@ -0,0 +1,783 @@ +//! Reth's MDBX-backed abstraction of [`LogProvider`] for superchain state. +//! +//! This module provides the [`LogProvider`] struct, which uses the +//! [`reth-db`] abstraction of reth to store execution logs +//! and block metadata required by the Optimism supervisor. +//! +//! It supports: +//! - Writing full blocks of logs with metadata +//! - Retrieving block metadata by number +//! - Finding a block from a specific log (with hash/index match) +//! - Fetching logs per block using dup-sorted key layout +//! +//! Logs are stored in [`LogEntries`] under dup-sorted tables, with log index +//! used as the subkey. Block metadata is stored in [`BlockRefs`]. + +use crate::{ + error::{EntryNotFoundError, StorageError}, + models::{BlockRefs, LogEntries}, +}; +use alloy_eips::BlockNumHash; +use alloy_primitives::ChainId; +use kona_protocol::BlockInfo; +use kona_supervisor_types::Log; +use reth_db_api::{ + cursor::{DbCursorRO, DbDupCursorRO, DbDupCursorRW}, + transaction::{DbTx, DbTxMut}, +}; + +use tracing::{debug, error, info, trace, warn}; + +const DEFAULT_LOG_INTERVAL: u64 = 100; + +/// A log storage that wraps a transactional reference to the MDBX backend. +#[derive(Debug)] +pub(crate) struct LogProvider<'tx, TX> { + tx: &'tx TX, + chain_id: ChainId, + #[doc(hidden)] + observability_interval: u64, +} + +impl<'tx, TX> LogProvider<'tx, TX> { + pub(crate) const fn new(tx: &'tx TX, chain_id: ChainId) -> Self { + Self::new_with_observability_interval(tx, chain_id, DEFAULT_LOG_INTERVAL) + } + + pub(crate) const fn new_with_observability_interval( + tx: &'tx TX, + chain_id: ChainId, + observability_interval: u64, + ) -> Self { + Self { tx, chain_id, observability_interval } + } +} + +impl LogProvider<'_, TX> +where + TX: DbTxMut + DbTx, +{ + pub(crate) fn initialise(&self, activation_block: BlockInfo) -> Result<(), StorageError> { + match self.get_block(0) { + Ok(block) if block == activation_block => Ok(()), + Ok(_) => Err(StorageError::ConflictError), + Err(StorageError::EntryNotFound(_)) => { + self.store_block_logs_internal(&activation_block, Vec::new()) + } + + Err(err) => Err(err), + } + } + + pub(crate) fn store_block_logs( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError> { + debug!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number = block.number, + "Storing logs", + ); + + let latest_block = match self.get_latest_block() { + Ok(block) => block, + Err(StorageError::EntryNotFound(_)) => return Err(StorageError::DatabaseNotInitialised), + Err(e) => return Err(e), + }; + + if latest_block.number >= block.number { + // If the latest block is ahead of the incoming block, it means + // the incoming block is old block, check if it is same as the stored block. + let stored_block = self.get_block(block.number)?; + if stored_block == *block { + return Ok(()); + } + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %stored_block, + incoming_block = %block, + "Incoming log block is not consistent with the stored log block", + ); + return Err(StorageError::ConflictError) + } + + if !latest_block.is_parent_of(block) { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %latest_block, + incoming_block = %block, + "Incoming block does not follow latest stored block" + ); + return Err(StorageError::BlockOutOfOrder); + } + + self.store_block_logs_internal(block, logs) + } + + fn store_block_logs_internal( + &self, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError> { + self.tx.put::(block.number, (*block).into()).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number = block.number, + %err, + "Failed to insert block" + ); + })?; + + let mut cursor = self.tx.cursor_dup_write::().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get dup cursor" + ); + })?; + + for log in logs { + cursor.append_dup(block.number, log.into()).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number = block.number, + %err, + "Failed to append logs" + ); + })?; + } + Ok(()) + } + + /// Rewinds the log storage by deleting all blocks and logs from the given block onward. + /// Fails if the given block exists with a mismatching hash (to prevent unsafe deletion). + pub(crate) fn rewind_to(&self, block: &BlockNumHash) -> Result<(), StorageError> { + info!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + target_block_hash = %block.hash, + "Starting rewind of log storage" + ); + + // Get the latest block number from BlockRefs + let latest_block = { + let mut cursor = self.tx.cursor_read::()?; + cursor.last()?.map(|(num, _)| num).unwrap_or(block.number) + }; + + // Check for future block + if block.number > latest_block { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + latest_block, + "Cannot rewind to future block" + ); + return Err(StorageError::FutureData); + } + + // total blocks to rewind down to and including tgt block + let total_blocks = latest_block - block.number + 1; + let mut processed_blocks = 0; + + // Delete all blocks and logs with number ≥ `block.number` + { + let mut cursor = self.tx.cursor_write::()?; + let mut walker = cursor.walk(Some(block.number))?; + + trace!( + target: "supervisor::storage", + chain_id = %self.chain_id, + target_block_number = %block.number, + target_block_hash = %block.hash, + latest_block, + total_blocks, + observability_interval = %self.observability_interval, + "Rewinding log storage..." + ); + + while let Some(Ok((key, stored_block))) = walker.next() { + if key == block.number && block.hash != stored_block.hash { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %stored_block, + incoming_block = ?block, + "Requested block to rewind does not match stored block" + ); + return Err(StorageError::ConflictError); + } + // remove the block + walker.delete_current()?; + + // remove the logs of that block + self.tx.delete::(key, None)?; + + processed_blocks += 1; + + // Log progress periodically or on last block + if processed_blocks % self.observability_interval == 0 || + processed_blocks == total_blocks + { + let percentage = if total_blocks > 0 { + (processed_blocks as f64 / total_blocks as f64 * 100.0).min(100.0) + } else { + 100.0 + }; + + info!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number = %key, + percentage = %format!("{:.2}%", percentage), + processed_blocks, + total_blocks, + "Rewind progress" + ); + } + } + + info!( + target: "supervisor::storage", + target_block_number = ?block.number, + target_block_hash = %block.hash, + chain_id = %self.chain_id, + total_blocks, + "Rewind completed successfully" + ); + } + + Ok(()) + } +} + +impl LogProvider<'_, TX> +where + TX: DbTx, +{ + pub(crate) fn get_block(&self, block_number: u64) -> Result { + debug!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + "Fetching block" + ); + + let block_option = self.tx.get::(block_number).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + %err, + "Failed to read block", + ); + })?; + + let block = block_option.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + "Block not found" + ); + EntryNotFoundError::DerivedBlockNotFound(block_number) + })?; + Ok(block.into()) + } + + pub(crate) fn get_latest_block(&self) -> Result { + debug!(target: "supervisor::storage", chain_id = %self.chain_id, "Fetching latest block"); + + let mut cursor = self.tx.cursor_read::().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get cursor" + ); + })?; + + let result = cursor.last().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to seek to last block" + ); + })?; + + let (_, block) = result.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + "No blocks found in storage" + ); + StorageError::DatabaseNotInitialised + })?; + Ok(block.into()) + } + + pub(crate) fn get_log(&self, block_number: u64, log_index: u32) -> Result { + debug!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + log_index, + "Fetching block by log" + ); + + let mut cursor = self.tx.cursor_dup_read::().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get cursor for LogEntries" + ); + })?; + + let result = cursor.seek_by_key_subkey(block_number, log_index).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + log_index, + %err, + "Failed to read log entry" + ); + })?; + + let log_entry = result.ok_or_else(|| { + warn!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + log_index, + "Log not found" + ); + EntryNotFoundError::LogNotFound { block_number, log_index } + })?; + + Ok(Log::from(log_entry)) + } + + pub(crate) fn get_logs(&self, block_number: u64) -> Result, StorageError> { + debug!(target: "supervisor::storage", chain_id = %self.chain_id, block_number, "Fetching logs"); + + let mut cursor = self.tx.cursor_dup_read::().inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + %err, + "Failed to get dup cursor" + ); + })?; + + let walker = cursor.walk_range(block_number..=block_number).inspect_err(|err| { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + %err, + "Failed to walk dup range", + ); + })?; + + let mut logs = Vec::new(); + for row in walker { + match row { + Ok((_, entry)) => logs.push(entry.into()), + Err(err) => { + error!( + target: "supervisor::storage", + chain_id = %self.chain_id, + block_number, + %err, + "Failed to read log entry", + ); + return Err(StorageError::Database(err)); + } + } + } + Ok(logs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Tables; + use alloy_primitives::B256; + use kona_cli::init_test_tracing; + use kona_protocol::BlockInfo; + use kona_supervisor_types::{ExecutingMessage, Log}; + use reth_db::{ + DatabaseEnv, + mdbx::{DatabaseArguments, init_db_for}, + }; + use reth_db_api::Database; + use tempfile::TempDir; + + static CHAIN_ID: ChainId = 1; + + fn genesis_block() -> BlockInfo { + BlockInfo { + hash: B256::from([0u8; 32]), + number: 0, + parent_hash: B256::ZERO, + timestamp: 100, + } + } + + fn sample_block_info(block_number: u64, parent_hash: B256) -> BlockInfo { + BlockInfo { + number: block_number, + hash: B256::from([0x11; 32]), + parent_hash, + timestamp: 123456, + } + } + + fn sample_log(log_index: u32, with_msg: bool) -> Log { + Log { + index: log_index, + hash: B256::from([log_index as u8; 32]), + executing_message: if with_msg { + Some(ExecutingMessage { + chain_id: 10, + block_number: 999, + log_index: 7, + hash: B256::from([0x44; 32]), + timestamp: 88888, + }) + } else { + None + }, + } + } + + /// Sets up a new temp DB + fn setup_db() -> DatabaseEnv { + let temp_dir = TempDir::new().expect("Could not create temp dir"); + init_db_for::<_, Tables>(temp_dir.path(), DatabaseArguments::default()) + .expect("Failed to init database") + } + + /// Helper to initialize database in a new transaction, committing if successful. + fn initialize_db(db: &DatabaseEnv, block: &BlockInfo) -> Result<(), StorageError> { + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = LogProvider::new(&tx, CHAIN_ID); + let res = provider.initialise(*block); + if res.is_ok() { + tx.commit().expect("Failed to commit transaction"); + } else { + tx.abort(); + } + res + } + + /// Helper to insert a pair in a new transaction, committing if successful. + fn insert_block_logs( + db: &DatabaseEnv, + block: &BlockInfo, + logs: Vec, + ) -> Result<(), StorageError> { + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = LogProvider::new(&tx, CHAIN_ID); + let res = provider.store_block_logs(block, logs); + if res.is_ok() { + tx.commit().expect("Failed to commit transaction"); + } + res + } + + #[test] + fn initialise_inserts_anchor_if_not_exists() { + let db = setup_db(); + let genesis = genesis_block(); + + // Should succeed and insert the anchor + assert!(initialize_db(&db, &genesis).is_ok()); + + // Check that the anchor is present + let tx = db.tx().expect("Could not get tx"); + let provider = LogProvider::new(&tx, CHAIN_ID); + let stored = provider.get_block(genesis.number).expect("should exist"); + assert_eq!(stored.hash, genesis.hash); + } + + #[test] + fn initialise_is_idempotent_if_anchor_matches() { + let db = setup_db(); + let genesis = genesis_block(); + + // First initialise + assert!(initialize_db(&db, &genesis).is_ok()); + + // Second initialise with the same anchor should succeed (idempotent) + assert!(initialize_db(&db, &genesis).is_ok()); + } + + #[test] + fn initialise_fails_if_anchor_mismatch() { + let db = setup_db(); + + // Initialize with the genesis block + let genesis = genesis_block(); + assert!(initialize_db(&db, &genesis).is_ok()); + + // Try to initialise with a different anchor (different hash) + let mut wrong_genesis = genesis; + wrong_genesis.hash = B256::from([42u8; 32]); + + let result = initialize_db(&db, &wrong_genesis); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn test_get_latest_block_empty() { + let db = setup_db(); + + let tx = db.tx().expect("Failed to start RO tx"); + let log_reader = LogProvider::new(&tx, CHAIN_ID); + + let result = log_reader.get_latest_block(); + assert!(matches!(result, Err(StorageError::DatabaseNotInitialised))); + } + + #[test] + fn test_storage_read_write_success() { + let db = setup_db(); + + // Initialize with genesis block + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB with genesis block"); + + let block1 = sample_block_info(1, genesis.hash); + let logs1 = vec![ + sample_log(0, false), + sample_log(1, true), + sample_log(3, false), + sample_log(4, true), + ]; + + // Store logs for block1 + assert!(insert_block_logs(&db, &block1, logs1.clone()).is_ok()); + + let block2 = sample_block_info(2, block1.hash); + let logs2 = vec![sample_log(0, false), sample_log(1, true)]; + + // Store logs for block2 + assert!(insert_block_logs(&db, &block2, logs2.clone()).is_ok()); + + let block3 = sample_block_info(3, block2.hash); + let logs3 = vec![sample_log(0, false), sample_log(1, true), sample_log(2, true)]; + + // Store logs for block3 + assert!(insert_block_logs(&db, &block3, logs3).is_ok()); + + let tx = db.tx().expect("Failed to start RO tx"); + let log_reader = LogProvider::new(&tx, CHAIN_ID); + + // get_block + let block = log_reader.get_block(block2.number).expect("Failed to get block"); + assert_eq!(block, block2); + + // get_latest_block + let block = log_reader.get_latest_block().expect("Failed to get latest block"); + assert_eq!(block, block3); + + // get log + let log = log_reader.get_log(1, 1).expect("Failed to get block by log"); + assert_eq!(log, logs1[1]); + + // get_logs + let logs = log_reader.get_logs(block2.number).expect("Failed to get logs"); + assert_eq!(logs.len(), 2); + assert_eq!(logs[0], logs2[0]); + assert_eq!(logs[1], logs2[1]); + } + + #[test] + fn test_not_found_error_and_empty_results() { + let db = setup_db(); + + let tx = db.tx().expect("Failed to start RO tx"); + let log_reader = LogProvider::new(&tx, CHAIN_ID); + + let result = log_reader.get_latest_block(); + assert!(matches!(result, Err(StorageError::DatabaseNotInitialised))); + + // Initialize with genesis block + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB with genesis block"); + + assert!( + insert_block_logs(&db, &sample_block_info(1, genesis.hash), vec![sample_log(0, true)]) + .is_ok() + ); + + let result = log_reader.get_block(2); + assert!(matches!(result, Err(StorageError::EntryNotFound(_)))); + + // should return empty logs but not an error + let logs = log_reader.get_logs(2).expect("Should not return error"); + assert_eq!(logs.len(), 0); + + let result = log_reader.get_log(1, 1); + assert!(matches!(result, Err(StorageError::EntryNotFound(_)))); + } + + #[test] + fn test_block_append_failed_on_order_mismatch() { + let db = setup_db(); + + // Initialize with genesis block + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB with genesis block"); + + let block1 = sample_block_info(1, genesis.hash); + let logs1 = vec![sample_log(0, false)]; + + let block2 = sample_block_info(3, genesis.hash); + let logs2 = vec![sample_log(0, false), sample_log(1, true)]; + + // Store logs + assert!(insert_block_logs(&db, &block1, logs1).is_ok()); + + let result = insert_block_logs(&db, &block2, logs2); + assert!(matches!(result, Err(StorageError::BlockOutOfOrder))); + } + + #[test] + fn store_block_logs_skips_if_block_already_exists() { + let db = setup_db(); + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB with genesis block"); + + let block1 = sample_block_info(1, genesis.hash); + let logs1 = vec![sample_log(0, false)]; + + // Store block1 for the first time + assert!(insert_block_logs(&db, &block1, logs1.clone()).is_ok()); + + // Try storing the same block again (should skip and succeed) + assert!(insert_block_logs(&db, &block1, logs1.clone()).is_ok()); + + // Try storing genesis block again (should skip and succeed) + assert!(insert_block_logs(&db, &genesis, Vec::new()).is_ok()); + + // Check that the logs are still present and correct + let tx = db.tx().expect("Failed to start RO tx"); + let log_reader = LogProvider::new(&tx, CHAIN_ID); + let logs = log_reader.get_logs(block1.number).expect("Should get logs"); + assert_eq!(logs, logs1); + } + + #[test] + fn store_block_logs_returns_conflict_if_block_exists_with_different_data() { + let db = setup_db(); + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB with genesis block"); + + let block1 = sample_block_info(1, genesis.hash); + let logs1 = vec![sample_log(0, false)]; + assert!(insert_block_logs(&db, &block1, logs1).is_ok()); + + // Try storing block1 again with a different hash (simulate conflict) + let mut block1_conflict = block1; + block1_conflict.hash = B256::from([0x22; 32]); + let logs1_conflict = vec![sample_log(0, false)]; + + let result = insert_block_logs(&db, &block1_conflict, logs1_conflict); + assert!(matches!(result, Err(StorageError::ConflictError))); + + // Try storing genesis block again with a different hash (simulate conflict) + let mut genesis_conflict = genesis; + genesis_conflict.hash = B256::from([0x33; 32]); + let result = insert_block_logs(&db, &genesis_conflict, Vec::new()); + assert!(matches!(result, Err(StorageError::ConflictError))); + } + + #[test] + fn test_rewind_to() { + init_test_tracing(); + + let db = setup_db(); + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB"); + + // Add 5 blocks with logs + let mut blocks = vec![genesis]; + for i in 1..=5 { + let prev = &blocks[i - 1]; + let block = sample_block_info(i as u64, prev.hash); + let logs = (0..3).map(|j| sample_log(j, j % 2 == 0)).collect(); + insert_block_logs(&db, &block, logs).expect("Failed to insert logs"); + blocks.push(block); + } + + // Rewind to block 3, blocks 3, 4, 5 should be removed + let tx = db.tx_mut().expect("Could not get mutable tx"); + let provider = LogProvider::new_with_observability_interval(&tx, CHAIN_ID, 1); + provider.rewind_to(&blocks[3].id()).expect("Failed to rewind blocks"); + tx.commit().expect("Failed to commit rewind"); + + let tx = db.tx().expect("Could not get RO tx"); + let provider = LogProvider::new_with_observability_interval(&tx, CHAIN_ID, 1); + + // Blocks 0,1,2 should still exist + for i in 0..=2 { + assert!(provider.get_block(i).is_ok(), "block {i} should exist after rewind"); + } + + // Logs for blocks 1,2 should exist + for i in 1..=2 { + let logs = provider.get_logs(i).expect("logs should exist"); + assert_eq!(logs.len(), 3, "block {i} should have 3 logs"); + } + + // Blocks 3,4,5 should be gone + for i in 3..=5 { + assert!( + matches!(provider.get_block(i), Err(StorageError::EntryNotFound(_))), + "block {i} should be removed" + ); + + let logs = provider.get_logs(i).expect("get_logs should not fail"); + assert!(logs.is_empty(), "logs for block {i} should be empty"); + } + } + + #[test] + fn test_rewind_to_conflict_hash() { + let db = setup_db(); + let genesis = genesis_block(); + initialize_db(&db, &genesis).expect("Failed to initialize DB"); + + // Insert block 1 + let block1 = sample_block_info(1, genesis.hash); + insert_block_logs(&db, &block1, vec![sample_log(0, true)]).expect("insert block 1"); + + // Create a conflicting block with the same number but different hash + let mut conflicting_block1 = block1; + conflicting_block1.hash = B256::from([0xAB; 32]); // different hash + + let tx = db.tx_mut().expect("Failed to get tx"); + let provider = LogProvider::new(&tx, CHAIN_ID); + + let result = provider.rewind_to(&conflicting_block1.id()); + assert!( + matches!(result, Err(StorageError::ConflictError)), + "Expected conflict error due to hash mismatch" + ); + } +} diff --git a/kona/crates/supervisor/storage/src/providers/mod.rs b/kona/crates/supervisor/storage/src/providers/mod.rs new file mode 100644 index 0000000000000..fb8aaeb54342f --- /dev/null +++ b/kona/crates/supervisor/storage/src/providers/mod.rs @@ -0,0 +1,15 @@ +//! Providers for supervisor state tracking. +//! +//! This module defines and implements storage providers used by the supervisor +//! for managing L2 execution state. It includes support for reading and writing: +//! - Logs and block metadata (via [`LogProvider`]) +//! - Derivation pipeline state (via [`DerivationProvider`]) +//! - Chain head tracking and progression +mod derivation_provider; +pub(crate) use derivation_provider::DerivationProvider; + +mod log_provider; +pub(crate) use log_provider::LogProvider; + +mod head_ref_provider; +pub(crate) use head_ref_provider::SafetyHeadRefProvider; diff --git a/kona/crates/supervisor/storage/src/traits.rs b/kona/crates/supervisor/storage/src/traits.rs new file mode 100644 index 0000000000000..b7a527ccfa199 --- /dev/null +++ b/kona/crates/supervisor/storage/src/traits.rs @@ -0,0 +1,475 @@ +use crate::StorageError; +use alloy_eips::eip1898::BlockNumHash; +use alloy_primitives::ChainId; +use kona_interop::DerivedRefPair; +use kona_protocol::BlockInfo; +use kona_supervisor_types::{Log, SuperHead}; +use op_alloy_consensus::interop::SafetyLevel; +use std::fmt::Debug; + +/// Provides an interface for supervisor storage to manage source and derived blocks. +/// +/// Defines methods to retrieve derived block information, +/// enabling the supervisor to track the derivation progress. +/// +/// Implementations are expected to provide persistent and thread-safe access to block data. +pub trait DerivationStorageReader: Debug { + /// Gets the source [`BlockInfo`] for a given derived block [`BlockNumHash`]. + /// + /// NOTE: [`LocalUnsafe`] block is not pushed to L1 yet, hence it cannot be part of derivation + /// storage. + /// + /// # Arguments + /// * `derived_block_id` - The identifier (number and hash) of the derived (L2) block. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the source block information if it exists. + /// * `Err(StorageError)` if there is an issue retrieving the source block. + /// + /// [`LocalUnsafe`]: SafetyLevel::LocalUnsafe + fn derived_to_source(&self, derived_block_id: BlockNumHash) -> Result; + + /// Gets the latest derived [`BlockInfo`] associated with the given source block + /// [`BlockNumHash`]. + /// + /// # Arguments + /// * `source_block_id` - The identifier (number and hash) of the L1 source block. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the latest derived block information if it exists. + /// * `Err(StorageError)` if there is an issue retrieving the derived block. + fn latest_derived_block_at_source( + &self, + source_block_id: BlockNumHash, + ) -> Result; + + /// Gets the latest derivation state [`DerivedRefPair`] from the storage, which includes the + /// latest source block and the latest derived block. + /// + /// # Returns + /// + /// * `Ok(DerivedRefPair)` containing the latest derived block pair if it exists. + /// * `Err(StorageError)` if there is an issue retrieving the pair. + fn latest_derivation_state(&self) -> Result; + + /// Gets the source block for the given source block number. + /// + /// # Arguments + /// * `source_block_number` - The number of the source block to retrieve. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the source block information if it exists. + /// * `Err(StorageError)` if there is an issue retrieving the source block. + fn get_source_block(&self, source_block_number: u64) -> Result; + + /// Gets the interop activation [`BlockInfo`]. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the activation block information if it exists. + /// * `Err(StorageError)` if there is an issue retrieving the activation block. + fn get_activation_block(&self) -> Result; +} + +/// Provides an interface for supervisor storage to write source and derived blocks. +/// +/// Defines methods to persist derived block information, +/// enabling the supervisor to track the derivation progress. +/// +/// Implementations are expected to provide persistent and thread-safe access to block data. +pub trait DerivationStorageWriter: Debug { + /// Initializes the derivation storage with a given [`DerivedRefPair`]. + /// This method is typically called once to set up the storage with the initial pair. + /// + /// # Arguments + /// * `incoming_pair` - The derived block pair to initialize the storage with. + /// + /// # Returns + /// * `Ok(())` if the storage was successfully initialized. + /// * `Err(StorageError)` if there is an issue initializing the storage. + fn initialise_derivation_storage( + &self, + incoming_pair: DerivedRefPair, + ) -> Result<(), StorageError>; + + /// Saves a [`DerivedRefPair`] to the storage. + /// + /// This method is **append-only**: it does not overwrite existing pairs. + /// - If a pair with the same block number already exists and is identical to the incoming pair, + /// the request is silently ignored (idempotent). + /// - If a pair with the same block number exists but differs from the incoming pair, an error + /// is returned to indicate a data inconsistency. + /// - If the pair is new and consistent, it is appended to the storage. + /// + /// Ensures that the latest stored pair is the parent of the incoming pair before saving. + /// + /// # Arguments + /// * `incoming_pair` - The derived block pair to save. + /// + /// # Returns + /// * `Ok(())` if the pair was successfully saved. + /// * `Err(StorageError)` if there is an issue saving the pair. + fn save_derived_block(&self, incoming_pair: DerivedRefPair) -> Result<(), StorageError>; + + /// Saves the latest incoming source [`BlockInfo`] to the storage. + /// + /// This method is **append-only**: it does not overwrite existing source blocks. + /// - If a source block with the same number already exists and is identical to the incoming + /// block, the request is silently ignored (idempotent). + /// - If a source block with the same number exists but differs from the incoming block, an + /// error is returned to indicate a data inconsistency. + /// - If the block is new and consistent, it is appended to the storage. + /// + /// Ensures that the latest stored source block is the parent of the incoming block before + /// saving. + /// + /// # Arguments + /// * `source` - The source block to save. + /// + /// # Returns + /// * `Ok(())` if the source block was successfully saved. + /// * `Err(StorageError)` if there is an issue saving the source block. + fn save_source_block(&self, source: BlockInfo) -> Result<(), StorageError>; +} + +/// Combines both reading and writing capabilities for derivation storage. +/// +/// Any type that implements both [`DerivationStorageReader`] and [`DerivationStorageWriter`] +/// automatically implements this trait. +pub trait DerivationStorage: DerivationStorageReader + DerivationStorageWriter {} + +impl DerivationStorage for T {} + +/// Provides an interface for retrieving logs associated with blocks. +/// +/// This trait defines methods to retrieve the latest block, +/// find a block by a specific log, and retrieve logs for a given block number. +/// +/// Implementations are expected to provide persistent and thread-safe access to block logs. +pub trait LogStorageReader: Debug { + /// Retrieves the latest [`BlockInfo`] from the storage. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the latest block information. + /// * `Err(StorageError)` if there is an issue retrieving the latest block. + fn get_latest_block(&self) -> Result; + + /// Retrieves the [`BlockInfo`] from the storage for a given block number + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the block information. + /// * `Err(StorageError)` if there is an issue retrieving the block. + fn get_block(&self, block_number: u64) -> Result; + + /// Finds a [`Log`] by block_number and log_index + /// + /// # Arguments + /// * `block_number` - The block number to search for the log. + /// * `log_index` - The index of the log within the block. + /// + /// # Returns + /// * `Ok(Log)` containing the [`Log`] object. + /// * `Err(StorageError)` if there is an issue retrieving the log or if the log is not found. + fn get_log(&self, block_number: u64, log_index: u32) -> Result; + + /// Retrieves all [`Log`]s associated with a specific block number. + /// + /// # Arguments + /// * `block_number` - The block number for which to retrieve logs. + /// + /// # Returns + /// * `Ok(Vec)` containing the logs associated with the block number. + /// * `Err(StorageError)` if there is an issue retrieving the logs or if no logs are found. + fn get_logs(&self, block_number: u64) -> Result, StorageError>; +} + +/// Provides an interface for storing blocks and logs associated with blocks. +/// +/// Implementations are expected to provide persistent and thread-safe access to block logs. +pub trait LogStorageWriter: Send + Sync + Debug { + /// Initializes the log storage with a given [`BlockInfo`]. + /// This method is typically called once to set up the storage with the initial block. + /// + /// # Arguments + /// * `block` - The [`BlockInfo`] to initialize the storage with. + /// + /// # Returns + /// * `Ok(())` if the storage was successfully initialized. + /// * `Err(StorageError)` if there is an issue initializing the storage. + fn initialise_log_storage(&self, block: BlockInfo) -> Result<(), StorageError>; + + /// Stores [`BlockInfo`] and [`Log`]s in the storage. + /// This method is append-only and does not overwrite existing logs. + /// Ensures that the latest stored block is the parent of the incoming block before saving. + /// + /// # Arguments + /// * `block` - [`BlockInfo`] to associate with the logs. + /// * `logs` - The [`Log`] events associated with the block. + /// + /// # Returns + /// * `Ok(())` if the logs were successfully stored. + /// * `Err(StorageError)` if there is an issue storing the logs. + fn store_block_logs(&self, block: &BlockInfo, logs: Vec) -> Result<(), StorageError>; +} + +/// Combines both reading and writing capabilities for log storage. +/// +/// Any type that implements both [`LogStorageReader`] and [`LogStorageWriter`] +/// automatically implements this trait. +pub trait LogStorage: LogStorageReader + LogStorageWriter {} + +impl LogStorage for T {} + +/// Provides an interface for retrieving head references. +/// +/// This trait defines methods to manage safety head references for different safety levels. +/// Each safety level maintains a reference to a block. +/// +/// Implementations are expected to provide persistent and thread-safe access to safety head +/// references. +pub trait HeadRefStorageReader: Debug { + /// Retrieves the current [`BlockInfo`] for a given [`SafetyLevel`]. + /// + /// # Arguments + /// * `safety_level` - The safety level for which to retrieve the head reference. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the current safety head reference. + /// * `Err(StorageError)` if there is an issue retrieving the reference. + fn get_safety_head_ref(&self, safety_level: SafetyLevel) -> Result; + + /// Retrieves the super head reference from the storage. + /// + /// # Returns + /// * `Ok(SuperHead)` containing the super head reference. + /// * `Err(StorageError)` if there is an issue retrieving the super head reference. + fn get_super_head(&self) -> Result; +} + +/// Provides an interface for storing head references. +/// +/// This trait defines methods to manage safety head references for different safety levels. +/// Each safety level maintains a reference to a block. +/// +/// Implementations are expected to provide persistent and thread-safe access to safety head +/// references. +pub trait HeadRefStorageWriter: Debug { + /// Updates the finalized head reference using a finalized source(l1) block. + /// + /// # Arguments + /// * `source_block` - The [`BlockInfo`] of the source block to use for the update. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the updated finalized derived(l2) block information. + /// * `Err(StorageError)` if there is an issue updating the finalized head reference. + fn update_finalized_using_source( + &self, + finalized_source_block: BlockInfo, + ) -> Result; + + /// Updates the current [`CrossUnsafe`](SafetyLevel::CrossUnsafe) head reference in storage. + /// + /// Ensures the provided block still exists in log storage and was not removed due to a re-org. + /// If the stored block's hash does not match the provided block, the update is aborted. + /// # Arguments + /// * `block` - The [`BlockInfo`] to set as the head reference + /// + /// # Returns + /// * `Ok(())` if the reference was successfully updated. + /// * `Err(StorageError)` if there is an issue updating the reference. + fn update_current_cross_unsafe(&self, block: &BlockInfo) -> Result<(), StorageError>; + + /// Updates the current [`CrossSafe`](SafetyLevel::CrossSafe) head reference in storage and + /// returns the corresponding derived pair. + /// + /// Ensures the provided block still exists in derivation storage and was not removed due to a + /// re-org. # Arguments + /// * `block` - The [`BlockInfo`] to set as the head reference + /// + /// # Returns + /// * `Ok(DerivedRefPair)` if the reference was successfully updated. + /// * `Err(StorageError)` if there is an issue updating the reference. + fn update_current_cross_safe(&self, block: &BlockInfo) -> Result; +} + +/// Combines both reading and writing capabilities for safety head ref storage. +/// +/// Any type that implements both [`HeadRefStorageReader`] and [`HeadRefStorageWriter`] +/// automatically implements this trait. +pub trait HeadRefStorage: HeadRefStorageReader + HeadRefStorageWriter {} + +impl HeadRefStorage for T {} + +/// Provides an interface for managing the finalized L1 block reference in the storage. +/// +/// This trait defines methods to update and retrieve the finalized L1 block reference. +pub trait FinalizedL1Storage { + /// Updates the finalized L1 block reference in the storage. + /// + /// # Arguments + /// * `block` - The new [`BlockInfo`] to set as the finalized L1 block reference. + /// + /// # Returns + /// * `Ok(())` if the reference was successfully updated. + /// * `Err(StorageError)` if there is an issue updating the reference. + fn update_finalized_l1(&self, block: BlockInfo) -> Result<(), StorageError>; + + /// Retrieves the finalized L1 block reference from the storage. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the finalized L1 block reference. + /// * `Err(StorageError)` if there is an issue retrieving the reference. + fn get_finalized_l1(&self) -> Result; +} + +/// Provides an interface for retrieving block and safety information across multiple chains. +/// +/// This trait defines methods required by the cross-chain safety checker to access +/// block metadata, logs, and safe head references for various chains. +pub trait CrossChainSafetyProvider { + /// Retrieves the [`BlockInfo`] for a given block number on the specified chain. + /// + /// # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `block_number` - The number of the block to retrieve. + /// + /// # Returns + /// * `Ok(BlockInfo)` containing the block metadata if available. + /// * `Err(StorageError)` if there is an issue fetching the block. + fn get_block(&self, chain_id: ChainId, block_number: u64) -> Result; + + /// Retrieves a [`Log`] by block_number and log_index + /// + /// # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `block_number` - The block number to search for the log. + /// * `log_index` - The index of the log within the block. + /// + /// # Returns + /// * `Ok(Log)` containing the [`Log`] object. + /// * `Err(StorageError)` if there is an issue retrieving the log or if the log is not found. + fn get_log( + &self, + chain_id: ChainId, + block_number: u64, + log_index: u32, + ) -> Result; + + /// Retrieves all logs associated with the specified block on the given chain. + /// + /// # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `block_number` - The number of the block whose logs should be retrieved. + /// + /// # Returns + /// * `Ok(Vec)` containing all logs for the block. + /// * `Err(StorageError)` if there is an issue fetching the logs. + fn get_block_logs( + &self, + chain_id: ChainId, + block_number: u64, + ) -> Result, StorageError>; + + /// Retrieves the latest known safe head reference for a given chain at the specified safety + /// level. + /// + /// # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `level` - The desired [`SafetyLevel`] (e.g., `CrossSafe`, `LocalSafe`). + /// + /// # Returns + /// * `Ok(BlockInfo)` representing the safe head block at the requested safety level. + /// * `Err(StorageError)` if the safe head cannot be retrieved. + fn get_safety_head_ref( + &self, + chain_id: ChainId, + level: SafetyLevel, + ) -> Result; + + /// Updates the current [`CrossUnsafe`](SafetyLevel::CrossUnsafe) head reference in storage. + /// + /// Ensures the provided block still exists in log storage and was not removed due to a re-org. + /// If the stored block's hash does not match the provided block, the update is aborted. + /// # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `block` - The [`BlockInfo`] to set as the head reference + /// + /// # Returns + /// * `Ok(())` if the reference was successfully updated. + /// * `Err(StorageError)` if there is an issue updating the reference. + fn update_current_cross_unsafe( + &self, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result<(), StorageError>; + + /// Updates the current [`CrossSafe`](SafetyLevel::CrossSafe) head reference in storage and + /// returns the corresponding derived pair. + /// + /// Ensures the provided block still exists in derivation storage and was not removed due to a + /// re-org. # Arguments + /// * `chain_id` - The [`ChainId`] of the target chain. + /// * `block` - The [`BlockInfo`] to set as the head reference + /// + /// # Returns + /// * `Ok(DerivedRefPair)` if the reference was successfully updated. + /// * `Err(StorageError)` if there is an issue updating the reference. + fn update_current_cross_safe( + &self, + chain_id: ChainId, + block: &BlockInfo, + ) -> Result; +} + +/// Trait for rewinding supervisor-related state in the database. +/// +/// This trait provides an interface to revert persisted log data, derivation records, +/// and safety head references from the latest block back to a specified block number (inclusive). +/// It is typically used during chain reorganizations or when invalid blocks are detected and need +/// to be rolled back. +pub trait StorageRewinder { + /// Rewinds the log storage from the latest block down to the specified block (inclusive). + /// This method ensures that log storage is never rewound to(since it's inclusive) and beyond + /// the local safe head. If the target block is beyond the local safe head, an error is + /// returned. Use [`StorageRewinder::rewind`] to rewind to and beyond the local safe head. + /// + /// # Arguments + /// * `to` - The block id to rewind to. + /// + /// # Errors + /// Returns a [`StorageError`] if any database operation fails during the rewind. + fn rewind_log_storage(&self, to: &BlockNumHash) -> Result<(), StorageError>; + + /// Rewinds all supervisor-managed state (log storage, derivation, and safety head refs) + /// from the latest block back to the given block (inclusive). + /// + /// This method performs a coordinated rewind across all components, ensuring consistency + /// of supervisor state after chain reorganizations or rollback of invalid blocks. + /// + /// # Arguments + /// * `to` - The target block id to rewind to. Rewind is performed from the latest block down to + /// this block. + /// + /// # Errors + /// Returns a [`StorageError`] if any part of the rewind process fails. + fn rewind(&self, to: &BlockNumHash) -> Result<(), StorageError>; + + /// Rewinds the storage to a specific source block (inclusive), ensuring that all derived blocks + /// and logs associated with that source blocks are also reverted. + /// + /// # Arguments + /// * `to` - The source block [`BlockNumHash`] to rewind to. + /// + /// # Returns + /// * [`BlockInfo`] of the derived block that was rewound to, or `None` if no derived blocks + /// were found. + /// * `Err(StorageError)` if there is an issue during the rewind operation. + fn rewind_to_source(&self, to: &BlockNumHash) -> Result, StorageError>; +} + +/// Combines the reader traits for the database. +/// +/// Any type that implements [`DerivationStorageReader`], [`HeadRefStorageReader`], and +/// [`LogStorageReader`] automatically implements this trait. +pub trait DbReader: DerivationStorageReader + HeadRefStorageReader + LogStorageReader {} + +impl DbReader for T {} diff --git a/kona/crates/supervisor/types/Cargo.toml b/kona/crates/supervisor/types/Cargo.toml new file mode 100644 index 0000000000000..8c98cd0964004 --- /dev/null +++ b/kona/crates/supervisor/types/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "kona-supervisor-types" +description = "Types used by the OP Stack supervisor" +version = "0.1.1" + +edition.workspace = true +license.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] + +# workspace +kona-interop = { workspace = true, features = ["serde"] } +kona-protocol = { workspace = true, features = ["serde"] } + +# alloy +alloy-eips.workspace = true +alloy-primitives = { workspace = true, features = ["map", "rlp", "serde"] } +alloy-serde = { workspace = true } + +# op-alloy +op-alloy-consensus.workspace = true + +# general +serde.workspace = true +derive_more = { workspace = true, default-features = false, features = ["constructor"] } +thiserror = {workspace = true} + +[dev-dependencies] +serde_json.workspace = true diff --git a/kona/crates/supervisor/types/README.md b/kona/crates/supervisor/types/README.md new file mode 100644 index 0000000000000..fc2d95572744d --- /dev/null +++ b/kona/crates/supervisor/types/README.md @@ -0,0 +1 @@ +## `kona-supervisor-types` \ No newline at end of file diff --git a/kona/crates/supervisor/types/src/access_list.rs b/kona/crates/supervisor/types/src/access_list.rs new file mode 100644 index 0000000000000..9371e7dbe8b6f --- /dev/null +++ b/kona/crates/supervisor/types/src/access_list.rs @@ -0,0 +1,396 @@ +use alloy_primitives::{B256, keccak256}; +use thiserror::Error; + +/// A structured representation of a parsed CrossL2Inbox message access entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Access { + /// Full 256-bit chain ID (combined from lookup + extension) + pub chain_id: [u8; 32], + /// Block number in the source chain + pub block_number: u64, + /// Timestamp of the message's block + pub timestamp: u64, + /// Log index of the message within the block + pub log_index: u32, + /// Provided checksum entry (prefix 0x03) + pub checksum: B256, +} + +impl Access { + /// Constructs a new [`Access`] from a `LookupEntry`, optional `ChainIdExtensionEntry`, + /// and a `ChecksumEntry`. Used internally by the parser. + fn from_entries( + lookup: LookupEntry, + chain_id_ext: Option, + checksum: ChecksumEntry, + ) -> Self { + let mut chain_id = [0u8; 32]; + + if let Some(ext) = chain_id_ext { + chain_id[0..24].copy_from_slice(&ext.upper_bytes); + } + + chain_id[24..32].copy_from_slice(&lookup.chain_id_low); + + Self { + chain_id, + block_number: lookup.block_number, + timestamp: lookup.timestamp, + log_index: lookup.log_index, + checksum: checksum.raw, + } + } + + /// Recomputes the checksum for this access entry. + /// + /// This follows the spec: + /// - `idPacked = 12 zero bytes ++ block_number ++ timestamp ++ log_index` + /// - `idLogHash = keccak256(log_hash ++ idPacked)` + /// - `bareChecksum = keccak256(idLogHash ++ chain_id)` + /// - Prepend 0x03 to `bareChecksum[1..]` + /// + /// Returns the full 32-byte checksum with prefix 0x03. + /// + /// Reference: [Checksum Calculation](https://github.com/ethereum-optimism/specs/blob/main/specs/interop/predeploys.md#type-3-checksum) + pub fn recompute_checksum(&self, log_hash: &B256) -> B256 { + // Step 1: idPacked = [0u8; 12] ++ block_number ++ timestamp ++ log_index + let mut id_packed = [0u8; 12 + 8 + 8 + 4]; // 32 bytes + id_packed[12..20].copy_from_slice(&self.block_number.to_be_bytes()); + id_packed[20..28].copy_from_slice(&self.timestamp.to_be_bytes()); + id_packed[28..32].copy_from_slice(&self.log_index.to_be_bytes()); + + // Step 2: keccak256(log_hash ++ id_packed) + let id_log_hash = keccak256([log_hash.as_slice(), &id_packed].concat()); + + // Step 3: keccak256(id_log_hash ++ chain_id) + let bare_checksum = keccak256([id_log_hash.as_slice(), &self.chain_id].concat()); + + // Step 4: Prepend type byte 0x03 (overwrite first byte) + let mut checksum = bare_checksum; + checksum.0[0] = 0x03; + + checksum + } + + /// Verify the checksums after recalculation + pub fn verify_checksum(&self, log_hash: &B256) -> Result<(), AccessListError> { + if self.recompute_checksum(log_hash) != self.checksum { + return Err(AccessListError::MalformedEntry); + } + Ok(()) + } +} + +/// Represents a single entry in the access list. +#[derive(Debug, Clone)] +enum AccessListEntry { + Lookup(LookupEntry), + ChainIdExtension(ChainIdExtensionEntry), + Checksum(ChecksumEntry), +} + +/// Parsed lookup identity entry (type 0x01). +#[derive(Debug, Clone)] +struct LookupEntry { + pub chain_id_low: [u8; 8], + pub block_number: u64, + pub timestamp: u64, + pub log_index: u32, +} + +/// Parsed Chain ID extension entry (type 0x02). +#[derive(Debug, Clone)] +struct ChainIdExtensionEntry { + pub upper_bytes: [u8; 24], +} + +/// Parsed checksum entry (type 0x03). +#[derive(Debug, Clone)] +struct ChecksumEntry { + pub raw: B256, +} + +/// Error returned when access list parsing fails. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum AccessListError { + /// Input ended before a complete message group was parsed. + #[error("unexpected end of access list")] + UnexpectedEnd, + + /// Unexpected entry type found. + #[error("expected type {expected:#x}, got {found:#x}")] + UnexpectedType { + /// The type we expected (e.g. 0x01, 0x02, or 0x03) + expected: u8, + /// The actual type byte we found + found: u8, + }, + + /// Malformed entry sequence or invalid prefix structure. + #[error("malformed entry")] + MalformedEntry, + + /// Message expired. + #[error("message expired")] + MessageExpired, + + /// Timestamp invariant violated. + #[error("executing timestamp is earlier than initiating timestamp")] + InvalidTimestampInvariant, +} + +// Access list entry type byte constants +const PREFIX_LOOKUP: u8 = 0x01; +const PREFIX_CHAIN_ID_EXTENSION: u8 = 0x02; +const PREFIX_CHECKSUM: u8 = 0x03; + +/// Parses a vector of raw `B256` access list entries into structured [`Access`] objects. +/// +/// Each `Access` group must follow the pattern: +/// - One `Lookup` entry (prefix `0x01`) +/// - Optionally one `ChainIdExtension` entry (prefix `0x02`) +/// - One `Checksum` entry (prefix `0x03`) +/// +/// Entries are consumed in order. If any group is malformed, this function returns a +/// [`AccessListError`]. +/// +/// # Arguments +/// +/// * `entries` - A `Vec` representing the raw access list entries. +/// +/// # Returns +/// +/// A vector of fully parsed [`Access`] items if all entries are valid. +/// +/// # Errors +/// +/// Returns [`AccessListError`] if entries are out-of-order, malformed, or incomplete. +pub fn parse_access_list(entries: Vec) -> Result, AccessListError> { + let mut list = Vec::with_capacity(entries.len() / 2); + let mut lookup_entry: Option = None; + let mut chain_id_ext: Option = None; + + for entry in entries { + let parsed = parse_entry(&entry)?; + + match parsed { + AccessListEntry::Lookup(lookup) => { + if lookup_entry.is_some() { + return Err(AccessListError::MalformedEntry); + } + lookup_entry = Some(lookup); + } + + AccessListEntry::ChainIdExtension(ext) => { + if lookup_entry.is_none() || chain_id_ext.is_some() { + return Err(AccessListError::MalformedEntry); + } + chain_id_ext = Some(ext); + } + + AccessListEntry::Checksum(checksum) => { + let lookup = lookup_entry.take().ok_or(AccessListError::MalformedEntry)?; + let access = Access::from_entries(lookup, chain_id_ext.take(), checksum); + list.push(access); + } + } + } + + if lookup_entry.is_some() { + return Err(AccessListError::UnexpectedEnd); + } + + Ok(list) +} + +/// Parses a single 32-byte access list entry into a typed [`AccessListEntry`]. +/// +/// This function performs a prefix-based decoding of the input hash: +/// +/// ### Entry Type Encoding +/// +/// | Prefix Byte | Type | Description | +/// |-------------|------------------------|-------------------------------------------------------------------| +/// | `0x01` | `LookupEntry` | Contains chain ID (low bits), block number, timestamp, log index. | +/// | `0x02` | `ChainIdExtensionEntry`| Contains upper 24 bytes of a 256-bit chain ID. | +/// | `0x03` | `ChecksumEntry` | Contains the checksum hash used for message validation. | +/// +/// ### Spec References +/// +/// - [Optimism Access List Format](https://github.com/ethereum-optimism/specs/blob/main/specs/interop/predeploys.md#access-list) +/// - Entry format and layout based on CrossL2Inbox access-list encoding. +fn parse_entry(entry: &B256) -> Result { + match entry[0] { + PREFIX_LOOKUP => { + if entry[1..4] != [0; 3] { + return Err(AccessListError::MalformedEntry); + } + Ok(AccessListEntry::Lookup(LookupEntry { + chain_id_low: entry[4..12].try_into().unwrap(), + block_number: u64::from_be_bytes(entry[12..20].try_into().unwrap()), + timestamp: u64::from_be_bytes(entry[20..28].try_into().unwrap()), + log_index: u32::from_be_bytes(entry[28..32].try_into().unwrap()), + })) + } + + PREFIX_CHAIN_ID_EXTENSION => { + if entry[1..8] != [0; 7] { + return Err(AccessListError::MalformedEntry); + } + Ok(AccessListEntry::ChainIdExtension(ChainIdExtensionEntry { + upper_bytes: entry[8..32].try_into().unwrap(), + })) + } + + PREFIX_CHECKSUM => Ok(AccessListEntry::Checksum(ChecksumEntry { raw: *entry })), + + other => Err(AccessListError::UnexpectedType { expected: PREFIX_LOOKUP, found: other }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{B256, U256, b256}; + + fn make_lookup_entry( + block_number: u64, + timestamp: u64, + log_index: u32, + chain_id_low: [u8; 8], + ) -> B256 { + let mut buf = [0u8; 32]; + buf[0] = PREFIX_LOOKUP; + // 3 zero padding + buf[4..12].copy_from_slice(&chain_id_low); + buf[12..20].copy_from_slice(&block_number.to_be_bytes()); + buf[20..28].copy_from_slice(×tamp.to_be_bytes()); + buf[28..32].copy_from_slice(&log_index.to_be_bytes()); + B256::from(buf) + } + + fn make_chain_id_ext(upper: [u8; 24]) -> B256 { + let mut buf = [0u8; 32]; + buf[0] = PREFIX_CHAIN_ID_EXTENSION; + // 7 zero padding + buf[8..32].copy_from_slice(&upper); + B256::from(buf) + } + + fn make_checksum(access: &Access, log_hash: &B256) -> B256 { + access.recompute_checksum(log_hash) + } + + #[test] + fn test_parse_valid_access_list_with_chain_id_ext() { + let block_number = 1234; + let timestamp = 9999; + let log_index = 5; + let chain_id_low = [1u8; 8]; + let upper_bytes = [2u8; 24]; + let log_hash = keccak256([0u8; 32]); + + let lookup = make_lookup_entry(block_number, timestamp, log_index, chain_id_low); + let chain_ext = make_chain_id_ext(upper_bytes); + + let access = Access::from_entries( + LookupEntry { chain_id_low, block_number, timestamp, log_index }, + Some(ChainIdExtensionEntry { upper_bytes }), + ChecksumEntry { + raw: B256::default(), // will override later + }, + ); + + let checksum = make_checksum(&access, &log_hash); + + let access = Access::from_entries( + LookupEntry { chain_id_low, block_number, timestamp, log_index }, + Some(ChainIdExtensionEntry { upper_bytes }), + ChecksumEntry { raw: checksum }, + ); + + let list = vec![lookup, chain_ext, checksum]; + let parsed = parse_access_list(list).unwrap(); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0], access); + assert!(parsed[0].verify_checksum(&log_hash).is_ok()); + } + + #[test] + fn test_parse_access_list_without_chain_id_ext() { + let block_number = 1; + let timestamp = 2; + let log_index = 3; + let chain_id_low = [0xaa; 8]; + let log_hash = keccak256([1u8; 32]); + + let lookup = make_lookup_entry(block_number, timestamp, log_index, chain_id_low); + let access = Access::from_entries( + LookupEntry { chain_id_low, block_number, timestamp, log_index }, + None, + ChecksumEntry { raw: B256::default() }, + ); + let checksum = make_checksum(&access, &log_hash); + let access = Access::from_entries( + LookupEntry { chain_id_low, block_number, timestamp, log_index }, + None, + ChecksumEntry { raw: checksum }, + ); + + let list = vec![lookup, checksum]; + let parsed = parse_access_list(list).unwrap(); + assert_eq!(parsed.len(), 1); + assert_eq!(parsed[0], access); + assert!(parsed[0].verify_checksum(&log_hash).is_ok()); + } + + #[test] + fn test_recompute_checksum_against_known_value() { + // Input data + let access = Access { + chain_id: U256::from(3).to_be_bytes(), + block_number: 2587, + timestamp: 4660, + log_index: 66, + checksum: B256::default(), // not used in this test + }; + + let log_hash = b256!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + + // Expected checksum computed previously using spec logic + let expected = b256!("0x03ca886771056d8ea647bb809b888ba14986f57daaf28954d40408321717716a"); + + let computed = access.recompute_checksum(&log_hash); + assert_eq!(computed, expected, "Checksum does not match expected value"); + } + + #[test] + fn test_checksum_mismatch() { + let block_number = 1; + let timestamp = 2; + let log_index = 3; + let chain_id_low = [0xaa; 8]; + let log_hash = keccak256([1u8; 32]); + + let lookup = make_lookup_entry(block_number, timestamp, log_index, chain_id_low); + let fake_checksum = + b256!("0x03ca886771056d8ea647bb809b888ba14986f57daaf28954d40408321717716a"); + let list = vec![lookup, fake_checksum]; + + let parsed = parse_access_list(list).unwrap(); + let err = parsed[0].verify_checksum(&log_hash); + assert_eq!(err, Err(AccessListError::MalformedEntry)); + } + + #[test] + fn test_invalid_entry_order_should_fail() { + let mut raw = [0u8; 32]; + raw[0] = PREFIX_CHECKSUM; + let checksum = B256::from(raw); + + let lookup = make_lookup_entry(0, 0, 0, [0u8; 8]); + let entries = vec![checksum, lookup]; + + assert!(matches!(parse_access_list(entries), Err(AccessListError::MalformedEntry))); + } +} diff --git a/kona/crates/supervisor/types/src/head.rs b/kona/crates/supervisor/types/src/head.rs new file mode 100644 index 0000000000000..6fa4b5ebbf0d2 --- /dev/null +++ b/kona/crates/supervisor/types/src/head.rs @@ -0,0 +1,36 @@ +//! Head of chain in context of superchain. + +use kona_protocol::BlockInfo; + +/// Head of a chain from superchain perspective. +/// +/// In context of a single chain, canonical head is tracked by its safe and finalized head. In +/// superchain context, earlier finality-stages (aka [`SafetyLevel`]s) are tracked too, i.e. +/// unsafe, cross-unsafe and local-safe heads. +/// +/// [`SafetyLevel`]: op_alloy_consensus::interop::SafetyLevel +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SuperHead { + /// Source (L1) block. + pub l1_source: Option, + /// [`Unsafe`] head of chain. + /// + /// [`Unsafe`]: op_alloy_consensus::interop::SafetyLevel::LocalUnsafe + pub local_unsafe: BlockInfo, + /// [`CrossUnsafe`] head of chain. + /// + /// [`CrossUnsafe`]: op_alloy_consensus::interop::SafetyLevel::CrossUnsafe + pub cross_unsafe: Option, + /// [`LocalSafe`] head of chain. + /// + /// [`LocalSafe`]: op_alloy_consensus::interop::SafetyLevel::LocalSafe + pub local_safe: Option, + /// [`Safe`] head of chain. + /// + /// [`Safe`]: op_alloy_consensus::interop::SafetyLevel::CrossSafe + pub cross_safe: Option, + /// [`Finalized`] head of chain. + /// + /// [`Finalized`]: op_alloy_consensus::interop::SafetyLevel::Finalized + pub finalized: Option, +} diff --git a/kona/crates/supervisor/types/src/hex_string_u64.rs b/kona/crates/supervisor/types/src/hex_string_u64.rs new file mode 100644 index 0000000000000..7df764180440a --- /dev/null +++ b/kona/crates/supervisor/types/src/hex_string_u64.rs @@ -0,0 +1,63 @@ +/// A wrapper around `u64` that supports hex string (e.g. `"0x1"`) or numeric deserialization +/// for RPC inputs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HexStringU64(pub u64); + +impl serde::Serialize for HexStringU64 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + alloy_serde::quantity::serialize(&self.0, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for HexStringU64 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let inner = alloy_serde::quantity::deserialize(deserializer)?; + Ok(Self(inner)) + } +} + +impl From for u64 { + fn from(value: HexStringU64) -> Self { + value.0 + } +} + +impl From for HexStringU64 { + fn from(value: u64) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_from_hex_string() { + let json = r#""0x1a""#; + let parsed: HexStringU64 = serde_json::from_str(json).expect("should parse hex string"); + let chain_id: u64 = parsed.0; + assert_eq!(chain_id, 0x1a); + } + + #[test] + fn test_serialize_to_hex() { + let value = HexStringU64(26); + let json = serde_json::to_string(&value).expect("should serialize"); + assert_eq!(json, r#""0x1a""#); + } + + #[test] + fn test_round_trip() { + let original = HexStringU64(12345); + let json = serde_json::to_string(&original).unwrap(); + let parsed: HexStringU64 = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.0, original.0); + } +} diff --git a/kona/crates/supervisor/types/src/lib.rs b/kona/crates/supervisor/types/src/lib.rs new file mode 100644 index 0000000000000..55e27f33fb42f --- /dev/null +++ b/kona/crates/supervisor/types/src/lib.rs @@ -0,0 +1,26 @@ +//! Core types shared across supervisor components. +//! +//! This crate defines the fundamental data structures used within the +//! Optimism supervisor. + +pub mod head; +pub use head::SuperHead; + +mod log; +pub use log::Log; + +mod message; +pub use message::ExecutingMessage; + +mod receipt; +pub use receipt::Receipts; + +mod access_list; +pub use access_list::{Access, AccessListError, parse_access_list}; + +mod hex_string_u64; +mod types; + +pub use hex_string_u64::HexStringU64; + +pub use types::{BlockSeal, OutputV0, SubscriptionEvent}; diff --git a/kona/crates/supervisor/types/src/log.rs b/kona/crates/supervisor/types/src/log.rs new file mode 100644 index 0000000000000..7a3ec19a71616 --- /dev/null +++ b/kona/crates/supervisor/types/src/log.rs @@ -0,0 +1,22 @@ +use crate::message::ExecutingMessage; +use alloy_primitives::B256; + +/// A reference entry representing a log observed in an L2 receipt. +/// +/// This struct does **not** store the actual log content. Instead: +/// - `index` is the index of the log. +/// - `hash` is the hash of the log, which uniquely identifies the log entry and can be used for +/// lookups or comparisons. +/// - `executing_message` is present if the log represents an `ExecutingMessage` emitted by the +/// `CrossL2Inbox` contract. +/// +/// This is the unit persisted by the log indexer into the database for later validation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Log { + /// The index of the log. + pub index: u32, + /// The hash of the log, derived from the log address and payload. + pub hash: B256, + /// The parsed message, if the log matches an `ExecutingMessage` event. + pub executing_message: Option, +} diff --git a/kona/crates/supervisor/types/src/message.rs b/kona/crates/supervisor/types/src/message.rs new file mode 100644 index 0000000000000..5c185552ac1fa --- /dev/null +++ b/kona/crates/supervisor/types/src/message.rs @@ -0,0 +1,17 @@ +use alloy_primitives::B256; + +/// A parsed executing message extracted from a log emitted by the +/// `CrossL2Inbox` contract on an L2 chain. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutingMessage { + /// The chain ID where the message was observed. + pub chain_id: u64, + /// The block number that contained the log. + pub block_number: u64, + /// The log index within the block. + pub log_index: u32, + /// The timestamp of the block. + pub timestamp: u64, + /// A unique hash identifying the log (based on payload and origin). + pub hash: B256, +} diff --git a/kona/crates/supervisor/types/src/receipt.rs b/kona/crates/supervisor/types/src/receipt.rs new file mode 100644 index 0000000000000..327f5a4df3e2c --- /dev/null +++ b/kona/crates/supervisor/types/src/receipt.rs @@ -0,0 +1,4 @@ +use op_alloy_consensus::OpReceiptEnvelope; + +/// Collection of transaction receipts. +pub type Receipts = Vec; diff --git a/kona/crates/supervisor/types/src/types.rs b/kona/crates/supervisor/types/src/types.rs new file mode 100644 index 0000000000000..5bd183911e6dd --- /dev/null +++ b/kona/crates/supervisor/types/src/types.rs @@ -0,0 +1,100 @@ +//! Types for communication between supervisor and op-node. +//! +//! This module defines the data structures used for communicating between the supervisor +//! and the op-node components in the rollup system. It includes block references, +//! block seals, derivation events, and event notifications. + +use alloy_primitives::B256; +use kona_interop::ManagedEvent; +use serde::{Deserialize, Serialize}; + +// todo:: Determine appropriate locations for these structs and move them accordingly. +// todo:: Link these structs to the spec documentation after the related PR is merged. + +/// Represents a sealed block with its hash, number, and timestamp. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockSeal { + /// The block's hash + pub hash: B256, + /// The block number + pub number: u64, + /// The block's timestamp + pub timestamp: u64, +} + +impl BlockSeal { + /// Creates a new [`BlockSeal`] with the given hash, number, and timestamp. + pub const fn new(hash: B256, number: u64, timestamp: u64) -> Self { + Self { hash, number, timestamp } + } +} +/// Output data for version 0 of the protocol. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct OutputV0 { + /// The state root hash + pub state_root: B256, + /// Storage root of the message passer contract + pub message_passer_storage_root: B256, + /// The block hash + pub block_hash: B256, +} + +impl OutputV0 { + /// Creates a new [`OutputV0`] instance. + pub const fn new( + state_root: B256, + message_passer_storage_root: B256, + block_hash: B256, + ) -> Self { + Self { state_root, message_passer_storage_root, block_hash } + } +} + +/// Represents the events structure sent by the node to the supervisor. +#[derive(Debug, Serialize, Deserialize)] +pub struct SubscriptionEvent { + /// Represents the event data sent by the node + pub data: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + use serde_json::{Value, json}; + + #[test] + fn test_output_v0_serialize_camel_case() { + let output = OutputV0 { + state_root: B256::from([1u8; 32]), + message_passer_storage_root: B256::from([2u8; 32]), + block_hash: B256::from([3u8; 32]), + }; + + let json_str = serde_json::to_string(&output).unwrap(); + let v: Value = serde_json::from_str(&json_str).unwrap(); + + // Check that keys are camelCase + assert!(v.get("stateRoot").is_some()); + assert!(v.get("messagePasserStorageRoot").is_some()); + assert!(v.get("blockHash").is_some()); + } + + #[test] + fn test_output_v0_deserialize_camel_case() { + let json_obj = json!({ + "stateRoot": "0x0101010101010101010101010101010101010101010101010101010101010101", + "messagePasserStorageRoot": "0x0202020202020202020202020202020202020202020202020202020202020202", + "blockHash": "0x0303030303030303030303030303030303030303030303030303030303030303" + }); + + let json_str = serde_json::to_string(&json_obj).unwrap(); + let output: OutputV0 = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(output.state_root, B256::from([1u8; 32])); + assert_eq!(output.message_passer_storage_root, B256::from([2u8; 32])); + assert_eq!(output.block_hash, B256::from([3u8; 32])); + } +} diff --git a/kona/crates/utilities/cli/Cargo.toml b/kona/crates/utilities/cli/Cargo.toml new file mode 100644 index 0000000000000..d42db96aa7da5 --- /dev/null +++ b/kona/crates/utilities/cli/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "kona-cli" +version = "0.3.2" +description = "Shared CLI utilities for Kona crates" +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[dependencies] +# Workspace +kona-genesis.workspace = true +kona-registry.workspace = true + +# Alloy +alloy-chains.workspace = true + +# General +tracing.workspace = true +serde = { workspace = true, features = ["derive"]} +clap = { workspace = true, features = ["derive", "env"] } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "json", "tracing-log"] } +tracing-appender.workspace = true +metrics-exporter-prometheus = { workspace = true, features = ["http-listener"] } +metrics-process.workspace = true +thiserror.workspace = true + +# `secrets` feature +libp2p = { workspace = true, features = ["secp256k1"], optional = true } +alloy-primitives.workspace = true + +[dev-dependencies] +rstest.workspace = true + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[features] +default = [] +secrets = [ "dep:libp2p" ] diff --git a/kona/crates/utilities/cli/README.md b/kona/crates/utilities/cli/README.md new file mode 100644 index 0000000000000..e706192ab5001 --- /dev/null +++ b/kona/crates/utilities/cli/README.md @@ -0,0 +1,3 @@ +# `kona-cli` + +Shared utilities for binaries in the `kona` repository. diff --git a/kona/crates/utilities/cli/src/backtrace.rs b/kona/crates/utilities/cli/src/backtrace.rs new file mode 100644 index 0000000000000..7b88a57ab26f1 --- /dev/null +++ b/kona/crates/utilities/cli/src/backtrace.rs @@ -0,0 +1,10 @@ +//! Helper to set the backtrace env var. + +/// Sets the RUST_BACKTRACE environment variable to 1 if it is not already set. +pub fn enable() { + // Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided. + if std::env::var_os("RUST_BACKTRACE").is_none() { + // We accept the risk that another process may set RUST_BACKTRACE at the same time. + unsafe { std::env::set_var("RUST_BACKTRACE", "1") }; + } +} diff --git a/kona/crates/utilities/cli/src/clap.rs b/kona/crates/utilities/cli/src/clap.rs new file mode 100644 index 0000000000000..ed5cfbbf76dca --- /dev/null +++ b/kona/crates/utilities/cli/src/clap.rs @@ -0,0 +1,15 @@ +//! Clap utilities. + +use clap::builder::styling::{AnsiColor, Color, Style}; + +/// Styles for the CLI application. +pub const fn cli_styles() -> clap::builder::Styles { + clap::builder::Styles::styled() + .usage(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Yellow)))) + .header(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Yellow)))) + .literal(Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)))) + .invalid(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Red)))) + .error(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Red)))) + .valid(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Green)))) + .placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White)))) +} diff --git a/kona/crates/utilities/cli/src/error.rs b/kona/crates/utilities/cli/src/error.rs new file mode 100644 index 0000000000000..a99705a88d8c8 --- /dev/null +++ b/kona/crates/utilities/cli/src/error.rs @@ -0,0 +1,26 @@ +//! Error types for CLI utilities. + +use thiserror::Error; + +/// Errors that can occur in CLI operations. +#[derive(Error, Debug)] +pub enum CliError { + /// Error when no chain config is found for the given chain ID. + #[error("No chain config found for chain ID: {0}")] + ChainConfigNotFound(u64), + + /// Error when no roles are found for the given chain ID. + #[error("No roles found for chain ID: {0}")] + RolesNotFound(u64), + + /// Error when no unsafe block signer is found for the given chain ID. + #[error("No unsafe block signer found for chain ID: {0}")] + UnsafeBlockSignerNotFound(u64), + + /// Error initializing metrics. + #[error("Failed to initialize metrics")] + MetricsInitialization(#[from] metrics_exporter_prometheus::BuildError), +} + +/// Type alias for CLI results. +pub type CliResult = Result; diff --git a/kona/crates/utilities/cli/src/flags/globals.rs b/kona/crates/utilities/cli/src/flags/globals.rs new file mode 100644 index 0000000000000..391283d7ddd8e --- /dev/null +++ b/kona/crates/utilities/cli/src/flags/globals.rs @@ -0,0 +1,116 @@ +//! Global arguments for the CLI. + +use alloy_chains::Chain; +use alloy_primitives::Address; +use clap::Parser; +use kona_genesis::RollupConfig; +use kona_registry::OPCHAINS; + +use crate::{CliError, CliResult, LogArgs, MetricsArgs, OverrideArgs}; + +/// Global arguments for the CLI. +#[derive(Parser, Default, Clone, Debug)] +pub struct GlobalArgs { + /// Logging arguments. + #[command(flatten)] + pub log_args: LogArgs, + /// The L2 chain ID to use. + #[arg( + long = "chain", + alias = "l2-chain-id", + short = 'c', + global = true, + default_value = "10", + env = "KONA_L2_CHAIN_ID", + help = "The L2 chain ID to use" + )] + pub l2_chain_id: Chain, + /// Embed the override flags globally to provide override values adjacent to the configs. + #[command(flatten)] + pub override_args: OverrideArgs, + /// Prometheus CLI arguments. + #[command(flatten)] + pub metrics: MetricsArgs, +} + +impl GlobalArgs { + /// Applies the specified overrides to the given rollup config. + /// + /// Transforms the rollup config and returns the updated config with the overrides applied. + pub fn apply_overrides(&self, config: RollupConfig) -> RollupConfig { + self.override_args.apply(config) + } + + /// Returns the signer [`Address`] from the rollup config for the given l2 chain id. + pub fn genesis_signer(&self) -> CliResult

{ + let id = self.l2_chain_id; + OPCHAINS + .get(&id.id()) + .ok_or(CliError::ChainConfigNotFound(id.id()))? + .roles + .as_ref() + .ok_or(CliError::RolesNotFound(id.id()))? + .unsafe_block_signer + .ok_or(CliError::UnsafeBlockSignerNotFound(id.id())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use rstest::rstest; + + #[rstest] + #[case::numeric_optimism("10", 10)] + #[case::numeric_ethereum("1", 1)] + #[case::numeric_base("8453", 8453)] + #[case::numeric_unknown("999999", 999999)] + #[case::string_optimism("optimism", 10)] + #[case::string_mainnet("mainnet", 1)] + #[case::string_base("base", 8453)] + fn test_l2_chain_id_parse_valid(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "--l2-chain-id", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::invalid_string("invalid_chain")] + fn test_l2_chain_id_parse_invalid(#[case] invalid_value: &str) { + let result = GlobalArgs::try_parse_from(["test", "--l2-chain-id", invalid_value]); + assert!(result.is_err()); + + // The error should be related to parsing + let err = result.unwrap_err(); + assert!(err.to_string().to_lowercase().contains("invalid")); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_short_flag(#[case] value: &str, #[case] expected_id: u64) { + let args = GlobalArgs::try_parse_from(["test", "-c", value]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + } + + #[rstest] + #[case::numeric("10", 10)] + #[case::string("optimism", 10)] + fn test_l2_chain_id_env_var(#[case] env_value: &str, #[case] expected_id: u64) { + unsafe { + std::env::set_var("KONA_NODE_L2_CHAIN_ID", env_value); + } + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), expected_id); + unsafe { + std::env::remove_var("KONA_NODE_L2_CHAIN_ID"); + } + } + + #[test] + fn test_l2_chain_id_default() { + // Test that the default value is chain ID 10 (Optimism) + let args = GlobalArgs::try_parse_from(["test"]).unwrap(); + assert_eq!(args.l2_chain_id.id(), 10); + } +} diff --git a/kona/crates/utilities/cli/src/flags/log.rs b/kona/crates/utilities/cli/src/flags/log.rs new file mode 100644 index 0000000000000..572cbe321d760 --- /dev/null +++ b/kona/crates/utilities/cli/src/flags/log.rs @@ -0,0 +1,94 @@ +//! Arguments for logging. + +use std::path::PathBuf; + +use clap::{ArgAction, Args}; +use serde::{Deserialize, Serialize}; + +use crate::{LogFormat, LogRotation}; + +/// Global configuration arguments. +#[derive(Args, Debug, Default, Serialize, Deserialize, Clone)] +pub struct LogArgs { + /// Verbosity level (1-5). + /// By default, the verbosity level is set to 3 (info level). + /// + /// This verbosity level is shared by both stdout and file logging (if enabled). + #[arg( + short = 'v', + global = true, + default_value = "3", + env = "KONA_LOG_LEVEL", + action = ArgAction::Count, + )] + pub level: u8, + /// If set, no logs are printed to stdout. + #[arg( + long = "logs.stdout.quiet", + short = 'q', + global = true, + default_value = "false", + env = "KONA_STDOUT_LOG_QUIET" + )] + pub stdout_quiet: bool, + /// The format of the logs printed to stdout. One of: full, json, pretty, compact, logfmt. + /// + /// full: The default rust log format. + /// json: The logs are printed in JSON structured format. + /// pretty: The logs are printed in a pretty, human readable format. + /// compact: The logs are printed in a compact format. + /// logfmt: The logs are printed in logfmt key=value format. + #[arg(long = "logs.stdout.format", default_value = "full", env = "KONA_LOG_STDOUT_FORMAT")] + pub stdout_format: LogFormat, + /// The directory to store the log files. + /// If not set, no logs are printed to files. + #[arg(long = "logs.file.directory", env = "KONA_LOG_FILE_DIRECTORY")] + pub file_directory: Option, + /// The format of the logs printed to log files. One of: full, json, pretty, compact, logfmt. + /// + /// full: The default rust log format. + /// json: The logs are printed in JSON structured format. + /// pretty: The logs are printed in a pretty, human readable format. + /// compact: The logs are printed in a compact format. + /// logfmt: The logs are printed in logfmt key=value format. + #[arg(long = "logs.file.format", default_value = "full", env = "KONA_LOG_FILE_FORMAT")] + pub file_format: LogFormat, + /// The rotation of the log files. One of: hourly, daily, weekly, monthly, never. + /// If set, new log files will be created every interval. + #[arg(long = "logs.file.rotation", default_value = "never", env = "KONA_LOG_FILE_ROTATION")] + pub file_rotation: LogRotation, +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + // Helper struct to parse GlobalArgs within a test CLI structure + #[derive(Parser, Debug)] + struct TestCli { + #[command(flatten)] + global: LogArgs, + } + + #[test] + fn test_default_verbosity_level() { + let cli = TestCli::parse_from(["test_app"]); + assert_eq!( + cli.global.level, 3, + "Default verbosity should be 3 when no -v flag is present." + ); + } + + #[test] + fn test_verbosity_count() { + let cli_v1 = TestCli::parse_from(["test_app", "-v"]); + assert_eq!(cli_v1.global.level, 1, "Verbosity with a single -v should be 1."); + + let cli_v3 = TestCli::parse_from(["test_app", "-vvv"]); + assert_eq!(cli_v3.global.level, 3, "Verbosity with -vvv should be 3."); + + let cli_v5 = TestCli::parse_from(["test_app", "-vvvvv"]); + assert_eq!(cli_v5.global.level, 5, "Verbosity with -vvvvv should be 5."); + } +} diff --git a/kona/crates/utilities/cli/src/flags/metrics.rs b/kona/crates/utilities/cli/src/flags/metrics.rs new file mode 100644 index 0000000000000..7333270bb2257 --- /dev/null +++ b/kona/crates/utilities/cli/src/flags/metrics.rs @@ -0,0 +1,97 @@ +//! Utility module to house implementation and declaration of MetricsArgs since it's being used in +//! multiple places, it's just being referenced from this module. + +use crate::{CliResult, init_prometheus_server}; +use clap::{Parser, arg}; +use std::net::IpAddr; + +/// Configuration for Prometheus metrics. +#[derive(Debug, Clone, Parser)] +#[command(next_help_heading = "Metrics")] +pub struct MetricsArgs { + /// Controls whether Prometheus metrics are enabled. Disabled by default. + #[arg( + long = "metrics.enabled", + global = true, + default_value_t = false, + env = "KONA_METRICS_ENABLED" + )] + pub enabled: bool, + + /// The port to serve Prometheus metrics on. + #[arg(long = "metrics.port", global = true, default_value = "9090", env = "KONA_METRICS_PORT")] + pub port: u16, + + /// The IP address to use for Prometheus metrics. + #[arg( + long = "metrics.addr", + global = true, + default_value = "0.0.0.0", + env = "KONA_METRICS_ADDR" + )] + pub addr: IpAddr, +} + +impl Default for MetricsArgs { + fn default() -> Self { + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl MetricsArgs { + /// Initialize the tracing stack and Prometheus metrics recorder. + /// + /// This function should be called at the beginning of the program. + pub fn init_metrics(&self) -> CliResult<()> { + if self.enabled { + init_prometheus_server(self.addr, self.port)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + use std::net::{IpAddr, Ipv4Addr}; + + /// Helper struct to parse MetricsArgs within a test CLI structure. + #[derive(Parser, Debug)] + struct TestCli { + #[command(flatten)] + metrics: MetricsArgs, + } + + #[test] + fn test_default_metrics_args() { + let cli = TestCli::parse_from(["test_app"]); + assert!(!cli.metrics.enabled, "Default for metrics.enabled should be false."); + assert_eq!(cli.metrics.port, 9090, "Default for metrics.port should be 9090."); + assert_eq!( + cli.metrics.addr, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + "Default for metrics.addr should be 0.0.0.0." + ); + } + + #[test] + fn test_metrics_args_from_cli() { + let cli = TestCli::parse_from([ + "test_app", + "--metrics.enabled", + "--metrics.port", + "9999", + "--metrics.addr", + "127.0.0.1", + ]); + assert!(cli.metrics.enabled, "metrics.enabled should be true."); + assert_eq!(cli.metrics.port, 9999, "metrics.port should be parsed from CLI."); + assert_eq!( + cli.metrics.addr, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + "metrics.addr should be parsed from CLI." + ); + } +} diff --git a/kona/crates/utilities/cli/src/flags/mod.rs b/kona/crates/utilities/cli/src/flags/mod.rs new file mode 100644 index 0000000000000..e4d272378183a --- /dev/null +++ b/kona/crates/utilities/cli/src/flags/mod.rs @@ -0,0 +1,15 @@ +//! Common CLI Flags +//! +//! These are cli flags that are shared across binaries to standardize kona's services CLI UX. + +mod globals; +pub use globals::GlobalArgs; + +mod overrides; +pub use overrides::OverrideArgs; + +mod log; +pub use log::LogArgs; + +mod metrics; +pub use metrics::MetricsArgs; diff --git a/kona/crates/utilities/cli/src/flags/overrides.rs b/kona/crates/utilities/cli/src/flags/overrides.rs new file mode 100644 index 0000000000000..dd804fa691bbf --- /dev/null +++ b/kona/crates/utilities/cli/src/flags/overrides.rs @@ -0,0 +1,168 @@ +//! Flags that allow overriding derived values. + +use clap::Parser; +use kona_genesis::RollupConfig; + +/// Override Flags. +#[derive(Parser, Debug, Clone, Copy, PartialEq, Eq)] +pub struct OverrideArgs { + /// Manually specify the timestamp for the Canyon fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_CANYON")] + pub canyon_override: Option, + /// Manually specify the timestamp for the Delta fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_DELTA")] + pub delta_override: Option, + /// Manually specify the timestamp for the Ecotone fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_ECOTONE")] + pub ecotone_override: Option, + /// Manually specify the timestamp for the Fjord fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_FJORD")] + pub fjord_override: Option, + /// Manually specify the timestamp for the Granite fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_GRANITE")] + pub granite_override: Option, + /// Manually specify the timestamp for the Holocene fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_HOLOCENE")] + pub holocene_override: Option, + /// Manually specify the timestamp for the Isthmus fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_ISTHMUS")] + pub isthmus_override: Option, + /// Manually specify the timestamp for the Jovian fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_JOVIAN")] + pub jovian_override: Option, + /// Manually specify the timestamp for the pectra blob schedule, overriding the bundled + /// setting. + #[arg(long, env = "KONA_OVERRIDE_PECTRA_BLOB_SCHEDULE")] + pub pectra_blob_schedule_override: Option, + /// Manually specify the timestamp for the Interop fork, overriding the bundled setting. + #[arg(long, env = "KONA_OVERRIDE_INTEROP")] + pub interop_override: Option, +} + +impl Default for OverrideArgs { + fn default() -> Self { + // Construct default values using the clap parser. + // This works since none of the cli flags are required. + Self::parse_from::<[_; 0], &str>([]) + } +} + +impl OverrideArgs { + /// Applies the override args to the given rollup config. + pub fn apply(&self, config: RollupConfig) -> RollupConfig { + let hardforks = kona_genesis::HardForkConfig { + regolith_time: config.hardforks.regolith_time, + canyon_time: self.canyon_override.map(Some).unwrap_or(config.hardforks.canyon_time), + delta_time: self.delta_override.map(Some).unwrap_or(config.hardforks.delta_time), + ecotone_time: self.ecotone_override.map(Some).unwrap_or(config.hardforks.ecotone_time), + fjord_time: self.fjord_override.map(Some).unwrap_or(config.hardforks.fjord_time), + granite_time: self.granite_override.map(Some).unwrap_or(config.hardforks.granite_time), + holocene_time: self + .holocene_override + .map(Some) + .unwrap_or(config.hardforks.holocene_time), + pectra_blob_schedule_time: self + .pectra_blob_schedule_override + .map(Some) + .unwrap_or(config.hardforks.pectra_blob_schedule_time), + isthmus_time: self.isthmus_override.map(Some).unwrap_or(config.hardforks.isthmus_time), + jovian_time: self.jovian_override.map(Some).unwrap_or(config.hardforks.jovian_time), + interop_time: self.interop_override.map(Some).unwrap_or(config.hardforks.interop_time), + }; + RollupConfig { hardforks, ..config } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A mock command that uses the override args. + #[derive(Parser, Debug, Clone)] + #[command(about = "Mock command")] + struct MockCommand { + /// Override flags. + #[clap(flatten)] + pub override_flags: OverrideArgs, + } + + #[test] + fn test_apply_overrides() { + let args = MockCommand::parse_from([ + "test", + "--canyon-override", + "1699981200", + "--delta-override", + "1703203200", + "--ecotone-override", + "1708534800", + "--fjord-override", + "1716998400", + "--granite-override", + "1723478400", + "--holocene-override", + "1732633200", + "--pectra-blob-schedule-override", + "1745000000", + "--isthmus-override", + "1740000000", + "--jovian-override", + "1745000001", + "--interop-override", + "1750000000", + ]); + let config = RollupConfig::default(); + let updated_config = args.override_flags.apply(config); + assert_eq!( + updated_config.hardforks, + kona_genesis::HardForkConfig { + regolith_time: Default::default(), + canyon_time: Some(1699981200), + delta_time: Some(1703203200), + ecotone_time: Some(1708534800), + fjord_time: Some(1716998400), + granite_time: Some(1723478400), + holocene_time: Some(1732633200), + pectra_blob_schedule_time: Some(1745000000), + isthmus_time: Some(1740000000), + jovian_time: Some(1745000001), + interop_time: Some(1750000000), + } + ); + } + + #[test] + fn test_apply_default_overrides() { + // Use OP Mainnet rollup config. + let config = kona_registry::ROLLUP_CONFIGS + .get(&10) + .expect("No config found for chain ID 10") + .clone(); + let init_forks = config.hardforks; + let args = MockCommand::parse_from(["test"]); + let updated_config = args.override_flags.apply(config); + assert_eq!(updated_config.hardforks, init_forks); + } + + #[test] + fn test_default_override_flags() { + let args = MockCommand::parse_from(["test"]); + assert_eq!( + args.override_flags, + OverrideArgs { + canyon_override: None, + delta_override: None, + ecotone_override: None, + fjord_override: None, + granite_override: None, + holocene_override: None, + pectra_blob_schedule_override: None, + isthmus_override: None, + jovian_override: None, + interop_override: None, + } + ); + // Sanity check that the default impl matches the expected default values. + assert_eq!(args.override_flags, OverrideArgs::default()); + } +} diff --git a/kona/crates/utilities/cli/src/lib.rs b/kona/crates/utilities/cli/src/lib.rs new file mode 100644 index 0000000000000..4c151dec0a230 --- /dev/null +++ b/kona/crates/utilities/cli/src/lib.rs @@ -0,0 +1,33 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod error; +pub use error::{CliError, CliResult}; + +mod flags; +pub use flags::{GlobalArgs, LogArgs, MetricsArgs, OverrideArgs}; + +mod logs; +pub use logs::{FileLogConfig, LogConfig, LogRotation, StdoutLogConfig}; + +mod clap; +pub use clap::cli_styles; + +#[cfg(feature = "secrets")] +mod secrets; +#[cfg(feature = "secrets")] +pub use secrets::{KeypairError, ParseKeyError, SecretKeyLoader}; + +pub mod backtrace; + +mod tracing; +pub use tracing::{LogFormat, init_test_tracing}; + +mod prometheus; +pub use prometheus::init_prometheus_server; + +pub mod sigsegv_handler; diff --git a/kona/crates/utilities/cli/src/logs.rs b/kona/crates/utilities/cli/src/logs.rs new file mode 100644 index 0000000000000..729ddd6ef8110 --- /dev/null +++ b/kona/crates/utilities/cli/src/logs.rs @@ -0,0 +1,97 @@ +//! Logging Configuration Types + +use std::path::PathBuf; + +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use tracing::level_filters::LevelFilter; + +use crate::{LogArgs, LogFormat}; + +/// The rotation of the log files. +#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum, Default)] +#[serde(rename_all = "lowercase")] +pub enum LogRotation { + /// Rotate the log files every minute. + Minutely, + /// Rotate the log files hourly. + Hourly, + /// Rotate the log files daily. + Daily, + /// Do not rotate the log files. + #[default] + Never, +} + +/// Configuration for file logging. +#[derive(Debug, Clone)] +pub struct FileLogConfig { + /// The path to the directory where the log files are stored. + pub directory_path: PathBuf, + /// The format of the logs printed to the log file. + pub format: LogFormat, + /// The rotation of the log files. + pub rotation: LogRotation, +} + +/// Configuration for stdout logging. +#[derive(Debug, Clone)] +pub struct StdoutLogConfig { + /// The format of the logs printed to stdout. + pub format: LogFormat, +} + +/// Global configuration for logging. +/// Default is to only print logs to stdout in full format. +#[derive(Debug, Clone)] +pub struct LogConfig { + /// Global verbosity level for logging. + pub global_level: LevelFilter, + /// The configuration for stdout logging. + pub stdout_logs: Option, + /// The configuration for file logging. + pub file_logs: Option, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + global_level: LevelFilter::INFO, + stdout_logs: Some(StdoutLogConfig { format: LogFormat::Full }), + file_logs: None, + } + } +} + +impl From for LogConfig { + fn from(args: LogArgs) -> Self { + Self::new(args) + } +} + +impl LogConfig { + /// Creates a new `LogConfig` from `LogArgs`. + pub fn new(args: LogArgs) -> Self { + let level = match args.level { + 1 => LevelFilter::ERROR, + 2 => LevelFilter::WARN, + 3 => LevelFilter::INFO, + 4 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + }; + + let stdout_logs = if args.stdout_quiet { + None + } else { + Some(StdoutLogConfig { format: args.stdout_format }) + }; + + let file_logs = args.file_directory.as_ref().map(|path| FileLogConfig { + directory_path: path.clone(), + format: args.file_format, + rotation: args.file_rotation, + }); + + Self { global_level: level, stdout_logs, file_logs } + } +} diff --git a/kona/crates/utilities/cli/src/prometheus.rs b/kona/crates/utilities/cli/src/prometheus.rs new file mode 100644 index 0000000000000..45eb51dc6cb81 --- /dev/null +++ b/kona/crates/utilities/cli/src/prometheus.rs @@ -0,0 +1,37 @@ +//! Utilities for spinning up a prometheus metrics server. + +use metrics_exporter_prometheus::{BuildError, PrometheusBuilder}; +use metrics_process::Collector; +use std::{ + net::{IpAddr, SocketAddr}, + thread::{self, sleep}, + time::Duration, +}; +use tracing::info; + +/// Start a Prometheus metrics server on the given port. +pub fn init_prometheus_server(addr: IpAddr, metrics_port: u16) -> Result<(), BuildError> { + let prometheus_addr = SocketAddr::from((addr, metrics_port)); + let builder = PrometheusBuilder::new().with_http_listener(prometheus_addr); + + builder.install()?; + + // Initialise collector for system metrics e.g. CPU, memory, etc. + let collector = Collector::default(); + collector.describe(); + + thread::spawn(move || { + loop { + collector.collect(); + sleep(Duration::from_secs(60)); + } + }); + + info!( + target: "prometheus", + "Serving metrics at: http://{}", + prometheus_addr + ); + + Ok(()) +} diff --git a/kona/crates/utilities/cli/src/secrets.rs b/kona/crates/utilities/cli/src/secrets.rs new file mode 100644 index 0000000000000..c1aaac2f88aaf --- /dev/null +++ b/kona/crates/utilities/cli/src/secrets.rs @@ -0,0 +1,147 @@ +//! Utility functions for working with secret keys. +//! +//! This module is adapted from + +use alloy_primitives::B256; +use libp2p::identity::{Keypair, secp256k1::SecretKey}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; +use thiserror::Error; + +/// A loader type for loading secret keys. +#[derive(Debug, Clone)] +pub struct SecretKeyLoader; + +impl SecretKeyLoader { + /// Attempts to load a [`Keypair`] from a specified path. + /// + /// If no file exists there, then it generates a secret key + /// and stores it in the provided path. I/O errors might occur + /// during write operations in the form of a [`KeypairError`] + pub fn load(secret_key_path: &Path) -> Result { + let exists = secret_key_path.try_exists(); + + match exists { + Ok(true) => { + let contents = std::fs::read_to_string(secret_key_path)?; + let mut decoded = B256::from_str(&contents)?; + Ok(Self::parse(&mut decoded.0)?) + } + Ok(false) => { + if let Some(dir) = secret_key_path.parent() { + std::fs::create_dir_all(dir)?; + } + + let secret = SecretKey::generate(); + let hex = alloy_primitives::hex::encode(secret.to_bytes()); + std::fs::write(secret_key_path, hex)?; + let kp = libp2p::identity::secp256k1::Keypair::from(secret); + Ok(Keypair::from(kp)) + } + Err(error) => Err(KeypairError::FailedToAccessKeyFile { + error, + secret_file: secret_key_path.to_path_buf(), + }), + } + } + + /// Parses raw bytes into a [`Keypair`]. + pub fn parse(input: &mut [u8]) -> Result { + let sk = + SecretKey::try_from_bytes(input).map_err(|_| ParseKeyError::FailedToParseSecretKey)?; + let kp = libp2p::identity::secp256k1::Keypair::from(sk); + Ok(Keypair::from(kp)) + } +} + +/// An error parsing raw secret key bytes into a [`Keypair`]. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum ParseKeyError { + /// Failed to parse the bytes into an `secp256k1` secret key. + #[error("Failed to parse bytes into an `secp256k1` secret key")] + FailedToParseSecretKey, +} + +/// Errors returned by loading a [`Keypair`], including IO errors. +#[derive(Error, Debug)] +pub enum KeypairError { + /// Error encountered during decoding of the secret key. + #[error(transparent)] + SecretKeyDecodeError(#[from] ParseKeyError), + + /// An error encountered converting a hex string into [`B256`] bytes. + #[error(transparent)] + HexError(#[from] alloy_primitives::hex::FromHexError), + + /// Error related to file system path operations. + #[error(transparent)] + SecretKeyFsPathError(#[from] std::io::Error), + + /// Represents an error when failed to access the key file. + #[error("failed to access key file {secret_file:?}: {error}")] + FailedToAccessKeyFile { + /// The encountered IO error. + error: std::io::Error, + /// Path to the secret key file. + secret_file: PathBuf, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::b256; + use std::path::PathBuf; + + #[test] + fn test_parse() { + let secret_key = SecretKey::generate(); + let mut key = B256::from(secret_key.to_bytes()); + assert!(SecretKeyLoader::parse(&mut key.0).is_ok()); + } + + #[test] + fn test_load_new_file() { + let dir = std::env::temp_dir(); + assert!(std::env::set_current_dir(dir).is_ok()); + + let path = PathBuf::from("./root/does_not_exist34.txt"); + assert!(SecretKeyLoader::load(&path).is_ok()); + } + + // Github actions panics on this test. + #[test] + #[ignore] + fn test_load_invalid_path() { + let dir = std::env::temp_dir(); + assert!(std::env::set_current_dir(dir).is_ok()); + + let path = PathBuf::from("/root/does_not_exist34.txt"); + assert!(!path.try_exists().unwrap()); + let err = SecretKeyLoader::load(&path).unwrap_err(); + let KeypairError::SecretKeyFsPathError(_) = err else { + panic!("Incorrect error thrown"); + }; + } + + #[test] + fn test_load_file() { + // Create a temporary directory. + let dir = std::env::temp_dir(); + let mut key_path = dir.clone(); + assert!(std::env::set_current_dir(dir).is_ok()); + + // Write a private key to a file. + let key = b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + let hex = alloy_primitives::hex::encode(key.0); + key_path.push("test.txt"); + std::fs::write(&key_path, &hex).unwrap(); + + // Validate the keypair and file contents (to make sure it wasn't overwritten). + assert!(SecretKeyLoader::load(&key_path).is_ok()); + let contents = std::fs::read_to_string(&key_path).unwrap(); + assert_eq!(contents, hex); + } +} diff --git a/kona/crates/utilities/cli/src/sigsegv_handler.rs b/kona/crates/utilities/cli/src/sigsegv_handler.rs new file mode 100644 index 0000000000000..374749e1616bb --- /dev/null +++ b/kona/crates/utilities/cli/src/sigsegv_handler.rs @@ -0,0 +1,149 @@ +//! Signal handler to extract a backtrace from reth, which is originally from stack overflow. +//! +//! Implementation modified from [reth](https://github.com/paradigmxyz/reth/blob/main/crates/cli/util/src/sigsegv_handler.rs#L120). +//! +//! Implementation modified from [`rustc`](https://github.com/rust-lang/rust/blob/3dee9775a8c94e701a08f7b2df2c444f353d8699/compiler/rustc_driver_impl/src/signal_handler.rs). + +use std::{ + alloc::{Layout, alloc}, + fmt, mem, ptr, +}; + +unsafe extern "C" { + fn backtrace_symbols_fd(buffer: *const *mut libc::c_void, size: libc::c_int, fd: libc::c_int); +} + +fn backtrace_stderr(buffer: &[*mut libc::c_void]) { + let size = buffer.len().try_into().unwrap_or_default(); + unsafe { backtrace_symbols_fd(buffer.as_ptr(), size, libc::STDERR_FILENO) }; +} + +/// Unbuffered, unsynchronized writer to stderr. +/// +/// Only acceptable because everything will end soon anyways. +struct RawStderr(()); + +impl fmt::Write for RawStderr { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + let ret = unsafe { libc::write(libc::STDERR_FILENO, s.as_ptr().cast(), s.len()) }; + if ret == -1 { Err(fmt::Error) } else { Ok(()) } + } +} + +/// We don't really care how many bytes we actually get out. SIGSEGV comes for our head. +/// Splash stderr with letters of our own blood to warn our friends about the monster. +macro_rules! raw_errln { + ($tokens:tt) => { + let _ = ::core::fmt::Write::write_fmt(&mut RawStderr(()), format_args!($tokens)); + let _ = ::core::fmt::Write::write_char(&mut RawStderr(()), '\n'); + }; +} + +/// Signal handler installed for SIGSEGV +extern "C" fn print_stack_trace(_: libc::c_int) { + const MAX_FRAMES: usize = 256; + let mut stack_trace: [*mut libc::c_void; MAX_FRAMES] = [ptr::null_mut(); MAX_FRAMES]; + let stack = unsafe { + // Collect return addresses + let depth = libc::backtrace(stack_trace.as_mut_ptr(), MAX_FRAMES as i32); + if depth == 0 { + return; + } + &stack_trace[0..depth as usize] + }; + + // Just a stack trace is cryptic. Explain what we're doing. + raw_errln!("error: reth interrupted by SIGSEGV, printing backtrace\n"); + let mut written = 1; + let mut consumed = 0; + // Begin elaborating return addrs into symbols and writing them directly to stderr + // Most backtraces are stack overflow, most stack overflows are from recursion + // Check for cycles before writing 250 lines of the same ~5 symbols + let cycled = |(runner, walker)| runner == walker; + let mut cyclic = false; + if let Some(period) = stack.iter().skip(1).step_by(2).zip(stack).position(cycled) { + let period = period.saturating_add(1); // avoid "what if wrapped?" branches + let Some(offset) = stack.iter().skip(period).zip(stack).position(cycled) else { + // impossible. + return; + }; + + // Count matching trace slices, else we could miscount "biphasic cycles" + // with the same period + loop entry but a different inner loop + let next_cycle = stack[offset..].chunks_exact(period).skip(1); + let cycles = 1 + next_cycle + .zip(stack[offset..].chunks_exact(period)) + .filter(|(next, prev)| next == prev) + .count(); + backtrace_stderr(&stack[..offset]); + written += offset; + consumed += offset; + if cycles > 1 { + raw_errln!("\n### cycle encountered after {offset} frames with period {period}"); + backtrace_stderr(&stack[consumed..consumed + period]); + raw_errln!("### recursed {cycles} times\n"); + written += period + 4; + consumed += period * cycles; + cyclic = true; + }; + } + let rem = &stack[consumed..]; + backtrace_stderr(rem); + raw_errln!(""); + written += rem.len() + 1; + + let random_depth = || 8 * 16; // chosen by random diceroll (2d20) + if cyclic || stack.len() > random_depth() { + // technically speculation, but assert it with confidence anyway. + // We only arrived in this signal handler because bad things happened + // and this message is for explaining it's not the programmer's fault + raw_errln!("note: reth unexpectedly overflowed its stack! this is a bug"); + written += 1; + } + if stack.len() == MAX_FRAMES { + raw_errln!("note: maximum backtrace depth reached, frames may have been lost"); + written += 1; + } + raw_errln!("note: we would appreciate a report at https://github.com/paradigmxyz/reth"); + written += 1; + if written > 24 { + // We probably just scrolled the earlier "we got SIGSEGV" message off the terminal + raw_errln!("note: backtrace dumped due to SIGSEGV! resuming signal"); + } +} + +/// Installs a SIGSEGV handler. +/// +/// When SIGSEGV is delivered to the process, print a stack trace and then exit. +pub fn install() { + unsafe { + let alt_stack_size: usize = min_sigstack_size() + 64 * 1024; + let mut alt_stack: libc::stack_t = mem::zeroed(); + alt_stack.ss_sp = alloc(Layout::from_size_align(alt_stack_size, 1).unwrap()).cast(); + alt_stack.ss_size = alt_stack_size; + libc::sigaltstack(&alt_stack, ptr::null_mut()); + + let mut sa: libc::sigaction = mem::zeroed(); + sa.sa_sigaction = print_stack_trace as libc::sighandler_t; + sa.sa_flags = libc::SA_NODEFER | libc::SA_RESETHAND | libc::SA_ONSTACK; + libc::sigemptyset(&mut sa.sa_mask); + libc::sigaction(libc::SIGSEGV, &sa, ptr::null_mut()); + } +} + +/// Modern kernels on modern hardware can have dynamic signal stack sizes. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn min_sigstack_size() -> usize { + const AT_MINSIGSTKSZ: core::ffi::c_ulong = 51; + let dynamic_sigstksz = unsafe { libc::getauxval(AT_MINSIGSTKSZ) }; + // If getauxval couldn't find the entry, it returns 0, + // so take the higher of the "constant" and auxval. + // This transparently supports older kernels which don't provide AT_MINSIGSTKSZ + libc::MINSIGSTKSZ.max(dynamic_sigstksz as _) +} + +/// Not all OS support hardware where this is needed. +#[cfg(not(any(target_os = "linux", target_os = "android")))] +const fn min_sigstack_size() -> usize { + libc::MINSIGSTKSZ +} diff --git a/kona/crates/utilities/cli/src/tracing.rs b/kona/crates/utilities/cli/src/tracing.rs new file mode 100644 index 0000000000000..05920d4cbeec3 --- /dev/null +++ b/kona/crates/utilities/cli/src/tracing.rs @@ -0,0 +1,152 @@ +//! [tracing_subscriber] utilities. + +use tracing_subscriber::{ + Layer, + fmt::{ + format::{FormatEvent, FormatFields, Writer}, + time::{FormatTime, SystemTime}, + }, + prelude::__tracing_subscriber_SubscriberExt, + registry::LookupSpan, + util::{SubscriberInitExt, TryInitError}, +}; + +use serde::{Deserialize, Serialize}; +use std::fmt; +use tracing_subscriber::EnvFilter; + +use crate::{LogConfig, LogRotation}; + +/// The format of the logs. +#[derive( + Default, Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum, Serialize, Deserialize, +)] +#[serde(rename_all = "lowercase")] +#[clap(rename_all = "lowercase")] +pub enum LogFormat { + /// Full format (default). + #[default] + Full, + /// JSON format. + Json, + /// Pretty format. + Pretty, + /// Compact format. + Compact, + /// Logfmt format. + Logfmt, +} + +/// Custom logfmt formatter for tracing events. +struct LogfmtFormatter; + +impl FormatEvent for LogfmtFormatter +where + S: tracing::Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &tracing::Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + + // Write timestamp + let time_format = SystemTime; + write!(writer, "time=\"")?; + time_format.format_time(&mut writer)?; + write!(writer, "\" ")?; + + // Write level + write!(writer, "level={} ", meta.level())?; + + // Write target + write!(writer, "target={} ", meta.target())?; + + // Write the message and fields + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} + +impl LogConfig { + /// Initializes the tracing subscriber + /// + /// # Arguments + /// * `verbosity_level` - The verbosity level (0-5). If `0`, no logs are printed. + /// * `env_filter` - Optional environment filter for the subscriber. + /// + /// # Returns + /// * `Result<()>` - Ok if successful, Err otherwise. + pub fn init_tracing_subscriber( + &self, + env_filter: Option, + ) -> Result<(), TryInitError> { + let file_layer = self.file_logs.as_ref().map(|file_logs| { + let directory_path = file_logs.directory_path.clone(); + + let appender = match file_logs.rotation { + LogRotation::Minutely => { + tracing_appender::rolling::minutely(directory_path, "kona.log") + } + LogRotation::Hourly => { + tracing_appender::rolling::hourly(directory_path, "kona.log") + } + LogRotation::Daily => tracing_appender::rolling::daily(directory_path, "kona.log"), + LogRotation::Never => tracing_appender::rolling::never(directory_path, "kona.log"), + }; + + match file_logs.format { + LogFormat::Full => tracing_subscriber::fmt::layer().with_writer(appender).boxed(), + LogFormat::Json => { + tracing_subscriber::fmt::layer().json().with_writer(appender).boxed() + } + LogFormat::Pretty => { + tracing_subscriber::fmt::layer().pretty().with_writer(appender).boxed() + } + LogFormat::Compact => { + tracing_subscriber::fmt::layer().compact().with_writer(appender).boxed() + } + LogFormat::Logfmt => tracing_subscriber::fmt::layer() + .event_format(LogfmtFormatter) + .with_writer(appender) + .boxed(), + } + }); + + let stdout_layer = self.stdout_logs.as_ref().map(|stdout_logs| match stdout_logs.format { + LogFormat::Full => tracing_subscriber::fmt::layer().boxed(), + LogFormat::Json => tracing_subscriber::fmt::layer().json().boxed(), + LogFormat::Pretty => tracing_subscriber::fmt::layer().pretty().boxed(), + LogFormat::Compact => tracing_subscriber::fmt::layer().compact().boxed(), + LogFormat::Logfmt => { + tracing_subscriber::fmt::layer().event_format(LogfmtFormatter).boxed() + } + }); + + let env_filter = env_filter + .unwrap_or(EnvFilter::from_default_env()) + .add_directive(self.global_level.into()); + + tracing_subscriber::registry() + .with(env_filter) + .with(file_layer) + .with(stdout_layer) + .try_init()?; + + Ok(()) + } +} + +/// This provides function for init tracing in testing +/// +/// # Functions +/// - `init_test_tracing`: A helper function for initializing tracing in test environments. +/// - `init_tracing_subscriber`: Initializes the tracing subscriber with a specified verbosity level +/// and optional environment filter. +pub fn init_test_tracing() { + let _ = LogConfig::default().init_tracing_subscriber(None::); +} diff --git a/kona/crates/utilities/macros/Cargo.toml b/kona/crates/utilities/macros/Cargo.toml new file mode 100644 index 0000000000000..9a7300af21445 --- /dev/null +++ b/kona/crates/utilities/macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "kona-macros" +version = "0.1.2" +description = "Utility macros for kona crates" + +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] + +[features] +default = [] diff --git a/kona/crates/utilities/macros/README.md b/kona/crates/utilities/macros/README.md new file mode 100644 index 0000000000000..a0d229a0f2fd8 --- /dev/null +++ b/kona/crates/utilities/macros/README.md @@ -0,0 +1,8 @@ +# `kona-macros` + +CI +Kona Engine +License +Codecov + +Utility helper macros for kona crates. diff --git a/kona/crates/utilities/macros/src/lib.rs b/kona/crates/utilities/macros/src/lib.rs new file mode 100644 index 0000000000000..f0796b849b71a --- /dev/null +++ b/kona/crates/utilities/macros/src/lib.rs @@ -0,0 +1,11 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] + +mod metrics; diff --git a/kona/crates/utilities/macros/src/metrics.rs b/kona/crates/utilities/macros/src/metrics.rs new file mode 100644 index 0000000000000..47d929749461e --- /dev/null +++ b/kona/crates/utilities/macros/src/metrics.rs @@ -0,0 +1,60 @@ +//! Macros for recording metrics. + +/// Sets a metric value, optionally with a specified label. +#[macro_export] +macro_rules! set { + (counter, $metric:path, $key:expr, $value:expr, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::counter!($metric, $key => $value).absolute($amount); + }; + ($instrument:ident, $metric:path, $key:expr, $value:expr, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric, $key => $value).set($amount); + }; + (counter, $metric:path, $value:expr, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::counter!($metric, "type" => $value).absolute($amount); + }; + ($instrument:ident, $metric:path, $value:expr, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric, "type" => $value).set($amount); + }; + (counter, $metric:path, $value:expr) => { + #[cfg(feature = "metrics")] + metrics::counter!($metric).absolute($value); + }; + ($instrument:ident, $metric:path, $value:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric).set($value); + }; +} + +/// Increments a metric value, optionally with a specified label. +#[macro_export] +macro_rules! inc { + ($instrument:ident, $metric:path, $value:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric, "type" => $value).increment(1); + }; + ($instrument:ident, $metric:path $(, $label_key:expr $(=> $label_value:expr)?)*$(,)?) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric $(, $label_key $(=> $label_value)?)*).increment(1); + }; + ($instrument:ident, $metric:path, $value:expr $(, $label_key:expr $(=> $label_value:expr)?)*$(,)?) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric $(, $label_key $(=> $label_value)?)*).increment($value); + }; +} + +/// Records a value, optionally with a specified label. +#[macro_export] +macro_rules! record { + ($instrument:ident, $metric:path, $key:expr, $value:expr, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric, $key => $value).record($amount); + }; + ($instrument:ident, $metric:path, $amount:expr) => { + #[cfg(feature = "metrics")] + metrics::$instrument!($metric).record($amount); + }; +} diff --git a/kona/crates/utilities/serde/Cargo.toml b/kona/crates/utilities/serde/Cargo.toml new file mode 100644 index 0000000000000..b72b3f585cadc --- /dev/null +++ b/kona/crates/utilities/serde/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "kona-serde" +version = "0.2.2" +description = "Serde related helpers for kona" + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["serde"] } +serde.workspace = true +serde_json = { workspace = true, features = ["alloc"] } + +[dev-dependencies] +toml = { workspace = true, features = ["parse"] } + +[package.metadata.cargo-udeps.ignore] +development = ["toml"] + +[features] +default = [] +std = [ + "alloy-primitives/serde", + "alloy-primitives/std", + "serde/std", + "serde_json/std", +] diff --git a/kona/crates/utilities/serde/README.md b/kona/crates/utilities/serde/README.md new file mode 100644 index 0000000000000..d5ca4b6f13cb9 --- /dev/null +++ b/kona/crates/utilities/serde/README.md @@ -0,0 +1,64 @@ +## `kona-serde` + +CI +kona-serde crate +MIT License +Docs + +Serde related helpers for kona. + +### Graceful Serialization + +This crate extends the serialization and deserialization +functionality provided by [`alloy-serde`][alloy-serde] to +deserialize raw number quantity values. + +This issue arose in `u128` toml deserialization where +deserialization of a raw number fails. +[This rust playground][invalid] demonstrates how toml fails to +deserialize a native `u128` internal value. + +With `kona-serde`, tagging the inner `u128` field with `#[serde(with = "kona_serde::quantity")]`, +allows the `u128` or any other type within the following constraints to be deserialized by toml properly. + +These are the supported native types: +- `bool` +- `u8` +- `u16` +- `u32` +- `u64` +- `u128` + +Below demonstrates the use of the `#[serde(with = "kona_serde::quantity")]` attribute. + +```rust +use serde::{Serialize, Deserialize}; + +/// My wrapper type. +#[derive(Debug, Serialize, Deserialize)] +pub struct MyStruct { + /// The inner `u128` value. + #[serde(with = "kona_serde::quantity")] + pub inner: u128, +} + +// Correctly deserializes a raw value. +let raw_toml = r#"inner = 120"#; +let b: MyStruct = toml::from_str(raw_toml).expect("failed to deserialize toml"); +println!("{}", b.inner); + +// Notice that a string value is also deserialized correctly. +let raw_toml = r#"inner = "120""#; +let b: MyStruct = toml::from_str(raw_toml).expect("failed to deserialize toml"); +println!("{}", b.inner); +``` + +### Provenance + +This code is heavily based on the [`alloy-serde`][alloy-serde] crate. + + + + +[invalid]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d3c674d02a90c574e3f543144621418d +[alloy-serde]: https://crates.io/crates/alloy-serde diff --git a/kona/crates/utilities/serde/src/lib.rs b/kona/crates/utilities/serde/src/lib.rs new file mode 100644 index 0000000000000..9433e41851f7a --- /dev/null +++ b/kona/crates/utilities/serde/src/lib.rs @@ -0,0 +1,12 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png", + html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico", + issue_tracker_base_url = "https://github.com/op-rs/kona/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] + +extern crate alloc; + +pub mod quantity; diff --git a/kona/crates/utilities/serde/src/quantity.rs b/kona/crates/utilities/serde/src/quantity.rs new file mode 100644 index 0000000000000..3447c2bbd2421 --- /dev/null +++ b/kona/crates/utilities/serde/src/quantity.rs @@ -0,0 +1,83 @@ +//! Kona quantity serialization and deserialization helpers. + +use alloc::string::ToString; +use core::str::FromStr; +use private::ConvertRuint; +use serde::{self, Deserialize, Deserializer, Serialize, Serializer, de}; +use serde_json::Value; + +/// Serializes a primitive number as a "quantity" hex string. +pub fn serialize(value: &T, serializer: S) -> Result +where + T: ConvertRuint, + S: Serializer, +{ + value.into_ruint().serialize(serializer) +} + +/// Deserializes a primitive number from a "quantity" hex string or raw number. +pub fn deserialize<'de, T, D>(deserializer: D) -> Result +where + T: ConvertRuint, + D: Deserializer<'de>, +{ + use serde::de::Error; + match Value::deserialize(deserializer)? { + Value::String(s) => T::Ruint::from_str(&s) + .map_err(|_| D::Error::custom("failed to deserialize str")) + .map(T::from_ruint), + Value::Number(num) => T::Ruint::from_str(&num.to_string()) + .map_err(|_| de::Error::custom("failed to deserialize number")) + .map(T::from_ruint), + _ => Err(de::Error::custom("only string and number types are supported")), + } +} + +/// Private implementation details of the [`quantity`](self) module. +#[allow(unnameable_types)] +mod private { + use core::str::FromStr; + + #[doc(hidden)] + pub trait ConvertRuint: Copy + Sized { + type Ruint: Copy + + serde::Serialize + + serde::de::DeserializeOwned + + TryFrom + + TryInto + + FromStr; + + #[inline] + fn into_ruint(self) -> Self::Ruint { + // We have to use `Try*` traits because `From` is not implemented by ruint types. + // They shouldn't ever error. + self.try_into().ok().unwrap() + } + + #[inline] + fn from_ruint(ruint: Self::Ruint) -> Self { + // We have to use `Try*` traits because `From` is not implemented by ruint types. + // They shouldn't ever error. + ruint.try_into().ok().unwrap() + } + } + + macro_rules! impl_from_ruint { + ($($primitive:ty = $ruint:ty),* $(,)?) => { + $( + impl ConvertRuint for $primitive { + type Ruint = $ruint; + } + )* + }; + } + + impl_from_ruint! { + bool = alloy_primitives::ruint::aliases::U1, + u8 = alloy_primitives::U8, + u16 = alloy_primitives::U16, + u32 = alloy_primitives::U32, + u64 = alloy_primitives::U64, + u128 = alloy_primitives::U128, + } +} diff --git a/kona/deny.toml b/kona/deny.toml new file mode 100644 index 0000000000000..89f44ed4899c8 --- /dev/null +++ b/kona/deny.toml @@ -0,0 +1,82 @@ +[graph] +targets = [] +all-features = false +no-default-features = false + +[output] +feature-depth = 1 + +[advisories] +ignore = [ + # paste crate is no longer maintained. + "RUSTSEC-2024-0436", + "RUSTSEC-2024-0384", + "RUSTSEC-2025-0012", +] + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unlicense", + "Unicode-3.0", + "MPL-2.0", + "Zlib", + "0BSD", + "CDLA-Permissive-2.0", +] +confidence-threshold = 0.8 +exceptions = [ + # CC0 is a permissive license but somewhat unclear status for source code + # so we prefer to not have dependencies using it + # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal + { allow = ["CC0-1.0"], name = "secp256k1" }, + { allow = ["CC0-1.0"], name = "aurora-engine-modexp" }, + { allow = ["CC0-1.0"], name = "secp256k1-sys" }, + { allow = ["CC0-1.0"], name = "tiny-keccak" }, + { allow = ["CC0-1.0"], name = "notify" }, + # aws-lc-sys includes OpenSSL in its composite license expression + { allow = ["OpenSSL"], name = "aws-lc-sys" }, +] + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[licenses.private] +ignore = false +registries = [] + +[bans] +multiple-versions = "warn" +wildcards = "allow" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" +allow = [] +deny = [] +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [ + "https://github.com/paradigmxyz/reth", + "https://github.com/alloy-rs/hardforks", + "https://github.com/bluealloy/revm", + "https://github.com/alloy-rs/evm", + "https://github.com/paritytech/jsonrpsee", + "https://github.com/flashbots/rollup-boost", +] + +[sources.allow-org] +github = [] +gitlab = [] +bitbucket = [] diff --git a/kona/docker/README.md b/kona/docker/README.md new file mode 100644 index 0000000000000..4a8dd09ab62d2 --- /dev/null +++ b/kona/docker/README.md @@ -0,0 +1,66 @@ +# `docker` + +This directory contains all of the repositories' dockerfiles as well as the [bake file](https://docs.docker.com/build/bake/) +used to define this repository's docker build configuration. In addition, the [recipes](./recipes) directory contains +example deployment strategies + grafana dashboards for applications such as [`kona-node`](../bin/node). + +## Install Dependencies + +* `docker`: https://www.docker.com/get-started/ +* `docker-buildx`: https://github.com/docker/buildx?tab=readme-ov-file#installing + +## Building Locally + +To build any image in the bake file locally, use `docker buildx bake`: + +```sh +# The target is one of the available bake targets within the `docker-bake.hcl`. +# A list can be viewed by running `docker buildx bake --list-targets` +export TARGET="" + +(cd "$(git rev-parse --show-toplevel)" && docker buildx bake \ + --progress plain \ + -f docker/docker-bake.hcl \ + $TARGET) +``` + +### Build Options + +Relevant build options (variables) for each target can be viewed by running `docker buildx bake --list-variables` or +manually inspecting the targets in the `docker-bake.hcl`. + +#### Troubleshooting + +If you receive an error like the following: + +``` +ERROR: Multi-platform build is not supported for the docker driver. +Switch to a different driver, or turn on the containerd image store, and try again. +Learn more at https://docs.docker.com/go/build-multi-platform/ +``` + +Create and activate a new builder and retry the bake command. + +```sh +docker buildx create --name kona-builder --use +``` + +## Cutting a Release (for maintainers / forks) + +To cut a release of the docker image for any of the targets, cut a new annotated tag for the target like so: + +```sh +# Example formats: +# - `kona-host/v0.1.0-beta.8` +# - `cannon-builder/v1.2.0` +TAG="/" +git tag -a $TAG -m "" && git push origin tag $TAG +``` + +To run the workflow manually, navigate over to the ["Build and Publish Docker Image"](https://github.com/op-rs/kona/actions/workflows/docker.yaml) +action. From there, run a `workflow_dispatch` trigger, select the tag you just pushed, and then finally select the image to release. + +Or, if you prefer to use the `gh` CLI, you can run: +```sh +gh workflow run "Build and Publish Docker Image" --ref -f image_to_release= +``` diff --git a/kona/docker/apps/README.md b/kona/docker/apps/README.md new file mode 100644 index 0000000000000..12ad4a0b8402d --- /dev/null +++ b/kona/docker/apps/README.md @@ -0,0 +1,66 @@ +# `docker-apps` + +This directory contains a dockerfile for building any binary in the `kona` repository. It supports building both the +local repository as well as a remote revision. + +## Building + +To build an image for any binary within `kona` locally, use the `justfile` located in this directory: + +```sh +# Build an application image from the local repository +just build-local [image_tag (default: 'kona:local')] + +# Build an application image from a remote revision +just build-remote [image_tag (default: 'kona:local')] +``` + +### Configuration + +#### Image Platform + +By default, the `justfile` directives will build the image only for the host machine's platform. This can be overridden +by setting the `PLATFORMS` environment variable to a comma-separated list, like so: + +```sh +export PLATFORMS="linux/amd64,linux/arm64,linux/aarch64" +``` + +#### Cargo Build Profile + +By default, the `release` profile will be used for the application's build. This can be overridden by setting the +`BUILD_PROFILE` environment variable to the desired profile, like so: + +```sh +export BUILD_PROFILE="debug" +``` + +## Publishing App Images + +The `generic` target in the [`docker-bake.hcl`](../docker-bake.hcl) supports publishing application binary images for +any binary in the `kona` repository. Optionally, if a custom target is desired, it can be overridden in the +[`docker-bake.hcl`](../docker-bake.hcl) like so: + +```hcl +target "" { + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "docker/apps/.dockerfile" + args = { + REPO_LOCATION = "${REPO_LOCATION}" + REPOSITORY = "${REPOSITORY}" + TAG = "${GIT_REF_NAME}" + BIN_TARGET = "" + BUILD_PROFILE = "${BUILD_PROFILE}" + } + platforms = split(",", PLATFORMS) +} +``` + +The [docker release workflow](../../.github/workflows/docker.yaml) will **first** check if a target is available, +using that target for the docker build. If the workflow can't find the target, it will fallback to the "generic" +target specified in the [`docker-bake.hcl`](../docker-bake.hcl). + +To cut a release for a generic binary, or an overridden target for that matter, follow the guidelines specified +in the ["cutting a release"](../README.md#cutting-a-release-for-maintainers--forks) section. This workflow allows +you to trigger a release just by pushing a tag to kona, for any binary. No code changes needed :) diff --git a/kona/docker/apps/entrypoint.sh b/kona/docker/apps/entrypoint.sh new file mode 100755 index 0000000000000..0a5e4063983d8 --- /dev/null +++ b/kona/docker/apps/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec "${BIN_TARGET}" "$@" diff --git a/kona/docker/apps/justfile b/kona/docker/apps/justfile new file mode 100644 index 0000000000000..dc58332131ed1 --- /dev/null +++ b/kona/docker/apps/justfile @@ -0,0 +1,48 @@ +DOCKER_JUSTFILE := source_directory() + +_docker_arch: + #!/bin/bash + if [[ -z "$PLATFORMS" ]]; then + echo $(docker system info --format '{{"{{"}}.OSType{{"}}"}}/{{"{{"}}.Architecture{{"}}"}}') + else + echo "$PLATFORMS" + fi + +# Builds an application image from the local repository. +build-local bin_name image_tag='kona:local' load_flag='': + #!/bin/bash + export BIN_TARGET="{{bin_name}}" + export DEFAULT_TAG="{{image_tag}}" + export PLATFORMS="$(just _docker_arch)" + export REPO_LOCATION="local" + + LOAD_FLAG="" + if [[ "{{load_flag}}" == "load" ]]; then + LOAD_FLAG="--load" + fi + + (cd {{DOCKER_JUSTFILE}}/../../ && docker buildx bake \ + --progress plain \ + -f docker/docker-bake.hcl \ + $LOAD_FLAG \ + generic) + +# Builds an application image from a remote revision. +build-remote bin_name git_tag='' image_tag='kona:local': + #!/bin/bash + export BIN_TARGET="{{bin_name}}" + export DEFAULT_TAG="{{image_tag}}" + export PLATFORMS="$(just _docker_arch)" + export REPO_LOCATION="remote" + + # If no git tag is provided, use `main` + if [[ -z "{{git_tag}}" ]]; then + export GIT_REF_NAME="main" + else + export GIT_REF_NAME="{{git_tag}}" + fi + + (cd {{DOCKER_JUSTFILE}}/../../ && docker buildx bake \ + --progress plain \ + -f docker/docker-bake.hcl \ + generic) diff --git a/kona/docker/apps/kona_app_generic.dockerfile b/kona/docker/apps/kona_app_generic.dockerfile new file mode 100644 index 0000000000000..919c1b20cf1ab --- /dev/null +++ b/kona/docker/apps/kona_app_generic.dockerfile @@ -0,0 +1,123 @@ +ARG REPO_LOCATION + +################################ +# Dependency Installation # +# Stage # +################################ +FROM ubuntu:22.04 AS dep-setup-stage +SHELL ["/bin/bash", "-c"] + +# Install deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + curl \ + ca-certificates \ + libssl-dev \ + clang \ + pkg-config + +# Install rust +ENV RUST_VERSION=1.88 +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain ${RUST_VERSION} --profile minimal +ENV PATH="/root/.cargo/bin:${PATH}" + +# Install cargo-binstall +RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + +RUN cargo binstall cargo-chef -y + +################################ +# Local Repo Setup Stage # +################################ +FROM dep-setup-stage AS app-local-setup-stage + +# Copy in the local repository +COPY . /kona + +################################ +# Remote Repo Setup Stage # +################################ +FROM dep-setup-stage AS app-remote-setup-stage +SHELL ["/bin/bash", "-c"] + +ARG TAG +ARG REPOSITORY + +# Clone kona at the specified tag +RUN git clone https://github.com/${REPOSITORY} && \ + cd kona && \ + git checkout "${TAG}" + +################################ +# App Build Stage # +################################ +FROM app-${REPO_LOCATION}-setup-stage AS app-setup + +# We need a separate entrypoint to take advantage of docker's cache. +# If we didn't do this, the full build would be triggered every time the source code changes. +FROM dep-setup-stage AS build-entrypoint +ARG BIN_TARGET +ARG BUILD_PROFILE + +WORKDIR /app + +FROM build-entrypoint AS planner +# Triggers a cache invalidation if `app-setup` is modified. +COPY --from=app-setup kona . +RUN cargo chef prepare --recipe-path recipe.json + +FROM build-entrypoint AS builder +# Since we only copy recipe.json, if the dependencies don't change, this step and the next one will be cached. +COPY --from=planner /app/recipe.json recipe.json + +# Build dependencies - this is the caching Docker layer! +RUN RUSTFLAGS="-C target-cpu=generic" cargo chef cook --bin "${BIN_TARGET}" --profile "${BUILD_PROFILE}" --recipe-path recipe.json + +# Build application. This step will systematically trigger a cache invalidation if the source code changes. +COPY --from=app-setup kona . +# Build the application binary on the selected tag. Since we build the external dependencies in the previous step, +# this step will reuse the target directory from the previous step. +RUN RUSTFLAGS="-C target-cpu=generic" cargo build --bin "${BIN_TARGET}" --profile "${BUILD_PROFILE}" + +# Export stage +FROM ubuntu:22.04 AS export-stage +SHELL ["/bin/bash", "-c"] + +ARG BIN_TARGET +ARG BUILD_PROFILE + +# Fixed non-root user/group for runtime +ARG UID=10001 +ARG GID=10001 + +# Install ca-certificates and libssl-dev for TLS support. +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root runtime user +RUN groupadd --gid ${GID} app \ + && useradd --uid ${UID} --gid ${GID} \ + --home-dir /home/app --create-home \ + --shell /usr/sbin/nologin \ + app + +# Copy in the binary from the build image. +COPY --from=builder "app/target/${BUILD_PROFILE}/${BIN_TARGET}" "/usr/local/bin/${BIN_TARGET}" + +# Copy in the entrypoint script. +COPY ./docker/apps/entrypoint.sh /entrypoint.sh + +# Ensure the entrypoint and binary are executable and readable by the non-root user +RUN chmod 0555 "/usr/local/bin/${BIN_TARGET}" \ + && chmod 0555 /entrypoint.sh + +# Export the binary name to the environment. +ENV BIN_TARGET="${BIN_TARGET}" + +# Drop privileges +USER ${UID}:${GID} + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/kona/docker/asterisc/asterisc.dockerfile b/kona/docker/asterisc/asterisc.dockerfile new file mode 100644 index 0000000000000..2abe5412cd49c --- /dev/null +++ b/kona/docker/asterisc/asterisc.dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:22.04 + +ENV SHELL=/bin/bash +ENV DEBIAN_FRONTEND noninteractive + +# todo: pin `nightly` version +ENV RUST_VERSION nightly + +RUN apt-get update && apt-get install --assume-yes --no-install-recommends \ + ca-certificates \ + build-essential \ + curl \ + g++-riscv64-linux-gnu \ + libc6-dev-riscv64-cross \ + binutils-riscv64-linux-gnu \ + llvm \ + clang \ + make \ + cmake \ + git + +# Install Rustup and Rust +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain ${RUST_VERSION} --component rust-src +ENV PATH="/root/.cargo/bin:${PATH}" + +# Set up the env vars to instruct rustc to use the correct compiler and linker +# and to build correctly to support the Cannon processor +ENV CC_riscv64_unknown_none_elf=riscv64-linux-gnu-gcc \ + CXX_riscv64_unknown_none_elf=riscv64-linux-gnu-g++ \ + CARGO_TARGET_RISCV64_UNKNOWN_NONE_ELF_LINKER=riscv64-linux-gnu-gcc \ + RUSTFLAGS="-Clink-arg=-e_start -Ctarget-feature=-c,-zicsr,-zifencei,-zicntr,zihpm" \ + CARGO_BUILD_TARGET="riscv64imac-unknown-none-elf" \ + RUSTUP_TOOLCHAIN=${RUST_VERSION} diff --git a/kona/docker/cannon/cannon.dockerfile b/kona/docker/cannon/cannon.dockerfile new file mode 100644 index 0000000000000..1ee23f2117763 --- /dev/null +++ b/kona/docker/cannon/cannon.dockerfile @@ -0,0 +1,36 @@ +FROM ubuntu:22.04 + +ENV SHELL=/bin/bash +ENV DEBIAN_FRONTEND=noninteractive + +# todo: pin `nightly` version +ENV RUST_VERSION=nightly + +RUN apt-get update && apt-get install --assume-yes --no-install-recommends \ + ca-certificates \ + build-essential \ + curl \ + g++-mips64-linux-gnuabi64 \ + libc6-dev-mips64-cross \ + binutils-mips64-linux-gnuabi64 \ + llvm \ + clang \ + make \ + cmake \ + git + +# Install Rustup and Rust +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain ${RUST_VERSION} --component rust-src +ENV PATH="/root/.cargo/bin:${PATH}" + +# Add the special cannon build target +COPY ./mips64-unknown-none.json . + +# Set up the env vars to instruct rustc to use the correct compiler and linker +# and to build correctly to support the Cannon processor +ENV CC_mips64_unknown_none=mips64-linux-gnuabi64-gcc \ + CXX_mips64_unknown_none=mips64-linux-gnuabi64-g++ \ + CARGO_TARGET_MIPS64_UNKNOWN_NONE_LINKER=mips64-linux-gnuabi64-gcc \ + RUSTFLAGS="-Clink-arg=-e_start -Cllvm-args=-mno-check-zero-division" \ + CARGO_BUILD_TARGET="/mips64-unknown-none.json" \ + RUSTUP_TOOLCHAIN=${RUST_VERSION} diff --git a/kona/docker/cannon/mips64-unknown-none.json b/kona/docker/cannon/mips64-unknown-none.json new file mode 100644 index 0000000000000..1f8a116149efd --- /dev/null +++ b/kona/docker/cannon/mips64-unknown-none.json @@ -0,0 +1,20 @@ +{ + "arch": "mips64", + "cpu": "mips64", + "llvm-target": "mips64-unknown-none", + "data-layout": "E-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128", + "target-endian": "big", + "target-pointer-width": 64, + "target-c-int-width": 64, + "os": "none", + "llvm-abiname": "n64", + "features": "+soft-float,+mips64", + "max-atomic-width": 64, + "linker": "rust-lld", + "linker-flavor": "ld.lld", + "executables": true, + "panic-strategy": "abort", + "relocation-model": "static", + "emit-debug-gdb-scripts": false, + "singlethread": true +} diff --git a/kona/docker/docker-bake.hcl b/kona/docker/docker-bake.hcl new file mode 100644 index 0000000000000..989951152dfbe --- /dev/null +++ b/kona/docker/docker-bake.hcl @@ -0,0 +1,176 @@ +//////////////////////////////////////////////////////////////// +// Globals // +//////////////////////////////////////////////////////////////// + +variable "REGISTRY" { + default = "ghcr.io" +} + +variable "REPOSITORY" { + default = "op-rs/kona" +} + +variable "DEFAULT_TAG" { + default = "kona:local" + description = "The tag to use for the built image." +} + +variable "PLATFORMS" { + default = "linux/amd64,linux/arm64" + description = "The platforms to build the image for, separated by commas." +} + +variable "GIT_REF_NAME" { + default = "main" + description = "The git reference name. This is typically the branch name, commit hash, or tag." +} + +variable "HOST_UID" { + default = "1000" + description = "The UID of the host user for volume permissions." +} + +variable "HOST_GID" { + default = "1000" + description = "The GID of the host user for volume permissions." +} + +// Special target: https://github.com/docker/metadata-action#bake-definition +target "docker-metadata-action" { + description = "Special target used with `docker/metadata-action`" + tags = ["${DEFAULT_TAG}"] +} + +//////////////////////////////////////////////////////////////// +// App Images // +//////////////////////////////////////////////////////////////// + +variable "REPO_LOCATION" { + default = "remote" + description = "The location of the repository to build in the kona-app-generic target. Valid options: local (uses local repo, ignores `GIT_REF_NAME`), remote (clones `kona`, checks out `GIT_REF_NAME`)" +} + +variable "BIN_TARGET" { + default = "kona-host" + description = "The binary target to build in the kona-app-generic target." +} + +variable "BUILD_PROFILE" { + default = "release-perf" + description = "The cargo build profile to use when building the binary in the kona-app-generic target." +} + +target "generic" { + description = "Generic kona app image" + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "docker/apps/kona_app_generic.dockerfile" + args = { + REPO_LOCATION = "${REPO_LOCATION}" + REPOSITORY = "${REPOSITORY}" + TAG = "${GIT_REF_NAME}" + BIN_TARGET = "${BIN_TARGET}" + BUILD_PROFILE = "${BUILD_PROFILE}" + } + platforms = split(",", PLATFORMS) +} + +//////////////////////////////////////////////////////////////// +// Proof Images // +//////////////////////////////////////////////////////////////// + +variable "ASTERISC_TAG" { + // The tag of `asterisc` to use in the `kona-asterisc-prestate` target. + // + // You can override this if you'd like to use a different tag to generate the prestate. + // https://github.com/ethereum-optimism/asterisc/releases + default = "v1.3.0" + description = "The tag of asterisc to use in the kona-asterisc-prestate target." +} + +variable "CANNON_TAG" { + // The tag of `cannon` to use in the `kona-cannon-prestate` target. + // + // You can override this if you'd like to use a different tag to generate the prestate. + // https://github.com/ethereum-optimism/optimism/releases + default = "cannon/v1.5.0-alpha.1" + description = "The tag of cannon to use in the kona-cannon-prestate target." +} + +variable "CLIENT_BIN" { + // The `kona-client` binary to use in the `kona-{asterisc/cannon}-prestate` targets. + // + // You can override this if you'd like to use a different `kona-client` binary to generate + // the prestate. + // + // Valid options: + // - `kona` (single-chain) + // - `kona-int` (interop) + default = "kona" + description = "The kona-client binary to use in the proof prestate targets. Valid options: kona, kona-int" +} + +variable "KONA_CUSTOM_CONFIGS" { + // Used to build a kona prestate using custom chain configurations + default = "false" + description = "Enables custom chain configurations to be built into kona artifacts" +} + +variable "CUSTOM_CONFIGS_CONTEXT" { + // The build context for custom chain configurations to add to the prestate build + default = "" + description = "The build context for custom chain configurations to add to the prestate build" +} + + +target "asterisc-builder" { + description = "Rust build environment for bare-metal RISC-V 64-bit IMA (Asterisc FPVM ISA)" + inherits = ["docker-metadata-action"] + context = "docker/asterisc" + dockerfile = "asterisc.dockerfile" + platforms = split(",", PLATFORMS) +} + +target "cannon-builder" { + description = "Rust build environment for bare-metal MIPS64r1 (Cannon FPVM ISA)" + inherits = ["docker-metadata-action"] + context = "docker/cannon" + dockerfile = "cannon.dockerfile" + args = { + HOST_UID = "${HOST_UID}" + HOST_GID = "${HOST_GID}" + } + platforms = split(",", PLATFORMS) +} + +target "kona-asterisc-prestate" { + description = "Prestate builder for kona-client with Asterisc FPVM" + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "docker/fpvm-prestates/asterisc-repro.dockerfile" + args = { + CLIENT_BIN = "${CLIENT_BIN}" + CLIENT_TAG = "${GIT_REF_NAME}" + ASTERISC_TAG = "${ASTERISC_TAG}" + } + # Only build on linux/amd64 for reproducibility. + platforms = ["linux/amd64"] +} + +target "kona-cannon-prestate" { + description = "Prestate builder for kona-client with Cannon FPVM" + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "docker/fpvm-prestates/cannon-repro.dockerfile" + contexts = { + custom_configs = "${CUSTOM_CONFIGS_CONTEXT}" + } + args = { + CLIENT_BIN = "${CLIENT_BIN}" + CLIENT_TAG = "${GIT_REF_NAME}" + CANNON_TAG = "${CANNON_TAG}" + KONA_CUSTOM_CONFIGS = "${KONA_CUSTOM_CONFIGS}" + } + # Only build on linux/amd64 for a single source of reproducibility. + platforms = ["linux/amd64"] +} diff --git a/kona/docker/fpvm-prestates/README.md b/kona/docker/fpvm-prestates/README.md new file mode 100644 index 0000000000000..f30f4ed7872aa --- /dev/null +++ b/kona/docker/fpvm-prestates/README.md @@ -0,0 +1,37 @@ +# `fpvm-prestates` + +Images for creating reproducible `kona-client` prestate builds for supported fault proof virtual machines. + +## Usage + +### All prestate artifacts + +```sh +# Produce the prestate artifacts for `kona-client` running on `asterisc` and `cannon` +# (FPVM versions specified by `asterisc_tag` + `cannon_tag`) +just all +``` + +### `kona-client` + `asterisc` prestate artifacts + +```sh +# Produce the prestate artifacts for `kona-client` running on `asterisc` (version specified by `asterisc_tag`) +just asterisc +``` + +### `kona-client` + `cannon` prestate artifacts + +```sh +# Produce the prestate artifacts for `kona-client` running on `cannon` (version specified by `cannon_tag`) +just cannon +``` + +### `kona-client` + `cannon` prestate artifacts for custom chains + +To create a reproducible kona-client prestate build that supports custom or devnet chain configurations that are not in the superchain-registry: + +```sh +# Produce the prestate artifacts for `kona-client` running on `cannon` (version specified by `cannon_tag`) +just cannon +``` + diff --git a/kona/docker/fpvm-prestates/asterisc-repro.dockerfile b/kona/docker/fpvm-prestates/asterisc-repro.dockerfile new file mode 100644 index 0000000000000..b32f9baad8580 --- /dev/null +++ b/kona/docker/fpvm-prestates/asterisc-repro.dockerfile @@ -0,0 +1,107 @@ +################################################################ +# Build Asterisc @ `ASTERISC_TAG` # +################################################################ + +FROM ubuntu:22.04 AS asterisc-build +SHELL ["/bin/bash", "-c"] + +ARG TARGETARCH +ARG ASTERISC_TAG + +# Install deps +RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates make + +ENV GO_VERSION=1.22.7 + +# Fetch go manually, rather than using a Go base image, so we can copy the installation into the final stage +RUN curl -sL https://go.dev/dl/go$GO_VERSION.linux-$TARGETARCH.tar.gz -o go$GO_VERSION.linux-$TARGETARCH.tar.gz && \ + tar -C /usr/local/ -xzf go$GO_VERSION.linux-$TARGETARCH.tar.gz +ENV GOPATH=/go +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Clone and build Asterisc @ `ASTERISC_TAG` +RUN git clone https://github.com/ethereum-optimism/asterisc && \ + cd asterisc && \ + git checkout $ASTERISC_TAG && \ + make && \ + cp rvgo/bin/asterisc /asterisc-bin + +################################################################ +# Build kona-client @ `CLIENT_TAG` # +################################################################ + +FROM ghcr.io/op-rs/kona/asterisc-builder:0.3.0 AS client-build +SHELL ["/bin/bash", "-c"] + +ARG CLIENT_BIN +ARG CLIENT_TAG + +# Install deps +RUN apt-get update && apt-get install -y --no-install-recommends git + +# Clone kona at the specified tag +RUN git clone https://github.com/op-rs/kona + +# Build kona-client on the selected tag +RUN cd kona && \ + git checkout $CLIENT_TAG && \ + cargo build -Zbuild-std=core,alloc -p kona-client --bin $CLIENT_BIN --locked --profile release-client-lto && \ + mv ./target/riscv64imac-unknown-none-elf/release-client-lto/$CLIENT_BIN /kona-client-elf + +################################################################ +# Create `prestate.bin.gz` + `prestate-proof.json` # +################################################################ + +FROM ubuntu:22.04 AS prestate-build +SHELL ["/bin/bash", "-c"] + +ARG UID=10001 +ARG GID=10001 + +RUN groupadd --gid ${GID} app \ + && useradd --uid ${UID} --gid ${GID} \ + --home-dir /home/app --create-home \ + --shell /usr/sbin/nologin \ + app + +# Use a writable workspace owned by the non-root user +WORKDIR /work +RUN chown ${UID}:${GID} /work + +# Copy asterisc binary +COPY --from=asterisc-build /asterisc-bin /work/asterisc + +# Copy kona-client binary +COPY --from=client-build /kona-client-elf /work/kona-client-elf + +# Make the binaries executable +RUN chmod 0555 /work/asterisc /work/kona-client-elf + +USER ${UID}:${GID} + +# Create `prestate.bin.gz` +RUN /work/asterisc load-elf \ + --path=/work/kona-client-elf \ + --out=/work/prestate.bin.gz + +# Create `prestate-proof.json` +RUN /work/asterisc run \ + --proof-at "=0" \ + --stop-at "=1" \ + --input /work/prestate.bin.gz \ + --meta /work/meta.json \ + --proof-fmt "/work/%d.json" \ + --output "" && \ + mv /work/0.json /work/prestate-proof.json + +################################################################ +# Export Artifacts # +################################################################ + +FROM scratch AS export-stage + +COPY --from=prestate-build /work/asterisc . +COPY --from=prestate-build /work/kona-client-elf . +COPY --from=prestate-build /work/prestate.bin.gz . +COPY --from=prestate-build /work/prestate-proof.json . +COPY --from=prestate-build /work/meta.json . diff --git a/kona/docker/fpvm-prestates/cannon-repro.dockerfile b/kona/docker/fpvm-prestates/cannon-repro.dockerfile new file mode 100644 index 0000000000000..fc73399d07f6f --- /dev/null +++ b/kona/docker/fpvm-prestates/cannon-repro.dockerfile @@ -0,0 +1,114 @@ +################################################################ +# Build Cannon @ `CANNON_TAG` # +################################################################ + +FROM ubuntu:22.04 AS cannon-build +SHELL ["/bin/bash", "-c"] + +ARG TARGETARCH +ARG CANNON_TAG + +# Install deps +RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates make + +ENV GO_VERSION=1.23.8 + +# Fetch go manually, rather than using a Go base image, so we can copy the installation into the final stage +RUN curl -sL https://go.dev/dl/go$GO_VERSION.linux-$TARGETARCH.tar.gz -o go$GO_VERSION.linux-$TARGETARCH.tar.gz && \ + tar -C /usr/local/ -xzf go$GO_VERSION.linux-$TARGETARCH.tar.gz +ENV GOPATH=/go +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Clone and build Cannon @ `CANNON_TAG` +RUN git clone https://github.com/ethereum-optimism/optimism && \ + cd optimism/cannon && \ + git checkout $CANNON_TAG && \ + make && \ + cp bin/cannon /cannon-bin + +################################################################ +# Build kona-client @ `CLIENT_TAG` # +################################################################ + +FROM ghcr.io/op-rs/kona/cannon-builder:0.3.0 AS client-build +SHELL ["/bin/bash", "-c"] + +ARG CLIENT_BIN +ARG CLIENT_TAG +ARG KONA_CUSTOM_CONFIGS + +COPY --from=custom_configs / /usr/local/kona-custom-configs + +# Install deps +RUN apt-get update && apt-get install -y --no-install-recommends git + +# Clone kona at the specified tag +RUN git clone https://github.com/op-rs/kona + +ENV KONA_CUSTOM_CONFIGS=$KONA_CUSTOM_CONFIGS +ENV KONA_CUSTOM_CONFIGS_DIR=/usr/local/kona-custom-configs + +# Build kona-client on the selected tag +RUN cd kona && \ + git checkout $CLIENT_TAG && \ + cargo build -Zbuild-std=core,alloc -p kona-client --bin $CLIENT_BIN --locked --profile release-client-lto && \ + mv ./target/mips64-unknown-none/release-client-lto/$CLIENT_BIN /kona-client-elf + +################################################################ +# Create `prestate.bin.gz` + `prestate-proof.json` # +################################################################ + +FROM ubuntu:22.04 AS prestate-build +SHELL ["/bin/bash", "-c"] + +ARG UID=10001 +ARG GID=10001 + +RUN groupadd --gid ${GID} app \ + && useradd --uid ${UID} --gid ${GID} \ + --home-dir /home/app --create-home \ + --shell /usr/sbin/nologin \ + app + +# Use a writable workspace owned by the non-root user +WORKDIR /work +RUN chown ${UID}:${GID} /work + +# Copy cannon binary +COPY --from=cannon-build /cannon-bin /work/cannon + +# Copy kona-client binary +COPY --from=client-build /kona-client-elf /work/kona-client-elf + +# Make the binaries executable +RUN chmod 0555 /work/cannon /work/kona-client-elf + +USER ${UID}:${GID} + +# Create `prestate.bin.gz` +RUN /work/cannon load-elf \ + --path=/work/kona-client-elf \ + --out=/work/prestate.bin.gz \ + --type multithreaded64-5 + +# Create `prestate-proof.json` +RUN /work/cannon run \ + --proof-at "=0" \ + --stop-at "=1" \ + --input /work/prestate.bin.gz \ + --meta /work/meta.json \ + --proof-fmt "/work/%d.json" \ + --output "" && \ + mv /work/0.json /work/prestate-proof.json + +################################################################ +# Export Artifacts # +################################################################ + +FROM scratch AS export-stage + +COPY --from=prestate-build /work/cannon . +COPY --from=prestate-build /work/kona-client-elf . +COPY --from=prestate-build /work/prestate.bin.gz . +COPY --from=prestate-build /work/prestate-proof.json . +COPY --from=prestate-build /work/meta.json . diff --git a/kona/docker/fpvm-prestates/justfile b/kona/docker/fpvm-prestates/justfile new file mode 100644 index 0000000000000..60ddc0353e722 --- /dev/null +++ b/kona/docker/fpvm-prestates/justfile @@ -0,0 +1,107 @@ +set positional-arguments +alias all := build-client-prestate-artifacts +alias cannon := build-client-prestate-cannon-artifacts +alias asterisc := build-client-prestate-asterisc-artifacts + +# default recipe to display help information +default: + @just --list + +# Build the `kona-client` prestate artifacts for the specified tags (asterisc + cannon). +build-client-prestate-artifacts kona_client_variant kona_tag asterisc_tag cannon_tag: + #!/bin/bash + + # Available variants of the client program, parsed from the binary targets of the `kona-client` crate. + manifest_path="$(realpath ../../bin/client/Cargo.toml)" + available_variants=($(cargo metadata --format-version=1 --manifest-path="$manifest_path" --no-deps | + jq -r --arg path "$manifest_path" '.packages[] | select(.manifest_path == $path) | .targets[] | select(.kind[] == "bin") | .name' | + xargs)) + + # Validates that `$1` is contained in `$2` + validate_option() { + local input="$1" + local valid_options=("${@:2}") + + for option in "${valid_options[@]}"; do + if [[ "$input" == "$option" ]]; then + return 0 + fi + done + + return 1 + } + + # Check if `kona_client_variant` is contained within the available variants. + if ! validate_option "{{kona_client_variant}}" "${available_variants[@]}"; then + echo "Invalid client program variant. Please choose from: ${available_variants[*]}" + exit 1 + fi + + just build-client-prestate-asterisc-artifacts {{kona_client_variant}} {{kona_tag}} {{asterisc_tag}} + just build-client-prestate-cannon-artifacts {{kona_client_variant}} {{kona_tag}} {{cannon_tag}} + +# Build the `kona-client` prestate artifacts for the latest release (asterisc). +build-client-prestate-asterisc-artifacts kona_client_variant kona_tag asterisc_tag out='./prestate-artifacts-asterisc': + #!/bin/bash + OUTPUT_DIR={{out}} + + # Docker bake env + export GIT_REF_NAME="{{kona_tag}}" + export CLIENT_BIN="{{kona_client_variant}}" + export ASTERISC_TAG="{{asterisc_tag}}" + export DEFAULT_TAG="kona-asterisc-prestate:local" + + # Navigate to workspace root + cd ../.. + + # Create the output directory + mkdir -p $OUTPUT_DIR + + echo "Building kona-client (variant: {{kona_client_variant}}) prestate artifacts for the asterisc target. 🐚 Kona Tag: {{kona_tag}} | 🎇 Asterisc Tag: {{asterisc_tag}}" + docker buildx bake \ + --set "*.output=$OUTPUT_DIR" \ + -f docker/docker-bake.hcl \ + kona-asterisc-prestate + +# Build the `kona-client` prestate artifacts for the latest release (cannon). +build-client-prestate-cannon-artifacts \ + kona_client_variant \ + kona_tag cannon_tag \ + out='./prestate-artifacts-cannon' \ + custom_config_dir='': + #!/bin/bash + OUTPUT_DIR={{out}} + + # Docker bake env + export GIT_REF_NAME="{{kona_tag}}" + export CLIENT_BIN="{{kona_client_variant}}" + export CANNON_TAG="{{cannon_tag}}" + export DEFAULT_TAG="kona-cannon-prestate:local" + + # Navigate to workspace root + cd ../.. + + if [[ -n "{{custom_config_dir}}" ]]; then + export KONA_CUSTOM_CONFIGS="true" + export CUSTOM_CONFIGS_CONTEXT="{{custom_config_dir}}" + if [ ! -d "{{custom_config_dir}}" ]; then + echo "Invalid custom config directory: {{custom_config_dir}}" + exit 1 + fi + echo "Using custom config directory: {{custom_config_dir}}" + else + # set to an empty directory to satisfy the docker build context requirement + TEMP_DIR=$(mktemp -d) + trap "rm -rf $TEMP_DIR" EXIT + export CUSTOM_CONFIGS_CONTEXT="$TEMP_DIR" + fi + + # Create the output directory + mkdir -p $OUTPUT_DIR + + echo "Building kona-client (variant: {{kona_client_variant}}) prestate artifacts for the cannon target. 🐚 Kona Tag: {{kona_tag}} | 🔫 Cannon Tag: {{cannon_tag}}" + docker buildx bake \ + --set "*.output=$OUTPUT_DIR" \ + -f docker/docker-bake.hcl \ + --allow fs=${CUSTOM_CONFIGS_CONTEXT} \ + kona-cannon-prestate diff --git a/kona/docker/recipes/kona-node/.gitignore b/kona/docker/recipes/kona-node/.gitignore new file mode 100644 index 0000000000000..eedad3063a688 --- /dev/null +++ b/kona/docker/recipes/kona-node/.gitignore @@ -0,0 +1,2 @@ +# JWT secret +jwttoken diff --git a/kona/docker/recipes/kona-node/README.md b/kona/docker/recipes/kona-node/README.md new file mode 100644 index 0000000000000..4d5a9a08c2216 --- /dev/null +++ b/kona/docker/recipes/kona-node/README.md @@ -0,0 +1,88 @@ +# `kona-node` recipe + +> [!WARNING] +> +> `kona-node` is in active development, and this recipe is subject to frequent change (and may not work!) For the time +> being, it is intended to be used for development purposes. Please [file an issue][new-issue] if you have any problems +> during development. + +This directory contains a simple `docker-compose` setup for `kona-node` and `op-reth`, including example Grafana +dashboards and a default Prometheus configuration. + +By default, this recipe is configured to sync the [`OP Sepolia`][op-sepolia] L2. + +## Usage + +### Running + +An L1 Execution Client RPC and L1 Beacon API endpoint must be configured in your environment. The `L1_PROVIDER_RPC` and +`L1_BEACON_API` environment variables can be set in [`cfg.env`](./cfg.env). + +Once these two environment variables are set, the environment can be spun up and shut down as follows: + +```sh +# Start `kona-node`, `op-reth`, and `grafana` + `prometheus` +just up + +# Shutdown the docker compose environment +just down + +# Restart the docker compose environment +just restart +``` + +### Grafana + +The grafana instance can be accessed at `http://localhost:3000` in your browser. The username and password, by default, +are both `admin`. + +#### Adding a new visualization + +The `kona-node` dashboard is provisioned within the grafana instance by default. A new visualization can be added to the +dashboard by navigating to the `Kona Node` dashboard, and then clicking `Add` > `Visualization` in the top right. + +Once your visualization has been added, click `Share` > `Export` (tab), and toggle "Export for sharing externally" on. +Then, copy the JSON, and replace the contents of [`overview.json`](./grafana/dashboards/overview.json) +before making a PR. + +## Default Ports + +| Port | Service | +|---------|-----------------------------| +| `9223` | `kona-node` discovery | +| `9002` | `kona-node` metrics | +| `5060` | `kona-node` RPC | +| `30303` | `op-reth` discovery | +| `9001` | `op-reth` metrics | +| `8545` | `op-reth` RPC | +| `8551` | `op-reth` engine | +| `9090` | `prometheus` metrics server | +| `3000` | `grafana` dashboard UI | + +## Configuration + +### Adjusting host ports + +Host ports for both `op-reth` and `kona-node` can be configured in [`cfg.env`](./cfg.env). + +### Syncing a different OP Stack chain + +To adjust the chain that the node is syncing, you must modify the `docker-compose.yml` file to specify the desired +network parameters. Specifically: +1. Ensure `L1_PROVIDER_RPC` and `L1_BEACON_API` are set to L1 clients that represent the settlement layer of the L2. +1. `op-reth` + - `--chain` must specify the desired chain. + - `--rollup.sequencer-http` must specify the sequencer endpoint. +1. `kona-node` + - `--chain` must specify the chain ID of the desired chain. + +### Adjusting log filters + +Log filters can be adjusted by setting the `RUST_LOG` environment variable. This environment variable will be forwarded +to the `kona-node` container's entrypoint. + +Example: `export RUST_LOG=engine_builder=trace,runtime=debug` + +[op-sepolia]: https://sepolia-optimism.etherscan.io +[op-reth]: https://github.com/paradigmxyz/reth +[new-issue]: https://github.com/op-rs/kona/issues/new diff --git a/kona/docker/recipes/kona-node/cfg.env b/kona/docker/recipes/kona-node/cfg.env new file mode 100644 index 0000000000000..18e11a8443297 --- /dev/null +++ b/kona/docker/recipes/kona-node/cfg.env @@ -0,0 +1,56 @@ +################# +# kona-node # +################# + +# Required - L1 EL RPC with `eth` & `debug` namespaces exposed. +L1_PROVIDER_RPC= +# Required - L1 CL API with: +# - `eth/v1/config/spec` +# - `eth/v1/beacon/genesis` +# - `eth/v1/beacon/blob_sidecars` +# exposed. +L1_BEACON_API= + +# (default: 9223) +KONA_NODE_DISCOVERY_PORT= +# (default: 9002) +KONA_NODE_METRICS_PORT= +# (default: 5060) +KONA_NODE_RPC_PORT= + +# (default: ghcr.io/op-rs/kona/kona-node:latest) +KONA_NODE_IMAGE= + +################# +# op-reth # +################# + +# (default: 9001) +OP_RETH_METRICS_PORT= +# (default: 30303) +OP_RETH_DISCOVERY_PORT= +# (default: 8545) +OP_RETH_RPC_PORT= +# (default: 8551) +OP_RETH_ENGINE_PORT= + +# (default: ghcr.io/paradigmxyz/op-reth:latest) +OP_RETH_IMAGE= + +################# +# grafana # +################# +# (default: 3000) +GRAFANA_PORT= + +################# +# prometheus # +################# +# (default: 9090) +PROMETHEUS_PORT= + +####################### +# docker cluster name # +####################### +# (default: kona-node) +CLUSTER_NAME= \ No newline at end of file diff --git a/kona/docker/recipes/kona-node/docker-compose.yaml b/kona/docker/recipes/kona-node/docker-compose.yaml new file mode 100644 index 0000000000000..95bc7b4e19a8d --- /dev/null +++ b/kona/docker/recipes/kona-node/docker-compose.yaml @@ -0,0 +1,101 @@ +name: '${CLUSTER_NAME:-kona-node}' + +services: + prometheus: + restart: unless-stopped + image: prom/prometheus:latest + volumes: + - "prometheus_data:/prometheus" + - "./prometheus:/etc/prometheus" + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + ports: + - "${PROMETHEUS_PORT:-9090}:9090" + + grafana: + restart: unless-stopped + image: grafana/grafana:latest + depends_on: + - prometheus + ports: + - "${GRAFANA_PORT:-3000}:3000" + environment: + PROMETHEUS_URL: ${PROMETHEUS_URL:-http://prometheus:9090} + volumes: + - "grafana_data:/var/lib/grafana" + - "./grafana/datasources:/etc/grafana/provisioning/datasources" + - "./grafana/dashboards:/etc/grafana/provisioning_temp/dashboards" + entrypoint: > + sh -c "cp -r /etc/grafana/provisioning_temp/dashboards/. /etc/grafana/provisioning/dashboards && + find /etc/grafana/provisioning/dashboards/ -name '*.json' -exec sed -i 's/$${DS_PROMETHEUS}/Prometheus/g' {} \+ && + /run.sh" + + op-reth: + restart: unless-stopped + image: ${OP_RETH_NODE_IMAGE:-ghcr.io/paradigmxyz/op-reth:latest} + depends_on: + - prometheus + ports: + - "${OP_RETH_METRICS_PORT:-9001}:9001" # metrics + - "${OP_RETH_DISCOVERY_PORT:-30303}:30303/tcp" # discovery + - "${OP_RETH_DISCOVERY_PORT:-30303}:30303/udp" # discovery + - "${OP_RETH_RPC_PORT:-8545}:8545" # rpc + - "${OP_RETH_ENGINE_PORT:-8551}:8551" # engine + volumes: + - "reth_data:/db" + - "logs:/root/logs" + - "./jwttoken:/root/jwt:ro" + command: > + node + --datadir /db + --chain optimism-sepolia + --rollup.sequencer-http https://sepolia-sequencer.optimism.io/ + --metrics 0.0.0.0:9001 + --log.file.directory /root/logs + --authrpc.addr 0.0.0.0 --authrpc.port 8551 --authrpc.jwtsecret /root/jwt/jwt.hex + --http --http.addr 0.0.0.0 --http.port 8545 --http.api "eth,net,web3,debug" + + kona-node: + restart: unless-stopped + image: ${KONA_NODE_IMAGE:-ghcr.io/op-rs/kona/kona-node:latest} + depends_on: + - prometheus + - op-reth + ports: + - "${KONA_NODE_DISCOVERY_PORT:-9223}:9223/tcp" # discovery + - "${KONA_NODE_DISCOVERY_PORT:-9223}:9223/udp" # discovery + - "${KONA_NODE_METRICS_PORT:-9002}:9002" # metrics + - "${KONA_NODE_RPC_PORT:-5060}:5060" # rpc + volumes: + - "kona_data:/db" + - "./jwttoken:/root/jwt:ro" + environment: + L1_PROVIDER_RPC: ${L1_PROVIDER_RPC:?} + L1_BEACON_API: ${L1_BEACON_API:?} + RUST_LOG: ${RUST_LOG:-} + command: > + --chain optimism-sepolia + --metrics.enabled + --metrics.port 9002 + node + --l1 $L1_PROVIDER_RPC + --l1-beacon $L1_BEACON_API + --l2 http://op-reth:8551 + --l2-engine-jwt-secret /root/jwt/jwt.hex + --rpc.port 5060 + --p2p.listen.tcp 9223 + --p2p.listen.udp 9223 + --p2p.bootstore /db + +volumes: + prometheus_data: + driver: local + grafana_data: + driver: local + reth_data: + driver: local + kona_data: + driver: local + logs: + driver: local diff --git a/kona/docker/recipes/kona-node/generate-jwt.sh b/kona/docker/recipes/kona-node/generate-jwt.sh new file mode 100755 index 0000000000000..5549ed83f24b8 --- /dev/null +++ b/kona/docker/recipes/kona-node/generate-jwt.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Borrowed from EthStaker's prepare for the merge guide +# See https://github.com/eth-educators/ethstaker-guides/blob/main/docs/prepare-for-the-merge.md#configuring-a-jwt-token-file + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +mkdir -p "${SCRIPT_DIR}/jwttoken" +if [[ ! -f "${SCRIPT_DIR}/jwttoken/jwt.hex" ]] +then + openssl rand -hex 32 | tr -d "\n" | tee > "${SCRIPT_DIR}/jwttoken/jwt.hex" +else + echo "${SCRIPT_DIR}/jwttoken/jwt.hex already exists!" +fi diff --git a/kona/docker/recipes/kona-node/grafana/dashboards/dashboard.yml b/kona/docker/recipes/kona-node/grafana/dashboards/dashboard.yml new file mode 100644 index 0000000000000..87b13ec38f8d0 --- /dev/null +++ b/kona/docker/recipes/kona-node/grafana/dashboards/dashboard.yml @@ -0,0 +1,7 @@ +apiVersion: 1 + +providers: + - name: 'Folder' + allowUiUpdates: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/kona/docker/recipes/kona-node/grafana/dashboards/overview.json b/kona/docker/recipes/kona-node/grafana/dashboards/overview.json new file mode 100644 index 0000000000000..13e480f04a86d --- /dev/null +++ b/kona/docker/recipes/kona-node/grafana/dashboards/overview.json @@ -0,0 +1,9108 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.2.0" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "text", + "name": "Text", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{version}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Version", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 3, + "y": 1 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{build_timestamp}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Build Timestamp", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 8, + "y": 1 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{git_sha}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Git SHA", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 11, + "y": 1 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{build_profile}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Build Profile", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 14, + "y": 1 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{target_triple}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Target Triple", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 5, + "x": 19, + "y": 1 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_info{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{cargo_features}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Cargo Features", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 10 + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 4 + }, + "id": 12, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_swarm_peer_count{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Peer Count", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 4, + "y": 4 + }, + "id": 26, + "options": { + "displayMode": "lcd", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "text" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\", label=~\"unsafe\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\", label=~\"cross-unsafe\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\", label=~\"local-safe\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\", label=~\"safe\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "D", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\", label=~\"finalized\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "E", + "useBackend": false + } + ], + "title": "Block Tags", + "transparent": true, + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "L2 Block Number", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "derivation-l1-origin" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "L1 block number" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_block_labels{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label}}", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_derivation_l1_origin{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "derivation-l1-origin", + "range": true, + "refId": "B", + "useBackend": false + } + ], + "title": "Sync Status", + "transparent": true, + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 16, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Seconds / unsafe block", + "axisPlacement": "auto", + "axisSoftMax": 3, + "axisSoftMin": 1, + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 41, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "seconds/block" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 37 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "1/avg(rate(kona_node_block_labels{instance=~\"$instance\", label=\"unsafe\"}[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "L2 Unsafe Sync Rate", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "1/avg(rate(kona_node_block_labels{instance=~\"$instance\", label=\"unsafe\"}[5m]))", + "hide": false, + "instant": false, + "legendFormat": "L2 Unsafe Sync Rate (5m avg)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "1/avg(rate(kona_node_block_labels{instance=~\"$instance\", label=\"unsafe\"}[10m]))", + "hide": false, + "instant": false, + "legendFormat": "L2 Unsafe Sync Rate (10m avg)", + "range": true, + "refId": "C" + } + ], + "title": "Seconds per Unsafe L2 Block", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 37 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "max(rate(kona_node_block_labels{instance=~\"$instance\", label=\"unsafe\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "unsafe", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "max(rate(kona_node_block_labels{instance=~\"$instance\", label=\"safe\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "safe", + "range": true, + "refId": "B" + } + ], + "title": "L2 Sync Speed (blocks/sec)", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 61 + }, + "id": 21, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto", + "value": "Request Duration" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisLabel": "Quantile", + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "kona_node_engine_method_request_duration{instance=~\"$instance\", method=\"engine_newPayload\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "`engine_newPayload` request duration heatmap", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 61 + }, + "id": 20, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto", + "value": "Request Duration" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisLabel": "Quantile", + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_engine_method_request_duration{instance=~\"$instance\", method=\"engine_forkchoiceUpdated\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "`engine_forkchoiceUpdated` request duration heatmap", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 22, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto", + "value": "Request Duration" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisLabel": "Quantile", + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_engine_method_request_duration{instance=~\"$instance\", method=\"engine_getPayload\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "`engine_getPayload` request duration heatmap", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Tasks Enqueued / min", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 69 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum by(type) (rate(kona_node_engine_task_count{instance=~\"$instance\"}[$__rate_interval])) * 60", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{type}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Engine Task Enqueue Rate / min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 77 + }, + "id": 23, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_engine_reset_count{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Engine Resets", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 77 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "kona_node_l1_reorg_count{instance=~\"$instance\"}", + "instant": false, + "legendFormat": "L1 Reorg Count", + "range": true, + "refId": "A" + } + ], + "title": "L1 Reorganization Count", + "transparent": true, + "type": "stat" + } + ], + "title": "Engine", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 2, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Peers", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 124 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_swarm_peer_count{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "swarm_peer_count", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_discovery_peer_count{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "discovery_peer_count", + "range": true, + "refId": "B", + "useBackend": false + } + ], + "title": "Peer Count", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 124 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(kona_node_dial_peer{instance=~\"$instance\"}[$__interval])) * 60", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Peer Dial Rate / min", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(kona_node_dial_peer_error{type=\"already_connected\", instance=\"$instance\"}[$__interval])) * 60", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Already connected Rate / min", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(kona_node_dial_peer_error{instance=\"$instance\", type=\"already_dialing\"}[$__interval])) * 60", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Already dialing Rate / min", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(kona_node_dial_peer_error{type=\"threshold_reached\", instance=\"$instance\"}[$__interval])) * 60", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Threshold reached rate / min", + "range": true, + "refId": "D", + "useBackend": false + } + ], + "title": "Peers Dialed / min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 132 + }, + "id": 35, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(type) (rate(kona_node_gossipsub_connection{instance=~\"$instance\"}[$__rate_interval])) * 60", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{type}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GossipSub Connection Events / min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "gossip event total" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "continuous-YlBl" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 132 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(type) (rate(kona_node_gossip_events{instance=~\"$instance\"}[$__rate_interval])) * 60", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{type}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Gossip Events / min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 140 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(type) (rate(kona_node_discovery_events{instance=~\"$instance\"}[$__rate_interval])) * 60", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Discovery Events / min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 140 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_find_node_requests{instance=~\"$instance\", find_node=\"find_node\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "find-node requests", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Find Node Request Count", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Time taken to store an ENR in the on-disk peerstore", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 148 + }, + "id": 18, + "options": { + "calculate": false, + "calculation": { + "yBuckets": { + "scale": { + "type": "linear" + } + } + }, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto", + "value": "Store Time" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisLabel": "Quantile", + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "kona_node_enr_store_time{instance=~\"$instance\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A" + } + ], + "title": "ENR Store Time", + "transparent": true, + "type": "heatmap" + } + ], + "title": "P2P", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 72, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The cumulative number of prepared attributes by the derivation pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Cumulative Derived Payloads", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Tx Count" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "#ff59c9", + "mode": "fixed" + } + }, + { + "id": "custom.axisLabel", + "value": "Transactions" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 4 + }, + "id": 82, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_prepared_attributes{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Derived Payload Attributes", + "range": true, + "refId": "payload attributes", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_payload_tx_count{instance=\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Tx Count", + "range": true, + "refId": "tx count", + "useBackend": false + } + ], + "title": "Prepared Payload Attributes", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Which data source the pipeline is reading txs from. Either calldata or blobs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Transactions", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 4 + }, + "id": 75, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_dap_sources{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{source}} txs", + "range": true, + "refId": "data", + "useBackend": false + } + ], + "title": "L1 Transactions Ingested by Data Source [Stage: L1 Retrieval]", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The latest block height at which the system config was updated.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "#3bde8c6b", + "mode": "continuous-GrYlRd" + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "None" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-yellow", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 11 + }, + "id": 92, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_genesis_latest_system_config_update{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Latest System Config Update", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The latest block height at which the system config update errored. This is a critical error and will halt the node. If this gauge is non-zero (is set to a block height, there is a bug).", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "semi-dark-red", + "mode": "continuous-GrYlRd" + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "None" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-yellow", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 11 + }, + "id": 93, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_genesis_sys_config_update_error{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "System Config Update Error", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Number of pipeline signals grouped by type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "activation": { + "index": 0, + "text": "Activation" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 12 + }, + "id": 80, + "options": { + "displayMode": "lcd", + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "top", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": { + "titleSize": 22 + }, + "valueMode": "text" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_pipeline_signals{instance=\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{type}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Pipeline Signals", + "transparent": true, + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "light-purple", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{__name__=\"kona_derive_pipeline_origin\", instance=\"clabby-op-sepolia-kona-node:9431\", job=\"clabby-op-sepolia-kona-node\"}" + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 12 + }, + "id": 73, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_pipeline_origin{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Pipeline L1 Origin", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The latest l2 block height the pipeline was stepped on.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "light-orange", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 15 + }, + "id": 84, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_pipeline_step_block{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Block Height", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The last recorded number of frames read in by the `FrameQueue` stage. This metric does not track that current number of frames in the buffer since it updates too quickly for prometheus to scrape the metrics.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "frame size (bytes)" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "Bytes" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "frame count" + }, + "properties": [ + { + "id": "custom.axisLabel", + "value": "Number of Frames" + }, + { + "id": "color", + "value": { + "fixedColor": "#906666", + "mode": "fixed" + } + }, + { + "id": "custom.axisSoftMax", + "value": 10 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 15 + }, + "id": 86, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_frame_queue_buffer{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "frame count", + "range": true, + "refId": "frame count", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_frame_queue_mem{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "frame size (bytes)", + "range": true, + "refId": "frame size", + "useBackend": false + } + ], + "title": "Parsed Frames [Stage: Frame Queue]", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The number of pipeline steps per minute", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 81, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "max(rate(kona_derive_pipeline_steps{instance=\"$instance\"}[$__rate_interval])) * 60", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "pipeline steps", + "range": true, + "refId": "pipeline steps", + "useBackend": false + } + ], + "title": "Pipeline Steps / Min", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The number of channels held in the `ChannelAssembler` stage of the derivation pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "#2d61a0", + "mode": "continuous-GrYlRd" + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 22 + }, + "id": 79, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_channel_buffer{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Channel Count", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The number of blocks until the current channel times out.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "#2d61a0", + "mode": "shades" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 22 + }, + "id": 78, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_blocks_until_channel_timeout{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Channel Timeout", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "If the batch reader is set.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "#2d9ea1", + "mode": "shades" + }, + "mappings": [ + { + "options": { + "0": { + "index": 0, + "text": "False" + }, + "1": { + "index": 1, + "text": "True" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 22 + }, + "id": 85, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_batch_reader_set{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Reading Batch", + "transparent": true, + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The time it took to build payload attributes.", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 83, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "#E02F44", + "mode": "scheme", + "reverse": false, + "scale": "linear", + "scheme": "RdYlGn", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_derive_attributes_build_duration{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Payload Attributes Build Time", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The current size of channels held in the `ChannelAssembler` stage of the derivation pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "channel size (bytes)" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "left" + }, + { + "id": "custom.axisLabel", + "value": "Bytes" + }, + { + "id": "custom.fillOpacity", + "value": 15 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max size" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "color", + "value": { + "fixedColor": "semi-dark-red", + "mode": "fixed" + } + }, + { + "id": "custom.axisSoftMax", + "value": 100000000 + }, + { + "id": "custom.axisLabel", + "value": "Bytes" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 76, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_channel_mem{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "channel size (bytes)", + "range": true, + "refId": "channel size", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_max_rlp_bytes{instance=\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max size", + "range": true, + "refId": "max size", + "useBackend": false + } + ], + "title": "Channel Size [Stage: Channel Assembler]", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The batches read by type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 89, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_read_batches{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{type}}", + "range": true, + "refId": "batch count", + "useBackend": false + } + ], + "title": "Cumulative Batches by Type", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The decompressed batch size and type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "decompression type" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "index": 0, + "text": "Brotli" + }, + "8": { + "index": 1, + "text": "Zlib" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 94, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_latest_decompressed_batch_size{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "decompressed batch size", + "range": true, + "refId": "channel size", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_latest_decompressed_batch_type{instance=\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "decompression type", + "range": true, + "refId": "max size", + "useBackend": false + } + ], + "title": "Decompressed Batch Size + Type [Stage: Channel Reader]", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The number and size of singular batches pulled into the `BatchStream` stage of the derivation pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "batch size (bytes)" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "Bytes" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "batch count" + }, + "properties": [ + { + "id": "custom.axisLabel", + "value": "Number of Frames" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 77, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_batch_buffer{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "batch count", + "range": true, + "refId": "batch count", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_batch_mem{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "batch size (bytes)", + "range": true, + "refId": "batch size", + "useBackend": false + } + ], + "title": "Derived Batches [Stage: Batch Stream]", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The l1 blocks in the `BatchValidator` stage of the derivation pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Block Height", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 88, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_l1_blocks_start{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "l1 blocks start", + "range": true, + "refId": "l1 blocks start", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_l1_blocks_end{instance=~\"$instance\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "l1 blocks end", + "range": true, + "refId": "l1 blocks end", + "useBackend": false + } + ], + "title": "Batch Validation L1 Blocks", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 46 + }, + "id": 74, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlGn", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_derive_pipeline_origin_advance{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Origin Advance Duration Heatmap", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Duration for checking batch prefixes.", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 91, + "options": { + "calculate": false, + "cellGap": 1, + "cellValues": { + "unit": "s" + }, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlGn", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "percentunit" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_derive_check_batch_prefix_duration{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{quantile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Batch Prefix Check Heatmap", + "transparent": true, + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Batches grouped by batch validity", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Block Height", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 14, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 90, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_derive_batch_validity{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{validity}}", + "range": true, + "refId": "validity", + "useBackend": false + } + ], + "title": "Batch Validity", + "transparent": true, + "type": "timeseries" + } + ], + "title": "Derivation", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 101, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 102, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_beacon_requests{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Beacon Requests", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 5 + }, + "id": 104, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_chain_cache_misses{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{cache}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Chain Provider Cache Misses", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 105, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_beacon_errors{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Beacon Errors", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 103, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_chain_cache_hits{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{cache}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Chain Provider Cache Hits", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 106, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_chain_rpc_calls{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L1 Provider RPC Calls", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 108, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_l2_chain_requests{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Provider Rpc Requests", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 107, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_chain_rpc_errors{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L1 Provider RPC Errors", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 109, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_l2_chain_errors{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Provider RPC Errors", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 110, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_blob_sidecar_fetches{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Beacon Provider RPC Calls", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 111, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_blob_sidecar_errors{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Beacon Provider RPC Errors", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 112, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_cache_entries{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{cache}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Cache Entries", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 113, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_providers_cache_memory_bytes{instance=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{cache}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Cache Memory Usage", + "transparent": true, + "type": "timeseries" + } + ], + "title": "Derivation RPC Providers", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 95, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 98, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_sequencer_attributes_build_duration{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Attributes Build Duration", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Attributes Build Duration", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 99, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_sequencer_block_building_seal_task_duration{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Block Building Seal Task Duration", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Block Building Seal Task Duration", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 48 + }, + "id": 96, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_sequencer_state{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{active}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Sequencer Active", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 48 + }, + "id": 97, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_sequencer_state{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{recovery}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Recovery Mode", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 100, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_node_sequencer_conductor_commitment_duration{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Conductor Commitment Duration", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Conductor Commitment Duration", + "transparent": true, + "type": "timeseries" + } + ], + "title": "Sequencer", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 28, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 126 + }, + "id": 27, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_runtime_loader{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{unsafe_block_signer_address}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Unsafe Block Signer Address", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 8, + "y": 126 + }, + "id": 29, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_runtime_loader{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{required_protocol_version}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Required Protocol Version", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 16, + "y": 126 + }, + "id": 30, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_runtime_loader{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{recommended_protocol_version}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Recommended Protocol Version", + "transparent": true, + "type": "stat" + } + ], + "title": "Dynamic Protocol Configuration", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 46, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "-1": { + "index": 0, + "text": "Not Scheduled" + } + }, + "type": "value" + } + ], + "noValue": "3", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "dateTimeAsSystem" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 15, + "x": 0, + "y": 127 + }, + "id": 70, + "options": { + "displayMode": "lcd", + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "sort(kona_node_hardforks{instance=~\"$instance\"} >= 0) * 1000", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "interval": "", + "legendFormat": "{{fork}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Hardfork Activation Times", + "transparent": true, + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Hardforks that the node supports, but are not scheduled within the RollupConfig for the current chain.\n\nSome chains do not enable optional hardforks (such as Pectra Blob Schedule,) and some forks are WIPs that have not yet received an activation timestamp.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "fieldMinMax": false, + "mappings": [ + { + "options": { + "-1": { + "index": 0, + "text": "Not Scheduled" + } + }, + "type": "value" + } + ], + "noValue": "3", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + }, + "unit": "dateTimeAsSystem" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 9, + "x": 15, + "y": 127 + }, + "id": 71, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "kona_node_hardforks{instance=~\"$instance\"} < 0", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "interval": "", + "legendFormat": "{{fork}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Unscheduled Hardforks", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Genesis L2 Time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 137 + }, + "id": 61, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{genesis_l2_time}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Genesis L2 Time", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "L2 Block Time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 137 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{block_time}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Block Time", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "L1 Chain ID", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 137 + }, + "id": 62, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{l1_chain_id}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "L1 Chain ID", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The Batch Inbox Address (Can be changed in the `SystemConfig`. The RollupConfig's value is _only_ the value at genesis, and may not be the chain's current batch inbox.)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 12, + "x": 12, + "y": 137 + }, + "id": 56, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{batch_inbox_address}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Batch Inbox Address", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Genesis L1 Block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 139 + }, + "id": 64, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{l1_genesis_block_num}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Genesis L1 Block", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Genesis L2 Block", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 139 + }, + "id": 65, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{l2_genesis_block_num}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Genesis L2 Block", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "L2 Chain ID", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 139 + }, + "id": 63, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{l2_chain_id}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Chain ID", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Deposit Contract Address", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 12, + "x": 12, + "y": 139 + }, + "id": 57, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{deposit_contract_address}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Deposit Contract Address", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Granite Channel Timeout", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 141 + }, + "id": 66, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{granite_channel_timeout}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Granite Channel Timeout", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Sequencer Window Size", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 141 + }, + "id": 68, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{sequencer_window_size}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Seq Window Size", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Max Sequencer Drift", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 141 + }, + "id": 67, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{max_sequencer_drift}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Max Seq Drift", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "L1 System Config Address", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 12, + "x": 12, + "y": 141 + }, + "id": 58, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{l1_system_config_address}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "L1 System Config Address", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Protocol Versions Address", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 12, + "x": 12, + "y": 143 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 20 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_node_rollup_config{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{protocol_versions_address}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Protocol Versions Address", + "transparent": true, + "type": "stat" + } + ], + "title": "Rollup Configuration", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 33, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The peer scoring level configured via the cli flag.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 128 + }, + "id": 36, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_peer_scoring_level}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Peer Scoring", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The amount of times peers are able to be redialed", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "max": -2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 128 + }, + "id": 37, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_peer_redialing}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Peer Redial", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Whether P2P Peer Banning is enabled.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 128 + }, + "id": 38, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_banning_enabled}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Ban Peers", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Gossip Mesh D", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 12, + "y": 128 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_gossip_mesh_d}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Gossip Mesh D", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Low-tide peer count. The discv5 service will automatically search for peers if its peer count falls below this number.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 16, + "y": 128 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_peers_lo}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Discv5 Peers Lo", + "transparent": true, + "type": "stat" + }, + { + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 128 + }, + "id": 48, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "- [Kona Monorepo](https://github.com/op-rs/kona)\n- [Optimism Monorepo](https://github.com/ethereum-optimism/optimism)\n- [Op Alloy](https://github.com/alloy-rs/op-alloy)\n- [Reth](https://github.com/paradigmxyz/reth)\n- [Revm](https://github.com/bluealloy/revm)", + "mode": "markdown" + }, + "pluginVersion": "11.2.0", + "title": "Resources", + "transparent": true, + "type": "text" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The P2P IP Address advertised by the node's ENR.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 130 + }, + "id": 40, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_advertise_ip}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Advertise IP", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The TCP Port advertised by the node's ENR.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 130 + }, + "id": 41, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_advertise_tcp}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Advertise TCP Port", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The UDP Port advertised in the node's ENR.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 130 + }, + "id": 42, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_advertise_udp}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Advertise UDP Port", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "This is the gossipsub topic stable mesh low watermark.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 12, + "y": 130 + }, + "id": 51, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_gossip_mesh_d_lo}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Gossip Mesh D Lo", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "High-tide peer count. The discv5 service will start pruning peer connections slowly after reaching this number.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 16, + "y": 130 + }, + "id": 54, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_peers_hi}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Discv5 Peers Hi", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Whether P2P Topic Scoring is enabled.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "max": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 132 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_topic_scoring_enabled}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Topic Scoring", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Whether P2P Topic Scoring is enabled.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "max": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 4, + "y": 132 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_topic_scoring_enabled}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Topic Scoring", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The interval at which to query the discv5 service for new nodes through a `FINDNODES` request.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 8, + "y": 132 + }, + "id": 44, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_discovery_interval}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Discovery Interval", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "This is the gossipsub topic stable mesh high watermark.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 12, + "y": 132 + }, + "id": 52, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_gossip_mesh_d_hi}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Gossip Mesh D Hi", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Gossip Mesh D Lazy", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 16, + "y": 132 + }, + "id": 69, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_gossip_mesh_d_lazy}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Gossip Mesh D Lazy", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "The duration to ban nodes in seconds.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 20, + "y": 132 + }, + "id": 55, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 25 + }, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_cli_opts{instance=~\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{kona_node_ban_duration}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Ban Duration", + "transparent": true, + "type": "stat" + } + ], + "title": "P2P Configuration", + "type": "row" + } + ], + "refresh": "5s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "query_result(kona_node_info)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "instance", + "options": [], + "query": { + "query": "query_result(kona_node_info)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "/.*instance=\\\"([^\\\"]*).*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Kona Node", + "uid": "eelp13vmygsu8b", + "version": 4, + "weekStart": "" +} diff --git a/kona/docker/recipes/kona-node/grafana/datasources/prometheus.yml b/kona/docker/recipes/kona-node/grafana/datasources/prometheus.yml new file mode 100644 index 0000000000000..6561365f85e55 --- /dev/null +++ b/kona/docker/recipes/kona-node/grafana/datasources/prometheus.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: $PROMETHEUS_URL + editable: true diff --git a/kona/docker/recipes/kona-node/justfile b/kona/docker/recipes/kona-node/justfile new file mode 100644 index 0000000000000..50c069b525d1a --- /dev/null +++ b/kona/docker/recipes/kona-node/justfile @@ -0,0 +1,15 @@ +# Generates a JWT secret, if one does not already exist. +generate-jwt: + @echo "Attempting to generate JWT secret" + @bash ./generate-jwt.sh + +# Spins up the docker environment +up: generate-jwt + docker compose --env-file ./cfg.env up -d + +# Winds down the docker containers gracefully +down: + docker compose --env-file ./cfg.env down + +# Restarts the environment +restart: down up diff --git a/kona/docker/recipes/kona-node/prometheus/prometheus.yml b/kona/docker/recipes/kona-node/prometheus/prometheus.yml new file mode 100644 index 0000000000000..386948fe6c50c --- /dev/null +++ b/kona/docker/recipes/kona-node/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +scrape_configs: + - job_name: op-reth + scrape_interval: 5s + static_configs: + - targets: ['op-reth:9001', 'localhost:9001', 'host.docker.internal:9001'] + - job_name: kona-node + scrape_interval: 5s + static_configs: + - targets: ['kona-node:9002'] diff --git a/kona/docker/recipes/kona-supervisor/grafana/dashboards/dashboard.yml b/kona/docker/recipes/kona-supervisor/grafana/dashboards/dashboard.yml new file mode 100644 index 0000000000000..2d95a2149527e --- /dev/null +++ b/kona/docker/recipes/kona-supervisor/grafana/dashboards/dashboard.yml @@ -0,0 +1,7 @@ +apiVersion: 1 + +providers: + - name: 'Kona Supervisor' + allowUiUpdates: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/kona/docker/recipes/kona-supervisor/grafana/dashboards/kona-supervisor.json b/kona/docker/recipes/kona-supervisor/grafana/dashboards/kona-supervisor.json new file mode 100644 index 0000000000000..093b0caab2f23 --- /dev/null +++ b/kona/docker/recipes/kona-supervisor/grafana/dashboards/kona-supervisor.json @@ -0,0 +1,4772 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.5.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-yellow", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{version}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "dark-yellow", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 0 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{build_timestamp}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Build Timestamp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 0 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{cargo_features}}", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Cargo Features", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "text", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 0 + }, + "id": 51, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{git_sha}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Git SHA", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 52, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{target_triple}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Target Triple", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "semi-dark-green", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_info{namespace=~\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{build_profile}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Build Profile", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Time since the supervisor service has started", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "yellow", + "mode": "fixed" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dthms" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 4 + }, + "id": 42, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "time() - process_start_time_seconds{namespace=\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Elapsed Time", + "range": true, + "refId": "Elapsed Time", + "useBackend": false + } + ], + "title": "Elapsed Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Does not include swapped out pages. ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 4, + "y": 4 + }, + "id": 41, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "process_resident_memory_bytes{namespace=\"kona-supervisor-supervisor-superchain\"} / 1000000", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "RAM Usage (in MB)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "If the % is > 100, it means it is using multiple cores", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 12, + "y": 4 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "rate(process_cpu_seconds_total{namespace=\"kona-supervisor-supervisor-superchain\"}[1m]) * 100", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{namespace}}", + "range": true, + "refId": "CPU Usage", + "useBackend": false + } + ], + "title": "CPU usage (%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 20, + "y": 4 + }, + "id": 43, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "process_threads{namespace=\"kona-supervisor-supervisor-superchain\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "No. of Threads", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 35, + "panels": [], + "title": "Supervisor Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 5 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 36, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_reorg_error_total{namespace=~\"kona-supervisor-supervisor-superchain\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Failed ({{chain_id}})", + "range": true, + "refId": "Error", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_reorg_success_total{namespace=\"kona-supervisor-supervisor-superchain\", method!=\"process_chain_reorg\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Successful ({{chain_id}})", + "range": true, + "refId": "Success", + "useBackend": false + } + ], + "title": "Reorg: $chain_id", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Total number of blocks processed, including reorgs, reset, etc.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 37, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_processing_success_total{type=~\"finalized\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Finalized ({{chain_id}})", + "range": true, + "refId": "Finalized", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_processing_success_total{type=~\"cross_safe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Cross-safe ({{chain_id}})", + "range": true, + "refId": "Cross-safe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_processing_success_total{type=~\"cross_unsafe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Cross-unsafe ({{chain_id}})", + "range": true, + "refId": "Cross-unsafe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_processing_success_total{type=~\"local_safe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Local-safe ({{chain_id}})", + "range": true, + "refId": "Local-safe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_processing_success_total{type=~\"local_unsafe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Local-unsafe ({{chain_id}})", + "range": true, + "refId": "Local-unsafe", + "useBackend": false + } + ], + "title": "Total Blocks Processed: $chain_id", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "shades" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 38, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(namespace) (supervisor_rpc_requests_error_total{namespace=\"kona-supervisor-supervisor-superchain\"})", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Failed Requests", + "range": true, + "refId": "Error", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(namespace) (supervisor_rpc_requests_success_total{namespace=\"kona-supervisor-supervisor-superchain\"})", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "Successful Requests", + "range": true, + "refId": "Success", + "useBackend": false + } + ], + "title": "Supervisor RPC", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the latest safety head ref block number for each chain", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 54, + "options": { + "displayMode": "lcd", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "text" + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_safety_head_ref_labels{type=~\"local_unsafe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{type}}: {{chain_id}}", + "range": true, + "refId": "Local Unsafe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_safety_head_ref_labels{type=~\"cross_unsafe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "{{type}}: {{chain_id}}", + "range": true, + "refId": "Cross Unsafe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_safety_head_ref_labels{type=~\"local_safe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "{{type}}: {{chain_id}}", + "range": true, + "refId": "Local Safe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_safety_head_ref_labels{type=~\"cross_safe\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "{{type}}: {{chain_id}}", + "range": true, + "refId": "Cross Safe", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_safety_head_ref_labels{type=~\"finalized\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "{{type}}: {{chain_id}}", + "range": true, + "refId": "Finalized", + "useBackend": false + } + ], + "title": "Safety Head Refs: $chain_id", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Total Requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(node) (managed_node_rpc_requests_error_total{namespace=\"kona-supervisor-supervisor-superchain\", node=~\".*$chain_id.*\"})", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "Failed Req ({{node}})", + "range": true, + "refId": "Error", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(node) (managed_node_rpc_requests_success_total{namespace=\"kona-supervisor-supervisor-superchain\", node=~\".*$chain_id.*\"})", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{node}}", + "range": true, + "refId": "Success", + "useBackend": false + } + ], + "title": "Managed Node RPC: $chain_id", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "^([^0-9]*)(\\d+)(.*)$", + "renamePattern": "$2" + } + } + ], + "type": "stat" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 29, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the no. of source blocks that the reorg happened for a given chain", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_reorg_l1_depth{quantile=\"0.95\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L1 Depth: $chain_id", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the no. of derived blocks reorg happened for the given chain", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "kona_supervisor_reorg_l2_depth{quantile=\"0.95\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "L2 Depth: $chain_id", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the success rate of all L2 chains when an L1 reorg occurs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 52 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(kona_supervisor_reorg_success_total{namespace=\"kona-supervisor-supervisor-superchain\", chain_id=~\"$chain_id\"}[5m])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Reorg Success Rate: $chain_id", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the time taken for all L2 chains to reorg when an L1 reorg occurs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 52 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "kona_supervisor_reorg_duration_seconds{quantile=\"0.95\", method!=\"process_chain_reorg\", chain_id=~\"$chain_id\"}", + "format": "time_series", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Reorg Duration in seconds: $chain_id", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Shows the error rate of all L2 chains when an L1 reorg occurs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 60 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(kona_supervisor_reorg_error_total{namespace=\"kona-supervisor-supervisor-superchain\", chain_id=~\"$chain_id\"}[5m])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Reorg Error Rate: $chain_id", + "type": "timeseries" + } + ], + "title": "Reorg", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 1, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful blocks processed (per 5 min) ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 29 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(supervisor_block_processing_success_total{type=\"local_safe\",chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Processing Speed: local_safe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful blocks processed (per 5 min) ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 29 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(supervisor_block_processing_success_total{type=\"local_unsafe\",chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Speed: local_unsafe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful blocks processed (per 5 min) ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(supervisor_block_processing_success_total{type=\"finalized\",chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Processing Speed: finalized", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful blocks processed (per 5 min) ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(supervisor_block_processing_success_total{type=\"cross_safe\",chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Speed: cross_safe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful blocks processed (per 5 min) ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 61 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(supervisor_block_processing_success_total{type=\"cross_unsafe\",chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Speed: cross_unsafe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken to process the blocks by supervisor since its block time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 61 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "supervisor_block_processing_latency_seconds{quantile=\"0.95\",type=\"local_safe\",chain_id=~\"$chain_id\"}", + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Processing Latency p95: local_safe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken to process the blocks by supervisor since its block time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "supervisor_block_processing_latency_seconds{quantile=\"0.95\",type=\"local_unsafe\",chain_id=~\"$chain_id\"}", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Latency p95: local_unsafe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken to process the blocks by supervisor since its block time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 69 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "supervisor_block_processing_latency_seconds{quantile=\"0.95\",type=\"finalized\",chain_id=~\"$chain_id\"}", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Latency p95: finalized", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken to process the blocks by supervisor since its block time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 77 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "supervisor_block_processing_latency_seconds{quantile=\"0.95\",type=\"cross_safe\",chain_id=~\"$chain_id\"}", + "legendFormat": "{{chain_id}}", + "refId": "A" + } + ], + "title": "Block Processing Latency p95: cross_safe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken to process the blocks by supervisor since its block time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 77 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "supervisor_block_processing_latency_seconds{quantile=\"0.95\",type=\"cross_unsafe\",chain_id=~\"$chain_id\"}", + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Processing Latency p95: cross_unsafe", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful block invalidation (per 5 min) over time for each chain", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 85 + }, + "id": 44, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(supervisor_block_invalidation_success_total{namespace=\"kona-supervisor-supervisor-superchain\", chain_id=~\"$chain_id\"}[5m])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Block Invalidation Speed: $chain_id", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken in seconds to process the block invalidation query.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 85 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_invalidation_latency_seconds{quantile=\"0.95\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Block Invalidation Latency p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Rate of successful block replacement (per 5 min) over time for each chain", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 93 + }, + "id": 45, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(supervisor_block_replacement_success_total{namespace=\"kona-supervisor-supervisor-superchain\", chain_id=~\"$chain_id\"}[5m])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Block Replacement Speed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Amount of time taken in seconds to process the block replacement query.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 93 + }, + "id": 47, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "supervisor_block_replacement_latency_seconds{quantile=\"0.95\", chain_id=~\"$chain_id\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{chain_id}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Block Replacement Latency p95", + "type": "timeseries" + } + ], + "title": "Block Processing", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 12, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(kona_supervisor_storage_success_total{chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{method}}", + "range": true, + "refId": "A" + } + ], + "title": "Storage Success Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(kona_supervisor_storage_error_total{chain_id=~\"$chain_id\"}[5m])", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Storage Error Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "kona_supervisor_storage_duration_seconds{quantile=\"0.95\",chain_id=~\"$chain_id\",method=~\"derived_to_source|latest_derived_block_at_source|latest_derivation_state|save_derived_block\"}", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Derivation Storage Latency p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "kona_supervisor_storage_duration_seconds{quantile=\"0.95\",chain_id=~\"$chain_id\",method=~\"get_latest_block|get_block|get_log|get_logs|store_block_logs\"}", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Log Storage Latency p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 62 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "kona_supervisor_storage_duration_seconds{quantile=\"0.95\",chain_id=~\"$chain_id\",method=~\"get_current_l1|get_safety_head_ref|get_super_head|update_current_l1|update_finalized_using_source|update_current_cross_unsafe|update_current_cross_safe\"}", + "legendFormat": "{{method}}", + "range": true, + "refId": "A" + } + ], + "title": "Ref Storage Latency p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 62 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "kona_supervisor_storage_duration_seconds{quantile=\"0.95\",chain_id=~\"$chain_id\",method=~\"update_finalized_l1|get_finalized_l1\"}", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Finalized Storage Latency p95", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 19, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "text" + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "kona_supervisor_storage_table_entries{chain_id=~\"$chain_id\"}", + "legendFormat": "{{table}} - {{chain_id}}", + "refId": "A" + } + ], + "title": "Storage Table Entries (Bar Gauge, per Chain)", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 78 + }, + "id": 20, + "options": { + "displayMode": "basic", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "text" + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "kona_supervisor_storage_table_size{chain_id=~\"$chain_id\"}", + "legendFormat": "{{table}}", + "refId": "A" + } + ], + "title": "Storage Table Size (Bar Gauge, per Chain)", + "type": "bargauge" + } + ], + "title": "Storage", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 21, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(supervisor_rpc_requests_success_total[5m])", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Supervisor RPC Success Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "rate(supervisor_rpc_requests_error_total[5m])", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Supervisor RPC Error Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "supervisor_rpc_request_duration_seconds{quantile=\"0.95\"}", + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "title": "Supervisor RPC Latency p95 (per Method)", + "type": "timeseries" + } + ], + "title": "Supervisor RPC", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 25, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "block_ref_by_number: ws://op-cl-2151909-node1-op-node:9645" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(managed_node_rpc_requests_success_total{node=~\".*$chain_id.*\"}[5m])", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Managed Node RPC Success Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(managed_node_rpc_requests_error_total{node=~\".*$chain_id.*\"}[5m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Managed Node RPC Error Rate (per Method)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "managed_node_rpc_request_duration_seconds{quantile=\"0.95\", node=~\".*$chain_id.*\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Managed Node RPC Latency p95 (per Method)", + "type": "timeseries" + } + ], + "title": "Managed Node RPC", + "type": "row" + } + ], + "refresh": "10s", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(chain_id)", + "includeAll": true, + "label": "Chain ID", + "multi": true, + "name": "chain_id", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(chain_id)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kona Supervisor Dashboard", + "uid": "fevgnwku1evi8b", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/kona/docs/README.md b/kona/docs/README.md new file mode 100644 index 0000000000000..736588de63ca6 --- /dev/null +++ b/kona/docs/README.md @@ -0,0 +1,23 @@ +# Vocs + +This is a [Vocs](https://vocs.dev) project bootstrapped with the Vocs CLI. + +## Usage + +Run the vocs site in developer mode, hosting it. + +```bash +just run-vocs +``` + +Build the vocs site as a static site. + +```bash +just build-vocs +``` + +Open the static site. + +```bash +just open-site +``` diff --git a/kona/docs/bun.lock b/kona/docs/bun.lock new file mode 100644 index 0000000000000..a8e82c6514623 --- /dev/null +++ b/kona/docs/bun.lock @@ -0,0 +1,1442 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "kona-docs", + "dependencies": { + "react": "19.2.1", + "react-dom": "19.2.1", + "vocs": "1.2.1", + }, + "devDependencies": { + "@types/node": "latest", + "@types/react": "latest", + "tailwindcss": "^4.1.11", + "typescript": "latest", + }, + }, + }, + "packages": { + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, ""], + + "@antfu/utils": ["@antfu/utils@9.3.0", "", {}, ""], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, ""], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, ""], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, ""], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, ""], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, ""], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, ""], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, ""], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, ""], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, ""], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, ""], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, ""], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, ""], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, ""], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, ""], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, ""], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, ""], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, ""], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, ""], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, ""], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, ""], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, ""], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, ""], + + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, ""], + + "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, ""], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, ""], + + "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, ""], + + "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, ""], + + "@clack/core": ["@clack/core@0.3.5", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, ""], + + "@clack/prompts": ["@clack/prompts@0.7.0", "", { "dependencies": { "@clack/core": "^0.3.3", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, ""], + + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, ""], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, ""], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, ""], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, ""], + + "@floating-ui/react": ["@floating-ui/react@0.27.16", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, ""], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, ""], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, ""], + + "@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.7.2", "", {}, ""], + + "@hono/node-server": ["@hono/node-server@1.19.5", "", { "peerDependencies": { "hono": "^4" } }, ""], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, ""], + + "@iconify/utils": ["@iconify/utils@3.0.2", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@antfu/utils": "^9.2.0", "@iconify/types": "^2.0.0", "debug": "^4.4.1", "globals": "^15.15.0", "kolorist": "^1.8.0", "local-pkg": "^1.1.1", "mlly": "^1.7.4" } }, ""], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, ""], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, ""], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, ""], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, ""], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, ""], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, ""], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, ""], + + "@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, ""], + + "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, ""], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, ""], + + "@radix-ui/colors": ["@radix-ui/colors@3.0.0", "", {}, ""], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, ""], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, ""], + + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, ""], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.43", "", {}, ""], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, ""], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, ""], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, ""], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], + + "@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, ""], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, ""], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, ""], + + "@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, ""], + + "@shikijs/rehype": ["@shikijs/rehype@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "1.29.2", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" } }, ""], + + "@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, ""], + + "@shikijs/transformers": ["@shikijs/transformers@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/types": "1.29.2" } }, ""], + + "@shikijs/twoslash": ["@shikijs/twoslash@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/types": "1.29.2", "twoslash": "^0.2.12" } }, ""], + + "@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, ""], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, ""], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, ""], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.15", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.15" } }, ""], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.15", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.15", "@tailwindcss/oxide-darwin-arm64": "4.1.15", "@tailwindcss/oxide-darwin-x64": "4.1.15", "@tailwindcss/oxide-freebsd-x64": "4.1.15", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", "@tailwindcss/oxide-linux-x64-musl": "4.1.15", "@tailwindcss/oxide-wasm32-wasi": "4.1.15", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" } }, ""], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.15", "", { "os": "android", "cpu": "arm64" }, "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.15", "", { "os": "linux", "cpu": "arm" }, "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.15", "", { "os": "linux", "cpu": "x64" }, ""], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.15", "", { "os": "linux", "cpu": "x64" }, ""], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.15", "", { "cpu": "none" }, "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.15", "", { "os": "win32", "cpu": "x64" }, "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.15", "", { "dependencies": { "@tailwindcss/node": "4.1.15", "@tailwindcss/oxide": "4.1.15", "tailwindcss": "4.1.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, ""], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, ""], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, ""], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, ""], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, ""], + + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, ""], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, ""], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, ""], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, ""], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, ""], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, ""], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, ""], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, ""], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, ""], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, ""], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, ""], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, ""], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, ""], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, ""], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, ""], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, ""], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, ""], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, ""], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, ""], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, ""], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, ""], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, ""], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, ""], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, ""], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, ""], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, ""], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, ""], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, ""], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, ""], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, ""], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, ""], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, ""], + + "@types/estree": ["@types/estree@1.0.8", "", {}, ""], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, ""], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, ""], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, ""], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, ""], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, ""], + + "@types/ms": ["@types/ms@2.1.0", "", {}, ""], + + "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, ""], + + "@types/unist": ["@types/unist@3.0.3", "", {}, ""], + + "@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, ""], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, ""], + + "@vanilla-extract/babel-plugin-debug-ids": ["@vanilla-extract/babel-plugin-debug-ids@1.2.2", "", { "dependencies": { "@babel/core": "^7.23.9" } }, ""], + + "@vanilla-extract/compiler": ["@vanilla-extract/compiler@0.3.1", "", { "dependencies": { "@vanilla-extract/css": "^1.17.4", "@vanilla-extract/integration": "^8.0.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vite-node": "^3.2.2" } }, ""], + + "@vanilla-extract/css": ["@vanilla-extract/css@1.17.4", "", { "dependencies": { "@emotion/hash": "^0.9.0", "@vanilla-extract/private": "^1.0.9", "css-what": "^6.1.0", "cssesc": "^3.0.0", "csstype": "^3.0.7", "dedent": "^1.5.3", "deep-object-diff": "^1.1.9", "deepmerge": "^4.2.2", "lru-cache": "^10.4.3", "media-query-parser": "^2.0.2", "modern-ahocorasick": "^1.0.0", "picocolors": "^1.0.0" } }, ""], + + "@vanilla-extract/dynamic": ["@vanilla-extract/dynamic@2.1.5", "", { "dependencies": { "@vanilla-extract/private": "^1.0.9" } }, ""], + + "@vanilla-extract/integration": ["@vanilla-extract/integration@8.0.4", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/plugin-syntax-typescript": "^7.23.3", "@vanilla-extract/babel-plugin-debug-ids": "^1.2.2", "@vanilla-extract/css": "^1.17.4", "dedent": "^1.5.3", "esbuild": "npm:esbuild@>=0.17.6 <0.26.0", "eval": "0.1.8", "find-up": "^5.0.0", "javascript-stringify": "^2.0.1", "mlly": "^1.4.2" } }, ""], + + "@vanilla-extract/private": ["@vanilla-extract/private@1.0.9", "", {}, ""], + + "@vanilla-extract/vite-plugin": ["@vanilla-extract/vite-plugin@5.1.1", "", { "dependencies": { "@vanilla-extract/compiler": "^0.3.1", "@vanilla-extract/integration": "^8.0.4" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, ""], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.43", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, ""], + + "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, ""], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, ""], + + "astring": ["astring@1.9.0", "", { "bin": "bin/astring" }, ""], + + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, ""], + + "bail": ["bail@2.0.2", "", {}, ""], + + "base64-js": ["base64-js@1.5.1", "", {}, ""], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": "dist/cli.js" }, ""], + + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, ""], + + "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, ""], + + "boolbase": ["boolbase@1.0.0", "", {}, ""], + + "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, ""], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, ""], + + "bytes": ["bytes@3.1.2", "", {}, ""], + + "cac": ["cac@6.7.14", "", {}, ""], + + "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, ""], + + "ccount": ["ccount@2.0.1", "", {}, ""], + + "chalk": ["chalk@5.6.2", "", {}, ""], + + "character-entities": ["character-entities@2.0.2", "", {}, ""], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, ""], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, ""], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, ""], + + "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, ""], + + "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, ""], + + "chroma-js": ["chroma-js@3.1.2", "", {}, ""], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, ""], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, ""], + + "clsx": ["clsx@2.1.1", "", {}, ""], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, ""], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, ""], + + "commander": ["commander@8.3.0", "", {}, ""], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, ""], + + "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, ""], + + "confbox": ["confbox@0.1.8", "", {}, ""], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, ""], + + "cookie": ["cookie@1.0.2", "", {}, ""], + + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, ""], + + "create-vocs": ["create-vocs@1.0.0", "", { "dependencies": { "@clack/prompts": "^0.7.0", "cac": "^6.7.14", "detect-package-manager": "^3.0.2", "fs-extra": "^11.3.0", "picocolors": "^1.1.1" }, "bin": "_lib/bin.js" }, ""], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], + + "css-selector-parser": ["css-selector-parser@3.1.3", "", {}, ""], + + "css-what": ["css-what@6.2.2", "", {}, ""], + + "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, ""], + + "csstype": ["csstype@3.1.3", "", {}, ""], + + "cytoscape": ["cytoscape@3.33.1", "", {}, ""], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, ""], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, ""], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, ""], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, ""], + + "d3-axis": ["d3-axis@3.0.0", "", {}, ""], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, ""], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, ""], + + "d3-color": ["d3-color@3.1.0", "", {}, ""], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, ""], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, ""], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, ""], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, ""], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, ""], + + "d3-ease": ["d3-ease@3.0.1", "", {}, ""], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, ""], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, ""], + + "d3-format": ["d3-format@3.1.0", "", {}, ""], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, ""], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, ""], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, ""], + + "d3-path": ["d3-path@3.1.0", "", {}, ""], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, ""], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, ""], + + "d3-random": ["d3-random@3.0.1", "", {}, ""], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, ""], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, ""], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, ""], + + "d3-selection": ["d3-selection@3.0.0", "", {}, ""], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, ""], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, ""], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, ""], + + "d3-timer": ["d3-timer@3.0.1", "", {}, ""], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, ""], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, ""], + + "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, ""], + + "dayjs": ["dayjs@1.11.18", "", {}, ""], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], + + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, ""], + + "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, ""], + + "deep-object-diff": ["deep-object-diff@1.1.9", "", {}, ""], + + "deepmerge": ["deepmerge@4.3.1", "", {}, ""], + + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, ""], + + "depd": ["depd@2.0.0", "", {}, ""], + + "dequal": ["dequal@2.0.3", "", {}, ""], + + "destroy": ["destroy@1.2.0", "", {}, ""], + + "detect-libc": ["detect-libc@2.1.2", "", {}, ""], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, ""], + + "detect-package-manager": ["detect-package-manager@3.0.2", "", { "dependencies": { "execa": "^5.1.1" } }, ""], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, ""], + + "direction": ["direction@2.0.1", "", { "bin": "cli.js" }, ""], + + "dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, ""], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, ""], + + "ee-first": ["ee-first@1.1.1", "", {}, ""], + + "electron-to-chromium": ["electron-to-chromium@1.5.243", "", {}, ""], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, ""], + + "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, ""], + + "encodeurl": ["encodeurl@2.0.0", "", {}, ""], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, ""], + + "entities": ["entities@6.0.1", "", {}, ""], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, ""], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, ""], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, ""], + + "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": "bin/esbuild" }, ""], + + "escalade": ["escalade@3.2.0", "", {}, ""], + + "escape-html": ["escape-html@1.0.3", "", {}, ""], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, ""], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, ""], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, ""], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, ""], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, ""], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, ""], + + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, ""], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, ""], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, ""], + + "etag": ["etag@1.8.1", "", {}, ""], + + "eval": ["eval@0.1.8", "", { "dependencies": { "@types/node": "*", "require-like": ">= 0.1.1" } }, ""], + + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, ""], + + "exsolve": ["exsolve@1.0.7", "", {}, ""], + + "extend": ["extend@3.0.2", "", {}, ""], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, ""], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""], + + "format": ["format@0.2.2", "", {}, ""], + + "fraction.js": ["fraction.js@4.3.7", "", {}, ""], + + "fresh": ["fresh@0.5.2", "", {}, ""], + + "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, ""], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, ""], + + "get-nonce": ["get-nonce@1.0.1", "", {}, ""], + + "get-stream": ["get-stream@6.0.1", "", {}, ""], + + "github-slugger": ["github-slugger@2.0.0", "", {}, ""], + + "globals": ["globals@15.15.0", "", {}, ""], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, ""], + + "hachure-fill": ["hachure-fill@0.5.2", "", {}, ""], + + "hast-util-classnames": ["hast-util-classnames@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "space-separated-tokens": "^2.0.0" } }, ""], + + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, ""], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, ""], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, ""], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, ""], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, ""], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, ""], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, ""], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, ""], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, ""], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, ""], + + "hastscript": ["hastscript@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, ""], + + "hono": ["hono@4.10.3", "", {}, ""], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, ""], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, ""], + + "human-signals": ["human-signals@2.1.0", "", {}, ""], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, ""], + + "ieee754": ["ieee754@1.2.1", "", {}, ""], + + "inherits": ["inherits@2.0.4", "", {}, ""], + + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, ""], + + "internmap": ["internmap@1.0.1", "", {}, ""], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, ""], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, ""], + + "is-decimal": ["is-decimal@2.0.1", "", {}, ""], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, ""], + + "is-interactive": ["is-interactive@2.0.0", "", {}, ""], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, ""], + + "is-stream": ["is-stream@2.0.1", "", {}, ""], + + "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, ""], + + "isexe": ["isexe@2.0.0", "", {}, ""], + + "javascript-stringify": ["javascript-stringify@2.1.0", "", {}, ""], + + "jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, ""], + + "js-tokens": ["js-tokens@4.0.0", "", {}, ""], + + "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, ""], + + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, ""], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], + + "katex": ["katex@0.16.25", "", { "dependencies": { "commander": "^8.3.0" }, "bin": "cli.js" }, ""], + + "khroma": ["khroma@2.1.0", "", {}, ""], + + "kolorist": ["kolorist@1.8.0", "", {}, ""], + + "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, ""], + + "layout-base": ["layout-base@1.0.2", "", {}, ""], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, ""], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, ""], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, ""], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, ""], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""], + + "lodash-es": ["lodash-es@4.17.21", "", {}, ""], + + "log-symbols": ["log-symbols@5.1.0", "", { "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" } }, ""], + + "longest-streak": ["longest-streak@3.1.0", "", {}, ""], + + "lru-cache": ["lru-cache@10.4.3", "", {}, ""], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, ""], + + "mark.js": ["mark.js@8.11.1", "", {}, ""], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, ""], + + "markdown-table": ["markdown-table@3.0.4", "", {}, ""], + + "marked": ["marked@16.4.1", "", { "bin": "bin/marked.js" }, ""], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, ""], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, ""], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, ""], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, ""], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, ""], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, ""], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, ""], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, ""], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, ""], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, ""], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, ""], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, ""], + + "media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, ""], + + "merge-stream": ["merge-stream@2.0.0", "", {}, ""], + + "mermaid": ["mermaid@11.12.1", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, ""], + + "mermaid-isomorphic": ["mermaid-isomorphic@3.0.4", "", { "dependencies": { "@fortawesome/fontawesome-free": "^6.0.0", "mermaid": "^11.0.0" }, "peerDependencies": { "playwright": "1" } }, ""], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, ""], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, ""], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, ""], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, ""], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, ""], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, ""], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, ""], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, ""], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, ""], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, ""], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, ""], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, ""], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, ""], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, ""], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, ""], + + "mime": ["mime@1.6.0", "", { "bin": "cli.js" }, ""], + + "mime-db": ["mime-db@1.54.0", "", {}, ""], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, ""], + + "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": "cli.js" }, ""], + + "minisearch": ["minisearch@7.2.0", "", {}, ""], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, ""], + + "modern-ahocorasick": ["modern-ahocorasick@1.1.0", "", {}, ""], + + "ms": ["ms@2.0.0", "", {}, ""], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""], + + "negotiator": ["negotiator@0.6.4", "", {}, ""], + + "node-releases": ["node-releases@2.0.27", "", {}, ""], + + "normalize-range": ["normalize-range@0.1.2", "", {}, ""], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, ""], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, ""], + + "nuqs": ["nuqs@2.7.2", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^6 || ^7", "react-router-dom": "^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router-dom"] }, ""], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""], + + "on-headers": ["on-headers@1.1.0", "", {}, ""], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, ""], + + "oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, ""], + + "ora": ["ora@7.0.1", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.9.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.3.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "string-width": "^6.1.0", "strip-ansi": "^7.1.0" } }, ""], + + "p-limit": ["p-limit@5.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, ""], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, ""], + + "package-manager-detector": ["package-manager-detector@1.5.0", "", {}, ""], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, ""], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, ""], + + "parseurl": ["parseurl@1.3.3", "", {}, ""], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, ""], + + "path-exists": ["path-exists@4.0.0", "", {}, ""], + + "path-key": ["path-key@3.1.1", "", {}, ""], + + "pathe": ["pathe@2.0.3", "", {}, ""], + + "picocolors": ["picocolors@1.1.1", "", {}, ""], + + "picomatch": ["picomatch@4.0.3", "", {}, ""], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, ""], + + "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": "cli.js" }, ""], + + "playwright-core": ["playwright-core@1.56.1", "", { "bin": "cli.js" }, ""], + + "points-on-curve": ["points-on-curve@0.2.0", "", {}, ""], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, ""], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, ""], + + "property-information": ["property-information@6.5.0", "", {}, ""], + + "quansync": ["quansync@0.2.11", "", {}, ""], + + "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react-dom"] }, ""], + + "range-parser": ["range-parser@1.2.1", "", {}, ""], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-intersection-observer": ["react-intersection-observer@9.16.0", "", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""], + + "react-refresh": ["react-refresh@0.18.0", "", {}, ""], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""], + + "react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, ""], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, ""], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, ""], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, ""], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, ""], + + "regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, ""], + + "regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, ""], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, ""], + + "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, ""], + + "rehype-class-names": ["rehype-class-names@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-classnames": "^3.0.0", "hast-util-select": "^6.0.0", "unified": "^11.0.4" } }, ""], + + "rehype-mermaid": ["rehype-mermaid@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "mermaid-isomorphic": "^3.0.0", "mini-svg-data-uri": "^1.0.0", "space-separated-tokens": "^2.0.0", "unified": "^11.0.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "playwright": "1" } }, ""], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, ""], + + "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, ""], + + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, ""], + + "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, ""], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, ""], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, ""], + + "remark-mdx-frontmatter": ["remark-mdx-frontmatter@5.2.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "estree-util-value-to-estree": "^3.0.0", "toml": "^3.0.0", "unified": "^11.0.0", "unist-util-mdx-define": "^1.0.0", "yaml": "^2.0.0" } }, ""], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, ""], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, ""], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, ""], + + "require-like": ["require-like@0.1.2", "", {}, ""], + + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, ""], + + "robust-predicates": ["robust-predicates@3.0.2", "", {}, ""], + + "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, ""], + + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, ""], + + "rw": ["rw@1.3.3", "", {}, ""], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, ""], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, ""], + + "scheduler": ["scheduler@0.27.0", "", {}, ""], + + "semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, ""], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, ""], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, ""], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, ""], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, ""], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], + + "shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, ""], + + "signal-exit": ["signal-exit@3.0.7", "", {}, ""], + + "sisteransi": ["sisteransi@1.0.5", "", {}, ""], + + "source-map": ["source-map@0.7.6", "", {}, ""], + + "source-map-js": ["source-map-js@1.2.1", "", {}, ""], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, ""], + + "statuses": ["statuses@2.0.1", "", {}, ""], + + "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, ""], + + "string-width": ["string-width@6.1.0", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^10.2.1", "strip-ansi": "^7.0.1" } }, ""], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, ""], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, ""], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, ""], + + "style-to-js": ["style-to-js@1.1.18", "", { "dependencies": { "style-to-object": "1.0.11" } }, ""], + + "style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, ""], + + "stylis": ["stylis@4.3.6", "", {}, ""], + + "tabbable": ["tabbable@6.3.0", "", {}, ""], + + "tailwindcss": ["tailwindcss@4.1.16", "", {}, ""], + + "tapable": ["tapable@2.3.0", "", {}, ""], + + "tinyexec": ["tinyexec@1.0.1", "", {}, ""], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""], + + "toidentifier": ["toidentifier@1.0.1", "", {}, ""], + + "toml": ["toml@3.0.0", "", {}, ""], + + "trim-lines": ["trim-lines@3.0.1", "", {}, ""], + + "trough": ["trough@2.2.0", "", {}, ""], + + "ts-dedent": ["ts-dedent@2.2.0", "", {}, ""], + + "tslib": ["tslib@2.8.1", "", {}, ""], + + "twoslash": ["twoslash@0.3.4", "", { "dependencies": { "@typescript/vfs": "^1.6.1", "twoslash-protocol": "0.3.4" }, "peerDependencies": { "typescript": "^5.5.0" } }, ""], + + "twoslash-protocol": ["twoslash-protocol@0.3.4", "", {}, ""], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": "script/cli.js" }, ""], + + "ufo": ["ufo@1.6.1", "", {}, ""], + + "undici-types": ["undici-types@7.16.0", "", {}, ""], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, ""], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, ""], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, ""], + + "unist-util-mdx-define": ["unist-util-mdx-define@1.1.2", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "vfile": "^6.0.0" } }, ""], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, ""], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, ""], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, ""], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, ""], + + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, ""], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, ""], + + "universalify": ["universalify@2.0.1", "", {}, ""], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, ""], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, ""], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, ""], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, ""], + + "uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, ""], + + "vary": ["vary@1.1.2", "", {}, ""], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, ""], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, ""], + + "vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, ""], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, ""], + + "vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, ""], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": "vite-node.mjs" }, ""], + + "vocs": ["vocs@1.1.0", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "@hono/node-server": "^1.19.5", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@mdx-js/rollup": "^3.1.1", "@noble/hashes": "^1.7.1", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-tabs": "^1.1.3", "@shikijs/rehype": "^1", "@shikijs/transformers": "^1", "@shikijs/twoslash": "^1", "@tailwindcss/vite": "4.1.15", "@vanilla-extract/css": "^1.17.4", "@vanilla-extract/dynamic": "^2.1.5", "@vanilla-extract/vite-plugin": "^5.1.1", "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.21", "cac": "^6.7.14", "chroma-js": "^3.1.2", "clsx": "^2.1.1", "compression": "^1.8.1", "create-vocs": "^1.0.0-alpha.5", "cross-spawn": "^7.0.6", "fs-extra": "^11.3.2", "hastscript": "^8.0.0", "hono": "^4.10.3", "mark.js": "^8.11.1", "mdast-util-directive": "^3.1.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx": "^3.0.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "mdast-util-to-markdown": "^2.1.2", "minisearch": "^7.2.0", "nuqs": "^2.7.2", "ora": "^7.0.1", "p-limit": "^5.0.0", "picomatch": "^4.0.3", "playwright": "^1.52.0", "postcss": "^8.5.2", "radix-ui": "^1.1.3", "react-intersection-observer": "^9.15.1", "react-router": "^7.9.4", "rehype-autolink-headings": "^7.1.0", "rehype-class-names": "^2.0.0", "rehype-mermaid": "^3.0.0", "rehype-slug": "^6.0.0", "remark-directive": "^3.0.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", "remark-mdx-frontmatter": "^5.2.0", "remark-parse": "^11.0.0", "serve-static": "^1.16.2", "shiki": "^1", "toml": "^3.0.0", "twoslash": "~0.3.4", "ua-parser-js": "^1.0.40", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile-matter": "^5.0.1", "vite": "^7.1.11", "yaml": "^2.8.1" }, "peerDependencies": { "react": "^19", "react-dom": "^19" }, "bin": "_lib/cli/index.js" }, "sha512-RvSdP+OP5w/mzY6rupup8RLRRfi+9dqZmiiB/NOFrQds5iZNR2h2hNui+oKXUwAxv/Dywo2ma4YYSy6v/aw0Dw=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, ""], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, ""], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, ""], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, ""], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, ""], + + "vscode-uri": ["vscode-uri@3.0.8", "", {}, ""], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, ""], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], + + "yallist": ["yallist@3.1.1", "", {}, ""], + + "yaml": ["yaml@2.8.1", "", { "bin": "bin.mjs" }, ""], + + "yocto-queue": ["yocto-queue@1.2.1", "", {}, ""], + + "zwitch": ["zwitch@2.0.4", "", {}, ""], + + "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, ""], + + "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "@iconify/utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, ""], + + "@shikijs/twoslash/twoslash": ["twoslash@0.2.12", "", { "dependencies": { "@typescript/vfs": "^1.6.0", "twoslash-protocol": "0.2.12" }, "peerDependencies": { "typescript": "*" } }, ""], + + "@tailwindcss/node/tailwindcss": ["tailwindcss@4.1.15", "", {}, ""], + + "@tailwindcss/vite/tailwindcss": ["tailwindcss@4.1.15", "", {}, ""], + + "@typescript/vfs/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, ""], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, ""], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, ""], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, ""], + + "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, ""], + + "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, ""], + + "hast-util-from-parse5/property-information": ["property-information@7.1.0", "", {}, ""], + + "hast-util-select/property-information": ["property-information@7.1.0", "", {}, ""], + + "hast-util-to-estree/property-information": ["property-information@7.1.0", "", {}, ""], + + "hast-util-to-html/property-information": ["property-information@7.1.0", "", {}, ""], + + "hast-util-to-jsx-runtime/property-information": ["property-information@7.1.0", "", {}, ""], + + "local-pkg/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, ""], + + "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, ""], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, ""], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, ""], + + "send/ms": ["ms@2.1.3", "", {}, ""], + + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "vite-node/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + + "@babel/core/debug/ms": ["ms@2.1.3", "", {}, ""], + + "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, ""], + + "@iconify/utils/debug/ms": ["ms@2.1.3", "", {}, ""], + + "@shikijs/twoslash/twoslash/twoslash-protocol": ["twoslash-protocol@0.2.12", "", {}, ""], + + "@typescript/vfs/debug/ms": ["ms@2.1.3", "", {}, ""], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, ""], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, ""], + + "hast-util-from-dom/hastscript/property-information": ["property-information@7.1.0", "", {}, ""], + + "local-pkg/pkg-types/confbox": ["confbox@0.2.2", "", {}, ""], + + "micromark/debug/ms": ["ms@2.1.3", "", {}, ""], + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, ""], + + "vite-node/debug/ms": ["ms@2.1.3", "", {}, ""], + } +} diff --git a/kona/docs/docs/components/CodeGroup.tsx b/kona/docs/docs/components/CodeGroup.tsx new file mode 100644 index 0000000000000..f508a4ce583b3 --- /dev/null +++ b/kona/docs/docs/components/CodeGroup.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react' + +interface CodeProps { + title: string + code: string +} + +interface CodeGroupProps { + children: React.ReactElement[] +} + +export function CodeGroup({ children }: CodeGroupProps) { + const [activeTab, setActiveTab] = useState(0) + + return ( +
+
+ {children.map((child, index) => ( + + ))} +
+
+
+          
+            {children[activeTab].props.code}
+          
+        
+
+
+ ) +} + +export function Code({ title, code }: CodeProps) { + return null // This component is only used as a child of CodeGroup +} \ No newline at end of file diff --git a/kona/docs/docs/components/SdkShowcase.tsx b/kona/docs/docs/components/SdkShowcase.tsx new file mode 100644 index 0000000000000..667c5ef928f4f --- /dev/null +++ b/kona/docs/docs/components/SdkShowcase.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +interface SdkProject { + name: string + description: string + linesOfCode: string + githubUrl: string +} + +const projects: SdkProject[] = [ + { + name: 'Kona Client', + description: 'Fault proof program for rollup state transitions', + linesOfCode: '~3K LoC', + githubUrl: 'https://github.com/op-rs/kona/tree/main/bin/client' + }, + { + name: 'Kona Node', + description: 'Modular OP Stack rollup node implementation', + linesOfCode: '~8K LoC', + githubUrl: 'https://github.com/op-rs/kona/tree/main/bin/node' + }, + { + name: 'OP Succinct', + description: 'zkVM-based proof system using Kona', + linesOfCode: '~2K LoC', + githubUrl: 'https://github.com/succinctlabs/op-succinct' + }, + { + name: 'Kailua', + description: 'zkVM-based proof system using Kona', + linesOfCode: '~5K LoC', + githubUrl: 'https://github.com/boundless-xyz/kailua' + } +] + +export function SdkShowcase() { + return ( +
+ {projects.map((project, index) => ( +
+
+

{project.name}

+ + + + + +
+

{project.description}

+
+ {project.linesOfCode} +
+
+ ))} +
+ ) +} diff --git a/kona/docs/docs/components/TrustedBy.tsx b/kona/docs/docs/components/TrustedBy.tsx new file mode 100644 index 0000000000000..bbb60c2efe6dc --- /dev/null +++ b/kona/docs/docs/components/TrustedBy.tsx @@ -0,0 +1,48 @@ +import React from 'react' + +interface TrustedCompany { + name: string + logoUrl: string +} + +const companies: TrustedCompany[] = [ + { + name: 'OP Labs', + logoUrl: 'https://avatars.githubusercontent.com/u/109625874?s=200&v=4' + }, + { + name: 'Base', + logoUrl: 'https://avatars.githubusercontent.com/u/106747352?s=200&v=4' + }, + { + name: 'Conduit', + logoUrl: 'https://avatars.githubusercontent.com/u/108968326?s=200&v=4' + }, + { + name: 'Lattice', + logoUrl: 'https://avatars.githubusercontent.com/u/17163988?s=200&v=4' + } +] + +export function TrustedBy() { + return ( +
+ {companies.map((company, index) => ( +
+
+ {`${company.name} +
+
+ ))} +
+ ) +} \ No newline at end of file diff --git a/kona/docs/docs/pages/glossary.mdx b/kona/docs/docs/pages/glossary.mdx new file mode 100644 index 0000000000000..41b12e4738357 --- /dev/null +++ b/kona/docs/docs/pages/glossary.mdx @@ -0,0 +1,41 @@ +# Glossary + +*This document contains definitions for terms used throughout the Kona book.* + +#### Fault Proof VM +A `Fault Proof VM` is a virtual machine, commonly supporting a subset of the Linux kernel's syscalls and a modified subset of an existing reduced instruction set architecture, +that is designed to execute verifiable programs. + +Full specification for the `cannon` & `cannon-rs` FPVMs, as an example, is available in the [Optimism Monorepo][cannon-specs]. + +#### Fault Proof Program +A `Fault Proof Program` is a program, commonly written in a general-purpose language such as Golang, C, or Rust, that may be compiled down +to a compatible `Fault Proof VM` target and provably executed on that target VM. + +Examples of `Fault Proof Programs` include the [OP Program][op-program], which runs on top of [`cannon`][cannon], [`cannon-rs`][cannon-rs], and +[`asterisc`][asterisc] to verify a claim about the state of an [OP Stack][op-stack] layer two. + +#### Preimage ABI +The `Preimage ABI` is a specification for a synchronous communication protocol between a `client` and a `host` that is used to request and read data from the `host`'s +datastore. Full specifications for the `Preimage ABI` are available in the [Optimism Monorepo][preimage-specs]. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/index.mdx b/kona/docs/docs/pages/index.mdx new file mode 100644 index 0000000000000..f272433214598 --- /dev/null +++ b/kona/docs/docs/pages/index.mdx @@ -0,0 +1,146 @@ +--- +layout: landing +--- + +import { HomePage } from 'vocs/components' +import { SdkShowcase } from '../components/SdkShowcase' +import { CodeGroup, Code } from '../components/CodeGroup' + +
+
+
+
+

+ Kona +

+

+ OP Stack Components built in Rust +

+

+ A comprehensive suite of low-level OP Stack types, portable `no_std` + components, and services built in Rust. Kona provides a rollup node + implementation called the `kona-node` that is spec-compliant, performant, + and modular as well as the Kona Fault Proof implementation. Built by OP Labs. +

+
+
+
+ + + + +
+
+
+ +
+
+ + +
+
+ + 200+ + stars +
+
+ 👥 + 50+ + contributors +
+
+ 📜 + MIT + license +
+
+
+
+ +
+
+
+
+
🔒
+

Secure

+

+ Built with Rust's memory safety guarantees and comprehensive test coverage for production reliability. +

+
+
+
+
+
+

Performant

+

+ Optimized for minimal resource usage with `no_std` compatibility and efficient execution. +

+
+
+
+
+
🧩
+

Customizable

+

+ Extensible architecture with composable crates that can be used independently or together. +

+
+
+
+
+
🌐
+

Extensible

+

+ Supports multiple proof backends including FPVM, SP-1, Risc0, and other verifiable environments. +

+
+
+
+
+ + +
+
+

Built with Kona SDK

+

+ Production implementations using Kona's modular architecture +

+ +
+
diff --git a/kona/docs/docs/pages/intro/contributing.mdx b/kona/docs/docs/pages/intro/contributing.mdx new file mode 100644 index 0000000000000..dc81eca93fd86 --- /dev/null +++ b/kona/docs/docs/pages/intro/contributing.mdx @@ -0,0 +1,63 @@ +# Contributing + +Thank you for looking into contributing! + +Before making contributions to Kona, please read through this +guide and discuss the change you wish to make via issue. + +## Dependencies + +Before working with this repository locally, you'll need to install several dependencies: + +- [Docker](https://www.docker.com/) for cross-compilation. +- [just](https://github.com/casey/just) for our command-runner scripts. +- The [Rust toolchain](https://rustup.rs/) +- The [Golang toolchain](https://go.dev/dl/) + + +## Pull Request Process + +1. Before anything, [create an issue](https://github.com/op-rs/kona/issues/new) to discuss the change you're + wanting to make, if it is significant or changes functionality. Feel free to skip this step for trivial changes. +1. Once your change is implemented, ensure that all checks are passing before creating a PR. The full CI pipeline can + be run locally via the `justfile`s in the repository. +1. Make sure to update any documentation that has gone stale as a result of the change, in the `README` files, the [book][book], + and in rustdoc comments. +1. Once you have sign-off from a maintainer, you may merge your pull request yourself if you have permissions to do so. + If not, the maintainer who approves your pull request will add it to the merge queue. + + +## Getting Help + +Need support or have questions? Open a github issue: + +- **GitHub Issues**: [Open an issue](https://github.com/op-rs/kona/issues/new) for bugs or feature requests + + +### Crates + +The repository is organized into focused crates under `/crates/`: + +``` +crates/ +├── node/ # Consensus Node +├── proof/ # Fault Proof Program +├── protocol/ # Protocol Libraries +├── providers/ # Provider Trait Implementations +├── supervisor/ # OP Stack Supervisor +├── utilities/ # Common Utility Crates +└── ... +``` + + +### Binaries + +Executables are located in the `/bin/` directory. + +``` +bin/ +├── node/ # Consensus Node +├── client/ # Fault Proof Client +├── host/ # Fault Proof Host +└── supervisor/ # OP Stack Supervisor +``` diff --git a/kona/docs/docs/pages/intro/lore.mdx b/kona/docs/docs/pages/intro/lore.mdx new file mode 100644 index 0000000000000..581ab02a4f48f --- /dev/null +++ b/kona/docs/docs/pages/intro/lore.mdx @@ -0,0 +1,46 @@ +# Lore + +A long time ago, during ETH Denver 2023 in February, [@clabby][clabby] +and [@refcell][refcell] embarked on a journey to apply the minimal diff +from `op-geth` to reth, creating the first Rust execution client +implementation for the OP Stack. This kicked off a series of development +to bring the OP Stack into a multi-client future. + +At the same time, Optimism was undergoing its biggest upgrade yet - Bedrock. +This upgrade completely redefined Optimism. By sunsetting the OVM, Bedrock +laid the foundation for the OP Stack, bringing Optimism out of the dark ages. +The key architectural change was the concept of "minimal diff" which effectively +applies a minimal set of changes to the Ethereum execution layer (`op-geth`), +rather than a new virtual machine (the OVM). + +Fast forward around 6 months, and `op-reth` was presented at Frontiers 2023. +It took a lot of debugging the night before to get it to work, but it was +syncing just in time. This piece of history was [recorded by the wonderful +folks at Paradigm][recording]. + +For the next year, a small 6 person team at OP Labs including [@clabby][clabby] and +[@refcell][refcell] went heads-down to build Fault Proofs for the OP Stack. `op-reth` +was left in the good hands of the Reth contributors and the community, +eventually being adopted by folks at Base in production. + +Once Fault Proofs were released with guardrails in the spring of 2024, +[@clabby] and [@refcell] returned to the world of Rust to kick off a new fault +proof implementation, this time in Rust. Using our learnings from [op-program], we +built the kona-proof, and [released it at Frontiers 2024][proofs]. + +After a few hardforks and side quests into interop, [@refcell][refcell] kicked +off the `kona-node` project to build an OP Stack rollup node in Rust, built off +the abstractions we built in Kona. It became clear that Kona is more than just +one OP Stack component, but a monorepo for the entire OP Stack in Rust. + +Now, in 2025, [@clabby][clabby], [@theochap][theo], and [@refcell][refcell] are +cooking on Kona, iteratively building out the OP Stack in Rust. Come join us! + + +[proofs]: https://www.youtube.com/watch?v=mkygYH-07Hw&pp=ygUVZnJvbnRpZXJzIDIwMjQgY2xhYmJ5 +[recording]: https://www.youtube.com/watch?v=AVYggka6_2Y&pp=ygUWZnJvbnRpZXJzIDIwMjMgb3AtcmV0aA%3D%3D +[refcell]: https://github.com/refcell +[clabby]: https://github.com/clabby +[theo]: https://github.com/theochap +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[optimism]: https://github.com/ethereum-optimism/optimism diff --git a/kona/docs/docs/pages/intro/overview.mdx b/kona/docs/docs/pages/intro/overview.mdx new file mode 100644 index 0000000000000..18d1c63de10b0 --- /dev/null +++ b/kona/docs/docs/pages/intro/overview.mdx @@ -0,0 +1,93 @@ +import { Callout } from 'vocs/components' + +# Kona [Documentation for Kona users and developers] + +Kona is an implementation of the [OP Stack][op-stack] written in Rust, +designed to be modular and extensible. `no_std` support is prioritized +to provide the building blocks for fault proofs. + + +Kona is in active development and should be considered experimental. + + + +These docs may contain inaccuracies as it evolves. + +Please [open an issue][new-issue] if you find any errors or have any suggestions +for improvements, and also feel free to [contribute][contributing] to the project! + + + +## Introduction + +Originally a suite of portable implementations of the OP Stack rollup state transition, +Kona has been extended to be _the monorepo_ for OP Stack +types, components, and services built in Rust. Kona provides an ecosystem of extensible, low-level +crates that compose into components and services required for the OP Stack. + +Protocol crates are `no_std` compatible for use within the Fault Proof. Types defined in these +libraries are shared by other components of the OP Stack including the rollup node. + +Proof crates are available for developing verifiable Rust programs targeting +[Fault Proof VMs](/glossary#fault-proof-vm). +These libraries provide tooling and abstractions around low-level syscalls, memory management, +and other common structures that authors of verifiable programs will need to interact with. +It also provides build pipelines for compiling `no_std` Rust programs to a format that can be +executed by supported Fault Proof VM targets. + +Kona is built and maintained by open source contributors and is licensed under the MIT License. + +## Goals of Kona + +**1. Composability** + +Kona provides a common set of tools and abstractions for developing verifiable Rust programs +on top of several supported Fault Proof VM targets. This is done to ensure that programs +written for one supported FPVM can be easily ported to another supported FPVM, and that the +ecosystem of programs built on top of these targets can be easily shared and reused. + +**2. Safety** + +Through standardization of these low-level system interfaces and build pipelines, Kona seeks +to increase coverage over the low-level operations that are required to build on top of a FPVM. + +**3. Developer Experience** + +Building on top of custom Rust targets can be difficult, especially when the target is +nascent and tooling is not yet mature. Kona seeks to improve this experience by standardizing +and streamlining the process of developing and compiling verifiable Rust programs, targeted +at supported FPVMs. + +**4. Performance** + +Kona is opinionated in that it favors `no_std` Rust programs for embedded FPVM development, +for both performance and portability. In contrast with alternative approaches, such as the +[`op-program`][op-program] using the Golang `MIPS64` target, `no_std` Rust programs produce +much smaller binaries, resulting in fewer instructions that need to be executed on the FPVM. +In addition, this offers developers more low-level control over interactions with the FPVM +kernel, which can be useful for optimizing performance-critical code. + +## Contributing + +Contributors are welcome! Please see the [contributing guide][contributing] for more information. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/intro/why.mdx b/kona/docs/docs/pages/intro/why.mdx new file mode 100644 index 0000000000000..7df0abc762f39 --- /dev/null +++ b/kona/docs/docs/pages/intro/why.mdx @@ -0,0 +1,45 @@ +import { Callout } from 'vocs/components' + +# Why Kona? + +Kona leverages the abstractions of the Rust Ethereum ecosystem +providing a modular, extensible implementation of the OP Stack. +By re-implementing the OP Stack in Rust, Kona brings the +superchain into a robust, multi-client future. + +## Abstractions + +Building off learnings from the [optimism monorepo][optimism], Kona +is trait abstracted and customizable. Entire components can be swapped +out, and new behavior easily injected. + +- **Components**: Kona is built and grouped into modular components + that can be used independently or together. This allows developers to + pick and choose the parts they need for their specific use case. +- **Trait Abstractions**: Kona uses trait abstractions between and inside + components to allow for easy customization and extension. +- **Public Crates**: Using Rust's crate system, Kona benefits from upstream + improvements through crate re-use. It also means, custom components for + Kona can be easily shared and reused across the ecosystem. + + +## Adoption + +Kona is being adopted and customized through its [abstractions](#abstractions) +across the OP Stack ecosystem. Further, Kona's OP Stack components are +continuously being tested against the reference implementations in the +[Optimism Monorepo][optimism] to ensure compatibility and correctness +(in both implementations). Continuous testing happens across different +repository CI sets as well as long-lived testnets and periphery nodes. + + +## Community + +Contributions are highly praised and appreciated. It's extremely difficult +to build a thriving contributor community. Kona isn't where we'd like to be +just yet, but we aspire to be like Reth. + +That said, come join us and leave a lasting impact on the OP Stack! + + +[optimism]: https://github.com/ethereum-optimism/optimism diff --git a/kona/docs/docs/pages/node/configuration.mdx b/kona/docs/docs/pages/node/configuration.mdx new file mode 100644 index 0000000000000..6fb9bd6bc3283 --- /dev/null +++ b/kona/docs/docs/pages/node/configuration.mdx @@ -0,0 +1,185 @@ +# Kona Node CLI Reference + +This document lists all CLI flags for the `kona-node node` subcommand, grouped by category. All flags can be provided as command-line arguments or via environment variables. + +:::note +For more details on each flag, see the inline help (`kona-node node --help`) or the source code. +::: + +## Default Ports + +| Service | Default Port | Flag/Env | +|-----------------|--------------|------------------------------------------| +| RPC HTTP | 9545 | `--port` / `KONA_NODE_RPC_PORT` | +| RPC WebSocket | 9545 | (same as HTTP, enabled with `--rpc.ws-enabled`) | +| P2P TCP | 9222 | `--p2p.listen.tcp` / `KONA_NODE_P2P_LISTEN_TCP_PORT` | +| P2P UDP | 9223 | `--p2p.listen.udp` / `KONA_NODE_P2P_LISTEN_UDP_PORT` | +| Supervisor RPC | 9333 | `--supervisor.port` / `KONA_NODE_SEQUENCER_PORT` | +| Conductor RPC | 8547 | `--conductor.rpc` / `KONA_NODE_CONDUCTOR_RPC` | + +## Core Node Arguments + +| Flag | Env | Description | Required | Default | +|------|-----|-------------|----------|---------| +| `--mode ` | `KONA_NODE_MODE` | Mode of operation for the node | Yes | `verifier` | +| `--l1-eth-rpc ` | `KONA_NODE_L1_ETH_RPC` | URL of the L1 execution client RPC API | Yes | - | +| `--l1-trust-rpc ` | `KONA_NODE_L1_TRUST_RPC` | Whether to trust the L1 RPC without verification | No | `true` | +| `--l1-beacon ` | `KONA_NODE_L1_BEACON` | URL of the L1 beacon API | Yes | - | +| `--l2-engine-rpc ` | `KONA_NODE_L2_ENGINE_RPC` | URL of the engine API endpoint of an L2 execution client | Yes | - | +| `--l2-trust-rpc ` | `KONA_NODE_L2_TRUST_RPC` | Whether to trust the L2 RPC without verification | No | `true` | +| `--l2-engine-jwt-secret ` | `KONA_NODE_L2_ENGINE_AUTH` | Path to file containing the hex-encoded JWT secret for the execution client | No | - | +| `--l2-config-file ` | `KONA_NODE_ROLLUP_CONFIG` | Path to a custom L2 rollup configuration file | No | - | +| `--l1-runtime-config-reload-interval ` | `KONA_NODE_L1_RUNTIME_CONFIG_RELOAD_INTERVAL` | Poll interval for reloading runtime config | No | `600` | + +## Global Arguments + +| Flag | Env | Description | Required | Default | +|------|-----|-------------|----------|---------| +| `--l2-chain-id ` or `-c ` | `KONA_NODE_L2_CHAIN_ID` | L2 chain ID (numeric) or chain name (string) | No | `10` (Optimism) | + +### Chain ID Support + +The `--l2-chain-id` flag supports flexible chain identification using the `alloy_chains` crate: + +**Numeric Chain IDs:** +```bash +kona-node --l2-chain-id 10 node [args...] # Optimism mainnet +kona-node --l2-chain-id 8453 node [args...] # Base mainnet +kona-node --l2-chain-id 1 node [args...] # Ethereum mainnet +``` + +**String Chain Names:** +```bash +kona-node --l2-chain-id optimism node [args...] +kona-node --l2-chain-id base node [args...] +kona-node --l2-chain-id mainnet node [args...] +``` + +**Short Flag and Environment Variable:** +```bash +kona-node -c optimism node [args...] +export KONA_NODE_L2_CHAIN_ID=optimism && kona-node node [args...] +``` + +Supported chain names include all those recognized by `alloy_chains` (e.g., `optimism`, `base`, `mainnet`). Unknown numeric chain IDs are accepted for custom networks. + +## P2P Arguments + +| Flag | Env | Description | Default | +|------|-----|-------------|---------| +| `--p2p.no-discovery` | `KONA_NODE_P2P_NO_DISCOVERY` | Disable Discv5 (node discovery) | `false` | +| `--p2p.priv.path ` | `KONA_NODE_P2P_PRIV_PATH` | Path to hex-encoded 32-byte private key for peer ID | - | +| `--p2p.priv.raw ` | `KONA_NODE_P2P_PRIV_RAW` | Hex-encoded 32-byte private key for peer ID | - | +| `--p2p.advertise.ip ` | `KONA_NODE_P2P_ADVERTISE_IP` | IP to advertise to external peers | - | +| `--p2p.advertise.tcp ` | `KONA_NODE_P2P_ADVERTISE_TCP_PORT` | TCP port to advertise | `0` | +| `--p2p.advertise.udp ` | `KONA_NODE_P2P_ADVERTISE_UDP_PORT` | UDP port to advertise | `0` | +| `--p2p.listen.ip ` | `KONA_NODE_P2P_LISTEN_IP` | IP to bind LibP2P/Discv5 to | `0.0.0.0` | +| `--p2p.listen.tcp ` | `KONA_NODE_P2P_LISTEN_TCP_PORT` | TCP port to bind LibP2P to | `9222` | +| `--p2p.listen.udp ` | `KONA_NODE_P2P_LISTEN_UDP_PORT` | UDP port to bind Discv5 to | `9223` | +| `--p2p.peers.lo ` | `KONA_NODE_P2P_PEERS_LO` | Low-tide peer count | `20` | +| `--p2p.peers.hi ` | `KONA_NODE_P2P_PEERS_HI` | High-tide peer count | `30` | +| `--p2p.peers.grace ` | `KONA_NODE_P2P_PEERS_GRACE` | Grace period for new peers | `30` | +| `--p2p.gossip.mesh.d ` | `KONA_NODE_P2P_GOSSIP_MESH_D` | GossipSub mesh target count | `8` | +| `--p2p.gossip.mesh.lo ` | `KONA_NODE_P2P_GOSSIP_MESH_DLO` | GossipSub mesh low watermark | `6` | +| `--p2p.gossip.mesh.dhi ` | `KONA_NODE_P2P_GOSSIP_MESH_DHI` | GossipSub mesh high watermark | `12` | +| `--p2p.gossip.mesh.dlazy ` | `KONA_NODE_P2P_GOSSIP_MESH_DLAZY` | GossipSub gossip target | `6` | +| `--p2p.gossip.mesh.floodpublish` | `KONA_NODE_P2P_GOSSIP_FLOOD_PUBLISH` | Publish to all known peers | `false` | +| `--p2p.scoring ` | `KONA_NODE_P2P_SCORING` | Peer scoring strategy | `light` | +| `--p2p.ban.peers` | `KONA_NODE_P2P_BAN_PEERS` | Enable peer banning | `false` | +| `--p2p.ban.threshold ` | `KONA_NODE_P2P_BAN_THRESHOLD` | Ban threshold | `-100` | +| `--p2p.ban.duration ` | `KONA_NODE_P2P_BAN_DURATION` | Ban duration | `60` | +| `--p2p.discovery.interval ` | `KONA_NODE_P2P_DISCOVERY_INTERVAL` | Peer discovery interval | `5` | +| `--p2p.bootstore ` | `KONA_NODE_P2P_BOOTSTORE` | Directory to store the bootstore | - | +| `--p2p.redial ` | `KONA_NODE_P2P_REDIAL` | Peer redialing threshold | `500` | +| `--p2p.redial.period ` | `KONA_NODE_P2P_REDIAL_PERIOD` | Peer dial period | `60` | +| `--p2p.bootnodes ` | `KONA_NODE_P2P_BOOTNODES` | List of bootnode ENRs | - | +| `--p2p.topic-scoring` | `KONA_NODE_P2P_TOPIC_SCORING` | Enable topic scoring | `false` | +| `--p2p.discovery.randomize ` | `KONA_NODE_P2P_DISCOVERY_RANDOMIZE` | Remove random peers from discovery | - | + +## RPC Arguments + +| Flag | Env | Description | Default | +|------|-----|-------------|---------| +| `--rpc.disabled` | `KONA_NODE_RPC_DISABLED` | Disable the RPC server | `false` | +| `--rpc.no-restart` | `KONA_NODE_RPC_NO_RESTART` | Prevent RPC server from restarting | `false` | +| `--rpc.addr ` | `KONA_NODE_RPC_ADDR` | RPC listening address | `0.0.0.0` | +| `--port ` | `KONA_NODE_RPC_PORT` | RPC listening port | `9545` | +| `--rpc.enable-admin` | `KONA_NODE_RPC_ENABLE_ADMIN` | Enable the admin API | `false` | +| `--rpc.admin-state ` | `KONA_NODE_RPC_ADMIN_STATE` | File path for admin state persistence | - | +| `--rpc.ws-enabled` | `KONA_NODE_RPC_WS_ENABLED` | Enable websocket RPC server | `false` | + +## Sequencer Arguments + +| Flag | Env | Description | Default | +|------|-----|-------------|---------| +| `--sequencer.stopped` | `KONA_NODE_SEQUENCER_STOPPED` | Start sequencer in stopped state | `false` | +| `--sequencer.max-safe-lag ` | `KONA_NODE_SEQUENCER_MAX_SAFE_LAG` | Max L2 safe/unsafe lag | `0` | +| `--sequencer.l1-confs ` | `KONA_NODE_SEQUENCER_L1_CONFS` | L1 block confirmations for sequencer | `4` | +| `--sequencer.recover` | `KONA_NODE_SEQUENCER_RECOVER` | Strictly prepare next L1 origin and create empty L2 blocks | `false` | +| `--conductor.enabled` | `KONA_NODE_CONDUCTOR_ENABLED` | Enable the conductor service | `false` | +| `--conductor.rpc ` | `KONA_NODE_CONDUCTOR_RPC` | Conductor service RPC endpoint | `127.0.0.1:8547` | +| `--conductor.rpc.timeout ` | `KONA_NODE_CONDUCTOR_RPC_TIMEOUT` | Conductor service RPC timeout | `1` | + +## Supervisor Arguments + +| Flag | Env | Description | Default | +|------|-----|-------------|---------| +| `--supervisor.rpc-enabled` | `KONA_NODE_SUPERVISOR_RPC_ENABLED` | Enable Supervisor Websocket | `false` | +| `--supervisor.ip.address ` | `KONA_NODE_SUPERVISOR_IP` | IP to bind Supervisor Websocket RPC server | `0.0.0.0` | +| `--supervisor.port ` | `KONA_NODE_SUPERVISOR_PORT` | TCP port for supervisor RPC | `9333` | +| `--supervisor.jwt.secret ` | `KONA_NODE_SUPERVISOR_JWT_SECRET` | JWT secret for supervisor websocket authentication | - | +| `--supervisor.jwt.secret.file ` | `KONA_NODE_SUPERVISOR_JWT_SECRET_FILE` | Path to file containing JWT secret | - | + +## RPC Trust Configuration + +The `--l1-trust-rpc` and `--l2-trust-rpc` flags control whether Kona performs additional verification on RPC responses to protect against malicious or faulty RPC providers. + +### Trust Modes + +**Default Behavior (trust enabled, `true`):** +- No additional block hash verification is performed +- Optimized for performance +- Suitable for local nodes and trusted infrastructure +- Assumes the RPC provider is reliable and honest + +**Verification Mode (trust disabled, `false`):** +- All fetched blocks have their hashes verified against the requested hashes +- Protects against malicious RPC providers returning incorrect blocks +- Recommended for public or third-party RPC endpoints +- Small performance overhead due to hash verification + +### Examples + +**Using trusted local RPCs (default):** +```bash +kona-node node \ + --l1-eth-rpc http://localhost:8545 \ + --l2-engine-rpc http://localhost:8551 \ + # trust-rpc defaults to true, no need to specify +``` + +**Using untrusted public RPCs:** +```bash +kona-node node \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l2-engine-rpc https://public-l2-rpc.com \ + --l2-trust-rpc false +``` + +**Mixed trust configuration:** +```bash +kona-node node \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l2-engine-rpc http://localhost:8551 \ + # L2 trust-rpc defaults to true for local engine +``` + +### Security Recommendations + +1. **Local Infrastructure**: Keep the default `true` setting for RPCs you control +2. **Public RPCs**: Always set `--trust-rpc false` when using third-party endpoints +3. **Shared Infrastructure**: Consider setting `--trust-rpc false` as a precaution +4. **Performance Testing**: The verification overhead is minimal but can be measured in high-throughput scenarios + diff --git a/kona/docs/docs/pages/node/design/derivation.mdx b/kona/docs/docs/pages/node/design/derivation.mdx new file mode 100644 index 0000000000000..14e65a3b16457 --- /dev/null +++ b/kona/docs/docs/pages/node/design/derivation.mdx @@ -0,0 +1,234 @@ +# Derivation in Kona Node + +The derivation system in kona-node is responsible for transforming L1 data into L2 payload attributes that can be executed to produce the canonical L2 blocks. This document covers how the [`kona-derive`][kd] crate is integrated and used within the kona-node architecture. + +## Overview + +The derivation subsystem in kona-node is built around the **DerivationActor**, which manages the derivation pipeline lifecycle and coordinates with other node components. The actor uses the trait-abstracted [`kona-derive`][kd] pipeline to continuously process L1 data and produce L2 payload attributes. + +### Key Components + +- **DerivationActor**: The main actor responsible for running the derivation pipeline +- **OnlinePipeline**: A concrete implementation of the derivation pipeline using online providers +- **PipelineBuilder**: Trait for constructing derivation pipelines with different configurations +- **DerivationState**: Manages pipeline state and stepping logic +- **Signal System**: Handles pipeline resets, hardfork activations, and error conditions + +## Architecture + +### DerivationActor + +The `DerivationActor` is a [`NodeActor`][na] that runs as part of the node service. It receives messages from other actors and steps the derivation pipeline forward to produce new payload attributes. + +```rust +pub struct DerivationActor +where + B: PipelineBuilder, +{ + /// The state for the derivation actor. + state: B, + /// Receiver for L1 head update notifications. + l1_head_updates: watch::Receiver>, + /// Receiver for L2 safe head update notifications. + engine_l2_safe_head: watch::Receiver, + /// Receiver for engine sync completion signal. + el_sync_complete_rx: oneshot::Receiver<()>, + /// Receiver for pipeline signals. + derivation_signal_rx: mpsc::Receiver, +} +``` + +The actor coordinates with several other node components: +- **Engine Actor**: Receives payload attributes for execution and sends safe head updates +- **P2P Actors**: Receives L1 head updates from the network +- **RPC Actors**: May trigger pipeline resets or provide status information + +### Pipeline Construction + +The derivation pipeline is constructed using the `DerivationBuilder` which implements the `PipelineBuilder` trait: + +```rust +#[derive(Debug)] +pub struct DerivationBuilder { + /// The L1 provider. + pub l1_provider: RootProvider, + /// The L1 beacon client. + pub l1_beacon: OnlineBeaconClient, + /// The L2 provider. + pub l2_provider: RootProvider, + /// The rollup config. + pub rollup_config: Arc, + /// The interop mode. + pub interop_mode: InteropMode, +} +``` + +The builder creates an `OnlinePipeline` which can operate in two modes: + +1. **Polled Mode**: Uses `PollingTraversal` for L1 block traversal +2. **Indexed Mode**: Uses `IndexedTraversal` for more efficient L1 block handling + +```rust +let pipeline = match self.interop_mode { + InteropMode::Polled => OnlinePipeline::new_polled( + self.rollup_config.clone(), + OnlineBlobProvider::init(self.l1_beacon.clone()).await, + l1_derivation_provider, + l2_derivation_provider, + ), + InteropMode::Indexed => OnlinePipeline::new_indexed( + self.rollup_config.clone(), + OnlineBlobProvider::init(self.l1_beacon.clone()).await, + l1_derivation_provider, + l2_derivation_provider, + ), +}; +``` + +### Provider Configuration + +The node uses caching providers to optimize performance: + +- **AlloyChainProvider**: Provides L1 blockchain data with configurable cache size +- **AlloyL2ChainProvider**: Provides L2 blockchain data and system configuration +- **OnlineBlobProvider**: Retrieves blob data from the beacon chain for post-4844 transactions + +The cache size is set to 1024 entries by default: + +```rust +const DERIVATION_PROVIDER_CACHE_SIZE: usize = 1024; +``` + +## Pipeline Operation + +### Main Processing Loop + +The derivation actor runs a continuous loop that handles various events: + +1. **Shutdown signals**: Graceful shutdown when cancellation token is triggered +2. **L1 head updates**: Triggers derivation when new L1 blocks are available +3. **Safe head updates**: Triggers derivation when the L2 safe head advances +4. **Pipeline signals**: Handles resets, hardfork activations, and channel flushes + +### Stepping Logic + +The core derivation logic is implemented in `produce_next_attributes()`: + +```rust +async fn produce_next_attributes( + &mut self, + engine_l2_safe_head: &watch::Receiver, + reset_request_tx: &mpsc::Sender<()>, +) -> Result +``` + +This method continuously steps the pipeline until payload attributes are produced: + +1. **Step the pipeline** with the current L2 safe head +2. **Handle step results**: + - `PreparedAttributes`: Attributes are ready to be consumed + - `AdvancedOrigin`: Pipeline advanced to next L1 block + - `OriginAdvanceErr`/`StepFailed`: Handle various error conditions +3. **Return attributes** when available + +### Error Handling + +The derivation actor handles three categories of pipeline errors: + +#### Temporary Errors +- `PipelineError::NotEnoughData`: Continue stepping, more data may become available +- `PipelineError::Eof`: Yield and wait for more L1 data + +#### Reset Errors +- `ResetError::HoloceneActivation`: Send `ActivationSignal` to handle hardfork +- `ResetError::ReorgDetected`: Send reset request to engine (if not in interop mode) +- Other reset errors: Wait for external signal before continuing + +#### Critical Errors +- Unrecoverable errors that terminate the derivation process +- Increment metrics counter and propagate error up + +### Signal Handling + +The pipeline supports several signal types for coordination: + +- **ResetSignal**: Resets pipeline state with new L1 origin and system config +- **ActivationSignal**: Handles hardfork activations (e.g., Holocene) +- **FlushChannel**: Invalidates current channel data for deposit-only blocks + +Signals are sent from the engine actor when specific conditions are detected during payload execution. + +## Configuration + +### Rollup Configuration + +The derivation pipeline requires a [`RollupConfig`][rc] that defines: +- Chain parameters (chain ID, block time, etc.) +- Hardfork activation heights +- System configuration addresses +- Batch and channel parameters + +### Runtime Configuration + +Runtime configuration includes: +- Provider cache sizes +- Polling intervals for L1 data +- Interop mode selection +- Metrics collection settings + +## Integration Patterns + +### With Engine Actor + +The derivation actor produces `OpAttributesWithParent` that are sent to the engine actor for execution: + +```rust +// Send payload attributes for execution +derived_attributes_tx + .send(payload_attrs) + .await + .map_err(|e| DerivationError::Sender(Box::new(e)))?; +``` + +The engine actor executes these attributes and updates the L2 safe head, which triggers the next derivation cycle. + +### With P2P Layer + +The derivation actor receives L1 head updates from the P2P layer, which indicate when new L1 data is available for processing: + +```rust +Some(msg) = self.l1_head_updates.changed() => { + if let Err(e) = state.process( + InboundDerivationMessage::L1HeadUpdated, + // ... other parameters + ).await { + // Handle derivation error + } +} +``` + +### With RPC Layer + +The RPC layer can query derivation status and potentially trigger pipeline operations through the standard node RPC interface. + +## Metrics and Observability + +The derivation actor exposes several metrics for monitoring: + +- `DERIVATION_L1_ORIGIN`: Current L1 origin block number +- `DERIVATION_CRITICAL_ERROR`: Count of critical derivation errors +- `L1_REORG_COUNT`: Count of detected L1 reorganizations + +These metrics help operators monitor the health and progress of the derivation process. + +## Related Documentation + +For more details on the underlying derivation pipeline implementation, see: +- [Derivation Pipeline Introduction](/sdk/protocol/derive/intro) +- [Custom Providers](/sdk/protocol/derive/providers) +- [Stage Swapping](/sdk/protocol/derive/stages) +- [Pipeline Signaling](/sdk/protocol/derive/signaling) + +[kd]: https://crates.io/crates/kona-derive +[na]: /node/design/intro#node-actors +[rc]: /sdk/protocol/genesis/rollup-config diff --git a/kona/docs/docs/pages/node/design/engine.mdx b/kona/docs/docs/pages/node/design/engine.mdx new file mode 100644 index 0000000000000..0c0770627aa06 --- /dev/null +++ b/kona/docs/docs/pages/node/design/engine.mdx @@ -0,0 +1,300 @@ +# Execution Engine + +The `kona-engine` crate provides a modular execution engine implementation for the OP Stack rollup node. It serves as the bridge between the rollup protocol and the execution layer (EL), managing Engine API interactions through a sophisticated task queue system. + +## Architecture Overview + +The execution engine is built around several key components: + +- **Engine Task Queue**: A priority-ordered queue that manages Engine API operations +- **Trait Abstractions**: Extensible interfaces for tasks, errors, and state management +- **Engine Client**: HTTP client for communicating with the execution layer +- **Actor Integration**: Service layer integration through the `EngineActor` + +## Core Trait Abstractions + +### EngineTaskExt + +The `EngineTaskExt` trait defines the interface for all engine tasks: + +```rust +#[async_trait] +pub trait EngineTaskExt { + type Output; + type Error: EngineTaskError; + + async fn execute(&self, state: &mut EngineState) -> Result; +} +``` + +This trait enables: +- **Atomic operations** over the `EngineState` +- **Extensible task implementation** for custom operations +- **Async execution** with proper error handling + +### EngineTaskError + +The `EngineTaskError` trait provides sophisticated error handling with severity levels: + +```rust +pub trait EngineTaskError { + fn severity(&self) -> EngineTaskErrorSeverity; +} + +pub enum EngineTaskErrorSeverity { + Temporary, // Retry the task + Critical, // Propagate to engine actor + Reset, // Request derivation reset + Flush, // Request derivation flush +} +``` + +This allows tasks to signal different recovery strategies based on the error type. + +## Task Queue System + +The engine uses a priority-based task queue where tasks are ordered according to OP Stack synchronization requirements: + +### Task Priority (Highest to Lowest) + +1. **ForkchoiceUpdate** - Synchronizes forkchoice state +2. **Build** - Builds new blocks (sequencer mode) +3. **Insert** - Inserts unsafe blocks from gossip +4. **Consolidate** - Advances safe chain via derivation +5. **Finalize** - Finalizes L2 blocks + +### Task Types + +#### SynchronizeTask + +Updates the execution layer's forkchoice state: + +```rust +pub struct SynchronizeTask { + pub client: Arc, + pub rollup: Arc, + pub envelope: Option, + pub state_update: EngineSyncStateUpdate, +} +``` + +Handles: +- Forkchoice synchronization without payload attributes +- Payload building initiation with attributes +- EL sync status management + +#### BuildTask + +Builds new blocks in sequencer mode: + +```rust +pub struct BuildTask { + pub engine: Arc, + pub cfg: Arc, + pub attributes: OpAttributesWithParent, + pub is_attributes_derived: bool, + pub payload_tx: Option>, +} +``` + +Handles: +- Payload building with `engine_forkchoiceUpdated` +- Payload retrieval with version-specific `engine_getPayload` calls +- Payload insertion and canonicalization + +#### InsertTask + +Inserts unsafe blocks received from gossip: + +```rust +pub struct InsertTask { + pub client: Arc, + pub rollup: Arc, + pub envelope: OpExecutionPayloadEnvelope, +} +``` + +#### ConsolidateTask + +Advances the safe chain through derivation: + +```rust +pub struct ConsolidateTask { + pub client: Arc, + pub rollup: Arc, + pub attributes: OpAttributesWithParent, +} +``` + +#### FinalizeTask + +Finalizes L2 blocks: + +```rust +pub struct FinalizeTask { + pub client: Arc, + pub rollup: Arc, + pub l2_block: L2BlockInfo, +} +``` + +## Engine State Management + +The `EngineState` tracks the current state of the execution engine: + +```rust +pub struct EngineState { + pub current: L2BlockInfo, + pub finalized: L2BlockInfo, + pub safe: L2BlockInfo, + pub sync_state: EngineSyncState, + pub el_sync_finished: bool, + // ... additional fields +} +``` + +State updates are communicated through watch channels, enabling reactive programming patterns across the system. + +## Integration with kona-node + +The `kona-node` service layer integrates the engine through the `EngineActor`: + +### Actor Pattern + +The `EngineActor` implements the `NodeActor` trait: + +```rust +#[async_trait] +pub trait NodeActor: Send + 'static { + type Error: std::fmt::Debug; + type OutboundData: CancellableContext; + type InboundData: Sized; + type Builder; + + fn build(builder: Self::Builder) -> (Self::InboundData, Self); + async fn start(self, inbound_context: Self::OutboundData) -> Result<(), Self::Error>; +} +``` + +### Communication Channels + +The `EngineActor` receives input through multiple channels: + +- **attributes_rx**: Payload attributes from derivation +- **unsafe_block_rx**: Unsafe blocks from gossip +- **reset_request_rx**: Reset requests +- **inbound_queries**: Engine state queries +- **runtime_config_rx**: Runtime configuration updates +- **build_request_rx**: Block building requests (sequencer mode only) + +### Engine Queries + +The engine supports queries for: + +```rust +pub enum EngineQueries { + Config(Sender), + State(Sender), + OutputAtBlock { block: BlockNumberOrTag, sender: Sender<(L2BlockInfo, OutputRoot, EngineState)> }, + StateReceiver(Sender>), +} +``` + +## Usage Patterns + +### Basic Engine Setup + +```rust +// Create engine client +let client = EngineClient::new_http( + engine_url, + l2_rpc_url, + l1_rpc_url, + rollup_config, + jwt_secret, +); + +// Initialize engine state +let state = EngineState::default(); +let (state_sender, state_receiver) = watch::channel(state); + +// Create engine with task queue +let engine = Engine::new(state, state_sender); +``` + +### Adding Tasks + +```rust +// Add a forkchoice update task +let task = EngineTask::ForkchoiceUpdate(SynchronizeTask::new( + client.clone(), + rollup_config.clone(), + state_update, + None, // No payload attributes +)); + +engine.add_task(task); +``` + +### Draining the Queue + +```rust +// Process all pending tasks +match engine.drain().await { + Ok(()) => info!("Tasks completed successfully"), + Err(e) => match e.severity() { + EngineTaskErrorSeverity::Reset => { + // Request derivation reset + }, + EngineTaskErrorSeverity::Critical => { + // Handle critical error + }, + _ => { + // Handle other error types + } + } +} +``` + +## Error Handling and Recovery + +The engine provides robust error handling through: + +### Severity-Based Recovery + +- **Temporary errors**: Automatically retried +- **Critical errors**: Propagated to the actor +- **Reset errors**: Trigger derivation pipeline reset +- **Flush errors**: Trigger derivation pipeline flush + +### State Consistency + +Tasks operate atomically on the `EngineState`, ensuring consistency even during error conditions. + +## Version Support + +The engine automatically selects appropriate Engine API versions based on hardfork activation: + +- **Pre-Ecotone**: Uses `engine_newPayloadV2` and `engine_getPayloadV2` +- **Post-Ecotone**: Uses `engine_newPayloadV3` and `engine_getPayloadV3` +- **Post-Isthmus**: Uses `engine_newPayloadV4` and `engine_getPayloadV4` + +## Metrics and Observability + +When the `metrics` feature is enabled, the engine provides comprehensive metrics for: + +- Task execution times +- Error rates by task type +- Engine state transitions +- API call latencies + +## Extensibility + +The trait-based architecture allows for: + +- **Custom task implementations** via `EngineTaskExt` +- **Custom error handling** via `EngineTaskError` +- **Custom state management** extensions +- **Testing and mocking** support + +This modular design ensures the engine can adapt to future OP Stack protocol changes while maintaining backward compatibility. diff --git a/kona/docs/docs/pages/node/design/intro.mdx b/kona/docs/docs/pages/node/design/intro.mdx new file mode 100644 index 0000000000000..d549fd0ff25ae --- /dev/null +++ b/kona/docs/docs/pages/node/design/intro.mdx @@ -0,0 +1,78 @@ +# Node Design Overview + +The entry-point for the `kona-node` is the [`RollupNodeService`][trait] +trait which encapsulates the core wiring for the node. The default +implementation of the trait [`start` method][start] handles connecting +all the different components of the node, running each in a spawned +thread. As such, each node component is considered an actor. + +The [`RollupNodeService`][trait] abstracts individual actors through +the [`NodeActor` trait][actor]. With the `NodeActor` trait, the +`RollupNodeService` builds the actor and then starts it. + +Kona provides implementations for all `NodeActor`s required +to run a `RollupNodeService`. Actors are defined in the +[actors][actors] module of the `kona-node-service` crate. + +The `kona-node` is an implementation of the `RollupNodeService` +that lives in the [standard][standard] module. + + +### Actors + +The architecture of `kona-node` is a web of actors that share +state through message passing, using channels, rather than using +shared memory. + +The [`RollupNodeService`][trait] defines the set of required +actors using associated types. These are subject to change, +but are currently defined as follows. + +- **Derivation Actor**: Orchestrates the derivation pipeline, + deriving L2 payload attributes from l1 blocks. Payload + attributes prepared this way are forwarded to the Engine + Actor to be executed. The [derivation][derivation] docs + dive deeper into how the derivation actor works. +- **Engine Actor**: Brokers the connection to the execution + layer client (or "execution engine"). The engine actor + turns messages from other actors into engine "tasks" + that are executed in a round-robin against the EL client. + The [engine][engine] docs expand on this. +- **Network Actor**: Manages the P2P Network for the rollup + node. The P2P stack consists of `discv5` peer discovery + and block gossip through libp2p. Visit the [network][p2p] + docs for more detail. +- **Supervisor Actor (beta)**: The supervisor actor is an + interop feature that allows the `kona-node` to be + "managed" (or "indexed") by the supervisor - a new + component in the OP Stack. A detailed overview of + interop and the supervisor's role is provided in the + [supervisor][supervisor] docs. +- **Runtime Actor**: Loads runtime values from the contracts + on the L1 chain for the OP Stack. This is a very + light-weight actor described in [runtime][runtime] docs. +- **Sequencer Actor**: The sequencer actor extends the + `kona-node` to be run as a sequencer. Sequencing is + periphery to the basic rollup node operation. See + the [sequencer][sequencer]. +- **RPC Actor**: The RPC actor spins up and serves an + RPC server that exposes the rpc methods required by + the [OP Stack Specs][specs]. + + + + +[p2p]: ./p2p +[engine]: ./engine +[derivation]: ./derivation +[supervisor]: #TODO +[runtime]: #TODO +[sequencer]: ./sequencer + +[specs]: https://specs.optimism.io/protocol/rollup-node.html + +[standard]: https://github.com/op-rs/kona/blob/main/crates/node/service/src/service/standard/node.rs +[actors]: https://github.com/op-rs/kona/tree/main/crates/node/service/src/actors +[actor]: https://github.com/op-rs/kona/blob/main/crates/node/service/src/actors/traits.rs#L19 +[start]: https://github.com/op-rs/kona/blob/main/crates/node/service/src/service/core.rs#L161-L162 +[trait]: https://github.com/op-rs/kona/blob/main/crates/node/service/src/service/core.rs#L56 diff --git a/kona/docs/docs/pages/node/design/p2p.mdx b/kona/docs/docs/pages/node/design/p2p.mdx new file mode 100644 index 0000000000000..52d3ed19383f2 --- /dev/null +++ b/kona/docs/docs/pages/node/design/p2p.mdx @@ -0,0 +1,177 @@ +import { Callout } from 'vocs/components' + +# P2P Networking + + + +Partly adapted from the [OP Stack P2P Specs][p2p-specs]. +Please reference the specs for up-to-date OP Stack requirements. + + + +The OP Stack uses P2P networking on the consensus layer to share +the sequencer's view of the L2 chain with other nodes on the +network. L2 blocks shared via P2P are considered "unsafe", and +will be reorganized to match the canonical chain, prioritizing L1. + +This means that behavior on the P2P layer does not affect the +rollup security. As such, rules around banning and scoring peers +based on their P2P gossip is policy - it is up to the user to +ultimately choose a configuration best for them. + +To understand how the P2P is hooked up to the `kona-node`, jump +to the [P2P Actor](#-p2p-actor) section below. Otherwise, read +on to learn more about the details of the P2P stack. + + +### Topography + +The P2P stack topography consists of the following. + +- Discovery of peers via [discv5][discv5]. +- Gossip and peer connection management through [libp2p][libp2p]. +- Publishing and validation of gossip by the node. + +In the `kona-node`, these layers are split up into modular +components either as modules or distinct crates. + + +#### Discovery + +Kona's discovery layer is encapsulated in a "driver" called +the [`Discv5Driver`][driver]. When started, the driver spawns a +new thread to handle [`discv5::Discv5`][discv5-service] events +from its event stream as well as metrics requests from the +`kona-node`. A "handler" is returned by the consumed +[`Discv5Driver`][driver] which allows other components of +the `kona-node` to communicate through channels to the +spawned [`discv5::Discv5`][discv5-service] service. + +When peers are discovered by kona's discovery service, their +"ENR"s need to be validated to ensure those peers are +participating in the right network gossip. Ethereum Node +Records (ENRs) and how they are validated is discussed in a +[later section](#-node-identification). After their ENRs are +validated, they are forwarded to the consumer (in kona's case +libp2p) which establishes and manages the connection to the +node. + +There are also a few more notable functions of Kona's discovery +driver. + +- Every X seconds it attempts to discover random ENRs. This is + configurable using `Discv5Builder::with_interval` +- Every Y seconds it evicts a random ENR from the discovery + table to keep peer discovery fresh. This is configurable + using `Discv5Builder::with_discovery_randomize`. +- Every Z seconds it stores its ENR table at a configurable + location so if the service is restarted, it doesn't need to + rediscover peers, it can just use the stored peers. The + interval is configurable using + `Discv5Builder::with_store_interval`. + + +#### Gossip + +L2 blocks on the OP Stack not otherwise derived from L1 are +shared over TCP in the P2P network of nodes. Unsafe L2 blocks +shared this way originate from the sequencer. + +In the `kona-node`, L2 block gossip is handled through the +[libp2p Swarm][swarm]. The `GossipDriver` is the component +in the `kona-node` that manages the libp2p swarm, including +any interfacing with the swarm like dialing peers, publishing +payloads (L2 blocks), handling events from the swarm, and more. + + + +The libp2p swarm must be polled via Swarm as Stream +in order to make progress. +Through kona's `GossipDriver`, this can be done by looping +over and consuming events from `GossipDriver::next`. + + + +The `GossipDriver` provides the methods to handle events +from the [libp2p Swarm][swarm]. Events should be consumed +this way in order to use the connection gater as well as peer +store and fields on the `GossipDriver`. + +The [libp2p Swarm][swarm] listens on a specified +[`Multiaddr`][multiaddr]. + + +#### L2 Block Publishing + +As mentioned in [the previous section](#-gossip), L2 blocks +are published as payloads through the [libp2p Swarm][swarm], +which is done using the `GossipDriver`. The actual payload +type that is published is an [`OpNetworkPayloadEnvelope`][env], +which is well documented in the [OP Stack P2P Specs][p2p-specs]. + +L2 blocks published through the `GossipDriver` are published on +a "topic". The topic is used by the gossipsub protocol to publish +the message on that given topic, allowing peers to choose which +topics they wish to subscribe to. + + +#### L2 Block Validation + +L2 blocks are validated in kona through a trait-abstracted +"block handler". Since messages in the libp2p mesh network are +snappy compressed, they need to be decompressed and then decoded +for the correct [block topic][block-topic] those messages are +published on. + +Only once the [`OpNetworkPayloadEnvelope`][env] is successfully +decoded for the corresponding block topic, is the block validated. + + + +Block validity in kona follows the [OP Stack block validation specs][validation]. + + + +As of writing these docs, block validation follows a few rules. + +- The timestamp is between 60 seconds in the past and at most 5 seconds in the future. +- The block hash is valid. This is checked by transforming the payload into a block + and then hashing the block header to produce the payload hash. +- The contents of the payload envelope are correct for its version. Since different + versions introduce new contents to the payload from hardforks, the + forwards-compatible payload envelope cannot have fields with content that don't exist + for previous versions. +- The block signature is valid. + + +### Node Identification + +TODO + + +### P2P Actor + +TODO + + + + + +[validation]: https://specs.optimism.io/protocol/rollup-node-p2p.html#block-validation + +[block-topic]: https://specs.optimism.io/protocol/rollup-node-p2p.html#gossip-topics + +[multiaddr]: https://docs.rs/libp2p/0.56.0/libp2p/struct.Multiaddr.html + +[env]: https://docs.rs/op-alloy-rpc-types-engine/latest/op_alloy_rpc_types_engine/struct.OpNetworkPayloadEnvelope.html + +[swarm]: https://docs.rs/libp2p/latest/libp2p/struct.Swarm.html + +[discv5-service]: https://docs.rs/discv5/latest/discv5/struct.Discv5.html + +[driver]: https://docs.rs/kona-p2p/latest/kona_p2p/struct.Discv5Driver.html + +[discv5]: https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md +[libp2p]: https://libp2p.io/ + +[p2p-specs]: https://specs.optimism.io/protocol/rollup-node-p2p.html diff --git a/kona/docs/docs/pages/node/design/sequencer.mdx b/kona/docs/docs/pages/node/design/sequencer.mdx new file mode 100644 index 0000000000000..fb2608476d124 --- /dev/null +++ b/kona/docs/docs/pages/node/design/sequencer.mdx @@ -0,0 +1,194 @@ +# Sequencer Mode + +The Kona node can operate in **sequencer mode** to build and produce new L2 blocks. In this mode, the node acts as the sequencer for an OP Stack rollup, building L2 blocks on top of the current unsafe head and extending the L2 chain. + +:::info +Sequencer mode is an advanced configuration primarily used by rollup operators. Most users will run nodes in the default validator mode. +::: + +## Overview + +When running in sequencer mode, the Kona node: + +- **Builds L2 blocks** by collecting transactions from the mempool and constructing new blocks +- **Selects L1 origins** for new L2 blocks based on finalized L1 data +- **Manages block production timing** and ensures proper sequencing constraints +- **Integrates with conductor services** for leader election in multi-sequencer setups +- **Handles recovery scenarios** when the sequencer needs to catch up with L1 + +The sequencer uses the same core derivation pipeline as validator nodes but operates in reverse - instead of deriving L2 blocks from L1 data, it produces L2 blocks that will later be derivable from L1. + +## Trait Abstractions + +### Core Interfaces + +The sequencer functionality is built around several key trait abstractions: + +#### `RollupNodeService` +The main service trait that defines the node's operational mode and actor types: + +```rust +pub trait RollupNodeService { + type SequencerActor: NodeActor< + Error: Display, + OutboundData = SequencerContext, + Builder: AttributesBuilderConfig, + InboundData = SequencerInboundData, + >; + + fn mode(&self) -> NodeMode; + // ... other methods +} +``` + +#### `AttributesBuilderConfig` +Configures how L2 block attributes are constructed: + +```rust +pub trait AttributesBuilderConfig { + type AB: AttributesBuilder; + fn build(self) -> Self::AB; +} +``` + +#### `SequencerActor` +The core actor responsible for block production: + +- Builds L2 blocks using the `AttributesBuilder` +- Manages timing and L1 origin selection +- Handles admin RPC commands for sequencer control +- Coordinates with conductor services for leader election + +## Programmatic Configuration + +### Using the RollupNodeBuilder + +To configure a Kona node programmatically for sequencer mode: + +```rust +use kona_node_service::{NodeMode, RollupNode, SequencerConfig}; +use url::Url; + +// Configure sequencer settings +let sequencer_config = SequencerConfig { + sequencer_stopped: false, // Start sequencer immediately + sequencer_recovery_mode: false, // Normal operation mode + conductor_rpc_url: Some( // Optional conductor integration + Url::parse("http://conductor:8080").unwrap() + ), +}; + +// Build and start the sequencer node +let node = RollupNode::builder(rollup_config) + .with_mode(NodeMode::Sequencer) // Enable sequencer mode + .with_sequencer_config(sequencer_config) // Apply sequencer settings + .with_l1_provider_rpc_url(l1_rpc_url) // L1 data source + .with_l2_engine_rpc_url(l2_engine_url) // L2 execution engine + .with_jwt_secret(jwt_secret) // Engine API authentication + // ... other configuration + .build() + .start() + .await?; +``` + +### Configuration Options + +| Field | Description | Default | +|-------|-------------|---------| +| `sequencer_stopped` | Start sequencer in stopped state | `false` | +| `sequencer_recovery_mode` | Enable recovery mode for catch-up | `false` | +| `conductor_rpc_url` | Conductor service endpoint for leader election | `None` | + +## CLI Usage + +### Basic Sequencer Setup + +To run a Kona node in sequencer mode: + +```bash +kona-node node \ + --mode=Sequencer \ + --l1-eth-rpc=http://l1-node:8545 \ + --l1-beacon=http://l1-beacon:5052 \ + --l2-engine-rpc=http://l2-execution:8551 \ + --l2.jwt-secret=./jwt.hex \ + --chain=123456 +``` + +### Required Arguments + +:::warning +Required Configuration + +Sequencer mode requires all standard node arguments plus the `--mode=Sequencer` flag. Missing any required argument will prevent the node from starting. +::: + +| Argument | Flag | Environment Variable | Description | +|----------|------|---------------------|-------------| +| **Mode** | `--mode` | `KONA_NODE_MODE` | Must be set to `Sequencer` | +| **L1 RPC** | `--l1-eth-rpc` | `KONA_NODE_L1_ETH_RPC` | L1 execution client RPC URL | +| **L1 Beacon** | `--l1-beacon` | `KONA_NODE_L1_BEACON` | L1 beacon API URL | +| **L2 Engine** | `--l2-engine-rpc` | `KONA_NODE_L2_ENGINE_RPC` | L2 engine API endpoint | +| **JWT Secret** | `--l2.jwt-secret` | `KONA_NODE_L2_ENGINE_AUTH` | Path to JWT secret file | +| **Chain ID** | `--chain` | `KONA_NODE_L2_CHAIN_ID` | L2 chain identifier | + +### Sequencer-Specific Flags + +| Flag | Environment Variable | Default | Description | +|------|---------------------|---------|-------------| +| `--sequencer.stopped` | `KONA_NODE_SEQUENCER_STOPPED` | `false` | Start sequencer in stopped state | +| `--sequencer.max-safe-lag` | `KONA_NODE_SEQUENCER_MAX_SAFE_LAG` | `0` | Max L2 blocks between safe and unsafe heads | +| `--sequencer.l1-confs` | `KONA_NODE_SEQUENCER_L1_CONFS` | `4` | L1 confirmations for origin selection | +| `--sequencer.recover` | `KONA_NODE_SEQUENCER_RECOVER` | `false` | Force recovery mode operation | +| `--conductor.rpc` | `KONA_NODE_CONDUCTOR_RPC` | - | Conductor service RPC endpoint | +| `--conductor.rpc.timeout` | `KONA_NODE_CONDUCTOR_RPC_TIMEOUT` | `1` | Conductor RPC timeout (seconds) | + +### Example Configurations + +#### Basic Sequencer +```bash +kona-node node \ + --mode=Sequencer \ + --l1-eth-rpc=http://localhost:8545 \ + --l1-beacon=http://localhost:5052 \ + --l2-engine-rpc=http://localhost:8551 \ + --chain=42161 +``` + +#### Sequencer with Conductor +```bash +kona-node node \ + --mode=Sequencer \ + --conductor.rpc=http://conductor:8080 \ + --conductor.rpc.timeout=5 \ + --sequencer.l1-confs=6 \ + --l1-eth-rpc=http://l1-node:8545 \ + --l1-beacon=http://l1-beacon:5052 \ + --l2-engine-rpc=http://l2-execution:8551 \ + --chain=123456 +``` + +#### Recovery Mode Sequencer +```bash +kona-node node \ + --mode=Sequencer \ + --sequencer.recover=true \ + --sequencer.max-safe-lag=100 \ + --l1-eth-rpc=http://localhost:8545 \ + --l1-beacon=http://localhost:5052 \ + --l2-engine-rpc=http://localhost:8551 \ + --chain=42161 +``` + +## Key Considerations + +:::tip +Sequencer Operation + +- **L1 Confirmations**: The `--sequencer.l1-confs` setting determines how many L1 blocks the sequencer waits before using an L1 block as an origin. Higher values provide more safety but increase latency. +- **Recovery Mode**: Use `--sequencer.recover=true` when the sequencer needs to catch up after being offline. +- **Conductor Integration**: For multi-sequencer deployments, configure the conductor service for proper leader election. +::: + + +Running a sequencer in production requires careful consideration of infrastructure, monitoring, and failover procedures. Ensure proper JWT secret management and secure network configuration. diff --git a/kona/docs/docs/pages/node/faq/overview.mdx b/kona/docs/docs/pages/node/faq/overview.mdx new file mode 100644 index 0000000000000..0869f331287b8 --- /dev/null +++ b/kona/docs/docs/pages/node/faq/overview.mdx @@ -0,0 +1,6 @@ +# FAQ + +1. [Ports](/node/faq/ports) - Detailed account of ports used by the `kona-node` for P2P communication, JSON-RPC APIs, and the Engine API for execution layer communication. + +2. [Profiling](/node/faq/profiling) - Profile performance of the Kona node including CPU profiling and memory analysis. + diff --git a/kona/docs/docs/pages/node/faq/ports.mdx b/kona/docs/docs/pages/node/faq/ports.mdx new file mode 100644 index 0000000000000..31c0637577441 --- /dev/null +++ b/kona/docs/docs/pages/node/faq/ports.mdx @@ -0,0 +1,10 @@ +# Node Ports + +| Service | Default Port | Flag/Env | +|-----------------|--------------|------------------------------------------| +| RPC HTTP | 9545 | `--port` / `KONA_NODE_RPC_PORT` | +| RPC WebSocket | 9545 | (same as HTTP, enabled with `--rpc.ws-enabled`) | +| P2P TCP | 9222 | `--p2p.listen.tcp` / `KONA_NODE_P2P_LISTEN_TCP_PORT` | +| P2P UDP | 9223 | `--p2p.listen.udp` / `KONA_NODE_P2P_LISTEN_UDP_PORT` | +| Supervisor RPC | 9333 | `--supervisor.port` / `KONA_NODE_SEQUENCER_PORT` | +| Conductor RPC | 8547 | `--conductor.rpc` / `KONA_NODE_CONDUCTOR_RPC` | diff --git a/kona/docs/docs/pages/node/faq/profiling.mdx b/kona/docs/docs/pages/node/faq/profiling.mdx new file mode 100644 index 0000000000000..ab92ba0690eb8 --- /dev/null +++ b/kona/docs/docs/pages/node/faq/profiling.mdx @@ -0,0 +1,3 @@ +# Profiling the Node + +Coming soon... diff --git a/kona/docs/docs/pages/node/install/binaries.mdx b/kona/docs/docs/pages/node/install/binaries.mdx new file mode 100644 index 0000000000000..66995da2760ff --- /dev/null +++ b/kona/docs/docs/pages/node/install/binaries.mdx @@ -0,0 +1,3 @@ +# Kona Binaries + +Download the latest pre-built binaries from the [GitHub releases page](https://github.com/op-rs/kona/releases). diff --git a/kona/docs/docs/pages/node/install/docker.mdx b/kona/docs/docs/pages/node/install/docker.mdx new file mode 100644 index 0000000000000..77867e5429913 --- /dev/null +++ b/kona/docs/docs/pages/node/install/docker.mdx @@ -0,0 +1,64 @@ +import { Callout } from 'vocs/components' + +# Docker + +There are two ways to obtain a Kona Docker image: + +1. [GitHub](#github) +2. [Building it from source](#building-the-docker-image) + +Once you have obtained the Docker image, you can run the node. + +Jump ahead to [Run a Node using Docker page](/node/run/docker). + + +## GitHub + +Kona docker images are published with every release on GitHub Container Registry. + +You can obtain the latest `kona-node` image with: + +```bash +docker pull ghcr.io/op-rs/kona/kona-node +``` + + +Specify a specific version (e.g. v0.1.0) like so. + +```bash +docker pull ghcr.io/op-rs/kona/kona-node:v0.1.0 +``` + + +You can test the image with: + +```bash +docker run --rm ghcr.io/op-rs/kona/kona-node --version +``` + +If you can see the [latest release](https://github.com/op-rs/kona/releases) version, +then you've successfully installed Kona via Docker. + + +## Building the Docker image + +To build the image from source, navigate to the root of the repository and run: + +```bash +just build-local kona-node +``` + + +This will create an image with the tag `kona:local`. To specify a custom +tag, just pass it in after `kona-node` in the command above, like so: + +```bash +just build-local kona-node my-custom-tag +``` + + +The build will likely take several minutes. Once it's built, test it with: + +```bash +docker run kona:local --version +``` diff --git a/kona/docs/docs/pages/node/install/overview.mdx b/kona/docs/docs/pages/node/install/overview.mdx new file mode 100644 index 0000000000000..4e2cb496600ff --- /dev/null +++ b/kona/docs/docs/pages/node/install/overview.mdx @@ -0,0 +1,56 @@ +--- +description: Installation instructions for Kona. +--- + +## Prerequisites + +Before installing Kona, ensure you have the following prerequisites: + +- **Rust toolchain** (MSRV: 1.82) +- **`just`** command runner +- **Docker** (optional, for containerized builds) + +### Installing Rust + +If you don't have Rust installed, you can install it using [rustup](https://rustup.rs/): + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Rustup is an easy way to update the Rust compiler, and works on all platforms. + +:::tip + +- During installation, when prompted, enter `1` for the default installation. +- After Rust installation completes, try running `cargo version` . If it cannot + be found, run `source $HOME/.cargo/env`. After that, running `cargo version` should return the version, for example `cargo 1.68.2`. +- It's generally advisable to append `source $HOME/.cargo/env` to `~/.bashrc`. + +::: + +The Minimum Supported Rust Version (MSRV) of this project is 1.82.0. If you +already have a version of Rust installed, you can check your version by running +`rustc --version`. To update your version of Rust, run rustup update. + + +### Installing Just + +`just` is a command runner that Kona uses for build tasks. Install it with: + +```bash +cargo install just +``` + +## Installation Methods + +There are three ways to obtain Kona: + +- [Docker images](/node/install/docker) +- [Pre-built binaries](/node/install/binaries) +- [Building from source](/node/install/source) + +:::note +If you have Docker installed, we recommend using the [Docker recipe](/node/run/docker) configuration +that will have kona-node, op-reth, Prometheus and Grafana running and syncing with just one command. +::: diff --git a/kona/docs/docs/pages/node/install/source.mdx b/kona/docs/docs/pages/node/install/source.mdx new file mode 100644 index 0000000000000..00d290fd43cdc --- /dev/null +++ b/kona/docs/docs/pages/node/install/source.mdx @@ -0,0 +1,83 @@ +# Building from Source + +:::note + +Building from source requires that the Rust toolchain is installed, as well as the `just` command runner. + +Visit the [Prerequisites](/node/install/overview) for details on installing Rust and `just`. + +::: + +First clone the repository: + +```bash +git clone https://github.com/op-rs/kona.git +cd kona +``` + +Then, install the `kona-node` binary into your PATH directly via: + +```bash +cargo install --locked --path bin/node --bin kona-node +``` + +The binary will now be accessible as `kona-node` via the command line, +and exist under your default .cargo/bin folder. + +Alternatively, you can build yourself with: + +```bash +cargo build --release --bin kona-node +``` + +This will place the reth binary under `./target/release/kona-node`, and +you can copy it to your directory of preference after that. + +## Update Kona + +You can update the `kona-node` to a specific version by running the +commands below. + +`${VERSION}` is the version you wish to build in the format `vX.X.X`. + +```bash +git fetch +git checkout ${VERSION} +cargo build --release --bin kona-node +``` + +## Troubleshooting + +### Command is not found + +Reth will be installed to `CARGO_HOME` or `$HOME/.cargo`. +This directory needs to be on your `PATH` before you can run the +`kona-node` binary. + +See ["Configuring the PATH environment variable"][path] for more information. + +[path]: https://www.rust-lang.org/tools/install + +### Compilation error + +Make sure you are running the latest version of Rust. +If you have installed Rust using rustup, simply run `rustup update`. + +If you can't install the latest version of Rust you can instead compile +using the Minimum Supported Rust Version (MSRV) which is listed under +the `rust-version` key in kona's [Cargo.toml](https://github.com/op-rs/kona/blob/main/Cargo.toml). + +If compilation fails with `(signal: 9, SIGKILL: kill)`, this could mean +your machine ran out of memory during compilation. If you are on Docker, +consider increasing the memory of the container, or use a [pre-built +binary](/node/install/binaries). + +If compilation fails with `error: linking with cc failed: exit code: 1`, +try running `cargo clean`. + + +## Next Steps + +- Read the [Overview](/intro/overview) to understand Kona's architecture +- Check out the [Binaries](/node/run/binary) documentation +- Explore the [Examples](/sdk/examples/intro) diff --git a/kona/docs/docs/pages/node/monitoring.mdx b/kona/docs/docs/pages/node/monitoring.mdx new file mode 100644 index 0000000000000..3abf2aeb47ddf --- /dev/null +++ b/kona/docs/docs/pages/node/monitoring.mdx @@ -0,0 +1,40 @@ +# Monitoring + +The `kona-node` exposes metrics by default. Optionally, metrics +can be disabled with the `--metrics.disabled` cli flag. + +Unless otherwise specified, metrics are exposed on port `9090`. + +To grab a snapshot of the metrics, you can visit `0.0.0.0:9090` +or `curl` the url. + +``` +curl 0.0.0.0:9090 +``` + +The output should be raw text mapping metrics with their values. + +Remember, this is just a snapshot of the metrics at that point +in time. To record and visualize the metrics, we'll use +[Grafana and Prometheus](#-Grafana-and-Prometheus). + + +## Grafana and Prometheus + +Prometheus is a simple service that scrapes metrics at a predefined +interval. Grafana then uses Prometheus as a "Data Source" to +visualize the collected metrics. + +The Reth book provides a great overview to setting up [Prometheus +and Grafana][setup]. Visit the Reth docs to follow along. + +The `kona-node` comes shipped with a default Grafana dashboard +for the `kona-node`. To import the dashboard to grafana, click +the `+` icon > `Import Dashboard` > paste the contents of [kona's +dashboard][dashboard] in the textbox > `Load`. + + + +[setup]: https://reth.rs/run/monitoring#prometheus--grafana + +[dashboard]: https://github.com/op-rs/kona/blob/f86052b5dacec7da46b12441aafab2867069f7e7/docker/recipes/kona-node/grafana/dashboards/overview.json diff --git a/kona/docs/docs/pages/node/requirements.mdx b/kona/docs/docs/pages/node/requirements.mdx new file mode 100644 index 0000000000000..6d797d728e772 --- /dev/null +++ b/kona/docs/docs/pages/node/requirements.mdx @@ -0,0 +1,20 @@ +import { Callout } from 'vocs/components' + +# System Requirements + +`kona-node` is an L2 consensus client, so it stores almost nothing on disk! + + +Anything stored on disk is configurable, and can be disabled. + + +As a rollup node, it always sends L2 blocks over to the execution client +(`op-reth` or `op-geth`) for execution. That way, chain state is entirely +handled by the execution client. In this way, the `kona-node` is incredibly +lightweight and can be run on a wide range of hardware. + +That said, a stable and dependable internet connection is critical for the +peer-to-peer (P2P) part of the node. The `kona-node` relies on P2P communication +to sync the unsafe chain. If the connection is unstable, the node may +struggle to keep up, and could be banned by its peers for being too slow. + diff --git a/kona/docs/docs/pages/node/rpc/admin.mdx b/kona/docs/docs/pages/node/rpc/admin.mdx new file mode 100644 index 0000000000000..992f7d4ac6d16 --- /dev/null +++ b/kona/docs/docs/pages/node/rpc/admin.mdx @@ -0,0 +1,128 @@ +# Admin RPC Methods + +The `admin` api provides methods for controlling and monitoring Kona's consensus node operations. + +## `admin_postUnsafePayload` + +Posts an unsafe payload to the network. + +| Client | Method invocation | +| ------ | ----------------------------------------------------------- | +| RPC | `{"method": "admin_postUnsafePayload", "params": [payload]}` | + +### Parameters + +- `payload` (`OpExecutionPayloadEnvelope`): The execution payload envelope to post + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_postUnsafePayload","params":[{...payload...}]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +## `admin_sequencerActive` + +Returns whether the sequencer is currently active. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "admin_sequencerActive"}` | + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_sequencerActive","params":[]} +{"jsonrpc":"2.0","id":1,"result":true} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). + +## `admin_startSequencer` + +Starts the sequencer. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "admin_startSequencer"}` | + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_startSequencer","params":[]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). + +## `admin_stopSequencer` + +Stops the sequencer and returns the hash of the last processed block. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "admin_stopSequencer"}` | + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_stopSequencer","params":[]} +{"jsonrpc":"2.0","id":1,"result":"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). + +## `admin_conductorEnabled` + +Returns whether the conductor is enabled. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "admin_conductorEnabled"}` | + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_conductorEnabled","params":[]} +{"jsonrpc":"2.0","id":1,"result":false} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). + +## `admin_setRecoverMode` + +Sets the recovery mode for the sequencer. + +| Client | Method invocation | +| ------ | --------------------------------------------------------- | +| RPC | `{"method": "admin_setRecoverMode", "params": [mode]}` | + +### Parameters + +- `mode` (`bool`): Whether to enable recovery mode (true) or disable it (false) + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_setRecoverMode","params":[true]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). + +## `admin_overrideLeader` + +Overrides the leader in the conductor. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "admin_overrideLeader"}` | + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"admin_overrideLeader","params":[]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +**Note**: This method will return a "Method not found" error if the node is running in validator mode (sequencer not enabled). diff --git a/kona/docs/docs/pages/node/rpc/overview.mdx b/kona/docs/docs/pages/node/rpc/overview.mdx new file mode 100644 index 0000000000000..d293249f0946f --- /dev/null +++ b/kona/docs/docs/pages/node/rpc/overview.mdx @@ -0,0 +1,25 @@ +# JSON-RPC + +The `kona-node` supports JSON-RPC for interacting with the node. + +By default, `kona-node` exposes an HTTP JSON-RPC server. A WebSocket JSON-RPC +endpoint is also available and can be enabled with the `--rpc.ws-enabled` flag +or the `KONA_NODE_RPC_WS_ENABLED` environment variable. IPC transport is not +supported. + +### Namespaces + +JSON-RPC methods are grouped into namespaces, which are listed below: + +| Namespace | Description | Sensitive | +| ---------------------------- | ------------------------------------------------------------------------------------------------------ | --------- | +| [`p2p`](/node/rpc/p2p) | The `p2p` API allows you to configure the p2p stack. | Maybe | +| [`rollup`](/node/rpc/rollup) | The `rollup` API provides OP Stack specific rpc methods. | No | +| [`admin`](/node/rpc/admin) | The `admin` API allows you to configure your node. | **Yes** | + + +### Interacting with the RPC + +Kona enables these RPC methods by default. + +You can interact with the RPC using any JSON-RPC client, such as `curl`, `httpie`, or a custom client in your preferred programming language. diff --git a/kona/docs/docs/pages/node/rpc/p2p.mdx b/kona/docs/docs/pages/node/rpc/p2p.mdx new file mode 100644 index 0000000000000..b60bef2683871 --- /dev/null +++ b/kona/docs/docs/pages/node/rpc/p2p.mdx @@ -0,0 +1,329 @@ +# P2P RPC Methods + +The `p2p` api provides methods for interacting with Kona's P2P stack. + +## Peer Information Methods + +### `opp2p_self` + +Returns information about the local node in the form of `PeerInfo`. + +| Client | Method invocation | +| ------ | -------------------------- | +| RPC | `{"method": "opp2p_self"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_self","params":[]} +{"jsonrpc":"2.0","id":1,"result":{"peerID":"16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x","nodeID":"0x311d8222ffc44e9c86f403d57f454bd823e7dc9d3c8e97171ddd862910352f31","userAgent":"kona","protocolVersion":"","ENR":"enr:-Jm4QBAdUpUqrpTj6yQor5mwif6RRmY11dlj-Um3TqKmJiYha4SUNqdJr2eM3pRsFVCwVikYcBk__5JVTwngUeimKxcCgmlkgnY0gmlwhC36_pOHb3BzdGFja4Xc76gFAIlzZWNwMjU2azGhA2WTa6OqvnWbRmoeuhRRu-BTPgP8y4_MY6snTsNW0gHBg3RjcIIj5oN1ZHCCn7U","addresses":["/ip4/127.0.0.1/tcp/9190/p2p/16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x","/ip4/172.18.0.9/tcp/9190/p2p/16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"],"protocols":["/ipfs/id/push/1.0.0","/meshsub/1.1.0","/ipfs/ping/1.0.0","/meshsub/1.2.0","/ipfs/id/1.0.0","/opstack/req/payload_by_number/2151908/0/","/meshsub/1.0.0","/floodsub/1.0.0"],"connectedness":1,"direction":1,"protected":false,"chainID":11155420,"latency":0,"gossipBlocks":true,"scores":{"gossip":{"total":0.0,"blocks":{"timeInMesh":0.0,"firstMessageDeliveries":0.0,"meshMessageDeliveries":0.0,"invalidMessageDeliveries":0.0},"IPColocationFactor":0.0,"behavioralPenalty":0.0},"reqResp":{"validResponses":0.0,"errorResponses":0.0,"rejectedPayloads":0.0}}}} +``` + +### `opp2p_peerCount` + +Returns the count of connected peers for both discovery and gossip networks. + +| Client | Method invocation | +| ------ | ------------------------------- | +| RPC | `{"method": "opp2p_peerCount"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_peerCount","params":[]} +{"jsonrpc":"2.0","id":1,"result":{"connectedDiscovery":15,"connectedGossip":12}} +``` + +### `opp2p_peers` + +Returns information about peers. If `connected` parameter is true, only returns connected peers. + +| Client | Method invocation | +| ------ | ----------------------------------------------------- | +| RPC | `{"method": "opp2p_peers", "params": [connected]}` | + +#### Parameters + +- `connected` (boolean): If true, only returns connected peers + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_peers","params":[true]} +{"jsonrpc":"2.0","id":1,"result":{"totalConnected":2,"peers":{"16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x":{"peerID":"16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x","nodeID":"0x311d8222ffc44e9c86f403d57f454bd823e7dc9d3c8e97171ddd862910352f31","userAgent":"kona","protocolVersion":"","addresses":["/ip4/127.0.0.1/tcp/9190"],"protocols":["/ipfs/ping/1.0.0","/meshsub/1.1.0"],"connectedness":1,"direction":2,"protected":false,"chainID":11155420,"latency":50000000,"gossipBlocks":true,"scores":{"gossip":{"total":1.5,"blocks":{"timeInMesh":100.0,"firstMessageDeliveries":10.0,"meshMessageDeliveries":5.0,"invalidMessageDeliveries":0.0},"IPColocationFactor":0.0,"behavioralPenalty":0.0},"reqResp":{"validResponses":25.0,"errorResponses":1.0,"rejectedPayloads":0.0}}}},"bannedPeers":[],"bannedIPS":[],"bannedSubnets":[]}} +``` + +### `opp2p_peerStats` + +Returns statistical information about peers including connection counts and topic subscriptions. + +| Client | Method invocation | +| ------ | -------------------------------- | +| RPC | `{"method": "opp2p_peerStats"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_peerStats","params":[]} +{"jsonrpc":"2.0","id":1,"result":{"connected":12,"table":50,"blocksTopic":8,"blocksTopicV2":10,"blocksTopicV3":5,"blocksTopicV4":2,"banned":3,"known":75}} +``` + +### `opp2p_discoveryTable` + +Returns the discovery table entries as a list of ENR strings. + +| Client | Method invocation | +| ------ | ------------------------------------- | +| RPC | `{"method": "opp2p_discoveryTable"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_discoveryTable","params":[]} +{"jsonrpc":"2.0","id":1,"result":["enr:-Jm4QBAdUpUqrpTj6yQor5mwif6RRmY11dlj-Um3TqKmJiYha4SUNqdJr2eM3pRsFVCwVikYcBk__5JVTwngUeimKxcCgmlkgnY0gmlwhC36_pOHb3BzdGFja4Xc76gFAIlzZWNwMjU2azGhA2WTa6OqvnWbRmoeuhRRu-BTPgP8y4_MY6snTsNW0gHBg3RjcIIj5oN1ZHCCn7U","enr:-Km4QBqBrKNq7F5L1dSrWW8Y1k8k4V2L2nTsNtGuKPpPwp3L_rBVMaQCQpnc2sBB-c2yV_n4qgM2_2yfcNjVXr4OFgCgmlkgnY0gmlwhH8AAAGHb3BzdGFja4OFAoAE"]} +``` + +## Peer Blocking Methods + +### `opp2p_blockPeer` + +Blocks a specific peer by peer ID, preventing any connections to or from that peer. + +| Client | Method invocation | +| ------ | --------------------------------------------------- | +| RPC | `{"method": "opp2p_blockPeer", "params": [peerID]}` | + +#### Parameters + +- `peerID` (string): The peer ID to block + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_blockPeer","params":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_unblockPeer` + +Unblocks a previously blocked peer by peer ID. + +| Client | Method invocation | +| ------ | ----------------------------------------------------- | +| RPC | `{"method": "opp2p_unblockPeer", "params": [peerID]}` | + +#### Parameters + +- `peerID` (string): The peer ID to unblock + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_unblockPeer","params":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_listBlockedPeers` + +Returns a list of all blocked peer IDs. + +| Client | Method invocation | +| ------ | -------------------------------------- | +| RPC | `{"method": "opp2p_listBlockedPeers"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_listBlockedPeers","params":[]} +{"jsonrpc":"2.0","id":1,"result":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x","16Uiu2HAm7Th3s3C1VHmKrYzA9nPzV4b3vqL8z1x8WZzGk2t9DjRK"]} +``` + +## Address Blocking Methods + +### `opp2p_blockAddr` + +Blocks connections from a specific IP address. + +| Client | Method invocation | +| ------ | -------------------------------------------------- | +| RPC | `{"method": "opp2p_blockAddr", "params": [address]}` | + +#### Parameters + +- `address` (string): The IP address to block (IPv4 or IPv6) + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_blockAddr","params":["192.168.1.100"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_unblockAddr` + +Unblocks a previously blocked IP address. + +| Client | Method invocation | +| ------ | ----------------------------------------------------- | +| RPC | `{"method": "opp2p_unblockAddr", "params": [address]}` | + +#### Parameters + +- `address` (string): The IP address to unblock (IPv4 or IPv6) + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_unblockAddr","params":["192.168.1.100"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_listBlockedAddrs` + +Returns a list of all blocked IP addresses. + +| Client | Method invocation | +| ------ | -------------------------------------- | +| RPC | `{"method": "opp2p_listBlockedAddrs"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_listBlockedAddrs","params":[]} +{"jsonrpc":"2.0","id":1,"result":["192.168.1.100","10.0.0.50","2001:db8::1"]} +``` + +## Subnet Blocking Methods + +### `opp2p_blockSubnet` + +Blocks connections from an entire IP subnet using CIDR notation. + +| Client | Method invocation | +| ------ | -------------------------------------------------- | +| RPC | `{"method": "opp2p_blockSubnet", "params": [subnet]}` | + +#### Parameters + +- `subnet` (string): The subnet to block in CIDR notation (e.g., "192.168.1.0/24") + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_blockSubnet","params":["192.168.1.0/24"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_unblockSubnet` + +Unblocks a previously blocked IP subnet. + +| Client | Method invocation | +| ------ | ---------------------------------------------------- | +| RPC | `{"method": "opp2p_unblockSubnet", "params": [subnet]}` | + +#### Parameters + +- `subnet` (string): The subnet to unblock in CIDR notation + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_unblockSubnet","params":["192.168.1.0/24"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_listBlockedSubnets` + +Returns a list of all blocked IP subnets. + +| Client | Method invocation | +| ------ | ---------------------------------------- | +| RPC | `{"method": "opp2p_listBlockedSubnets"}` | + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_listBlockedSubnets","params":[]} +{"jsonrpc":"2.0","id":1,"result":["192.168.1.0/24","10.0.0.0/16","2001:db8::/32"]} +``` + +## Peer Protection Methods + +### `opp2p_protectPeer` + +Protects a peer from being disconnected due to connection limits or other automatic pruning mechanisms. + +| Client | Method invocation | +| ------ | ---------------------------------------------------- | +| RPC | `{"method": "opp2p_protectPeer", "params": [peerID]}` | + +#### Parameters + +- `peerID` (string): The peer ID to protect + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_protectPeer","params":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_unprotectPeer` + +Removes protection from a peer, allowing it to be disconnected by automatic pruning mechanisms. + +| Client | Method invocation | +| ------ | ------------------------------------------------------ | +| RPC | `{"method": "opp2p_unprotectPeer", "params": [peerID]}` | + +#### Parameters + +- `peerID` (string): The peer ID to unprotect + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_unprotectPeer","params":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +## Connection Management Methods + +### `opp2p_connectPeer` + +Attempts to establish a connection to a specific peer using a multiaddress. + +| Client | Method invocation | +| ------ | ------------------------------------------------------ | +| RPC | `{"method": "opp2p_connectPeer", "params": [multiaddr]}` | + +#### Parameters + +- `multiaddr` (string): The multiaddress of the peer to connect to + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_connectPeer","params":["/ip4/127.0.0.1/tcp/9190/p2p/16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` + +### `opp2p_disconnectPeer` + +Disconnects from a specific peer by peer ID. + +| Client | Method invocation | +| ------ | ------------------------------------------------------- | +| RPC | `{"method": "opp2p_disconnectPeer", "params": [peerID]}` | + +#### Parameters + +- `peerID` (string): The peer ID to disconnect from + +#### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"opp2p_disconnectPeer","params":["16Uiu2HAmKVVub7edwZ3RKDnqMpZVsusYW9TKRgbwpH54nvDWLE4x"]} +{"jsonrpc":"2.0","id":1,"result":null} +``` diff --git a/kona/docs/docs/pages/node/rpc/rollup.mdx b/kona/docs/docs/pages/node/rpc/rollup.mdx new file mode 100644 index 0000000000000..ba88245c46456 --- /dev/null +++ b/kona/docs/docs/pages/node/rpc/rollup.mdx @@ -0,0 +1,354 @@ +# Rollup RPC Methods + +The `optimism` API provides methods for interacting with Kona's rollup state and configuration. + +## `optimism_outputAtBlock` + +Returns the output root at a specific block number, including the L2 block reference, withdrawal storage root, state root, and sync status. + +| Client | Method invocation | +| ------ | -------------------------------------------------------------- | +| RPC | `{"method": "optimism_outputAtBlock", "params": [blockNumber]}` | + +### Parameters + +- `blockNumber` (`BlockNumberOrTag`): The block number to get the output for. Can be a number, "latest", "earliest", "pending", "safe", or "finalized". + +### Returns + +`OutputResponse` - An object containing: +- `version` (`string`): The output version hash +- `outputRoot` (`string`): The output root hash +- `blockRef` (`L2BlockInfo`): Reference to the L2 block +- `withdrawalStorageRoot` (`string`): The withdrawal storage root +- `stateRoot` (`string`): The state root +- `syncStatus` (`SyncStatus`): The current sync status of the node + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"optimism_outputAtBlock","params":["latest"]} +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "version": "0x0000000000000000000000000000000000000000000000000000000000000000", + "outputRoot": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "blockRef": { + "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "number": 12345, + "parentHash": "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba", + "timestamp": 1699123456, + "l1Origin": { + "hash": "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "number": 18123456 + }, + "sequenceNumber": 42 + }, + "withdrawalStorageRoot": "0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234", + "stateRoot": "0xcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab", + "syncStatus": { + "current_l1": { + "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "number": 18123456 + }, + "current_l1_finalized": { + "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "number": 18123400 + }, + "head_l1": { + "hash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "number": 18123460 + }, + "safe_l1": { + "hash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "number": 18123450 + }, + "finalized_l1": { + "hash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "number": 18123400 + }, + "unsafe_l2": { + "hash": "0x6666666666666666666666666666666666666666666666666666666666666666", + "number": 12350, + "parentHash": "0x7777777777777777777777777777777777777777777777777777777777777777", + "timestamp": 1699123500, + "l1Origin": { + "hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "number": 18123460 + }, + "sequenceNumber": 47 + }, + "safe_l2": { + "hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "number": 12345, + "parentHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": 1699123456, + "l1Origin": { + "hash": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "number": 18123456 + }, + "sequenceNumber": 42 + }, + "finalized_l2": { + "hash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "number": 12340, + "parentHash": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "timestamp": 1699123400, + "l1Origin": { + "hash": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "number": 18123400 + }, + "sequenceNumber": 37 + }, + "cross_unsafe_l2": { + "hash": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "number": 12350, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "timestamp": 1699123500, + "l1Origin": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "number": 18123460 + }, + "sequenceNumber": 47 + }, + "local_safe_l2": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000003", + "number": 12345, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000004", + "timestamp": 1699123456, + "l1Origin": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000005", + "number": 18123456 + }, + "sequenceNumber": 42 + } + } + } +} +``` + +## `optimism_syncStatus` + +Returns the current synchronization status of the rollup node, including information about L1 and L2 block states. + +| Client | Method invocation | +| ------ | -------------------------------------------- | +| RPC | `{"method": "optimism_syncStatus", "params": []}` | + +### Returns + +`SyncStatus` - An object containing detailed sync information: +- `current_l1` (`BlockInfo`): The current L1 block that derivation is idled at +- `current_l1_finalized` (`BlockInfo`): The current L1 finalized block (legacy/deprecated) +- `head_l1` (`BlockInfo`): The L1 head block reference +- `safe_l1` (`BlockInfo`): The L1 safe head block reference +- `finalized_l1` (`BlockInfo`): The finalized L1 block reference +- `unsafe_l2` (`L2BlockInfo`): The unsafe L2 block reference (absolute tip) +- `safe_l2` (`L2BlockInfo`): The safe L2 block reference (derived from L1) +- `finalized_l2` (`L2BlockInfo`): The finalized L2 block reference +- `cross_unsafe_l2` (`L2BlockInfo`): Cross-unsafe L2 block with verified cross-L2 dependencies +- `local_safe_l2` (`L2BlockInfo`): Local safe L2 block derived from L1, not yet cross-verified + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"optimism_syncStatus","params":[]} +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "current_l1": { + "hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "number": 18123456 + }, + "current_l1_finalized": { + "hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "number": 18123400 + }, + "head_l1": { + "hash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "number": 18123460 + }, + "safe_l1": { + "hash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "number": 18123450 + }, + "finalized_l1": { + "hash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "number": 18123400 + }, + "unsafe_l2": { + "hash": "0x6666666666666666666666666666666666666666666666666666666666666666", + "number": 12350, + "parentHash": "0x7777777777777777777777777777777777777777777777777777777777777777", + "timestamp": 1699123500, + "l1Origin": { + "hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "number": 18123460 + }, + "sequenceNumber": 47 + }, + "safe_l2": { + "hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "number": 12345, + "parentHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": 1699123456, + "l1Origin": { + "hash": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "number": 18123456 + }, + "sequenceNumber": 42 + }, + "finalized_l2": { + "hash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "number": 12340, + "parentHash": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "timestamp": 1699123400, + "l1Origin": { + "hash": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "number": 18123400 + }, + "sequenceNumber": 37 + }, + "cross_unsafe_l2": { + "hash": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "number": 12350, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "timestamp": 1699123500, + "l1Origin": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "number": 18123460 + }, + "sequenceNumber": 47 + }, + "local_safe_l2": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000003", + "number": 12345, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000004", + "timestamp": 1699123456, + "l1Origin": { + "hash": "0x0000000000000000000000000000000000000000000000000000000000000005", + "number": 18123456 + }, + "sequenceNumber": 42 + } + } +} +``` + +## `optimism_rollupConfig` + +Returns the rollup configuration parameters that define the rollup chain's behavior and properties. + +| Client | Method invocation | +| ------ | ---------------------------------------------- | +| RPC | `{"method": "optimism_rollupConfig", "params": []}` | + +### Returns + +`RollupConfig` - An object containing the complete rollup configuration: +- `genesis` (`ChainGenesis`): The genesis state of the rollup +- `blockTime` (`number`): The block time of the L2 in seconds +- `maxSequencerDrift` (`number`): Maximum sequencer drift in seconds +- `seqWindowSize` (`number`): The sequencer window size +- `channelTimeout` (`number`): Number of L1 blocks between channel open/close +- `graniteChannelTimeout` (`number`): Channel timeout after Granite hardfork +- `l1ChainId` (`number`): The L1 chain ID +- `l2ChainId` (`number`): The L2 chain ID +- `batchInboxAddress` (`string`): L1 address where batches are sent +- `depositContractAddress` (`string`): L1 address for deposits +- `l1SystemConfigAddress` (`string`): L1 address for system config +- `protocolVersionsAddress` (`string`): L1 address for protocol versions +- Additional configuration fields for hardforks, fees, and interoperability + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"optimism_rollupConfig","params":[]} +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "genesis": { + "l1": { + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "number": 18000000 + }, + "l2": { + "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "number": 0, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": 1699000000, + "l1Origin": { + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "number": 18000000 + }, + "sequenceNumber": 0 + }, + "l2Time": 1699000000, + "systemConfig": { + "batcherAddr": "0x1234567890123456789012345678901234567890", + "overhead": "0x00000000000000000000000000000000000000000000000000000000000000bc", + "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "gasLimit": 30000000 + } + }, + "blockTime": 2, + "maxSequencerDrift": 600, + "seqWindowSize": 3600, + "channelTimeout": 300, + "graniteChannelTimeout": 50, + "l1ChainId": 1, + "l2ChainId": 10, + "batchInboxAddress": "0xff00000000000000000000000000000000000010", + "depositContractAddress": "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed", + "l1SystemConfigAddress": "0x229047fed2591dbec1eF1118d64F7aF3dB9EB290", + "protocolVersionsAddress": "0x8062AbC286f5e7D9428a0Ccb9AbD71e50d93b935", + "superchainConfigAddress": "0x95703e0982140D16f8ebA6d158FccEde42f04a4C", + "blobs_data": 1710374400, + "interopMessageExpiryWindow": 3600, + "chainOpConfig": { + "canyon_denominator": 250, + "canyon_elasticity": 6 + } + } +} +``` + +## `optimism_version` + +Returns the software version of the Kona rollup node. + +| Client | Method invocation | +| ------ | ---------------------------------------- | +| RPC | `{"method": "optimism_version", "params": []}` | + +### Returns + +`string` - The version string of the Kona software (e.g., "0.1.0") + +### Example + +```js +// > {"jsonrpc":"2.0","id":1,"method":"optimism_version","params":[]} +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0.1.0" +} +``` + +## Deprecated Methods + +### `optimism_safeHeadAtL1Block` + +This RPC endpoint is **not supported** in Kona. It was used to track the safe head for every L1 block, but this is no longer necessary post-interop. Calling this method will return a "Method not found" error. + +| Client | Method invocation | +| ------ | ------------------------------------------------------------------- | +| RPC | `{"method": "optimism_safeHeadAtL1Block", "params": [blockNumber]}` | + +### Returns + +This method returns an error with code `-32601` (Method not found). diff --git a/kona/docs/docs/pages/node/run/binary.mdx b/kona/docs/docs/pages/node/run/binary.mdx new file mode 100644 index 0000000000000..b236822f8316a --- /dev/null +++ b/kona/docs/docs/pages/node/run/binary.mdx @@ -0,0 +1,232 @@ +import { Callout } from 'vocs/components' + +# Run Kona Node as a Binary + +:::note + +If you haven't already built the `kona-node` binary, head over to the +[Installation](/node/install/overview) guide. + +::: + +`kona-node` is an L2 consensus client (also called a "rollup node"). +This means that the node is a _consensus_-layer client, which needs +a corresponding execution-layer client in order to sync and follow +the L2 chain. A number of L2 execution layer clients are available, +since the OP Stack uses a minimal diff approach to L1 execution +clients, including [`op-reth`][op-reth] and [`op-geth`][op-geth]. + +This section will illustrate running the `kona-node` with an instance +of [`op-reth`][op-reth]. + +Out of the box, the `kona-node` can be used for any `OP Stack` chain +that is part of the [`superchain-registry`][scr]. In order to use an +out-of-band `OP Stack` chain, for example a new devnet, you'll need +to specify the rollup config using the custom `--l2-config-file` +cli flag. More on that in the +[More Detailed Node Docs Section](#-More-Detailed-Node-Docs). + + +This tutorial walks through running the `kona-node` as +a binary. To use docker, head over to the +[Docker Guide](/node/run/docker) which uses a `docker-compose` +setup provided by `kona`. The `docker-compose` setup +automatically bootstraps the `kona-node` with `op-reth`, +provisioning grafana dashboards and a default Prometheus +configuration. It is encouraged to follow the +[Docker Guide](/node/run/docker) to avoid misconfigurations. + + +The `kona-node` requires a few CLI flags. + +- `--l1-eth-rpc ` + URL of the L1 execution client RPC API. +- `--l1-beacon ` + URL of the L1 beacon API. +- `--l2-engine-rpc ` + URL of the engine API endpoint of an L2 execution client. + +The L2 engine RPC points to the execution layer client's engine API, +[`op-reth`][op-reth]. + +An L1 beacon endpoint and rpc endpoint are also required to +fetch the L1 chain data that the L2 chain is derived from. + +First, start an instance of [`op-reth`][op-reth]. The +[`op-reth` docs][op-reth-docs] provide very detailed instructions +for running `op-reth` nodes for OP Stack chains (L2). For this +demo, we'll use `base`, but any other OP Stack chain will do. + +``` +op-reth node \ + --chain base \ + --rollup.sequencer-http https://mainnet-sequencer.base.org \ + --http \ + --ws \ + --authrpc.port 9551 \ + --authrpc.jwtsecret /path/to/jwt.hex +``` + +Kona has a `generate-jwt` justfile target that can be used to +create the `jwt.hex` file. Run `just generate-jwt`. + + + +The JWT token file path passed into `--authrpc.jwtsecret` +**MUST** be the same as the one passed into the `kona-node`. + +This JWT token is how the `op-reth` client authenticates +requests made by the `kona-node` to the engine rpc. + +By default, the `kona-node` will attempt to read a JWT token +from a `jwt.hex` file in the local directory. If it cannot +find one, it will create a JWT token in a new `jwt.hex` file. + +To specify the path to the file that contains the JWT token, +pass the file path into the `--l2.jwt-secret` CLI flag or +use the `KONA_NODE_L2_ENGINE_AUTH` environment variable. + + +Then, run the `kona-node` using Base's chain id - `8453`. + +``` +kona-node node \ + --chain 8453 \ + --l1-eth-rpc \ + --l1-beacon \ + --l2-engine-rpc http://127.0.0.1:9551 \ +``` + +That's it! Your node should connect to P2P and start syncing +quickly. + +#### RPC Trust Configuration + +By default, `kona-node` trusts RPC providers and doesn't perform additional verification. +This is suitable for local nodes but should be changed when using public RPC endpoints: + +``` +kona-node node \ + --chain 8453 \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l1-beacon https://public-beacon-api.com \ + --l2-engine-rpc http://127.0.0.1:9551 \ + # L2 trust-rpc defaults to true for local engine +``` + +The `--l1-trust-rpc false` flag enables block hash verification for the L1 RPC, +protecting against malicious or faulty public RPC providers. + + +#### Debugging + +`kona-node` provides a `-v` (or `--v`) flag as a way to set +the "verbosity" level for logs. By default, the verbosity level +is set to `3`, which is the INFO level. The level ranges from `0` +being no logs to `5` which includes trace logs. Log levels are +listed below, with each level including the one below it. + +- `5`: `TRACE` - very verbose logs that provide a detailed trace +- `4`: `DEBUG` - logs meant to print debugging information +- `3`: `INFO` - informational logs +- `2`: `WARN` - includes warning and error logs +- `1`: `ERROR` - only error logs are shown +- `0`: No logs + +The default verbosity level is `-vvv` which is the `3` or `INFO` +level. To set the `kona-node` to print `DEBUG` logs or level `4`, +run the node like so: `kona-node node -vvvv`. + +By default, the `kona-node` initializes its tracing (logging) +using the default environment variable filter [provided by the +tracing_subscriber crate][tracing-env]. This uses the value in +the `RUST_LOG` environment variable to set the tracing level +for specific targets. Effectively, `RUST_LOG` allows you to +bypass the default log level for the whole node or specific +log targets. For example, by prepending `RUST_LOG=engine=debug` +to the `kona-node` command (or setting that as an environment +variable), only `INFO` logs will be displayed except for the +`engine` log target which will also print `DEBUG` logs. + +This comes in handy say for when we would like to debug Kona's +P2P stack, we could prepend `RUST_LOG=discv5=debug,libp2p=debug` +to view debug logs from only `discv5` and `libp2p` targets. + + +#### More Detailed Node Docs + +There are a number of important defaults. + +- The node will poll and update the runtime config every 10 minutes. + Configurable via the `--l1.runtime-config-reload-interval` flag. +- The P2P stack is spun up. The libp2p swarm listens on TCP `9222` + to receive block gossip. The `discv5` discovery service runs on + UDP port `9223`. Peer scoring is enabled. +- An RPC server is exposed at `0.0.0.0:9545`. Websocket connections + are disabled by default. +- Metrics are enabled, serving prometheus metrics on `0.0.0.0:9090`. + This can be configured using the `--metrics.enabled`, + `--metrics.port`, and `--metrics.addr` cli flags. + + +If a file path to a rollup config is _not_ specified via the +`--l2-config-file` cli flag, the Rollup Config will be loaded +via the [superchain registry][scr]. + +A custom rollup config can either be specified through the +`--l2-config-file` flag, or specific values may be overridden +using a set of override flags provided by the `kona-node`. + +Override flags (for example `--canyon-override`) can be viewed +in the help menu by running `kona-node node --help`. The only +overrides currently supported are hardfork timestamps in seconds. + + +A set of CLI flags relating to the sequencer and supervisor are +also available to the `kona-node` binary. + +Now, when the `kona-node` starts up, it should immediately spin up +the P2P stack. It will begin discovering valid peers on the network +with the same chain id (base - `8453`) and OP Stack enr key "opstack". +When valid peers are discovered, they are sent to the libp2p swarm +which attempts to connect to them and listen for block gossip. + +Depending on the chain, and the P2P network topology, it may take +longer for the `kona-node` to establish a strong set of peers and +begin receiving block gossip. For larger, more mature chains like +OP Mainnet and Base, peer discovery should happen quickly via the +chain's P2P bootnodes. + +Once the first unsafe L2 payload attributes (block) is received +from peers in the libp2p swarm, it is sent off to the `kona-node`'s +engine actor which will kick off execution layer sync on the +`op-reth` execution client. When this happens, the `op-reth` logs +will start to show that it is fetching past L2 blocks to sync the +chain to tip. + +Once EL sync is finished, `kona-node`'s derivation actor will +kick off the derivation pipeline and begin deriving the L2 chain. +All the while, the P2P stack is separately receiving unsafe L2 +blocks from the chain's sequencer, and sending them off to the +engine actor to insert into the chain. + + +### Configuring a Dockerfile + +To learn more about running a `kona-node` using docker, check +out the [docker guide](/node/run/docker). + + +[tracing-env]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#method.from_default_env + +[scr]: https://github.com/ethereum-optimism/superchain-registry/tree/main + +[op-reth-docs]: https://reth.rs/run/opstack + +[op-reth]: https://github.com/paradigmxyz/reth/blob/main/crates/optimism/bin/src/main.rs +[op-geth]: https://github.com/ethereum-optimism/op-geth + +[buildx]: https://github.com/op-rs/kona/tree/main/docker +[packages]: https://github.com/orgs/op-rs/packages?repo_name=kona +[pdocs]: https://github.com/op-rs/kona/pkgs/container/kona%2Fkona-node/446969659?tag=latest diff --git a/kona/docs/docs/pages/node/run/docker.mdx b/kona/docs/docs/pages/node/run/docker.mdx new file mode 100644 index 0000000000000..53ff6e98ff23e --- /dev/null +++ b/kona/docs/docs/pages/node/run/docker.mdx @@ -0,0 +1,177 @@ +# Docker Guide + +:::info + +This guide uses Kona's pre-packaged docker config. + +For detailed usage of the `kona-node` binary, head +over to [the binary guide](/node/run/binary). + +::: + +Kona provides a [`kona-node` docker recipe][recipe] +with detailed instructions for running a complete node setup. + +## Quick Start + +The easiest way to run `kona-node` with Docker is using the provided recipe: + +1. **Navigate to the recipe directory:** + ```bash + cd docker/recipes/kona-node + ``` + +2. **Configure environment variables:** + Edit `cfg.env` to set your L1 RPC endpoints: + ```bash + L1_PROVIDER_RPC=https://your-l1-rpc-endpoint + L1_BEACON_API=https://your-l1-beacon-endpoint + ``` + +3. **Start the services:** + ```bash + just up + ``` + +This will start: +- `kona-node` - The OP Stack node implementation +- `op-reth` - Execution layer client +- `prometheus` - Metrics collection +- `grafana` - Monitoring dashboards (accessible at http://localhost:3000) + +## Docker Compose + +In the [provided docker compose][compose], there are a few services +aside from the `kona-node` and `op-reth`. These are `prometheus` +and `grafana` which automatically come provisioned with dashboards +for monitoring and insight into the `kona-node` and `op-reth` services. +For more detail into how Prometheus and Grafana work, head over to the +[Monitoring][monitoring] docs. + +The `docker-compose.yaml` uses published images from GitHub Container Registry: + +- **`op-reth`**: ghcr.io/paradigmxyz/op-reth:latest +- **`kona-node`**: ghcr.io/op-rs/kona/kona-node:latest + +### Service Configuration + +#### kona-node Service + +The `kona-node` service is configured with the following key settings: + +- **Ports**: + - `5060` - RPC endpoint + - `9223` - P2P discovery (TCP/UDP) + - `9002` - Metrics +- **Environment**: L1 RPC and Beacon API endpoints are required +- **Volumes**: Persistent data storage and JWT token for engine API authentication + +#### op-reth Service + +The `op-reth` service provides the execution layer: + +- **Ports**: + - `8545` - HTTP RPC + - `8551` - Engine API (authenticated) + - `30303` - P2P discovery + - `9001` - Metrics +- **Configuration**: Pre-configured for OP Sepolia testnet + +## Configuration + +### Network Selection + +By default, the recipe is configured for **OP Sepolia**. To sync a different OP Stack chain: + +1. Set appropriate L1 endpoints for your target network in `cfg.env` +2. Modify the docker-compose.yaml: + - Update `op-reth --chain` parameter + - Update `op-reth --rollup.sequencer-http` endpoint + - Update `kona-node --chain` parameter + +### RPC Trust Configuration + +By default, `kona-node` trusts RPC providers (both L1 and L2). When using public or untrusted RPC endpoints, you should disable trust to enable block hash verification: + +```bash +# In cfg.env or as environment variables: +KONA_NODE_L1_TRUST_RPC=false +KONA_NODE_L2_TRUST_RPC=false +``` + +Or modify the docker-compose.yaml command: +```yaml +kona-node: + command: | + node + --chain op-sepolia + --l1-eth-rpc ${L1_PROVIDER_RPC} + --l1-beacon ${L1_BEACON_API} + --l1-trust-rpc false # Add this for untrusted L1 RPCs + --l2-engine-rpc ws://op-reth:8551 + --l2-trust-rpc false # Add this for untrusted L2 RPCs +``` + +See the [configuration guide](/node/configuration#rpc-trust-configuration) for more details on RPC trust settings. + +### Port Configuration + +All host ports can be customized via environment variables in `cfg.env`: + +```bash +# Kona Node ports +KONA_NODE_RPC_PORT=5060 +KONA_NODE_DISCOVERY_PORT=9223 +KONA_NODE_METRICS_PORT=9002 + +# OP Reth ports +OP_RETH_RPC_PORT=8545 +OP_RETH_ENGINE_PORT=8551 +OP_RETH_METRICS_PORT=9001 +OP_RETH_DISCOVERY_PORT=30303 + +# Monitoring +PROMETHEUS_PORT=9090 +``` + +### Logging + +Adjust log levels by setting the `RUST_LOG` environment variable: + +```bash +export RUST_LOG=engine_builder=trace,runtime=debug +``` + +## Management Commands + +The recipe includes convenient Just commands: + +```bash +# Start all services +just up + +# Stop all services +just down + +# Restart all services +just restart + +# Generate JWT token (if needed) +./generate-jwt.sh +``` + +## Using Local Images + +To use locally built images instead of published ones: + +1. **Build the kona-node image:** + ```bash + just build-local kona-node + ``` + +2. **Update docker-compose.yaml** to use `kona-node:local` instead of the published image. + +[monitoring]: ../monitoring.mdx + +[recipe]: https://github.com/op-rs/kona/blob/f86052b5dacec7da46b12441aafab2867069f7e7/docker/recipes/kona-node/README.md +[compose]: https://github.com/op-rs/kona/blob/f86052b5dacec7da46b12441aafab2867069f7e7/docker/recipes/kona-node/docker-compose.yaml diff --git a/kona/docs/docs/pages/node/run/mechanics.mdx b/kona/docs/docs/pages/node/run/mechanics.mdx new file mode 100644 index 0000000000000..e53a582845629 --- /dev/null +++ b/kona/docs/docs/pages/node/run/mechanics.mdx @@ -0,0 +1,225 @@ +# How it Works + +Kona brings together a powerful suite of `no-std` and `std` Rust components, +purpose-built for the OP Stack. At the heart of this ecosystem is the +`kona-node` — a modern, modular rollup node (L2 consensus node) that can +be used as a drop-in binary or as a foundation for custom services. + +The `kona-node` is a fully compliant implementation of the ["Rollup Node" +Specifications][rollup-node] and is released as a [binary][package] in +the [kona repository][kona]. Whether you're running a production network, +building new rollup features, or experimenting with the OP Stack, +`kona-node` is built to be both robust and extensible. + + +### Background + +A rollup node is responsible for deriving the canonical L2 chain from L1 +blocks and their receipts. It validates these blocks using the +[Engine API][engine-api], passing them to the execution layer for processing. + +Paired with an execution engine like op-reth or op-geth, `kona-node` tracks +the unsafe, safe, and finalized tips of the L2 OP Stack chain, ensuring the +node is always in sync with the latest state. + +:::note + +Rollup nodes hold minimal state. Unsafe and safe payloads alike are sent +away to the execution engine client which holds the chain's db. This way, +the rollup node holds a view of the tip of the chain - unsafe, safe, and +finalized block info. All data otherwise needed is held in memory. + +::: + +There are a few core architectural pieces of the `kona-node`. + +- **Derivation Pipeline:** Constructs L2 payload attributes from L1 blocks, + forming the backbone of rollup logic. +- **Execution Engine Integration:** Executes L2 payload attributes via the + [Engine API][engine-api], abstracting away different EL clients. +- **P2P Networking:** Enables block gossip and peer discovery. + +For an in-depth breakdown of these three pillars and a detailed design +of the `kona-node`, visit the [Node Design section](/node/design/intro). + +Additionally, an RPC server exposes essential methods, including the +[L2 Output RPC method][l2o-rpc]. + + +### Syncing + +The `kona-node` syncs the L2 chain in two main phases: + +1. **Execution Layer (EL) Sync:** + When starting, the node initially has an empty engine task queue. + Once an unsafe block is received from P2P gossip, an `InsertUnsafe` + task is executed. The `InsertUnsafe` task executes a forkchoice + update through the engine api. Since the engine state is lazily + initialized (the safe and finalized heads are zero), the forkchoice + update kicks off EL sync on the execution layer (EL) client (such + as op-reth or op-geth). EL sync instructs the execution client to + fetch and sync L2 blocks directly from peers. During this phase, + the `kona-node` effectively waits for the EL client to reach the + chain tip, when it returns that it is `synced`. No L2 blocks + are derived from L1 during this period. + +2. **Consensus Layer (CL) Sync:** + Once the EL client is fully synced to the tip, the node transitions + to consensus layer (CL) sync. In this phase, `kona-node` begins + deriving new L2 payload attributes from the L1 chain, following + the rollup derivation process. `OpPayloadAttributesWithParent` + derived this way are executed as an engine task which submits + the payloads to the execution engine for validation and execution. + +:::note + +- `kona-node` does **not** support historical CL sync or backfilling + L2 blocks from L1 for past chain history. It relies on the EL client + to perform the initial sync of the L2 chain. +- Only after the EL is fully synced does the node begin deriving and + following new L2 blocks from L1. + +::: + +### Extensibility + +The `kona-node` is designed as a modular, actor-based node SDK, +making it possible to extend or customize node behavior by adding +new actors or swapping out existing ones. This extensibility is +currently in **beta**, but the architecture is intentionally built +to support advanced use cases and custom integrations. + + +#### Actor Model + +At the core of `kona-node` is the concept of **actors**—independent, +async services that communicate via channels. Each actor implements +the `NodeActor` trait, which defines how the actor is built, started, +and how it communicates with other actors. + +Key built-in actors include: +- **EngineActor**: Manages the execution layer (EL) Engine API. +- **DerivationActor**: Runs the derivation pipeline to produce L2 payloads. +- **NetworkActor**: Handles P2P networking and block gossip. +- **RpcActor**: Runs the node's RPC server. +- **SupervisorActor**: Integrates with the supervisor RPC API. +- **L1WatcherActor**: Watches L1 for new blocks and events. + + +#### Extending with Custom Actors + +You can add your own actors to the node by implementing the `NodeActor` +trait. This allows you to introduce new background services, event +processors, or integrations with external systems. + +**Example: Defining a Custom Actor** + +```rust +use kona_node_service::NodeActor; +use async_trait::async_trait; +use tokio_util::sync::CancellationToken; + +struct MyCustomActor; + +#[async_trait] +impl NodeActor for MyCustomActor { + type Error = std::io::Error; + type InboundData = MyCustomContext; + type OutboundData = (); + type State = (); + + fn build(_state: Self::State) -> (Self::OutboundData, Self) { + ((), MyCustomActor) + } + + async fn start(self, ctx: Self::InboundData) -> Result<(), Self::Error> { + // Your actor logic here + Ok(()) + } +} + +struct MyCustomContext { + cancellation: CancellationToken, +} + +impl kona_node_service::CancellableContext for MyCustomContext { + fn cancelled(&self) -> tokio_util::sync::WaitForCancellationFuture<'_> { + self.cancellation.cancelled() + } +} +``` + +#### Integrating Custom Actors + +To integrate your custom actor, you can create your own implementation +of the `RollupNodeService` trait, which defines the set of actors and +pipelines used by the node. You can swap out any of the built-in actors +for your own, or add entirely new ones. + +**Example: Custom RollupNodeService** + +```rust +use kona_node_service::{RollupNodeService, NodeActor, ...}; + +struct MyNodeService; + +#[async_trait] +impl RollupNodeService for MyNodeService { + // Use built-in actors, or your own custom ones + type DataAvailabilityWatcher = MyCustomActor; + type DerivationPipeline = ...; + type DerivationActor = ...; + type EngineActor = ...; + type NetworkActor = ...; + type SupervisorExt = ...; + type SupervisorActor = ...; + type RpcActor = ...; + type Error = ...; + + // Implement required trait methods... +} +``` + +You can then instantiate and run your custom node service in your +own binary or integration test. + +#### Programmatic Node Construction + +The `RollupNodeBuilder` provides a convenient way to construct a +standard node, but for advanced use cases, you can build your own +node by composing actors and services directly, or by implementing +your own builder pattern. + +#### Current Limitations + +- The extensibility API is **beta** and may change. +- Most users will want to start by subclassing or wrapping the + standard `RollupNode` and only override specific actors or + pipelines as needed. +- Documentation and examples for advanced extensibility are still + evolving—contributions and feedback are welcome! + +#### Learn More + +- See the [`NodeActor` trait documentation][node-actor] for details + on implementing actors. +- Explore the [kona-node-service crate][service] for source code + and more examples. + + +[cli-docs]: ../configuration.mdx +[subcommands]: ../subcommands.mdx + +[service]: https://github.com/op-rs/kona/tree/main/crates/node/service +[node-actor]: https://docs.rs/kona-node-service/latest/kona_node_service/trait.NodeActor.html + +[kona]: https://github.com/op-rs/kona +[packages]: https://github.com/orgs/op-rs/packages?repo_name=kona +[rollup-node]: https://specs.optimism.io/protocol/rollup-node.html +[package]: https://github.com/op-rs/kona/pkgs/container/kona%2Fkona-node +[pdocs]: https://github.com/op-rs/kona/pkgs/container/kona%2Fkona-node/446969659?tag=latest + +[buildx]: https://github.com/op-rs/kona/tree/main/docker + +[engine-api]: https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md +[l2o-rpc]: https://specs.optimism.io/protocol/rollup-node.html#l2-output-rpc-method diff --git a/kona/docs/docs/pages/node/run/overview.mdx b/kona/docs/docs/pages/node/run/overview.mdx new file mode 100644 index 0000000000000..b462ad2afb91e --- /dev/null +++ b/kona/docs/docs/pages/node/run/overview.mdx @@ -0,0 +1,38 @@ +# Run a Node + +Now that you have [installed the `kona-node`](/node/install/overview), +it's time to run it. + +In this section, we'll guide you through running the kona-node on +various networks and with different configurations. + + +## Supported Networks + +Kona uses the [superchain-registry][scr] to dynamically load +chain configurations for the specified network. As such, Kona +can only support networks that are defined this way. + +[scr]: https://github.com/ethereum-optimism/superchain-registry + +To view available networks, the `kona-node` binary provides +a `registry` subcommand that lists all available networks: + +```bash +kona-node registry +``` + +:::tip +Want to add support for a new network? +Feel free to [add a chain](https://github.com/ethereum-optimism/superchain-registry/blob/main/docs/ops.md#adding-a-chain) +to the superchain-registry! +::: + + +## Configuration & Monitoring + +Learn how to configure and monitor your node: + +- **[Configuration](/node/configuration)** - Configure your node +- **[Monitoring](/node/monitoring)** - Set up logs, metrics, and observability + diff --git a/kona/docs/docs/pages/node/subcommands.mdx b/kona/docs/docs/pages/node/subcommands.mdx new file mode 100644 index 0000000000000..79f15a159ea3b --- /dev/null +++ b/kona/docs/docs/pages/node/subcommands.mdx @@ -0,0 +1,15 @@ +# `kona-node` Subcommands + +Below are the available subcommands for `kona-node`: + +- **node**: Runs the main consensus node service. This is the primary subcommand for operating a rollup node. +- **info**: Displays information about the node, build, and environment. +- **bootstore**: Manages the P2P bootstore (used for peer discovery and persistence). +- **net**: Provides network-related utilities and diagnostics. +- **registry**: Interacts with the chain registry for configuration and metadata. + +For more details on each subcommand and their flags, run: + +``` +kona-node --help +``` \ No newline at end of file diff --git a/kona/docs/docs/pages/rfc/active/intro.mdx b/kona/docs/docs/pages/rfc/active/intro.mdx new file mode 100644 index 0000000000000..5e8a523494664 --- /dev/null +++ b/kona/docs/docs/pages/rfc/active/intro.mdx @@ -0,0 +1,8 @@ +# Request For Comment [RFC] + +Documents in this section are in the request-for-comment stage. + +To comment on these documents, [open an issue in the kona repository](https://github.com/op-rs/kona/issues/new) +and provide detail on the changes you're requesting. + +Once the document has been reviewed, they will be moved to the archives. diff --git a/kona/docs/docs/pages/rfc/archived/monorepo.mdx b/kona/docs/docs/pages/rfc/archived/monorepo.mdx new file mode 100644 index 0000000000000..eccd69104a19f --- /dev/null +++ b/kona/docs/docs/pages/rfc/archived/monorepo.mdx @@ -0,0 +1,153 @@ +import { Callout } from 'vocs/components' + +# Monorepo Project + +| Document | Date | Decision | +| ----------- | ------------- | -------- | +| Monorepo | 02/19/2025 | Approved | + +This is a document outlining merging multiple external +repositories into `kona` to create a rust monorepo for Optimism. + +## Provenance + +Let's rewind the clock to when `kona` was just being started +in the spring of 2024. What little optimism rust types existed +were siloed in applications like `op-reth` and `revm`. Library +code that could have been re-used was unfortunately placed in +an `std` environment that `kona` cannot use because the kona +fault proof program is built for a minimal instruction set. + +Effectively, `kona` needed a bunch of Optimism-specific and +slightly-modified Ethereum types that were available in a +`no_std` environment. + +As development started, types were defined in `kona`. Mostly +jammed into the `kona-derive` crate for use in derivation, these +types were now being duplicated across a number of rust repos just +to support `no_std`. + +Enter `op-alloy`. The first effort to de-duplicate types between +`reth`, `revm`, and `kona` (as well as periphery applications like `magi`). +Rather than keeping types in `kona`, `op-alloy` was introduced as a shared +place for `no_std` compatible optimism rust types. This worked +well as a common place to contribute and decoupled the rapid +development in `kona` from the more stable definition of Optimism +rust types. + +Fast forward to 2025, [interop](https://specs.optimism.io/interop/overview.html) +started seriously picking up momentum as a critical project +in the Optimism Rust world. Interop introduces a whole host +of new Optimism-specific types that really should live in a +shared library. But `op-alloy` was already becoming quite large +beyond the minimal, de-duplicated set of types originally intended. + +This led to OP folks splitting out Optimism-specific types from +`op-alloy` into a new repo called `maili`. What was not foreseen +was the grievance with yet-another level in dependency chain for +Optimism rust projects. Now, downstream optimism rust projects +could have to import all of these crates just to construct an +OP Stack service: +- `op-alloy` +- `maili` +- `op-revm` +- `op-reth` +- `kona` + +And those are just the Optimism-specific crates, let alone +Ethereum crates like `alloy`. + + +## Why? + +The current dependency chain is ever growing. +A small change in `op-alloy` cascades into the following. + +- Release `op-alloy` crates +- Update `maili` with `op-alloy` version and changes. +- Release `maili` crates. +- Update `kona` to work with both `op-alloy` and `maili` crates. +- Release `kona`. +- If something breaks in `kona` or downstream consumers, repeat. + +To iterate faster without needing to manage releases or change +dependencies into git refs, this doc proposes a monorepo structure +that pulls `maili` into `kona`, while retaining `maili` crate +names **and** versioning. + +We propose gone are the days of releasing a single version for +all crates. With a larger, more extensive `kona` monorepo, crates +will need to manage their own semver. + +To re-iterate: the key takeaway here is current downstream consumers +of `maili` crates _will not have to change a thing_. Crates pulled +into `kona` will still be published under `maili-` prefixed crate +names. How this is managed while keeping the `kona-` prefix crate +naming consistent is discussed below. + + +## Proposed Repository Structure + +The new repository structure would look as follows. + +```ignore +crates/ + proof/ + mpt/ + executor/ + preimage/ + fpvm/ + fpvm-proc/ + proof + proof-interop/ + protocol/ + derive/ + driver/ + interop/ + genesis/ <-- Has Maili Shadow + registry/ <-- Has Maili Shadow + protocol/ <-- Has Maili Shadow + services/ + rpc/ <-- Has Maili Shadow + net/ + providers-alloy/ + providers-local/ + utilities/ + serde/ <-- Has Maili Shadow + cli/ + ... +``` + + +Crates denoted with `<-- Has Maili Shadow` are ported from `maili`, +but contain a nested crate with the `maili-` prefix instead of `kona-` +prefix. These crates re-export their `kona-` equivalent crates. This +allows downstream users to not need to change their dependencies to +keep using `maili-` crates! Eventually once the kona monorepo matures, +and downstream consumers use `kona-` crates instead of `maili-`, these +can be removed. + + +#### Maili Shadow Example + +Let's look at `crates/protocol/genesis`. + +This crate will have a `Cargo.toml` that defines itself as `kona-genesis`. + +The contents of the `crates/protocol/genesis` directory will be + +```ignore +../genesis/ + README.md + Cargo.toml <-- package.name = "kona-genesis" + src/ + .. <-- current contents of `maili-genesis`, ported + maili/ + Cargo.toml <-- package.name = "maili-genesis" + src/ + lib.rs <-- Re-exports `kona-genesis` +``` + +This structure allows us to seamlessly remain backwards compatible, +while being able to work in the new `kona-` crates without requiring +heavy lifting to support `maili-` crates. diff --git a/kona/docs/docs/pages/rfc/archived/umbrellas.mdx b/kona/docs/docs/pages/rfc/archived/umbrellas.mdx new file mode 100644 index 0000000000000..ae0401b6fd3a1 --- /dev/null +++ b/kona/docs/docs/pages/rfc/archived/umbrellas.mdx @@ -0,0 +1,124 @@ +import { Callout } from 'vocs/components' + +# Umbrella Crates + + +TL;DR, this is a proposal to introduce tiny crates inside each +container directory (e.g. `crates/protocol/`) to re-export all +crates contained in that directory. + + +## Context + +#### Repository Structure + +Kona now has a [monorepo](/rfc/archived/monorepo) structure that merged +`maili` and `hilo` crates into `kona`. This introduces a number of higher-level +directories that hold a variety of crates themselves. As of the time at which +this document was written the `kona` repository loosely looks like the following. + +```ignore +bins/ + -> client/ + -> host/ +crates/ + -> protocol/ + -> genesis/ + -> protocol/ + -> derive/ + -> driver/ + -> interop/ + -> registry/ + -> proof/ + -> mpt/ + -> executor/ + -> proof/ + -> proof-interop/ + -> preimage/ + -> std-fpvm/ + -> std-fpvm-proc/ + -> node/ + -> net/ + -> rpc/ + -> engine/ + -> providers/ + -> providers-alloy/ + -> providers-local/ + -> utilities/ + -> serde/ +``` + +Within crates, the `protocol`, `proof`, `node`, `providers`, and `utilities` +directories all contain crates, and are not crates themselves - only directories. + +#### Publishing Crates + +When crates in `kona` are published, they are all published individually, with no +way to add all kona crates as a dependency. This makes discoverability difficult +without accurate and up-to-date documentation, adding overhead. + +## Problem + +As a monorepo, `kona` will likely have a growing number of crates that will make +it increasingly difficult to discover new `kona` crates and manage `kona` as a +dependency, for downstream consumers. + +Additionally, each crate has its own independent version, which makes it more nuanced +to manage `kona` dependencies and less clear which crate versions are compatible. + +## Considered Options + +### Single Umbrella Crate + +One option that would make `kona` crates the easiest to consume is to provide +a single umbrella crate that lives at the top-level (e.g. `crates/umbrella/Cargo.toml`). + +This crate could simply be called `kona` and re-export all crates in the `kona` +monorepo under various feature flags, with propagating `std` and `serde` feature +flags. + +#### Tradeoffs + +The benefit of this option is providing a single crate to consume all of `kona`, +with the downside of having to manage all the various feature flags and crate +re-exports in the single umbrella crate. + +### Grouped Umbrella Crates + +In each of the `crates/` sub-directories, provide an umbrella crate that exports +all crates within that subdirectory. + +For example, in the `crates/protocol/` subdirectory, an umbrella crate would +re-export all crates in `crates/protocol/`. It could be called +`kona-umbrella-protocol` or some other name to make it easily discoverable. + +#### Tradeoffs + +While this simplifies updates when adding or removing a re-export, it introduces +`n-1` additional crates as the single umbrella crate, where `n` is the number +of sub-directories in `crates/`. These many umbrella crates also make `kona` less +easily consumed by downstream users of `kona` as opposed to the singular umbrella +crate. + +### Top-level Umbrella with Subdirectory Umbrellas (Combined) + +Effectively, this option is to combined the previous two options into one. + +In this configuration, the top-level umbrella crate could just re-export +each of the umbrella crates in the sub-directory. + +#### Tradeoffs + +Unfortunately this option now introduces `n + 1` number of crates where `n` +is the number of subdirectories in `crates/`, but it still only requires +updates to the subdirectory umbrellas when a crate is added or removed. + +The benefit of this option is the top-level umbrella crate is very much +simplified, since it only needs to re-export the `n` umbrella crates and not +every crate in the workspace. It also provides the single consumable `kona` +crate for downstream users that greatly simplifies managing `kona` as a +dependency. + +## Proposed Solution + +This proposal has been iced. diff --git a/kona/docs/docs/pages/run.mdx b/kona/docs/docs/pages/run.mdx new file mode 100644 index 0000000000000..082c82b6ca6b6 --- /dev/null +++ b/kona/docs/docs/pages/run.mdx @@ -0,0 +1,56 @@ +import { Callout } from 'vocs/components' + +# Run a Node + + +This tutorial walks through running the `kona-node` as +a binary. To use docker, head over to the +[Docker Guide](/node/install/docker) which uses a `docker-compose` +setup provided by `kona`. The `docker-compose` setup +automatically bootstraps the `kona-node` with `op-reth`, +provisioning grafana dashboards and a default Prometheus +data source. + + +## Prerequisites + +In order to follow this tutorial, you'll need: + +1. An L1 Archive node (e.g., `op-geth`) with enough history for the rollup network you want to run. +2. A `kona-node` binary. See [installation](/node/install/binaries) for instructions. +3. A rollup configuration file. See [rollup configuration](/sdk/protocol/genesis/rollup-config) for more information. + +## Quick Start + +The fastest way to get started is to use one of the pre-built configurations: + +```bash +# Download a rollup configuration +curl -o rollup.json https://raw.githubusercontent.com/op-rs/kona/main/configs/base-mainnet.json + +# Run the node +kona-node --rollup-config rollup.json --l1-rpc-url http://localhost:8545 +``` + +## Configuration + +The `kona-node` can be configured using command-line flags or environment variables. For a complete list of options, run: + +```bash +kona-node --help +``` + +### Key Configuration Options + +- `--rollup-config`: Path to the rollup configuration file +- `--l1-rpc-url`: L1 RPC endpoint URL +- `--l2-rpc-url`: L2 RPC endpoint URL (optional) +- `--data-dir`: Directory for storing node data +- `--log-level`: Logging level (debug, info, warn, error) + +## Next Steps + +- [Docker Support](/node/run/docker) - Run with Docker +- [Monitoring](/node/monitoring) - Set up monitoring and metrics +- [CLI Reference](/node/configuration) - Complete CLI documentation +- [Subcommands](/node/subcommands) - Available subcommands diff --git a/kona/docs/docs/pages/sdk/examples/batch-to-frames.mdx b/kona/docs/docs/pages/sdk/examples/batch-to-frames.mdx new file mode 100644 index 0000000000000..67b614bea06f1 --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/batch-to-frames.mdx @@ -0,0 +1,212 @@ +# Transform a Batch into Frames + +:::info +This example performs the reverse transformation as the [frames-to-batch][frames-to-batch] example. +::: + +This example walks through transforming a [`Batch`][batch] into [`Frame`][frame]s. + +Effectively, this example demonstrates the _encoding_ process from an L2 batch into the +serialized bytes that are posted to the data availability layer. + +:::danger +Steps and handling of types with respect to chain tip, ordering of frames, re-orgs, and +more are not covered by this example. This example solely demonstrates the most trivial +way to transform an individual [`Batch`][batch] into [`Frame`][frame]s. +::: + + + +## Walkthrough + +The high level transformation is the following. + +```ignore +Batch -> decompressed batch data -> ChannelOut -> frames[] -> bytes[] +``` + +Given the [`Batch`][batch], the first step to encode the batch +using the [`Batch::encode()`][encode-batch] method. The output bytes +need to then be compressed prior to adding them to the +[`ChannelOut`][channel-out]. + +:::info +The [`ChannelOut`][channel-out] type also provides a method for adding +the [`Batch`][batch] itself, handling encoding and compression, but +this method is not available yet. +::: + +Once compressed using the [`compress_brotli`][compress-brotli] method, the +compressed bytes can be added to a newly constructed [`ChannelOut`][channel-out]. +As long as the [`ChannelOut`][channel-out] has [`ready_bytes()`][ready-bytes], +[`Frame`][frame]s can be constructed using the +[`ChannelOut::output_frame()`][output-frame] method, specifying the maximum +frame size. + +Once [`Frame`][frame]s are returned from the [`ChannelOut`][channel-out], +they can be [`Frame::encode`][encode-frame] into raw, serialized data +ready to be batch-submitted to the data-availability layer. + + +## Running this example: + +- Clone the examples repository: `git clone git@github.com:op-rs/kona.git` +- Run: `cargo run --example batch_to_frames` + +```rust +//! An example encoding and decoding a [SingleBatch]. +//! +//! This example demonstrates EIP-2718 encoding a [SingleBatch] +//! through a [ChannelOut] and into individual [Frame]s. +//! +//! Notice, the raw batch is first _encoded_. +//! Once encoded, it is compressed into raw data that the channel is constructed with. +//! +//! The [ChannelOut] then outputs frames individually using the maximum frame size, +//! in this case hardcoded to 100, to construct the frames. +//! +//! Finally, once [Frame]s are built from the [ChannelOut], they are encoded and ready +//! to be batch-submitted to the data availability layer. + +#[cfg(feature = "std")] +fn main() { + use alloy_primitives::BlockHash; + use kona_comp::{ChannelOut, CompressionAlgo, VariantCompressor}; + use kona_genesis::RollupConfig; + use kona_protocol::{Batch, ChannelId, SingleBatch}; + + // Use the example transaction + let transactions = example_transactions(); + + // Construct a basic `SingleBatch` + let parent_hash = BlockHash::ZERO; + let epoch_num = 1; + let epoch_hash = BlockHash::ZERO; + let timestamp = 1; + let single_batch = SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions }; + let batch = Batch::Single(single_batch); + + // Create a new channel. + let id = ChannelId::default(); + let config = RollupConfig::default(); + let compressor: VariantCompressor = CompressionAlgo::Brotli10.into(); + let mut channel_out = ChannelOut::new(id, &config, compressor); + + // Add the compressed batch to the `ChannelOut`. + channel_out.add_batch(batch).unwrap(); + + // Output frames + while channel_out.ready_bytes() > 0 { + let frame = channel_out.output_frame(100).expect("outputs frame"); + println!("Frame: {}", alloy_primitives::hex::encode(frame.encode())); + if channel_out.ready_bytes() <= 100 { + channel_out.close(); + } + } + + assert!(channel_out.closed); + println!("Successfully encoded Batch to frames"); +} + +#[cfg(feature = "std")] +fn example_transactions() -> Vec { + use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope}; + use alloy_eips::eip2718::{Decodable2718, Encodable2718}; + use alloy_primitives::{Address, Signature, U256}; + + let mut transactions = Vec::new(); + + // First Transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[6]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + // Second transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[7]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + transactions +} + +#[cfg(not(feature = "std"))] +fn main() { + /* not implemented for no_std */ +} +``` + +[frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html +[batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[add-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.add_frame +[decode-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.decode +[hex]: https://docs.rs/alloy_primitives/latest/alloy_primitives/macro.hex.html +[is-ready]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.is_ready +[frame-data]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.frame_data +[bytes]: https://docs.rs/alloy_primitives/latest/alloy_primitives/struct.Bytes.html +[decode-batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.decode +[fjord]: https://specs.optimism.io/protocol/fjord/overview.html +[channel-id]: https://docs.rs/kona-protocol/latest/kona_protocol/type.ChannelId.html + +[encode-batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.encode +[compress-brotli]: https://docs.rs/op-alloy-protocol/latest/op_alloy_protocol/struct.BrotliCompressor.html +[channel-out]: https://docs.rs/kona-comp/latest/kona_comp/struct.ChannelOut.html +[ready-bytes]: https://docs.rs/kona-comp/latest/kona_comp/struct.ChannelOut.html#method.ready_bytes +[output-frame]: https://docs.rs/kona-comp/latest/kona_comp/struct.ChannelOut.html#method.output_frame +[encode-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.encode + + +[frames-to-batch]: /sdk/examples/frames-to-batch + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/examples/custom-derivation-pipeline.mdx b/kona/docs/docs/pages/sdk/examples/custom-derivation-pipeline.mdx new file mode 100644 index 0000000000000..decb017e9c60f --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/custom-derivation-pipeline.mdx @@ -0,0 +1,199 @@ +import { Callout } from 'vocs/components' + +# Custom Derivation Pipeline Stage + +Extend Kona's derivation pipeline by wrapping the top-level `AttributesQueue` stage with custom logic for monitoring, validation, or transformation. + +## Core Concepts + +The derivation pipeline uses a stage-based architecture where each stage wraps the previous one: + +``` +L1Traversal → L1Retrieval → FrameQueue → ChannelProvider → +ChannelReader → BatchStream → BatchProvider → AttributesQueue +``` + +### Key Traits + +Custom stages that wrap the `AttributesQueue` must implement: +- `NextAttributes` - Provides payload attributes for block building +- `OriginProvider` - Provides current L1 origin +- `SignalReceiver` - Handles pipeline resets +- `OriginAdvancer` - Advances L1 origin + +## Example: Monitoring Stage + +Wrap the `AttributesQueue` to add metrics tracking: + +```rust +use kona_derive::{ + NextAttributes, OriginProvider, SignalReceiver, OriginAdvancer, + PipelineResult, Signal, OpAttributesWithParent +}; +use kona_protocol::{BlockInfo, L2BlockInfo}; +use async_trait::async_trait; +use std::time::Instant; + +#[derive(Debug)] +pub struct LoggingStage { + inner: S, + attributes_count: u64, + last_origin: Option, +} + +impl LoggingStage { + pub fn new(inner: S) -> Self { + Self { + inner, + attributes_count: 0, + last_origin: None, + } + } +} + +#[async_trait] +impl NextAttributes for LoggingStage +where + S: NextAttributes + Send + Sync, +{ + async fn next_attributes( + &mut self, + parent: L2BlockInfo + ) -> PipelineResult { + let start = Instant::now(); + + // Delegate to inner stage + let attributes = self.inner.next_attributes(parent).await?; + + // Track metrics + self.attributes_count += 1; + let duration = start.elapsed(); + + info!( + target: "pipeline::logging", + count = self.attributes_count, + duration_ms = duration.as_millis(), + parent_hash = ?parent.block_info.hash, + "Generated attributes" + ); + + Ok(attributes) + } +} + +impl OriginProvider for LoggingStage +where + S: OriginProvider, +{ + fn origin(&self) -> Option { + self.inner.origin() + } +} + +#[async_trait] +impl SignalReceiver for LoggingStage +where + S: SignalReceiver + Send + Sync, +{ + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + info!(target: "pipeline::logging", ?signal, "Received signal"); + + // Track origin changes on reset + if let Signal::Reset(reset) = &signal { + self.last_origin = Some(reset.l1_origin); + self.attributes_count = 0; // Reset counter + } + + self.inner.signal(signal).await + } +} + +#[async_trait] +impl OriginAdvancer for LoggingStage +where + S: OriginAdvancer + Send + Sync, +{ + async fn advance_origin(&mut self) -> PipelineResult<()> { + let prev_origin = self.inner.origin(); + self.inner.advance_origin().await?; + let new_origin = self.inner.origin(); + + if prev_origin != new_origin { + info!( + target: "pipeline::logging", + prev = ?prev_origin, + new = ?new_origin, + "Advanced origin" + ); + } + + Ok(()) + } +} +``` + + + Custom stages wrap the `AttributesQueue` (top-level stage). For deeper pipeline modifications, you'd need to rebuild the entire pipeline. + + +## Integration + +```rust +use kona_derive::{PipelineBuilder, DerivationPipeline}; +use kona_node::{StatefulAttributesBuilder}; +use alloc::sync::Arc; + +// Build standard pipeline +let pipeline = PipelineBuilder::new() + .rollup_config(rollup_config.clone()) + .origin(origin) + .chain_provider(chain_provider) + .l2_chain_provider(l2_chain_provider.clone()) + .dap_source(dap_source) + .builder(attributes_builder) + .build_polled(); + +// Wrap with monitoring +let monitoring_stage = LoggingStage::new(pipeline.attributes); + +// Create new pipeline +let custom_pipeline = DerivationPipeline::new( + monitoring_stage, + rollup_config, + l2_chain_provider, +); +``` + +## Testing + +```rust +#[cfg(test)] +mod tests { + use super::*; + use kona_derive::test_utils::TestNextAttributes; + + #[tokio::test] + async fn test_logging_stage() { + let mock_inner = TestNextAttributes::new(); + let mut stage = LoggingStage::new(mock_inner); + + // Test attributes generation + let parent = L2BlockInfo::default(); + let result = stage.next_attributes(parent).await; + assert!(result.is_ok()); + assert_eq!(stage.attributes_count, 1); + + // Test signal handling + let signal = Signal::Reset(Default::default()); + stage.signal(signal).await.unwrap(); + assert_eq!(stage.attributes_count, 0); + } +} +``` + +## Related Resources + +- [kona-derive](https://github.com/op-rs/kona/tree/main/crates/protocol/derive) - Core derivation pipeline +- [Pipeline Traits](https://github.com/op-rs/kona/tree/main/crates/protocol/derive/src/traits) - Trait definitions +- [Stage Examples](https://github.com/op-rs/kona/tree/main/crates/protocol/derive/src/stages) - Built-in stages +- [OP Stack Derivation Spec](https://specs.optimism.io/protocol/derivation.html) - Protocol specification \ No newline at end of file diff --git a/kona/docs/docs/pages/sdk/examples/executor-test-fixtures.mdx b/kona/docs/docs/pages/sdk/examples/executor-test-fixtures.mdx new file mode 100644 index 0000000000000..68388f2a3d5c8 --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/executor-test-fixtures.mdx @@ -0,0 +1,21 @@ +# `kona-executor` test fixtures + +The `StatelessL2Builder` type uses static test data fixtures to run stateless execution of certain blocks offline. The +test data fixtures include: +* The `RollupConfig` of the chain that the block belongs to. +* The parent block header, which we apply state on top of. +* The payload attributes for building the new block. +* A `rocksdb` database containing the witness data for stateless execution of the block building job. + +Sometimes, updates in the block building code can add new state accesses, requiring these fixtures to be re-generated. + +To generate a new fixture and add it to the test suite, run: + +```sh +cargo r -p execution-fixture \ + --l2-rpc \ + --block-number +``` + +this command will add a new compressed test fixture for the given L2 block into `kona-executor`'s `testdata` directory. +The test suite will automatically pick this new test fixture up, and no further action is needed to register it. diff --git a/kona/docs/docs/pages/sdk/examples/frames-to-batch.mdx b/kona/docs/docs/pages/sdk/examples/frames-to-batch.mdx new file mode 100644 index 0000000000000..453223c0bcdfa --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/frames-to-batch.mdx @@ -0,0 +1,193 @@ +# Transform Frames into a Batch + +:::info +This example performs the reverse transformation as the [batch-to-frames][batch-to-frames] example. +::: + +This example walks through transforming [`Frame`][frame]s into the [`Batch`][batch] types. + +:::danger +Steps and handling of types with respect to chain tip, ordering of frames, re-orgs, and +more are not covered by this example. This example solely demonstrates the most trivial +way to transform individual [`Frame`][frame]s into a [`Batch`][batch] type. +::: + + +## Walkthrough + +The high level transformation is the following. + +```ignore +raw bytes[] -> frames[] -> channel -> decompressed channel data -> Batch +``` + +Given the raw, batch-submitted frame data as bytes (read in with the [`hex!` macro][hex]), +the first step is to decode the frame data into [`Frame`][frame]s using +[`Frame::decode`][decode-frame]. Once all the [`Frame`][frame]s are decoded, +the [`Channel`][channel] can be constructed using the [`ChannelId`][channel-id] +of the first frame. + + +:::info +[`Frame`][frame]s may also be added to a [`Channel`][channel] +once decoded with the [`Channel::add_frame`][add-frame] method. +::: + +When the [`Channel`][channel] is [`Channel::is_ready()`][is-ready], +the frame data can taken from the [`Channel`][channel] using +[`Channel::frame_data()`][frame-data]. This data is represented as [`Bytes`][bytes] +and needs to be decompressed using the respective compression algorithm depending on +which hardforks are activated (using the `RollupConfig`). For the sake of this example, +`brotli` is used (which was activated in the [Fjord hardfork][fjord]). Decompressed +brotli bytes can then be passed right into [`Batch::decode`][decode-batch] +to wind up with the example's desired [`Batch`][batch]. + + +## Running this example: + +- Clone the examples repository: `git clone git@github.com:op-rs/kona.git` +- Run: `cargo run --example frames_to_batch` + +```rust +//! This example decodes raw [Frame]s and reads them into a [Channel] and into a [SingleBatch]. + +use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope}; +use alloy_eips::eip2718::{Decodable2718, Encodable2718}; +use alloy_primitives::{Address, BlockHash, Bytes, Signature, U256, hex}; +use kona_genesis::RollupConfig; +use kona_protocol::{Batch, BlockInfo, Channel, Frame, SingleBatch, decompress_brotli}; + +fn main() { + // Raw frame data taken from the `encode_channel` example. + let first_frame = hex!( + "60d54f49b71978b1b09288af847b11d200000000004d1b1301f82f0f6c3734f4821cd090ef3979d71a98e7e483b1dccdd525024c0ef16f425c7b4976a7acc0c94a0514b72c096d4dcc52f0b22dae193c70c86d0790a304a08152c8250031d091063ea000" + ); + let second_frame = hex!( + "60d54f49b71978b1b09288af847b11d2000100000046b00d00005082edde7ccf05bded2004462b5e80e1c42cd08e307f5baac723b22864cc6cd01ddde84efc7c018d7ada56c2fa8e3c5bedd494c3a7a884439d5771afcecaf196cb3801" + ); + + // Decode the raw frames. + let decoded_first = Frame::decode(&first_frame).expect("decodes frame").1; + let decoded_second = Frame::decode(&second_frame).expect("decodes frame").1; + + // Create a channel. + let id = decoded_first.id; + let open_block = BlockInfo::default(); + let mut channel = Channel::new(id, open_block); + + // Add the frames to the channel. + let l1_inclusion_block = BlockInfo::default(); + channel.add_frame(decoded_first, l1_inclusion_block).expect("adds frame"); + channel.add_frame(decoded_second, l1_inclusion_block).expect("adds frame"); + + // Get the frame data from the channel. + let frame_data = channel.frame_data().expect("some frame data"); + println!("Frame data: {}", hex::encode(&frame_data)); + + // Decompress the frame data with brotli. + let config = RollupConfig::default(); + let max = config.max_rlp_bytes_per_channel(open_block.timestamp) as usize; + let decompressed = decompress_brotli(&frame_data, max).expect("decompresses brotli"); + println!("Decompressed frame data: {}", hex::encode(&decompressed)); + + // Decode the single batch from the decompressed data. + let batch = Batch::decode(&mut decompressed.as_slice(), &config).expect("batch decodes"); + assert_eq!( + batch, + Batch::Single(SingleBatch { + parent_hash: BlockHash::ZERO, + epoch_num: 1, + epoch_hash: BlockHash::ZERO, + timestamp: 1, + transactions: example_transactions(), + }) + ); + + println!("Successfully decoded frames into a Batch"); +} + +fn example_transactions() -> Vec { + let mut transactions = Vec::new(); + + // First Transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[6]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + // Second transaction in the batch. + let tx = TxEip1559 { + chain_id: 10u64, + nonce: 2, + max_fee_per_gas: 3, + max_priority_fee_per_gas: 4, + gas_limit: 5, + to: Address::left_padding_from(&[7]).into(), + value: U256::from(7_u64), + input: vec![8].into(), + access_list: Default::default(), + }; + let sig = Signature::test_signature(); + let tx_signed = tx.into_signed(sig); + let envelope: TxEnvelope = tx_signed.into(); + let encoded = envelope.encoded_2718(); + transactions.push(encoded.clone().into()); + let mut slice = encoded.as_slice(); + let decoded = TxEnvelope::decode_2718(&mut slice).unwrap(); + assert!(matches!(decoded, TxEnvelope::Eip1559(_))); + + transactions +} +``` + + +[batch-to-frames]: /sdk/examples/batch-to-frames +[frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html +[batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[add-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.add_frame +[decode-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.decode +[hex]: https://docs.rs/alloy_primitives/latest/alloy_primitives/macro.hex.html +[is-ready]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.is_ready +[frame-data]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.frame_data +[bytes]: https://docs.rs/alloy_primitives/latest/alloy_primitives/struct.Bytes.html +[decode-batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.decode +[fjord]: https://specs.optimism.io/protocol/fjord/overview.html +[channel-id]: https://docs.rs/kona-protocol/latest/kona_protocol/type.ChannelId.html + + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/examples/intro.mdx b/kona/docs/docs/pages/sdk/examples/intro.mdx new file mode 100644 index 0000000000000..0eab9b96d06b4 --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/intro.mdx @@ -0,0 +1,11 @@ +# Examples + +Examples for working with `kona` crates. + +- [Load a Rollup Config for a Chain ID](/sdk/examples/load-a-rollup-config) +- [Transform Frames to a Batch](/sdk/examples/frames-to-batch) +- [Transform a Batch to Frames](/sdk/examples/batch-to-frames) +- [Create a new L1BlockInfoTx Hardfork Variant](/sdk/examples/new-l1-block-info-tx-hardfork) +- [Create a new `kona-executor` test fixture](/sdk/examples/executor-test-fixtures) +- [Configuring P2P Network Peer Scoring](/sdk/examples/p2p-peer-scoring) +- [Custom Derivation Pipeline with New Stage](/sdk/examples/custom-derivation-pipeline) diff --git a/kona/docs/docs/pages/sdk/examples/load-a-rollup-config.mdx b/kona/docs/docs/pages/sdk/examples/load-a-rollup-config.mdx new file mode 100644 index 0000000000000..fac36ba7c5e9a --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/load-a-rollup-config.mdx @@ -0,0 +1,36 @@ +import { Callout } from 'vocs/components' + +# Loading a Rollup Config from a Chain ID + +In this section, the code examples demonstrate loading the +rollup config for the given L2 Chain ID. + +Let's load the Rollup Config for OP Mainnet which hash chain id 10. + +```rust +extern crate kona_genesis; +extern crate kona_protocol; + +use kona_registry::ROLLUP_CONFIGS; +use kona_genesis::OP_MAINNET_CHAIN_ID; + +// Load a rollup config from the chain id. +let op_mainnet_config = ROLLUP_CONFIGS.get(&OP_MAINNET_CHAIN_ID).expect("infallible"); + +// The chain id should match the hardcoded chain id. +assert_eq!(op_mainnet_config.chain_id, OP_MAINNET_CHAIN_ID); +``` + + + +Available Configs + +[kona-registry][kona-registry] dynamically provides all rollup configs +from the [superchain-registry][registry] for their respective chain ids. +Note though, that this requires `serde` since it deserializes the rollup +configs dynamically from json files. + + + +[kona-registry]: https://crates.io/crates/kona-registry +[registry]: https://github.com/ethereum-optimism/superchain-registry diff --git a/kona/docs/docs/pages/sdk/examples/new-l1-block-info-tx-hardfork.mdx b/kona/docs/docs/pages/sdk/examples/new-l1-block-info-tx-hardfork.mdx new file mode 100644 index 0000000000000..ac211939421d0 --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/new-l1-block-info-tx-hardfork.mdx @@ -0,0 +1,116 @@ +import { Callout } from 'vocs/components' + +# Create a `L1BlockInfoTx` Variant for a new `Hardfork` + +This example walks through creating a variant of the [`L1BlockInfoTx`][info-tx] +for a new Hardfork. + + + +This example is very verbose. +To grok required changes, view [this PR diff][pr-diff] +which introduces Isthmus hardfork changes to the `L1BlockInfoTx` with a new variant. + + + + +## Required Genesis Updates + +The first updates that need to be made are to [`kona-genesis`][genesis] +types, namely the [`RollupConfig`][rc] and [`HardForkConfig`][hfc]. + +First, add a timestamp field to the [`RollupConfig`][rc]. Let's use the +hardfork name "Glacier" as an example. + +```rust +pub struct RollupConfig { + ... + /// `glacier_time` sets the activation time for the Glacier network upgrade. + /// Active if `glacier_time` != None && L2 block timestamp >= Some(glacier_time), inactive + /// otherwise. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub glacier_time: Option, + ... +} +``` + +Add an accessor on the [`RollupConfig`][rc] to provide a way of checking whether the +"Glacier" hardfork is active for a given timestamp. Also update the prior hardfork +accessor to call this method (let's use "Isthmus" as the prior hardfork). + +```rust + /// Returns true if Isthmus is active at the given timestamp. + pub fn is_isthmus_active(&self, timestamp: u64) -> bool { + self.isthmus_time.map_or(false, |t| timestamp >= t) || self.is_glacier_active(timestamp) + } + + /// Returns true if Glacier is active at the given timestamp. + pub fn is_glacier_active(&self, timestamp: u64) -> bool { + self.glacier_time.map_or(false, |t| timestamp >= t) + } +``` + +Lastly, add the "Glacier" timestamp to the [`HardForkConfig`][hfc]. + +```rust +pub struct HardForkConfig { + ... + /// Glacier hardfork activation time + pub glacier_time: Option, +} +``` + + +## Protocol Changes + +Introduce a new `glacier.rs` module containing a `L1BlockInfoGlacier` type +in [`kona_genesis::info` module][info-mod]. + +This should include a few methods used in the `L1BlockInfoTx` later. + +```rust + pub fn encode_calldata(&self) -> Bytes { ... } + + pub fn decode_calldata(r: &[u8]) -> Result { ... } +``` + +Use other hardfork variants like the [`L1BlockInfoEcotone`][ecotone] +for reference. + +Next, add the new "Glacier" variant to the [`L1BlockInfoTx`][info-tx]. + +```rust +pub enum L1BlockInfoTx { + ... + Glacier(L1BlockInfoGlacier) +} +``` + +Update [`L1BlockInfoTx::try_new`][try-new] to construct the `L1BlockInfoGlacier` +if the hardfork is active using the `RollupConfig::is_glacier_active`. + +Also, be sure to update [`L1BlockInfoTx::decode_calldata`][decode-calldata] +with the new variant decoding, as well as other [`L1BlockInfoTx`][info-tx] +methods. + +Once some tests are added surrounding the decoding and encoding of the new +`L1BlockInfoGlacier` variant, all required changes are complete! + +Now, [this example PR diff][pr-diff] introducing the Isthmus changes should +make sense, since it effectively implements the above changes for the Isthmus +hardfork (replacing "Glacier" with "Isthmus"). Notice, Isthmus introduces +some new "operator fee" fields as part of it's `L1BlockInfoIsthmus` type. +Some new error variants to the [`BlockInfoError`][bie] are needed as well. + + + +[bie]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.BlockInfoError.html +[pr-diff]: https://github.com/alloy-rs/op-alloy/pull/130/files +[decode-calldata]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.L1BlockInfoTx.html#method.decode_calldata +[try-new]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.L1BlockInfoTx.html#method.try_new +[ecotone]: https://github.com/op-rs/kona/blob/main/crates/protocol/hardforks/src/ecotone.rs +[info-mod]: https://github.com/op-rs/kona/blob/main/crates/protocol/protocol/src/info/mod.rs +[genesis]: https://docs.rs/kona-genesis/latest/kona_genesis/index.html +[rc]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.RollupConfig.html +[hfc]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.HardForkConfig.html +[info-tx]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.L1BlockInfoTx.html diff --git a/kona/docs/docs/pages/sdk/examples/p2p-peer-scoring.mdx b/kona/docs/docs/pages/sdk/examples/p2p-peer-scoring.mdx new file mode 100644 index 0000000000000..ca2bb3eb28f89 --- /dev/null +++ b/kona/docs/docs/pages/sdk/examples/p2p-peer-scoring.mdx @@ -0,0 +1,352 @@ +import { Callout } from 'vocs/components' + +# Configuring P2P Network Peer Scoring + +Peer scoring is a critical mechanism in Kona's P2P network that evaluates the behavior and +reliability of peers. By assigning scores to peers based on their performance, the network +can prioritize connections with well-behaved peers and disconnect from malicious or poorly +performing ones, ensuring overall network health and resilience. + +This guide demonstrates how to configure peer scoring in Kona's P2P stack, including basic +setup, advanced configurations, and monitoring strategies. + +## Understanding Peer Scoring + +Peer scoring in Kona evaluates peers based on multiple factors: +- **Message Delivery**: How reliably peers deliver messages +- **Mesh Participation**: Active participation in gossipsub mesh +- **Invalid Messages**: Penalties for invalid or malformed messages +- **IP Colocation**: Penalties for multiple peers from the same IP +- **Behavior Patterns**: General peer behavior and responsiveness + + + Peer scoring helps maintain network quality by identifying and removing peers that: + - Fail to deliver messages reliably + - Send invalid or spam messages + - Exhibit malicious behavior patterns + - Have poor network connectivity + + +## Peer Score Levels + +Kona supports two peer scoring levels: + +```rust +use kona_peers::PeerScoreLevel; + +// Disable peer scoring entirely +let no_scoring = PeerScoreLevel::Off; + +// Enable light peer scoring with default parameters +let light_scoring = PeerScoreLevel::Light; +``` + +### Score Calculation Parameters + +The scoring system uses several key parameters: + +```rust +use kona_peers::PeerScoreLevel; +use std::time::Duration; + +// Key scoring constants +const DECAY_TO_ZERO: f64 = 0.01; // Decay factor for scores +const MESH_WEIGHT: f64 = -0.7; // Weight for mesh delivery +const MAX_IN_MESH_SCORE: f64 = 10.0; // Maximum score for mesh participation + +// Calculate decay factor for a given duration +let block_time = Duration::from_secs(2); // OP Stack default +let epoch = block_time * 6; +let decay = PeerScoreLevel::score_decay(epoch * 10, block_time); +``` + +## Basic Configuration + +### Using CLI Flags + +Configure peer scoring via command-line flags: + +```bash +# Enable light peer scoring (default) +kona node --p2p.scoring light + +# Disable peer scoring +kona node --p2p.scoring off + +# Configure with peer banning +kona node \ + --p2p.scoring light \ + --p2p.ban.peers \ + --p2p.ban.threshold -100 \ + --p2p.ban.duration 60 +``` + +### Programmatic Configuration + +Configure peer scoring in code: + +```rust +use kona_node_service::NetworkConfig; +use kona_peers::{PeerScoreLevel, PeerMonitoring}; +use kona_genesis::RollupConfig; +use libp2p::Multiaddr; +use alloy_primitives::Address; +use std::time::Duration; + +// Create network configuration with peer scoring +let mut config = NetworkConfig::new( + rollup_config, + discovery_listen, + gossip_address, + unsafe_block_signer, +); + +// Set peer scoring level +config.scoring = PeerScoreLevel::Light; + +// Configure peer monitoring and banning +config.monitor_peers = Some(PeerMonitoring { + ban_threshold: -100.0, // Ban peers with score below -100 + ban_duration: Duration::from_secs(3600), // Ban for 1 hour +}); +``` + +## Advanced Configuration + +### Topic Scoring Parameters + +Configure topic-specific scoring based on block time: + +```rust +use kona_peers::PeerScoreLevel; +use libp2p::gossipsub::{TopicHash, TopicScoreParams}; +use std::collections::HashMap; + +// Get topic score parameters for OP Stack (2 second blocks) +let block_time = 2; // seconds +let topic_params = PeerScoreLevel::topic_score_params(block_time); + +// Create topic scores for specific topics +let topics = vec![ + TopicHash::from_raw("/optimism/10/0/blocks"), + TopicHash::from_raw("/optimism/10/1/blocks"), +]; +let topic_scores = PeerScoreLevel::topic_scores(topics, block_time); +``` + + + Topic scoring is being phased out in the OP Stack. It's disabled by default + and should only be enabled for backwards compatibility or debugging purposes. + + Use the `--p2p.topic-scoring` flag to enable if needed. + + +### Gossipsub Mesh Parameters + +Fine-tune gossipsub mesh parameters for optimal performance: + +```rust +use kona_gossip::default_config_builder; + +// Create custom gossipsub configuration +let gossip_config = default_config_builder() + .mesh_n(8) // Target mesh degree (D) + .mesh_n_low(6) // Lower bound (Dlo) + .mesh_n_high(12) // Upper bound (Dhi) + .gossip_lazy(6) // Gossip degree (Dlazy) + .flood_publish(false) // Don't flood publish + .build() + .expect("valid config"); + +// Apply to network configuration +config.gossip_config = gossip_config; +``` + +### Peer Score Thresholds + +Configure thresholds that determine peer treatment: + +```rust +use libp2p::gossipsub::PeerScoreThresholds; + +// Default thresholds used by Kona +const DEFAULT_THRESHOLDS: PeerScoreThresholds = PeerScoreThresholds { + gossip_threshold: -10.0, // Below this: no gossip propagation + publish_threshold: -40.0, // Below this: ignore published messages + graylist_threshold: -40.0, // Below this: remove from mesh + accept_px_threshold: 20.0, // Above this: accept peer exchange + opportunistic_graft_threshold: 0.05, // Threshold for opportunistic grafting +}; +``` + +## Complete Example + +Here's a comprehensive example configuring P2P with peer scoring: + +```rust +use kona_node_service::NetworkConfig; +use kona_peers::{PeerScoreLevel, PeerMonitoring}; +use kona_genesis::RollupConfig; +use kona_gossip::{GaterConfig, default_config_builder}; +use kona_disc::LocalNode; +use libp2p::{Multiaddr, identity::Keypair}; +use alloy_primitives::Address; +use std::time::Duration; +use std::net::IpAddr; + +async fn setup_p2p_with_scoring() -> NetworkConfig { + // Load rollup configuration + let rollup_config = RollupConfig { + l2_chain_id: 10.into(), // OP Mainnet + block_time: 2, + // ... other config + }; + + // Generate or load keypair + let keypair = Keypair::generate_secp256k1(); + + // Configure discovery + let discovery_address = LocalNode::new( + keypair.clone().try_into_secp256k1().unwrap().secret(), + "0.0.0.0".parse::().unwrap(), + 9222, // TCP port + 9223, // UDP port + ); + + // Configure gossip listening address + let mut gossip_address = Multiaddr::from("0.0.0.0".parse::().unwrap()); + gossip_address.push(libp2p::multiaddr::Protocol::Tcp(9222)); + + // Set unsafe block signer + let unsafe_block_signer = Address::from([0u8; 20]); + + // Create base configuration + let mut config = NetworkConfig::new( + rollup_config.clone(), + discovery_address, + gossip_address, + unsafe_block_signer, + ); + + // Configure peer scoring + config.scoring = PeerScoreLevel::Light; + + // Enable peer monitoring with banning + config.monitor_peers = Some(PeerMonitoring { + ban_threshold: -100.0, + ban_duration: Duration::from_secs(3600), + }); + + // Configure gossipsub parameters + config.gossip_config = default_config_builder() + .mesh_n(8) + .mesh_n_low(6) + .mesh_n_high(12) + .gossip_lazy(6) + .flood_publish(false) + .heartbeat_interval(Duration::from_millis(500)) + .build() + .expect("valid gossipsub config"); + + // Configure connection gater for peer redialing + config.gater_config = GaterConfig { + peer_redialing: Some(500), // Max redial attempts + dial_period: Duration::from_secs(3600), // Reset period + }; + + // Set the keypair + config.keypair = keypair; + + // Optionally disable topic scoring (recommended) + config.topic_scoring = false; + + config +} +``` + +## CLI Configuration Examples + +### Development Setup + +Minimal scoring for local development: + +```bash +kona node \ + --p2p.scoring off \ + --p2p.listen.ip 127.0.0.1 \ + --p2p.listen.tcp 9222 +``` + +### Production Setup + +Full peer scoring with monitoring: + +```bash +kona node \ + --p2p.scoring light \ + --p2p.ban.peers \ + --p2p.ban.threshold -100 \ + --p2p.ban.duration 60 \ + --p2p.gossip.mesh.d 8 \ + --p2p.gossip.mesh.lo 6 \ + --p2p.gossip.mesh.dhi 12 \ + --p2p.redial 500 \ + --p2p.redial.period 60 +``` + +### High-Security Setup + +Strict scoring with aggressive banning: + +```bash +kona node \ + --p2p.scoring light \ + --p2p.ban.peers \ + --p2p.ban.threshold -50 \ + --p2p.ban.duration 120 \ + --p2p.redial 100 \ + --p2p.discovery.interval 3 +``` + +## Monitoring Peer Scores + +Kona inspects peer scores every 15 seconds and can automatically ban poorly performing peers: + +```rust +use kona_gossip::PEER_SCORE_INSPECT_FREQUENCY; + +// Inspection happens every 15 seconds +assert_eq!(*PEER_SCORE_INSPECT_FREQUENCY, Duration::from_secs(15)); + +// The network handler automatically: +// 1. Checks all connected peer scores +// 2. Identifies peers below ban threshold +// 3. Disconnects and bans problematic peers +// 4. Updates metrics for monitoring +``` + + + Peer scores and banning events are recorded in metrics. Monitor these metrics + to understand your node's peer health: + - `kona_gossip_peer_scores`: Histogram of peer scores + - `kona_gossip_banned_peers`: Counter of banned peers + - `kona_gossip_peer_connection_duration_seconds`: Connection durations + + +## Best Practices + +1. **Start with Light Scoring**: Use `PeerScoreLevel::Light` for most deployments +2. **Monitor Before Banning**: Run without banning initially to understand score distributions +3. **Adjust Thresholds Gradually**: Start with default -100 threshold and adjust based on observations +4. **Consider Network Conditions**: Increase ban duration in hostile environments +5. **Balance Security and Connectivity**: Overly aggressive scoring can isolate your node +6. **Disable Topic Scoring**: Unless specifically needed for compatibility +7. **Use Metrics**: Monitor peer scores and ban events to tune configuration + +## Related Resources + +- [kona-peers](https://github.com/op-rs/kona/tree/main/crates/node/peers) - Peer management and scoring +- [kona-gossip](https://github.com/op-rs/kona/tree/main/crates/node/gossip) - Gossipsub implementation +- [kona-node-service](https://github.com/op-rs/kona/tree/main/crates/node/service) - Network service implementation +- [libp2p gossipsub](https://docs.rs/libp2p-gossipsub) - Underlying gossipsub protocol +- [OP Stack P2P Spec](https://specs.optimism.io/protocol/rollup-node-p2p.html) - Protocol specification \ No newline at end of file diff --git a/kona/docs/docs/pages/sdk/fpp-dev/env.mdx b/kona/docs/docs/pages/sdk/fpp-dev/env.mdx new file mode 100644 index 0000000000000..3bfb61f33d3b5 --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/env.mdx @@ -0,0 +1,82 @@ +# Environment + +Before kicking off the development of your own [Fault Proof Program](/glossary#fault-proof-program), +it's important to understand the environment that your program will be running in. + +The FPP runs on top of a custom FPVM target, which is typically a VM with a modified subset of an existing reduced instruction set architecture and a subset of Linux syscalls. The FPVM is designed to +execute verifiable programs, and commonly modifies the instruction set it is derived from as well as the internal representation of memory to support verifiable memory access, `client` (program) +communication with the `host` (the FPVM), and other implementation-specific features. + +## Host and Client Communication + +While the program is running on top of the FPVM, it is considered to be in the `client` role, while the VM is in the `host` role. The only way for the `client` and `host` +to communicate with one another is synchronously through the [Preimage ABI](/glossary#preimage-abi) ([specification][preimage-specs]). + +In order for the `client` to read from the `host`, the `read` and `write` syscalls are modified within the FPVM to allow the `client` to request preparation of and read foreign data. + +### Reading + +When the `client` wants to read data from the `host`, it must first send a "hint" to the `host` through the hint file descriptor, which signals a request for the `host` to prepare the data for reading. The `host` will then +prepare the data, and send a hint acknowledgement back to the `client`. The `client` can then read the data from the host through the designated file descriptor. + +The preparation step ("hinting") is an optimization that allows the `host` to know ahead of time the intents of the `client` and the data it requires for execution. This can allow +for lazy loading of data, and also prevent the need for unnecessary allocations within the `host`'s memory. This step is a no-op on-chain, and is only ran locally +when the `host` is the native implementation of the FPVM. + +
+ +```mermaid +sequenceDiagram + Client->>+Host: Hint preimage (no-op on-chain / read-only mode) + Host-->>-Client: Hint acknowledgement + Client-->>+Host: Preimage Request + Host-->>Host: Prepare Preimage + Host-->>-Client: Preimage Data +``` + +
+ +## Full Example + +Below, we have a full architecture diagram of the [`op-program`][op-program] (source: [fault proof specs][fp-specs]), the reference implementation for the OP Stack's Fault Proof Program, +which has the objective of verifying claims about the state of an [OP Stack][op-stack] layer two. + +![op-program-architecture](/op-program-fpp.svg) + +In this program, execution and derivation of the L2 chain is performed within it, and ultimately the claimed state of the L2 chain is verified in the [prologue](/sdk/fpp-dev/prologue) stage. + +It communicates with the `host` for two reasons: + +1. To request preparation of L1 and L2 state data preimages. +1. To read the L1 and L2 state data preimages that were prepared after the above requests. + +The `host` is responsible for: + +1. Preparing and maintaining a store of the L1 and L2 state data preimages, as well as localized bootstrap k/v pairs. +1. Providing the L1 and L2 state data preimages to the `client` for reading. + +Other programs (`clients`) may have different requirements for communication with the `host`, but the above is a common pattern for programs built on top of a FPVMs. In general: + +1. The `client` program is a state machine that is responsible for bootstrapping itself from the inputs, executing the program logic, and verifying the outcome. +1. The `host` is responsible for providing the `client` with data it wasn't bootstrapped with, and for executing the program itself. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/fpp-dev/epilogue.mdx b/kona/docs/docs/pages/sdk/fpp-dev/epilogue.mdx new file mode 100644 index 0000000000000..326a7af4dd25c --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/epilogue.mdx @@ -0,0 +1,17 @@ +# Epilogue + +The epilogue stage of the program is intended to perform the final validation on the outputs from the +[execution phase](/sdk/fpp-dev/execution). In most programs, this entails comparing the outputs of the execution phase +to portions of the bootstrap data made available during the [prologue phase](/sdk/fpp-dev/prologue). + +Generally, this phase should consist almost entirely of validation steps. + +## Example + +In the `kona-client` program, the prologue phase only contains two directives: + +1. Validate that the L2 safe chain could be produced at the claimed L2 block height. +1. The constructed output root is equivalent to the claimed [L2 output root][l2-output-root]. + + +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction diff --git a/kona/docs/docs/pages/sdk/fpp-dev/execution.mdx b/kona/docs/docs/pages/sdk/fpp-dev/execution.mdx new file mode 100644 index 0000000000000..ec2df1b371b5d --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/execution.mdx @@ -0,0 +1,21 @@ +# Execution + +The execution phase of the program is commonly the heaviest portion of the fault proof program, where the computation +that is being verified is performed. + +This phase consumes the outputs of the [prologue phase](/sdk/fpp-dev/prologue), and performs the bulk of the verifiable +computation. After execution has concluded, the outputs are passed along to the [epilogue phase](/sdk/fpp-dev/epilogue) for +final verification. + +## Example + +At a high-level, in the `kona-client` program, the execution phase: + +1. Derives the inputs to the L2 derivation pipeline by unrolling the L1 head hash fetched in the epilogue. +1. Passes the inputs to the L2 derivation pipeline, producing the L2 execution payloads required to reproduce + the L2 safe chain at the claimed height. +1. Executes the payloads produced by the L2 derivation pipeline, producing the [L2 output root][l2-output-root] at the + L2 claim height. + + +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction diff --git a/kona/docs/docs/pages/sdk/fpp-dev/intro.mdx b/kona/docs/docs/pages/sdk/fpp-dev/intro.mdx new file mode 100644 index 0000000000000..e64d9beac3484 --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/intro.mdx @@ -0,0 +1,21 @@ +# Fault Proof Program Development + +This chapter provides an overview of [Fault Proof Program](/glossary#fault-proof-program) development +on top of the custom FPVM targets supported by [Kona][kona]. + +At a high level, a Fault Proof Program is not much different from a regular `no_std` Rust program. A custom entrypoint is provided, and the program +is compiled down to a custom target, which is then executed on the FPVM. + +Fault Proof Programs are structured with 3 stages: +1. **Prologue**: The bootstrapping stage, where the program is loaded into memory and the initial state is set up. During this phase, the program's initial + state is written to the FPVM's memory, and the program's entrypoint is set. +1. **Execution**: The main execution stage, where the program is executed on the FPVM. During this phase, the program's entrypoint is called, and the + program is executed until it exits. +1. **Epilogue**: The finalization stage, where the program's final state is read from the FPVM's memory. During this phase, the program's final state is + inspected and properties of the state transition are verified. + +The following sections will provide a more in-depth overview of each of these stages, as well as the tools and abstractions provided by Kona for +developing your own Fault Proof Programs. + + +[kona]: https://github.com/op-rs/kona diff --git a/kona/docs/docs/pages/sdk/fpp-dev/io.mdx b/kona/docs/docs/pages/sdk/fpp-dev/io.mdx new file mode 100644 index 0000000000000..1b3628b3b010d --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/io.mdx @@ -0,0 +1 @@ +# IO diff --git a/kona/docs/docs/pages/sdk/fpp-dev/prologue.mdx b/kona/docs/docs/pages/sdk/fpp-dev/prologue.mdx new file mode 100644 index 0000000000000..2e92f399f1a40 --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/prologue.mdx @@ -0,0 +1,42 @@ +# Prologue + +The prologue stage of the program is commonly responsible for bootstrapping the program with inputs from an external +source, pulled in through the [Host-Client communication](/sdk/fpp-dev/env#host---client-communication) implementation. + +As a rule of thumb, the prologue implementation should be kept minimal, and should not do much more than establish +the inputs for the [execution phase](/sdk/fpp-dev/execution). + +## Example + +As an example, the prologue stage of the `kona-client` program runs through several steps: + +1. Pull in the boot information over the [Preimage Oracle ABI][preimage-specs], containing: + - The L1 head hash containing all data required to reproduce the L2 safe chain at the claimed block height. + - The latest finalized [L2 output root][l2-output-root]. + - The [L2 output root][l2-output-root] claim. + - The block number of the [L2 output root][l2-output-root] claim. + - The L2 chain ID. +1. Pull in the `RollupConfig` and `L2ChainConfig` corresponding to the passed L2 chain ID. +1. Validate these values. +1. Pass the boot information to the execution phase. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/fpp-dev/targets.mdx b/kona/docs/docs/pages/sdk/fpp-dev/targets.mdx new file mode 100644 index 0000000000000..82383499f7830 --- /dev/null +++ b/kona/docs/docs/pages/sdk/fpp-dev/targets.mdx @@ -0,0 +1,67 @@ +# Supported Targets + +Kona seeks to support all FPVM targets that LLVM and `rustc` can offer introductory support for. Below is a matrix of features that Kona offers +for each FPVM target: + +| Target | Build Pipeline | IO | malloc | +| ---------------------- | -------------- | --- | ------ | +| `cannon` & `cannon-rs` | ✅ | ✅ | ✅ | +| `asterisc` | ✅ | ✅ | ✅ | + +If there is a feature that you would like to see supported, please [open an issue][new-issue] or [consider contributing][contributing]! + +## Asterisc (RISC-V) + +Asterisc is based off of the `rv64gc` target architecture, which defines the following extensions: + +- `RV32I` support - 32 bit base instruction set + - `FENCE`, `ECALL`, `EBREAK` are hardwired to implement a minimal subset of systemcalls of the linux kernel + - Work in progress. All syscalls used by the Golang `risc64` runtime. +- `RV64I` support +- `RV32M`+`RV64M`: Multiplication support +- `RV32A`+`RV64A`: Atomics support +- `RV{32,64}{D,F,Q}`: no-op: No floating points support (since no IEEE754 determinism with rounding modes etc., nor worth the complexity) +- `Zifencei`: `FENCE.I` no-op: No need for `FENCE.I` +- `Zicsr`: no-op: some support for Control-and-status registers may come later though. +- `Ztso`: no-op: no need for Total Store Ordering +- other: revert with error code on unrecognized instructions + +`asterisc` supports a plethora of syscalls, documented [in the repository][asterisc-syscalls]. `kona` offers an interface for +programs to directly invoke a select few syscalls: + +1. `EXIT` - Terminate the process with the provided exit code. +1. `WRITE` - Write the passed buffer to the passed file descriptor. +1. `READ` - Read the specified number of bytes from the passed file descriptor. + +[asterisc-syscalls]: https://github.com/ethereum-optimism/asterisc/blob/master/docs/golang.md#linux-syscalls-used-by-go + +## Cannon (MIPS64r2) + +Cannon is based off of the `mips64r2` target architecture, specified in [MIPS® Architecture For Programmers Volume II-A: The MIPS64® Instruction Set Reference Manual](https://s3-eu-west-1.amazonaws.com/downloads-mips/documents/MIPS_Architecture_MIPS64_InstructionSet_%20AFP_P_MD00087_06.05.pdf) + +### Syscalls + +Syscalls supported by `cannon` can be found within the `cannon` specification [here][cannon-syscalls]. + +[cannon-syscalls]: https://specs.optimism.io/fault-proof/cannon-fault-proof-vm.html#syscalls + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/overview.mdx b/kona/docs/docs/pages/sdk/overview.mdx new file mode 100644 index 0000000000000..f62a6db7c8e7f --- /dev/null +++ b/kona/docs/docs/pages/sdk/overview.mdx @@ -0,0 +1,42 @@ +# Kona as a Library + +
+ CI + Codecov + License + Docs +
+ + +Kona is designed as a modular, library-first OP Stack implementation in Rust. This design philosophy allows developers to integrate Kona components into their applications and build custom solutions on top of the OP Stack. + +## Library Structure + +Kona is organized as a collection of focused crates that can be used independently or together: + +- **Protocol Libraries**: Core protocol logic and data structures +- **Node Components**: Modular node architecture for building custom rollup nodes +- **Proof System**: Fault proof generation and verification +- **Utilities**: Common utilities and helper functions + +## Key Benefits + +- **Modularity**: Use only the components you need +- **Performance**: Rust's zero-cost abstractions and memory safety +- **Extensibility**: Easy to extend and customize for specific use cases +- **Reliability**: Strong typing and comprehensive testing + +## Getting Started + +To use Kona as a library, add the relevant crates to your `Cargo.toml`: + +```toml +[dependencies] +kona-derive = "0.1" +kona-protocol = "0.1" +kona-node = "0.1" +``` + +## Examples + +See the [Examples](/sdk/examples/intro) section for practical usage examples and integration patterns. diff --git a/kona/docs/docs/pages/sdk/proof/custom-backend.mdx b/kona/docs/docs/pages/sdk/proof/custom-backend.mdx new file mode 100644 index 0000000000000..2ab904e7afbec --- /dev/null +++ b/kona/docs/docs/pages/sdk/proof/custom-backend.mdx @@ -0,0 +1,139 @@ +# Custom Backends + +## Understanding the OP Stack STF + +The OP Stack state transition is comprised of two primary components: + +- **The [derivation pipeline](https://specs.optimism.io/protocol/derivation.html)** (`kona-derive`) + - Responsible for deriving L2 chain state from the DA layer. +- **The [execution engine](https://specs.optimism.io/protocol/exec-engine.html#l2-execution-engine)** (`kona-executor`) + - Responsible for the execution of transactions and state commitments. + - Ensures correct application of derived L2 state. + +To prove the correctness of the state transition, Kona composes these two components: + +- It combines the derivation of the L2 chain with its execution in the same process. +- It pulls in necessary data from sources to complete the STF, verifiably unrolling the input commitments along the way. + +`kona-client` serves as an implementation of this process, capable of deriving and executing a single L2 block in a +verifiable manner. + +> 📖 Why just a single block by default? +> +> On the OP Stack, we employ an interactive bisection game that narrows in on the disagreed upon block -> block state +> transition before requiring a fault proof to be ran. Because of this, the default implementation only serves +> to derive and execute the single block that the participants of the bisection game landed on. + +## Backend Traits + +Covered in the [FPVM Backend](/sdk/proof/fpvm-backend) section of the book, `kona-client` ships with an implementation of +`kona-derive` and `kona-executor`'s data source traits which pull in data over the [PreimageOracle ABI][preimage-specs]. + +However, running `kona-client` on top of a different verifiable environment, i.e. a zkVM or TEE, is also possible +through custom implementations of these data source traits. + +[`op-succinct`](https://github.com/succinctlabs/op-succinct) is an excellent example of both a custom backend and a custom +program, implementing both `kona-derive` and `kona-executor`'s data source traits backed by [sp1_lib::io](https://docs.rs/sp1-lib/latest/sp1_lib/io/index.html) +in order to: + +1. Execute `kona-client` verbatim, proving a single block's derivation and execution on SP-1. +1. Derive and execute an entire [Span Batch](https://specs.optimism.io/protocol/delta/span-batches.html#span-batches) + worth of L2 blocks, using `kona-derive` and `kona-executor`. + +This section of the book outlines how you can do the same for a different platform. + +### Custom `kona-derive` sources + +Before getting started, we need to create custom implementations of the following traits: + +| Trait | Description | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| [`ChainProvider`](https://docs.rs/kona-derive/latest/kona_derive/trait.ChainProvider.html) | The `ChainProvider` trait describes the minimal interface for fetching data from L1 during L2 chain derivation. | +| [`L2ChainProvider`](https://docs.rs/kona-derive/latest/kona_derive/trait.L2ChainProvider.html) | The `ChainProvider` trait describes the minimal interface for fetching data from the safe L2 chain during L2 chain derivation. | +| [`BlobProvider`](https://docs.rs/kona-derive/latest/kona_derive/trait.BlobProvider.html) | The `BlobProvider` trait describes an interface for fetching EIP-4844 blobs from the L1 consensus layer during L2 chain derivation. | + +Once these are implemented, constructing the pipeline is as simple as passing in the data sources to the `PipelineBuilder`. Keep in mind the requirements for validation of incoming data, depending on your platform. For example, programs +targeting zkVMs must constrain that the incoming data is indeed valid, whereas fault proof programs can offload this validation to the on-chain implementation of the host. + +```rust +let chain_provider = ...; +let l2_chain_provider = ...; +let blob_provider = ...; +let l1_origin = ...; + +let cfg = Arc::new(RollupConfig::default()); +let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l2_chain_provider.clone(), + chain_provider.clone(), +); +let dap = EthereumDataSource::new( + chain_provider.clone(), + blob_provider, + cfg.as_ref() +); + +// Construct a new derivation pipeline. +let pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(dap) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(attributes) + .origin(l1_origin) + .build(); +``` + +From here, a custom derivation driver is needed to produce the desired execution payload(s). An example of this for +`kona-client` can be found in the [single proof implementation](https://github.com/op-rs/kona/blob/main/bin/client/src/single.rs#L98). + +### `kona-mpt` / `kona-executor` sources + +Before getting started, we need to create custom implementations of the following traits: + +| Trait | Description | +| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`TrieProvider`](https://docs.rs/kona-mpt/latest/kona_mpt/trait.TrieProvider.html) | The `TrieProvider` trait describes the interface for fetching trie node preimages and chain information while executing a payload on the L2 chain. | +| [`TrieDBHinter`](https://docs.rs/kona-mpt/latest/kona_mpt/trait.TrieHinter.html) | The `TrieDBHinter` trait describes the interface for requesting the host program to prepare trie proof preimages for the client's consumption. For targets with upfront witness generation, i.e. zkVMs, a no-op hinter is exported as [`NoopTrieHinter`](https://docs.rs/kona-mpt/latest/kona_mpt/struct.NoopTrieHinter.html). | + +Once we have those, the `StatelessL2BlockExecutor` can be constructed like so: + +```rust +let cfg = RollupConfig::default(); +let provider = ...; +let hinter = ...; + +let executor = StatelessL2BlockExecutor::builder(&cfg, provider, hinter) + .with_parent_header(...) + .build(); + +let header = executor.execute_payload(...).expect("Failed execution"); +``` + +### Bringing it Together + +Once your custom backend traits for both `kona-derive` and `kona-executor` have been implemented, +your final binary may look something like [that of `kona-client`'s](https://github.com/op-rs/kona/blob/main/bin/client/src/kona.rs). +Alternatively, if you're looking to prove a wider range of blocks, [`op-succinct`'s `range` program](https://github.com/succinctlabs/op-succinct/tree/main/programs/range) +offers a good example of running the pipeline and executor across a string of contiguous blocks. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/proof/exec-ext.mdx b/kona/docs/docs/pages/sdk/proof/exec-ext.mdx new file mode 100644 index 0000000000000..f806cf127915e --- /dev/null +++ b/kona/docs/docs/pages/sdk/proof/exec-ext.mdx @@ -0,0 +1,211 @@ +# `kona-executor` Extensions + +The `kona-executor` crate offers a to-spec, stateless implementation of the OP Stack STF. However, due to the +power of [`alloy-evm`][alloy-evm]'s factory abstractions, the logic of the STF can be easily customized. + +To customize the EVM behavior, for example to add a custom precompile, modify the behavior of an EVM opcode, +or change the fee handling, you can implement a custom [`EvmFactory`][evm-factory]. The factory is responsible +for creating EVM instances with your desired customizations. + +## Example - Custom Precompile + +```rust +use alloy_evm::{Database, EvmEnv, EvmFactory}; +use alloy_op_evm::OpEvm; +use alloy_primitives::{Address, Bytes, u64_to_address}; +use kona_executor::StatelessL2Builder; +use kona_genesis::RollupConfig; +use op_revm::{ + DefaultOp, OpContext, OpEvm as RevmOpEvm, OpHaltReason, OpSpecId, OpTransaction, + OpTransactionError, +}; +use revm::{ + Context, Inspector, + context::{Evm as RevmEvm, FrameStack, TxEnv, result::EVMError}, + handler::instructions::EthInstructions, + inspector::NoOpInspector, + precompile::{PrecompileResult, PrecompileOutput, Precompiles}, +}; + +const MY_PRECOMPILE_ADDRESS: Address = u64_to_address(0xFF); + +fn my_precompile(input: &Bytes, gas_limit: u64) -> PrecompileResult { + Ok(PrecompileOutput::new(50, "hello, world!".as_bytes().into())) +} + +#[derive(Debug, Clone)] +pub struct CustomEvmFactory; + +impl EvmFactory for CustomEvmFactory { + type Evm>> = OpEvm; + type Context = OpContext; + type Tx = OpTransaction; + type Error = + EVMError; + type HaltReason = OpHaltReason; + type Spec = OpSpecId; + type Precompiles = CustomPrecompiles; + + fn create_evm( + &self, + db: DB, + input: EvmEnv, + ) -> Self::Evm { + let spec_id = *input.spec_id(); + let ctx = Context::op().with_db(db).with_block(input.block_env).with_cfg(input.cfg_env); + + // Create custom precompiles with our added precompile + let mut precompiles = op_revm::precompiles::granite(); + precompiles.insert(MY_PRECOMPILE_ADDRESS, my_precompile); + let custom_precompiles = CustomPrecompiles { precompiles }; + + let revm_evm = RevmOpEvm(RevmEvm { + ctx, + inspector: NoOpInspector {}, + instruction: EthInstructions::new_mainnet(), + precompiles: custom_precompiles, + frame_stack: FrameStack::new(), + }); + + OpEvm::new(revm_evm, false) + } + + fn create_evm_with_inspector>>( + &self, + db: DB, + input: EvmEnv, + inspector: I, + ) -> Self::Evm { + let spec_id = *input.spec_id(); + let ctx = Context::op().with_db(db).with_block(input.block_env).with_cfg(input.cfg_env); + + // Create custom precompiles with our added precompile + let mut precompiles = op_revm::precompiles::granite(); + precompiles.insert(MY_PRECOMPILE_ADDRESS, my_precompile); + let custom_precompiles = CustomPrecompiles { precompiles }; + + let revm_evm = RevmOpEvm(RevmEvm { + ctx, + inspector, + instruction: EthInstructions::new_mainnet(), + precompiles: custom_precompiles, + frame_stack: FrameStack::new(), + }); + + OpEvm::new(revm_evm, true) + } +} + +// Custom precompiles wrapper +#[derive(Debug)] +pub struct CustomPrecompiles { + precompiles: Precompiles, +} + +impl revm::handler::PrecompileProvider for CustomPrecompiles +where + CTX: revm::context::ContextTr>, +{ + type Output = revm::interpreter::InterpreterResult; + + fn set_spec(&mut self, spec: OpSpecId) -> bool { + // Update precompiles based on spec if needed + false + } + + fn run( + &mut self, + _context: &mut CTX, + address: &Address, + inputs: &revm::interpreter::InputsImpl, + _is_static: bool, + gas_limit: u64, + ) -> Result, String> { + use revm::interpreter::{Gas, InstructionResult, InterpreterResult}; + + let input = match &inputs.input { + revm::interpreter::CallInput::Bytes(bytes) => bytes.clone(), + revm::interpreter::CallInput::SharedBuffer(range) => { + // Handle shared buffer case - simplified for example + Bytes::new() + } + }; + + if let Some(precompile) = self.precompiles.get(address) { + let result = (*precompile)(&input, gas_limit); + match result { + Ok(output) => Ok(Some(InterpreterResult { + result: InstructionResult::Return, + gas: Gas::new(gas_limit - output.gas_used), + output: output.bytes, + })), + Err(_) => Ok(Some(InterpreterResult { + result: InstructionResult::PrecompileError, + gas: Gas::new(0), + output: Bytes::new(), + })), + } + } else { + Ok(None) + } + } +} + +// - snip - + +let cfg = RollupConfig::default(); +let provider = ...; +let hinter = ...; +let parent_header = ...; + +let executor = StatelessL2Builder::new( + &cfg, + CustomEvmFactory, + provider, + hinter, + parent_header, +); +``` + +## Migration from the old API + +Prior to the integration of `alloy-evm`, `kona-executor` used a builder pattern with `StatelessL2BlockExecutorBuilder::with_handle_register` for EVM customization. The new approach using `EvmFactory` provides better composability and aligns with the broader Alloy ecosystem. + +### Key Changes: + +1. **Direct construction**: Use `StatelessL2Builder::new()` instead of a builder pattern +2. **Factory-based customization**: Implement `EvmFactory` instead of registering handlers +3. **Type safety**: The factory approach provides better compile-time guarantees +4. **Ecosystem alignment**: Leverages the standard `alloy-evm` interfaces + +### Benefits: + +- **Composability**: Custom EVM factories can be easily shared and reused +- **Flexibility**: Full control over EVM creation and configuration +- **Performance**: Reduced indirection compared to the handler approach +- **Maintainability**: Cleaner separation of concerns between execution and customization + +For more complex customizations involving multiple precompiles, custom opcodes, or specialized execution logic, refer to the [`FpvmOpEvmFactory`](https://github.com/op-rs/kona/blob/main/bin/client/src/fpvm_evm/factory.rs) implementation in the `kona-client` for a comprehensive example. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm +[alloy-evm]: https://github.com/alloy-rs/evm +[evm-factory]: https://docs.rs/alloy-evm/latest/alloy_evm/eth/struct.EthEvmFactory.html + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/proof/fpvm-backend.mdx b/kona/docs/docs/pages/sdk/proof/fpvm-backend.mdx new file mode 100644 index 0000000000000..dab1ecfc4adc3 --- /dev/null +++ b/kona/docs/docs/pages/sdk/proof/fpvm-backend.mdx @@ -0,0 +1,159 @@ +# FPVM Backend + +> 📖 Before reading this section of the book, it is advised to read the [Fault Proof Program Environment](/sdk/fpp-dev/env) +> section to familiarize yourself with the PreimageOracle IO pattern. + +Kona is effectively split into three parts: + +- OP Stack state transition logic (`kona-derive`, `kona-executor`, `kona-mpt`) +- OP Stack state transition proof SDK (`kona-preimage`, `kona-proof`) +- [Fault Proof VM](/glossary#fault-proof-vm) IO and utilities + (`kona-std-fpvm`, `kona-std-fpvm-proc`) + +This section of the book focuses on the usage of `kona-std-fpvm` and `kona-preimage` to facilitate communication between host and client +for programs running on top of the [FPVM targets](/sdk/fpp-dev/env). + +## Host and Client Communication API + +The FPVM system API is built on several layers. In this document, we'll cover these layers, from lowest-level to +highest-level API. + +### `kona-std-fpvm` + +`kona-std-fpvm` implements raw syscall dispatch, a default global memory allocator, and a blocking async runtime. +`kona-std-fpvm` relies on a minimal linux backend to function, supporting only the syscalls required to implement the +[PreimageOracle ABI][preimage-specs] (`read`, `write`, `exit_group`). + +These syscalls are exposed to the user through the `io` module directly, with each supported platform implementing the +[`BasicKernelInterface`](https://docs.rs/kona-std-fpvm/latest/kona_std_fpvm/trait.BasicKernelInterface.html) trait. + +To directly dispatch these syscalls, the [`io`](https://docs.rs/kona-std-fpvm/latest/kona_std_fpvm/io/index.html) module +exposes a safe API: + +```rust +use kona_std_fpvm::{io, FileDescriptor}; + +// Print to `stdout`. Infallible, will panic if dispatch fails. +io::print("Hello, world!"); + +// Print to `stderr`. Infallible, will panic if dispatch fails. +io::print_err("Goodbye, world!"); + +// Read from or write to a specified file descriptor. Returns a result with the +// return value or syscall errno. +let _ = io::write(FileDescriptor::StdOut, "Hello, world!".as_bytes()); +let mut buf = Vec::with_capacity(8); +let _ = io::read(FileDescriptor::StdIn, buf.as_mut_slice()); + +// Exit the program with a specified exit code. +io::exit(0); +``` + +With this library, you can implement a custom communication protocol between the host and client, or extend the existing +[PreimageOracle ABI][preimage-specs]. However, for most developers, we recommend sticking with `kona-preimage` +when developing programs that target the [FPVMs](/sdk/fpp-dev/env), barring needs like printing directly to +`stdout`. + +### `kona-preimage` + +`kona-preimage` is an implementation of the [PreimageOracle ABI][preimage-specs]. This crate enables synchronous +communication between the host and client program, described in +[Host - Client Communication](/sdk/fpp-dev/env#host---client-communication) in the FPP Dev environment section of the +book. + +The crate is built around the [`Channel`](https://docs.rs/kona-preimage/latest/kona_preimage/trait.Channel.html) trait, +which serves as a single end of a bidirectional pipe (see: [`pipe` manpage](https://man7.org/linux/man-pages/man2/pipe.2.html)). + +Through this handle, the higher-level constructs can read and write data to the counterparty holding on to the other end +of the channel, following the protocol below: + +
+ +```mermaid +sequenceDiagram + Client->>+Host: Hint preimage (no-op on-chain / read-only mode) + Host-->>-Client: Hint acknowledgement + Client-->>+Host: Preimage Request + Host-->>Host: Prepare Preimage + Host-->>-Client: Preimage Data +``` + +
+ +The interfaces of each part of the above protocol are described by the following traits: + +- [`PreimageOracleClient`](https://docs.rs/kona-preimage/latest/kona_preimage/trait.PreimageOracleClient.html) + - To-spec implementation: [`OracleReader`](https://docs.rs/kona-preimage/latest/kona_preimage/struct.OracleReader.html) +- [`HintWriterClient`](https://docs.rs/kona-preimage/latest/kona_preimage/trait.HintWriterClient.html) + - To-spec implementation: [`HintWriter`](https://docs.rs/kona-preimage/latest/kona_preimage/struct.HintWriter.html) +- [`PreimageOracleServer`](https://docs.rs/kona-preimage/latest/kona_preimage/trait.PreimageOracleServer.html) + - To-spec implementation: [`OracleServer`](https://docs.rs/kona-preimage/latest/kona_preimage/struct.OracleServer.html) +- [`HintReaderServer`](https://docs.rs/kona-preimage/latest/kona_preimage/trait.HintReaderServer.html) + - To-spec implementation: [`HintReader`](https://docs.rs/kona-preimage/latest/kona_preimage/struct.HintReader.html) + +Each of these traits, however, can be re-implemented to redefine the communication protocol between the host and client if the needs +of the consumer are not covered by the to-[spec][preimage-specs] implementations. + +### `kona-proof` - Oracle-backed sources (example) + +Finally, in `kona-proof`, implementations of data source traits from `kona-derive` and `kona-executor` are provided +to pull in untyped data from the host by `PreimageKey`. These data source traits are covered in more detail within +the [Custom Backend](/sdk/proof/custom-backend) section, but we'll quickly gloss over them here to build intuition. + +Let's take, for example, [`OracleL1ChainProvider`](https://github.com/op-rs/kona/blob/40a8d7ec3def4a1eeb26492a1e4338d8b032e428/bin/client/src/l1/chain_provider.rs#L16-L23). +The [`ChainProvider`](https://docs.rs/kona-derive/latest/kona_derive/trait.ChainProvider.html) trait in `kona-derive` +defines a simple interface for fetching information about the L1 chain. In the `OracleL1ChainProvider`, this information +is pulled in over the [PreimageOracle ABI][preimage-specs]. There are many other examples of these data source traits, +namely the `L2ChainProvider`, `BlobProvider`, `TrieProvider`, and `TrieHinter`, which enable the creation of different +data-source backends. + +As an example, let's look at `OracleL1ChainProvider::header_by_hash`, built on top of the `CommsClient` trait, which +is a composition trait of the `PreimageOracleClient + HintReaderServer` traits outlined above. + +```rust +#[async_trait] +impl ChainProvider for OracleL1ChainProvider { + type Error = anyhow::Error; + + async fn header_by_hash(&mut self, hash: B256) -> Result
{ + // Send a hint for the block header. + self.oracle.write(&HintType::L1BlockHeader.encode_with(&[hash.as_ref()])).await?; + + // Fetch the header RLP from the oracle. + let header_rlp = + self.oracle.get(PreimageKey::new(*hash, PreimageKeyType::Keccak256)).await?; + + // Decode the header RLP into a Header. + Header::decode(&mut header_rlp.as_slice()) + .map_err(|e| anyhow!("Failed to decode header RLP: {e}")) + } + + // - snip - +} +``` + +In `header_by_hash`, we use the inner `HintWriter` to send a hint to the host to prepare the block hash preimage. +Then, once we've received an acknowledgement from the host that the preimage has been prepared, we reach out for +the RLP (which is the preimage of the hash). After the RLP is received, we decode the `Header` type, and return +it to the user. + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/proof/intro.mdx b/kona/docs/docs/pages/sdk/proof/intro.mdx new file mode 100644 index 0000000000000..17529bc862b42 --- /dev/null +++ b/kona/docs/docs/pages/sdk/proof/intro.mdx @@ -0,0 +1,61 @@ +# Kona Proof SDK + +Welcome to the Kona Proof SDK, a powerful set of libraries designed +from first principles to build proofs with the OP Stack STF on top +of the OP Stack's FPVMs and other verifiable backends like [SP-1][sp-1], +[Risc0][rzero], [Intel TDX][tdx], and [AMD SEV-SNP][sev-snp]. At its +core, Kona is built on the principles of modularity, extensibility, +and developer empowerment. + +## A Foundation of Flexibility + +The kona repository is more than a fault proof program for the OP Stack +— it's an ecosystem of interoperable components, each crafted with +reusability and extensibility as primary goals. While we provide +[Fault Proof VM](/glossary#fault-proof-vm) and "online" backends +for key components like `kona-derive` and `kona-executor`, the true +power of `kona` lies in its adaptability. + +## Extend Without Forking + +One of Kona's standout features is its ability to support custom +features and data sources without requiring you to fork the entire +project. Through careful use of Rust's powerful trait system and +abstract interfaces, we've created a framework that allows you to +plug in your own features and ideas seamlessly. + +## What You'll Learn + +In this section of the developer book, we'll dive deep into the Kona SDK, covering: +* **Building on the FPVM Backend**: Learn how to leverage the Fault Proof VM tooling to create your own fault proof programs. +* **Creating Custom Backends**: Discover the process of designing and implementing your own backend to run `kona-client` or a variation of it on different targets. +* **Extending Core Components**: Explore techniques for creating new constructs that integrate smoothly with crates like `kona-derive` and `kona-executor`. + +Whether you're looking to use Kona as-is, extend its functionality, or create entirely new programs based on its libraries, +this guide is intended to provide you with the knowledge and tools you need to succeed. + +[sp-1]: https://github.com/succinctlabs/sp1 +[rzero]: https://github.com/risc0/risc0 +[tdx]: https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/documentation.html +[sev-snp]: https://www.amd.com/en/developer/sev.html + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/protocol/derive/intro.mdx b/kona/docs/docs/pages/sdk/protocol/derive/intro.mdx new file mode 100644 index 0000000000000..16178bcf9990b --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/derive/intro.mdx @@ -0,0 +1,312 @@ +import { Callout } from 'vocs/components' + +# The `kona-derive` Derivation Pipeline + +[`kona-derive`][kd] defines an entirely trait-abstracted, `no_std` derivation +pipeline for the OP Stack. It can be used through the [`Pipeline`][p] trait, +which is implemented for the concrete [`DerivationPipeline`][dp] object. + +This document dives into the inner workings of the derivation pipeline, its +stages, and how to build and interface with Kona's pipeline. Other documents +in this section will provide a comprehensive overview of Derivation Pipeline +extensibility including trait-abstracted providers, custom stages, signaling, +and hardfork activation including multiplexed stages. + +- [Swapping out a stage](/sdk/protocol/derive/stages) +- [Defining a custom Provider](/sdk/protocol/derive/providers) +- [Extending Pipeline Signals](/sdk/protocol/derive/signaling) +- [Implementing Hardfork Activations](/sdk/protocol/hardforks) + + +## What is a Derivation Pipeline? + +Simply put, an OP Stack Derivation Pipeline transforms data on L1 into L2 +payload attributes that can be executed to produce the canonical L2 block. + +Within a pipeline, there are a set of stages that break up this transformation +further. When composed, these stages operate over the input data, sequentially +producing payload attributes. + +In [`kona-derive`][kd], stages are architected using composition - each sequential +stage owns the previous one, forming a stack. For example, let's define stage A +as the first stage, accepting raw L1 input data, and stage C produces the pipeline +output - payload attributes. Stage B "owns" stage A, and stage C then owns stage B. +Using this example, the [`DerivationPipeline`][dp] type in [`kona-derive`][kd] only +holds stage C, since ownership of the other stages is nested within stage C. + + +In a future architecture of the derivation pipeline, stages could be made +standalone such that communication between stages happens through channels. +In a multi-threaded, non-fault-proof environment, these stages can then +run in parallel since stage ownership is decoupled. + + + +## Kona's Derivation Pipeline + +The top-level stage in [`kona-derive`][kd] that produces +[`OpAttributesWithParent`][attributes] is the [`AttributesQueue`][attributes-queue]. + +Post-Holocene (the Holocene hardfork), the following stages are composed by +the [`DerivationPipeline`][dp]. +- [`AttributesQueue`][attributes-queue] + - [`BatchProvider`][batch-provider] + - [`BatchStream`][batch-stream] + - [`ChannelReader`][channel-reader] + - [`ChannelProvider`][channel-provider] + - [`FrameQueue`][frame-queue] + - [`L1Retrieval`][retrieval] + - [`IndexedTraversal` or `PollingTraversal`][traversal] + +Notice, from top to bottom, each stage owns the stage nested below it. +Where the [`IndexedTraversal` or `PollingTraversal`][traversal] stage iterates over L1 data, the +[`AttributesQueue`][attributes-queue] stage produces +[`OpAttributesWithParent`][attributes], creating a function that transforms +L1 data into payload attributes. + + +## The [`Pipeline`][p] interface + +Now that we've broken down the stages inside the [`DerivationPipeline`][dp] +type, let's move up another level to break down how the [`DerivationPipeline`][dp] +type functions itself. At the highest level, [`kona-derive`][kd] defines the +interface for working with the pipeline through the [`Pipeline`][p] trait. + +[`Pipeline`][p] provides two core methods. +- `peek() -> Option<&OpAttributesWithParent>` +- `async step() -> StepResult` + +Functionally, a pipeline can be "stepped" on, which attempts to derive +payload attributes from input data. Steps do not guarantee that payload attributes +are produced, they only attempt to advance the stages within the pipeline. + +The `peek()` method provides a way to check if attributes are prepared. +Beyond `peek()` returning `Option::Some(&OpAttributesWithParent)`, the [`Pipeline`][p] +extends the [Iterator][iterator] trait, providing a way to consume the generated payload +attributes. + + +## Constructing a Derivation Pipeline + +[`kona-derive`][kd] provides a [`PipelineBuilder`][builder] to abstract the complexity +of generics away from the downstream consumers. Below we provide an example for using +the [`PipelineBuilder`][builder] to instantiate a [`DerivationPipeline`][dp]. + +```rust,ignore +// Imports +use std::sync::Arc; +use kona_protocol::BlockInfo; +use kona_genesis::RollupConfig; +use kona_providers_alloy::*; + +// Use a default rollup config. +let rollup_config = Arc::new(RollupConfig::default()); + +// Providers are instantiated to with localhost urls (`127.0.0.1`) +let chain_provider = + AlloyChainProvider::new_http("http://127.0.0.1:8545".try_into().unwrap()); +let l2_chain_provider = AlloyL2ChainProvider::new_http( + "http://127.0.0.1:9545".try_into().unwrap(), + rollup_config.clone(), +); +let beacon_client = OnlineBeaconClient::new_http("http://127.0.0.1:5555".into()); +let blob_provider = OnlineBlobProvider::new(beacon_client, None, None); +let blob_provider = OnlineBlobProviderWithFallback::new(blob_provider, None); +let dap_source = + EthereumDataSource::new(chain_provider.clone(), blob_provider, &rollup_config); +let builder = StatefulAttributesBuilder::new( + rollup_config.clone(), + l2_chain_provider.clone(), + chain_provider.clone(), +); + +// This is the starting L1 block for the pipeline. +// +// To get the starting L1 block for a given L2 block, +// use the `AlloyL2ChainProvider::l2_block_info_by_number` +// method to get the `L2BlockInfo.l1_origin`. This l1_origin +// is the origin that can be passed here. +let origin = BlockInfo::default(); + +// Build the pipeline using the `PipelineBuilder`. +// Alternatively, use the `new_online_pipeline` helper +// method provided by the `kona-derive-alloy` crate. +let pipeline = PipelineBuilder::new() + .rollup_config(rollup_config.clone()) + .dap_source(dap_source) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(builder) + .origin(origin) + .build(); + +assert_eq!(pipeline.rollup_config, rollup_config); +assert_eq!(pipeline.origin(), Some(origin)); +``` + + +## Producing Payload Attributes + +Since the [`Pipeline`][p] trait extends the [`Iterator`][iterator] trait, +producing [`OpAttributesWithParent`][attributes] is as simple as calling +[`Iterator::next()`][next] method on the [`DerivationPipeline`][dp]. + +Extending the example from above, producing the attributes is shown below. + +```rust +// Import the iterator trait to show where `.next` is sourced. +use core::iter::Iterator; + +// ... +// example from above constructing the pipeline +// ... + +let attributes = pipeline.next(); + +// Since we haven't stepped on the pipeline, +// there shouldn't be any payload attributes prepared. +assert!(attributes.is_none()); +``` + +As demonstrated, the pipeline won't have any payload attributes +without having been "stepped" on. Naively, we can continuously +step on the pipeline until attributes are ready, and then consume them. + +```rust +// Import the iterator trait to show where `.next` is sourced. +use core::iter::Iterator; + +// ... +// example from constructing the pipeline +// ... + +// Continuously step on the pipeline until attributes are prepared. +let l2_safe_head = L2BlockInfo::default(); +loop { + if matches!(pipeline.step(l2_safe_head).await, StepResult::PreparedAttributes) { + // The pipeline has successfully prepared payload attributes, break the loop. + break; + } +} + +// Since the loop is only broken once attributes are prepared, +// this must be `Option::Some`. +let attributes = pipeline.next().expect("Must contain payload attributes"); + +// The parent of the prepared payload attributes should be +// the l2 safe head that we "stepped on". +assert_eq!(attributes.parent, l2_safe_head); +``` + +Importantly, the above is not sufficient logic to produce payload attributes and drive +the derivation pipeline. There are multiple different `StepResult`s to handle when +stepping on the pipeline, including advancing the origin, re-orgs, and pipeline resets. +In the next section, pipeline resets are outlined. + +For an up-to-date driver that runs the derivation pipeline as part of the fault proof +program, reference kona's [client driver][driver]. + + +## Resets + +When stepping on the [`DerivationPipeline`][dp] produces a reset error, the driver +of the pipeline must perform a reset on the pipeline. This is done by sending a "signal" +through the [`DerivationPipeline`][dp]. Below demonstrates this. + +```rust +// Import the iterator trait to show where `.next` is sourced. +use core::iter::Iterator; + +// ... +// example from constructing the pipeline +// ... + +// Continuously step on the pipeline until attributes are prepared. +let l2_safe_head = L2BlockInfo::default(); +loop { + match pipeline.step(l2_safe_head).await { + StepResult::StepFailed(e) | StepResult::OriginAdvanceErr(e) => { + match e { + PipelineErrorKind::Reset(e) => { + // Get the system config from the provider. + let system_config = l2_chain_provider + .system_config_by_number( + l2_safe_head.block_info.number, + rollup_config.clone(), + ) + .await?; + // Reset the pipeline to the initial L2 safe head and L1 origin. + self.pipeline + .signal( + ResetSignal { + l2_safe_head: l2_safe_head, + l1_origin: pipeline + .origin() + .ok_or_else(|| anyhow!("Missing L1 origin"))?, + system_config: Some(system_config), + } + .signal(), + ) + .await?; + // ... + } + _ => { /* Handling left to the driver */ } + } + } + _ => { /* Handling left to the driver */ } + } +} +``` + + +## Learn More + +[`kona-derive`][kd] is one implementation of the OP Stack derivation pipeline. + +To learn more, it is highly encouraged to read the ["first" derivation pipeline][op-dp] +written in [golang][go]. It is often colloquially referred to as the "reference" +implementation and provides the basis for how much of Kona's derivation pipeline +was built. + + +## Provenance + +> The lore do be bountiful. +> +> - Bard XVIII of the Logic Gates + +The kona project spawned out of the need to build a secondary fault proof for the OP Stack. +Initially, we sought to re-use [magi][magi]'s derivation pipeline, but the ethereum-rust +ecosystem moves quickly and [magi][magi] was behind by a generation of types - using +[ethers-rs] instead of new [alloy][alloy] types. Additionally, [magi][magi]'s derivation +pipeline was not `no_std` compatible - a hard requirement for running a rust fault proof +program on top of the RISCV or MIPS ISAs. + +So, [@clabby][clabby] and [@refcell][refcell] stood up [kona][kona] in a few months. + + +[driver]: https://docs.rs/kona-driver/latest/kona_driver/struct.Driver.html +[next]: https://doc.rust-lang.org/nightly/core/iter/trait.Iterator.html#tymethod.next +[builder]: https://docs.rs/kona-derive/latest/kona_derive/struct.PipelineBuilder.html +[alloy]: https://github.com/alloy-rs/alloy +[ethers-rs]: https://github.com/gakonst/ethers-rs +[kona]: https://github.com/op-rs/kona +[clabby]: https://github.com/clabby +[refcell]: https://github.com/refcell +[go]: https://go.dev/ +[magi]: https://github.com/a16z/magi +[kd]: https://crates.io/crates/kona-derive +[iterator]: https://doc.rust-lang.org/nightly/core/iter/trait.Iterator.html +[p]: https://docs.rs/kona-derive/latest/kona_derive/trait.Pipeline.html +[op-dp]: https://github.com/ethereum-optimism/optimism/tree/develop/op-node/rollup/derive +[dp]: https://docs.rs/kona-derive/latest/kona_derive/struct.DerivationPipeline.html +[attributes]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.OpAttributesWithParent.html + +[attributes-queue]: https://docs.rs/kona-derive/latest/kona_derive/struct.AttributesQueue.html +[batch-provider]: https://docs.rs/kona-derive/latest/kona_derive/struct.BatchProvider.html +[batch-stream]: https://docs.rs/kona-derive/latest/kona_derive/struct.BatchStream.html +[channel-reader]: https://docs.rs/kona-derive/latest/kona_derive/struct.ChannelReader.html +[channel-provider]: https://docs.rs/kona-derive/latest/kona_derive/struct.ChannelProvider.html +[frame-queue]: https://docs.rs/kona-derive/latest/kona_derive/struct.FrameQueue.html +[retrieval]: https://docs.rs/kona-derive/latest/kona_derive/struct.L1Retrieval.html +[traversal]: https://docs.rs/kona-derive/latest/kona_derive/struct.IndexedTraversal.html diff --git a/kona/docs/docs/pages/sdk/protocol/derive/providers.mdx b/kona/docs/docs/pages/sdk/protocol/derive/providers.mdx new file mode 100644 index 0000000000000..05999248f3e71 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/derive/providers.mdx @@ -0,0 +1,101 @@ +# Trait-abstracted Providers + +Kona's derivation pipeline pulls in data from sources that are trait +abstracted so the pipeline can be generic over various data sources. +Note, "data sources" is used interchangeably with "trait-abstracted +providers" for the purpose of this document. + +The key traits required for the pipeline are the following. + +- [`ChainProvider`][chain-provider] +- [`L2ChainProvider`][l2-chain-provider] +- [`DataAvailabilityProvider`][dap] + +The [`kona-derive-alloy`][kda] crate provides `std` implementations +of these traits using [Alloy][alloy]'s `reqwest`-backed providers. + +## Provider Usage + +Although trait-abstracted Providers are used throughout the pipeline and +its stages, the [`PipelineBuilder`][builder] makes constructing the pipeline +generic over the providers. An example is shown below, where the three +required trait implementations are the providers stubbed with `todo!()`. + +```rust +use std::sync::Arc; +use kona_genesis::RollupConfig; +use kona_derive::PipelineBuilder; +use kona_derive::StatefulAttributesBuilder; + +// The rollup config for your chain. +let cfg = Arc::new(RollupConfig::default()); + +// Must implement the `ChainProvider` trait. +let chain_provider = todo!("your chain provider"); + +// Must implement the `L2ChainProvider` trait. +let l2_chain_provider = todo!("your l2 chain provider"); + +// Must implement the `DataAvailabilityProvider` trait. +let dap = todo!("your data availability provider"); + +// Generic over the providers. +let attributes = StatefulAttributesBuilder::new( + cfg.clone(), + l2_chain_provider.clone(), + chain_provider.clone(), +); + +// Construct a new derivation pipeline. +let pipeline = PipelineBuilder::new() + .rollup_config(cfg) + .dap_source(dap) + .l2_chain_provider(l2_chain_provider) + .chain_provider(chain_provider) + .builder(attributes) + .origin(BlockInfo::default()) + .build(); +``` + +## Implementing a Custom Data Availability Provider + +> Notice +> +> The only required method for the [`DataAvailabilityProvider`][dap] +> trait is the [`next`][next] method. + +```rust +use async_trait::async_trait; +use alloy_primitives::Bytes; +use kona_protocol::BlockInfo; +use kona_derive::DataAvailabilityProvider; +use kona_derive::PipelineResult; + +/// ExampleAvail +/// +/// An example implementation of the `DataAvailabilityProvider` trait. +#[derive(Debug)] +pub struct ExampleAvail { + // Place your data in here +} + +#[async_trait] +impl DataAvailabilityProvider for ExampleAvail { + type Item = Bytes; + + async fn next(&self, block_ref: &BlockInfo) -> PipelineResult { + todo!("return an AsyncIterator implementation here") + } +} +``` + + + +[dap]: https://docs.rs/kona-derive/latest/kona_derive/trait.DataAvailabilityProvider.html +[next]: https://docs.rs/kona-derive/latest/kona_derive/trait.DataAvailabilityProvider.html#tymethod.next +[builder]: https://docs.rs/kona-derive/latest/kona_derive/struct.PipelineBuilder.html +[alloy]: https://github.com/alloy-rs/alloy +[kda]: https://crates.io/crates/kona-derive-alloy +[chain-provider]: https://docs.rs/kona-derive/latest/kona_derive/trait.ChainProvider.html +[l2-chain-provider]: https://docs.rs/kona-derive/latest/kona_derive/trait.L2ChainProvider.html +[dap]: https://docs.rs/kona-derive/latest/kona_derive/trait.DataAvailabilityProvider.html diff --git a/kona/docs/docs/pages/sdk/protocol/derive/signaling.mdx b/kona/docs/docs/pages/sdk/protocol/derive/signaling.mdx new file mode 100644 index 0000000000000..9376d775e951d --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/derive/signaling.mdx @@ -0,0 +1,155 @@ +# Signals + +Understanding signals first require a more in-depth review of the result +returned by stepping on the derivation pipeline. + + +## The [`StepResult`][step-result] + +As briefly outlined in the [intro](./intro.mdx), stepping on the derivation +pipeline returns a [`StepResult`][step-result]. Step results provide a +an extensible way for pipeline stages to signal different results to the +pipeline driver. The variants of [`StepResult`][step-result] and what they +signal include the following. + +- `StepResult::PreparedAttributes` - signals that payload attributes are + ready to be consumed by the pipeline driver. +- `StepResult::AdvancedOrigin` - signals that the pipeline has derived all + payload attributes for the given L1 block, and the origin of the pipeline + was advanced to the next canonical L1 block. +- `StepResult::OriginAdvanceErr(_)` - The driver failed to advance the + origin of pipeline. +- `StepResult::StepFailed(_)` - The step failed. + +No action is needed when the prepared attributes step result is received. +The pipeline driver may chose to consume the payload attributes how it +wishes. Likewise, `StepResult::AdvancedOrigin` simply notifies the driver +that the pipeline advanced its origin - the driver may continue stepping +on the pipeline. Now, it becomes more involved with the remaining two +variants of [`StepResult`][step-result]. + +When either `StepResult::OriginAdvanceErr(_)` or `StepResult::StepFailed(_)` +are received, the pipeline driver needs to introspect the error within these +variants. Depending on the [`PipelineErrorKind`][error-kind], the driver may +need to send a "signal" down through the pipeline. + +The next section goes over pipeline signals by looking at the variants of +the [`PipelineErrorKind`][error-kind] and the driver's response. + + +## [`PipelineErrorKind`][error-kind] + +There are three variants of the [`PipelineErrorKind`][error-kind], each +groups the inner error based on severity (or how they should be handled). + +- `PipelineErrorKind::Temporary` - This is an error that's expected, and + is temporary. For example, not all channel data has been posted to L1 + so the pipeline doesn't have enough data yet to continue deriving + payload attributes. +- `PipelineErrorKind::Critical` - This is an unexpected error that breaks + the derivation pipeline. It should cause the driver to error since this + is behavior that is breaking the derivation of payload attributes. +- `PipelineErrorKind::Reset` - When this is received, it effectively + requests that the driver perform some action on the pipeline. Kona + uses message passing so the driver can send a [`Signal`][signal] down + the pipeline with whatever action that needs to be performed. By + allowing both the driver and individual pipeline stages to define their + own behaviour around signals, they become very extensible. More on this + in [a later section](#extending-the-signal-type). + + +## The [`Signal`][signal] Type + +Continuing from the [`PipelineErrorKind`][error-kind], when the driver +receives a `PipelineErrorKind::Reset`, it needs to send a signal down +through the pipeline. + +Prior to the Holocene hardfork, the pipeline only needed to be reset +when the reset pipeline error was received. Holocene activation rules +changed this to require Holocene-specific activation logic internal to +the pipeline stages. The way kona's driver handles this activation is +by sending a new `ActivationSignal` if the `PipelineErrorKind::Reset` +type is a `ResetError::HoloceneActivation`. Otherwise, it will send the +`ResetSignal`. + +The last of the three [`Signal`][signal] variants is the `FlushChannel` +signal. Similar to `ActivationSignal`, the flush channel signal is logic +introduced post-Holocene. When the driver fails to execute payload +attributes and Holocene is active, a `FlushChannel` signal needs to +forwards invalidate the associated batch and channel, and the block +is replaced with a deposit-only block. + + +## Extending the Signal Type + +To extend the [`Signal`][signal] type, all that is needed is to introduce +a new variant to the [`Signal`][signal] enum. + +Once the variant is added, the segments where signals are handled need to +be updated. Anywhere the [`SignalReceiver`][receiver] trait is +implemented, handling needs to be updated for the new signal variant. Most +notably, this is on the top-level [`DerivationPipeline`][dp] type, as well +as all [the pipeline stages][stages]. + +#### An Example + +Let's create a new [`Signal`][signal] variant that updates the `RollupConfig` +in the [`IndexedTraversal` or `PollingTraversal`][traversal] stage. Let's call it `SetConfig`. +The [`signal`][signal] type would look like the following with this new +variant. + +```rust +/// A signal to send to the pipeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Signal { + /// Reset the pipeline. + Reset(ResetSignal), + /// Hardfork Activation. + Activation(ActivationSignal), + /// Flush the currently active channel. + FlushChannel, + /// Updates the rollup config in the IndexedTraversal or PollingTraversal stage. + UpdateConfig(ConfigUpdateSignal), +} + +/// A signal that updates the `RollupConfig`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct ConfigUpdateSignal(Arc); +``` + +Next, all handling of the [`Signal`][signal] type needs to be updated for +the new `UpdateConfig` variant. For the sake of this example, we'll just +focus on updating the [`IndexedTraversal` or `PollingTraversal`][traversal] stage. + +```rust +#[async_trait] +impl SignalReceiver for IndexedTraversal { + async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { + match signal { + Signal::Reset(ResetSignal { l1_origin, system_config, .. }) | + Signal::Activation(ActivationSignal { l1_origin, system_config, .. }) => { + self.block = Some(l1_origin); + self.done = false; + self.system_config = system_config.expect("System config must be provided."); + } + Signal::UpdateConfig(inner) => { + self.rollup_config = Arc::clone(&inner.0); + } + _ => {} + } + + Ok(()) + } +} +``` + + + +[traversal]: https://docs.rs/kona-derive/latest/kona_derive/struct.IndexedTraversal.html +[dp]: https://docs.rs/kona-derive/latest/kona_derive/struct.DerivationPipeline.html +[stages]: https://docs.rs/kona-derive/latest/kona_derive/index.html +[receiver]: https://docs.rs/kona-derive/latest/kona_derive/trait.SignalReceiver.html +[signal]: https://docs.rs/kona-derive/latest/kona_derive/enum.Signal.html +[error-kind]: https://docs.rs/kona-derive/latest/kona_derive/enum.PipelineErrorKind.html +[step-result]: https://docs.rs/kona-derive/latest/kona_derive/enum.StepResult.html diff --git a/kona/docs/docs/pages/sdk/protocol/derive/stages.mdx b/kona/docs/docs/pages/sdk/protocol/derive/stages.mdx new file mode 100644 index 0000000000000..1dd61178d369d --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/derive/stages.mdx @@ -0,0 +1,112 @@ +# Swapping out a Stage + +In the [introduction to the derivation pipeline][intro], the derivation pipeline +is broken down to demonstrate the composition of stages, forming the transformation +function from L1 data into L2 payload attributes. + +What makes kona's derivation pipeline extensible is that stages are composed using +trait-abstraction. That is, each successive stage composes the previous stage as +a generic. As such as long as a stage satisfies two rules, it can be swapped into +the pipeline seamlessly. +1. The stage implements the trait required by the next stage. +2. The stage uses the same trait for the previous stage as the + current stage to be swapped out. + +Below provides a concrete example, swapping out the `L1Retrieval` stage. + +## Example + +In the current, post-Holocene hardfork [`DerivationPipeline`][dp], the bottom three +stages of the pipeline are as follows (from top down). + +- [`FrameQueue`][frame-queue] +- [`L1Retrieval`][retrieval] +- [`IndexedTraversal` or `PollingTraversal`][traversal] + +In this set of stages, the [`IndexedTraversal` or `PollingTraversal`][traversal] stage sits at the bottom. +It implements the [`L1Retrieval`][retrieval] trait called the +[`L1RetrievalProvider`][retrieval-provider]. This provides generic methods that +allow the [`L1Retrieval`][retrieval] stage to call those methods on the generic +previous stage that implements this provider trait. + +As we go up a level, the same trait abstraction occurs. The [`L1Retrieval`][retrieval] +stage implements the provider trait that the [`FrameQueue`][frame-queue] stage requires. +This trait is the [`FrameQueueProvider`][frame-queue-provider]. + +Now that we understand the trait abstractions, let's swap out the +[`L1Retrieval`][retrieval] stage for a custom `DapRetrieval` stage. + +```rust +// ... +// imports +// ... + +// We use the same "L1RetrievalProvider" trait here +// in order to seamlessly use the `IndexedTraversal` or `PollingTraversal` + +/// DapRetrieval stage +#[derive(Debug)] +pub struct DapRetrieval

+where + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver, +{ + /// The previous stage in the pipeline. + pub prev: P, + provider: YourDataAvailabilityProvider, + data: Option, +} + +#[async_trait] +impl

FrameQueueProvider for DapRetrieval

+where + P: L1RetrievalProvider + OriginAdvancer + OriginProvider + SignalReceiver + Send, +{ + type Item = Bytes; + + async fn next_data(&mut self) -> PipelineResult { + if self.data.is_none() { + let next = self + .prev + .next_l1_block() + .await? // SAFETY: This question mark bubbles up the Eof error. + .ok_or(PipelineError::MissingL1Data.temp())?; + self.data = Some(self.provider.get_data(&next).await?); + } + + match self.data.as_mut().expect("Cannot be None").next().await { + Ok(data) => Ok(data), + Err(e) => { + if let PipelineErrorKind::Temporary(PipelineError::Eof) = e { + self.data = None; + } + Err(e) + } + } + } +} + +// ... +// impl OriginAdvancer for DapRetrieval +// impl OriginProvider for DapRetrieval +// impl SignalReceiver for DapRetrieval +// .. +``` + +Notice, the `L1RetrievalProvider` is used as a trait bound so the +[`IndexedTraversal` or `PollingTraversal`][traversal] stage can be used seamlessly as the "prev" stage in the pipeline. +Concretely, an instantiation of the `DapRetrieval` stage could be the following. + +``` +DapRetrieval> +``` + + + +[intro]: ./intro.mdx +[dp]: https://docs.rs/kona-derive/latest/kona_derive/struct.DerivationPipeline.html +[retrieval-provider]: https://docs.rs/kona-derive/latest/kona_derive/trait.L1RetrievalProvider.html +[frame-queue-provider]: https://docs.rs/kona-derive/latest/kona_derive/trait.FrameQueueProvider.html + +[frame-queue]: https://docs.rs/kona-derive/latest/kona_derive/struct.FrameQueue.html +[retrieval]: https://docs.rs/kona-derive/latest/kona_derive/struct.L1Retrieval.html +[traversal]: https://docs.rs/kona-derive/latest/kona_derive/struct.IndexedTraversal.html diff --git a/kona/docs/docs/pages/sdk/protocol/genesis/intro.mdx b/kona/docs/docs/pages/sdk/protocol/genesis/intro.mdx new file mode 100644 index 0000000000000..f22cac45220a6 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/genesis/intro.mdx @@ -0,0 +1,32 @@ +# Genesis + +kona-genesis crate + +The genesis crate contains types related to chain genesis. + +This section contains in-depth sections on building with [`kona-genesis`][genesis] crate types. + +- [The Rollup Config](./rollup-config.mdx) +- [The System Config](./system-config.mdx) + + +[op-stack]: https://github.com/ethereum-optimism/optimism +[op-program]: https://github.com/ethereum-optimism/optimism/tree/develop/op-program +[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon +[cannon-rs]: https://github.com/op-rs/cannon-rs +[asterisc]: https://github.com/ethereum-optimism/asterisc +[fp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html +[fpp-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#fault-proof-program +[preimage-specs]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle +[cannon-specs]: https://specs.optimism.io/experimental/fault-proof/cannon-fault-proof-vm.html#cannon-fault-proof-virtual-machine +[l2-output-root]: https://specs.optimism.io/protocol/proposals.html#l2-output-commitment-construction +[op-succinct]: https://github.com/succinctlabs/op-succinct +[revm]: https://github.com/bluealloy/revm + +[kona]: https://github.com/op-rs/kona +[issues]: https://github.com/op-rs/kona/issues +[new-issue]: https://github.com/op-rs/kona/issues/new +[contributing]: https://github.com/op-rs/kona/tree/main/CONTRIBUTING.md + +[op-labs]: https://github.com/ethereum-optimism +[bad-boi-labs]: https://github.com/BadBoiLabs diff --git a/kona/docs/docs/pages/sdk/protocol/genesis/rollup-config.mdx b/kona/docs/docs/pages/sdk/protocol/genesis/rollup-config.mdx new file mode 100644 index 0000000000000..7b87dc1d96e64 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/genesis/rollup-config.mdx @@ -0,0 +1,22 @@ +# Rollup Configs + +Rollup configurations are a consensus construct used to configure an Optimism Consensus client. +When an OP Stack chain is deployed into production or consensus nodes are configured to sync the chain, +certain consensus parameters can be configured. These parameters are defined in the +[OP Stack specs][specs]. + +Consensus parameters are consumed by OP Stack software through the `RollupConfig` type defined in the +[`kona-genesis`][genesis] crate. + +## `RollupConfig` Type + +The [`RollupConfig`][rc] type is defined in [`kona-genesis`][genesis]. + +Rollup configs can be loaded for a given chain id using [`kona-registry`][registry]. +The `ROLLUP_CONFIG` mapping in the `kona-registry` provides a mapping from chain ids +to rollup config. + +[specs]: https://specs.optimism.io/ +[genesis]: https://docs.rs/kona-genesis/latest/kona_genesis/index.html +[rc]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.RollupConfig.html +[registry]: https://docs.rs/kona-registry/latest/kona_registry/index.html diff --git a/kona/docs/docs/pages/sdk/protocol/genesis/system-config.mdx b/kona/docs/docs/pages/sdk/protocol/genesis/system-config.mdx new file mode 100644 index 0000000000000..298f82b3cd29d --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/genesis/system-config.mdx @@ -0,0 +1,34 @@ +# System Config + +The system configuration is a set of configurable chain parameters +defined in a contract on L1. These parameters can be changed through +the system config contract, emitting events that are picked up by +the [rollup node derivation process][derivation]. To dive deeper +into the System Config, visit the +[OP Stack Specifications][specs]. + +## `SystemConfig` Type + +The [`SystemConfig`][system-config] type is defined in +[`kona-genesis`][genesis]. + +Parameters defined in the [`SystemConfig`][system-config] are expected to be +updated through L1 receipts, using the [`update_with_receipts`][update] method. + +## Holocene Updates + +The [Holocene Hardfork][holocene] introduced an update to the +[`SystemConfig`][system-config] type, adding EIP-1559 parameters to the config. + +The [`SystemConfig`][system-config] type in [`kona-genesis`][genesis] provides +a method called [`eip_1559_params`][eip] that returns the EIP-1559 parameters +encoded as a [`B64`][b64]. + + +[specs]: https://specs.optimism.io/ +[genesis]: https://docs.rs/kona-genesis/latest/kona_genesis/index.html +[system-config]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.SystemConfig.html +[update]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.SystemConfig.html#method.update_with_receipts +[holocene]: https://specs.optimism.io/protocol/holocene/overview.html +[eip]: https://docs.rs/kona-genesis/latest/kona_genesis/struct.SystemConfig.html#method.eip_1559_params +[b64]: https://docs.rs/kona-registry/latest/kona_registry/index.html diff --git a/kona/docs/docs/pages/sdk/protocol/hardforks.mdx b/kona/docs/docs/pages/sdk/protocol/hardforks.mdx new file mode 100644 index 0000000000000..0607994fcc736 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/hardforks.mdx @@ -0,0 +1,43 @@ +# Hardforks + +kona-hardforks crate + +Hardforks are consensus layer types of the OP Stack. + +`kona-hardforks` most directly exports the [`Hardforks`][hardforks] type that provides +the network upgrade transactions for OP Stack hardforks including the following. +- [Ecotone][ecotone] +- [Fjord][fjord] +- [Isthmus][isthmus] + +Each hardfork has its own type in `kona-hardforks` that exposes the network +upgrade transactions for that hardfork. + +For example, the [`Ecotone`][ecotone-ty] type can be used to retrieve the Ecotone +network upgrade transactions through its [`txs() -> impl Iterator`][txs] method. + +```rust +// Notice, the `Hardfork` trait must be imported in order to +// provide the `txs()` method implemented for the hardfork type. +use kona_hardforks::{Hardfork, Ecotone}; +let ecotone_upgrade_txs = Ecotone.txs(); +assert_eq!(ecotone_upgrade_txs.collect::>().len(), 6); +``` + +Conveniently, the [`Hardforks`][hardforks] type exposes each hardfork type as a field +that can be directly referenced without needing to import all the different hardforks. + +```rust +use kona_hardforks::{Hardfork, Hardforks}; +let ecotone_upgrade_txs = Hardforks::ECOTONE.txs(); +assert_eq!(ecotone_upgrade_txs.collect::>().len(), 6); +``` + +[fjord]: https://specs.optimism.io/protocol/fjord/overview.html +[ecotone]: https://specs.optimism.io/protocol/ecotone/overview.html +[isthmus]: https://specs.optimism.io/protocol/isthmus/overview.html + +[ecotone-ty]: https://docs.rs/kona-hardforks/latest/kona_hardforks/struct.Ecotone.html +[hardforks]: https://docs.rs/kona-hardforks/latest/kona_hardforks/struct.Hardforks.html + +[txs]: https://docs.rs/kona-hardforks/latest/kona_hardforks/struct.Ecotone.html#method.txs diff --git a/kona/docs/docs/pages/sdk/protocol/interop.mdx b/kona/docs/docs/pages/sdk/protocol/interop.mdx new file mode 100644 index 0000000000000..a11d1e346de2a --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/interop.mdx @@ -0,0 +1,5 @@ +# Interop + +kona-interop crate + +`kona-interop` provides core types for the interop protocol. diff --git a/kona/docs/docs/pages/sdk/protocol/intro.mdx b/kona/docs/docs/pages/sdk/protocol/intro.mdx new file mode 100644 index 0000000000000..f8c1c14f3e488 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/intro.mdx @@ -0,0 +1,38 @@ +# Kona Protocol Libraries + +The Kona monorepo contains a set of protocol crates that are designed +to be `no_std` compatible for Kona's fault proof sdk. Protocol crates +are built on [alloy][alloy] and [op-alloy][op-alloy] types. + +The following protocol crates are published to [crates.io][crates]. + +

+ kona-hardforks crate + kona-registry crate + kona-protocol crate + kona-genesis crate + kona-interop crate + kona-derive crate + kona-driver crate +
+ +At the lowest level, `kona-genesis` and `kona-hardforks` expose +core genesis and hardfork types. + +`kona-protocol` sits just above `kona-genesis`, composing genesis types +into other core protocol types, as well as many independent protocol types. + +More recently, the `kona-interop` crate was introduced that contains types +specific to [Interop][interop]. + +`kona-registry` contains bindings to the [superchain-registry][scr]. +The registry is available in a `no_std` environment +but requires `serde` to read serialized configs at compile time. `kona-registry` uses +types defined in `kona-genesis` to deserialize the superchain registry configs at compile time. + + +[crates]: https://crates.io +[alloy]: https://github.com/alloy-rs/alloy +[op-alloy]: https://github.com/alloy-rs/op-alloy +[interop]: https://specs.optimism.io/interop/overview.html +[scr]: https://github.com/ethereum-optimism/superchain-registry diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/batches.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/batches.mdx new file mode 100644 index 0000000000000..a21e3d3846684 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/batches.mdx @@ -0,0 +1,108 @@ +# Batches + +A [Batch][batch] contains a list of transactions to be included in a specific +L2 block. Since the [Delta hardfork][delta], there are two Batch types or +variants: [`SingleBatch`][single-batch] and [`SpanBatch`][span-batch]. + +## Where Batches fit in the OP Stack + +The [Batch][batch] is the highest-level data type in the OP Stack +derivation process that comes prior to building payload attributes. +A [Batch][batch] is constructed by taking the raw data from a +[Channel][channel], decompressing it, and decoding the [Batch][batch] +from this decompressed data. + +Alternatively, when looking at the [Batch][batch] type from a batching +perspective, and not from the derivation perspective, the [Batch][batch] +type contains a list of L2 transactions and is compressed into the +[`Channel`][channel] type. In turn, the [`Channel`][channel] is split +into frames which are posted to the data availability layer through batcher +transactions. + +## Contents of a `Batch` + +A [`Batch`][batch] is either a [`SingleBatch`][single-batch] or a +[`SpanBatch`][span-batch], each with their own contents. Below, +these types are broken down in their respective sections. + +### `SingleBatch` Type + +The [`SingleBatch`][single-batch] type contains the following. + +- A [`BlockHash`][block-hash] parent hash that represents the parent L2 block. +- A `u64` epoch number that identifies the [epoch][epoch] for this batch. +- A [`BlockHash`][block-hash] epoch hash. +- The timestamp for the batch as a `u64`. +- A list of EIP-2718 encoded transactions (represented as [`Bytes`][bytes]). + +In order to validate the [`SingleBatch`][single-batch] once decoded, +the [`SingleBatch::check_batch`][check-batch-single] method should be used, +providing the rollup config, l1 blocks, l2 safe head, and inclusion block. + +### `SpanBatch` Type + +The [`SpanBatch`][span-batch] type (available since the [Delta hardfork][delta]) +comprises the data needed to build a "span" of multiple L2 blocks. It contains +the following data. + +- The parent check (the first 20 bytes of the block's parent hash). +- The l1 origin check (the first 20 bytes of the last block's l1 origin hash). +- The genesis timestamp. +- The chain id. +- A list of [`SpanBatchElement`][span-batch-element]s. These are similar to + the [`SingleBatch`][single-batch] type but don't contain the parent hash + and epoch hash for this L2 block. +- Origin bits. +- Block transaction counts. +- Span batch transactions which contain information for transactions in a span batch. + +Similar to the `SingleBatch` type discussed above, the [`SpanBatch`][span-batch] type +must be validated once decoded. For this, the [`SpanBatch::check_batch`][check-batch-span] +method is available. + +After the [Holocene hardfork][holocene] was introduced, span batch validation is greatly +simplified to be forwards-invalidating instead of backwards-invalidating, so a new +[`SpanBatch::check_batch_prefix`][check-batch-prefix] method provides a way to validate +each batch as it is loaded, in an iterative fashion. + +## Batch Encoding + +The first byte of the decompressed channel data is the +[`BatchType`][batch-type], which identifies whether the batch is a +[`SingleBatch`][single-batch] or a [`SpanBatch`][span-batch]. +From there, the respective type is decoded, and [derived][derived] +in the case of the [`SpanBatch`][span-batch]. + +The `Batch` encoding format for the [`SingleBatch`][single-batch] is +broken down [in the specs][specs]. + +## The `Batch` Type + +The [`Batch`][batch] type itself only provides two useful methods. + +- [`timestamp`][timestamp] returns the timestamp of the [`Batch`][batch] +- [`decode`][decode], constructs a new [`Batch`][batch] from the provided + raw, decompressed batch data and rollup config. + +Within each [`Batch`][batch] variant, the individual types contain +more functionality. + + +[holocene]: https://specs.optimism.io/protocol/holocene/overview.html +[check-batch-prefix]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SpanBatch.html#method.check_batch_prefix +[check-batch-span]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SpanBatch.html#method.check_batch +[span-batch-element]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SpanBatchElement.html +[check-batch-single]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SingleBatch.html#method.check_batch +[bytes]: https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html +[block-hash]: https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.BlockHash.html +[epoch]: https://specs.optimism.io/glossary.html?highlight=Epoch#sequencing-epoch +[decode]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.decode +[timestamp]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.timestamp +[specs]: https://specs.optimism.io/protocol/derivation.html#batch-format +[derived]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.RawSpanBatch.html#method.derive +[batch-type]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.BatchType.html +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html +[span-batch]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SpanBatch.html +[single-batch]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.SingleBatch.html +[delta]: https://specs.optimism.io/protocol/delta/overview.html diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/block-info.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/block-info.mdx new file mode 100644 index 0000000000000..78582d7cb3118 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/block-info.mdx @@ -0,0 +1,11 @@ +# BlockInfo + +The [`BlockInfo`][bi] type is straightforward, containing the block hash, +number, parent hash, and timestamp. + +The `BlockInfo` is a subset of information provided by the block header, +used for protocol operations. + + + +[bi]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.BlockInfo.html diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/channels.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/channels.mdx new file mode 100644 index 0000000000000..b48439e4f9f13 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/channels.mdx @@ -0,0 +1,108 @@ +# Channels + +Taken from the [OP Stack specs][specs], [`Channel`][channel]s are a set of +sequencer batches (for any L2 blocks) compressed together. + + +## Where Channels fit in the OP Stack + +L2 transactions are grouped into what are called [sequencer batches][seq-batch]. +In order to obtain a better compression ratio when posting these L2 transactions +to the data availability layer, [sequencer batches][seq-batch] are compressed +together into what is called a [Channel][channel]. This ultimately reduces +data availability costs. As previously noted in the [Frame][frame-docs] section, +[Channel][channel]s may not "fit" in a single batcher transaction, posting the +data to the data availability layer. In order to accommodate large +[Channel][channel]s, a tertiary [Frame][frame] data type breaks the +[Channel][channel] up into multiple [Frame][frame]s where a batcher transaction +then consists of one or multiple [Frame][frame]s. + + +## Contents of a Channel + +A [Channel][channel] is comprised of the following items. +- A [`ChannelId`][cid] which is a 16 byte long identifier for the channel. + Notice, [Frame][frame]s also contain a [`ChannelId`][cid], which is the + identical to this identifier, since frames "belong" to a given channel. +- A [`BlockInfo`][block-info] that marks the L1 block at which the channel + is "opened" at. +- The estimated size of the channel (as a `usize`) used to drop the channel + if there is a data overflow. +- A `boolean` if the channel is "closed". This indicates if the last frame + has been buffered, and added to the channel. +- A `u16` indicating the highest frame number within the channel. +- The frame number of the last frame (where `is_last` set to `true`). +- A mapping from Frame number to the [`Frame`][frame] itself. +- A [`BlockInfo`][block-info] for highest L1 inclusion block that a frame + was included in. + + +## Channel Encoding + +[`Channel`][channel] encoding is even more straightforward than that of a +[`Frame`][frame]. Simply, a [`Channel`][channel] is the concatenated list +of encoded [`Frame`][frame]s. + +Since each [`Frame`][frame] contains the [`ChannelId`][cid] that corresponds +to the given [`Channel`][channel], constructing a [`Channel`][channel] is as +simple as calling the [`Channel::add_frame`][add-frame] method for each of +its [`Frame`][frame]s. + +Once the [`Channel`][channel] has ingested all of it's [`Frame`][frame]s, +it will be marked as "ready", with the [`Channel::is_ready`][is-ready] +method returning `true`. + + +## The `Channel` Type + +As discussed [above](#-channel-encoding), the [`Channel`][channel] type is +expected to be populated with [`Frame`][frame]s using its +[`Channel::add_frame`][add-frame] method. Below we demonstrate constructing +a minimal [`Channel`][channel] using a few frames. + +```rust +use kona_protocol::{Channel, Frame}; + +// Construct a channel at the given L1 block. +let id = [0xee; 16]; +let block = BlockInfo::default(); +let mut channel = Channel::new(id, block); + +// The channel will consist of 3 frames. +let frame_0 = Frame { id: [0xee; 16], number: 0, ..Default::default() }; +let frame_1 = Frame { id: [0xee; 16], number: 1, ..Default::default() }; +let frame_2 = Frame { id: [0xee; 16], number: 2, is_last: true, ..Default::default() }; + +// Add the frames to the channel. +channel.add_frame(frame_0); +channel.add_frame(frame_1); +channel.add_frame(frame_2); + +// Since the last frame was ingested, +// the channel should be ready. +assert!(channel.is_ready()); +``` + +There are a few rules when adding a [`Frame`][frame] to a [`Channel`][channel]. +- The [`Frame`][frame]'s id must be the same [`ChannelId`][cid] as the [`Channel`][channel]s. +- [`Frame`][frame]s cannot be added once a [`Channel`][channel] is closed. +- [`Frame`][frame]s within a [`Channel`][channel] must have distinct numbers. + +Notice, [`Frame`][frame]s can be added out-of-order so long as the [`Channel`][channel] is +still open, and the frame hasn't already been added. + + + +[is-ready]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.is_ready +[add-frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.add_frame + +[block-info]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.BlockInfo.html + +[frame-docs]: ./frames.mdx +[specs]: https://specs.optimism.io/protocol/derivation.html#batch-submission-wire-format +[seq-batch]: https://specs.optimism.io/glossary.html#sequencer-batch + + +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[cid]: https://docs.rs/kona-protocol/latest/kona_protocol/type.ChannelId.html +[frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/frames.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/frames.mdx new file mode 100644 index 0000000000000..7bfeb4f8b14aa --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/frames.mdx @@ -0,0 +1,84 @@ +# Frames + +[`Frame`][frame]s are the lowest level data format in the OP Stack protocol. + + +## Where Frames fit in the OP Stack + +Transactions posted to the data availability layer of the rollup +contain one or multiple [Frame][frame]s. Frames are chunks of raw data that +belong to a given [Channel][channel], the next, higher up data format in the +OP Stack protocol. Importantly, a given transaction can contain +a variety of frames from _different_ channels, allowing maximum flexibility +when breaking up channels into batcher transactions. + + +## Contents of a Frame + +A [Frame][frame] is comprised of the following items. +- A [`ChannelId`][cid] which is a 16 byte long identifier for the channel that + the given frame belongs to. +- A `number` that identifies the index of the frame within the channel. Frames + are 0-indexed and are bound to `u16` size limit. +- `data` contains the raw data within the frame. +- `is_last` marks if the frame is the last within the channel. + + +## Frame Encoding + +When frames are posted through a batcher transaction, they are encoded as a +contiguous list with a single byte prefix denoting the derivation version. +The encoding can be represented as the following concatenated bytes. + +``` +encoded = DERIVATION_VERSION_0 ++ encoded_frame_0 ++ encoded_frame_1 ++ .. +``` + +Where `DERIVATION_VERSION_0` is a single byte (`0x00`) indicating the derivation +version including how the frames are encoded. Currently, the only supported +derivation version is `0`. + + +`encoded_frame_0`, `encoded_frame_1`, and so on, are all [`Frame`][frame]s encoded +as raw bytes. A single encoded [`Frame`][frame] can be represented by the following +concatenation of it's fields. + +``` +encoded_frame = channel_id ++ frame_number ++ frame_data_length ++ frame_data ++ is_last +``` + +Where `++` represents concatenation. The frame's fields map to it's encoding. +- `channel_id` is the 16 byte long [`Frame::id`][id]. +- `frame_number` is the 2 byte long (or `u16`) [`Frame::number`][number]. +- `frame_data_length` and `frame_data` provide the necessary details to decode + the [`Frame::data`][data], where `frame_data_length` is 4 bytes long (or `u32`). +- `is_last` is a single byte [`Frame::is_last`][is_last]. + + +## kona's `Frame` Type + +[`kona-protocol`][protocol] provides the [`Frame`][frame] type with a few useful +methods. [`Frame`][frame]s can be encoded and decoded using the [`Frame::encode`][encode] +and [`Frame::decode`][decode] methods. Given the raw batcher transaction data or blob data +containing the concatenated derivation version and contiguous list of encoded frames, +the [`Frame::parse_frame`][parse_frame] and [`Frame::parse_frames`][parse_frames] methods +provide ways to decode single and multiple frames, respectively. + + + +[encode]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.encode +[decode]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.decode + +[parse_frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.parse_frame +[parse_frames]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.parse_frames + +[protocol]: https://crates.io/crates/kona-protocol + +[id]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#structfield.id +[number]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#structfield.number +[data]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#structfield.data +[is_last]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#structfield.is_last + +[cid]: https://docs.rs/kona-protocol/latest/kona_protocol/type.ChannelId.html +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/intro.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/intro.mdx new file mode 100644 index 0000000000000..ef8bd48d05fcc --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/intro.mdx @@ -0,0 +1,65 @@ +# Protocol + +kona-protocol crate + +The [`kona-protocol`][protocol] crate contains types, constants, and methods +specific to Optimism derivation and batch-submission. + +[`kona-protocol`][protocol] supports `no_std`. + +## Background + +Protocol types are primarily used for L2 chain derivation. This section will +break down L2 chain derivation as it relates to types defined in +`kona-protocol` - that is, from the raw L2 chain data posted to L1, to the +[`Batch`][batch] type. And since the [`Batch`][batch] type naively breaks up +into the payload attributes, once executed, it becomes the canonical L2 block! +Note though, this provides an incredibly simplified introduction. It is advised +to reference [the specs][s] for the most up-to-date information regarding +derivation. + +The L2 chain is derived from data posted to the L1 chain - either as calldata +or blob data. Data is iteratively pulled from each L1 block and translated +into the first type defined by `kona-protocol`: the [`Frame`][frame] type. + +[`Frame`][frame]s are [parsed][parsed] from the raw data. Each [`Frame`][frame] +is a part of a [`Channel`][channel], the next type one level up in deriving +L2 blocks. [`Channel`][channel]s have IDs that frames reference. [`Frame`][frame]s +are [added][added] iteratively to the [`Channel`][channel]. Once a +[`Channel`][channel] [is ready][ready], it can be used to read a [`Batch`][batch]. + +Since a [`Channel`][channel] stitches together frames, it contains the raw frame +data. In order to turn this [`Channel`][channel] data into a [`Batch`][batch], +it needs to be decompressed using the respective (de)compression algorithm +(see [the channel specs][channel-specs] for more detail on this). Once +decompressed, the raw data can be [decoded][decoded] into the [`Batch`][batch] +type. + + +## Sections + +#### Core Derivation Types (discussed above) + +- [Frames](./frames.mdx) +- [Channels](./channels.mdx) +- [Batches](./batches.mdx) + +#### Other Critical Protocol Types + +- [BlockInfo](./block-info.mdx) +- [L2BlockInfo](./l2-block-info.mdx) + + + +[decoded]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html#method.decode +[batch]: https://docs.rs/kona-protocol/latest/kona_protocol/enum.Batch.html +[ready]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.is_ready +[added]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html#method.add_frame +[channel]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Channel.html +[frame]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html +[parsed]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.Frame.html#method.parse_frames + +[protocol]: https://crates.io/crates/kona-protocol +[s]: https://specs.optimism.io/protocol/derivation.html#overview +[lcd]: https://specs.optimism.io/protocol/derivation.html#overview +[channel-specs]: https://specs.optimism.io/protocol/derivation.html#channel-format diff --git a/kona/docs/docs/pages/sdk/protocol/protocol/l2-block-info.mdx b/kona/docs/docs/pages/sdk/protocol/protocol/l2-block-info.mdx new file mode 100644 index 0000000000000..06e96fa7aa414 --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/protocol/l2-block-info.mdx @@ -0,0 +1,17 @@ +# L2BlockInfo + +The [`L2BlockInfo`][lbi] extends the [`BlockInfo`][bi] type for the canonical +L2 chain. It contains the "L1 origin" which is a set of block info for the L1 +block that this L2 block "originated". + +Similarly to the [`BlockInfo`][bi] type, `L2BlockInfo` is a subset of information +provided by a block header, used for protocol operations. + +[`L2BlockInfo`][lbi] provides a [`from_block_and_genesis`][fbg] method to +construct the [`L2BlockInfo`][lbi] from a block and `ChainGenesis`. + + + +[bi]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.BlockInfo.html +[lbi]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.L2BlockInfo.html +[fbg]: https://docs.rs/kona-protocol/latest/kona_protocol/struct.L2BlockInfo.html#method.from_block_and_genesis diff --git a/kona/docs/docs/pages/sdk/protocol/registry.mdx b/kona/docs/docs/pages/sdk/protocol/registry.mdx new file mode 100644 index 0000000000000..e355197523d9f --- /dev/null +++ b/kona/docs/docs/pages/sdk/protocol/registry.mdx @@ -0,0 +1,69 @@ +# Registry + +kona-registry crate + +[`kona-registry`][sc] is a `no_std` crate that exports rust type definitions for chains +in the [`superchain-registry`][osr]. These are lazily evaluated statics that provide +`ChainConfig`s, `RollupConfig`s, and `Chain` objects for all chains with static definitions +in the [`superchain-registry`][osr]. + +Since it reads static files to read configurations for various chains into instantiated +objects, the [`kona-registry`][sc] crate requires [`serde`][serde] as a dependency. + +There are three core statics exposed by the [`kona-registry`][sc]. +- `CHAINS`: A list of chain objects containing the superchain metadata for this chain. +- `OPCHAINS`: A map from chain id to `ChainConfig`. +- `ROLLUP_CONFIGS`: A map from chain id to `RollupConfig`. + +[`kona-registry`][sc] exports the _complete_ list of chains within the superchain, as well as each +chain's `RollupConfig`s and `ChainConfig`s. + +### Usage + +Add the following to your `Cargo.toml`. + +```toml +[dependencies] +kona-registry = "0.1.0" +``` + +To make `kona-registry` `no_std`, toggle `default-features` off like so. + +```toml +[dependencies] +kona-registry = { version = "0.1.0", default-features = false } +``` + +Below demonstrates getting the `RollupConfig` for OP Mainnet (Chain ID `10`). + +```rust +use kona_registry::ROLLUP_CONFIGS; + +let op_chain_id = 10; +let op_rollup_config = ROLLUP_CONFIGS.get(&op_chain_id); +println!("OP Mainnet Rollup Config: {:?}", op_rollup_config); +``` + +A mapping from chain id to `ChainConfig` is also available. + +```rust +use kona_registry::OPCHAINS; + +let op_chain_id = 10; +let op_chain_config = OPCHAINS.get(&op_chain_id); +println!("OP Mainnet Chain Config: {:?}", op_chain_config); +``` + +[serde]: https://crates.io/crates/serde +[alloy]: https://github.com/alloy-rs/alloy +[op-alloy]: https://github.com/alloy-rs/op-alloy +[op-superchain]: https://docs.optimism.io/stack/explainer +[osr]: https://github.com/ethereum-optimism/superchain-registry + +[s]: #TODO +[sc]: https://crates.io/crates/kona-registry +[g]: https://crates.io/crates/kona-genesis + +[chains]: https://docs.rs/kona-registry/latest/kona_registry/struct.CHAINS.html +[opchains]: https://docs.rs/kona-registry/latest/kona_registry/struct.OPCHAINS.html +[rollups]: https://docs.rs/kona-registry/latest/kona_registry/struct.ROLLUP_CONFIGS.html diff --git a/kona/docs/docs/public/banner.png b/kona/docs/docs/public/banner.png new file mode 100644 index 0000000000000..af7316880ff0c Binary files /dev/null and b/kona/docs/docs/public/banner.png differ diff --git a/kona/docs/docs/public/favicon.ico b/kona/docs/docs/public/favicon.ico new file mode 100644 index 0000000000000..6f6a6fcc06188 Binary files /dev/null and b/kona/docs/docs/public/favicon.ico differ diff --git a/kona/docs/docs/public/logo.png b/kona/docs/docs/public/logo.png new file mode 100644 index 0000000000000..31d76367e4476 Binary files /dev/null and b/kona/docs/docs/public/logo.png differ diff --git a/kona/docs/docs/public/op-program-fpp.svg b/kona/docs/docs/public/op-program-fpp.svg new file mode 100644 index 0000000000000..51a998145f325 --- /dev/null +++ b/kona/docs/docs/public/op-program-fpp.svg @@ -0,0 +1,4 @@ + + + +
L2 Oracle
L2 Oracle
L2 Engine API
L2 Engine API
L2 OracleEngine
L2 OracleEngine
OracleBackedL2Chain
OracleBackedL2Chain
L2 pre-image
fetcher
L2 pre-image...
Main configuration: chain and rollup configs
Main configuration:...
Preimage KV Store
Preimage KV Store
L1 OracleEthClient
L1 OracleEthClient
prologue:
dispute and
L1 lookup
prologue:...
Pre-image Oracle
Client
Pre-image Oracle...
L1 Oracle
L1 Oracle
epilogue:
output root construction
& claim check
epilogue:...
Program Client:
- stateless
- no temp errors
- no environment access
- onchain
Program Client:...
Program Host / VM:
- stateful
- pre-image store on disk
- offchain
Program Host / VM:...
execution trace
execution trace
Pre-image Hint
Writer
Pre-image Hint...
derivation loop
derivation loop
Pre-image Hint
Reader
Pre-image Hint...
Pre-image Oracle
Server
Pre-image Oracle...
Program tools:
- pre-image fetching
- retry on fetch errors
Program tools:...
L1 pre-image
fetcher
L1 pre-image...
Pre-image hint router
Pre-image hint router
No-op when onchain / readonly
No-op when onchain / readon...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/kona/docs/docs/styles.css b/kona/docs/docs/styles.css new file mode 100644 index 0000000000000..2645d84abb396 --- /dev/null +++ b/kona/docs/docs/styles.css @@ -0,0 +1,131 @@ +@import 'tailwindcss'; + +@layer components { + .dark { + --vocs-color-accent: #87ceeb; + } +} + +.vocs_LandingPage_button { + border-radius: 4px; + height: 36px; +} + +.vocs_LandingPage_codeGroup { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; +} + +.vocs_DesktopTopNav, .vocs_DesktopTopNav_withLogo { + padding: 0 2rem; +} + +.vocs_DesktopTopNav_logoWrapper { + display: none; +} + +.vocs_LandingPage_install .vocs_CodeBlock_content { + font-size: 18px; +} + +.vocs_accent_border { + border-color: var(--vocs-color-accent); +} + +/* Override ALL vocs layout constraints */ +.vocs_LandingPage_root, +.vocs_LandingPage_root > div, +.vocs_LandingPage_root > div > div, +.vocs_LandingPage_root * { + max-width: none !important; +} + +/* Force full width on the main content area */ +.vocs_Content_root { + max-width: none !important; +} + +.vocs_Content_main { + max-width: none !important; +} + +/* Override any container classes */ +[class*="container"] { + max-width: none !important; +} + +/* Force full width on our custom sections */ +.full-width { + width: 100vw !important; + max-width: none !important; + margin-left: calc(-50vw + 50%) !important; + margin-right: calc(-50vw + 50%) !important; +} + +.full-width-inner { + max-width: 1400px !important; + margin: 0 auto !important; + padding: 0 8rem !important; +} + +@media (max-width: 1440px) { + .full-width-inner { + padding: 0 6rem !important; + } +} + +@media (max-width: 1200px) { + .full-width-inner { + padding: 0 4rem !important; + } +} + +@media (max-width: 1024px) { + .full-width-inner { + padding: 0 3rem !important; + } +} + +@media (max-width: 768px) { + .full-width-inner { + padding: 0 2rem !important; + } +} + +@media (max-width: 480px) { + .full-width-inner { + padding: 0 1rem !important; + } +} + +/* Hide only the logo in the navbar, keep navigation */ +.vocs_Header_logo { + display: none !important; +} + +.vocs_Header_logoContainer { + display: none !important; +} + +/* Hide only the logo in mobile navigation, keep the rest */ +.vocs_MobileTopNav_group .vocs_Header_logo, +.vocs_MobileTopNav_group .vocs_Header_logoContainer, +.vocs_MobileTopNav_group a[href="/"] { + display: none !important; +} + +/* Hide the home link that contains the logo */ +.vocs_Header_root a[href="/"] { + display: none !important; +} + +/* Adjust navbar layout without logo */ +.vocs_Header_nav { + justify-content: flex-start !important; +} + +/* .vocs_Header_root { */ +/* padding-left: 2rem !important; */ +/* } */ diff --git a/kona/docs/justfile b/kona/docs/justfile new file mode 100644 index 0000000000000..d0f2ebd580685 --- /dev/null +++ b/kona/docs/justfile @@ -0,0 +1,11 @@ +# Run the vocs documentation +run-vocs: + npm install && npm run dev -- --host + +# Build the vocs static site +build-vocs: + npm install && npm run build + +# Builds and opens the static site in the browser +open-site: build-vocs + open docs/dist/index.html diff --git a/kona/docs/package-lock.json b/kona/docs/package-lock.json new file mode 100644 index 0000000000000..6c160d3391ce9 --- /dev/null +++ b/kona/docs/package-lock.json @@ -0,0 +1,10448 @@ +{ + "name": "kona-docs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kona-docs", + "version": "0.0.0", + "dependencies": { + "react": "19.2.1", + "react-dom": "19.2.1", + "vocs": "1.2.1" + }, + "devDependencies": { + "@types/node": "latest", + "@types/react": "latest", + "tailwindcss": "^4.1.11", + "typescript": "latest" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@clack/core": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz", + "integrity": "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz", + "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==", + "bundleDependencies": [ + "is-unicode-supported" + ], + "license": "MIT", + "dependencies": { + "@clack/core": "^0.3.3", + "is-unicode-supported": "*", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts/node_modules/is-unicode-supported": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", + "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mdx-js/rollup": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/rollup/-/rollup-3.1.1.tgz", + "integrity": "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@rollup/pluginutils": "^5.0.0", + "source-map": "^0.7.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "rollup": ">=2" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "license": "MIT" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/rehype": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-1.29.2.tgz", + "integrity": "sha512-sxi53HZe5XDz0s2UqF+BVN/kgHPMS9l6dcacM4Ra3ZDzCJa5rDGJ+Ukpk4LxdD1+MITBM6hoLbPfGv9StV8a5Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@types/hast": "^3.0.4", + "hast-util-to-string": "^3.0.1", + "shiki": "1.29.2", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/transformers": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.29.2.tgz", + "integrity": "sha512-NHQuA+gM7zGuxGWP9/Ub4vpbwrYCrho9nQCLcCPfOe3Yc7LOYwmSuhElI688oiqIXk9dlZwDiyAG9vPBTuPJMA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/twoslash": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/twoslash/-/twoslash-1.29.2.tgz", + "integrity": "sha512-2S04ppAEa477tiaLfGEn1QJWbZUmbk8UoPbAEw4PifsrxkBXtAtOflIZJNtuCwz8ptc/TPxy7CO7gW4Uoi6o/g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/types": "1.29.2", + "twoslash": "^0.2.12" + } + }, + "node_modules/@shikijs/twoslash/node_modules/twoslash": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.2.12.tgz", + "integrity": "sha512-tEHPASMqi7kqwfJbkk7hc/4EhlrKCSLcur+TcvYki3vhIfaRMXnXjaYFgXpoZRbT6GdprD4tGuVBEmTpUgLBsw==", + "license": "MIT", + "dependencies": { + "@typescript/vfs": "^1.6.0", + "twoslash-protocol": "0.2.12" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@shikijs/twoslash/node_modules/twoslash-protocol": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/twoslash-protocol/-/twoslash-protocol-0.2.12.tgz", + "integrity": "sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg==", + "license": "MIT" + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz", + "integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.15" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz", + "integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==", + "license": "MIT" + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz", + "integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-x64": "4.1.15", + "@tailwindcss/oxide-freebsd-x64": "4.1.15", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-x64-musl": "4.1.15", + "@tailwindcss/oxide-wasm32-wasi": "4.1.15", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz", + "integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz", + "integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz", + "integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz", + "integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz", + "integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz", + "integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz", + "integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz", + "integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz", + "integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz", + "integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz", + "integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz", + "integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz", + "integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.15", + "@tailwindcss/oxide": "4.1.15", + "tailwindcss": "4.1.15" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz", + "integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript/vfs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", + "integrity": "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vanilla-extract/babel-plugin-debug-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.2.2.tgz", + "integrity": "sha512-MeDWGICAF9zA/OZLOKwhoRlsUW+fiMwnfuOAqFVohL31Agj7Q/RBWAYweqjHLgFBCsdnr6XIfwjJnmb2znEWxw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.9" + } + }, + "node_modules/@vanilla-extract/compiler": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/compiler/-/compiler-0.3.3.tgz", + "integrity": "sha512-y/RCcjhITi/JV/jbH22QN0aDSTtWELOBbkod/rcrUfGTS8bfVrthSsFmH+0ZoL9LJBx3vHrf0Qaf24xZkoiJoQ==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/css": "^1.17.5", + "@vanilla-extract/integration": "^8.0.6", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vite-node": "^3.2.2" + } + }, + "node_modules/@vanilla-extract/css": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.5.tgz", + "integrity": "sha512-u29cUVL5Z2qjJ2Eh8pusT1ToGtTeA4eb/y0ygaw2vWv9XFQSixtkBYEsVkrJExSI/0+SR1g8n5NYas4KlWOdfA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.9", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.2.3", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "lru-cache": "^10.4.3", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, + "node_modules/@vanilla-extract/dynamic": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.5.tgz", + "integrity": "sha512-QGIFGb1qyXQkbzx6X6i3+3LMc/iv/ZMBttMBL+Wm/DetQd36KsKsFg5CtH3qy+1hCA/5w93mEIIAiL4fkM8ycw==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/private": "^1.0.9" + } + }, + "node_modules/@vanilla-extract/integration": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-8.0.6.tgz", + "integrity": "sha512-BlDtXtb6Fin8XEGwf4BhsJkKQh0rhj/YiN6ylNNOqXtRU0+DQmzE5WGE056ScKg3p5e0IFaeH7PPxuWJca9aXw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/plugin-syntax-typescript": "^7.23.3", + "@vanilla-extract/babel-plugin-debug-ids": "^1.2.2", + "@vanilla-extract/css": "^1.17.5", + "dedent": "^1.5.3", + "esbuild": "npm:esbuild@>=0.17.6 <0.28.0", + "eval": "0.1.8", + "find-up": "^5.0.0", + "javascript-stringify": "^2.0.1", + "mlly": "^1.4.2" + } + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", + "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==", + "license": "MIT" + }, + "node_modules/@vanilla-extract/vite-plugin": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/vite-plugin/-/vite-plugin-5.1.3.tgz", + "integrity": "sha512-QKojhn+O4NIjPQsjfF3Lz+DCC9VaGE/P6eNXcZGoWhdCuGXbMOdSX0xogCX9O6ewzwJOiJJ++3NvuSlh7oVkcw==", + "license": "MIT", + "dependencies": { + "@vanilla-extract/compiler": "^0.3.3", + "@vanilla-extract/integration": "^8.0.6" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz", + "integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/create-vocs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/create-vocs/-/create-vocs-1.0.0.tgz", + "integrity": "sha512-Lv1Bd3WZEgwG4nrogkM54m8viW+TWPlGivLyEi7aNb3cuKPsEfMDZ/kTbo87fzOGtsZ2yh7scO54ZmVhhgBgTw==", + "dependencies": { + "@clack/prompts": "^0.7.0", + "cac": "^6.7.14", + "detect-package-manager": "^3.0.2", + "fs-extra": "^11.3.0", + "picocolors": "^1.1.1" + }, + "bin": { + "create-vocs": "_lib/bin.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.2.0.tgz", + "integrity": "sha512-L1bdkNKUP5WYxiW5dW6vA2hd3sL8BdRNLy2FCX0rLVise4eNw9nBdeBuJHxlELieSE2H1f6bYQFfwVUwWCV9rQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/detect-package-manager": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-3.0.2.tgz", + "integrity": "sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.263", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", + "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/hast-util-classnames": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-classnames/-/hast-util-classnames-3.0.0.tgz", + "integrity": "sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-dom/node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hono": { + "version": "4.10.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.7.tgz", + "integrity": "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.25", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz", + "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid-isomorphic": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/mermaid-isomorphic/-/mermaid-isomorphic-3.0.4.tgz", + "integrity": "sha512-XQTy7H1XwHK3DPEHf+ZNWiqUEd9BwX3Xws38R9Fj2gx718srmgjlZoUzHr+Tca+O+dqJOJsAJaKzCoP65QDfDg==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.0.0", + "mermaid": "^11.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/modern-ahocorasick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", + "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nuqs": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.2.tgz", + "integrity": "sha512-KMb6gmUJaLVRw+SbKUmBTo0IWLGU2s1Z4Iz/N64+EIDcu6Iw51CuppgKmxZR2EW3iXaOz5LF4avGKD2wq45eqg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/franky47" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "@tanstack/react-router": "^1", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router": "^5 || ^6 || ^7", + "react-router-dom": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.0.tgz", + "integrity": "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-class-names": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-class-names/-/rehype-class-names-2.0.0.tgz", + "integrity": "sha512-jldCIiAEvXKdq8hqr5f5PzNdIDkvHC6zfKhwta9oRoMu7bn0W7qLES/JrrjBvr9rKz3nJ8x4vY1EWI+dhjHVZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-classnames": "^3.0.0", + "hast-util-select": "^6.0.0", + "unified": "^11.0.4" + } + }, + "node_modules/rehype-mermaid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-mermaid/-/rehype-mermaid-3.0.0.tgz", + "integrity": "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "mermaid-isomorphic": "^3.0.0", + "mini-svg-data-uri": "^1.0.0", + "space-separated-tokens": "^2.0.0", + "unified": "^11.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx-frontmatter": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-5.2.0.tgz", + "integrity": "sha512-U/hjUYTkQqNjjMRYyilJgLXSPF65qbLPdoESOkXyrwz2tVyhAnm4GUKhfXqOOS9W34M3545xEMq+aMpHgVjEeQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "estree-util-value-to-estree": "^3.0.0", + "toml": "^3.0.0", + "unified": "^11.0.0", + "unist-util-mdx-define": "^1.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/twoslash": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.3.4.tgz", + "integrity": "sha512-RtJURJlGRxrkJmTcZMjpr7jdYly1rfgpujJr1sBM9ch7SKVht/SjFk23IOAyvwT1NLCk+SJiMrvW4rIAUM2Wug==", + "license": "MIT", + "dependencies": { + "@typescript/vfs": "^1.6.1", + "twoslash-protocol": "0.3.4" + }, + "peerDependencies": { + "typescript": "^5.5.0" + } + }, + "node_modules/twoslash-protocol": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/twoslash-protocol/-/twoslash-protocol-0.3.4.tgz", + "integrity": "sha512-HHd7lzZNLUvjPzG/IE6js502gEzLC1x7HaO1up/f72d8G8ScWAs9Yfa97igelQRDl5h9tGcdFsRp+lNVre1EeQ==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-mdx-define": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-mdx-define/-/unist-util-mdx-define-1.1.2.tgz", + "integrity": "sha512-9ncH7i7TN5Xn7/tzX5bE3rXgz1X/u877gYVAUB3mLeTKYJmQHmqKTDBi6BTGXV7AeolBCI9ErcVsOt2qryoD0g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz", + "integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz", + "integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==", + "license": "MIT", + "dependencies": { + "vfile": "^6.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vocs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vocs/-/vocs-1.2.1.tgz", + "integrity": "sha512-rQ5aoD68+UJQeJ9G/nPcqcwhbBpMFZnHJ9ZkIsRHaeqBdiA4S86ufplJRKxmX56XZLEpY+wlU+TGz8Qsxtb8Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "@hono/node-server": "^1.19.5", + "@mdx-js/mdx": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@mdx-js/rollup": "^3.1.1", + "@noble/hashes": "^1.7.1", + "@radix-ui/colors": "^3.0.0", + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-tabs": "^1.1.3", + "@shikijs/rehype": "^1", + "@shikijs/transformers": "^1", + "@shikijs/twoslash": "^1", + "@tailwindcss/vite": "4.1.15", + "@vanilla-extract/css": "^1.17.4", + "@vanilla-extract/dynamic": "^2.1.5", + "@vanilla-extract/vite-plugin": "^5.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.21", + "cac": "^6.7.14", + "chroma-js": "^3.1.2", + "clsx": "^2.1.1", + "compression": "^1.8.1", + "create-vocs": "^1.0.0-alpha.5", + "cross-spawn": "^7.0.6", + "fs-extra": "^11.3.2", + "hastscript": "^8.0.0", + "hono": "^4.10.3", + "mark.js": "^8.11.1", + "mdast-util-directive": "^3.1.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-hast": "^13.2.0", + "mdast-util-to-markdown": "^2.1.2", + "minisearch": "^7.2.0", + "nuqs": "^2.7.2", + "ora": "^7.0.1", + "p-limit": "^5.0.0", + "picomatch": "^4.0.3", + "playwright": "^1.52.0", + "postcss": "^8.5.2", + "radix-ui": "^1.1.3", + "react-intersection-observer": "^9.15.1", + "react-router": "^7.9.4", + "rehype-autolink-headings": "^7.1.0", + "rehype-class-names": "^2.0.0", + "rehype-mermaid": "^3.0.0", + "rehype-slug": "^6.0.0", + "remark-directive": "^3.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.1", + "remark-mdx-frontmatter": "^5.2.0", + "remark-parse": "^11.0.0", + "serve-static": "^1.16.2", + "shiki": "^1", + "toml": "^3.0.0", + "twoslash": "~0.3.4", + "ua-parser-js": "^1.0.40", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile-matter": "^5.0.1", + "vite": "^7.1.11", + "yaml": "^2.8.1" + }, + "bin": { + "vocs": "_lib/cli/index.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "react": "^19", + "react-dom": "^19" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/kona/docs/package.json b/kona/docs/package.json new file mode 100644 index 0000000000000..d4dc2e78d7a99 --- /dev/null +++ b/kona/docs/package.json @@ -0,0 +1,22 @@ +{ + "name": "kona-docs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vocs dev", + "build": "vocs build", + "preview": "vocs preview" + }, + "dependencies": { + "react": "19.2.1", + "react-dom": "19.2.1", + "vocs": "1.2.1" + }, + "devDependencies": { + "@types/node": "latest", + "@types/react": "latest", + "tailwindcss": "^4.1.11", + "typescript": "latest" + } +} diff --git a/kona/docs/sidebar.ts b/kona/docs/sidebar.ts new file mode 100644 index 0000000000000..54a107226a7b1 --- /dev/null +++ b/kona/docs/sidebar.ts @@ -0,0 +1,213 @@ +import { SidebarItem } from "vocs"; + +export const sidebar: SidebarItem[] = [ + { + text: "Introduction", + items: [ + { text: "Overview", link: "/intro/overview" }, + { text: "Why Kona?", link: "/intro/why" }, + { text: "Contributing", link: "/intro/contributing" }, + { text: "Kona Lore", link: "/intro/lore" } + ] + }, + { + text: "Kona for Node Operators", + items: [ + { text: "System Requirements", link: "/node/requirements" }, + { + text: "Installation", + collapsed: true, + items: [ + { + text: "Prerequisites", + link: "/node/install/overview" + }, + { + text: "Pre-Built Binaries", + link: "/node/install/binaries" + }, + { + text: "Docker", + link: "/node/install/docker" + }, + { + text: "Build from Source", + link: "/node/install/source" + } + ] + }, + { + text: "Run a Node", + items: [ + { + text: "Overview", + link: "/node/run/overview", + }, + { + text: "Binary", + link: "/node/run/binary", + }, + { + text: "Docker", + link: "/node/run/docker", + }, + { + text: "How it Works", + link: "/node/run/mechanics", + } + ] + }, + { + text: "JSON-RPC Reference", + items: [ + { + text: "Overview", + link: "/node/rpc/overview", + }, + { + text: "p2p", + link: "/node/rpc/p2p", + }, + { + text: "rollup", + link: "/node/rpc/rollup", + }, + { + text: "admin", + link: "/node/rpc/admin", + } + ] + }, + { text: "Configuration", link: "/node/configuration" }, + { text: "Kurtosis Integration", link: "/kurtosis/overview" }, + { text: "Monitoring", link: "/node/monitoring" }, + { text: "Subcommands", link: "/node/subcommands" }, + { + text: "FAQ", + link: "/node/faq/overview", + collapsed: true, + items: [ + { + text: "Ports", + link: "/node/faq/ports" + }, + { + text: "Profiling", + link: "/node/faq/profiling" + } + ] + } + ] + }, + { + text: "Kona as a Library", + items: [ + { text: "Overview", link: "/sdk/overview" }, + { + text: "Node SDK", + items: [ + { text: "Introduction", link: "/node/design/intro" }, + { text: "Derivation", link: "/node/design/derivation" }, + { text: "Engine", link: "/node/design/engine" }, + { text: "P2P", link: "/node/design/p2p" }, + { text: "Sequencer", link: "/node/design/sequencer" } + ] + }, + { + text: "Proof SDK", + items: [ + { text: "Introduction", link: "/sdk/proof/intro" }, + { text: "FPVM Backend", link: "/sdk/proof/fpvm-backend" }, + { text: "Custom Backend", link: "/sdk/proof/custom-backend" }, + { text: "kona-executor Extensions", link: "/sdk/proof/exec-ext" } + ] + }, + { + text: "Fault Proof Program Development", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/fpp-dev/intro" }, + { text: "Environment", link: "/sdk/fpp-dev/env" }, + { text: "Supported Targets", link: "/sdk/fpp-dev/targets" }, + { text: "Prologue", link: "/sdk/fpp-dev/prologue" }, + { text: "Execution", link: "/sdk/fpp-dev/execution" }, + { text: "Epilogue", link: "/sdk/fpp-dev/epilogue" } + ] + }, + { + text: "Protocol Libraries", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/protocol/intro" }, + { text: "Registry", link: "/sdk/protocol/registry" }, + { text: "Interop", link: "/sdk/protocol/interop" }, + { text: "Hardforks", link: "/sdk/protocol/hardforks" }, + { + text: "Derivation", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/protocol/derive/intro" }, + { text: "Custom Providers", link: "/sdk/protocol/derive/providers" }, + { text: "Stage Swapping", link: "/sdk/protocol/derive/stages" }, + { text: "Signaling", link: "/sdk/protocol/derive/signaling" } + ] + }, + { + text: "Genesis", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/protocol/genesis/intro" }, + { text: "Rollup Config", link: "/sdk/protocol/genesis/rollup-config" }, + { text: "System Config", link: "/sdk/protocol/genesis/system-config" } + ] + }, + { + text: "Protocol", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/protocol/protocol/intro" }, + { text: "BlockInfo", link: "/sdk/protocol/protocol/block-info" }, + { text: "L2BlockInfo", link: "/sdk/protocol/protocol/l2-block-info" }, + { text: "Frames", link: "/sdk/protocol/protocol/frames" }, + { text: "Channels", link: "/sdk/protocol/protocol/channels" }, + { text: "Batches", link: "/sdk/protocol/protocol/batches" } + ] + } + ] + }, + { + text: "Examples", + collapsed: true, + items: [ + { text: "Introduction", link: "/sdk/examples/intro" }, + { text: "Load a Rollup Config", link: "/sdk/examples/load-a-rollup-config" }, + { text: "Transform Frames to a Batch", link: "/sdk/examples/frames-to-batch" }, + { text: "Transform a Batch into Frames", link: "/sdk/examples/batch-to-frames" }, + { text: "Create a new L1BlockInfoTx Hardfork Variant", link: "/sdk/examples/new-l1-block-info-tx-hardfork" }, + { text: "Create a new kona-executor test fixture", link: "/sdk/examples/executor-test-fixtures" }, + { text: "Configuring P2P Network Peer Scoring", link: "/sdk/examples/p2p-peer-scoring" }, + { text: "Custom Derivation Pipeline with New Stage", link: "/sdk/examples/custom-derivation-pipeline" }, + { text: "Testing Kona Sequencing with Kurtosis", link: "/sdk/examples/kurtosis-sequencing-test" } + ] + } + ] + }, + { + text: "RFC", + link: "/rfc/active/intro", + items: [ + { + text: "Active RFCs", + items: [ ] + }, + { + text: "Archived RFCs", + collapsed: true, + items: [ + { text: "Umbrellas", link: "/rfc/archived/umbrellas" }, + { text: "Monorepo", link: "/rfc/archived/monorepo" } + ] + } + ] + } +]; diff --git a/kona/docs/tsconfig.json b/kona/docs/tsconfig.json new file mode 100644 index 0000000000000..d2636aac47ec3 --- /dev/null +++ b/kona/docs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/kona/docs/vocs.config.ts b/kona/docs/vocs.config.ts new file mode 100644 index 0000000000000..0338924d97498 --- /dev/null +++ b/kona/docs/vocs.config.ts @@ -0,0 +1,57 @@ +import { defineConfig } from 'vocs' +import { sidebar } from './sidebar' + +export default defineConfig({ + title: 'Kona', + description: 'Modular, performant, and secure OP Stack infrastructure in Rust', + logoUrl: '/logo.png', + iconUrl: '/logo.png', + ogImageUrl: '/kona-prod.png', + sidebar, + topNav: [ + { text: 'Run', link: '/node/run/overview' }, + { text: 'SDK', link: '/sdk/overview' }, + { text: 'Rustdocs', link: 'https://docs.rs/kona-node/latest/' }, + { text: 'GitHub', link: 'https://github.com/op-rs/kona' }, + { + text: 'v0.1.0', + items: [ + { + text: 'Releases', + link: 'https://github.com/op-rs/kona/releases' + }, + { + text: 'Contributing', + link: 'https://github.com/op-rs/kona/blob/main/CONTRIBUTING.md' + } + ] + } + ], + socials: [ + { + icon: 'github', + link: 'https://github.com/op-rs/kona', + }, + ], + theme: { + accentColor: { + light: '#1f1f1f', + dark: '#ffffff' + } + }, + sponsors: [ + { + name: 'Supporters', + height: 120, + items: [ + [ + { + name: 'OP Labs', + link: 'https://oplabs.co', + image: 'https://avatars.githubusercontent.com/u/109625874?s=200&v=4', + } + ] + ] + } + ] +}) diff --git a/kona/examples/README.md b/kona/examples/README.md new file mode 100644 index 0000000000000..0055c80934d67 --- /dev/null +++ b/kona/examples/README.md @@ -0,0 +1,17 @@ +## Examples + +These examples demonstrate how to work with kona crates. +Some examples are isolated services broken out from OP Stack components. + +To run an example, use the command `cargo run -p `. + +If you have an idea for a new example, [open an issue][issue]. +Otherwise if you already have an example you'd like to add, open a PR! + +#### Discovery + + + + + +[issue]: https://github.com/op-rs/kona/issues/new diff --git a/kona/examples/discovery/Cargo.toml b/kona/examples/discovery/Cargo.toml new file mode 100644 index 0000000000000..51ab9593c8be6 --- /dev/null +++ b/kona/examples/discovery/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "example-discovery" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +tracing.workspace = true +kona-cli.workspace = true +kona-disc.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } +discv5.workspace = true diff --git a/kona/examples/discovery/src/main.rs b/kona/examples/discovery/src/main.rs new file mode 100644 index 0000000000000..aa24f9b261cc3 --- /dev/null +++ b/kona/examples/discovery/src/main.rs @@ -0,0 +1,117 @@ +//! Example of how to use kona's discv5 discovery service as a standalone component +//! +//! ## Usage +//! +//! ```sh +//! cargo run --release -p example-discovery +//! ``` +//! +//! ## Inputs +//! +//! The discovery service takes the following inputs: +//! +//! - `-v` or `--verbosity`: Verbosity level (0-2) +//! - `-c` or `--l2-chain-id`: The L2 chain ID to use +//! - `-l` or `--disc-port`: Port to listen for discovery on +//! - `-i` or `--interval`: Interval to send discovery packets + +#![warn(unused_crate_dependencies)] + +use clap::Parser; +use discv5::enr::CombinedKey; +use kona_cli::{LogArgs, LogConfig}; +use kona_disc::{Discv5Builder, LocalNode}; +use std::net::{IpAddr, Ipv4Addr}; + +/// The discovery command. +#[derive(Parser, Debug, Clone)] +#[command(about = "Runs the discovery service")] +pub struct DiscCommand { + /// Verbosity level (0-5). + /// If set to 0, no logs are printed. + /// By default, the verbosity level is set to 3 (info level). + #[command(flatten)] + pub v: LogArgs, + /// The L2 chain ID to use. + #[arg(long, short = 'c', default_value = "10", help = "The L2 chain ID to use")] + pub l2_chain_id: u64, + /// Discovery port to listen on. + #[arg(long, short = 'l', default_value = "9099", help = "Port to listen to discovery")] + pub disc_port: u16, + /// Interval to send discovery packets. + #[arg(long, short = 'i', default_value = "3", help = "Interval to send discovery packets")] + pub interval: u64, +} + +impl DiscCommand { + /// Run the discovery subcommand. + pub async fn run(self) -> anyhow::Result<()> { + let filter = tracing_subscriber::EnvFilter::from_default_env() + .add_directive("discv5=error".parse()?); + LogConfig::new(self.v).init_tracing_subscriber(Some(filter))?; + + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let socket = LocalNode::new( + secret_key, + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + self.disc_port, + self.disc_port, + ); + tracing::info!(target: "discovery", "Starting discovery service on {:?}", socket); + + let discovery_builder = Discv5Builder::new( + socket, + self.l2_chain_id, + discv5::ConfigBuilder::new(discv5::ListenConfig::Ipv4 { + ip: Ipv4Addr::UNSPECIFIED, + port: self.disc_port, + }) + .build(), + ); + let mut discovery = discovery_builder.build()?; + discovery.interval = std::time::Duration::from_secs(self.interval); + discovery.forward = false; + let (handler, mut enr_receiver) = discovery.start(); + tracing::info!(target: "discovery", "Discovery service started, receiving peers."); + + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); + loop { + tokio::select! { + enr = enr_receiver.recv() => { + match enr { + Some(enr) => { + tracing::debug!(target: "discovery", "Received peer: {:?}", enr); + } + None => { + tracing::warn!(target: "discovery", "Failed to receive peer"); + } + } + } + _ = interval.tick() => { + let metrics = handler.metrics(); + let peer_count = handler.peer_count(); + tokio::spawn(async move { + if let Ok(metrics) = metrics.await { + tracing::debug!(target: "discovery", "Discovery metrics: {:?}", metrics); + } + if let Ok(pc) = peer_count.await { + tracing::debug!(target: "discovery", "Discovery peer count: {:?}", pc); + } + }); + } + } + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + if let Err(err) = DiscCommand::parse().run().await { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } + Ok(()) +} diff --git a/kona/examples/execution-fixture/Cargo.toml b/kona/examples/execution-fixture/Cargo.toml new file mode 100644 index 0000000000000..e15a3d6d3b685 --- /dev/null +++ b/kona/examples/execution-fixture/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "execution-fixture" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +url.workspace = true +tracing.workspace = true +kona-cli.workspace = true +kona-executor = { workspace = true, features = ["test-utils"] } +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } diff --git a/kona/examples/execution-fixture/src/main.rs b/kona/examples/execution-fixture/src/main.rs new file mode 100644 index 0000000000000..07f8831f8b0e1 --- /dev/null +++ b/kona/examples/execution-fixture/src/main.rs @@ -0,0 +1,74 @@ +//! Example for creating a static test fixture for `kona-executor` from a live chain +//! +//! ## Usage +//! +//! ```sh +//! cargo run --release -p execution-fixture +//! ``` +//! +//! ## Inputs +//! +//! The test fixture creator takes the following inputs: +//! +//! - `-v` or `--verbosity`: Verbosity level (0-2) +//! - `-r` or `--l2-rpc`: The L2 execution layer RPC URL to use. Must be archival. +//! - `-b` or `--block-number`: L2 block number to execute for the fixture. +//! - `-o` or `--output-dir`: (Optional) The output directory for the fixture. If not provided, +//! defaults to `kona-executor`'s `testdata` directory. + +use anyhow::{Result, anyhow}; +use clap::Parser; +use kona_cli::{LogArgs, LogConfig}; +use kona_executor::test_utils::ExecutorTestFixtureCreator; +use std::path::PathBuf; +use tracing::info; +use tracing_subscriber::EnvFilter; +use url::Url; + +/// The execution fixture creation command. +#[derive(Parser, Debug, Clone)] +#[command(about = "Creates a static test fixture for `kona-executor` from a live chain")] +pub struct ExecutionFixtureCommand { + #[command(flatten)] + pub v: LogArgs, + /// The L2 archive EL to use. + #[arg(long, short = 'r')] + pub l2_rpc: Url, + /// L2 block number to execute. + #[arg(long, short = 'b')] + pub block_number: u64, + /// The output directory for the fixture. + #[arg(long, short = 'o')] + pub output_dir: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = ExecutionFixtureCommand::parse(); + LogConfig::new(cli.v).init_tracing_subscriber(None::)?; + + let output_dir = if let Some(output_dir) = cli.output_dir { + output_dir + } else { + // Default to `crates/proof/executor/testdata` + let output = std::process::Command::new(env!("CARGO")) + .arg("locate-project") + .arg("--workspace") + .arg("--message-format=plain") + .output()? + .stdout; + let workspace_root: PathBuf = String::from_utf8(output)?.trim().into(); + + workspace_root + .parent() + .ok_or(anyhow!("Failed to locate workspace root"))? + .join("crates/proof/executor/testdata") + }; + + ExecutorTestFixtureCreator::new(cli.l2_rpc.as_str(), cli.block_number, output_dir) + .create_static_fixture() + .await; + + info!(target: "execution_fixture", block_number = cli.block_number, "Successfully created static test fixture"); + Ok(()) +} diff --git a/kona/examples/gossip/Cargo.toml b/kona/examples/gossip/Cargo.toml new file mode 100644 index 0000000000000..755800d805cc8 --- /dev/null +++ b/kona/examples/gossip/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "example-gossip" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +tracing.workspace = true +kona-cli.workspace = true +kona-disc.workspace = true +kona-node-service.workspace = true +kona-registry.workspace = true +libp2p.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tokio-util.workspace = true +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } +discv5.workspace = true diff --git a/kona/examples/gossip/src/main.rs b/kona/examples/gossip/src/main.rs new file mode 100644 index 0000000000000..3a936efbb7700 --- /dev/null +++ b/kona/examples/gossip/src/main.rs @@ -0,0 +1,141 @@ +//! Example of how to use kona's libp2p gossip service as a standalone component +//! +//! ## Usage +//! +//! ```sh +//! cargo run --release -p example-gossip +//! ``` +//! +//! ## Inputs +//! +//! The gossip service takes the following inputs: +//! +//! - `-v` or `--verbosity`: Verbosity level (0-2) +//! - `-c` or `--l2-chain-id`: The L2 chain ID to use +//! - `-l` or `--gossip-port`: Port to listen for gossip on +//! - `-i` or `--interval`: Interval to send discovery packets + +#![warn(unused_crate_dependencies)] + +use clap::Parser; +use discv5::enr::CombinedKey; +use kona_cli::{LogArgs, LogConfig}; +use kona_disc::LocalNode; +use kona_node_service::{NetworkActor, NetworkConfig, NetworkContext, NodeActor}; +use kona_registry::ROLLUP_CONFIGS; +use libp2p::{Multiaddr, identity::Keypair}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, +}; +use tokio_util::sync::CancellationToken; +use tracing_subscriber::EnvFilter; + +/// The gossip command. +#[derive(Parser, Debug, Clone)] +#[command(about = "Runs the gossip service")] +pub struct GossipCommand { + #[command(flatten)] + pub v: LogArgs, + /// The L2 chain ID to use. + #[arg(long, short = 'c', default_value = "10", help = "The L2 chain ID to use")] + pub l2_chain_id: u64, + /// Port to listen for gossip on. + #[arg(long, short = 'l', default_value = "9099", help = "Port to listen for gossip on")] + pub gossip_port: u16, + /// Port to listen for discovery on. + #[arg(long, short = 'd', default_value = "9098", help = "Port to listen for discovery on")] + pub disc_port: u16, + /// Interval to send discovery packets. + #[arg(long, short = 'i', default_value = "1", help = "Interval to send discovery packets")] + pub interval: u64, +} + +impl GossipCommand { + /// Run the gossip subcommand. + pub async fn run(self) -> anyhow::Result<()> { + LogConfig::new(self.v).init_tracing_subscriber(None::)?; + + let rollup_config = ROLLUP_CONFIGS + .get(&self.l2_chain_id) + .ok_or(anyhow::anyhow!("No rollup config found for chain ID"))?; + let signer = rollup_config + .genesis + .system_config + .as_ref() + .ok_or(anyhow::anyhow!("No system config found for chain ID"))? + .batcher_address; + tracing::debug!(target: "gossip", "Gossip configured with signer: {:?}", signer); + + let gossip = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.gossip_port); + tracing::info!(target: "gossip", "Starting gossip driver on {:?}", gossip); + + let mut gossip_addr = Multiaddr::from(gossip.ip()); + gossip_addr.push(libp2p::multiaddr::Protocol::Tcp(gossip.port())); + + let CombinedKey::Secp256k1(secret_key) = CombinedKey::generate_secp256k1() else { + unreachable!() + }; + + let disc_ip = Ipv4Addr::UNSPECIFIED; + let disc_addr = + LocalNode::new(secret_key, IpAddr::V4(disc_ip), self.disc_port, self.disc_port); + + let (_, network) = NetworkActor::new( + NetworkConfig { + discovery_address: disc_addr, + gossip_address: gossip_addr, + unsafe_block_signer: signer, + discovery_config: discv5::ConfigBuilder::new(discv5::ListenConfig::Ipv4 { + ip: disc_ip, + port: self.disc_port, + }) + .build(), + discovery_interval: Duration::from_secs(self.interval), + discovery_randomize: None, + keypair: Keypair::generate_secp256k1(), + gossip_config: Default::default(), + scoring: Default::default(), + topic_scoring: Default::default(), + monitor_peers: Default::default(), + bootstore: None, + gater_config: Default::default(), + bootnodes: Default::default(), + rollup_config: rollup_config.clone(), + gossip_signer: None, + enr_update: true, + } + .into(), + ); + + let (unsafe_blocks_tx, mut unsafe_blocks_rx) = tokio::sync::mpsc::channel(1024); + + network + .start(NetworkContext { + blocks: unsafe_blocks_tx, + cancellation: CancellationToken::new(), + }) + .await?; + + tracing::info!(target: "gossip", "Gossip driver started, receiving blocks."); + loop { + match unsafe_blocks_rx.recv().await { + Some(block) => { + tracing::info!(target: "gossip", "Received unsafe block: {:?}", block); + } + None => { + tracing::warn!(target: "gossip", "unsafe block gossip channel closed"); + } + } + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + if let Err(err) = GossipCommand::parse().run().await { + eprintln!("Error: {err:?}"); + std::process::exit(1); + } + Ok(()) +} diff --git a/kona/justfile b/kona/justfile new file mode 100644 index 0000000000000..e352f88c3416c --- /dev/null +++ b/kona/justfile @@ -0,0 +1,277 @@ + +# E2e integration tests for kona. +import "./tests/justfile" +# Builds docker images for kona +import "./docker/apps/justfile" +# Vocs Documentation commands +import "./docs/justfile" + +KONA_ROOT := source_directory() +KONA_VERSION := shell('jq -r .version "$1/version.json"', KONA_ROOT) +KONA_PRESTATE_HASH := shell('jq -r .prestateHash "$1/version.json"', KONA_ROOT) +KONA_INTEROP_PRESTATE_HASH := shell('jq -r .interopPrestateHash "$1/version.json"', KONA_ROOT) + +set positional-arguments +alias t := tests +alias la := lint-all +alias l := lint-native +alias lint := lint-native +alias f := fmt-native-fix +alias b := build-native +alias h := hack + +# default recipe to display help information +default: + @just --list + +# Build the rollup node in a single command. +build-node: + cargo build --release --bin kona-node + +# Build the supervisor +build-supervisor: + cargo build --release --bin kona-supervisor + +# Run all tests (excluding online tests) +tests: test test-docs + +# Test for the native target with all features. By default, excludes online tests. +test *args="-E '!test(test_online)'": + cargo nextest run --release --workspace --all-features {{args}} + just test-custom-embeds + +# Run all online tests +test-online: + just test "-E 'test(test_online)'" + +# Test custom embedded chain configuration functionality +test-custom-embeds: + cargo test --release --package kona-registry custom_chain_is_loaded_when_enabled \ + --config 'env.KONA_CUSTOM_CONFIGS="true"' \ + --config "env.KONA_CUSTOM_CONFIGS_DIR=\"{{justfile_directory()}}/crates/protocol/registry/tests/fixtures/custom\"" \ + --config 'env.KONA_CUSTOM_CONFIGS_TEST="true"' + +# Runs the tests with llvm-cov +llvm-cov-tests: + #!/usr/bin/env bash + # collect coverage of `just test` and `just test-custom-embeds` + cargo llvm-cov nextest --no-report --locked --workspace \ + --all-features \ + --exclude kona-node --exclude kona-p2p --exclude kona-sources \ + --ignore-run-fail --profile ci -E '!test(test_online)' + + cargo llvm-cov nextest --no-report --locked \ + --all-features \ + --ignore-run-fail --profile ci \ + --package kona-registry \ + -E 'test(custom_chain_is_loaded_when_enabled)' \ + --config 'env.KONA_CUSTOM_CONFIGS="true"' \ + --config "env.KONA_CUSTOM_CONFIGS_DIR=\"{{justfile_directory()}}/crates/protocol/registry/tests/fixtures/custom\"" \ + --config 'env.KONA_CUSTOM_CONFIGS_TEST="true"' + + cargo llvm-cov report --lcov --output-path lcov.info + +# Runs benchmarks +benches: + cargo bench --no-run --workspace --features test-utils --exclude example-gossip --exclude example-discovery + +# Lint the workspace for all available targets +lint-all: lint-native lint-cannon lint-asterisc lint-docs lint-typos + +# Check spelling with typos (`cargo install typos-cli`) +lint-typos: + typos + +# Runs `cargo hack check` against the workspace +hack: + cargo hack check --feature-powerset --no-dev-deps + +# Fixes the formatting of the workspace +fmt-native-fix: + cargo +nightly fmt --all + +# Check the formatting of the workspace +fmt-native-check: + cargo +nightly fmt --all -- --check + +# Lint the workspace +lint-native: fmt-native-check lint-docs + cargo clippy --workspace --all-features --all-targets -- -D warnings + +# Lint the workspace (mips arch). Currently, only the `kona-std-fpvm` crate is linted for the `cannon` target, as it is the only crate with architecture-specific code. +lint-cannon: + docker run \ + --rm \ + -v {{KONA_ROOT}}/../:/workdir \ + -w="/workdir/kona" \ + ghcr.io/op-rs/kona/cannon-builder:0.3.0 cargo clippy -p kona-std-fpvm --all-features -Zbuild-std=core,alloc -- -D warnings + +# Lint the workspace (risc-v arch). Currently, only the `kona-std-fpvm` crate is linted for the `asterisc` target, as it is the only crate with architecture-specific code. +lint-asterisc: + docker run \ + --rm \ + -v {{KONA_ROOT}}/../:/workdir \ + -w="/workdir/kona" \ + ghcr.io/op-rs/kona/asterisc-builder:0.3.0 cargo clippy -p kona-std-fpvm --all-features -Zbuild-std=core,alloc -- -D warnings + +# Lint the Rust documentation +lint-docs: + RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items + +# Test the Rust documentation +test-docs: + cargo test --doc --workspace --locked + +# Build for the native target +build-native *args='': + #!/usr/bin/env bash + cargo build --workspace $@ + +# Build `kona-client` for the `cannon` target. +build-cannon-client: + docker run \ + --rm \ + -v {{KONA_ROOT}}/../:/workdir \ + -w="/workdir/kona" \ + ghcr.io/op-rs/kona/cannon-builder:0.3.0 cargo build -Zbuild-std=core,alloc -p kona-client --bin kona-client --profile release-client-lto + +# Build `kona-client` for the `asterisc` target. +build-asterisc-client: + docker run \ + --rm \ + -v {{KONA_ROOT}}/../:/workdir \ + -w="/workdir/kona" \ + ghcr.io/op-rs/kona/asterisc-builder:0.3.0 cargo build -Zbuild-std=core,alloc -p kona-client --bin kona-client --profile release-client-lto + +# Check for unused dependencies in the crate graph. +check-udeps: + cargo +nightly udeps --release --workspace --all-features --all-targets + + +# Updates the `superchain-registry` git submodule source +source-registry: + @just --justfile ./crates/protocol/registry/justfile source + +# Generate file bindings for super-registry +bind-registry: + @just --justfile ./crates/protocol/registry/justfile bind + +check-no-std: + #!/usr/bin/env bash + no_std_packages=( + # proof crates + kona-executor + kona-mpt + kona-preimage + kona-proof + kona-proof-interop + + # protocol crates + kona-genesis + kona-hardforks + kona-registry + kona-protocol + kona-derive + kona-driver + kona-interop + + # utilities + kona-serde + ) + + for package in "${no_std_packages[@]}"; do + echo "Checking no-std build for: $package" + + cargo build -p $package --target riscv32imac-unknown-none-elf --no-default-features + + if [ $? -ne 0 ]; then + echo "Failed to build no-std for: $package" + exit 1 + fi + + echo "Successfully checked no-std build for: $package" + done + +### TODO(ethereum-optimism/optimism#18654): Remove these recipes once the migration is complete + +build-all: build-prestates build-kona-host + +build-prestates: build-cannon-prestate build-interop-prestate + +build-cannon-prestate: + @just build-prestate kona-client {{KONA_PRESTATE_HASH}} + +build-interop-prestate: + @just build-prestate kona-client-int {{KONA_INTEROP_PRESTATE_HASH}} + +build-prestate VARIANT HASH: + #!/usr/bin/env bash + set -euo pipefail + + cd "{{KONA_ROOT}}" + # Check if required prestate already exists + if [[ -f "prestates/{{HASH}}.bin.gz" ]]; then + echo "Prestate {{HASH}} for variant {{VARIANT}} already exists" + exit + fi + echo "Building prestate..." + just checkout-kona + cd "{{KONA_ROOT}}/build/kona" + cd docker/fpvm-prestates + # Delete any existing artifacts (they're in .gitignore so reset --hard won't delete them) + rm -rf ../../prestate-artifacts-cannon + echo just cannon {{VARIANT}} "kona-client/v{{KONA_VERSION}}" $(cat ../../.config/cannon_tag) + just cannon {{VARIANT}} "kona-client/v{{KONA_VERSION}}" $(cat ../../.config/cannon_tag) + + # Check the prestate hash matches what we expect + ACTUAL_HASH=$(jq -r .pre ../../prestate-artifacts-cannon/prestate-proof.json) + if [[ "${ACTUAL_HASH}" != "{{HASH}}" ]]; then + echo "Incorrect prestate hash, expected {{HASH}} but was ${ACTUAL_HASH}" + exit 1 + fi + + mkdir -p "{{KONA_ROOT}}/prestates" + cp ../../prestate-artifacts-cannon/prestate.bin.gz "{{KONA_ROOT}}/prestates/{{HASH}}.bin.gz" + +build-kona-host: + #!/usr/bin/env bash + set -euo pipefail + + # Check if kona-host has already been built + # This is a simplistic check that relies on CircleCI's cache being keyed on this file + # which contains the kona version we're checking out. Locally you may need to run + # just clean to force a rebuild if the kona version has changed. + if [[ -f "bin/kona-host" ]]; then + echo "kona-host already built. Assuming it is built from kona-client/v{{KONA_VERSION}}" + exit + fi + + echo "Building kona-host" + just checkout-kona + cd "{{KONA_ROOT}}/build/kona" + just build-native --bin kona-host + + mkdir -p "{{KONA_ROOT}}/bin" + cp "{{KONA_ROOT}}/build/kona/target/debug/kona-host" "{{KONA_ROOT}}/bin/kona-host" + +checkout-kona: + #!/usr/bin/env bash + set -euo pipefail + + DIR="{{KONA_ROOT}}/build" + mkdir -p "${DIR}" + cd "${DIR}" + if [[ -d kona ]]; then + cd kona + git fetch origin + git checkout -f "kona-client/v{{KONA_VERSION}}" + git reset --hard HEAD + else + git clone -b "kona-client/v{{KONA_VERSION}}" https://github.com/op-rs/kona kona + cd kona + fi + +clean: + #!/usr/bin/env bash + set -euo pipefail + rm -rf "{{KONA_ROOT}}/build" "{{KONA_ROOT}}/prestates" "{{KONA_ROOT}}/bin" \ No newline at end of file diff --git a/kona/lychee.toml b/kona/lychee.toml new file mode 100644 index 0000000000000..388c842084c88 --- /dev/null +++ b/kona/lychee.toml @@ -0,0 +1,21 @@ +# Config example at: https://github.com/lycheeverse/lychee/blob/master/lychee.example.toml + +verbose = "debug" + +no_progress = false + +exclude_all_private = false + +# Accept status codes - 403 is often returned by private repos instead of 404 +accept = [200, 403] + +exclude = [ + 'foo.bar', + 'localhost', + '^https://github\.com/op-rs/kona/pull/', + '^https://www\.intel\.com/content/www/us/en/developer/tools/trust-domain-extensions/documentation\.html', + 'https://sepolia-optimism\.etherscan\.io' +] + +# exclude_path values are ignored if cli is glob pattern that matches +# this list, at least in lycheeverse/lychee-action@v2 diff --git a/kona/release.toml b/kona/release.toml new file mode 100644 index 0000000000000..bce80bcdd3489 --- /dev/null +++ b/kona/release.toml @@ -0,0 +1,10 @@ +# Config file for [`cargo-release`](https://github.com/crate-ci/cargo-release) +# See: https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md + +allow-branch = ["main"] +sign-commit = true +sign-tag = true +shared-version = true +pre-release-commit-message = "chore: release {{version}}" +tag-prefix = "" # tag only once instead of per every crate +pre-release-hook = ["sh", "-c", "$WORKSPACE_ROOT/.config/changelog.sh --tag {{version}}"] diff --git a/kona/rust-toolchain.toml b/kona/rust-toolchain.toml new file mode 100644 index 0000000000000..c95c90571ff47 --- /dev/null +++ b/kona/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.88" diff --git a/kona/rustfmt.toml b/kona/rustfmt.toml new file mode 100644 index 0000000000000..68c3c93033d4f --- /dev/null +++ b/kona/rustfmt.toml @@ -0,0 +1,11 @@ +reorder_imports = true +imports_granularity = "Crate" +use_small_heuristics = "Max" +comment_width = 100 +wrap_comments = true +binop_separator = "Back" +trailing_comma = "Vertical" +trailing_semicolon = false +use_field_init_shorthand = true +format_code_in_doc_comments = true +doc_comment_code_block_width = 100 diff --git a/kona/tests/.gitignore b/kona/tests/.gitignore new file mode 100644 index 0000000000000..7e3a9f0ac4552 --- /dev/null +++ b/kona/tests/.gitignore @@ -0,0 +1,6 @@ +devnets/specs/* +optimism-package +kona-test-logs +proofs +actions +builder_jwt.hex \ No newline at end of file diff --git a/kona/tests/README.md b/kona/tests/README.md new file mode 100644 index 0000000000000..77b7711123fcd --- /dev/null +++ b/kona/tests/README.md @@ -0,0 +1,80 @@ +# E2e testing for the kona-node + +This repository contains the e2e testing resources for the kona-node. The e2e testing is done using the `op-devstack` from the [Optimism monorepo](https://github.com/ethereum-optimism/optimism) with the `sysgo` orchestrator. + +## Prerequisites + +Make sure to initialize the git submodules before running the tests: + +```bash +git submodule init +git submodule update --recursive +``` + +Then run `go mod tidy` to fetch dependencies: + +```bash +go mod tidy +``` + +## Description + +The interactions with this repository are done through the [`justfile`](./justfile) recipes. + +### Running E2E Tests + +To run the e2e tests, use the following command: + +```bash +just test-e2e-sysgo BINARY GO_PKG_NAME DEVNET FILTER +``` + +Where: +- `BINARY`: The binary to test (`node` or `supervisor`) +- `GO_PKG_NAME`: The Go package name to test (e.g., `node/common`) +- `DEVNET`: The devnet configuration (`simple-kona`, `simple-kona-geth`, `simple-kona-sequencer`, `large-kona-sequencer`) +- `FILTER`: Optional test filter + +For example, to run the common node tests: + +```bash +just test-e2e-sysgo node node/common simple-kona +``` + +### Acceptance Tests + +To run acceptance tests for the Rust stack: + +```bash +just acceptance-tests CL_TYPE EL_TYPE GATE +``` + +Where: +- `CL_TYPE`: Consensus layer type (`kona` or `op-node`) +- `EL_TYPE`: Execution layer type (`op-reth` or `op-geth`) +- `GATE`: The gate to run (default: `jovian`) + +### Other Recipes + +- `just build-devnet BINARY`: Builds the Docker image for the specified binary (`node` or `supervisor`). + +- `just build-kona`: Builds the kona-node binary. + +- `just build-reth`: Builds the op-reth binary. + +- `just long-running-test FILTER OUTPUT_LOGS_DIR`: Runs long-running tests with optional filter and output directory. + +- `just action-tests-single`: Runs action tests for the single-chain client program. + +- `just action-tests-interop`: Runs action tests for the interop client program. + +## Environment Variables + +When using `op-devstack` for testing, the following environment variables are set automatically by the justfile recipes: + +- `DEVSTACK_ORCHESTRATOR=sysgo`: Tells `op-devstack` to use the sysgo orchestrator. +- `DISABLE_OP_E2E_LEGACY=true`: Tells `op-devstack` not to use the `op-e2e` tests that rely on e2e config and contracts-bedrock artifacts. + +## Contributing + +We welcome contributions to this repository. diff --git a/kona/tests/justfile b/kona/tests/justfile new file mode 100644 index 0000000000000..5e23d7ca3fc66 --- /dev/null +++ b/kona/tests/justfile @@ -0,0 +1,279 @@ +SOURCE := source_directory() +CURRENT_DIR := `pwd` +DEFAULT_DEVNET_PATH := source_directory() + "/devnets/simple-kona.yaml" +DEFAULT_OP_PACKAGE_PATH := "github.com/ethpandaops/optimism-package@998796c0f3bb478d63d729e65f0b76e24112e00d" + +build-devnet BINARY: + #!/usr/bin/env bash + if [ {{BINARY}} != "node" ] && [ {{BINARY}} != "supervisor" ]; then + echo "Invalid binary specified. Must be either 'node' or 'supervisor'." + exit 1 + fi + + export BUILD_PROFILE="release" + cd {{SOURCE}}/../docker/apps && just build-local "kona-{{BINARY}}" "kona-{{BINARY}}:local" + +build-reth: + #!/bin/bash + cd {{SOURCE}}/../../reth && cargo build --bin op-reth -p op-reth + +build-kona PROFILE="release": + #!/bin/bash + cd {{SOURCE}}/.. && cargo build --bin kona-node --profile {{PROFILE}} + +acceptance-tests CL_TYPE="kona" EL_TYPE="op-reth" GATE="jovian": + #!/bin/bash + if [ "{{CL_TYPE}}" = "kona" ] ; then + echo "Building kona-node..." + just build-kona + fi + + if [ "{{EL_TYPE}}" = "op-reth" ] ; then + echo "Building op-reth..." + just build-reth + fi + + just acceptance-tests-run {{CL_TYPE}} {{EL_TYPE}} {{GATE}} + +# Run acceptance tests for the rust stack. By default runs the acceptance tests for kona-node with op-reth. +# Uses the jovian gate by default. +acceptance-tests-run CL_TYPE="kona" EL_TYPE="op-reth" GATE="jovian": + #!/bin/bash + if [ "{{CL_TYPE}}" = "kona" ] ; then + echo "Running acceptance tests for kona-node" + export KONA_NODE_EXEC_PATH="{{SOURCE}}/../target/debug/kona-node" + fi + + if [ "{{EL_TYPE}}" = "op-reth" ] ; then + echo "Running acceptance tests for op-reth" + export OP_RETH_EXEC_PATH="{{SOURCE}}/../../reth/target/debug/op-reth" + fi + + echo "Running acceptance tests for {{CL_TYPE}} with {{EL_TYPE}} on gate {{GATE}}..." + + export DEVSTACK_L2CL_KIND="{{CL_TYPE}}" + export DEVSTACK_L2EL_KIND="{{EL_TYPE}}" + export LOG_LEVEL="debug" + cd {{SOURCE}}/optimism/op-acceptance-tests && just acceptance-test "" "{{GATE}}" + +# Run the e2e tests for the sysgo orchestrator. +# Builds the kona-node and op-reth binaries and runs the tests. +test-e2e-sysgo BINARY="node" GO_PKG_NAME="node/common" DEVNET="simple-kona" FILTER="" : + #!/bin/bash + if [ "{{BINARY}}" = "node" ]; then + echo "Building kona-node..." + just build-kona + echo "Building op-reth..." + just build-reth + elif [ "{{BINARY}}" = "supervisor" ]; then + echo "Building supervisor..." + cd {{SOURCE}}/.. && just build-supervisor + fi + + just test-e2e-sysgo-run {{BINARY}} {{GO_PKG_NAME}} {{DEVNET}} {{FILTER}} + +# Run the e2e tests for the sysgo orchestrator. Don't build the binaries. +test-e2e-sysgo-run BINARY="node" GO_PKG_NAME="node/common" DEVNET="simple-kona" FILTER="" : + #!/bin/bash + echo "Running e2e tests for the sysgo orchestrator for binary {{BINARY}} with package name {{GO_PKG_NAME}} on devnet {{DEVNET}}..." + + export DISABLE_OP_E2E_LEGACY=true + export DEVSTACK_ORCHESTRATOR=sysgo + export GO_PKG_NAME="{{GO_PKG_NAME}}" + + if [ "{{BINARY}}" = "node" ]; then + if [ $KONA_NODE_EXEC_PATH == "" ]; then + export KONA_NODE_EXEC_PATH="{{SOURCE}}/../target/debug/kona-node" + fi + if [ $OP_RETH_EXEC_PATH == "" ]; then + export OP_RETH_EXEC_PATH="{{SOURCE}}/../../reth/target/debug/op-reth" + fi + elif [ "{{BINARY}}" = "supervisor" ]; then + export DEVSTACK_SUPERVISOR_KIND=kona + export KONA_SUPERVISOR_EXEC_PATH="{{SOURCE}}/../target/release/kona-supervisor" + else + echo "Invalid BINARY specified. Must be either 'node' or 'supervisor'." + exit 1 + fi + + if ! [ -z "{{FILTER}}" ]; then + export FILTER="-run {{FILTER}}" + echo "Running tests with filter {{FILTER}}..." + fi + + if [ "{{DEVNET}}" = "simple-kona" ]; then + export KONA_SEQUENCER_WITH_RETH=0 + export KONA_VALIDATOR_WITH_RETH=1 + export KONA_SEQUENCER_WITH_GETH=0 + export KONA_VALIDATOR_WITH_GETH=0 + + export OP_SEQUENCER_WITH_RETH=1 + export OP_VALIDATOR_WITH_RETH=1 + export OP_SEQUENCER_WITH_GETH=0 + export OP_VALIDATOR_WITH_GETH=0 + elif [ "{{DEVNET}}" = "simple-kona-geth" ]; then + export KONA_SEQUENCER_WITH_RETH=0 + export KONA_VALIDATOR_WITH_RETH=0 + export KONA_SEQUENCER_WITH_GETH=0 + export KONA_VALIDATOR_WITH_GETH=1 + + export OP_SEQUENCER_WITH_RETH=0 + export OP_VALIDATOR_WITH_RETH=0 + export OP_SEQUENCER_WITH_GETH=1 + export OP_VALIDATOR_WITH_GETH=1 + elif [ "{{DEVNET}}" = "simple-kona-sequencer" ]; then + export KONA_SEQUENCER_WITH_RETH=1 + export KONA_VALIDATOR_WITH_RETH=1 + export KONA_SEQUENCER_WITH_GETH=0 + export KONA_VALIDATOR_WITH_GETH=1 + + export OP_SEQUENCER_WITH_RETH=0 + export OP_VALIDATOR_WITH_RETH=0 + export OP_SEQUENCER_WITH_GETH=0 + export OP_VALIDATOR_WITH_GETH=0 + elif [ "{{DEVNET}}" = "large-kona-sequencer" ]; then + export KONA_SEQUENCER_WITH_RETH=1 + export KONA_VALIDATOR_WITH_RETH=4 + export KONA_SEQUENCER_WITH_GETH=0 + export KONA_VALIDATOR_WITH_GETH=4 + + export OP_SEQUENCER_WITH_RETH=0 + export OP_VALIDATOR_WITH_RETH=0 + export OP_SEQUENCER_WITH_GETH=0 + export OP_VALIDATOR_WITH_GETH=0 + fi + + export SKIP_P2P_CONNECTION_CHECK=true + + # Run the test with count=1 to avoid caching the test results. + cd {{SOURCE}} && go test -count=1 -timeout 40m -v ./$GO_PKG_NAME $FILTER + +long-running-test FILTER="" OUTPUT_LOGS_DIR="": + #!/bin/bash + if ! [ -z "{{FILTER}}" ]; then + export FILTER="-run {{FILTER}}" + fi + + export KONA_SEQUENCER_WITH_RETH=1 + export KONA_VALIDATOR_WITH_RETH=4 + export KONA_SEQUENCER_WITH_GETH=0 + export KONA_VALIDATOR_WITH_GETH=4 + + export OP_SEQUENCER_WITH_RETH=0 + export OP_VALIDATOR_WITH_RETH=0 + export OP_SEQUENCER_WITH_GETH=0 + export OP_VALIDATOR_WITH_GETH=0 + + export KONA_NODE_EXEC_PATH="{{SOURCE}}/../target/debug/kona-node" + export OP_RETH_EXEC_PATH="{{SOURCE}}/../../reth/target/debug/op-reth" + export DEVSTACK_ORCHESTRATOR=sysgo + echo "Building kona-node..." + just build-kona + echo "Building op-reth..." + just build-reth + + export OP_TESTLOG_FILE_LOGGER_OUTDIR="{{OUTPUT_LOGS_DIR}}" + if [ -z "{{OUTPUT_LOGS_DIR}}" ]; then + export OP_TESTLOG_FILE_LOGGER_OUTDIR="{{CURRENT_DIR}}/kona-test-logs" + fi + + mkdir -p $OP_TESTLOG_FILE_LOGGER_OUTDIR + + export DISABLE_OP_E2E_LEGACY=true + export SKIP_P2P_CONNECTION_CHECK=true + + # Run the test with count=1 to avoid caching the test results. + cd {{SOURCE}} && go test -count=1 -timeout 0 -v ./node/long-running $FILTER + +# Run action tests for the single-chain client program on the native target +action-tests-single test_name='Test_ProgramAction' *args='': action-tests-single-build (action-tests-single-run test_name args) + +# Build action tests for the single-chain client program on the native target +action-tests-single-build: + #!/bin/bash + + echo "Building host program for the native target" + just build-native --release --bin kona-host + +# Run action tests for the single-chain client program on the native target +action-tests-single-run test_name='Test_ProgramAction' parallel="0" *args='': + #!/bin/bash + + if [ ! -n "$KONA_HOST_PATH" ]; then + export KONA_HOST_PATH="{{SOURCE}}/../target/release/kona-host" + fi + + echo "KONA_HOST_PATH: $KONA_HOST_PATH" + + # GitHub actions patch - do not print logs. + # https://github.com/gotestyourself/gotestsum/blob/b4b13345fee56744d80016a20b760d3599c13504/testjson/format.go#L442-L444 + echo "Running action tests for the client program on the native target" + + cd {{SOURCE}}/../../op-e2e/actions/proofs + + # Set parallel to the number of cores available if {{parallel}} is not greater than 0 + if [ {{parallel}} -gt 0 ]; then + export PARALLEL={{parallel}} + else + export PARALLEL=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + fi + + echo "Running tests with $PARALLEL cores" + + # CircleCI parallelism + if [ -n "$CIRCLE_NODE_TOTAL" ] && [ "$CIRCLE_NODE_TOTAL" -gt 1 ]; then + echo "Setting up test directories..." + mkdir -p ./tmp/test-results ./tmp/testlogs + + export NODE_INDEX=${CIRCLE_NODE_INDEX:-0} + export NODE_TOTAL=${CIRCLE_NODE_TOTAL:-1} + go test -list '^{{test_name}}' > /tmp/tests.txt + circleci tests split --split-by=name /tmp/tests.txt > /tmp/tests_shard.txt + + echo "Node $NODE_INDEX/$NODE_TOTAL running tests: $(cat /tmp/tests_shard.txt)" + xargs -n 1 -I {} gotestsum --format=testname \ + --junitfile=./tmp/test-results/results-$NODE_INDEX.xml \ + --jsonfile=./tmp/testlogs/log-$NODE_INDEX.json \ + -- -count=1 -parallel=$PARALLEL -coverprofile=coverage-$NODE_INDEX.out -timeout=60m -run '^{}$' \ + < /tmp/tests_shard.txt + + exit 0 + fi + + gotestsum --format=testname \ + --junitfile=./tmp/test-results/results.xml \ + --jsonfile=./tmp/testlogs/log.json \ + -- -count=1 -parallel=$PARALLEL -coverprofile=coverage.out -timeout=60m -run "{{test_name}}" + +# Run action tests for the interop client program on the native target +action-tests-interop test_name='TestInteropFaultProofs' *args='': + #!/bin/bash + + echo "Building host program for the native target" + just build-native --bin kona-host + export KONA_HOST_PATH="{{justfile_directory()}}/target/debug/kona-host" + + # GitHub actions patch - do not print logs. + # https://github.com/gotestyourself/gotestsum/blob/b4b13345fee56744d80016a20b760d3599c13504/testjson/format.go#L442-L444 + echo "Running action tests for the client program on the native target" + + cd {{SOURCE}}/../../op-e2e/actions/interop && GITHUB_ACTIONS=false gotestsum --format=testname -- -run "{{test_name}}" {{args}} -count=1 ./... + +action-tests-interop-build test_name='TestInteropFaultProofs' *args='': + #!/bin/bash + echo "Not implemented. See https://github.com/op-rs/kona/issues/3010" + +action-tests-interop-run test_name='TestInteropFaultProofs' *args='': + #!/bin/bash + echo "Not implemented. See https://github.com/op-rs/kona/issues/3010" + +update-packages: + #!/bin/bash + cd {{SOURCE}}/optimism/op-deployer && just build-contracts copy-contract-artifacts + cp {{SOURCE}}/optimism/op-deployer/pkg/deployer/artifacts/forge-artifacts/artifacts.tzst {{SOURCE}}/artifacts/compressed/artifacts.tzst + +# Updates the pinned version of the optimism +update-optimism: + #!/bin/bash + git submodule update --remote --force + diff --git a/kona/tests/node/common/conductor_test.go b/kona/tests/node/common/conductor_test.go new file mode 100644 index 0000000000000..a96a12a8325fb --- /dev/null +++ b/kona/tests/node/common/conductor_test.go @@ -0,0 +1,131 @@ +package node + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-conductor/consensus" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +type conductorWithInfo struct { + *dsl.Conductor + info consensus.ServerInfo +} + +// TestConductorLeadershipTransfer checks if the leadership transfer works correctly on the conductors +func TestConductorLeadershipTransfer(gt *testing.T) { + t := devtest.SerialT(gt) + logger := testlog.Logger(t, log.LevelInfo).With("Test", "TestConductorLeadershipTransfer") + + sys := node_utils.NewMixedOpKonaWithConductors(t) + tracer := t.Tracer() + ctx := t.Ctx() + logger.Info("Started Conductor Leadership Transfer test") + + for _, conductors := range sys.ConductorSets { + t.Gate().Greater(len(conductors), 0, "Expected at least one conductor in the system") + } + + ctx, span := tracer.Start(ctx, "test chains") + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // Test all L2 chains in the system + for l2Chain, conductors := range sys.ConductorSets { + chainId := l2Chain.String() + + _, span = tracer.Start(ctx, fmt.Sprintf("test chain %s", chainId)) + defer span.End() + + membership := conductors[0].FetchClusterMembership() + require.Equal(t, len(membership.Servers), len(conductors), "cluster membership does not match the number of conductors", "chainId", chainId) + + idToConductor := make(map[string]conductorWithInfo) + for _, conductor := range conductors { + conductorId := strings.TrimPrefix(conductor.String(), stack.ConductorKind.String()+"-") + idToConductor[conductorId] = conductorWithInfo{conductor, consensus.ServerInfo{}} + } + for _, memberInfo := range membership.Servers { + conductor, ok := idToConductor[memberInfo.ID] + require.True(t, ok, "unknown conductor in cluster membership", "unknown conductor id", memberInfo.ID, "chainId", chainId) + conductor.info = memberInfo + idToConductor[memberInfo.ID] = conductor + } + + leaderInfo, err := conductors[0].Escape().RpcAPI().LeaderWithID(ctx) + require.NoError(t, err, "failed to get current conductor info", "chainId", chainId) + + leaderConductor := idToConductor[leaderInfo.ID] + + voters := []conductorWithInfo{leaderConductor} + for _, member := range membership.Servers { + if member.ID == leaderInfo.ID || member.Suffrage == consensus.Nonvoter { + continue + } + + voters = append(voters, idToConductor[member.ID]) + } + + if len(voters) == 1 { + t.Skip("only one voter found in the cluster, skipping leadership transfer test") + continue + } + + t.Run(fmt.Sprintf("L2_Chain_%s", chainId), func(tt devtest.T) { + numOfLeadershipTransfers := len(voters) + for i := range numOfLeadershipTransfers { + oldLeaderIndex, newLeaderIndex := i%len(voters), (i+1)%len(voters) + oldLeader, newLeader := voters[oldLeaderIndex], voters[newLeaderIndex] + + time.Sleep(3 * time.Second) + + testTransferLeadershipAndCheck(t, oldLeader, newLeader) + } + }) + } +} + +// testTransferLeadershipAndCheck tests conductor's leadership transfer from one leader to another +func testTransferLeadershipAndCheck(t devtest.T, oldLeader, targetLeader conductorWithInfo) { + t.Run(fmt.Sprintf("Conductor_%s_to_%s", oldLeader, targetLeader), func(tt devtest.T) { + // ensure that the current and target leader are healthy and unpaused before transferring leadership + require.True(tt, oldLeader.FetchSequencerHealthy(), "current leader's sequencer is not healthy, id", oldLeader) + require.True(tt, targetLeader.FetchSequencerHealthy(), "target leader's sequencer is not healthy, id", targetLeader) + require.False(tt, oldLeader.FetchPaused(), "current leader's sequencer is paused, id", oldLeader) + require.False(tt, targetLeader.FetchPaused(), "target leader's sequencer is paused, id", targetLeader) + + // ensure that the current leader is the leader before transferring leadership + require.True(tt, oldLeader.IsLeader(), "current leader was not found to be the leader") + require.False(tt, targetLeader.IsLeader(), "target leader was already found to be the leader") + + oldLeader.TransferLeadershipTo(targetLeader.info) + + require.Eventually( + tt, + func() bool { return targetLeader.IsLeader() }, + 5*time.Second, 1*time.Second, "target leader was not found to be the leader", + ) + + require.False(tt, oldLeader.IsLeader(), "old leader was still found to be the leader") + + // sometimes leadership transfer can cause a very brief period of unhealthiness, + // but eventually, they should be healthy again + require.Eventually( + tt, + func() bool { return oldLeader.FetchSequencerHealthy() && targetLeader.FetchSequencerHealthy() }, + 3*time.Second, 1*time.Second, "at least one of the sequencers was found to be unhealthy", + ) + }) +} diff --git a/kona/tests/node/common/engine_test.go b/kona/tests/node/common/engine_test.go new file mode 100644 index 0000000000000..0820072f1aadb --- /dev/null +++ b/kona/tests/node/common/engine_test.go @@ -0,0 +1,51 @@ +package node + +import ( + "sync" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/stretchr/testify/require" +) + +func TestEngine(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + // Get the nodes from the network. + nodes := out.L2CLKonaNodes() + + wg := sync.WaitGroup{} + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + + queue := make(chan []uint64) + + // Spawn a task that gets the engine queue length with a ws connection. + go func() { + done := make(chan struct{}) + go func() { + // Wait for 40 unsafe blocks to be produced. + node.Advanced(types.LocalUnsafe, 40, 100) + done <- struct{}{} + }() + + queue <- node_utils.GetDevWS(t, node, "engine_queue_size", done) + }() + + q := <-queue + for _, q := range q { + require.LessOrEqual(t, q, uint64(1), "engine queue length should be 1 or less") + } + }(&node) + } + + wg.Wait() + +} diff --git a/kona/tests/node/common/init_test.go b/kona/tests/node/common/init_test.go new file mode 100644 index 0000000000000..af0d44b16b57e --- /dev/null +++ b/kona/tests/node/common/init_test.go @@ -0,0 +1,17 @@ +package node + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + config := node_utils.ParseL2NodeConfigFromEnv() + + fmt.Printf("Running e2e tests with Config: %d\n", config) + presets.DoMain(m, node_utils.WithMixedOpKona(config)) +} diff --git a/kona/tests/node/common/p2p_test.go b/kona/tests/node/common/p2p_test.go new file mode 100644 index 0000000000000..288836432fbe6 --- /dev/null +++ b/kona/tests/node/common/p2p_test.go @@ -0,0 +1,134 @@ +package node + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +func checkProtocols(t devtest.T, peer *apis.PeerInfo) { + nodeName := peer.PeerID.String() + + require.Contains(t, peer.Protocols, "/meshsub/1.0.0", fmt.Sprintf("%s is not using the meshsub protocol 1.0.0", nodeName)) + require.Contains(t, peer.Protocols, "/meshsub/1.1.0", fmt.Sprintf("%s is not using the meshsub protocol 1.1.0", nodeName)) + require.Contains(t, peer.Protocols, "/meshsub/1.2.0", fmt.Sprintf("%s is not using the meshsub protocol 1.2.0", nodeName)) + require.Contains(t, peer.Protocols, "/ipfs/id/1.0.0", fmt.Sprintf("%s is not using the id protocol 1.0.0", nodeName)) + require.Contains(t, peer.Protocols, "/ipfs/id/push/1.0.0", fmt.Sprintf("%s is not using the id push protocol 1.0.0", nodeName)) + require.Contains(t, peer.Protocols, "/floodsub/1.0.0", fmt.Sprintf("%s is not using the floodsub protocol 1.0.0", nodeName)) + +} + +// Check that the node has enough connected peers and peers in the discovery table. +func checkPeerStats(t devtest.T, node *dsl.L2CLNode, minConnected uint, minBlocksTopic uint) { + peerStats, err := node.Escape().P2PAPI().PeerStats(t.Ctx()) + nodeName := node.Escape().ID() + + require.NoError(t, err, "failed to get peer stats for %s", nodeName) + + require.GreaterOrEqual(t, peerStats.Connected, minConnected, fmt.Sprintf("%s has no connected peers", nodeName)) + require.GreaterOrEqual(t, peerStats.BlocksTopic, minBlocksTopic, fmt.Sprintf("%s has no peers in the blocks topic", nodeName)) + require.GreaterOrEqual(t, peerStats.BlocksTopicV2, minBlocksTopic, fmt.Sprintf("%s has no peers in the blocks topic v2", nodeName)) + require.GreaterOrEqual(t, peerStats.BlocksTopicV3, minBlocksTopic, fmt.Sprintf("%s has no peers in the blocks topic v3", nodeName)) + require.GreaterOrEqual(t, peerStats.BlocksTopicV4, minBlocksTopic, fmt.Sprintf("%s has no peers in the blocks topic v4", nodeName)) +} + +// Check that `node` is connected to the other node and exposes the expected protocols. +func arePeers(t devtest.T, node *dsl.L2CLNode, otherNodeId peer.ID) { + nodePeers := node.Peers() + + found := false + for _, peer := range nodePeers.Peers { + if peer.PeerID == otherNodeId { + // TODO(ethereum-optimism/optimism#18655): this test is flaky, we should fix it. + // require.Equal(t, network.Connected, peer.Connectedness, fmt.Sprintf("%s is not connected to the %s", node.Escape().ID(), otherNodeId)) + checkProtocols(t, peer) + found = true + } + } + require.True(t, found, fmt.Sprintf("%s is not in the %s's peers", otherNodeId, node.Escape().ID())) +} + +func TestP2PMinimal(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLNodes() + firstNode := nodes[0] + secondNode := nodes[1] + + opNodeId := firstNode.PeerInfo().PeerID + konaNodeId := secondNode.PeerInfo().PeerID + + // Wait for a few blocks to be produced. + dsl.CheckAll(t, secondNode.ReachedFn(types.LocalUnsafe, 40, 80), firstNode.ReachedFn(types.LocalUnsafe, 40, 80)) + + // Check that the nodes are connected to each other. + arePeers(t, &firstNode, konaNodeId) + arePeers(t, &secondNode, opNodeId) + + // Check that the nodes have enough connected peers and peers in the discovery table. + checkPeerStats(t, &firstNode, 1, 1) + checkPeerStats(t, &secondNode, 1, 1) +} + +// Check that, for every node in the network, all the peers are connected to the expected protocols and the same chainID. +func TestP2PProtocols(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLNodes() + + for _, node := range nodes { + for _, peer := range node.Peers().Peers { + checkProtocols(t, peer) + } + } +} + +func TestP2PChainID(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLKonaNodes() + + t.Gate().NotEmpty(nodes, "no KONA nodes found") + + chainID := nodes[0].PeerInfo().ChainID + + for _, node := range nodes { + nodeChainID, ok := node.Escape().ID().ChainID().Uint64() + require.True(t, ok, "chainID is too large for a uint64") + require.Equal(t, chainID, nodeChainID, fmt.Sprintf("%s has a different chainID", node.Escape().ID())) + + for _, peer := range node.Peers().Peers { + // Sometimes peers don't have a chainID because they are not part of the discovery table while being connected to gossip. + if peer.ChainID != 0 { + require.Equal(t, chainID, peer.ChainID, fmt.Sprintf("%s has a different chainID", node.Escape().ID())) + } + } + } +} + +// Check that all the nodes in the network have enough connected peers and peers in the discovery table. +func TestNetworkConnectivity(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLNodes() + numNodes := len(nodes) + + for _, node := range nodes { + checkPeerStats(t, &node, uint(numNodes)-1, uint(numNodes)/2) + } +} diff --git a/kona/tests/node/common/rpc_test.go b/kona/tests/node/common/rpc_test.go new file mode 100644 index 0000000000000..5d36a88e0fd59 --- /dev/null +++ b/kona/tests/node/common/rpc_test.go @@ -0,0 +1,196 @@ +package node + +import ( + "sync" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// Check that the node p2p RPC endpoints are working. +func TestP2PPeers(gt *testing.T) { + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKona(t) + + p2pPeersAndPeerStats(t, out) + + p2pSelfAndPeers(t, out) + + p2pBanPeer(t, out) +} + +// Ensure that the `opp2p_peers` and `opp2p_self` RPC endpoints return the same information. +func p2pSelfAndPeers(t devtest.T, out *node_utils.MixedOpKonaPreset) { + nodes := out.L2CLKonaNodes() + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + clRPC := node_utils.GetNodeRPCEndpoint(node) + clName := node.Escape().ID().Key() + + // Gather the peers for the node. + peers := &apis.PeerDump{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peers", peers, true), "failed to send RPC request to node %s: %s", clName) + + // Check that every peer's info matches the node's info. + for _, peer := range peers.Peers { + // Find the node that is the peer. We loop over all the nodes in the network and try to match their peerID's to + // the peerID we are looking for. + for _, node := range nodes { + // We get the peer's info. + otherPeerInfo := &apis.PeerInfo{} + otherCLRPC := node_utils.GetNodeRPCEndpoint(&node) + otherCLName := node.Escape().ID().Key() + require.NoError(t, node_utils.SendRPCRequest(otherCLRPC, "opp2p_self", otherPeerInfo), "failed to send RPC request to node %s: %s", clName) + + // These checks fail for the op-node. It seems that their p2p handler is flaky and doesn't always return the correct peer info. + if otherPeerInfo.PeerID == peer.PeerID { + require.Equal(t, otherPeerInfo.NodeID, peer.NodeID, "nodeID mismatch, %s", otherCLName) + require.Equal(t, otherPeerInfo.ProtocolVersion, peer.ProtocolVersion, "protocolVersion mismatch, %s", otherCLName) + + // Sometimes the node is not part of the discovery table so we don't have an ENR. + if peer.ENR != "" { + require.Equal(t, otherPeerInfo.ENR, peer.ENR, "ENR mismatch, %s", otherCLName) + } + + // Sometimes the node is not part of the discovery table so we don't have a valid chainID. + if peer.ChainID != 0 { + require.Equal(t, otherPeerInfo.ChainID, peer.ChainID, "chainID mismatch, %s", otherCLName) + } + + for _, addr := range peer.Addresses { + require.Contains(t, otherPeerInfo.Addresses, addr, "the peer's address should be in the node's known addresses, %s", otherCLName) + } + + for _, protocol := range peer.Protocols { + require.Contains(t, otherPeerInfo.Protocols, protocol, "protocol %s not found, %s", protocol, otherCLName) + } + + require.Equal(t, otherPeerInfo.UserAgent, peer.UserAgent, "userAgent mismatch, %s", otherCLName) + } + } + } + }(&node) + } + wg.Wait() +} + +// Check that the `opp2p_peers` and `opp2p_peerStats` RPC endpoints return coherent information. +func p2pPeersAndPeerStats(t devtest.T, out *node_utils.MixedOpKonaPreset) { + nodes := out.L2CLNodes() + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + clRPC := node_utils.GetNodeRPCEndpoint(node) + clName := node.Escape().ID().Key() + + peers := &apis.PeerDump{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peers", peers, true), "failed to send RPC request to node %s: %s", clName) + + peerStats := &apis.PeerStats{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peerStats", peerStats), "failed to send RPC request to node %s: %s", clName) + + require.Equal(t, peers.TotalConnected, peerStats.Connected, "totalConnected mismatch node %s", clName) + require.Equal(t, len(peers.Peers), int(peers.TotalConnected), "peer count mismatch node %s", clName) + }(&node) + } + wg.Wait() +} + +func p2pBanPeer(t devtest.T, out *node_utils.MixedOpKonaPreset) { + nodes := out.L2CLNodes() + for _, node := range nodes { + clRPC := node_utils.GetNodeRPCEndpoint(&node) + clName := node.Escape().ID().Key() + + peers := &apis.PeerDump{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peers", peers, true), "failed to send RPC request to node %s: %s", clName) + + connectedPeers := peers.TotalConnected + + // Try to ban a peer. + // We pick the first peer that is connected. + peerToBan := "" + for _, peer := range peers.Peers { + peerToBan = peer.PeerID.String() + break + } + + require.NotEmpty(t, peerToBan, "no connected peer found") + + require.NoError(t, node_utils.SendRPCRequest[any](clRPC, "opp2p_blockPeer", nil, peerToBan), "failed to send RPC request to node %s: %s", clName) + + // Check that the peer is banned. + peersAfterBan := &apis.PeerDump{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peers", peersAfterBan, true), "failed to send RPC request to node %s: %s", clName) + + require.Equal(t, connectedPeers, peersAfterBan.TotalConnected, "totalConnected mismatch node %s", clName) + + contains := false + // Loop over all the banned peers and check that the peer is banned. + for _, bannedPeer := range peersAfterBan.BannedPeers { + if bannedPeer.String() == peerToBan { + require.Equal(t, bannedPeer.String(), peerToBan, "peer %s not banned", peerToBan) + contains = true + } + } + + require.True(t, contains, "peer %s not banned", peerToBan) + + // Try to unban the peer. + require.NoError(t, node_utils.SendRPCRequest[any](clRPC, "opp2p_unblockPeer", nil, peerToBan), "failed to send RPC request to node %s: %s", clName) + + // Check that the peer is unbanned. + peersAfterUnban := &apis.PeerDump{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "opp2p_peers", peersAfterUnban, true), "failed to send RPC request to node %s: %s", clName) + + require.Equal(t, connectedPeers, peersAfterUnban.TotalConnected, "totalConnected mismatch node %s", clName) + require.NotContains(t, peersAfterUnban.BannedPeers, peerToBan, "peer %s is banned", peerToBan) + } +} + +func rollupConfig(t devtest.T, node *dsl.L2CLNode) *rollup.Config { + clRPC := node_utils.GetNodeRPCEndpoint(node) + clName := node.Escape().ID().Key() + + rollupConfig := &rollup.Config{} + require.NoError(t, node_utils.SendRPCRequest(clRPC, "optimism_rollupConfig", rollupConfig), "failed to send RPC request to node %s: %s", clName) + + return rollupConfig +} + +func rollupConfigMatches(t devtest.T, configA *rollup.Config, configB *rollup.Config) { + // ProtocolVersionsAddress is deprecated in kona-node while not yet removed from the op-node. + configA.ProtocolVersionsAddress = common.Address{} + configB.ProtocolVersionsAddress = common.Address{} + + require.Equal(t, configA, configB, "rollup config mismatch") +} + +func TestRollupConfig(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + rollupConfigs := make([]*rollup.Config, 0) + + for _, node := range out.L2CLNodes() { + rollupConfigs = append(rollupConfigs, rollupConfig(t, &node)) + } + + // Check that the rollup configs are the same. + for _, config := range rollupConfigs { + rollupConfigMatches(t, rollupConfigs[0], config) + } +} diff --git a/kona/tests/node/common/sync_test.go b/kona/tests/node/common/sync_test.go new file mode 100644 index 0000000000000..de3aa0975d22c --- /dev/null +++ b/kona/tests/node/common/sync_test.go @@ -0,0 +1,64 @@ +package node + +import ( + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// Check that all the nodes in the network are synced to the local safe block and can catch up to the sequencer node. +func TestL2SafeSync(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + sequencer := out.L2CLSequencerNodes()[0] + nodes := out.L2CLValidatorNodes() + + checkFuns := make([]dsl.CheckFunc, 0, 2*len(nodes)) + + for _, node := range nodes { + checkFuns = append(checkFuns, node.ReachedFn(types.LocalSafe, 20, 40)) + checkFuns = append(checkFuns, node_utils.MatchedWithinRange(t, node, sequencer, 5, types.LocalSafe, 100)) + } + + dsl.CheckAll(t, checkFuns...) +} + +// Check that all the nodes in the network are synced to the local unsafe block and can catch up to the sequencer node. +func TestL2UnsafeSync(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLNodes() + + checkFuns := make([]dsl.CheckFunc, 0, len(nodes)) + + for _, node := range nodes { + checkFuns = append(checkFuns, node.ReachedFn(types.LocalUnsafe, 40, 80)) + } + + dsl.CheckAll(t, checkFuns...) +} + +// Check that all the kona nodes in the network are synced to the finalized block. +func TestL2FinalizedSync(gt *testing.T) { + t := devtest.ParallelT(gt) + t.Skip("Skipping finalized sync test") + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLNodes() + + checkFuns := make([]dsl.CheckFunc, 0, len(nodes)) + + for _, node := range nodes { + checkFuns = append(checkFuns, node.ReachedFn(types.Finalized, 10, 600)) + } + + dsl.CheckAll(t, checkFuns...) +} diff --git a/kona/tests/node/common/sync_ws_test.go b/kona/tests/node/common/sync_ws_test.go new file mode 100644 index 0000000000000..74c74e7643093 --- /dev/null +++ b/kona/tests/node/common/sync_ws_test.go @@ -0,0 +1,224 @@ +package node + +import ( + "sync" + "testing" + "time" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/stretchr/testify/require" +) + +// Check that unsafe heads eventually consolidate and become safe +// For this test to be deterministic it should... +// 1. Only have two L2 nodes +// 2. Only have one DA layer node +func TestSyncUnsafeBecomesSafe(gt *testing.T) { + const SECS_WAIT_FOR_UNSAFE_HEAD = 10 + // We are waiting longer for the safe head to sync because it is usually a few seconds behind the unsafe head. + const SECS_WAIT_FOR_SAFE_HEAD = 60 + + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLKonaNodes() + + // Ensure that all the nodes advance the unsafe and safe head + advancedFns := make([]dsl.CheckFunc, 0, len(nodes)) + for _, node := range nodes { + advancedFns = append(advancedFns, node.AdvancedFn(types.LocalSafe, 20, 80)) + advancedFns = append(advancedFns, node.AdvancedFn(types.LocalUnsafe, 20, 80)) + } + dsl.CheckAll(t, advancedFns...) + + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + + unsafeBlocks := node_utils.GetKonaWs(t, node, "unsafe_head", time.After(SECS_WAIT_FOR_UNSAFE_HEAD*time.Second)) + + safeBlocks := node_utils.GetKonaWs(t, node, "safe_head", time.After(SECS_WAIT_FOR_SAFE_HEAD*time.Second)) + + require.GreaterOrEqual(t, len(unsafeBlocks), 1, "we didn't receive enough unsafe gossip blocks!") + require.GreaterOrEqual(t, len(safeBlocks), 1, "we didn't receive enough safe gossip blocks!") + + safeBlockMap := make(map[uint64]eth.L2BlockRef) + // Create a map of safe blocks with block number as the key + for _, safeBlock := range safeBlocks { + safeBlockMap[safeBlock.Number] = safeBlock + } + + cond := false + + // Iterate over unsafe blocks and find matching safe blocks + for _, unsafeBlock := range unsafeBlocks { + if safeBlock, exists := safeBlockMap[unsafeBlock.Number]; exists { + require.Equal(t, unsafeBlock, safeBlock, "unsafe block %d doesn't match safe block %d", unsafeBlock.Number, safeBlock.Number) + cond = true + } + } + + require.True(t, cond, "No matching safe block found for unsafe block") + + t.Log("✓ unsafe and safe head blocks match between all nodes") + }(&node) + } + wg.Wait() +} + +// System tests that ensure that the kona-nodes are syncing the unsafe chain. +// Note: this test should only be ran on networks that don't reorg, only have one sequencer and that only have one DA layer node. +func TestSyncUnsafe(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLKonaNodes() + + // Ensure that all the nodes advance the unsafe head + advancedFns := make([]dsl.CheckFunc, 0, len(nodes)) + for _, node := range nodes { + advancedFns = append(advancedFns, node.AdvancedFn(types.LocalUnsafe, 20, 80)) + } + dsl.CheckAll(t, advancedFns...) + + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + + output := node_utils.GetKonaWs(t, node, "unsafe_head", time.After(2*time.Minute)) + + // For each block, we check that the block is actually in the chain of the other nodes. + // That should always be the case unless there is a reorg or a long sync. + // We shouldn't have safe heads reorgs in this very simple testnet because there is only one DA layer node. + for _, block := range output { + for _, node := range nodes { + otherCLNode := node.Escape().ID().Key() + otherCLSyncStatus := node.ChainSyncStatus(out.L2Chain.ChainID(), types.LocalUnsafe) + + if otherCLSyncStatus.Number < block.Number { + t.Log("✗ peer too far behind!", otherCLNode, block.Number, otherCLSyncStatus.Number) + continue + } + + expectedOutputResponse, err := node.Escape().RollupAPI().OutputAtBlock(t.Ctx(), block.Number) + require.NoError(t, err, "impossible to get block from node %s", otherCLNode) + + // Make sure the blocks match! + require.Equal(t, expectedOutputResponse.BlockRef, block, "block mismatch between %s and %s", otherCLNode, node.Escape().ID().Key()) + } + } + + t.Log("✓ unsafe head blocks match between all nodes") + }(&node) + } + wg.Wait() +} + +// System tests that ensure that the kona-nodes are syncing the safe chain. +// Note: this test should only be ran on networks that don't reorg and that only have one DA layer node. +func TestSyncSafe(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLKonaNodes() + + // Ensure that all the nodes advance the safe head + advancedFns := make([]dsl.CheckFunc, 0, len(nodes)) + for _, node := range nodes { + advancedFns = append(advancedFns, node.AdvancedFn(types.LocalSafe, 20, 80)) + } + dsl.CheckAll(t, advancedFns...) + + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + clName := node.Escape().ID().Key() + + output := node_utils.GetKonaWs(t, node, "safe_head", time.After(2*time.Minute)) + + // For each block, we check that the block is actually in the chain of the other nodes. + // That should always be the case unless there is a reorg or a long sync. + // We shouldn't have safe heads reorgs in this very simple testnet because there is only one DA layer node. + for _, block := range output { + for _, node := range nodes { + otherCLNode := node.Escape().ID().Key() + otherCLSyncStatus := node.ChainSyncStatus(out.L2Chain.ChainID(), types.LocalSafe) + + if otherCLSyncStatus.Number < block.Number { + t.Log("✗ peer too far behind!", otherCLNode, block.Number, otherCLSyncStatus.Number) + continue + } + + expectedOutputResponse, err := node.Escape().RollupAPI().OutputAtBlock(t.Ctx(), block.Number) + require.NoError(t, err, "impossible to get block from node %s", otherCLNode) + + // Make sure the blocks match! + require.Equal(t, expectedOutputResponse.BlockRef, block, "block mismatch between %s and %s", otherCLNode, clName) + } + } + + t.Log("✓ safe head blocks match between all nodes") + }(&node) + } + wg.Wait() +} + +// System tests that ensure that the kona-nodes are syncing the finalized chain. +// Note: this test can be ran on any sort of network, including the ones that should reorg. +func TestSyncFinalized(gt *testing.T) { + t := devtest.ParallelT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLKonaNodes() + + var wg sync.WaitGroup + for _, node := range nodes { + wg.Add(1) + go func(node *dsl.L2CLNode) { + defer wg.Done() + clName := node.Escape().ID().Key() + + output := node_utils.GetKonaWs(t, node, "finalized_head", time.After(4*time.Minute)) + + // We should check that we received at least 1 finalized block within 4 minutes! + require.GreaterOrEqual(t, len(output), 1, "we didn't receive enough finalized gossip blocks!") + t.Log("Number of finalized blocks received within 4 minutes:", len(output)) + + // For each block, we check that the block is actually in the chain of the other nodes. + for _, block := range output { + for _, node := range nodes { + otherCLNode := node.Escape().ID().Key() + otherCLSyncStatus := node.ChainSyncStatus(out.L2Chain.ChainID(), types.Finalized) + + if otherCLSyncStatus.Number < block.Number { + t.Log("✗ peer too far behind!", otherCLNode, block.Number, otherCLSyncStatus.Number) + continue + } + + expectedOutputResponse, err := node.Escape().RollupAPI().OutputAtBlock(t.Ctx(), block.Number) + require.NoError(t, err, "impossible to get block from node %s", otherCLNode) + + // Make sure the blocks match! + require.Equal(t, expectedOutputResponse.BlockRef, block, "block mismatch between %s and %s", otherCLNode, clName) + } + } + + t.Log("✓ finalized head blocks match between all nodes") + }(&node) + } + wg.Wait() +} diff --git a/kona/tests/node/common/tx_inclusion_test.go b/kona/tests/node/common/tx_inclusion_test.go new file mode 100644 index 0000000000000..adbc057dd5ff9 --- /dev/null +++ b/kona/tests/node/common/tx_inclusion_test.go @@ -0,0 +1,49 @@ +package node + +import ( + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func TestL2TransactionInclusion(gt *testing.T) { + t := devtest.SerialT(gt) + out := node_utils.NewMixedOpKona(t) + + originNode := out.L2ELSequencerNodes()[0] + funder := dsl.NewFunder(out.Wallet, out.Faucet, originNode) + + user := funder.NewFundedEOA(eth.OneEther) + to := out.Wallet.NewEOA(originNode) + toInitialBalance := to.GetBalance() + tx := user.Transfer(to.Address(), eth.HalfEther) + + inclusionBlock, err := tx.IncludedBlock.Eval(t.Ctx()) + if err != nil { + gt.Fatal("transaction receipt not found", "error", err) + } + + // Ensure the block containing the transaction has propagated to the rest of the network. + for _, node := range out.L2ELNodes() { + block := node.WaitForBlockNumber(inclusionBlock.Number) + blockID := block.Hash() + blockNumber := block.NumberU64() + + // It's possible that the block has already been included, and `WaitForBlockNumber` returns a block + // at a taller height. + if blockNumber > inclusionBlock.Number { + blockID = node.BlockRefByNumber(inclusionBlock.Number).Hash + } + + // Ensure that the block ID matches the expected inclusion block hash. + if blockID != inclusionBlock.Hash { + gt.Fatal("transaction not included in block", "node", node.String(), "expectedBlockHash", inclusionBlock.Hash, "actualBlockHash", blockID) + } + + // Ensure that the recipient's balance has been updated in the eyes of the EL node. + to.AsEL(node).VerifyBalanceExact(toInitialBalance.Add(eth.HalfEther)) + } +} diff --git a/kona/tests/node/long-running/README.md b/kona/tests/node/long-running/README.md new file mode 100644 index 0000000000000..536c074eea57d --- /dev/null +++ b/kona/tests/node/long-running/README.md @@ -0,0 +1,11 @@ +## Long-running system tests + +Those tests are meant to be *long-running*, meaning that they're not going to stop unless user input is received. + +These tests are useful to simulate realistic network conditions in sysgo networks. + +### How to run them? + +``` + just long-running-test {OPTIONAL_TEST_FILTER} {OPTIONAL_LOG_STORAGE_PATH} +``` \ No newline at end of file diff --git a/kona/tests/node/long-running/init_test.go b/kona/tests/node/long-running/init_test.go new file mode 100644 index 0000000000000..4efdc2319df06 --- /dev/null +++ b/kona/tests/node/long-running/init_test.go @@ -0,0 +1,32 @@ +package node + +import ( + "flag" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +var ( + num_threads = flag.Int("num-threads", 10, "number of threads to use for the test") + percentageNewAccounts = flag.Int("percentage-new-accounts", 20, "percentage of new accounts to produce transactions for") + fundAmount = flag.Int("fund-amount", 10, "eth amount to fund each new account with") + initNumAccounts = flag.Int("init-num-accounts", 10, "initial number of accounts to fund") +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + flag.Parse() + + presets.DoMain(m, node_utils.WithMixedOpKona(node_utils.L2NodeConfig{ + OpSequencerNodesWithGeth: 0, + OpSequencerNodesWithReth: 0, + KonaSequencerNodesWithGeth: 1, + KonaSequencerNodesWithReth: 0, + OpNodesWithGeth: 1, + OpNodesWithReth: 1, + KonaNodesWithGeth: 1, + KonaNodesWithReth: 1, + })) +} diff --git a/kona/tests/node/long-running/tx_producer_test.go b/kona/tests/node/long-running/tx_producer_test.go new file mode 100644 index 0000000000000..7a1b1f9932d45 --- /dev/null +++ b/kona/tests/node/long-running/tx_producer_test.go @@ -0,0 +1,182 @@ +package node + +import ( + "math/rand" + "sync" + "sync/atomic" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" +) + +// Define a global atomic counter for the number of transactions produced. +var ( + txProduced = atomic.Int64{} +) + +type TxProducer struct { + t devtest.T + out *node_utils.MixedOpKonaPreset + accounts []*dsl.EOA + // Unique identifier for the producer/receiver pair + idx int + pending_txs chan<- *txplan.PlannedTx +} + +type TxReceiver struct { + t devtest.T + out *node_utils.MixedOpKonaPreset + // Unique identifier for the producer/receiver pair + idx int + txs <-chan *txplan.PlannedTx +} + +func (tp *TxProducer) NewFunder() *dsl.Funder { + return dsl.NewFunder(tp.out.Wallet, tp.out.Faucet, tp.out.L2ELSequencerNodes()[0]) +} + +func (tp *TxProducer) NewAccounts(count int, fundAmount eth.ETH) []*dsl.EOA { + new_accounts := tp.NewFunder().NewFundedEOAs(count, fundAmount) + tp.accounts = append(tp.accounts, new_accounts...) + + return new_accounts +} + +func (tp *TxProducer) NewAccount(fundAmount eth.ETH) *dsl.EOA { + new_account := tp.NewFunder().NewFundedEOA(fundAmount) + tp.accounts = append(tp.accounts, new_account) + + return new_account +} + +func NewTxProducer(t devtest.T, out *node_utils.MixedOpKonaPreset, txs chan<- *txplan.PlannedTx, idx int) *TxProducer { + return &TxProducer{ + out: out, + t: t, + accounts: []*dsl.EOA{}, + pending_txs: txs, + idx: idx, + } +} + +func (tp *TxProducer) Start(wg *sync.WaitGroup) { + // Initialize the accounts + tp.NewAccounts(*initNumAccounts, eth.Ether(uint64(*fundAmount))) + tp.t.Logf("%d accounts initialized", *initNumAccounts) + + wg.Add(1) + go func() { + defer wg.Done() + for { + var toAccount *dsl.EOA + if rand.Intn(100) < *percentageNewAccounts { + toAccount = tp.NewAccount(eth.Ether(uint64(*fundAmount))) + } else { + toAccount = tp.accounts[rand.Intn(len(tp.accounts))] + } + + fromAccount := tp.accounts[rand.Intn(len(tp.accounts))] + + if fromAccount.GetBalance().Lt(eth.HalfEther) { + tp.NewFunder().FundAtLeast(fromAccount, eth.HalfEther) + } + + amount := fromAccount.GetBalance().Mul(uint64(rand.Intn(100))).Div(100) + + tp.t.Logf("producer %d: producing transaction from %s to %s with amount %s", tp.idx, fromAccount.Address(), toAccount.Address(), amount) + + new_planned_txs := fromAccount.Transact(fromAccount.PlanTransfer(toAccount.Address(), amount)) + + tp.t.Logf("producer %d: transaction produced with hash: %s", tp.idx, new_planned_txs.Signed.Value().Hash()) + + tp.pending_txs <- new_planned_txs + } + }() +} + +func NewTxReceiver(t devtest.T, out *node_utils.MixedOpKonaPreset, txs <-chan *txplan.PlannedTx, idx int) *TxReceiver { + return &TxReceiver{ + t: t, + txs: txs, + idx: idx, + out: out, + } +} + +func (tr *TxReceiver) processTx(tx *txplan.PlannedTx) { + inclusionBlock, err := tx.IncludedBlock.Eval(tr.t.Ctx()) + if err != nil { + tr.t.Errorf("producer %d: transaction (hash %s) receipt not found. error: %s", tr.idx, tx.Signed.Value().Hash(), err) + return + } + + _, err = tx.Success.Eval(tr.t.Ctx()) + if err != nil { + tr.t.Errorf("producer %d: transaction (hash %s) failed. error: %s", tr.idx, tx.Signed.Value().Hash(), err) + } + + // Ensure the block containing the transaction has propagated to the rest of the network. + for _, node := range tr.out.L2ELNodes() { + block := node.WaitForBlockNumber(inclusionBlock.Number) + blockID := block.Hash() + + // It's possible that the block has already been included, and `WaitForBlockNumber` returns a block + // at a taller height. + if block.NumberU64() > inclusionBlock.Number { + blockID = node.BlockRefByNumber(inclusionBlock.Number).Hash + } + + // Ensure that the block ID matches the expected inclusion block hash. + if blockID != inclusionBlock.Hash { + tr.t.Errorf("producer %d: transaction (hash %s) not included in block %d with hash %s.", tr.idx, tx.Signed.Value().Hash(), inclusionBlock.Number, inclusionBlock.Hash) + } + } + + txProduced.Add(1) + tr.t.Logf("producer %d: transaction (hash %s) included in block %d with hash %s. %d transactions produced.", tr.idx, tx.Signed.Value().Hash(), inclusionBlock.Number, inclusionBlock.Hash, txProduced.Load()) +} + +func (tr *TxReceiver) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-tr.t.Ctx().Done(): + tr.t.Logf("receiver context done") + return + case tx := <-tr.txs: + tr.processTx(tx) + } + } + }() + +} + +// Produces transactions in a loop. Ensures that... +// - transactions get included +// - transactions get gossiped +func TestTxProducer(gt *testing.T) { + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKona(t) + + var wg sync.WaitGroup + + for i := 0; i < *num_threads; i++ { + txs := make(chan *txplan.PlannedTx) + txProducer := NewTxProducer(t, out, txs, i) + txReceiver := NewTxReceiver(t, out, txs, i) + + txProducer.Start(&wg) + txReceiver.Start(&wg) + } + + wg.Wait() + + t.Logf("producer and receiver threads finished") +} diff --git a/kona/tests/node/reorgs/init_test.go b/kona/tests/node/reorgs/init_test.go new file mode 100644 index 0000000000000..ec943d524efbd --- /dev/null +++ b/kona/tests/node/reorgs/init_test.go @@ -0,0 +1,18 @@ +package reorgs + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + l2Config := node_utils.ParseL2NodeConfigFromEnv() + + fmt.Printf("Running e2e reorg tests with Config: %d\n", l2Config) + + presets.DoMain(m, node_utils.WithMixedWithTestSequencer(l2Config)) +} diff --git a/kona/tests/node/reorgs/l2_reorg_after_l1_reorgs_test.go b/kona/tests/node/reorgs/l2_reorg_after_l1_reorgs_test.go new file mode 100644 index 0000000000000..b40ac4584ff5c --- /dev/null +++ b/kona/tests/node/reorgs/l2_reorg_after_l1_reorgs_test.go @@ -0,0 +1,174 @@ +package reorgs + +import ( + "testing" + "time" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type checksFunc func(t devtest.T, sys *node_utils.MinimalWithTestSequencersPreset) + +func TestL2ReorgAfterL1Reorg(gt *testing.T) { + gt.Run("unsafe reorg", func(gt *testing.T) { + var localSafeRef, unsafeRef []eth.L2BlockRef + pre := func(t devtest.T, sys *node_utils.MinimalWithTestSequencersPreset) { + for _, elNode := range sys.L2ELNodes() { + localSafeRef = append(localSafeRef, elNode.BlockRefByLabel(eth.Safe)) + unsafeRef = append(unsafeRef, elNode.BlockRefByLabel(eth.Unsafe)) + } + } + post := func(t devtest.T, sys *node_utils.MinimalWithTestSequencersPreset) { + for i, elNode := range sys.L2ELNodes() { + require.True(t, elNode.IsCanonical(localSafeRef[i].ID()), "Previous local-safe block should still be canonical") + require.False(t, elNode.IsCanonical(unsafeRef[i].ID()), "Previous unsafe block should have been reorged") + } + } + testL2ReorgAfterL1Reorg(gt, 3, pre, post) + }) + + gt.Run("unsafe, local-safe, cross-unsafe, cross-safe reorgs", func(gt *testing.T) { + var localSafeRef, unsafeRef []eth.L2BlockRef + pre := func(t devtest.T, sys *node_utils.MinimalWithTestSequencersPreset) { + for _, elNode := range sys.L2ELNodes() { + localSafeRef = append(localSafeRef, elNode.BlockRefByLabel(eth.Safe)) + unsafeRef = append(unsafeRef, elNode.BlockRefByLabel(eth.Unsafe)) + } + } + post := func(t devtest.T, sys *node_utils.MinimalWithTestSequencersPreset) { + for i, elNode := range sys.L2ELNodes() { + require.False(t, elNode.IsCanonical(unsafeRef[i].ID()), "Previous unsafe block should have been reorged", "elNode", elNode.ID(), "unsafeRef", unsafeRef[i].ID()) + require.False(t, elNode.IsCanonical(localSafeRef[i].ID()), "Previous local-safe block should have been reorged", "elNode", elNode.ID(), "localSafeRef", localSafeRef[i].ID()) + } + } + testL2ReorgAfterL1Reorg(gt, 20, pre, post) + }) +} + +// testL2ReorgAfterL1Reorg tests that the L2 chain reorgs after an L1 reorg, and takes n, number of blocks to reorg, as parameter +// for unsafe reorgs - n must be at least >= confDepth, which is 2 in our test deployments +// for cross-safe reorgs - n must be at least >= safe distance, which is 10 in our test deployments +// pre- and post-checks are sanity checks to ensure that the blocks we expected to be reorged were indeed reorged or not +func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksFunc) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := node_utils.NewMixedOpKonaWithTestSequencer(t) + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + sys.L1Network.WaitForBlock() + + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + // sequence a few L1 and L2 blocks + for range n + 1 { + sequenceL1Block(t, ts, common.Hash{}) + + sys.L2Chain.WaitForBlock() + sys.L2Chain.WaitForBlock() + } + + // select a divergence block to reorg from + var divergence eth.L1BlockRef + { + tip := sys.L1EL.BlockRefByLabel(eth.Unsafe) + require.Greater(t, tip.Number, uint64(n), "n is larger than L1 tip, cannot reorg out block number `tip-n`") + + divergence = sys.L1EL.BlockRefByNumber(tip.Number - uint64(n)) + } + + // print the chains before sequencing an alternative L1 block + sys.L2Chain.PrintChain() + sys.L1Network.PrintChain() + + // pre reorg trigger validations and checks + preChecks(t, sys) + + tipL2_preReorg := sys.L2ELSequencerNodes()[0].BlockRefByLabel(eth.Unsafe) + + // reorg the L1 chain -- sequence an alternative L1 block from divergence block parent + sequenceL1Block(t, ts, divergence.ParentHash) + + // continue building on the alternative L1 chain + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // confirm L1 reorged + sys.L1EL.ReorgTriggered(divergence, 5) + + // wait until L2 chain cross-safe ref caught up to where it was before the reorg + var waitFunc []dsl.CheckFunc + for _, clNode := range sys.L2CLNodes() { + waitFunc = append(waitFunc, clNode.ReachedFn(types.CrossSafe, tipL2_preReorg.Number, 200)) + } + + dsl.CheckAll(t, waitFunc...) + + // test that latest chain unsafe is not referencing a reorged L1 block (through the L1Origin field) + require.Eventually(t, func() bool { + for _, elNode := range sys.L2ELNodes() { + unsafe := elNode.BlockRefByLabel(eth.Unsafe) + + block, err := sys.L1EL.Escape().EthClient().InfoByNumber(ctx, unsafe.L1Origin.Number) + if err != nil { + sys.Log.Warn("failed to get L1 block info by number", "number", unsafe.L1Origin.Number, "err", err) + return false + } + + sys.Log.Info("current unsafe ref", "tip", unsafe, "tip_origin", unsafe.L1Origin, "l1blk", eth.InfoToL1BlockRef(block)) + + if block.Hash() != unsafe.L1Origin.Hash { + return false + } + } + + return true + }, 120*time.Second, 7*time.Second, "L1 block origin hash should match hash of block on L1 at that number. If not, it means there was a reorg, and L2 blocks L1Origin field is referencing a reorged block.") + + // confirm all L1Origin fields point to canonical blocks + require.Eventually(t, func() bool { + for _, elNode := range sys.L2ELNodes() { + ref := elNode.BlockRefByLabel(eth.Unsafe) + var err error + + // wait until L2 chain's L1Origin points to a L1 block after the one that was reorged + if ref.L1Origin.Number < divergence.Number { + return false + } + + sys.Log.Info("L2 chain progressed, pointing to newer L1 block", "ref", ref, "ref_origin", ref.L1Origin, "divergence", divergence) + + for i := ref.Number; i > 0 && ref.L1Origin.Number >= divergence.Number; i-- { + ref, err = elNode.Escape().L2EthClient().L2BlockRefByNumber(ctx, i) + if err != nil { + return false + } + + if !sys.L1EL.IsCanonical(ref.L1Origin) { + return false + } + } + } + + return true + }, 120*time.Second, 5*time.Second, "all L1Origin fields should point to canonical L1 blocks") + + // post reorg test validations and checks + postChecks(t, sys) +} + +func sequenceL1Block(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash) { + require.NoError(t, ts.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent})) + require.NoError(t, ts.Next(t.Ctx())) +} diff --git a/kona/tests/node/reorgs/l2_reorg_test.go b/kona/tests/node/reorgs/l2_reorg_test.go new file mode 100644 index 0000000000000..dcba40270dd24 --- /dev/null +++ b/kona/tests/node/reorgs/l2_reorg_test.go @@ -0,0 +1,146 @@ +package reorgs + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/stretchr/testify/require" +) + +func TestL2Reorg(gt *testing.T) { + gt.Skip("Skipping l2 reorg test because the L2 test sequencer is flaky") + const NUM_BLOCKS_TO_REORG = 5 + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKonaWithTestSequencer(t) + sequencerCL := out.L2CLSequencerNodes()[0] + sequencerEL := out.L2ELSequencerNodes()[0] + + funder := dsl.NewFunder(out.Wallet, out.Faucet, sequencerEL) + // three EOAs for triggering transfers + alice := funder.NewFundedEOA(eth.OneHundredthEther) + bob := funder.NewFundedEOA(eth.OneHundredthEther) + + advancedFnsPreReorg := make([]dsl.CheckFunc, 0, len(out.L2CLNodes())) + + // Wait for the nodes to advance a little bit + for _, node := range out.L2CLNodes() { + advancedFnsPreReorg = append(advancedFnsPreReorg, node.AdvancedFn(types.LocalUnsafe, 20, 40)) + } + + dsl.CheckAll(t, advancedFnsPreReorg...) + + unsafeHead := sequencerEL.BlockRefByLabel(eth.Unsafe) + + advancedFnsReorgedBlocks := make([]dsl.CheckFunc, 0, len(out.L2CLNodes())) + // Wait for the nodes to advance a little bit more ahead the unsafe head + for _, node := range out.L2CLNodes() { + advancedFnsReorgedBlocks = append(advancedFnsReorgedBlocks, node.AdvancedFn(types.LocalUnsafe, NUM_BLOCKS_TO_REORG, 2*NUM_BLOCKS_TO_REORG)) + } + dsl.CheckAll(t, advancedFnsReorgedBlocks...) + + checksPostReorg := []dsl.CheckFunc{} + // Ensure all the nodes reorg as expected... + for _, node := range out.L2ELSequencerNodes() { + reorgedHead := node.BlockRefByLabel(eth.Unsafe) + require.Greater(t, reorgedHead.Number, unsafeHead.Number) + checksPostReorg = append(checksPostReorg, node.ReorgTriggeredFn(unsafeHead, 40)) + } + + // Ensure that all the nodes still advance even after the reorg + for _, node := range out.L2CLNodes() { + checksPostReorg = append(checksPostReorg, node.AdvancedFn(types.LocalUnsafe, 20, 40)) + } + + reorgFun := func() error { + + // Stop the batcher + out.L2Batcher.Stop() + + // Stop the main sequencer + sequencerCL.StopSequencer() + + t.Logger().Info("Rewinding to unsafe head", unsafeHead.Hash) + + parentOfHeadToReorgA := unsafeHead.ParentID() + parentsL1Origin, err := sequencerEL.Escape().L2EthClient().L2BlockRefByHash(t.Ctx(), parentOfHeadToReorgA.Hash) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + nextL1Origin := parentsL1Origin.L1Origin.Number + 1 + l1Origin, err := out.L1EL.EthClient().InfoByNumber(t.Ctx(), nextL1Origin) + require.NoError(t, err, "Expected to get block number %v from L1 execution client", nextL1Origin) + l1OriginHash := l1Origin.Hash() + + // Reorg the L2 Chain to the unsafe head + controlAPI := out.TestSequencer.Escape().ControlAPI(out.L2CLNodes()[0].ChainID()) + t.Require().NoError(controlAPI.New(t.Ctx(), seqtypes.BuildOpts{ + Parent: unsafeHead.ParentHash, + L1Origin: &l1OriginHash, + })) + t.Require().NoError(controlAPI.Open(t.Ctx())) + + // include simple transfer tx in opened block + { + t.Logger().Info("Sequencing with op-test-sequencer simple transfer tx") + to := alice.PlanTransfer(bob.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(t.Ctx()) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = controlAPI.IncludeTx(t.Ctx(), txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = controlAPI.Next(t.Ctx()) + require.NoError(t, err, "Expected to be able to call Next() after IncludeTx() on op-test-sequencer, but got error") + + // Resume the main sequencer + sequencerCL.StartSequencer() + + // Resume the batcher + out.L2Batcher.Start() + + // Ensure all the nodes are connected to the sequencer + sequencerPeerID := sequencerCL.PeerInfo().PeerID + for _, node := range out.L2CLValidatorNodes() { + found := false + for _, peer := range node.Peers().Peers { + if peer.PeerID == sequencerPeerID { + found = true + break + } + } + if !found { + return fmt.Errorf("expected node %s to be connected to the sequencer", node.Escape().ID().Key()) + } + } + + return nil + } + + checksPostReorg = append(checksPostReorg, reorgFun) + + dsl.CheckAll(t, checksPostReorg...) + + // Ensure the current unsafe head is ahead of the reorg head + for _, node := range out.L2CLNodes() { + require.Greater(t, node.HeadBlockRef(types.LocalUnsafe).Number, unsafeHead.Number) + } + + // Ensure that bob has the funds + for _, node := range out.L2ELSequencerNodes() { + // Ensure that the recipient's balance has been updated in the eyes of the EL node. + bob.AsEL(node).VerifyBalanceExact(eth.OneHundredthEther.Add(eth.OneGWei)) + alice.AsEL(node).VerifyBalanceLessThan(eth.OneHundredthEther.Sub(eth.OneGWei)) + } +} diff --git a/kona/tests/node/restart/conn_drop_test.go b/kona/tests/node/restart/conn_drop_test.go new file mode 100644 index 0000000000000..afd813cac3691 --- /dev/null +++ b/kona/tests/node/restart/conn_drop_test.go @@ -0,0 +1,120 @@ +package node_restart + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// Ensure that kona-nodes reconnect to the sequencer and sync properly when the connection is dropped. +func TestConnDropSync(gt *testing.T) { + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLValidatorNodes() + sequencerNodes := out.L2CLSequencerNodes() + t.Gate().Greater(len(nodes), 0, "expected at least one validator node") + t.Gate().Greater(len(sequencerNodes), 0, "expected at least one sequencer node") + + // Ensure that the nodes are advancing. + var preCheckFuns []dsl.CheckFunc + for _, node := range out.L2CLNodes() { + preCheckFuns = append(preCheckFuns, node.AdvancedFn(types.LocalSafe, 20, 100), node.AdvancedFn(types.LocalUnsafe, 20, 100)) + } + dsl.CheckAll(t, preCheckFuns...) + + sequencer := sequencerNodes[0] + + var postDisconnectCheckFuns []dsl.CheckFunc + for _, node := range nodes { + clName := node.Escape().ID().Key() + + node.DisconnectPeer(&sequencer) + + // Ensure that the node is no longer connected to the sequencer + t.Logf("node %s is disconnected from sequencer %s", clName, sequencer.Escape().ID().Key()) + seqPeers := sequencer.Peers() + for _, peer := range seqPeers.Peers { + t.Require().NotEqual(peer.PeerID, node.PeerInfo().PeerID, "expected node %s to be disconnected from sequencer %s", clName, sequencer.Escape().ID().Key()) + } + + peers := node.Peers() + for _, peer := range peers.Peers { + t.Require().NotEqual(peer.PeerID, sequencer.PeerInfo().PeerID, "expected node %s to be disconnected from sequencer %s", clName, sequencer.Escape().ID().Key()) + } + + currentUnsafeHead := node.ChainSyncStatus(node.ChainID(), types.LocalUnsafe) + + endSignal := make(chan struct{}) + + safeHeads := node_utils.GetKonaWsAsync(t, &node, "safe_head", endSignal) + unsafeHeads := node_utils.GetKonaWsAsync(t, &node, "unsafe_head", endSignal) + + // Ensures that.... + // - the node's safe head is advancing and eventually catches up with the unsafe head + // - the node's unsafe head is NOT advancing during this time + check := func() error { + outer_loop: + for { + select { + case safeHead := <-safeHeads: + t.Logf("node %s safe head is advancing", clName) + if safeHead.Number >= currentUnsafeHead.Number { + t.Logf("node %s safe head caught up with unsafe head", clName) + break outer_loop + } + case unsafeHead := <-unsafeHeads: + return fmt.Errorf("node %s unsafe head is advancing: %d", clName, unsafeHead.Number) + } + } + + endSignal <- struct{}{} + + return nil + } + + // Check that... + // - the node's safe head is advancing + // - the node's unsafe head is advancing (through consolidation) + // - the node's safe head's number is catching up with the unsafe head's number + // - the node's unsafe head is strictly lagging behind the sequencer's unsafe head + postDisconnectCheckFuns = append(postDisconnectCheckFuns, node.AdvancedFn(types.LocalSafe, 50, 200), node.AdvancedFn(types.LocalUnsafe, 50, 200), check) + } + + postDisconnectCheckFuns = append(postDisconnectCheckFuns, sequencer.AdvancedFn(types.LocalUnsafe, 50, 200)) + + dsl.CheckAll(t, postDisconnectCheckFuns...) + + var postReconnectCheckFuns []dsl.CheckFunc + for _, node := range nodes { + clName := node.Escape().ID().Key() + + node.ConnectPeer(&sequencer) + + // Check that the node is connected to the reference node + peers := node.Peers() + t.Require().Greater(len(peers.Peers), 0, "expected at least one peer") + + // Check that there is at least a peer with the same ID as the ref node + found := false + for _, peer := range peers.Peers { + if peer.PeerID == sequencer.PeerInfo().PeerID { + t.Logf("node %s is connected to reference node %s", clName, sequencer.Escape().ID().Key()) + found = true + break + } + } + + t.Require().True(found, "expected node %s to be connected to reference node %s", clName, sequencer.Escape().ID().Key()) + + // Check that the node is resyncing with the unsafe head network + postReconnectCheckFuns = append(postReconnectCheckFuns, node_utils.MatchedWithinRange(t, node, sequencer, 3, types.LocalSafe, 50), node.AdvancedFn(types.LocalUnsafe, 50, 100), node_utils.MatchedWithinRange(t, node, sequencer, 3, types.LocalUnsafe, 100)) + } + + dsl.CheckAll(t, postReconnectCheckFuns...) +} diff --git a/kona/tests/node/restart/init_test.go b/kona/tests/node/restart/init_test.go new file mode 100644 index 0000000000000..11763ac7a1092 --- /dev/null +++ b/kona/tests/node/restart/init_test.go @@ -0,0 +1,21 @@ +package node_restart + +import ( + "fmt" + "testing" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // Currently, the restart tests only support kona nodes. The op node based configs are not supported (because of req-resp sync incompatibility). + config := node_utils.L2NodeConfig{ + KonaSequencerNodesWithGeth: 1, + KonaNodesWithGeth: 1, + } + + fmt.Printf("Running restart e2e tests with Config: %d\n", config) + presets.DoMain(m, node_utils.WithMixedOpKona(config)) +} diff --git a/kona/tests/node/restart/restart_test.go b/kona/tests/node/restart/restart_test.go new file mode 100644 index 0000000000000..80d5670e09398 --- /dev/null +++ b/kona/tests/node/restart/restart_test.go @@ -0,0 +1,98 @@ +package node_restart + +import ( + "context" + "fmt" + "testing" + "time" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// Ensure that kona-nodes reconnect to the sequencer and sync properly when the connection is dropped. +func TestRestartSync(gt *testing.T) { + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLValidatorNodes() + sequencerNodes := out.L2CLSequencerNodes() + t.Gate().Greater(len(nodes), 0, "expected at least one validator node") + t.Gate().Greater(len(sequencerNodes), 0, "expected at least one sequencer node") + + sequencer := sequencerNodes[0] + + // Ensure that the nodes are advancing. + var preCheckFuns []dsl.CheckFunc + for _, node := range out.L2CLNodes() { + preCheckFuns = append(preCheckFuns, node.AdvancedFn(types.LocalSafe, 20, 100), node.AdvancedFn(types.LocalUnsafe, 20, 100)) + } + dsl.CheckAll(t, preCheckFuns...) + + for _, node := range nodes { + t.Logf("testing restarts for node %s", node.Escape().ID().Key()) + clName := node.Escape().ID().Key() + nodePeerId := node.PeerInfo().PeerID + + t.Logf("stopping node %s", clName) + node.Stop() + + // Ensure that the node is no longer connected to the sequencer + // Retry with an exponential backoff because the node may take a few seconds to stop. + _, err := retry.Do(t.Ctx(), 5, &retry.ExponentialStrategy{Max: 10 * time.Second, Min: 1 * time.Second, MaxJitter: 250 * time.Millisecond}, func() (any, error) { + seqPeers := sequencer.Peers() + for _, peer := range seqPeers.Peers { + if peer.PeerID == nodePeerId { + return nil, fmt.Errorf("expected node %s to be disconnected from sequencer %s", clName, sequencer.Escape().ID().Key()) + } + } + return nil, nil + }) + + t.Require().NoError(err) + + // Ensure that the node is stopped + // Check that calling any rpc method returns an error + rpc := node_utils.GetNodeRPCEndpoint(&node) + var out *eth.SyncStatus + err = rpc.CallContext(context.Background(), &out, "opp2p_syncStatus") + t.Require().Error(err, "expected node %s to be stopped", clName) + } + + sequencer.Advanced(types.LocalUnsafe, 50, 200) + + var postStartCheckFuns []dsl.CheckFunc + for _, node := range nodes { + clName := node.Escape().ID().Key() + t.Logf("starting node %s", clName) + node.Start() + + node.ConnectPeer(&sequencer) + + // Check that the node is resyncing with the network + postStartCheckFuns = append(postStartCheckFuns, node_utils.MatchedWithinRange(t, node, sequencer, 3, types.LocalSafe, 100), node_utils.MatchedWithinRange(t, node, sequencer, 3, types.LocalUnsafe, 100)) + + // Check that the node is connected to the reference node + peers := node.Peers() + t.Require().Greater(len(peers.Peers), 0, "expected at least one peer") + + // Check that there is at least a peer with the same ID as the ref node + found := false + for _, peer := range peers.Peers { + if peer.PeerID == sequencer.PeerInfo().PeerID { + t.Logf("node %s is connected to reference node %s", clName, sequencer.Escape().ID().Key()) + found = true + break + } + } + + t.Require().True(found, "expected node %s to be connected to reference node %s", clName, sequencer.Escape().ID().Key()) + } + + dsl.CheckAll(t, postStartCheckFuns...) +} diff --git a/kona/tests/node/restart/sequencer_restart_test.go b/kona/tests/node/restart/sequencer_restart_test.go new file mode 100644 index 0000000000000..eada3d04f1f17 --- /dev/null +++ b/kona/tests/node/restart/sequencer_restart_test.go @@ -0,0 +1,79 @@ +package node_restart + +import ( + "fmt" + "testing" + "time" + + node_utils "github.com/ethereum-optimism/optimism/kona/tests/node/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +func TestSequencerRestart(gt *testing.T) { + t := devtest.SerialT(gt) + + out := node_utils.NewMixedOpKona(t) + + nodes := out.L2CLValidatorNodes() + sequencerNodes := out.L2CLSequencerNodes() + t.Gate().Greater(len(nodes), 0, "expected at least one validator node") + t.Gate().Greater(len(sequencerNodes), 0, "expected at least one sequencer node") + + sequencer := sequencerNodes[0] + seqPeerId := sequencer.PeerInfo().PeerID + + // Let's ensure that all the nodes are properly advancing. + var preCheckFuns []dsl.CheckFunc + for _, node := range nodes { + preCheckFuns = append(preCheckFuns, node.LaggedFn(&sequencer, types.CrossUnsafe, 20, true), node.AdvancedFn(types.LocalSafe, 20, 40)) + } + dsl.CheckAll(t, preCheckFuns...) + + // Let's stop the sequencer node. + t.Logf("Stopping sequencer %s", sequencer.Escape().ID().Key()) + sequencer.Stop() + + var stopCheckFuns []dsl.CheckFunc + for _, node := range nodes { + // Ensure that the node is no longer connected to the sequencer + nodePeers := node.Peers() + _, err := retry.Do(t.Ctx(), 5, &retry.ExponentialStrategy{Max: 10 * time.Second, Min: 1 * time.Second, MaxJitter: 250 * time.Millisecond}, func() (any, error) { + for _, peer := range nodePeers.Peers { + if peer.PeerID == seqPeerId { + return nil, fmt.Errorf("expected node %s to be disconnected from sequencer %s", node.Escape().ID().Key(), sequencer.Escape().ID().Key()) + } + } + return nil, nil + }) + t.Require().NoError(err) + + // Ensure that the other nodes are not advancing. + // The local safe head may advance (for the next l1 block to be processed), but the unsafe head should not. + stopCheckFuns = append(stopCheckFuns, node.NotAdvancedFn(types.LocalUnsafe, 50)) + } + + dsl.CheckAll(t, stopCheckFuns...) + + // Let's restart the sequencer node. + t.Logf("Starting sequencer %s", sequencer.Escape().ID().Key()) + sequencer.Start() + + // Let's reconnect the sequencer to the nodes. + t.Logf("Reconnecting sequencer %s to nodes", sequencer.Escape().ID().Key()) + for _, node := range nodes { + t.Logf("Connecting sequencer %s to node %s", sequencer.Escape().ID().Key(), node.Escape().ID().Key()) + sequencer.ConnectPeer(&node) + } + + // Let's ensure that the nodes are advancing. + t.Logf("Waiting for nodes to advance") + var postCheckFuns []dsl.CheckFunc + for _, node := range nodes { + postCheckFuns = append(postCheckFuns, node.AdvancedFn(types.LocalSafe, 10, 100), node.AdvancedFn(types.LocalUnsafe, 10, 100)) + } + + dsl.CheckAll(t, postCheckFuns...) +} diff --git a/kona/tests/node/utils/mixed_preset.go b/kona/tests/node/utils/mixed_preset.go new file mode 100644 index 0000000000000..e959f7e4320eb --- /dev/null +++ b/kona/tests/node/utils/mixed_preset.go @@ -0,0 +1,568 @@ +package node_utils + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-node/rollup/sync" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type L2NodeKind string + +const ( + OpNode L2NodeKind = "op" + KonaNode L2NodeKind = "kona" + Sequencer L2NodeKind = "sequencer" + Validator L2NodeKind = "validator" +) + +type L2NodeConfig struct { + OpSequencerNodesWithGeth int + OpSequencerNodesWithReth int + KonaSequencerNodesWithGeth int + KonaSequencerNodesWithReth int + OpNodesWithGeth int + OpNodesWithReth int + KonaNodesWithGeth int + KonaNodesWithReth int +} + +const ( + DefaultOpSequencerGeth = 0 + DefaultOpSequencerReth = 0 + + DefaultKonaSequencerGeth = 0 + DefaultKonaSequencerReth = 1 + + DefaultOpValidatorGeth = 0 + DefaultOpValidatorReth = 0 + + DefaultKonaValidatorGeth = 3 + DefaultKonaValidatorReth = 3 +) + +func ParseL2NodeConfigFromEnv() L2NodeConfig { + // Get environment variable: OP_SEQUENCER_NODES. Convert to int. + opSequencerGeth := os.Getenv("OP_SEQUENCER_WITH_GETH") + opSequencerGethInt, err := strconv.Atoi(opSequencerGeth) + if err != nil { + opSequencerGethInt = DefaultOpSequencerGeth + } + // Get environment variable: KONA_SEQUENCER_NODES + konaSequencerGeth := os.Getenv("KONA_SEQUENCER_WITH_GETH") + konaSequencerGethInt, err := strconv.Atoi(konaSequencerGeth) + if err != nil { + konaSequencerGethInt = DefaultKonaSequencerGeth + } + // Get environment variable: OP_SEQUENCER_WITH_RETH + opSequencerReth := os.Getenv("OP_SEQUENCER_WITH_RETH") + opSequencerRethInt, err := strconv.Atoi(opSequencerReth) + if err != nil { + opSequencerRethInt = DefaultOpSequencerReth + } + // Get environment variable: KONA_SEQUENCER_WITH_RETH + konaSequencerReth := os.Getenv("KONA_SEQUENCER_WITH_RETH") + konaSequencerRethInt, err := strconv.Atoi(konaSequencerReth) + if err != nil { + konaSequencerRethInt = DefaultKonaSequencerReth + } + // Get environment variable: OP_VALIDATOR_WITH_GETH + opValidatorGeth := os.Getenv("OP_VALIDATOR_WITH_GETH") + opValidatorGethInt, err := strconv.Atoi(opValidatorGeth) + if err != nil { + opValidatorGethInt = DefaultOpValidatorGeth + } + // Get environment variable: OP_VALIDATOR_WITH_RETH + opValidatorReth := os.Getenv("OP_VALIDATOR_WITH_RETH") + opValidatorRethInt, err := strconv.Atoi(opValidatorReth) + if err != nil { + opValidatorRethInt = DefaultOpValidatorReth + } + // Get environment variable: KONA_VALIDATOR_WITH_GETH + konaValidatorGeth := os.Getenv("KONA_VALIDATOR_WITH_GETH") + konaValidatorGethInt, err := strconv.Atoi(konaValidatorGeth) + if err != nil { + konaValidatorGethInt = DefaultKonaValidatorGeth + } + // Get environment variable: KONA_VALIDATOR_WITH_RETH + konaValidatorReth := os.Getenv("KONA_VALIDATOR_WITH_RETH") + konaValidatorRethInt, err := strconv.Atoi(konaValidatorReth) + if err != nil { + konaValidatorRethInt = DefaultKonaValidatorReth + } + + return L2NodeConfig{ + OpSequencerNodesWithGeth: opSequencerGethInt, + OpSequencerNodesWithReth: opSequencerRethInt, + + OpNodesWithGeth: opValidatorGethInt, + OpNodesWithReth: opValidatorRethInt, + + KonaSequencerNodesWithGeth: konaSequencerGethInt, + KonaSequencerNodesWithReth: konaSequencerRethInt, + + KonaNodesWithGeth: konaValidatorGethInt, + KonaNodesWithReth: konaValidatorRethInt, + } +} + +func (l2NodeConfig L2NodeConfig) TotalNodes() int { + return l2NodeConfig.OpSequencerNodesWithGeth + l2NodeConfig.OpSequencerNodesWithReth + l2NodeConfig.KonaSequencerNodesWithGeth + l2NodeConfig.KonaSequencerNodesWithReth + l2NodeConfig.OpNodesWithGeth + l2NodeConfig.OpNodesWithReth + l2NodeConfig.KonaNodesWithGeth + l2NodeConfig.KonaNodesWithReth +} + +func (l2NodeConfig L2NodeConfig) OpSequencerNodes() int { + return l2NodeConfig.OpSequencerNodesWithGeth + l2NodeConfig.OpSequencerNodesWithReth +} + +func (l2NodeConfig L2NodeConfig) KonaSequencerNodes() int { + return l2NodeConfig.KonaSequencerNodesWithGeth + l2NodeConfig.KonaSequencerNodesWithReth +} + +func (l2NodeConfig L2NodeConfig) OpValidatorNodes() int { + return l2NodeConfig.OpNodesWithGeth + l2NodeConfig.OpNodesWithReth +} + +func (l2NodeConfig L2NodeConfig) KonaValidatorNodes() int { + return l2NodeConfig.KonaNodesWithGeth + l2NodeConfig.KonaNodesWithReth +} + +type MixedOpKonaPreset struct { + Log log.Logger + T devtest.T + ControlPlane stack.ControlPlane + + L1Network *dsl.L1Network + L1EL *dsl.L1ELNode + + L2Chain *dsl.L2Network + L2Batcher *dsl.L2Batcher + + L2ELKonaSequencerNodes []dsl.L2ELNode + L2CLKonaSequencerNodes []dsl.L2CLNode + + L2ELOpSequencerNodes []dsl.L2ELNode + L2CLOpSequencerNodes []dsl.L2CLNode + + L2ELOpValidatorNodes []dsl.L2ELNode + L2CLOpValidatorNodes []dsl.L2CLNode + + L2ELKonaValidatorNodes []dsl.L2ELNode + L2CLKonaValidatorNodes []dsl.L2CLNode + + Wallet *dsl.HDWallet + + FaucetL1 *dsl.Faucet + Faucet *dsl.Faucet + FunderL1 *dsl.Funder + Funder *dsl.Funder +} + +// L2ELNodes returns all the L2EL nodes in the network (op-reth, op-geth, etc.), validator and sequencer. +func (m *MixedOpKonaPreset) L2ELNodes() []dsl.L2ELNode { + return append(m.L2ELSequencerNodes(), m.L2ELValidatorNodes()...) +} + +// L2CLNodes returns all the L2CL nodes in the network (op-nodes and kona-nodes), validator and sequencer. +func (m *MixedOpKonaPreset) L2CLNodes() []dsl.L2CLNode { + return append(m.L2CLSequencerNodes(), m.L2CLValidatorNodes()...) +} + +// L2CLValidatorNodes returns all the validator L2CL nodes in the network (op-nodes and kona-nodes). +func (m *MixedOpKonaPreset) L2CLValidatorNodes() []dsl.L2CLNode { + return append(m.L2CLOpValidatorNodes, m.L2CLKonaValidatorNodes...) +} + +// L2CLSequencerNodes returns all the sequencer L2CL nodes in the network (op-nodes and kona-nodes). +func (m *MixedOpKonaPreset) L2CLSequencerNodes() []dsl.L2CLNode { + return append(m.L2CLOpSequencerNodes, m.L2CLKonaSequencerNodes...) +} + +// L2ELValidatorNodes returns all the validator L2EL nodes in the network (op-reth, op-geth, etc.). +func (m *MixedOpKonaPreset) L2ELValidatorNodes() []dsl.L2ELNode { + return append(m.L2ELOpValidatorNodes, m.L2ELKonaValidatorNodes...) +} + +// L2ELSequencerNodes returns all the sequencer L2EL nodes in the network (op-reth, op-geth, etc.). +func (m *MixedOpKonaPreset) L2ELSequencerNodes() []dsl.L2ELNode { + return append(m.L2ELOpSequencerNodes, m.L2ELKonaSequencerNodes...) +} + +func (m *MixedOpKonaPreset) L2CLKonaNodes() []dsl.L2CLNode { + return append(m.L2CLKonaValidatorNodes, m.L2CLKonaSequencerNodes...) +} + +func L2NodeMatcher[ + I interface { + comparable + Key() string + }, E stack.Identifiable[I]](value ...string) stack.Matcher[I, E] { + return match.MatchElemFn[I, E](func(elem E) bool { + for _, v := range value { + if !strings.Contains(elem.ID().Key(), v) { + return false + } + } + return true + }) +} + +func (m *MixedOpKonaPreset) L2Networks() []*dsl.L2Network { + return []*dsl.L2Network{ + m.L2Chain, + } +} + +func WithMixedOpKona(l2NodeConfig L2NodeConfig) stack.CommonOption { + return stack.MakeCommon(DefaultMixedOpKonaSystem(&DefaultMixedOpKonaSystemIDs{}, l2NodeConfig)) +} + +func L2CLNodes(nodes []stack.L2CLNode, orch stack.Orchestrator) []dsl.L2CLNode { + out := make([]dsl.L2CLNode, len(nodes)) + for i, node := range nodes { + out[i] = *dsl.NewL2CLNode(node, orch.ControlPlane()) + } + return out +} + +func L2ELNodes(nodes []stack.L2ELNode, orch stack.Orchestrator) []dsl.L2ELNode { + out := make([]dsl.L2ELNode, len(nodes)) + for i, node := range nodes { + out[i] = *dsl.NewL2ELNode(node, orch.ControlPlane()) + } + return out +} + +func NewMixedOpKona(t devtest.T) *MixedOpKonaPreset { + system := shim.NewSystem(t) + orch := presets.Orchestrator() + orch.Hydrate(system) + + t.Gate().Equal(len(system.L2Networks()), 1, "expected exactly one L2 network") + t.Gate().Equal(len(system.L1Networks()), 1, "expected exactly one L1 network") + + l1Net := system.L1Network(match.FirstL1Network) + l2Net := system.L2Network(match.Assume(t, match.L2ChainA)) + + t.Gate().GreaterOrEqual(len(l2Net.L2CLNodes()), 2, "expected at least two L2CL nodes") + + opSequencerCLNodes := L2NodeMatcher[stack.L2CLNodeID, stack.L2CLNode](string(OpNode), string(Sequencer)).Match(l2Net.L2CLNodes()) + konaSequencerCLNodes := L2NodeMatcher[stack.L2CLNodeID, stack.L2CLNode](string(KonaNode), string(Sequencer)).Match(l2Net.L2CLNodes()) + + opCLNodes := L2NodeMatcher[stack.L2CLNodeID, stack.L2CLNode](string(OpNode), string(Validator)).Match(l2Net.L2CLNodes()) + konaCLNodes := L2NodeMatcher[stack.L2CLNodeID, stack.L2CLNode](string(KonaNode), string(Validator)).Match(l2Net.L2CLNodes()) + + opSequencerELNodes := L2NodeMatcher[stack.L2ELNodeID, stack.L2ELNode](string(OpNode), string(Sequencer)).Match(l2Net.L2ELNodes()) + konaSequencerELNodes := L2NodeMatcher[stack.L2ELNodeID, stack.L2ELNode](string(KonaNode), string(Sequencer)).Match(l2Net.L2ELNodes()) + opELNodes := L2NodeMatcher[stack.L2ELNodeID, stack.L2ELNode](string(OpNode), string(Validator)).Match(l2Net.L2ELNodes()) + konaELNodes := L2NodeMatcher[stack.L2ELNodeID, stack.L2ELNode](string(KonaNode), string(Validator)).Match(l2Net.L2ELNodes()) + + out := &MixedOpKonaPreset{ + Log: t.Logger(), + T: t, + ControlPlane: orch.ControlPlane(), + L1Network: dsl.NewL1Network(system.L1Network(match.FirstL1Network)), + L1EL: dsl.NewL1ELNode(l1Net.L1ELNode(match.Assume(t, match.FirstL1EL))), + L2Chain: dsl.NewL2Network(l2Net, orch.ControlPlane()), + L2Batcher: dsl.NewL2Batcher(l2Net.L2Batcher(match.Assume(t, match.FirstL2Batcher))), + + L2ELOpSequencerNodes: L2ELNodes(opSequencerELNodes, orch), + L2CLOpSequencerNodes: L2CLNodes(opSequencerCLNodes, orch), + + L2ELOpValidatorNodes: L2ELNodes(opELNodes, orch), + L2CLOpValidatorNodes: L2CLNodes(opCLNodes, orch), + + L2ELKonaSequencerNodes: L2ELNodes(konaSequencerELNodes, orch), + L2CLKonaSequencerNodes: L2CLNodes(konaSequencerCLNodes, orch), + + L2ELKonaValidatorNodes: L2ELNodes(konaELNodes, orch), + L2CLKonaValidatorNodes: L2CLNodes(konaCLNodes, orch), + + Wallet: dsl.NewHDWallet(t, devkeys.TestMnemonic, 30), + Faucet: dsl.NewFaucet(l2Net.Faucet(match.Assume(t, match.FirstFaucet))), + } + return out +} + +type DefaultMixedOpKonaSystemIDs struct { + L1 stack.L1NetworkID + L1EL stack.L1ELNodeID + L1CL stack.L1CLNodeID + + L2 stack.L2NetworkID + + L2ELOpGethSequencerNodes []stack.L2ELNodeID + L2ELOpRethSequencerNodes []stack.L2ELNodeID + + L2CLOpGethSequencerNodes []stack.L2CLNodeID + L2CLOpRethSequencerNodes []stack.L2CLNodeID + + L2ELKonaGethSequencerNodes []stack.L2ELNodeID + L2ELKonaRethSequencerNodes []stack.L2ELNodeID + + L2CLKonaGethSequencerNodes []stack.L2CLNodeID + L2CLKonaRethSequencerNodes []stack.L2CLNodeID + + L2CLOpGethNodes []stack.L2CLNodeID + L2ELOpGethNodes []stack.L2ELNodeID + + L2CLOpRethNodes []stack.L2CLNodeID + L2ELOpRethNodes []stack.L2ELNodeID + + L2CLKonaGethNodes []stack.L2CLNodeID + L2ELKonaGethNodes []stack.L2ELNodeID + + L2CLKonaRethNodes []stack.L2CLNodeID + L2ELKonaRethNodes []stack.L2ELNodeID + + L2Batcher stack.L2BatcherID + L2Proposer stack.L2ProposerID +} + +func (ids *DefaultMixedOpKonaSystemIDs) L2CLSequencerNodes() []stack.L2CLNodeID { + list := append(ids.L2CLOpGethSequencerNodes, ids.L2CLOpRethSequencerNodes...) + list = append(list, ids.L2CLKonaGethSequencerNodes...) + list = append(list, ids.L2CLKonaRethSequencerNodes...) + return list +} + +func (ids *DefaultMixedOpKonaSystemIDs) L2ELSequencerNodes() []stack.L2ELNodeID { + list := append(ids.L2ELOpGethSequencerNodes, ids.L2ELOpRethSequencerNodes...) + list = append(list, ids.L2ELKonaGethSequencerNodes...) + list = append(list, ids.L2ELKonaRethSequencerNodes...) + return list +} + +func (ids *DefaultMixedOpKonaSystemIDs) L2CLValidatorNodes() []stack.L2CLNodeID { + list := append(ids.L2CLOpGethNodes, ids.L2CLOpRethNodes...) + list = append(list, ids.L2CLKonaGethNodes...) + list = append(list, ids.L2CLKonaRethNodes...) + return list +} +func (ids *DefaultMixedOpKonaSystemIDs) L2ELValidatorNodes() []stack.L2ELNodeID { + list := append(ids.L2ELOpGethNodes, ids.L2ELOpRethNodes...) + list = append(list, ids.L2ELKonaGethNodes...) + list = append(list, ids.L2ELKonaRethNodes...) + return list +} + +func (ids *DefaultMixedOpKonaSystemIDs) L2CLNodes() []stack.L2CLNodeID { + return append(ids.L2CLSequencerNodes(), ids.L2CLValidatorNodes()...) +} + +func (ids *DefaultMixedOpKonaSystemIDs) L2ELNodes() []stack.L2ELNodeID { + return append(ids.L2ELSequencerNodes(), ids.L2ELValidatorNodes()...) +} + +func NewDefaultMixedOpKonaSystemIDs(l1ID, l2ID eth.ChainID, l2NodeConfig L2NodeConfig) DefaultMixedOpKonaSystemIDs { + rethOpCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.OpNodesWithReth) + rethOpELNodes := make([]stack.L2ELNodeID, l2NodeConfig.OpNodesWithReth) + rethKonaCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.KonaNodesWithReth) + rethKonaELNodes := make([]stack.L2ELNodeID, l2NodeConfig.KonaNodesWithReth) + + gethOpCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.OpNodesWithGeth) + gethOpELNodes := make([]stack.L2ELNodeID, l2NodeConfig.OpNodesWithGeth) + gethKonaCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.KonaNodesWithGeth) + gethKonaELNodes := make([]stack.L2ELNodeID, l2NodeConfig.KonaNodesWithGeth) + + gethOpSequencerCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.OpSequencerNodesWithGeth) + gethOpSequencerELNodes := make([]stack.L2ELNodeID, l2NodeConfig.OpSequencerNodesWithGeth) + gethKonaSequencerCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.KonaSequencerNodesWithGeth) + gethKonaSequencerELNodes := make([]stack.L2ELNodeID, l2NodeConfig.KonaSequencerNodesWithGeth) + + rethOpSequencerCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.OpSequencerNodesWithReth) + rethOpSequencerELNodes := make([]stack.L2ELNodeID, l2NodeConfig.OpSequencerNodesWithReth) + rethKonaSequencerCLNodes := make([]stack.L2CLNodeID, l2NodeConfig.KonaSequencerNodesWithReth) + rethKonaSequencerELNodes := make([]stack.L2ELNodeID, l2NodeConfig.KonaSequencerNodesWithReth) + + for i := range l2NodeConfig.OpSequencerNodesWithGeth { + gethOpSequencerCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-geth-op-sequencer-%d", i), l2ID) + gethOpSequencerELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-geth-op-sequencer-%d", i), l2ID) + } + + for i := range l2NodeConfig.KonaSequencerNodesWithGeth { + gethKonaSequencerCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-geth-kona-sequencer-%d", i), l2ID) + gethKonaSequencerELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-geth-kona-sequencer-%d", i), l2ID) + } + + for i := range l2NodeConfig.OpSequencerNodesWithReth { + rethOpSequencerCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-reth-op-sequencer-%d", i), l2ID) + rethOpSequencerELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-reth-op-sequencer-%d", i), l2ID) + } + + for i := range l2NodeConfig.KonaSequencerNodesWithReth { + rethKonaSequencerCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-reth-kona-sequencer-%d", i), l2ID) + rethKonaSequencerELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-reth-kona-sequencer-%d", i), l2ID) + } + + for i := range l2NodeConfig.OpNodesWithGeth { + gethOpCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-geth-op-validator-%d", i), l2ID) + gethOpELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-geth-op-validator-%d", i), l2ID) + } + + for i := range l2NodeConfig.OpNodesWithReth { + rethOpCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-reth-op-validator-%d", i), l2ID) + rethOpELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-reth-op-validator-%d", i), l2ID) + } + + for i := range l2NodeConfig.KonaNodesWithGeth { + gethKonaCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-geth-kona-validator-%d", i), l2ID) + gethKonaELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-geth-kona-validator-%d", i), l2ID) + } + + for i := range l2NodeConfig.KonaNodesWithReth { + rethKonaCLNodes[i] = stack.NewL2CLNodeID(fmt.Sprintf("cl-reth-kona-validator-%d", i), l2ID) + rethKonaELNodes[i] = stack.NewL2ELNodeID(fmt.Sprintf("el-reth-kona-validator-%d", i), l2ID) + } + + ids := DefaultMixedOpKonaSystemIDs{ + L1: stack.L1NetworkID(l1ID), + L1EL: stack.NewL1ELNodeID("l1", l1ID), + L1CL: stack.NewL1CLNodeID("l1", l1ID), + L2: stack.L2NetworkID(l2ID), + + L2CLOpGethSequencerNodes: gethOpSequencerCLNodes, + L2ELOpGethSequencerNodes: gethOpSequencerELNodes, + + L2CLOpRethSequencerNodes: rethOpSequencerCLNodes, + L2ELOpRethSequencerNodes: rethOpSequencerELNodes, + + L2CLOpGethNodes: gethOpCLNodes, + L2ELOpGethNodes: gethOpELNodes, + + L2CLOpRethNodes: rethOpCLNodes, + L2ELOpRethNodes: rethOpELNodes, + + L2CLKonaGethSequencerNodes: gethKonaSequencerCLNodes, + L2ELKonaGethSequencerNodes: gethKonaSequencerELNodes, + + L2CLKonaRethSequencerNodes: rethKonaSequencerCLNodes, + L2ELKonaRethSequencerNodes: rethKonaSequencerELNodes, + + L2CLKonaGethNodes: gethKonaCLNodes, + L2ELKonaGethNodes: gethKonaELNodes, + + L2CLKonaRethNodes: rethKonaCLNodes, + L2ELKonaRethNodes: rethKonaELNodes, + + L2Batcher: stack.NewL2BatcherID("main", l2ID), + L2Proposer: stack.NewL2ProposerID("main", l2ID), + } + return ids +} + +func DefaultMixedOpKonaSystem(dest *DefaultMixedOpKonaSystemIDs, l2NodeConfig L2NodeConfig) stack.CombinedOption[*sysgo.Orchestrator] { + l1ID := eth.ChainIDFromUInt64(DefaultL1ID) + l2ID := eth.ChainIDFromUInt64(DefaultL2ID) + ids := NewDefaultMixedOpKonaSystemIDs(l1ID, l2ID, l2NodeConfig) + + opt := stack.Combine[*sysgo.Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *sysgo.Orchestrator) { + o.P().Logger().Info("Setting up") + })) + + opt.Add(sysgo.WithMnemonicKeys(devkeys.TestMnemonic)) + + opt.Add(sysgo.WithDeployer(), + sysgo.WithDeployerOptions( + sysgo.WithLocalContractSources(), + sysgo.WithCommons(ids.L1.ChainID()), + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2.ChainID()), + ), + ) + + opt.Add(sysgo.WithL1Nodes(ids.L1EL, ids.L1CL)) + + // Spawn all nodes. + for i := range ids.L2CLKonaGethSequencerNodes { + opt.Add(sysgo.WithOpGeth(ids.L2ELKonaGethSequencerNodes[i])) + opt.Add(sysgo.WithKonaNode(ids.L2CLKonaGethSequencerNodes[i], ids.L1CL, ids.L1EL, ids.L2ELKonaGethSequencerNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.IsSequencer = true + cfg.SequencerSyncMode = sync.ELSync + cfg.VerifierSyncMode = sync.ELSync + }))) + } + + for i := range ids.L2CLOpGethSequencerNodes { + opt.Add(sysgo.WithOpGeth(ids.L2ELOpGethSequencerNodes[i])) + opt.Add(sysgo.WithOpNode(ids.L2CLOpGethSequencerNodes[i], ids.L1CL, ids.L1EL, ids.L2ELOpGethSequencerNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.IsSequencer = true + }))) + } + + for i := range ids.L2CLKonaRethSequencerNodes { + opt.Add(sysgo.WithOpReth(ids.L2ELKonaRethSequencerNodes[i])) + opt.Add(sysgo.WithKonaNode(ids.L2CLKonaRethSequencerNodes[i], ids.L1CL, ids.L1EL, ids.L2ELKonaRethSequencerNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.IsSequencer = true + cfg.SequencerSyncMode = sync.ELSync + cfg.VerifierSyncMode = sync.ELSync + }))) + } + + for i := range ids.L2CLOpRethSequencerNodes { + opt.Add(sysgo.WithOpReth(ids.L2ELOpRethSequencerNodes[i])) + opt.Add(sysgo.WithOpNode(ids.L2CLOpRethSequencerNodes[i], ids.L1CL, ids.L1EL, ids.L2ELOpRethSequencerNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.IsSequencer = true + }))) + } + + for i := range ids.L2CLKonaGethNodes { + opt.Add(sysgo.WithOpGeth(ids.L2ELKonaGethNodes[i])) + opt.Add(sysgo.WithKonaNode(ids.L2CLKonaGethNodes[i], ids.L1CL, ids.L1EL, ids.L2ELKonaGethNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.SequencerSyncMode = sync.ELSync + cfg.VerifierSyncMode = sync.ELSync + }))) + } + + for i := range ids.L2ELOpGethNodes { + opt.Add(sysgo.WithOpGeth(ids.L2ELOpGethNodes[i])) + opt.Add(sysgo.WithOpNode(ids.L2CLOpGethNodes[i], ids.L1CL, ids.L1EL, ids.L2ELOpGethNodes[i])) + } + + for i := range ids.L2CLKonaRethNodes { + opt.Add(sysgo.WithOpReth(ids.L2ELKonaRethNodes[i])) + opt.Add(sysgo.WithKonaNode(ids.L2CLKonaRethNodes[i], ids.L1CL, ids.L1EL, ids.L2ELKonaRethNodes[i], sysgo.L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.SequencerSyncMode = sync.ELSync + cfg.VerifierSyncMode = sync.ELSync + }))) + } + + for i := range ids.L2ELOpRethNodes { + opt.Add(sysgo.WithOpReth(ids.L2ELOpRethNodes[i])) + opt.Add(sysgo.WithOpNode(ids.L2CLOpRethNodes[i], ids.L1CL, ids.L1EL, ids.L2ELOpRethNodes[i])) + } + + // Connect all nodes to each other in the p2p network. + CLNodeIDs := ids.L2CLNodes() + ELNodeIDs := ids.L2ELNodes() + + for i := range CLNodeIDs { + for j := range i { + opt.Add(sysgo.WithL2CLP2PConnection(CLNodeIDs[i], CLNodeIDs[j])) + opt.Add(sysgo.WithL2ELP2PConnection(ELNodeIDs[i], ELNodeIDs[j], false)) + } + } + + opt.Add(sysgo.WithBatcher(ids.L2Batcher, ids.L1EL, CLNodeIDs[0], ELNodeIDs[0])) + opt.Add(sysgo.WithProposer(ids.L2Proposer, ids.L1EL, &CLNodeIDs[0], nil)) + + opt.Add(sysgo.WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ELNodeIDs[0]})) + + opt.Add(stack.Finally(func(orch *sysgo.Orchestrator) { + *dest = ids + })) + + return opt +} diff --git a/kona/tests/node/utils/mixed_preset_with_conductor.go b/kona/tests/node/utils/mixed_preset_with_conductor.go new file mode 100644 index 0000000000000..e8b11c82de0f0 --- /dev/null +++ b/kona/tests/node/utils/mixed_preset_with_conductor.go @@ -0,0 +1,34 @@ +package node_utils + +import ( + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" +) + +type MinimalWithConductors struct { + *MixedOpKonaPreset + + ConductorSets map[stack.L2NetworkID]dsl.ConductorSet +} + +func NewMixedOpKonaWithConductors(t devtest.T) *MinimalWithConductors { + system := shim.NewSystem(t) + orch := presets.Orchestrator() + orch.Hydrate(system) + chains := system.L2Networks() + conductorSets := make(map[stack.L2NetworkID]dsl.ConductorSet) + for _, chain := range chains { + chainMatcher := match.L2ChainById(chain.ID()) + l2 := system.L2Network(match.Assume(t, chainMatcher)) + + conductorSets[chain.ID()] = dsl.NewConductorSet(l2.Conductors()) + } + return &MinimalWithConductors{ + MixedOpKonaPreset: NewMixedOpKona(t), + ConductorSets: conductorSets, + } +} diff --git a/kona/tests/node/utils/mod.go b/kona/tests/node/utils/mod.go new file mode 100644 index 0000000000000..3443119dd9a07 --- /dev/null +++ b/kona/tests/node/utils/mod.go @@ -0,0 +1,78 @@ +package node_utils + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +const DefaultL1ID = 900 +const DefaultL2ID = 901 + +// --- Generic RPC request/response types ------------------------------------- + +// --------------------------------------------------------------------------- + +const ( + DEFAULT_TIMEOUT = 10 * time.Second +) + +func GetNodeRPCEndpoint(node *dsl.L2CLNode) client.RPC { + return node.Escape().ClientRPC() +} + +func SendRPCRequest[T any](clientRPC client.RPC, method string, resOutput *T, params ...any) error { + ctx, cancel := context.WithTimeout(context.Background(), DEFAULT_TIMEOUT) + defer cancel() + + return clientRPC.CallContext(ctx, &resOutput, method, params...) +} + +func MatchedWithinRange(t devtest.T, baseNode, refNode dsl.L2CLNode, delta uint64, lvl types.SafetyLevel, attempts int) dsl.CheckFunc { + logger := t.Logger() + chainID := baseNode.ChainID() + + return func() error { + base := baseNode.ChainSyncStatus(chainID, lvl) + ref := refNode.ChainSyncStatus(chainID, lvl) + logger.Info("Expecting node to match with reference", "base", base.Number, "ref", ref.Number) + return retry.Do0(t.Ctx(), attempts, &retry.FixedStrategy{Dur: 2 * time.Second}, + func() error { + base = baseNode.ChainSyncStatus(chainID, lvl) + ref = refNode.ChainSyncStatus(chainID, lvl) + if ref.Number <= base.Number+delta || ref.Number >= base.Number-delta { + logger.Info("Node matched", "ref_id", refNode, "base_id", baseNode, "ref", ref.Number, "base", base.Number, "delta", delta) + + // We get the same block from the head and tail node + var headNode dsl.L2CLNode + var tailNode eth.BlockID + if ref.Number > base.Number { + headNode = refNode + tailNode = base + } else { + headNode = baseNode + tailNode = ref + } + + baseBlock, err := headNode.Escape().RollupAPI().OutputAtBlock(t.Ctx(), tailNode.Number) + if err != nil { + return err + } + + t.Require().Equal(baseBlock.BlockRef.Number, tailNode.Number, "expected block number to match") + t.Require().Equal(baseBlock.BlockRef.Hash, tailNode.Hash, "expected block hash to match") + + return nil + } + logger.Info("Node sync status", "base", base.Number, "ref", ref.Number) + return fmt.Errorf("expected head to match: %s", lvl) + }) + } +} diff --git a/kona/tests/node/utils/test_sequencer_preset.go b/kona/tests/node/utils/test_sequencer_preset.go new file mode 100644 index 0000000000000..5fbaf46327280 --- /dev/null +++ b/kona/tests/node/utils/test_sequencer_preset.go @@ -0,0 +1,87 @@ +package node_utils + +import ( + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type MinimalWithTestSequencersPreset struct { + *MixedOpKonaPreset + + TestSequencer dsl.TestSequencer +} + +func WithMixedWithTestSequencer(l2Config L2NodeConfig) stack.CommonOption { + if l2Config.OpSequencerNodesWithGeth == 0 && l2Config.OpSequencerNodesWithReth == 0 { + l2Config.OpSequencerNodesWithGeth = 1 + } + + return stack.MakeCommon(DefaultMixedWithTestSequencer(&DefaultMinimalWithTestSequencerIds{}, l2Config)) +} + +func NewMixedOpKonaWithTestSequencer(t devtest.T) *MinimalWithTestSequencersPreset { + system := shim.NewSystem(t) + orch := presets.Orchestrator() + orch.Hydrate(system) + + t.Gate().Equal(len(system.L2Networks()), 1, "expected exactly one L2 network") + t.Gate().Equal(len(system.L1Networks()), 1, "expected exactly one L1 network") + + TestSequencer := + dsl.NewTestSequencer(system.TestSequencer(match.Assume(t, match.FirstTestSequencer))) + + return &MinimalWithTestSequencersPreset{ + MixedOpKonaPreset: NewMixedOpKona(t), + TestSequencer: *TestSequencer, + } +} + +type DefaultMinimalWithTestSequencerIds struct { + DefaultMixedOpKonaSystemIDs DefaultMixedOpKonaSystemIDs + TestSequencerId stack.TestSequencerID +} + +func NewDefaultMinimalWithTestSequencerIds(l2Config L2NodeConfig) DefaultMinimalWithTestSequencerIds { + return DefaultMinimalWithTestSequencerIds{ + DefaultMixedOpKonaSystemIDs: NewDefaultMixedOpKonaSystemIDs(eth.ChainIDFromUInt64(DefaultL1ID), eth.ChainIDFromUInt64(DefaultL2ID), L2NodeConfig{ + OpSequencerNodesWithGeth: l2Config.OpSequencerNodesWithGeth, + OpSequencerNodesWithReth: l2Config.OpSequencerNodesWithReth, + OpNodesWithGeth: l2Config.OpNodesWithGeth, + OpNodesWithReth: l2Config.OpNodesWithReth, + KonaNodesWithGeth: l2Config.KonaNodesWithGeth, + KonaNodesWithReth: l2Config.KonaNodesWithReth, + }), + TestSequencerId: "test-sequencer", + } +} + +func DefaultMixedWithTestSequencer(dest *DefaultMinimalWithTestSequencerIds, l2Config L2NodeConfig) stack.Option[*sysgo.Orchestrator] { + + opt := DefaultMixedOpKonaSystem(&dest.DefaultMixedOpKonaSystemIDs, L2NodeConfig{ + OpSequencerNodesWithGeth: l2Config.OpSequencerNodesWithGeth, + OpSequencerNodesWithReth: l2Config.OpSequencerNodesWithReth, + OpNodesWithGeth: l2Config.OpNodesWithGeth, + OpNodesWithReth: l2Config.OpNodesWithReth, + KonaNodesWithGeth: l2Config.KonaNodesWithGeth, + KonaNodesWithReth: l2Config.KonaNodesWithReth, + }) + + ids := NewDefaultMinimalWithTestSequencerIds(l2Config) + + L2SequencerCLNodes := ids.DefaultMixedOpKonaSystemIDs.L2CLSequencerNodes() + L2SequencerELNodes := ids.DefaultMixedOpKonaSystemIDs.L2ELSequencerNodes() + + opt.Add(sysgo.WithTestSequencer(ids.TestSequencerId, ids.DefaultMixedOpKonaSystemIDs.L1CL, L2SequencerCLNodes[0], ids.DefaultMixedOpKonaSystemIDs.L1EL, L2SequencerELNodes[0])) + + opt.Add(stack.Finally(func(orch *sysgo.Orchestrator) { + *dest = ids + })) + + return opt +} diff --git a/kona/tests/node/utils/ws.go b/kona/tests/node/utils/ws.go new file mode 100644 index 0000000000000..6b0bee053f5a3 --- /dev/null +++ b/kona/tests/node/utils/ws.go @@ -0,0 +1,159 @@ +package node_utils + +import ( + "encoding/json" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/gorilla/websocket" + "github.com/stretchr/testify/require" +) + +// --- Generic RPC request/response types ------------------------------------- + +type rpcRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` + ID uint64 `json:"id"` +} + +type rpcResponse struct { + JSONRPC string `json:"jsonrpc"` + ID uint64 `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *rpcError `json:"error,omitempty"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// push { "jsonrpc":"2.0", "method":"time", "params":{ "subscription":"0x…", "result":"…" } } +type push[Out any] struct { + Method string `json:"method"` + Params struct { + SubID uint64 `json:"subscription"` + Result Out `json:"result"` + } `json:"params"` +} + +// --------------------------------------------------------------------------- + +func AsyncGetPrefixedWs[T any, Out any](t devtest.T, node *dsl.L2CLNode, prefix string, method string, runUntil <-chan T) <-chan Out { + userRPC := node.Escape().UserRPC() + wsRPC := strings.Replace(userRPC, "http", "ws", 1) + + output := make(chan Out, 128) + + go func() { + conn, _, err := websocket.DefaultDialer.DialContext(t.Ctx(), wsRPC, nil) + require.NoError(t, err, "dial: %v", err) + defer conn.Close() + defer close(output) + + // 1. send the *_subscribe request + require.NoError(t, conn.WriteJSON(rpcRequest{ + JSONRPC: "2.0", + ID: 1, + Method: prefix + "_" + "subscribe_" + method, + Params: nil, + }), "subscribe: %v", err) + + // 2. read the ack – blocking read just once + var a rpcResponse + require.NoError(t, conn.ReadJSON(&a), "ack: %v", err) + t.Log("subscribed to websocket - id=", string(a.Result)) + + // 3. defer the unsubscribe request + defer func() { + require.NoError(t, conn.WriteJSON(rpcRequest{ + JSONRPC: "2.0", + ID: 2, + Method: prefix + "_unsubscribe_" + method, + Params: []any{a.Result}, + }), "unsubscribe: %v", err) + + t.Log("gracefully closed websocket connection") + }() + + // Function to handle JSON reading with error channel + msgChan := make(chan json.RawMessage, 1) // Buffered channel to avoid goroutine leak + + go func() { + var msg json.RawMessage + defer close(msgChan) + + for { + if err := conn.ReadJSON(&msg); err != nil { + t.Log("readJSON channel closed") + return + } + + msgChan <- msg + } + }() + + // 4. start a goroutine that keeps reading pushes + for { + select { + case _, ok := <-runUntil: + // Clean‑up if necessary, then exit + if ok { + t.Log(method, "subscriber", "stopping: runUntil condition met") + } else { + t.Log(method, "subscriber", "stopping: runUntil channel closed") + } + return + case <-t.Ctx().Done(): + // Clean‑up if necessary, then exit + t.Log("unsafe head subscriber", "stopping: context cancelled") + return + case msg, ok := <-msgChan: + if !ok { + t.Log("readJSON channel closed") + return + } + + var p push[Out] + require.NoError(t, json.Unmarshal(msg, &p), "decode: %v", err) + + t.Log(wsRPC, method, "received websocket message", p.Params.Result) + output <- p.Params.Result + } + } + + }() + + return output +} + +func GetPrefixedWs[T any, Out any](t devtest.T, node *dsl.L2CLNode, prefix string, method string, runUntil <-chan T) []Out { + output := AsyncGetPrefixedWs[T, Out](t, node, prefix, method, runUntil) + + results := make([]Out, 0) + for result := range output { + results = append(results, result) + } + + return results +} + +func GetKonaWs[T any](t devtest.T, node *dsl.L2CLNode, method string, runUntil <-chan T) []eth.L2BlockRef { + return GetPrefixedWs[T, eth.L2BlockRef](t, node, "ws", method, runUntil) +} + +func GetKonaWsAsync[T any](t devtest.T, node *dsl.L2CLNode, method string, runUntil <-chan T) <-chan eth.L2BlockRef { + return AsyncGetPrefixedWs[T, eth.L2BlockRef](t, node, "ws", method, runUntil) +} + +func GetDevWS[T any](t devtest.T, node *dsl.L2CLNode, method string, runUntil <-chan T) []uint64 { + return GetPrefixedWs[T, uint64](t, node, "dev", method, runUntil) +} + +func GetDevWSAsync[T any](t devtest.T, node *dsl.L2CLNode, method string, runUntil <-chan T) <-chan uint64 { + return AsyncGetPrefixedWs[T, uint64](t, node, "dev", method, runUntil) +} diff --git a/kona/tests/supervisor/l1reorg/init_test.go b/kona/tests/supervisor/l1reorg/init_test.go new file mode 100644 index 0000000000000..10cfb13f2b953 --- /dev/null +++ b/kona/tests/supervisor/l1reorg/init_test.go @@ -0,0 +1,12 @@ +package reorgl1 + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/kona/tests/supervisor/l1reorg/reorg_test.go b/kona/tests/supervisor/l1reorg/reorg_test.go new file mode 100644 index 0000000000000..5321753b52b64 --- /dev/null +++ b/kona/tests/supervisor/l1reorg/reorg_test.go @@ -0,0 +1,150 @@ +package reorgl1 + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/kona/tests/supervisor/utils" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type checksFunc func(t devtest.T, sys *presets.SimpleInterop) + +func TestL1Reorg(gt *testing.T) { + gt.Run("unsafe reorg", func(gt *testing.T) { + var crossSafeRef, localSafeRef, unsafeRef, reorgAfter eth.BlockID + pre := func(t devtest.T, sys *presets.SimpleInterop) { + ss := sys.Supervisor.FetchSyncStatus() + + crossSafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossSafe + localSafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalSafe + unsafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalUnsafe.ID() + gt.Logf("Pre:: CrossSafe: %s, LocalSafe: %s, Unsafe: %s", crossSafeRef, localSafeRef, unsafeRef) + + // Calculate the divergent block + blockRef, err := sys.Supervisor.Escape().QueryAPI().CrossDerivedToSource(t.Ctx(), sys.L2ChainA.ChainID(), localSafeRef) + assert.Nil(gt, err, "Failed to query cross derived to source") + reorgAfter = blockRef.ID() + } + post := func(t devtest.T, sys *presets.SimpleInterop) { + require.True(t, sys.L2ELA.IsCanonical(crossSafeRef), "Previous cross-safe block should still be canonical") + require.True(t, sys.L2ELA.IsCanonical(localSafeRef), "Previous local-safe block should still be canonical") + require.False(t, sys.L2ELA.IsCanonical(unsafeRef), "Previous unsafe block should have been reorged") + } + testL2ReorgAfterL1Reorg(gt, &reorgAfter, pre, post) + }) +} + +func testL2ReorgAfterL1Reorg(gt *testing.T, reorgAfter *eth.BlockID, preChecks, postChecks checksFunc) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + trm := utils.NewTestReorgManager(t) + + sys.L1Network.WaitForBlock() + + trm.StopL1CL() + + // sequence some l1 blocks initially + for range 10 { + trm.GetBlockBuilder().BuildBlock(ctx, nil) + time.Sleep(5 * time.Second) + } + + // pre reorg trigger validations and checks + preChecks(t, sys) + + tip := sys.L1EL.BlockRefByLabel(eth.Unsafe).Number + + // create at least 5 blocks after the divergence point + for tip-reorgAfter.Number < 5 { + trm.GetBlockBuilder().BuildBlock(ctx, nil) + time.Sleep(5 * time.Second) + tip++ + } + + // Give some time so that those block are derived + time.Sleep(time.Second * 10) + + divergence := sys.L1EL.BlockRefByNumber(reorgAfter.Number + 1) + + tipL2_preReorg := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + // reorg the L1 chain -- sequence an alternative L1 block from divergence block parent + t.Log("Building Divergence Chain from:", divergence) + trm.GetBlockBuilder().BuildBlock(ctx, &divergence.ParentHash) + + t.Log("Stopping the batchers") + sys.L2BatcherA.Stop() + sys.L2BatcherB.Stop() + + t.Log("Starting the batchers again") + sys.L2BatcherA.Start() + sys.L2BatcherB.Start() + + // Give some time to batcher catch up + time.Sleep(5 * time.Second) + + // Start sequential block building + err := trm.GetPOS().Start() + require.NoError(t, err, "Expected to be able to start POS") + + // Wait sometime(5*5 = 25 at least) so that pos can create required + time.Sleep(30 * time.Second) + + // confirm L1 reorged + sys.L1EL.ReorgTriggered(divergence, 5) + + // wait until L2 chain A cross-safe ref caught up to where it was before the reorg + sys.L2CLA.Reached(types.CrossSafe, tipL2_preReorg.Number, 100) + + // test that latest chain A unsafe is not referencing a reorged L1 block (through the L1Origin field) + require.Eventually(t, func() bool { + unsafe := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + block, err := sys.L1EL.Escape().EthClient().InfoByNumber(ctx, unsafe.L1Origin.Number) + if err != nil { + sys.Log.Warn("failed to get L1 block info by number", "number", unsafe.L1Origin.Number, "err", err) + return false + } + + sys.Log.Info("current unsafe ref", "tip", unsafe, "tip_origin", unsafe.L1Origin, "l1blk", eth.InfoToL1BlockRef(block)) + + return block.Hash() == unsafe.L1Origin.Hash + }, 120*time.Second, 7*time.Second, "L1 block origin hash should match hash of block on L1 at that number. If not, it means there was a reorg, and L2 blocks L1Origin field is referencing a reorged block.") + + // confirm all L1Origin fields point to canonical blocks + require.Eventually(t, func() bool { + ref := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + var err error + + // wait until L2 chains' L1Origin points to a L1 block after the one that was reorged + if ref.L1Origin.Number < divergence.Number { + return false + } + + sys.Log.Info("L2 chain progressed, pointing to newer L1 block", "ref", ref, "ref_origin", ref.L1Origin, "divergence", divergence) + + for i := ref.Number; i > 0 && ref.L1Origin.Number >= divergence.Number; i-- { + ref, err = sys.L2ELA.Escape().L2EthClient().L2BlockRefByNumber(ctx, i) + if err != nil { + return false + } + + if !sys.L1EL.IsCanonical(ref.L1Origin) { + return false + } + } + + return true + }, 120*time.Second, 5*time.Second, "all L1Origin fields should point to canonical L1 blocks") + + // post reorg test validations and checks + postChecks(t, sys) +} diff --git a/kona/tests/supervisor/l2reorg/init_exec_msg_test.go b/kona/tests/supervisor/l2reorg/init_exec_msg_test.go new file mode 100644 index 0000000000000..b54f5e82036b1 --- /dev/null +++ b/kona/tests/supervisor/l2reorg/init_exec_msg_test.go @@ -0,0 +1,247 @@ +package l2reorg + +import ( + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go#L25 +func TestReorgInitExecMsg(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // three EOAs for triggering the init and exec interop txs, as well as a simple transfer tx + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + cathrine := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + + sys.L1Network.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + // stop batchers on chain A and on chain B + sys.L2BatcherA.Stop() + sys.L2BatcherB.Stop() + + // deploy event logger on chain A + var eventLoggerAddress common.Address + { + tx := txplan.NewPlannedTx(txplan.Combine( + alice.Plan(), + txplan.WithData(common.FromHex(bindings.EventloggerBin)), + )) + res, err := tx.Included.Eval(ctx) + require.NoError(t, err) + + eventLoggerAddress = res.ContractAddress + l.Info("deployed EventLogger", "chainID", tx.ChainID.Value(), "address", eventLoggerAddress) + } + + sys.L1Network.WaitForBlock() + + var initTrigger *txintent.InitTrigger + // prepare init trigger (i.e. what logs to emit on chain A) + { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + nTopics := 3 + lenData := 10 + initTrigger = interop.RandomInitTrigger(rng, eventLoggerAddress, nTopics, lenData) + + l.Info("created init trigger", "address", eventLoggerAddress, "topics", nTopics, "lenData", lenData) + } + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + var initTx *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput] + var initReceipt *types.Receipt + // prepare and include initiating message on chain A + { + initTx = txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](alice.Plan()) + initTx.Content.Set(initTrigger) + var err error + initReceipt, err = initTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + + l.Info("initiating message included", "chain", sys.L2ChainA.ChainID(), "block_number", initReceipt.BlockNumber, "block_hash", initReceipt.BlockHash, "now", time.Now().Unix()) + } + + // stop sequencer on chain A so that we later force a reorg/removal of the init msg + sys.L2CLA.StopSequencer() + + // at least one block between the init tx on chain A and the exec tx on chain B + sys.L2ChainB.WaitForBlock() + + var execTx *txintent.IntentTx[*txintent.ExecTrigger, *txintent.InteropOutput] + var execReceipt *types.Receipt + // prepare and include executing message on chain B + { + execTx = txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](bob.Plan()) + execTx.Content.DependOn(&initTx.Result) + // single event in tx so index is 0. ExecuteIndexed returns a lambda to transform InteropOutput to a new ExecTrigger + execTx.Content.Fn(txintent.ExecuteIndexed(constants.CrossL2Inbox, &initTx.Result, 0)) + var err error + execReceipt, err = execTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(execReceipt.Logs)) + + l.Info("executing message included", "chain", sys.L2ChainB.ChainID(), "block_number", execReceipt.BlockNumber, "block_hash", execReceipt.BlockHash, "now", time.Now().Unix()) + } + + // record divergence block numbers and original refs for future validation checks + var divergenceBlockNumber_A, divergenceBlockNumber_B uint64 + var originalRef_A, originalRef_B eth.L2BlockRef + + // sequence a conflicting block with a simple transfer tx, based on the parent of the parent of the unsafe head + { + var err error + divergenceBlockNumber_B = execReceipt.BlockNumber.Uint64() + originalRef_B, err = sys.L2ELB.Escape().L2EthClient().L2BlockRefByHash(ctx, execReceipt.BlockHash) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + headToReorgA := initReceipt.BlockHash + headToReorgARef, err := sys.L2ELA.Escape().L2EthClient().L2BlockRefByHash(ctx, headToReorgA) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + divergenceBlockNumber_A = headToReorgARef.Number + originalRef_A = headToReorgARef + + parentOfHeadToReorgA := headToReorgARef.ParentID() + parentsL1Origin, err := sys.L2ELA.Escape().L2EthClient().L2BlockRefByHash(ctx, parentOfHeadToReorgA.Hash) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + nextL1Origin := parentsL1Origin.L1Origin.Number + 1 + l1Origin, err := sys.L1Network.Escape().L1ELNode(match.FirstL1EL).EthClient().InfoByNumber(ctx, nextL1Origin) + require.NoError(t, err, "Expected to get block number %v from L1 execution client", nextL1Origin) + l1OriginHash := l1Origin.Hash() + + l.Info("Sequencing a conflicting block", "chain", sys.L2ChainA.ChainID(), "newL1Origin", eth.ToBlockID(l1Origin), "headToReorgA", headToReorgARef, "parent", parentOfHeadToReorgA, "parent_l1_origin", parentsL1Origin.L1Origin) + + err = ia.New(ctx, seqtypes.BuildOpts{ + Parent: parentOfHeadToReorgA.Hash, + L1Origin: &l1OriginHash, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include simple transfer tx in opened block + { + to := cathrine.PlanTransfer(alice.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // sequence a second block with op-test-sequencer + { + unsafe := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + l.Info("Current unsafe ref", "unsafeHead", unsafe) + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: unsafe.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // continue sequencing with op-node + sys.L2CLA.StartSequencer() + + // start batchers on chain A and on chain B + sys.L2BatcherA.Start() + sys.L2BatcherB.Start() + + // wait and confirm reorgs on chain A and B + dsl.CheckAll(t, + sys.L2ELA.ReorgTriggeredFn(eth.L2BlockRef{ + Number: divergenceBlockNumber_A, + Hash: originalRef_A.Hash, + ParentHash: originalRef_A.ParentID().Hash, + }, 30), + sys.L2ELB.ReorgTriggeredFn(eth.L2BlockRef{ + Number: divergenceBlockNumber_B, + Hash: originalRef_B.Hash, + ParentHash: originalRef_B.ParentID().Hash, + }, 30), + ) + + // executing tx should eventually be no longer confirmed on chain B + require.Eventually(t, func() bool { + receipt, err := sys.L2ELB.Escape().EthClient().TransactionReceipt(ctx, execReceipt.TxHash) + if err == nil || err.Error() != "not found" { // want to get "not found" error + return false + } + if receipt != nil { // want to get nil receipt + return false + } + return true + }, 60*time.Second, 3*time.Second, "Expected for the executing tx to be removed from chain B") + + err := wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_supervisor_A := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_supervisor_B := sys.Supervisor.SafeBlockID(sys.L2ChainB.ChainID()).Hash + safeL2Head_sequencer_A := sys.L2CLA.SafeL2BlockRef() + safeL2Head_sequencer_B := sys.L2CLB.SafeL2BlockRef() + + if safeL2Head_sequencer_A.Number < divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block A number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_sequencer_A.Number) + return false, nil + } + + if safeL2Head_sequencer_B.Number < divergenceBlockNumber_B { + l.Info("Safe ref number is still behind divergence block B number", "divergence", divergenceBlockNumber_B, "safe", safeL2Head_sequencer_B.Number) + return false, nil + } + + if safeL2Head_sequencer_A.Hash.Cmp(safeL2Head_supervisor_A) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer A", "supervisor", safeL2Head_supervisor_A, "sequencer", safeL2Head_sequencer_A.Hash) + return false, nil + } + + if safeL2Head_sequencer_B.Hash.Cmp(safeL2Head_supervisor_B) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer B", "supervisor", safeL2Head_supervisor_B, "sequencer", safeL2Head_sequencer_B.Hash) + return false, nil + } + + l.Info("Safe ref the same across supervisor and sequencers", + "supervisor_A", safeL2Head_supervisor_A, + "sequencer_A", safeL2Head_sequencer_A.Hash, + "supervisor_B", safeL2Head_supervisor_B, + "sequencer_B", safeL2Head_sequencer_B.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +} diff --git a/kona/tests/supervisor/l2reorg/init_test.go b/kona/tests/supervisor/l2reorg/init_test.go new file mode 100644 index 0000000000000..a20170b148c4d --- /dev/null +++ b/kona/tests/supervisor/l2reorg/init_test.go @@ -0,0 +1,13 @@ +package l2reorg + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // Other setups may be added here, hydrated from the same orchestrator + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/kona/tests/supervisor/l2reorg/invalid_exec_msgs_test.go b/kona/tests/supervisor/l2reorg/invalid_exec_msgs_test.go new file mode 100644 index 0000000000000..e082a6cc4b1f3 --- /dev/null +++ b/kona/tests/supervisor/l2reorg/invalid_exec_msgs_test.go @@ -0,0 +1,244 @@ +package l2reorg + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txplan" + suptypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// TestReorgInvalidExecMsgs tests that the supervisor reorgs the chain when an invalid exec msg is included +// Each subtest runs a test with a different invalid message, by modifying the message in the txModifierFn +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go#L28 +func TestReorgInvalidExecMsgs(gt *testing.T) { + gt.Run("invalid log index", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.LogIndex = 1024 + }) + }) + + gt.Run("invalid block number", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.BlockNumber = msg.Identifier.BlockNumber - 1 + }) + }) + + gt.Run("invalid chain id", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.ChainID = eth.ChainIDFromUInt64(1024) + }) + }) +} + +func testReorgInvalidExecMsg(gt *testing.T, txModifierFn func(msg *suptypes.Message)) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // three EOAs for triggering the init and exec interop txs, as well as a simple transfer tx + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + cathrine := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + + sys.L1Network.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + // stop batcher on chain A + sys.L2BatcherA.Stop() + + // deploy event logger on chain B + var eventLoggerAddress common.Address + { + tx := txplan.NewPlannedTx(txplan.Combine( + bob.Plan(), + txplan.WithData(common.FromHex(bindings.EventloggerBin)), + )) + res, err := tx.Included.Eval(ctx) + require.NoError(t, err) + + eventLoggerAddress = res.ContractAddress + l.Info("deployed EventLogger", "chainID", tx.ChainID.Value(), "address", eventLoggerAddress) + } + + sys.L1Network.WaitForBlock() + + var initTrigger *txintent.InitTrigger + // prepare init trigger (i.e. what logs to emit on chain A) + { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + nTopics := 3 + lenData := 10 + initTrigger = interop.RandomInitTrigger(rng, eventLoggerAddress, nTopics, lenData) + + l.Info("created init trigger", "address", eventLoggerAddress, "topics", nTopics, "lenData", lenData) + } + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + var initTx *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput] + var initReceipt *types.Receipt + // prepare and include initiating message on chain B + { + initTx = txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](bob.Plan()) + initTx.Content.Set(initTrigger) + var err error + initReceipt, err = initTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + + l.Info("initiating message included in chain B", "chain", sys.L2ChainB.ChainID(), "block_number", initReceipt.BlockNumber, "block_hash", initReceipt.BlockHash, "now", time.Now().Unix()) + } + + // at least one block between the init tx on chain B and the exec tx on chain A + sys.L2ChainA.WaitForBlock() + + // stop sequencer on chain A so that we later force include an invalid exec msg + latestUnsafe_A := sys.L2CLA.StopSequencer() + + var execTx *txintent.IntentTx[*txintent.ExecTrigger, *txintent.InteropOutput] + var execSignedTx *types.Transaction + var execTxEncoded []byte + // prepare and include invalid executing message on chain B via the op-test-sequencer (no other way to force-include an invalid message) + { + execTx = txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](alice.Plan()) + execTx.Content.DependOn(&initTx.Result) + // single event in tx so index is 0. + index := 0 + // lambda to transform InteropOutput to a new broken ExecTrigger + execTx.Content.Fn(func(ctx context.Context) (*txintent.ExecTrigger, error) { + events := initTx.Result.Value() + if x := len(events.Entries); x <= index { + return nil, fmt.Errorf("invalid index: %d, only have %d events", index, x) + } + msg := events.Entries[index] + // modify the message in order to make it invalid + txModifierFn(&msg) + return &txintent.ExecTrigger{ + Executor: constants.CrossL2Inbox, + Msg: msg, + }, nil + }) + + var err error + execSignedTx, err = execTx.PlannedTx.Signed.Eval(ctx) + require.NoError(t, err) + + l.Info("executing message signed", "to", execSignedTx.To(), "nonce", execSignedTx.Nonce(), "data", len(execSignedTx.Data())) + + execTxEncoded, err = execSignedTx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + } + + // sequence a new block with an invalid executing msg on chain A + { + l.Info("Building chain A with op-test-sequencer, and include invalid exec msg", "chain", sys.L2ChainA.ChainID(), "unsafeHead", latestUnsafe_A) + + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: latestUnsafe_A, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include invalid executing msg in opened block + err = ia.IncludeTx(ctx, execTxEncoded) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // record divergence block numbers and original refs for future validation checks + var divergenceBlockNumber_A uint64 + var originalHash_A common.Hash + var originalParentHash_A common.Hash + // sequence a second block with op-test-sequencer + { + currentUnsafeRef := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + l.Info("Unsafe head after invalid exec msg has been included in chain A", "chain", sys.L2ChainA.ChainID(), "unsafeHead", currentUnsafeRef, "parent", currentUnsafeRef.ParentID()) + + divergenceBlockNumber_A = currentUnsafeRef.Number + originalHash_A = currentUnsafeRef.Hash + originalParentHash_A = currentUnsafeRef.ParentHash + l.Info("Continue building chain A with another block with op-test-sequencer", "chain", sys.L2ChainA.ChainID(), "unsafeHead", currentUnsafeRef, "parent", currentUnsafeRef.ParentID()) + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: currentUnsafeRef.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + + // include simple transfer tx in opened block + { + to := cathrine.PlanTransfer(alice.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + } + + // continue sequencing with op-node + sys.L2CLA.StartSequencer() + + // start batcher on chain A + sys.L2BatcherA.Start() + + // wait for reorg on chain A + sys.L2ELA.ReorgTriggered(eth.L2BlockRef{ + Number: divergenceBlockNumber_A, + Hash: originalHash_A, + ParentHash: originalParentHash_A, + }, 30) + + err := wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_supervisor_A := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_sequencer_A := sys.L2CLA.SafeL2BlockRef() + + if safeL2Head_sequencer_A.Number < divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block A number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_sequencer_A.Number) + return false, nil + } + + if safeL2Head_sequencer_A.Hash.Cmp(safeL2Head_supervisor_A) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer A", "supervisor", safeL2Head_supervisor_A, "sequencer", safeL2Head_sequencer_A.Hash) + return false, nil + } + + l.Info("Safe ref the same across supervisor and sequencers", + "supervisor_A", safeL2Head_supervisor_A, + "sequencer_A", safeL2Head_sequencer_A.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +} diff --git a/kona/tests/supervisor/l2reorg/unsafe_head_test.go b/kona/tests/supervisor/l2reorg/unsafe_head_test.go new file mode 100644 index 0000000000000..a6c1411952f3c --- /dev/null +++ b/kona/tests/supervisor/l2reorg/unsafe_head_test.go @@ -0,0 +1,134 @@ +package l2reorg + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/stretchr/testify/require" +) + +// TestReorgUnsafeHead starts an interop chain with an op-test-sequencer, which takes control over sequencing the L2 chain and introduces a reorg on the unsafe head +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go#L17 +func TestReorgUnsafeHead(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // stop batcher on chain A + sys.L2BatcherA.Stop() + + // two EOAs for a sample transfer tx used later in a conflicting block + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.Wallet.NewEOA(sys.L2ELA) + + sys.L1Network.WaitForBlock() + + sys.L2ChainA.WaitForBlock() + // waiting for two blocks in order to make sure we are not jumping ahead of a L1 origin (i.e. can't build a chain with L1Origin gaps) + sys.L2ChainA.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + unsafeHead := sys.L2CLA.StopSequencer() + + var divergenceBlockNumber_A uint64 + var originalRef_A eth.L2BlockRef + // prepare and sequence a conflicting block for the L2A chain + { + unsafeHeadRef := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + l.Info("Current unsafe ref", "unsafeHead", unsafeHead, "parent", unsafeHeadRef.ParentID().Hash, "l1_origin", unsafeHeadRef.L1Origin) + + l.Info("Expect to reorg the chain on current unsafe block", "number", unsafeHeadRef.Number, "head", unsafeHead, "parent", unsafeHeadRef.ParentID().Hash) + divergenceBlockNumber_A = unsafeHeadRef.Number + originalRef_A = unsafeHeadRef + + parentOfUnsafeHead := unsafeHeadRef.ParentID() + + l.Info("Sequencing a conflicting block", "unsafeHead", unsafeHeadRef, "parent", parentOfUnsafeHead) + + // sequence a conflicting block with a simple transfer tx, based on the parent of the parent of the unsafe head + { + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: parentOfUnsafeHead.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include simple transfer tx in opened block + { + to := alice.PlanTransfer(bob.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + } + + // start batcher on chain A + sys.L2BatcherA.Start() + + // sequence a second block with op-test-sequencer (no L1 origin override) + { + l.Info("Sequencing with op-test-sequencer (no L1 origin override)") + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: sys.L2ELA.BlockRefByLabel(eth.Unsafe).Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + } + + // continue sequencing with consensus node (op-node) + sys.L2CLA.StartSequencer() + + sys.L2ChainA.WaitForBlock() + + reorgedRef_A, err := sys.L2ELA.Escape().EthClient().BlockRefByNumber(ctx, divergenceBlockNumber_A) + require.NoError(t, err, "Expected to be able to call BlockRefByNumber API, but got error") + + l.Info("Reorged chain A on divergence block number (prior the reorg)", "number", divergenceBlockNumber_A, "head", originalRef_A.Hash, "parent", originalRef_A.ParentID().Hash) + l.Info("Reorged chain A on divergence block number (after the reorg)", "number", divergenceBlockNumber_A, "head", reorgedRef_A.Hash, "parent", reorgedRef_A.ParentID().Hash) + require.NotEqual(t, originalRef_A.Hash, reorgedRef_A.Hash, "Expected to get different heads on divergence block number, but got the same hash, so no reorg happened on chain A") + require.Equal(t, originalRef_A.ParentID().Hash, reorgedRef_A.ParentHash, "Expected to get same parent hashes on divergence block number, but got different hashes") + + err = wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_A_supervisor := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_A_sequencer := sys.L2CLA.SafeL2BlockRef() + + if safeL2Head_A_sequencer.Number <= divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_A_sequencer.Number) + return false, nil + } + if safeL2Head_A_sequencer.Hash.Cmp(safeL2Head_A_supervisor) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer", "supervisor", safeL2Head_A_supervisor, "sequencer", safeL2Head_A_sequencer.Hash) + return false, nil + } + l.Info("Safe ref is the same on both supervisor and sequencer", "supervisor", safeL2Head_A_supervisor, "sequencer", safeL2Head_A_sequencer.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +} diff --git a/kona/tests/supervisor/l2reorgAfterL1reorg/init_test.go b/kona/tests/supervisor/l2reorgAfterL1reorg/init_test.go new file mode 100644 index 0000000000000..4764a31a444f4 --- /dev/null +++ b/kona/tests/supervisor/l2reorgAfterL1reorg/init_test.go @@ -0,0 +1,14 @@ +package sysgo + +import ( + "testing" + + spresets "github.com/ethereum-optimism/optimism/kona/tests/supervisor/presets" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // Other setups may be added here, hydrated from the same orchestrator + presets.DoMain(m, spresets.WithSimpleInteropMinimal()) +} diff --git a/kona/tests/supervisor/l2reorgAfterL1reorg/reorg_test.go b/kona/tests/supervisor/l2reorgAfterL1reorg/reorg_test.go new file mode 100644 index 0000000000000..7a99149d3359f --- /dev/null +++ b/kona/tests/supervisor/l2reorgAfterL1reorg/reorg_test.go @@ -0,0 +1,165 @@ +package sysgo + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type checksFunc func(t devtest.T, sys *presets.SimpleInterop) + +func TestL2ReorgAfterL1Reorg(gt *testing.T) { + gt.Run("unsafe reorg", func(gt *testing.T) { + var crossSafeRef, localSafeRef, unsafeRef eth.BlockID + pre := func(t devtest.T, sys *presets.SimpleInterop) { + ss := sys.Supervisor.FetchSyncStatus() + crossSafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossSafe + localSafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalSafe + unsafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalUnsafe.ID() + } + post := func(t devtest.T, sys *presets.SimpleInterop) { + require.True(t, sys.L2ELA.IsCanonical(crossSafeRef), "Previous cross-safe block should still be canonical") + require.True(t, sys.L2ELA.IsCanonical(localSafeRef), "Previous local-safe block should still be canonical") + require.False(t, sys.L2ELA.IsCanonical(unsafeRef), "Previous unsafe block should have been reorged") + } + testL2ReorgAfterL1Reorg(gt, 3, pre, post) + }) + + gt.Run("unsafe, local-safe, cross-unsafe, cross-safe reorgs", func(gt *testing.T) { + var crossSafeRef, crossUnsafeRef, localSafeRef, unsafeRef eth.BlockID + pre := func(t devtest.T, sys *presets.SimpleInterop) { + ss := sys.Supervisor.FetchSyncStatus() + crossUnsafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossUnsafe + crossSafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossSafe + localSafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalSafe + unsafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalUnsafe.ID() + } + post := func(t devtest.T, sys *presets.SimpleInterop) { + require.False(t, sys.L2ELA.IsCanonical(crossSafeRef), "Previous cross-safe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(crossUnsafeRef), "Previous cross-unsafe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(localSafeRef), "Previous local-safe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(unsafeRef), "Previous unsafe block should have been reorged") + } + testL2ReorgAfterL1Reorg(gt, 10, pre, post) + }) +} + +// testL2ReorgAfterL1Reorg tests that the L2 chain reorgs after an L1 reorg, and takes n, number of blocks to reorg, as parameter +// for unsafe reorgs - n must be at least >= confDepth, which is 2 in our test deployments +// for cross-safe reorgs - n must be at least >= safe distance, which is 10 in our test deployments (set in +// op-e2e/e2eutils/geth/geth.go when initialising FakePoS) +// pre- and post-checks are sanity checks to ensure that the blocks we expected to be reorged were indeed reorged or not +func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksFunc) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + sys.L1Network.WaitForBlock() + + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + // sequence a few L1 and L2 blocks + for range n + 1 { + sequenceL1Block(t, ts, common.Hash{}) + + sys.L2ChainA.WaitForBlock() + sys.L2ChainA.WaitForBlock() + } + + // select a divergence block to reorg from + var divergence eth.L1BlockRef + { + tip := sys.L1EL.BlockRefByLabel(eth.Unsafe) + require.Greater(t, tip.Number, uint64(n), "n is larger than L1 tip, cannot reorg out block number `tip-n`") + + divergence = sys.L1EL.BlockRefByNumber(tip.Number - uint64(n)) + } + + // print the chains before sequencing an alternative L1 block + sys.L2ChainA.PrintChain() + sys.L1Network.PrintChain() + + // pre reorg trigger validations and checks + preChecks(t, sys) + + tipL2_preReorg := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + // reorg the L1 chain -- sequence an alternative L1 block from divergence block parent + sequenceL1Block(t, ts, divergence.ParentHash) + + // continue building on the alternative L1 chain + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // confirm L1 reorged + sys.L1EL.ReorgTriggered(divergence, 5) + + // wait until L2 chain A cross-safe ref caught up to where it was before the reorg + sys.L2CLA.Reached(types.CrossSafe, tipL2_preReorg.Number, 50) + + // test that latest chain A unsafe is not referencing a reorged L1 block (through the L1Origin field) + require.Eventually(t, func() bool { + unsafe := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + block, err := sys.L1EL.Escape().EthClient().InfoByNumber(ctx, unsafe.L1Origin.Number) + if err != nil { + sys.Log.Warn("failed to get L1 block info by number", "number", unsafe.L1Origin.Number, "err", err) + return false + } + + sys.Log.Info("current unsafe ref", "tip", unsafe, "tip_origin", unsafe.L1Origin, "l1blk", eth.InfoToL1BlockRef(block)) + + // print the chains so we have information to debug if the test fails + sys.L2ChainA.PrintChain() + sys.L1Network.PrintChain() + + return block.Hash() == unsafe.L1Origin.Hash + }, 120*time.Second, 7*time.Second, "L1 block origin hash should match hash of block on L1 at that number. If not, it means there was a reorg, and L2 blocks L1Origin field is referencing a reorged block.") + + // confirm all L1Origin fields point to canonical blocks + require.Eventually(t, func() bool { + ref := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + var err error + + // wait until L2 chains' L1Origin points to a L1 block after the one that was reorged + if ref.L1Origin.Number < divergence.Number { + return false + } + + sys.Log.Info("L2 chain progressed, pointing to newer L1 block", "ref", ref, "ref_origin", ref.L1Origin, "divergence", divergence) + + for i := ref.Number; i > 0 && ref.L1Origin.Number >= divergence.Number; i-- { + ref, err = sys.L2ELA.Escape().L2EthClient().L2BlockRefByNumber(ctx, i) + if err != nil { + return false + } + + if !sys.L1EL.IsCanonical(ref.L1Origin) { + return false + } + } + + return true + }, 120*time.Second, 5*time.Second, "all L1Origin fields should point to canonical L1 blocks") + + // post reorg test validations and checks + postChecks(t, sys) +} + +func sequenceL1Block(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash) { + require.NoError(t, ts.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent})) + require.NoError(t, ts.Next(t.Ctx())) +} diff --git a/kona/tests/supervisor/message/init_test.go b/kona/tests/supervisor/message/init_test.go new file mode 100644 index 0000000000000..d51f54da12053 --- /dev/null +++ b/kona/tests/supervisor/message/init_test.go @@ -0,0 +1,21 @@ +package message + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +const ( + // SLEEP_BACKEND_READY is the time to wait for the backend to be ready + SLEEP_BACKEND_READY = 60 * time.Second +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // sleep to ensure the backend is ready + time.Sleep(SLEEP_BACKEND_READY) + + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/kona/tests/supervisor/message/interop_contract_test.go b/kona/tests/supervisor/message/interop_contract_test.go new file mode 100644 index 0000000000000..659f84ea7fe65 --- /dev/null +++ b/kona/tests/supervisor/message/interop_contract_test.go @@ -0,0 +1,86 @@ +package message + +import ( + "math/rand" + "testing" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum/go-ethereum/common" +) + +// TODO: Run the test directly from the https://github.com/ethereum-optimism/optimism/tree/develop/op-acceptance-tests + +// TestRegularMessage checks that messages can be sent and relayed via L2ToL2CrossDomainMessenger +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/contract/interop_contract_test.go +func TestRegularMessage(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + rng := rand.New(rand.NewSource(1234)) + + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + // deploy event logger at chain B + eventLoggerAddress := bob.DeployEventLogger() + // only use the binding to generate calldata + eventLogger := bindings.NewBindings[bindings.EventLogger]() + // manually build topics and data for EventLogger + topics := []eth.Bytes32{} + for range rng.Intn(5) { + var topic [32]byte + copy(topic[:], testutils.RandomData(rng, 32)) + topics = append(topics, topic) + } + data := testutils.RandomData(rng, rng.Intn(30)) + + calldata, err := eventLogger.EmitLog(topics, data).EncodeInputLambda() + require.NoError(err, "failed to prepare calldata") + + logger.Info("Send message", "address", eventLoggerAddress, "topicCnt", len(topics), "dataLen", len(data)) + trigger := &txintent.SendTrigger{ + Emitter: constants.L2ToL2CrossDomainMessenger, + DestChainID: bob.ChainID(), + Target: eventLoggerAddress, + RelayedCalldata: calldata, + } + // Intent to send message on chain A + txA := txintent.NewIntent[*txintent.SendTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(trigger) + + sendMsgReceipt, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err, "send msg receipt not found") + require.Equal(1, len(sendMsgReceipt.Logs)) // SentMessage event + require.Equal(constants.L2ToL2CrossDomainMessenger, sendMsgReceipt.Logs[0].Address) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to relay message on chain B + txB := txintent.NewIntent[*txintent.RelayTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + idx := 0 + txB.Content.Fn(txintent.RelayIndexed(constants.L2ToL2CrossDomainMessenger, &txA.Result, &txA.PlannedTx.Included, idx)) + + relayMsgReceipt, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err, "relay msg receipt not found") + + // ExecutingMessage, EventLogger, RelayedMessage Events + require.Equal(3, len(relayMsgReceipt.Logs)) + for logIdx, addr := range []common.Address{constants.CrossL2Inbox, eventLoggerAddress, constants.L2ToL2CrossDomainMessenger} { + require.Equal(addr, relayMsgReceipt.Logs[logIdx].Address) + } + // EventLogger topics and data + eventLog := relayMsgReceipt.Logs[1] + require.Equal(len(topics), len(eventLog.Topics)) + for topicIdx := range len(eventLog.Topics) { + require.Equal(topics[topicIdx][:], eventLog.Topics[topicIdx].Bytes()) + } + require.Equal(data, eventLog.Data) +} diff --git a/kona/tests/supervisor/message/interop_happy_tx_test.go b/kona/tests/supervisor/message/interop_happy_tx_test.go new file mode 100644 index 0000000000000..659ae37dc4ce0 --- /dev/null +++ b/kona/tests/supervisor/message/interop_happy_tx_test.go @@ -0,0 +1,57 @@ +package message + +import ( + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + + stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// TODO: Run the test directly from the https://github.com/ethereum-optimism/optimism/tree/develop/op-acceptance-tests + +// TestInteropHappyTx is testing that a valid init message, followed by a valid exec message are correctly +// included in two L2 chains and that the cross-safe ref for both of them progresses as expected beyond +// the block number where the messages were included +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_happy_tx_test.go +func TestInteropHappyTx(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + + // two EOAs for triggering the init and exec interop txs + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + // send initiating message on chain A + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + initTx, initReceipt := alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(3), rng.Intn(10))) + + // at least one block between the init tx on chain A and the exec tx on chain B + sys.L2ChainB.WaitForBlock() + + // send executing message on chain B + _, execReceipt := bob.SendExecMessage(initTx, 0) + + // confirm that the cross-safe safety passed init and exec receipts and that blocks were not reorged + dsl.CheckAll(t, + sys.L2CLA.ReachedRefFn(stypes.CrossSafe, eth.BlockID{ + Number: initReceipt.BlockNumber.Uint64(), + Hash: initReceipt.BlockHash, + }, 500), + sys.L2CLB.ReachedRefFn(stypes.CrossSafe, eth.BlockID{ + Number: execReceipt.BlockNumber.Uint64(), + Hash: execReceipt.BlockHash, + }, 500), + ) +} diff --git a/kona/tests/supervisor/message/interop_msg_test.go b/kona/tests/supervisor/message/interop_msg_test.go new file mode 100644 index 0000000000000..98dc675763b14 --- /dev/null +++ b/kona/tests/supervisor/message/interop_msg_test.go @@ -0,0 +1,645 @@ +package message + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/dsl/contract" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/plan" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum-optimism/optimism/op-service/txintent/contractio" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/sync/errgroup" + + suptypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// TODO: Run the test directly from the https://github.com/ethereum-optimism/optimism/tree/develop/op-acceptance-tests + +// TestInitExecMsg tests basic interop messaging +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L33 +func TestInitExecMsg(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + rng := rand.New(rand.NewSource(1234)) + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + eventLoggerAddress := alice.DeployEventLogger() + // Trigger random init message at chain A + initIntent, _ := alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(5), rng.Intn(30))) + // Make sure supervisor indexes block which includes init message + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + // Single event in tx so index is 0 + bob.SendExecMessage(initIntent, 0) +} + +// TestInitExecMsgWithDSL tests basic interop messaging with contract DSL +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L50 +func TestInitExecMsgWithDSL(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + rng := rand.New(rand.NewSource(1234)) + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + require := t.Require() + + eventLoggerAddress := alice.DeployEventLogger() + + clientA := sys.L2ELA.Escape().EthClient() + clientB := sys.L2ELB.Escape().EthClient() + + // Initialize eventLogger binding + eventLogger := bindings.NewBindings[bindings.EventLogger](bindings.WithClient(clientA), bindings.WithTest(t), bindings.WithTo(eventLoggerAddress)) + // Initialize crossL2Inbox binding + crossL2Inbox := bindings.NewBindings[bindings.CrossL2Inbox](bindings.WithClient(clientB), bindings.WithTest(t), bindings.WithTo(common.HexToAddress(predeploys.CrossL2Inbox))) + + // manually build topics and data for EventLogger + topics := []eth.Bytes32{} + for range rng.Intn(5) { + var topic [32]byte + copy(topic[:], testutils.RandomData(rng, 32)) + topics = append(topics, topic) + } + data := testutils.RandomData(rng, rng.Intn(30)) + + // Write: Alice triggers initiating message + receipt := contract.Write(alice, eventLogger.EmitLog(topics, data)) + block, err := clientA.BlockRefByNumber(t.Ctx(), receipt.BlockNumber.Uint64()) + require.NoError(err) + + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Manually build identifier, message, accesslist for executing message + // Single event in tx so index is 0 + logIdx := uint32(0) + payload := suptypes.LogToMessagePayload(receipt.Logs[logIdx]) + identifier := suptypes.Identifier{ + Origin: eventLoggerAddress, + BlockNumber: receipt.BlockNumber.Uint64(), + LogIndex: logIdx, + Timestamp: block.Time, + ChainID: sys.L2ELA.ChainID(), + } + payloadHash := crypto.Keccak256Hash(payload) + msgHash := eth.Bytes32(payloadHash) + msg := suptypes.Message{ + Identifier: identifier, PayloadHash: payloadHash, + } + accessList := types.AccessList{{ + Address: predeploys.CrossL2InboxAddr, + StorageKeys: suptypes.EncodeAccessList([]suptypes.Access{msg.Access()}), + }} + + call := crossL2Inbox.ValidateMessage(identifier, msgHash) + + // Read not using the DSL. Therefore you need to manually error handle and also set context + _, err = contractio.Read(call, t.Ctx()) + // Will revert because access list not provided + require.Error(err) + // Provide access list using txplan + _, err = contractio.Read(call, t.Ctx(), txplan.WithAccessList(accessList)) + // Success because access list made storage slot warm + require.NoError(err) + + // Read: Trigger executing message + contract.Read(call, txplan.WithAccessList(accessList)) + + // Write: Bob triggers executing message + contract.Write(bob, call, txplan.WithAccessList(accessList)) +} + +// TestRandomDirectedGraph tests below scenario: +// Construct random directed graph of messages. +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L125 +func TestRandomDirectedGraph(gt *testing.T) { + t := devtest.SerialT(gt) + + sys := presets.NewSimpleInterop(t) + logger := sys.Log.With("Test", "TestRandomDirectedGraph") + rng := rand.New(rand.NewSource(1234)) + require := sys.T.Require() + + // interop network has at least two chains + l2ChainNum := 2 + + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + // Deploy eventLoggers per every L2 chains because initiating messages can happen on any L2 chains + eventLoggerAddresses := []common.Address{alice.DeployEventLogger(), bob.DeployEventLogger()} + + // pubSubPairCnt is the count of (publisher, subscriber) pairs which + // - publisher initiates messages + // - subscriber validates messages + pubSubPairCnt := 5 + // txCnt is the count of transactions that each publisher emits + txCnt := 3 + // fundAmount is the ETH amount to fund publishers and subscribers + fundAmount := eth.OneTenthEther + + // jitter randomizes tx + jitter := func(rng *rand.Rand) { + time.Sleep(time.Duration(rng.Intn(250)) * time.Millisecond) + } + + // fund EOAs per chain + eoasPerChain := make([][]*dsl.EOA, l2ChainNum) + for chainIdx, funder := range []*dsl.Funder{sys.FunderA, sys.FunderB} { + eoas := funder.NewFundedEOAs(pubSubPairCnt, fundAmount) + eoasPerChain[chainIdx] = eoas + } + + // runPubSubPair spawns publisher goroutine, paired with subscriber goroutine + runPubSubPair := func(pubEOA, subEOA *dsl.EOA, eventLoggerAddress common.Address, localRng *rand.Rand) error { + ctx, cancel := context.WithCancel(t.Ctx()) + defer cancel() + + g, ctx := errgroup.WithContext(ctx) + + ch := make(chan *txintent.IntentTx[*txintent.MultiTrigger, *txintent.InteropOutput]) + + publisherRng := rand.New(rand.NewSource(localRng.Int63())) + subscriberRng := rand.New(rand.NewSource(localRng.Int63())) + + // publisher initiates txCnt transactions that includes multiple random messages + g.Go(func() error { + defer close(ch) + for range txCnt { + select { + case <-ctx.Done(): + return ctx.Err() + default: + tx, receipt, err := pubEOA.SendPackedRandomInitMessages(publisherRng, eventLoggerAddress) + if err != nil { + return fmt.Errorf("publisher error: %w", err) + } + logger.Info("Initiate messages included", "chainID", tx.PlannedTx.ChainID.Value(), "blockNumber", receipt.BlockNumber, "block", receipt.BlockHash) + select { + case ch <- tx: + case <-ctx.Done(): + return ctx.Err() + } + jitter(publisherRng) + } + } + return nil + }) + + // subscriber validates every messages that was initiated by the publisher + g.Go(func() error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case dependsOn, ok := <-ch: + if !ok { + return nil + } + tx, receipt, err := subEOA.SendPackedExecMessages(dependsOn) + if err != nil { + return fmt.Errorf("subscriber error: %w", err) + } + logger.Info("Validate messages included", "blockNumber", receipt.BlockNumber, "block", receipt.BlockHash) + logger.Info("Message dependency", + "sourceChainID", dependsOn.PlannedTx.ChainID.Value(), + "destChainID", tx.PlannedTx.ChainID.Value(), + "sourceBlockNum", dependsOn.PlannedTx.IncludedBlock.Value().Number, + "destBlockNum", receipt.BlockNumber) + jitter(subscriberRng) + } + } + }) + return g.Wait() + } + + var g errgroup.Group + + runPubSubPairWrapper := func(sourceIdx, destIdx, pairIdx int, localRng *rand.Rand) error { + return runPubSubPair(eoasPerChain[sourceIdx][pairIdx], eoasPerChain[destIdx][pairIdx], eventLoggerAddresses[sourceIdx], localRng) + } + + for pairIdx := range pubSubPairCnt { + // randomize source and destination L2 chain + sourceIdx := rng.Intn(2) + destIdx := 1 - sourceIdx + // localRng is needed per pubsub pair because rng cannot be shared without mutex + localRng := rand.New(rand.NewSource(rng.Int63())) + g.Go(func() error { + return runPubSubPairWrapper(sourceIdx, destIdx, pairIdx, localRng) + }) + } + require.NoError(g.Wait()) +} + +// TestInitExecMultipleMsg tests below scenario: +// Transaction initiates and executes multiple messages of self +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L247 +func TestInitExecMultipleMsg(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + // Intent to initiate two message(or emit event) on chain A + initCalls := []txintent.Call{ + interop.RandomInitTrigger(rng, eventLoggerAddress, 1, 15), + interop.RandomInitTrigger(rng, eventLoggerAddress, 2, 13), + } + txA := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(&txintent.MultiTrigger{Emitter: constants.MultiCall3, Calls: initCalls}) + + // Trigger two events + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate messages included", "block", receiptA.BlockHash) + require.Equal(2, len(receiptA.Logs)) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to validate messages on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // Two events in tx so use every index + indexes := []int{0, 1} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate messages included", "block", receiptB.BlockHash) + + // Check two ExecutingMessage triggered + require.Equal(2, len(receiptB.Logs)) +} + +// TestExecSameMsgTwice tests below scenario: +// Transaction that executes the same message twice. +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L292 +func TestExecSameMsgTwice(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // Intent to initiate message(or emit event) on chain A + txA := txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](alice.Plan()) + randomInitTrigger := interop.RandomInitTrigger(rng, eventLoggerAddress, 3, 10) + txA.Content.Set(randomInitTrigger) + + // Trigger single event + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate message included", "block", receiptA.BlockHash) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to validate same message two times on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // Single event in tx so indexes are 0, 0 + indexes := []int{0, 0} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate messages included", "block", receiptB.BlockHash) + + // Check two ExecutingMessage triggered + require.Equal(2, len(receiptB.Logs)) + // Check two messages are identical + require.Equal(receiptB.Logs[0].Topics, receiptB.Logs[1].Topics) +} + +// TestExecDifferentTopicCount tests below scenario: +// Execute message that links with initiating message with: 0, 1, 2, 3, or 4 topics in it +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L336 +func TestExecDifferentTopicCount(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // Intent to initiate message with different topic counts on chain A + initCalls := make([]txintent.Call, 5) + for topicCnt := range 5 { + initCalls[topicCnt] = interop.RandomInitTrigger(rng, eventLoggerAddress, topicCnt, 10) + } + txA := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(&txintent.MultiTrigger{Emitter: constants.MultiCall3, Calls: initCalls}) + + // Trigger five events, each have {0, 1, 2, 3, 4} topics in it + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate messages included", "block", receiptA.BlockHash) + require.Equal(5, len(receiptA.Logs)) + + for topicCnt := range 5 { + require.Equal(topicCnt, len(receiptA.Logs[topicCnt].Topics)) + } + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to validate message on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // Five events in tx so use every index + indexes := []int{0, 1, 2, 3, 4} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate message included", "block", receiptB.BlockHash) + + // Check five ExecutingMessage triggered + require.Equal(5, len(receiptB.Logs)) +} + +// TestExecMsgOpaqueData tests below scenario: +// Execute message that links with initiating message with: 0, 10KB of opaque event data in it +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L386 +func TestExecMsgOpaqueData(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // Intent to initiate message with two messages: 0, 10KB of opaque event data + initCalls := make([]txintent.Call, 2) + emptyInitTrigger := interop.RandomInitTrigger(rng, eventLoggerAddress, 2, 0) // 0B + largeInitTrigger := interop.RandomInitTrigger(rng, eventLoggerAddress, 3, 10_000) // 10KB + initCalls[0] = emptyInitTrigger + initCalls[1] = largeInitTrigger + + txA := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(&txintent.MultiTrigger{Emitter: constants.MultiCall3, Calls: initCalls}) + + // Trigger two events + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate messages included", "block", receiptA.BlockHash) + require.Equal(2, len(receiptA.Logs)) + require.Equal(emptyInitTrigger.OpaqueData, receiptA.Logs[0].Data) + require.Equal(largeInitTrigger.OpaqueData, receiptA.Logs[1].Data) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to validate messages on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // Two events in tx so use every index + indexes := []int{0, 1} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate messages included", "block", receiptB.BlockHash) + + // Check two ExecutingMessage triggered + require.Equal(2, len(receiptB.Logs)) +} + +// TestExecMsgDifferEventIndexInSingleTx tests below scenario: +// Execute message that links with initiating message with: first, random or last event of a tx. +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L436 +func TestExecMsgDifferEventIndexInSingleTx(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + alice, bob := sys.FunderA.NewFundedEOA(eth.OneTenthEther), sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // Intent to initiate message with multiple messages, all included in single tx + eventCnt := 10 + initCalls := make([]txintent.Call, eventCnt) + for index := range eventCnt { + initCalls[index] = interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(5), rng.Intn(100)) + } + + txA := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(&txintent.MultiTrigger{Emitter: constants.MultiCall3, Calls: initCalls}) + + // Trigger multiple events + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate messages included", "block", receiptA.BlockHash) + require.Equal(eventCnt, len(receiptA.Logs)) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Intent to validate messages on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // first, random or last event of a tx. + indexes := []int{0, 1 + rng.Intn(eventCnt-1), eventCnt - 1} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate messages included", "block", receiptB.BlockHash) + + // Check three ExecutingMessage triggered + require.Equal(len(indexes), len(receiptB.Logs)) +} + +type invalidAttributeType string + +const ( + randomOrigin invalidAttributeType = "randomOrigin" + randomBlockNumber invalidAttributeType = "randomBlockNumber" + randomLogIndex invalidAttributeType = "randomLogIndex" + randomTimestamp invalidAttributeType = "randomTimestamp" + randomChainID invalidAttributeType = "randomChainID" + mismatchedLogIndex invalidAttributeType = "mismatchedLogIndex" + mismatchedTimestamp invalidAttributeType = "mismatchedTimestamp" + msgNotPresent invalidAttributeType = "msgNotPresent" + logIndexGreaterOrEqualToEventCnt invalidAttributeType = "logIndexGreaterOrEqualToEventCnt" +) + +// executeIndexedFault builds on top of txintent.ExecuteIndexed to inject a fault for the identifier of message +func executeIndexedFault( + executor common.Address, + events *plan.Lazy[*txintent.InteropOutput], + index int, + rng *rand.Rand, + faults []invalidAttributeType, + destChainID eth.ChainID, +) func(ctx context.Context) (*txintent.ExecTrigger, error) { + return func(ctx context.Context) (*txintent.ExecTrigger, error) { + execTrigger, err := txintent.ExecuteIndexed(executor, events, index)(ctx) + if err != nil { + return nil, err + } + newMsg := execTrigger.Msg + for _, fault := range faults { + switch fault { + case randomOrigin: + newMsg.Identifier.Origin = testutils.RandomAddress(rng) + case randomBlockNumber: + // make sure that the faulty blockNumber does not exceed type(uint64).max for CrossL2Inbox check + newMsg.Identifier.BlockNumber = rng.Uint64() / 2 + case randomLogIndex: + // make sure that the faulty logIndex does not exceed type(uint32).max for CrossL2Inbox check + newMsg.Identifier.LogIndex = rng.Uint32() / 2 + case randomTimestamp: + // make sure that the faulty Timestamp does not exceed type(uint64).max for CrossL2Inbox check + newMsg.Identifier.Timestamp = rng.Uint64() / 2 + case randomChainID: + newMsg.Identifier.ChainID = eth.ChainIDFromBytes32([32]byte(testutils.RandomData(rng, 32))) + case mismatchedLogIndex: + // valid msg within block, but mismatching event index + newMsg.Identifier.LogIndex += 1 + case mismatchedTimestamp: + // within time window, but mismatching block + newMsg.Identifier.Timestamp += 2 + case msgNotPresent: + // valid chain but msg not there + // use destination chain ID because initiating message is not present in dest chain + newMsg.Identifier.ChainID = destChainID + case logIndexGreaterOrEqualToEventCnt: + // execute implied-conflict message: point to event-index >= number of logs + // number of logs == number of entries + // so set the invalid logindex to number of entries + newMsg.Identifier.LogIndex = uint32(len(events.Value().Entries)) + default: + panic("invalid type") + } + } + return &txintent.ExecTrigger{ + Executor: executor, + Msg: newMsg, + }, nil + } +} + +// TestExecMessageInvalidAttributes tests below scenario: +// Execute message, but with one or more invalid attributes inside identifiers +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/message/interop_msg_test.go#L554 +func TestExecMessageInvalidAttributes(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + require := sys.T.Require() + logger := t.Logger() + + rng := rand.New(rand.NewSource(1234)) + // honest EOA which initiates messages + alice := sys.FunderA.NewFundedEOA(eth.OneTenthEther) + // honest EOA which executes messages + bob := sys.FunderB.NewFundedEOA(eth.OneTenthEther) + // malicious EOA which creates executing messages with invalid attributes + chuck := sys.FunderB.NewFundedEOA(eth.OneTenthEther) + + eventLoggerAddress := alice.DeployEventLogger() + + // Intent to initiate messages(or emit events) on chain A + initCalls := []txintent.Call{ + interop.RandomInitTrigger(rng, eventLoggerAddress, 3, 10), + interop.RandomInitTrigger(rng, eventLoggerAddress, 2, 95), + interop.RandomInitTrigger(rng, eventLoggerAddress, 1, 50), + } + txA := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](alice.Plan()) + txA.Content.Set(&txintent.MultiTrigger{Emitter: constants.MultiCall3, Calls: initCalls}) + + // Trigger multiple events + receiptA, err := txA.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Initiate messages included", "block", receiptA.BlockHash) + + // Make sure supervisor syncs the chain A events + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + faultsLists := [][]invalidAttributeType{ + // test each identifier attributes to be faulty for upper bound tests + {randomOrigin}, {randomBlockNumber}, {randomLogIndex}, {randomTimestamp}, {randomChainID}, + // test for every attributes to be faulty for upper bound tests + {randomOrigin, randomBlockNumber, randomLogIndex, randomTimestamp, randomChainID}, + // test for non-random invalid attributes + {mismatchedLogIndex}, {mismatchedTimestamp}, {msgNotPresent}, {logIndexGreaterOrEqualToEventCnt}, + } + + for _, faults := range faultsLists { + logger.Info("Attempt to validate message with invalid attribute", "faults", faults) + // Intent to validate message on chain B + txC := txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](chuck.Plan()) + txC.Content.DependOn(&txA.Result) + + // Random select event index in tx for injecting faults + eventIdx := rng.Intn(len(initCalls)) + txC.Content.Fn(executeIndexedFault(constants.CrossL2Inbox, &txA.Result, eventIdx, rng, faults, chuck.ChainID())) + + // make sure that the transaction is not reverted by CrossL2Inbox... + gas, err := txC.PlannedTx.Gas.Eval(t.Ctx()) + require.NoError(err) + require.Greater(gas, uint64(0)) + + // but rather not included at chain B because of supervisor check + // chain B L2 EL will query supervisor to check whether given message is valid + // supervisor will throw ErrConflict(conflicting data), and L2 EL will drop tx + _, err = txC.PlannedTx.Included.Eval(t.Ctx()) + require.Error(err) + logger.Info("Validate message not included") + } + + // we now attempt to execute msg correctly + // Intent to validate message on chain B + txB := txintent.NewIntent[*txintent.MultiTrigger, *txintent.InteropOutput](bob.Plan()) + txB.Content.DependOn(&txA.Result) + + // Three events in tx so use every index + indexes := []int{0, 1, 2} + txB.Content.Fn(txintent.ExecuteIndexeds(constants.MultiCall3, constants.CrossL2Inbox, &txA.Result, indexes)) + + receiptB, err := txB.PlannedTx.Included.Eval(t.Ctx()) + require.NoError(err) + logger.Info("Validate message included", "block", receiptB.BlockHash) + + // Check three ExecutingMessage triggered + require.Equal(3, len(receiptB.Logs)) +} diff --git a/kona/tests/supervisor/pre_interop/init_test.go b/kona/tests/supervisor/pre_interop/init_test.go new file mode 100644 index 0000000000000..b32be2f8b752d --- /dev/null +++ b/kona/tests/supervisor/pre_interop/init_test.go @@ -0,0 +1,20 @@ +package preinterop + +// todo: add tests +import ( + "testing" + + spresets "github.com/ethereum-optimism/optimism/kona/tests/supervisor/presets" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // sleep to ensure the backend is ready + + presets.DoMain(m, + spresets.WithSimpleInteropMinimal(), + presets.WithSuggestedInteropActivationOffset(30), + presets.WithInteropNotAtGenesis()) + +} diff --git a/kona/tests/supervisor/pre_interop/post_test.go b/kona/tests/supervisor/pre_interop/post_test.go new file mode 100644 index 0000000000000..b3c2d46eefc2f --- /dev/null +++ b/kona/tests/supervisor/pre_interop/post_test.go @@ -0,0 +1,216 @@ +package preinterop + +import ( + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + "github.com/ethereum-optimism/optimism/op-core/forks" + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/eth" + stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func testSupervisorActivationBlock(t devtest.T, sys *presets.SimpleInterop, net *dsl.L2Network, activationBlock eth.BlockID) { + require := t.Require() + + // wait for some time to ensure the interop activation block is become cross-safe + t.Logger().Info("Waiting for interop activation block to be cross-safe") + sys.Supervisor.WaitForL2HeadToAdvanceTo(net.ChainID(), stypes.CrossSafe, activationBlock) + + interopTime := net.Escape().ChainConfig().InteropTime + pre := net.LatestBlockBeforeTimestamp(t, *interopTime) + require.NotNil(pre, "Pre-interop block should be found before interop time") + + // make sure pre-interop block is parent of activation block + require.Equal(pre.Number, activationBlock.Number-1, "Activation block should be one after pre-interop block") + + // fetching the source for the pre-interop block should return the error + // this is to make sure that we only store the blocks after interop + _, err := sys.Supervisor.Escape().QueryAPI().CrossDerivedToSource(t.Ctx(), net.ChainID(), pre.ID()) + require.Error(err, "CrossDerivedToSource should error before interop") + + // fetch the source for the activation block + derivedFrom, err := sys.Supervisor.Escape().QueryAPI().CrossDerivedToSource(t.Ctx(), net.ChainID(), activationBlock) + require.NoError(err, "CrossDerivedToSource should not error after interop") + require.NotNil(derivedFrom, "CrossDerivedToSource should return a valid source block") +} + +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/upgrade/post_test.go +// test case modified to check the correctness of the supervisor activation block as well +func TestPostInbox(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + devtest.RunParallel(t, sys.L2Networks(), func(t devtest.T, net *dsl.L2Network) { + require := t.Require() + activationBlock := net.AwaitActivation(t, forks.Interop) + + el := net.Escape().L2ELNode(match.FirstL2EL) + implAddrBytes, err := el.EthClient().GetStorageAt(t.Ctx(), predeploys.CrossL2InboxAddr, + genesis.ImplementationSlot, activationBlock.Hash.String()) + require.NoError(err) + implAddr := common.BytesToAddress(implAddrBytes[:]) + require.NotEqual(common.Address{}, implAddr) + code, err := el.EthClient().CodeAtHash(t.Ctx(), implAddr, activationBlock.Hash) + require.NoError(err) + require.NotEmpty(code) + + testSupervisorActivationBlock(t, sys, net, activationBlock) + }) +} + +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/upgrade/post_test.go +func TestPostInteropUpgradeComprehensive(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + require := t.Require() + logger := t.Logger() + + // Wait for networks to be online by waiting for blocks + sys.L1Network.WaitForBlock() + sys.L2ChainA.WaitForBlock() + sys.L2ChainB.WaitForBlock() + + // Get interop activation time + interopTime := sys.L2ChainA.Escape().ChainConfig().InteropTime + require.NotNil(interopTime, "InteropTime must be set") + + logger.Info("Starting comprehensive post-interop upgrade tests", "interopTime", *interopTime) + + // 1. Check that anchor block of supervisor matches the activation block + logger.Info("Checking supervisor anchor block matches activation block") + testSupervisorAnchorBlock(t, sys) + + // 2. Check that the supervisor has safety progression for each level + logger.Info("Checking supervisor safety progression") + testSupervisorSafetyProgression(t, sys) + + // 3. Confirms that interop message can be included + logger.Info("Testing interop message inclusion") + testInteropMessageInclusion(t, sys) + + logger.Info("All comprehensive post-interop upgrade tests completed successfully") +} + +// testSupervisorAnchorBlock checks that the supervisor's anchor block has been set and matches the upgrade timestamp +func testSupervisorAnchorBlock(t devtest.T, sys *presets.SimpleInterop) { + logger := t.Logger() + + // Use the DSL helper for anchor block validation + logger.Info("Testing supervisor anchor block functionality") + + // Phase 1: Wait for L2 chains to reach interop activation time + logger.Info("Phase 1: Waiting for L2 chains to reach interop activation time") + + devtest.RunParallel(t, sys.L2Networks(), func(t devtest.T, net *dsl.L2Network) { + + // Gate test to not time out before upgrade happens + forkTimestamp := net.Escape().ChainConfig().InteropTime + t.Gate().NotNil(forkTimestamp, "Must have fork configured") + t.Gate().Greater(*forkTimestamp, uint64(0), "Must not start fork at genesis") + upgradeTime := time.Unix(int64(*forkTimestamp), 0) + if deadline, hasDeadline := t.Deadline(); hasDeadline { + t.Gate().True(upgradeTime.Before(deadline), "test must not time out before upgrade happens") + } + + activationBlock := net.AwaitActivation(t, forks.Interop) + sys.Supervisor.WaitForL2HeadToAdvanceTo(net.ChainID(), stypes.CrossSafe, activationBlock) + + logger.Info("Validating anchor block timing", + "chainID", net.ChainID(), + "derivedBlockNumber", activationBlock.Number, + "interopTime", *forkTimestamp) + }) + + logger.Info("Supervisor anchor block validation completed successfully") +} + +// testSupervisorSafetyProgression checks that supervisor has safety progression for each level +func testSupervisorSafetyProgression(t devtest.T, sys *presets.SimpleInterop) { + logger := t.Logger() + logger.Info("Testing supervisor safety progression") + + delta := uint64(3) // Minimum blocks of progression expected + dsl.CheckAll(t, + sys.L2CLA.AdvancedFn(stypes.LocalUnsafe, delta, 30), + sys.L2CLB.AdvancedFn(stypes.LocalUnsafe, delta, 30), + + sys.L2CLA.AdvancedFn(stypes.LocalSafe, delta, 30), + sys.L2CLB.AdvancedFn(stypes.LocalSafe, delta, 30), + + sys.L2CLA.AdvancedFn(stypes.CrossUnsafe, delta, 30), + sys.L2CLB.AdvancedFn(stypes.CrossUnsafe, delta, 30), + + sys.L2CLA.AdvancedFn(stypes.CrossSafe, delta, 60), + sys.L2CLB.AdvancedFn(stypes.CrossSafe, delta, 60), + ) + + logger.Info("Supervisor safety progression validation completed successfully") +} + +// testInteropMessageInclusion confirms that interop messages can be included post-upgrade +func testInteropMessageInclusion(t devtest.T, sys *presets.SimpleInterop) { + logger := t.Logger() + logger.Info("Starting interop message inclusion test") + + // Phase 1: Setup test accounts and contracts + alice, bob, eventLoggerAddress := setupInteropTestEnvironment(sys) + + // Phase 2: Send init message on chain A + rng := rand.New(rand.NewSource(1234)) + initIntent, initReceipt := alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(5), rng.Intn(30))) + + // Make sure supervisor indexes block which includes init message + sys.Supervisor.WaitForUnsafeHeadToAdvance(alice.ChainID(), 2) + + // Single event in tx so index is 0 + _, execReceipt := bob.SendExecMessage(initIntent, 0) + + // Phase 5: Verify cross-safe progression + verifyInteropMessagesProgression(t, sys, initReceipt, execReceipt) + + logger.Info("Interop message inclusion test completed successfully") +} + +// setupInteropTestEnvironment creates test accounts and deploys necessary contracts +func setupInteropTestEnvironment(sys *presets.SimpleInterop) (alice, bob *dsl.EOA, eventLoggerAddress common.Address) { + + // Create EOAs for interop messaging + alice = sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob = sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + // Deploy event logger contract on chain A + eventLoggerAddress = alice.DeployEventLogger() + + // Wait for chains to catch up + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + return alice, bob, eventLoggerAddress +} + +// verifyInteropMessagesProgression verifies cross-safe progression for both init and exec messages +func verifyInteropMessagesProgression(t devtest.T, sys *presets.SimpleInterop, initReceipt, execReceipt *types.Receipt) { + logger := t.Logger() + + // Verify cross-safe progression for both messages + dsl.CheckAll(t, + sys.L2CLA.ReachedRefFn(stypes.CrossSafe, eth.BlockID{ + Number: initReceipt.BlockNumber.Uint64(), + Hash: initReceipt.BlockHash, + }, 60), + sys.L2CLB.ReachedRefFn(stypes.CrossSafe, eth.BlockID{ + Number: execReceipt.BlockNumber.Uint64(), + Hash: execReceipt.BlockHash, + }, 60), + ) + + logger.Info("Cross-safe progression verified for both init and exec messages") +} diff --git a/kona/tests/supervisor/pre_interop/pre_test.go b/kona/tests/supervisor/pre_interop/pre_test.go new file mode 100644 index 0000000000000..12b3d8a4a87a9 --- /dev/null +++ b/kona/tests/supervisor/pre_interop/pre_test.go @@ -0,0 +1,114 @@ +package preinterop + +import ( + "math/rand" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + "github.com/ethereum-optimism/optimism/op-core/predeploys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txintent" + stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum/go-ethereum/core/types" +) + +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/upgrade/pre_test.go +func TestPreNoInbox(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + require := t.Require() + + t.Logger().Info("Starting") + + devtest.RunParallel(t, sys.L2Networks(), func(t devtest.T, net *dsl.L2Network) { + interopTime := net.Escape().ChainConfig().InteropTime + t.Require().NotNil(interopTime) + pre := net.LatestBlockBeforeTimestamp(t, *interopTime) + el := net.Escape().L2ELNode(match.FirstL2EL) + codeAddr := common.HexToAddress("0xC0D3C0d3C0D3C0d3c0d3c0D3c0D3C0d3C0D30022") + implCode, err := el.EthClient().CodeAtHash(t.Ctx(), codeAddr, pre.Hash) + require.NoError(err) + require.Len(implCode, 0, "needs to be empty") + implAddrBytes, err := el.EthClient().GetStorageAt(t.Ctx(), predeploys.CrossL2InboxAddr, + genesis.ImplementationSlot, pre.Hash.String()) + require.NoError(err) + require.Equal(common.Address{}, common.BytesToAddress(implAddrBytes[:])) + }) + + // try access the sync-status of the supervisor, assert that the sync-status returns the expected error + devtest.RunParallel(t, sys.L2Networks(), func(t devtest.T, net *dsl.L2Network) { + interopTime := net.Escape().ChainConfig().InteropTime + + _, err := sys.Supervisor.Escape().QueryAPI().SyncStatus(t.Ctx()) + require.ErrorContains(err, "chain database is not initialized") + + // confirm we are still pre-interop + require.False(net.IsActivated(*interopTime)) + t.Logger().Info("Timestamps", "interopTime", *interopTime, "now", time.Now().Unix()) + }) + + var initReceipt *types.Receipt + var initTx *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput] + // try interop before the upgrade, confirm that messages do not get included + { + // two EOAs for triggering the init and exec interop txs + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + interopTimeA := sys.L2ChainA.Escape().ChainConfig().InteropTime + interopTimeB := sys.L2ChainB.Escape().ChainConfig().InteropTime + + eventLoggerAddress := alice.DeployEventLogger() + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + // send initiating message on chain A + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + initTx, initReceipt = alice.SendInitMessage(interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(3), rng.Intn(10))) + + // at least one block between the init tx on chain A and the exec tx on chain B + sys.L2ChainB.WaitForBlock() + + // send executing message on chain B and confirm we got an error + execTx := txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](bob.Plan()) + execTx.Content.DependOn(&initTx.Result) + execTx.Content.Fn(txintent.ExecuteIndexed(constants.CrossL2Inbox, &initTx.Result, 0)) + execReceipt, err := execTx.PlannedTx.Included.Eval(sys.T.Ctx()) + require.ErrorContains(err, "implementation not initialized", "error did not contain expected string") + require.Nil(execReceipt) + + t.Logger().Info("initReceipt", "blocknum", initReceipt.BlockNumber, "txhash", initReceipt.TxHash) + + // confirm we are still pre-interop + require.False(sys.L2ChainA.IsActivated(*interopTimeA)) + require.False(sys.L2ChainB.IsActivated(*interopTimeB)) + t.Logger().Info("Timestamps", "interopTimeA", *interopTimeA, "interopTimeB", *interopTimeB, "now", time.Now().Unix()) + } + + // check that log events from a block before activation, when converted into an access-list, fail the check-access-list RPC check + { + ctx := sys.T.Ctx() + + execTrigger, err := txintent.ExecuteIndexed(constants.CrossL2Inbox, &initTx.Result, 0)(ctx) + require.NoError(err) + + ed := stypes.ExecutingDescriptor{Timestamp: uint64(time.Now().Unix())} + accessEntries := []stypes.Access{execTrigger.Msg.Access()} + accessList := stypes.EncodeAccessList(accessEntries) + + err = sys.Supervisor.Escape().QueryAPI().CheckAccessList(ctx, accessList, stypes.CrossSafe, ed) + require.ErrorContains(err, "conflicting data") + } + + t.Logger().Info("Done") +} diff --git a/kona/tests/supervisor/presets/interop_minimal.go b/kona/tests/supervisor/presets/interop_minimal.go new file mode 100644 index 0000000000000..64d9c77e98be6 --- /dev/null +++ b/kona/tests/supervisor/presets/interop_minimal.go @@ -0,0 +1,98 @@ +package presets + +import ( + "os" + "path/filepath" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder" +) + +// WithSimpleInteropMinimal specifies a system that meets the SimpleInterop criteria removing the Challenger. +func WithSimpleInteropMinimal() stack.CommonOption { + return stack.MakeCommon(DefaultMinimalInteropSystem(&sysgo.DefaultInteropSystemIDs{})) +} + +func DefaultMinimalInteropSystem(dest *sysgo.DefaultInteropSystemIDs) stack.Option[*sysgo.Orchestrator] { + ids := sysgo.NewDefaultInteropSystemIDs(sysgo.DefaultL1ID, sysgo.DefaultL2AID, sysgo.DefaultL2BID) + opt := stack.Combine[*sysgo.Orchestrator]() + + // start with single chain interop system + opt.Add(baseInteropSystem(&ids.DefaultSingleChainInteropSystemIDs)) + + opt.Add(sysgo.WithDeployerOptions( + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2B.ChainID()), + sysgo.WithInteropAtGenesis(), // this can be overridden by later options + )) + opt.Add(sysgo.WithL2ELNode(ids.L2BEL, sysgo.L2ELWithSupervisor(ids.Supervisor))) + opt.Add(sysgo.WithL2CLNode(ids.L2BCL, ids.L1CL, ids.L1EL, ids.L2BEL, sysgo.L2CLSequencer(), sysgo.L2CLIndexing())) + opt.Add(sysgo.WithBatcher(ids.L2BBatcher, ids.L1EL, ids.L2BCL, ids.L2BEL)) + + opt.Add(sysgo.WithManagedBySupervisor(ids.L2BCL, ids.Supervisor)) + + // Note: we provide L2 CL nodes still, even though they are not used post-interop. + // Since we may create an interop infra-setup, before interop is even scheduled to run. + opt.Add(sysgo.WithProposer(ids.L2BProposer, ids.L1EL, &ids.L2BCL, &ids.Supervisor)) + + opt.Add(sysgo.WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2AEL, ids.L2BEL})) + + // Upon evaluation of the option, export the contents we created. + // Ids here are static, but other things may be exported too. + opt.Add(stack.Finally(func(orch *sysgo.Orchestrator) { + *dest = ids + })) + + return opt +} + +// baseInteropSystem defines a system that supports interop with a single chain +// Components which are shared across multiple chains are not started, allowing them to be added later including +// any additional chains that have been added. +func baseInteropSystem(ids *sysgo.DefaultSingleChainInteropSystemIDs) stack.Option[*sysgo.Orchestrator] { + opt := stack.Combine[*sysgo.Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *sysgo.Orchestrator) { + o.P().Logger().Info("Setting up") + })) + + opt.Add(sysgo.WithMnemonicKeys(devkeys.TestMnemonic)) + + // Get artifacts path + artifactsPath := os.Getenv("OP_DEPLOYER_ARTIFACTS") + if artifactsPath == "" { + panic("OP_DEPLOYER_ARTIFACTS is not set") + } + + opt.Add(sysgo.WithDeployer(), + sysgo.WithDeployerPipelineOption( + sysgo.WithDeployerCacheDir(artifactsPath), + ), + sysgo.WithDeployerOptions( + func(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) { + builder.WithL1ContractsLocator(artifacts.MustNewFileLocator(filepath.Join(artifactsPath, "src"))) + builder.WithL2ContractsLocator(artifacts.MustNewFileLocator(filepath.Join(artifactsPath, "src"))) + }, + sysgo.WithCommons(ids.L1.ChainID()), + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2A.ChainID()), + ), + ) + + opt.Add(sysgo.WithL1Nodes(ids.L1EL, ids.L1CL)) + + opt.Add(sysgo.WithSupervisor(ids.Supervisor, ids.Cluster, ids.L1EL)) + + opt.Add(sysgo.WithL2ELNode(ids.L2AEL, sysgo.L2ELWithSupervisor(ids.Supervisor))) + opt.Add(sysgo.WithL2CLNode(ids.L2ACL, ids.L1CL, ids.L1EL, ids.L2AEL, sysgo.L2CLSequencer(), sysgo.L2CLIndexing())) + opt.Add(sysgo.WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2ACL, ids.L1EL, ids.L2AEL)) + opt.Add(sysgo.WithBatcher(ids.L2ABatcher, ids.L1EL, ids.L2ACL, ids.L2AEL)) + + opt.Add(sysgo.WithManagedBySupervisor(ids.L2ACL, ids.Supervisor)) + + // Note: we provide L2 CL nodes still, even though they are not used post-interop. + // Since we may create an interop infra-setup, before interop is even scheduled to run. + opt.Add(sysgo.WithProposer(ids.L2AProposer, ids.L1EL, &ids.L2ACL, &ids.Supervisor)) + return opt +} diff --git a/kona/tests/supervisor/rpc/init_test.go b/kona/tests/supervisor/rpc/init_test.go new file mode 100644 index 0000000000000..b11738839ec9b --- /dev/null +++ b/kona/tests/supervisor/rpc/init_test.go @@ -0,0 +1,21 @@ +package rpc + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +const ( + // SLEEP_BACKEND_READY is the time to wait for the backend to be ready + SLEEP_BACKEND_READY = 90 * time.Second +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // sleep to ensure the backend is ready + time.Sleep(SLEEP_BACKEND_READY) + + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/kona/tests/supervisor/rpc/rpc_test.go b/kona/tests/supervisor/rpc/rpc_test.go new file mode 100644 index 0000000000000..534f198dce8db --- /dev/null +++ b/kona/tests/supervisor/rpc/rpc_test.go @@ -0,0 +1,331 @@ +package rpc + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TODO: add test for dependencySetV1 after devstack support is added to the QueryAPI + +func TestRPCLocalUnsafe(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid chain ID", func(gt devtest.T) { + _, err := client.QueryAPI().LocalUnsafe(context.Background(), eth.ChainIDFromUInt64(100)) + require.Error(t, err, "expected LocalUnsafe to fail with raw chain ID") + }) + + for _, chainID := range []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()} { + t.Run(fmt.Sprintf("succeeds with valid chain ID %d", chainID), func(gt devtest.T) { + safe, err := client.QueryAPI().LocalUnsafe(context.Background(), chainID) + require.NoError(t, err) + assert.Greater(t, safe.Number, uint64(0)) + assert.Len(t, safe.Hash, 32) + }) + } +} + +func TestRPCCrossSafe(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid chain ID", func(gt devtest.T) { + _, err := client.QueryAPI().CrossSafe(context.Background(), eth.ChainIDFromUInt64(100)) + require.Error(t, err, "expected CrossSafe to fail with invalid chain") + }) + + for _, chainID := range []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()} { + t.Run(fmt.Sprintf("succeeds with valid chain ID %d", chainID), func(gt devtest.T) { + blockPair, err := client.QueryAPI().CrossSafe(context.Background(), chainID) + require.NoError(t, err) + assert.Greater(t, blockPair.Derived.Number, uint64(0)) + assert.Len(t, blockPair.Derived.Hash, 32) + + assert.Greater(t, blockPair.Source.Number, uint64(0)) + assert.Len(t, blockPair.Source.Hash, 32) + }) + } +} + +func TestRPCFinalized(gt *testing.T) { + gt.Skip() + t := devtest.ParallelT(gt) + + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid chain ID", func(gt devtest.T) { + _, err := client.QueryAPI().Finalized(context.Background(), eth.ChainIDFromUInt64(100)) + require.Error(t, err, "expected Finalized to fail with invalid chain") + }) + + for _, chainID := range []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()} { + t.Run(fmt.Sprintf("succeeds with valid chain ID %d", chainID), func(gt devtest.T) { + safe, err := client.QueryAPI().Finalized(context.Background(), chainID) + require.NoError(t, err) + assert.Greater(t, safe.Number, uint64(0)) + assert.Len(t, safe.Hash, 32) + }) + } +} + +func TestRPCFinalizedL1(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + t.Run("succeeds to get finalized L1 block", func(gt devtest.T) { + block, err := client.QueryAPI().FinalizedL1(context.Background()) + require.NoError(t, err) + assert.Greater(t, block.Number, uint64(0)) + assert.Less(t, block.Time, uint64(time.Now().Unix()+5)) + assert.Len(t, block.Hash, 32) + }) +} + +func TestRPCSuperRootAtTimestamp(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid timestamp", func(gt devtest.T) { + _, err := client.QueryAPI().SuperRootAtTimestamp(context.Background(), 0) + require.Error(t, err) + }) + + t.Run("succeeds with valid timestamp", func(gt devtest.T) { + timeNow := uint64(time.Now().Unix()) + root, err := client.QueryAPI().SuperRootAtTimestamp(context.Background(), hexutil.Uint64(timeNow-90)) + require.NoError(t, err) + assert.Len(t, root.SuperRoot, 32) + assert.Len(t, root.Chains, 2) + + for _, chain := range root.Chains { + assert.Len(t, chain.Canonical, 32) + assert.Contains(t, []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()}, chain.ChainID) + } + }) +} + +func TestRPCAllSafeDerivedAt(gt *testing.T) { + t := devtest.ParallelT(gt) + + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid L1 block hash", func(gt devtest.T) { + _, err := client.QueryAPI().AllSafeDerivedAt(context.Background(), eth.BlockID{ + Number: 100, + Hash: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), + }) + require.Error(t, err) + }) + + t.Run("succeeds with valid synced L1 block hash", func(gt devtest.T) { + sync, err := client.QueryAPI().SyncStatus(context.Background()) + require.NoError(t, err) + + allSafe, err := client.QueryAPI().AllSafeDerivedAt(context.Background(), eth.BlockID{ + Number: sync.MinSyncedL1.Number, + Hash: sync.MinSyncedL1.Hash, + }) + require.NoError(t, err) + + require.Equal(t, 2, len(allSafe)) + for key, value := range allSafe { + require.Contains(t, []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()}, key) + require.Len(t, value.Hash, 32) + } + }) +} + +func TestRPCCrossDerivedToSource(gt *testing.T) { + t := devtest.ParallelT(gt) + + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid chain ID", func(gt devtest.T) { + _, err := client.QueryAPI().CrossDerivedToSource(context.Background(), eth.ChainIDFromUInt64(100), eth.BlockID{Number: 25}) + require.Error(t, err, "expected CrossDerivedToSource to fail with invalid chain") + }) + + safe, err := client.QueryAPI().CrossSafe(context.Background(), sys.L2ChainA.ChainID()) + require.NoError(t, err) + + t.Run(fmt.Sprintf("succeeds with valid chain ID %d", sys.L2ChainA.ChainID()), func(gt devtest.T) { + source, err := client.QueryAPI().CrossDerivedToSource( + context.Background(), + sys.L2ChainA.ChainID(), + eth.BlockID{ + Number: safe.Derived.Number, + Hash: safe.Derived.Hash, + }, + ) + require.NoError(t, err) + assert.Greater(t, source.Number, uint64(0)) + assert.Len(t, source.Hash, 32) + assert.Equal(t, source.Number, safe.Source.Number) + assert.Equal(t, source.Hash, safe.Source.Hash) + }) + +} + +func TestRPCCheckAccessList(gt *testing.T) { + t := devtest.ParallelT(gt) + + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + ctx := sys.T.Ctx() + + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + + eventLoggerAddress := alice.DeployEventLogger() + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + _, initReceipt := alice.SendInitMessage( + interop.RandomInitTrigger(rng, eventLoggerAddress, rng.Intn(3), rng.Intn(10)), + ) + + logToAccess := func(chainID eth.ChainID, log *gethTypes.Log, timestamp uint64) types.Access { + msgPayload := make([]byte, 0) + for _, topic := range log.Topics { + msgPayload = append(msgPayload, topic.Bytes()...) + } + msgPayload = append(msgPayload, log.Data...) + + msgHash := crypto.Keccak256Hash(msgPayload) + args := types.ChecksumArgs{ + BlockNumber: log.BlockNumber, + Timestamp: timestamp, + LogIndex: uint32(log.Index), + ChainID: chainID, + LogHash: types.PayloadHashToLogHash(msgHash, log.Address), + } + return args.Access() + } + + blockRef := sys.L2ChainA.PublicRPC().BlockRefByNumber(initReceipt.BlockNumber.Uint64()) + + var accessEntries []types.Access + for _, evLog := range initReceipt.Logs { + accessEntries = append(accessEntries, logToAccess(alice.ChainID(), evLog, blockRef.Time)) + } + + cloneAccessEntries := func() []types.Access { + clone := make([]types.Access, len(accessEntries)) + copy(clone, accessEntries) + return clone + } + + sys.L2ChainB.WaitForBlock() + + t.Run("succeeds with valid access list", func(gt devtest.T) { + accessList := types.EncodeAccessList(cloneAccessEntries()) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.NoError(t, err, "CheckAccessList should succeed with valid access list and chain ID") + }) + + t.Run("fails with invalid chain ID", func(gt devtest.T) { + accessList := types.EncodeAccessList(cloneAccessEntries()) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: eth.ChainIDFromUInt64(99999999), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.Error(t, err, "CheckAccessList should fail with invalid chain ID") + }) + + t.Run("fails with invalid timestamp", func(gt devtest.T) { + accessList := types.EncodeAccessList(cloneAccessEntries()) + ed := types.ExecutingDescriptor{ + Timestamp: blockRef.Time - 1, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.Error(t, err, "CheckAccessList should fail with invalid timestamp") + }) + + t.Run("fails with conflicting data - log index mismatch", func(gt devtest.T) { + entries := cloneAccessEntries() + entries[0].LogIndex = 10 + accessList := types.EncodeAccessList(entries) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.Error(t, err, "CheckAccessList should fail with conflicting log index") + }) + + t.Run("fails with conflicting data - invalid block number", func(gt devtest.T) { + entries := cloneAccessEntries() + entries[0].BlockNumber = entries[0].BlockNumber - 1 + accessList := types.EncodeAccessList(entries) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.Error(t, err, "CheckAccessList should fail with invalid block number") + }) + + t.Run("fails with conflicting data - invalid checksum", func(gt devtest.T) { + entries := cloneAccessEntries() + // Corrupt the checksum + entries[0].Checksum[10] ^= 0xFF + accessList := types.EncodeAccessList(entries) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.LocalUnsafe, ed) + require.Error(t, err, "CheckAccessList should fail with invalid checksum") + }) + + t.Run("fails with safety violation", func(gt devtest.T) { + accessList := types.EncodeAccessList(cloneAccessEntries()) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{ + Timestamp: timestamp, + ChainID: bob.ChainID(), + } + + err := client.QueryAPI().CheckAccessList(ctx, accessList, types.Finalized, ed) + require.Error(t, err, "CheckAccessList should fail due to safety level violation") + }) +} diff --git a/kona/tests/supervisor/sync/init_test.go b/kona/tests/supervisor/sync/init_test.go new file mode 100644 index 0000000000000..3562c1cbf47fc --- /dev/null +++ b/kona/tests/supervisor/sync/init_test.go @@ -0,0 +1,21 @@ +package sync + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +const ( + // SLEEP_BACKEND_READY is the time to wait for the backend to be ready + SLEEP_BACKEND_READY = 90 * time.Second +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // sleep to ensure the backend is ready + time.Sleep(SLEEP_BACKEND_READY) + + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/kona/tests/supervisor/sync/resync_test.go b/kona/tests/supervisor/sync/resync_test.go new file mode 100644 index 0000000000000..442181c8d3bfe --- /dev/null +++ b/kona/tests/supervisor/sync/resync_test.go @@ -0,0 +1,86 @@ +package sync + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// TestL2CLResync checks that unsafe head advances after restarting L2CL. +// Resync is only possible when supervisor and L2CL reconnects. +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go +func TestL2CLResync(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + logger := sys.Log.With("Test", "TestL2CLResync") + + logger.Info("Check unsafe chains are advancing") + dsl.CheckAll(t, + sys.L2ELA.AdvancedFn(eth.Unsafe, 5), + sys.L2ELB.AdvancedFn(eth.Unsafe, 5), + ) + + logger.Info("Stop L2CL nodes") + sys.L2CLA.Stop() + sys.L2CLB.Stop() + + logger.Info("Make sure L2ELs does not advance") + dsl.CheckAll(t, + sys.L2ELA.NotAdvancedFn(eth.Unsafe, 30), + sys.L2ELB.NotAdvancedFn(eth.Unsafe, 30), + ) + + logger.Info("Restart L2CL nodes") + sys.L2CLA.Start() + sys.L2CLB.Start() + + // L2CL may advance a few blocks without supervisor connection, but eventually it will stop without the connection + // we must check that unsafe head is advancing due to reconnection + logger.Info("Boot up L2CL nodes") + + dsl.CheckAll(t, + sys.L2ELA.AdvancedFn(eth.Unsafe, 30), + sys.L2ELB.AdvancedFn(eth.Unsafe, 30), + ) + + // supervisor will attempt to reconnect with L2CLs at this point because L2CL ws endpoint is recovered + logger.Info("Check unsafe chains are advancing again") + dsl.CheckAll(t, + sys.L2ELA.AdvancedFn(eth.Unsafe, 10), + sys.L2ELB.AdvancedFn(eth.Unsafe, 10), + ) + + // supervisor successfully connected with managed L2CLs +} + +// TestSupervisorResync checks that heads advances after restarting the Supervisor. +func TestSupervisorResync(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSimpleInterop(t) + logger := sys.Log.With("Test", "TestSupervisorResync") + + logger.Info("Check unsafe chains are advancing") + + for _, level := range []types.SafetyLevel{types.LocalUnsafe, types.LocalSafe, types.CrossUnsafe, types.CrossSafe} { + sys.Supervisor.WaitForL2HeadToAdvance(sys.L2ChainA.ChainID(), 2, level, 20) + sys.Supervisor.WaitForL2HeadToAdvance(sys.L2ChainB.ChainID(), 2, level, 20) + } + + logger.Info("Stop Supervisor node") + sys.Supervisor.Stop() + + logger.Info("Restart Supervisor node") + sys.Supervisor.Start() + + logger.Info("Boot up Supervisor node") + + // Re check syncing is not blocked + for _, level := range []types.SafetyLevel{types.LocalUnsafe, types.LocalSafe, types.CrossUnsafe, types.CrossSafe} { + sys.Supervisor.WaitForL2HeadToAdvance(sys.L2ChainA.ChainID(), 2, level, 20) + sys.Supervisor.WaitForL2HeadToAdvance(sys.L2ChainB.ChainID(), 2, level, 20) + } +} diff --git a/kona/tests/supervisor/sync/sync_test.go b/kona/tests/supervisor/sync/sync_test.go new file mode 100644 index 0000000000000..ddec52b0a366e --- /dev/null +++ b/kona/tests/supervisor/sync/sync_test.go @@ -0,0 +1,251 @@ +package sync + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" +) + +const ( + // UnSafeHeadAdvanceRetries is the number of retries for unsafe head advancement + UnSafeHeadAdvanceRetries = 15 + + // CrossUnsafeHeadAdvanceRetries is the number of retries for cross-unsafe head advancement + CrossUnsafeHeadAdvanceRetries = 15 + + // LocalSafeHeadAdvanceRetries is the number of retries for safe head advancement + LocalSafeHeadAdvanceRetries = 15 + + // SafeHeadAdvanceRetries is the number of retries for safe head advancement + SafeHeadAdvanceRetries = 25 + + // FinalizedHeadAdvanceRetries is the number of retries for finalized head advancement + FinalizedHeadAdvanceRetries = 100 +) + +func TestLocalUnsafeHeadAdvancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2aChainID := out.L2CLA.ChainID() + l2bChainID := out.L2CLB.ChainID() + + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainA.ChainID(), 2, "unsafe", UnSafeHeadAdvanceRetries) + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainB.ChainID(), 2, "unsafe", UnSafeHeadAdvanceRetries) + + // Wait and check if the local unsafe head has advanced on L2A + err := wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.UnsafeL2.Number > supervisorStatus.Chains[l2aChainID].LocalUnsafe.Number, nil + }) + t.Require().NoError(err) + + // Wait and check if the local unsafe head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.UnsafeL2.Number > supervisorStatus.Chains[l2bChainID].LocalUnsafe.Number, nil + }) + t.Require().NoError(err) + + // Wait and cross check the supervisor unsafe heads to advance on both chains + err = wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.Chains[l2aChainID].LocalUnsafe.Number > supervisorStatus.Chains[l2aChainID].LocalUnsafe.Number && + latestSupervisorStatus.Chains[l2bChainID].LocalUnsafe.Number >= supervisorStatus.Chains[l2bChainID].LocalUnsafe.Number, nil + }) + t.Require().NoError(err) +} + +func TestCrossUnsafeHeadAdvancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2aChainID := out.L2CLA.ChainID() + l2bChainID := out.L2CLB.ChainID() + + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainA.ChainID(), 2, "cross-unsafe", CrossUnsafeHeadAdvanceRetries) + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainB.ChainID(), 2, "cross-unsafe", CrossUnsafeHeadAdvanceRetries) + + // Wait and cross check the supervisor cross unsafe heads to advance on both chains + err := wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.Chains[l2aChainID].LocalUnsafe.Number > supervisorStatus.Chains[l2aChainID].LocalUnsafe.Number && + latestSupervisorStatus.Chains[l2bChainID].LocalUnsafe.Number >= supervisorStatus.Chains[l2bChainID].LocalUnsafe.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the cross unsafe head has advanced on L2A + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.CrossUnsafeL2.Number > supervisorStatus.Chains[l2aChainID].CrossUnsafe.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the cross unsafe head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.CrossUnsafeL2.Number > supervisorStatus.Chains[l2bChainID].CrossUnsafe.Number, nil + }) + + t.Require().NoError(err) +} + +func TestLocalSafeHeadAdvancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2aChainID := out.L2CLA.ChainID() + l2bChainID := out.L2CLB.ChainID() + + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainA.ChainID(), 1, "local-safe", LocalSafeHeadAdvanceRetries) + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainB.ChainID(), 1, "local-safe", LocalSafeHeadAdvanceRetries) + + // Wait and check if the local safe head has advanced on L2A + err := wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.LocalSafeL2.Number > supervisorStatus.Chains[l2aChainID].LocalSafe.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the local safe head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.LocalSafeL2.Number > supervisorStatus.Chains[l2bChainID].LocalSafe.Number, nil + }) + t.Require().NoError(err) + // Wait and cross check the supervisor local safe heads to advance on both chains + err = wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.Chains[l2aChainID].LocalSafe.Number > supervisorStatus.Chains[l2aChainID].LocalSafe.Number && + latestSupervisorStatus.Chains[l2bChainID].LocalSafe.Number >= supervisorStatus.Chains[l2bChainID].LocalSafe.Number, nil + }) + t.Require().NoError(err) +} + +func TestCrossSafeHeadAdvancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2aChainID := out.L2CLA.ChainID() + l2bChainID := out.L2CLB.ChainID() + + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainA.ChainID(), 1, "safe", SafeHeadAdvanceRetries) + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainB.ChainID(), 1, "safe", SafeHeadAdvanceRetries) + + // Wait and cross check the supervisor cross safe heads to advance on both chains + err := wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.Chains[l2aChainID].CrossSafe.Number > supervisorStatus.Chains[l2aChainID].CrossSafe.Number && + latestSupervisorStatus.Chains[l2bChainID].CrossSafe.Number >= supervisorStatus.Chains[l2bChainID].CrossSafe.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the cross safe head has advanced on L2A + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.SafeL2.Number > supervisorStatus.Chains[l2aChainID].CrossSafe.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the cross safe head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.SafeL2.Number > supervisorStatus.Chains[l2bChainID].CrossSafe.Number, nil + }) + + t.Require().NoError(err) +} + +func TestMinSyncedL1Advancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.AwaitMinL1(supervisorStatus.MinSyncedL1.Number + 1) + + // Wait and check if the currentL1 head has advanced on L2A + err := wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.CurrentL1.Number > supervisorStatus.MinSyncedL1.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the currentL1 head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.CurrentL1.Number > supervisorStatus.MinSyncedL1.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the min synced L1 has advanced + err = wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.MinSyncedL1.Number > supervisorStatus.MinSyncedL1.Number, nil + }) + t.Require().NoError(err) +} + +func TestFinalizedHeadAdvancing(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2aChainID := out.L2CLA.ChainID() + l2bChainID := out.L2CLB.ChainID() + + supervisorStatus := out.Supervisor.FetchSyncStatus() + + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainA.ChainID(), 1, "finalized", FinalizedHeadAdvanceRetries) + out.Supervisor.WaitForL2HeadToAdvance(out.L2ChainB.ChainID(), 1, "finalized", FinalizedHeadAdvanceRetries) + + // Wait and cross check the supervisor finalized heads to advance on both chains + err := wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + latestSupervisorStatus := out.Supervisor.FetchSyncStatus() + return latestSupervisorStatus.Chains[l2aChainID].Finalized.Number > supervisorStatus.Chains[l2aChainID].Finalized.Number && + latestSupervisorStatus.Chains[l2bChainID].Finalized.Number >= supervisorStatus.Chains[l2bChainID].Finalized.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the finalized head has advanced on L2A + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLA.SyncStatus() + return status.FinalizedL1.Time > supervisorStatus.FinalizedTimestamp && + status.FinalizedL2.Number > supervisorStatus.Chains[l2aChainID].Finalized.Number, nil + }) + t.Require().NoError(err) + // Wait and check if the finalized head has advanced on L2B + err = wait.For(t.Ctx(), 2*time.Second, func() (bool, error) { + status := out.L2CLB.SyncStatus() + return status.FinalizedL1.Time > supervisorStatus.FinalizedTimestamp && + status.FinalizedL2.Number > supervisorStatus.Chains[l2bChainID].Finalized.Number, nil + }) + t.Require().NoError(err) +} + +func TestDerivationPipeline(gt *testing.T) { + t := devtest.SerialT(gt) + + out := presets.NewSimpleInterop(t) + l2BlockHead := out.Supervisor.L2HeadBlockID(out.L2ChainA.ChainID(), "local-safe") + + // Get current L1 at which L2 is at and wait for new L1 to be synced in supervisor. + current_l1_at_l2 := out.L2CLA.SyncStatus().CurrentL1 + out.Supervisor.AwaitMinL1(current_l1_at_l2.Number + 1) + new_l1 := out.Supervisor.FetchSyncStatus().MinSyncedL1 + + t.Require().NotEqual(current_l1_at_l2.Hash, new_l1.Hash) + t.Require().Greater(new_l1.Number, current_l1_at_l2.Number) + + // Wait for the L2 chain to sync to the new L1 block. + err := wait.For(t.Ctx(), 5*time.Second, func() (bool, error) { + new_l1_at_l2 := out.L2CLA.SyncStatus().CurrentL1 + return new_l1_at_l2.Number >= new_l1.Number, nil + }) + t.Require().NoError(err) + + new_l2BlockHead := out.Supervisor.L2HeadBlockID(out.L2ChainA.ChainID(), "local-safe") + t.Require().Greater(new_l2BlockHead.Number, l2BlockHead.Number) +} diff --git a/kona/tests/supervisor/utils/builder.go b/kona/tests/supervisor/utils/builder.go new file mode 100644 index 0000000000000..382d059c5db27 --- /dev/null +++ b/kona/tests/supervisor/utils/builder.go @@ -0,0 +1,353 @@ +package utils + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/big" + "math/rand" + "net/http" + "strings" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + opeth "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" +) + +type rpcRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` + ID int `json:"id"` +} + +type rpcResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID int `json:"id"` + Result json.RawMessage `json:"result"` + Error *rpcError `json:"error,omitempty"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type TestBlockBuilderConfig struct { + safeBlockDistance uint64 + finalizedBlockDistance uint64 + + GethRPC string + + EngineRPC string + JWTSecret string +} + +type TestBlockBuilder struct { + t devtest.CommonT + + withdrawalsIndex uint64 + + cfg TestBlockBuilderConfig + ethClient *ethclient.Client +} + +func NewTestBlockBuilder(t devtest.CommonT, cfg TestBlockBuilderConfig) *TestBlockBuilder { + ethClient, err := ethclient.Dial(cfg.GethRPC) + if err != nil { + t.Errorf("failed to connect to Geth RPC: %v", err) + return nil + } + + return &TestBlockBuilder{t, 1001, cfg, ethClient} +} + +func createJWT(secret []byte) (string, error) { + // try to decode hex string (support "0x..." or plain hex), fall back to raw bytes + secretStr := string(secret) + secretStr = strings.TrimPrefix(secretStr, "0x") + key, err := hex.DecodeString(secretStr) + if err != nil { + key = secret + } + + // typos:disable + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) + // typos:enable + payload := fmt.Sprintf(`{"iat":%d}`, time.Now().Unix()) + payloadEnc := base64.RawURLEncoding.EncodeToString([]byte(payload)) + toSign := header + "." + payloadEnc + h := hmac.New(sha256.New, key) + h.Write([]byte(toSign)) + sig := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + return toSign + "." + sig, nil +} + +func (s *TestBlockBuilder) rpcCallWithJWT(url, method string, params interface{}) (*rpcResponse, error) { + reqBody, _ := json.Marshal(rpcRequest{Jsonrpc: "2.0", Method: method, Params: params, ID: 1}) + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + + // Create JWT token + jwtToken, err := createJWT([]byte(s.cfg.JWTSecret)) + if err != nil { + return nil, fmt.Errorf("failed to create JWT: %w", err) + } + req.Header.Set("Authorization", "Bearer "+jwtToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + bodyBytes, _ := io.ReadAll(resp.Body) + + // Non-200 -> surface the body for debugging + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("non-200 response %d from %s: %s", resp.StatusCode, url, string(bodyBytes)) + } + + var rpcResp rpcResponse + if err := json.Unmarshal(bodyBytes, &rpcResp); err != nil { + // include raw body to help debugging the bad payload + return nil, fmt.Errorf("failed to parse RPC response: %w; body: %s", err, string(bodyBytes)) + } + if rpcResp.Error != nil { + return nil, fmt.Errorf("RPC error: %s", rpcResp.Error.Message) + } + return &rpcResp, nil +} + +func (s *TestBlockBuilder) rpcCall(url, method string, params interface{}) (*rpcResponse, error) { + return s.rpcCallWithJWT(url, method, params) +} + +func (s *TestBlockBuilder) rewindTo(ctx context.Context, blockHash common.Hash) (*types.Block, error) { + s.t.Logf("Rewinding to block %s", blockHash.Hex()) + + block, err := s.ethClient.BlockByHash(ctx, blockHash) + if err != nil { + s.t.Errorf("failed to fetch block by hash %s: %v", blockHash.Hex(), err) + return nil, fmt.Errorf("failed to fetch block by hash: %w", err) + } + + // Attempt rewind using debug_setHead + _, err = s.rpcCall(s.cfg.GethRPC, "debug_setHead", []interface{}{fmt.Sprintf("0x%x", block.NumberU64())}) + if err != nil { + s.t.Errorf("failed to rewind to block %s: %v", blockHash.Hex(), err) + return nil, fmt.Errorf("rewind failed: %w", err) + } + + // Confirm head matches requested parent + head, err := s.ethClient.BlockByNumber(ctx, big.NewInt(int64(rpc.LatestBlockNumber))) + if err != nil { + s.t.Errorf("failed to fetch latest block: %w", err) + return nil, fmt.Errorf("failed to fetch latest block: %w", err) + } + + if head.Hash() != blockHash { + s.t.Errorf("head mismatch after rewind: expected %s, got %s", blockHash.Hex(), head.Hash().Hex()) + return nil, fmt.Errorf("head mismatch after rewind") + } + + s.t.Logf("Successfully rewound to block %s", blockHash.Hex()) + return block, nil +} + +func (s *TestBlockBuilder) BuildBlock(ctx context.Context, parentHash *common.Hash) { + var head *types.Block + var err error + if parentHash != nil { + head, err = s.rewindTo(ctx, *parentHash) + if err != nil { + s.t.Errorf("failed to rewind to parent block: %v", err) + return + } + } else { + head, err = s.ethClient.BlockByNumber(ctx, big.NewInt(int64(rpc.LatestBlockNumber))) + if err != nil { + s.t.Errorf("failed to fetch latest block: %v", err) + return + } + } + + finalizedBlock, _ := s.ethClient.BlockByNumber(ctx, big.NewInt(rpc.FinalizedBlockNumber.Int64())) + if finalizedBlock == nil { + // set sb to genesis if safe block is not set + finalizedBlock, err = s.ethClient.BlockByNumber(ctx, big.NewInt(0)) + if err != nil { + s.t.Errorf("failed to fetch genesis block: %v", err) + return + } + } + + // progress finalised block + if head.NumberU64() > uint64(s.cfg.finalizedBlockDistance) { + finalizedBlock, err = s.ethClient.BlockByNumber(ctx, big.NewInt(int64(head.NumberU64()-s.cfg.finalizedBlockDistance))) + if err != nil { + s.t.Errorf("failed to fetch safe block: %v", err) + return + } + } + + safeBlock, _ := s.ethClient.BlockByNumber(ctx, big.NewInt(rpc.SafeBlockNumber.Int64())) + if safeBlock == nil { + safeBlock = finalizedBlock + } + + // progress safe block + if head.NumberU64() > uint64(s.cfg.safeBlockDistance) { + safeBlock, err = s.ethClient.BlockByNumber(ctx, big.NewInt(int64(head.NumberU64()-s.cfg.safeBlockDistance))) + if err != nil { + s.t.Errorf("failed to fetch safe block: %v", err) + return + } + } + + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: head.Hash(), + SafeBlockHash: safeBlock.Hash(), + FinalizedBlockHash: finalizedBlock.Hash(), + } + + newBlockTimestamp := head.Time() + 6 + nonce := time.Now().UnixNano() + var nonceBytes [8]byte + binary.LittleEndian.PutUint64(nonceBytes[:], uint64(nonce)) + randomHash := crypto.Keccak256Hash(nonceBytes[:]) + payloadAttrs := engine.PayloadAttributes{ + Timestamp: uint64(newBlockTimestamp), + Random: randomHash, + SuggestedFeeRecipient: head.Coinbase(), + Withdrawals: randomWithdrawals(s.withdrawalsIndex), + BeaconRoot: fakeBeaconBlockRoot(uint64(head.Time())), + } + + // Start payload build + fcResp, err := s.rpcCallWithJWT(s.cfg.EngineRPC, "engine_forkchoiceUpdatedV3", + []interface{}{fcState, payloadAttrs}) + if err != nil { + s.t.Errorf("forkchoiceUpdated failed: %v", err) + return + } + + var fcResult engine.ForkChoiceResponse + err = json.Unmarshal(fcResp.Result, &fcResult) + if err != nil { + s.t.Errorf("failed to unmarshal forkchoiceUpdated response: %v", err) + return + } + if fcResult.PayloadStatus.Status != "VALID" && fcResult.PayloadStatus.Status != "SYNCING" { + s.t.Errorf("forkchoiceUpdated returned invalid status: %s", fcResult.PayloadStatus.Status) + return + } + + if fcResult.PayloadID == nil { + s.t.Errorf("forkchoiceUpdated did not return a payload ID") + return + } + + time.Sleep(150 * time.Millisecond) + + // Get payload + plResp, err := s.rpcCallWithJWT(s.cfg.EngineRPC, "engine_getPayloadV3", []interface{}{fcResult.PayloadID}) + if err != nil { + s.t.Errorf("getPayload failed: %v", err) + return + } + + var envelope engine.ExecutionPayloadEnvelope + err = json.Unmarshal(plResp.Result, &envelope) + if err != nil { + s.t.Errorf("failed to unmarshal getPayload response: %v", err) + return + } + if envelope.ExecutionPayload == nil { + s.t.Errorf("getPayload returned empty execution payload") + return + } + + blobHashes := make([]common.Hash, 0) + if envelope.BlobsBundle != nil { + for _, commitment := range envelope.BlobsBundle.Commitments { + if len(commitment) != 48 { + break + } + blobHashes = append(blobHashes, opeth.KZGToVersionedHash(*(*[48]byte)(commitment))) + } + if len(blobHashes) != len(envelope.BlobsBundle.Commitments) { + s.t.Errorf("blob hashes length mismatch: expected %d, got %d", len(envelope.BlobsBundle.Commitments), len(blobHashes)) + return + } + } + + // Insert + newPayloadResp, err := s.rpcCallWithJWT(s.cfg.EngineRPC, "engine_newPayloadV3", []interface{}{envelope.ExecutionPayload, blobHashes, payloadAttrs.BeaconRoot}) + if err != nil { + s.t.Errorf("newPayload failed: %v", err) + return + } + + var npRes engine.PayloadStatusV1 + err = json.Unmarshal(newPayloadResp.Result, &npRes) + if err != nil { + s.t.Errorf("failed to unmarshal newPayload response: %v", err) + return + } + if npRes.Status != "VALID" && npRes.Status != "ACCEPTED" { + s.t.Errorf("newPayload returned invalid status: %s", npRes.Status) + return + } + + // Update forkchoice + updateFc := engine.ForkchoiceStateV1{ + HeadBlockHash: envelope.ExecutionPayload.BlockHash, + SafeBlockHash: safeBlock.Hash(), + FinalizedBlockHash: finalizedBlock.Hash(), + } + _, err = s.rpcCallWithJWT(s.cfg.EngineRPC, "engine_forkchoiceUpdatedV3", []interface{}{updateFc, nil}) + if err != nil { + s.t.Errorf("forkchoiceUpdated failed after newPayload: %v", err) + return + } + + s.withdrawalsIndex += uint64(len(envelope.ExecutionPayload.Withdrawals)) + + s.t.Logf("Successfully built block %s:%d at timestamp %d", envelope.ExecutionPayload.BlockHash.Hex(), envelope.ExecutionPayload.Number, newBlockTimestamp) +} + +func fakeBeaconBlockRoot(time uint64) *common.Hash { + var dat [8]byte + binary.LittleEndian.PutUint64(dat[:], time) + hash := crypto.Keccak256Hash(dat[:]) + return &hash +} + +func randomWithdrawals(startIndex uint64) []*types.Withdrawal { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + withdrawals := make([]*types.Withdrawal, r.Intn(4)) + for i := 0; i < len(withdrawals); i++ { + withdrawals[i] = &types.Withdrawal{ + Index: startIndex + uint64(i), + Validator: r.Uint64() % 100_000_000, // 100 million fake validators + Address: testutils.RandomAddress(r), + Amount: uint64(r.Intn(50_000_000_000) + 1), + } + } + return withdrawals +} diff --git a/kona/tests/supervisor/utils/pos.go b/kona/tests/supervisor/utils/pos.go new file mode 100644 index 0000000000000..cbcfa1177e55c --- /dev/null +++ b/kona/tests/supervisor/utils/pos.go @@ -0,0 +1,82 @@ +package utils + +import ( + "context" + "math/big" + "sync" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" +) + +type TestPOS struct { + t devtest.CommonT + + ethClient *ethclient.Client + blockBuilder *TestBlockBuilder + + // background management + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func NewTestPOS(t devtest.CommonT, rpcURL string, blockBuilder *TestBlockBuilder) *TestPOS { + ethClient, err := ethclient.Dial(rpcURL) + if err != nil { + t.Errorf("failed to connect to RPC: %v", err) + return nil + } + + return &TestPOS{t: t, ethClient: ethClient, blockBuilder: blockBuilder} +} + +// Starts a background process to build blocks +func (p *TestPOS) Start() error { + p.t.Log("Starting sequential block builder") + // already started + if p.ctx != nil { + return nil + } + + p.ctx, p.cancel = context.WithCancel(context.Background()) + p.wg.Add(1) + + go func() { + defer p.wg.Done() + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + for { + select { + case <-p.ctx.Done(): + return + case <-ticker.C: + _, err := p.ethClient.BlockByNumber(p.ctx, big.NewInt(rpc.LatestBlockNumber.Int64())) + if err != nil { + p.t.Errorf("failed to fetch latest block: %v", err) + } + + // Build a new block + p.blockBuilder.BuildBlock(p.ctx, nil) + } + } + }() + + return nil +} + +// Stops the background process +func (p *TestPOS) Stop() { + // cancel the context to signal the goroutine to exit + if p.cancel != nil { + p.cancel() + p.cancel = nil + } + // wait for goroutine to finish + p.wg.Wait() + // clear the context to mark stopped + p.ctx = nil +} diff --git a/kona/tests/supervisor/utils/reorg.go b/kona/tests/supervisor/utils/reorg.go new file mode 100644 index 0000000000000..b53da9bd10a32 --- /dev/null +++ b/kona/tests/supervisor/utils/reorg.go @@ -0,0 +1,116 @@ +package utils + +import ( + "fmt" + "os" + + "github.com/ethereum-optimism/optimism/devnet-sdk/shell/env" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" +) + +type TestReorgManager struct { + t devtest.CommonT + env *env.DevnetEnv + blockBuilder *TestBlockBuilder + pos *TestPOS +} + +func NewTestReorgManager(t devtest.CommonT) *TestReorgManager { + url := os.Getenv(env.EnvURLVar) + if url == "" { + t.Errorf("environment variable %s is not set", env.EnvURLVar) + return nil + } + + env, err := env.LoadDevnetFromURL(url) + if err != nil { + t.Errorf("failed to load devnet environment from URL %s: %v", url, err) + return nil + } + + var engineURL, rpcURL string + for _, node := range env.Env.L1.Nodes { + el, ok := node.Services["el"] + if !ok { + continue + } + + engine, ok := el.Endpoints["engine-rpc"] + if !ok { + continue + } + + rpc, ok := el.Endpoints["rpc"] + if !ok { + continue + } + + engineURL = fmt.Sprintf("http://%s:%d", engine.Host, engine.Port) + rpcURL = fmt.Sprintf("http://%s:%d", rpc.Host, rpc.Port) + break + } + + if engineURL == "" || rpcURL == "" { + t.Errorf("could not find engine or RPC endpoints in the devnet environment") + return nil + } + + blockBuilder := NewTestBlockBuilder(t, TestBlockBuilderConfig{ + GethRPC: rpcURL, + EngineRPC: engineURL, + JWTSecret: env.Env.L1.JWT, + safeBlockDistance: 10, + finalizedBlockDistance: 20, + }) + + pos := NewTestPOS(t, rpcURL, blockBuilder) + return &TestReorgManager{t, env, blockBuilder, pos} +} + +func (m *TestReorgManager) StopL1CL() { + m.t.Log("Stopping L1 CL services") + + panic("not implemented. TODO(op-rs/kona#3174): implement this `https://github.com/op-rs/kona/issues/3174`") + + // kurtosisCtx, err := kurtosis_context.NewKurtosisContextFromLocalEngine() + // if err != nil { + // m.t.Errorf("failed to create kurtosis context: %v", err) + // return + // } + + // // Use a bounded context to avoid hanging tests if Kurtosis call stalls. + // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // defer cancel() + // enclaveCtx, err := kurtosisCtx.GetEnclaveContext(ctx, m.env.Env.Name) + // if err != nil { + // m.t.Errorf("failed to get enclave context: %v", err) + // return + // } + + // for _, node := range m.env.Env.L1.Nodes { + // cl, ok := node.Services["cl"] + // if !ok { + // continue + // } + + // svcCtx, err := enclaveCtx.GetServiceContext(cl.Name) + // if err != nil { + // m.t.Errorf("failed to get service context for %s: %v", cl.Name, err) + // return + // } + + // _, _, err = svcCtx.ExecCommand([]string{"sh", "-c", "kill 1"}) + // if err != nil { + // m.t.Errorf("failed to stop service %s: %v", cl.Name, err) + // return + // } + // } +} + +func (m *TestReorgManager) GetBlockBuilder() *TestBlockBuilder { + return m.blockBuilder +} + +func (m *TestReorgManager) GetPOS() *TestPOS { + return m.pos +} diff --git a/kona/typos.toml b/kona/typos.toml new file mode 100644 index 0000000000000..2705ddfec2cf0 --- /dev/null +++ b/kona/typos.toml @@ -0,0 +1,81 @@ +[files] +extend-exclude = [ + "target", + "Cargo.lock", +] + +[default] +extend-ignore-re = [ + # Base64 encoded strings (common in tests and configs) + "[A-Za-z0-9+/]{20,}={0,2}", +] + +[default.extend-words] +# Valid Rust/Cargo terms +crate = "crate" +crates = "crates" + +# Blockchain/Ethereum specific terms +alloy = "alloy" +anvil = "anvil" +asm = "asm" +asterisc = "asterisc" +batcher = "batcher" +bedrock = "bedrock" +bootnode = "bootnode" +cannon = "cannon" +chainid = "chainid" +codegen = "codegen" +derivation = "derivation" +enr = "enr" +ethereum = "ethereum" +fpvm = "fpvm" +hel = "hel" # Part of hostname bootnode-hetzner-hel +interop = "interop" +kona = "kona" +libmdbx = "libmdbx" +merkle = "merkle" +mips = "mips" +mpsc = "mpsc" +optimism = "optimism" +preimage = "preimage" +revm = "revm" +risc = "risc" +rollup = "rollup" +rpc = "rpc" +sequencer = "sequencer" +ser = "ser" # Serialization abbreviation +serde = "serde" +supervisor = "supervisor" +superchain = "superchain" +trie = "trie" +txs = "txs" # Transactions abbreviation +udeps = "udeps" # Unused dependencies tool +usize = "usize" +workspaces = "workspaces" + +# Technical abbreviations and acronyms +api = "api" +cli = "cli" +cfg = "cfg" +const = "const" +env = "env" +impl = "impl" +io = "io" +lru = "lru" +mpt = "mpt" # Merkle Patricia Trie +msg = "msg" +mut = "mut" +nums = "nums" +num = "num" +ok = "ok" +std = "std" +structs = "structs" +ty = "ty" # Type abbreviation +vec = "vec" +typ = "typ" + +# Additional allowed words from typos scan +flate = "flate" # zlib-flate tool name +ratatui = "ratatui" # TUI crate name +superseed = "superseed" # Superseed network name (proper noun) \ No newline at end of file diff --git a/kona-proofs/version.json b/kona/version.json similarity index 99% rename from kona-proofs/version.json rename to kona/version.json index f99d3f5867b02..8d8e7807a200a 100644 --- a/kona-proofs/version.json +++ b/kona/version.json @@ -2,4 +2,4 @@ "version": "1.2.7", "prestateHash": "0x0323914d3050e80c3d09da528be54794fde60cd26849cd3410dde0da7cd7d4fa", "interopPrestateHash": "0x03f03018773fae0603f7c110ef1defa8d19b601b32ee530f9951987baec435b0" -} +} \ No newline at end of file diff --git a/mise.toml b/mise.toml index 6735d595e28fd..fa8bdf6f0d4bb 100644 --- a/mise.toml +++ b/mise.toml @@ -5,7 +5,7 @@ go = "1.24.10" golangci-lint = "1.64.8" gotestsum = "1.12.3" mockery = "2.53.3" -rust = "1.91.0" +rust = "1.92.0" python = "3.12.0" uv = "0.5.5" jq = "1.7.1" diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index 593d177143efa..ce815be28e7db 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -65,7 +65,7 @@ acceptance-test devnet="" gate="base": echo "Building Rust binaries (kona-node, kona-supervisor, op-rbuilder, rollup-boost)..." cd {{REPO_ROOT}} - just build-rust-debug + just build-rust-release fi cd {{REPO_ROOT}}/op-acceptance-tests diff --git a/op-devstack/shared/challenger/challenger.go b/op-devstack/shared/challenger/challenger.go index 68ea35f2376d6..8c50b23f7801c 100644 --- a/op-devstack/shared/challenger/challenger.go +++ b/op-devstack/shared/challenger/challenger.go @@ -79,12 +79,12 @@ func applyCannonKonaConfig(c *config.Config, rollupCfgs []*rollup.Config, l1Gene if err := applyVmConfig(root, &c.CannonKona, c.Datadir, rollupCfgs, l1Genesis, l2Geneses); err != nil { return err } - c.CannonKona.Server = root + "kona-proofs/bin/kona-host" + c.CannonKona.Server = root + "kona/target/release/kona-host" absRoot, err := filepath.Abs(root) if err != nil { return fmt.Errorf("failed to get absolute path to prestate dir: %w", err) } - c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona-proofs/prestates") + c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona/prestates") if err != nil { return fmt.Errorf("failed to create kona prestates url: %w", err) } diff --git a/op-devstack/sysgo/l2_el_p2p_util.go b/op-devstack/sysgo/l2_el_p2p_util.go index e69662d98ac77..e78b2aaa8ce76 100644 --- a/op-devstack/sysgo/l2_el_p2p_util.go +++ b/op-devstack/sysgo/l2_el_p2p_util.go @@ -2,6 +2,7 @@ package sysgo import ( "context" + "os" "slices" "strings" "time" @@ -64,6 +65,13 @@ func ConnectP2P(ctx context.Context, require *testreq.Assertions, initiator RpcC require.True(peerAddedTrusted, "should have added trusted peer successfully") } + // Skip P2P connection verification if SKIP_P2P_CONNECTION_CHECK is set + // FIXME(#18570): it seems we have some issues getting op-reth to connect to op-geth. This is a temporary workaround to ensure we can still run the + // devstack tests. + if os.Getenv("SKIP_P2P_CONNECTION_CHECK") != "" { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() err = wait.For(ctx, time.Second, func() (bool, error) { diff --git a/op-devstack/sysgo/superroot.go b/op-devstack/sysgo/superroot.go index 7e5d48e6b1909..3b358ec770420 100644 --- a/op-devstack/sysgo/superroot.go +++ b/op-devstack/sysgo/superroot.go @@ -241,7 +241,7 @@ func getCannonKonaAbsolutePrestate(t devtest.CommonT) common.Hash { } func loadKonaVersions(t devtest.CommonT) konaVersions { - konaVersionPath := "kona-proofs/version.json" + konaVersionPath := "kona/version.json" root, err := findMonorepoRoot(konaVersionPath) t.Require().NoError(err) p := path.Join(root, konaVersionPath) diff --git a/op-rbuilder b/op-rbuilder index 272d462d980a4..33c825a9e51d6 160000 --- a/op-rbuilder +++ b/op-rbuilder @@ -1 +1 @@ -Subproject commit 272d462d980a43e7caf568df0fbbc0c2e0066207 +Subproject commit 33c825a9e51d68b18eee6d6d32f173c77974ff0b diff --git a/reth b/reth new file mode 160000 index 0000000000000..7388d6636de49 --- /dev/null +++ b/reth @@ -0,0 +1 @@ +Subproject commit 7388d6636de498b228ffaa46fc3e59812a20827b diff --git a/rollup-boost b/rollup-boost index 196237bab2a02..d2b228b8b356a 160000 --- a/rollup-boost +++ b/rollup-boost @@ -1 +1 @@ -Subproject commit 196237bab2a02298de994b439e0455abb1ac512f +Subproject commit d2b228b8b356a8f92e09687ee6c0c318c0457ab9